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