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