001/*
002 * (C) Copyright 2006-2008 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     bstefanescu
016 *
017 * $Id$
018 */
019
020package org.nuxeo.runtime.jetty;
021
022import java.io.File;
023import java.io.IOException;
024import java.net.MalformedURLException;
025import java.net.URL;
026
027import org.apache.commons.logging.Log;
028import org.apache.commons.logging.LogFactory;
029import org.mortbay.jetty.NCSARequestLog;
030import org.mortbay.jetty.Server;
031import org.mortbay.jetty.handler.ContextHandlerCollection;
032import org.mortbay.jetty.handler.HandlerCollection;
033import org.mortbay.jetty.handler.RequestLogHandler;
034import org.mortbay.jetty.webapp.Configuration;
035import org.mortbay.jetty.webapp.WebAppContext;
036import org.mortbay.jetty.webapp.WebInfConfiguration;
037import org.mortbay.jetty.webapp.WebXmlConfiguration;
038import org.mortbay.xml.XmlConfiguration;
039import org.nuxeo.common.Environment;
040import org.nuxeo.common.server.WebApplication;
041import org.nuxeo.common.utils.ExceptionUtils;
042import org.nuxeo.runtime.api.Framework;
043import org.nuxeo.runtime.model.ComponentContext;
044import org.nuxeo.runtime.model.ComponentInstance;
045import org.nuxeo.runtime.model.ComponentName;
046import org.nuxeo.runtime.model.DefaultComponent;
047import org.xml.sax.SAXException;
048
049/**
050 * This component registers and configures an embedded Jetty server.
051 * <p>
052 * Contexts are registered like this:
053 * <p>
054 * First, if there is a {@code jetty.xml} config file, the contexts defined there are registered first; if there is no
055 * {@code jetty.xml}, a log context will be create programatically and registered first.
056 * <p>
057 * Second an empty collection context is registered. Here will be registered all regular war contexts.
058 * <p>
059 * Third, the root collection is registered. This way all requests not handled by regular wars are directed to the root
060 * war, which usually is the webengine war in a nxserver application.
061 *
062 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
063 */
064public class JettyComponent extends DefaultComponent {
065
066    public static final ComponentName NAME = new ComponentName("org.nuxeo.runtime.server");
067
068    public static final String XP_WEB_APP = "webapp";
069
070    public static final String XP_SERVLET = "servlet";
071
072    public static final String XP_FILTER = "filter";
073
074    public static final String XP_LISTENERS = "listeners";
075
076    public static final String P_SCAN_WEBDIR = "org.nuxeo.runtime.jetty.scanWebDir";
077
078    protected Server server;
079
080    protected ContextManager ctxMgr;
081
082    // here we are putting all regular war contexts
083    // the root context will be appended after this context collection to be
084    // sure the regular contexts are checked first
085    // This is because the root context is bound to / so if it is checked first
086    // it will consume
087    // all requests even if there is a context that is the target of the request
088    protected ContextHandlerCollection warContexts;
089
090    protected File config;
091
092    protected File log;
093
094    private static final Log logger = LogFactory.getLog(JettyComponent.class);
095
096    public Server getServer() {
097        return server;
098    }
099
100    @Override
101    public void activate(ComponentContext context) {
102
103        // apply bundled configuration
104        URL cfg = null;
105
106        String cfgName = Framework.getProperty("org.nuxeo.jetty.config");
107        if (cfgName != null) {
108            if (cfgName.contains(":/")) {
109                try {
110                    cfg = new URL(cfgName);
111                } catch (MalformedURLException e) {
112                    throw new RuntimeException(e);
113                }
114            } else { // assume a file
115                File file = new File(cfgName);
116                if (file.isFile()) {
117                    try {
118                        cfg = file.toURI().toURL();
119                    } catch (MalformedURLException e) {
120                        throw new RuntimeException(e);
121                    }
122                }
123            }
124        } else {
125            File file = new File(Environment.getDefault().getConfig(), "jetty.xml");
126            if (file.isFile()) {
127                try {
128                    cfg = file.toURI().toURL();
129                } catch (MalformedURLException e) {
130                    throw new RuntimeException(e);
131                }
132            }
133        }
134        boolean hasConfigFile = false;
135        if (cfg != null) {
136            hasConfigFile = true;
137            XmlConfiguration configuration;
138            try {
139                configuration = new XmlConfiguration(cfg);
140            } catch (SAXException | IOException e) {
141                throw new RuntimeException(e);
142            }
143            try {
144                server = (Server) configuration.configure();
145            } catch (Exception e) { // stupid Jetty API throws Exception
146                throw ExceptionUtils.runtimeException(e);
147            }
148        } else {
149            int p = 8080;
150            String port = Environment.getDefault().getProperty("http_port");
151            if (port != null) {
152                try {
153                    p = Integer.parseInt(port);
154                } catch (NumberFormatException e) {
155                    // do noting
156                }
157            }
158            server = new Server(p);
159
160        }
161
162        // if a jetty.xml is present we don't configure logging - this should be
163        // done in that file.
164        if (!hasConfigFile) {
165            RequestLogHandler requestLogHandler = new RequestLogHandler();
166            File logDir = Environment.getDefault().getLog();
167            logDir.mkdirs();
168            File logFile = new File(logDir, "jetty.log");
169            NCSARequestLog requestLog = new NCSARequestLog(logFile.getAbsolutePath());
170            requestLogHandler.setRequestLog(requestLog);
171            // handlers = new Handler[] {contexts, new DefaultHandler(),
172            // requestLogHandler};
173            server.addHandler(requestLogHandler);
174            server.setSendServerVersion(true);
175            server.setStopAtShutdown(true);
176
177        }
178
179        // create the war context handler if needed
180        HandlerCollection hc = (HandlerCollection) server.getHandler();
181        warContexts = (ContextHandlerCollection) hc.getChildHandlerByClass(ContextHandlerCollection.class);
182        if (warContexts == null) {
183            // create the war context
184            warContexts = new ContextHandlerCollection();
185            server.addHandler(warContexts);
186        }
187
188        // scan for WAR files
189        // deploy any war found in web directory
190        String scanWebDir = Framework.getProperty(P_SCAN_WEBDIR);
191        if (scanWebDir != null && scanWebDir.equals("true")) {
192            logger.info("Scanning for WARs in web directory");
193            File web = Environment.getDefault().getWeb();
194            scanForWars(web);
195        }
196
197        ctxMgr = new ContextManager(server);
198
199        // start the server
200        // server.start(); -> server will be start after frameworks starts to be
201        // sure that all services
202        // used by web.xml filters are registered.
203    }
204
205    @Override
206    public void deactivate(ComponentContext context) {
207        ctxMgr = null;
208        try {
209            server.stop();
210        } catch (Exception e) { // stupid Jetty API throws Exception
211            throw ExceptionUtils.runtimeException(e);
212        }
213        server = null;
214    }
215
216    @Override
217    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
218        if (XP_WEB_APP.equals(extensionPoint)) {
219            File home = Environment.getDefault().getHome();
220            WebApplication app = (WebApplication) contribution;
221            // TODO preprocessing was removed from this component -
222            // preprocessing should be done in another bundle
223            // if still required (on equinox distribution)
224            // if (app.needsWarPreprocessing()) {
225            // logger.info("Starting deployment preprocessing");
226            // DeploymentPreprocessor dp = new DeploymentPreprocessor(home);
227            // dp.init();
228            // dp.predeploy();
229            // logger.info("Deployment preprocessing terminated");
230            // }
231
232            WebAppContext ctx = new WebAppContext();
233            ctx.setContextPath(app.getContextPath());
234            String root = app.getWebRoot();
235            if (root != null) {
236                File file = new File(home, root);
237                ctx.setWar(file.getAbsolutePath());
238            }
239            String webXml = app.getConfigurationFile();
240            if (webXml != null) {
241                File file = new File(home, root);
242                ctx.setDescriptor(file.getAbsolutePath());
243            }
244            File defWebXml = new File(Environment.getDefault().getConfig(), "default-web.xml");
245            if (defWebXml.isFile()) {
246                ctx.setDefaultsDescriptor(defWebXml.getAbsolutePath());
247            }
248            if ("/".equals(app.getContextPath())) { // the root context must be
249                                                    // put at the end
250                server.addHandler(ctx);
251            } else {
252                warContexts.addHandler(ctx);
253            }
254            org.mortbay.log.Log.setLog(new Log4JLogger(logger));
255            // ctx.start();
256            // HandlerWrapper wrapper = (HandlerWrapper)ctx.getHandler();
257            // wrapper = (HandlerWrapper)wrapper.getHandler();
258            // wrapper.setHandler(new NuxeoServletHandler());
259
260            if (ctx.isFailed()) {
261                logger.error("Error in war deployment");
262            }
263
264        } else if (XP_FILTER.equals(extensionPoint)) {
265            ctxMgr.addFilter((FilterDescriptor) contribution);
266        } else if (XP_SERVLET.equals(extensionPoint)) {
267            ctxMgr.addServlet((ServletDescriptor) contribution);
268        } else if (XP_LISTENERS.equals(extensionPoint)) {
269            ctxMgr.addLifecycleListener((ServletContextListenerDescriptor) contribution);
270        }
271    }
272
273    public ContextManager getContextManager() {
274        return ctxMgr;
275    }
276
277    @Override
278    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
279        if (XP_WEB_APP.equals(extensionPoint)) {
280
281        } else if (XP_FILTER.equals(extensionPoint)) {
282            ctxMgr.removeFilter((FilterDescriptor) contribution);
283        } else if (XP_SERVLET.equals(extensionPoint)) {
284            ctxMgr.removeServlet((ServletDescriptor) contribution);
285        } else if (XP_LISTENERS.equals(extensionPoint)) {
286            ctxMgr.removeLifecycleListener((ServletContextListenerDescriptor) contribution);
287        }
288    }
289
290    @Override
291    public <T> T getAdapter(Class<T> adapter) {
292        if (adapter == org.mortbay.jetty.Server.class) {
293            return adapter.cast(server);
294        }
295        return null;
296    }
297
298    // let's nuxeo runtime get access to the JNDI context
299    // injected through the JettyTransactionalListener
300    protected ClassLoader nuxeoCL;
301
302    public void setNuxeoClassLoader(ClassLoader cl) {
303        nuxeoCL = cl;
304    }
305
306    protected ClassLoader getClassLoader(ClassLoader cl) {
307        if (!Boolean.valueOf(System.getProperty("org.nuxeo.jetty.propagateNaming"))) {
308            return cl;
309        }
310        if (nuxeoCL == null) {
311            return cl;
312        }
313        return nuxeoCL;
314    }
315
316    @Override
317    public int getApplicationStartedOrder() {
318        return -100;
319    }
320
321    @Override
322    public void applicationStarted(ComponentContext context) {
323        if (server == null) {
324            return;
325        }
326        ctxMgr.applyLifecycleListeners();
327        Thread t = Thread.currentThread();
328        ClassLoader oldcl = t.getContextClassLoader();
329        t.setContextClassLoader(getClass().getClassLoader());
330        try {
331            server.start();
332        } catch (Exception e) { // stupid Jetty API throws Exception
333            throw ExceptionUtils.runtimeException(e);
334        } finally {
335            t.setContextClassLoader(getClassLoader(oldcl));
336        }
337    }
338
339    private void scanForWars(File dir) {
340        scanForWars(dir, "");
341    }
342
343    private void scanForWars(File dir, String basePath) {
344        File[] roots = dir.listFiles();
345        if (roots != null) {
346            for (File root : roots) {
347                String name = root.getName();
348                if (name.endsWith(".war")) {
349                    logger.info("Found war: " + name);
350                    name = name.substring(0, name.length() - 4);
351                    boolean isRoot = "root".equals(name);
352                    String ctxPath = isRoot ? "/" : basePath + "/" + name;
353                    WebAppContext ctx = new WebAppContext(root.getAbsolutePath(), ctxPath);
354                    ctx.setConfigurations(new Configuration[] { new WebInfConfiguration(), new WebXmlConfiguration() });
355                    if (isRoot) {
356                        server.addHandler(ctx);
357                    } else {
358                        warContexts.addHandler(ctx);
359                    }
360                } else if (root.isDirectory()) {
361                    scanForWars(root, basePath + "/" + name);
362                }
363            }
364        }
365    }
366
367}