001/*
002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     bstefanescu
011 */
012package org.nuxeo.ecm.webengine.jaxrs.servlet;
013
014import java.io.File;
015import java.io.IOException;
016import java.io.InputStream;
017import java.io.OutputStream;
018import java.net.URL;
019import java.util.Enumeration;
020
021import javax.servlet.ServletConfig;
022import javax.servlet.ServletException;
023import javax.servlet.http.HttpServlet;
024import javax.servlet.http.HttpServletRequest;
025import javax.servlet.http.HttpServletResponse;
026
027import org.nuxeo.ecm.webengine.jaxrs.ApplicationHost;
028import org.nuxeo.ecm.webengine.jaxrs.ApplicationManager;
029import org.nuxeo.ecm.webengine.jaxrs.BundleNotFoundException;
030import org.nuxeo.ecm.webengine.jaxrs.Reloadable;
031import org.nuxeo.ecm.webengine.jaxrs.Utils;
032import org.nuxeo.ecm.webengine.jaxrs.servlet.config.ServletDescriptor;
033import org.nuxeo.ecm.webengine.jaxrs.views.ResourceContext;
034import org.nuxeo.ecm.platform.rendering.api.RenderingEngine;
035import org.nuxeo.ecm.platform.rendering.api.ResourceLocator;
036import org.nuxeo.ecm.platform.rendering.fm.FreemarkerEngine;
037import org.osgi.framework.Bundle;
038
039import com.sun.jersey.api.core.ApplicationAdapter;
040import com.sun.jersey.api.core.ResourceConfig;
041import com.sun.jersey.spi.container.servlet.ServletContainer;
042
043/**
044 * A hot re-loadable JAX-RS servlet. This servlet is building a Jersey JAX-RS Application. If you need to support other
045 * JAX-RS containers than Jersey you need to write your own servlet.
046 * <p>
047 * Use it as the webengine servlet in web.xml if you want hot reload, otherwise directly use the Jersey servlet:
048 * {@link ServletContainer}.
049 *
050 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
051 */
052public class ApplicationServlet extends HttpServlet implements ManagedServlet, Reloadable, ResourceLocator {
053
054    private static final long serialVersionUID = 1L;
055
056    protected volatile boolean isDirty = false;
057
058    protected Bundle bundle;
059
060    protected ApplicationHost app;
061
062    protected ServletContainer container;
063
064    protected String resourcesPrefix;
065
066    @Override
067    public void setDescriptor(ServletDescriptor sd) {
068        this.bundle = sd.getBundle();
069    }
070
071    @Override
072    public void init(ServletConfig config) throws ServletException {
073        super.init(config);
074        resourcesPrefix = config.getInitParameter("resources.prefix");
075        if (resourcesPrefix == null) {
076            resourcesPrefix = "/skin";
077        }
078        String name = config.getInitParameter("application.name");
079        if (name == null) {
080            name = ApplicationManager.DEFAULT_HOST;
081        }
082        app = ApplicationManager.getInstance().getOrCreateApplication(name);
083        // use init parameters that are booleans as features
084        for (Enumeration<String> en = config.getInitParameterNames(); en.hasMoreElements();) {
085            String n = en.nextElement();
086            String v = config.getInitParameter(n);
087            if (Boolean.TRUE.toString().equals(v) || Boolean.FALSE.toString().equals(v)) {
088                app.getFeatures().put(n, Boolean.valueOf(v));
089            }
090        }
091        container = createServletContainer(app);
092
093        initContainer(config);
094        app.setRendering(initRendering(config));
095
096        app.addReloadListener(this);
097    }
098
099    @Override
100    public void destroy() {
101        destroyContainer();
102        destroyRendering();
103        container = null;
104        app = null;
105        bundle = null;
106        resourcesPrefix = null;
107    }
108
109    @Override
110    public synchronized void reload() {
111        isDirty = true;
112    }
113
114    public RenderingEngine getRenderingEngine() {
115        return app.getRendering();
116    }
117
118    public Bundle getBundle() {
119        return bundle;
120    }
121
122    public ServletContainer getContainer() {
123        return container;
124    }
125
126    @Override
127    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
128        String pinfo = request.getPathInfo();
129        if (pinfo != null && pinfo.startsWith(resourcesPrefix)) {
130            super.service(request, response);
131        } else {
132            containerService(request, response);
133        }
134    }
135
136    protected void containerService(HttpServletRequest request, HttpServletResponse response) throws ServletException,
137            IOException {
138        if (isDirty) {
139            reloadContainer();
140        }
141        String method = request.getMethod().toUpperCase();
142        if (!"GET".equals(method)) {
143            // force reading properties because jersey is consuming one
144            // character
145            // from the input stream - see WebComponent.isEntityPresent.
146            request.getParameterMap();
147        }
148        ResourceContext ctx = new ResourceContext(app);
149        ctx.setRequest(request);
150        ResourceContext.setContext(ctx);
151        request.setAttribute(ResourceContext.class.getName(), ctx);
152        try {
153            container.service(request, response);
154        } finally {
155            ResourceContext.destroyContext();
156            request.removeAttribute(ResourceContext.class.getName());
157        }
158    }
159
160    @Override
161    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
162        String pathInfo = req.getPathInfo();
163        InputStream in = getServletContext().getResourceAsStream(pathInfo.substring(resourcesPrefix.length()));
164        if (in != null) {
165            String ctype = getServletContext().getMimeType(pathInfo);
166            if (ctype != null) {
167                resp.addHeader("Content-Type", ctype);
168            }
169            try {
170                OutputStream out = resp.getOutputStream();
171                byte[] bytes = new byte[1024 * 64];
172                int r = in.read(bytes);
173                while (r > -1) {
174                    if (r > 0) {
175                        out.write(bytes, 0, r);
176                    }
177                    r = in.read(bytes);
178                }
179                out.flush();
180            } finally {
181                in.close();
182            }
183        }
184    }
185
186    protected RenderingEngine initRendering(ServletConfig config) throws ServletException {
187        RenderingEngine rendering;
188        try {
189            String v = config.getInitParameter(RenderingEngine.class.getName());
190            if (v != null) {
191                rendering = (RenderingEngine) Utils.getClassRef(v, bundle).newInstance();
192            } else { // default settings
193                rendering = new FreemarkerEngine();
194                ((FreemarkerEngine) rendering).getConfiguration().setClassicCompatible(false);
195            }
196            rendering.setResourceLocator(this);
197            return rendering;
198        } catch (ReflectiveOperationException | BundleNotFoundException e) {
199            throw new ServletException(e);
200        }
201    }
202
203    protected void destroyRendering() {
204        // do nothing
205    }
206
207    protected void initContainer(ServletConfig config) throws ServletException {
208        container.init(getServletConfig());
209    }
210
211    protected void destroyContainer() {
212        container.destroy();
213        container = null;
214    }
215
216    protected synchronized void reloadContainer() throws ServletException {
217        // reload is not working correctly since old classes are still referenced
218        // for this to work we need a custom ResourceConfig but all fields in jersey
219        // classes are private so we cannot set it ...
220        try {
221            container.destroy();
222            container = createServletContainer(app);
223            container.init(getServletConfig());
224        } finally {
225            isDirty = false;
226        }
227    }
228
229    protected ServletContainer createServletContainer(ApplicationHost app) {
230        ApplicationAdapter adapter = new ApplicationAdapter(app);
231        // disable wadl since we got class loader pb in JAXB under equinox
232        adapter.getFeatures().put(ResourceConfig.FEATURE_DISABLE_WADL, Boolean.TRUE);
233        // copy all features recorded in app
234        adapter.getFeatures().putAll(app.getFeatures());
235        return new ServletContainer(adapter);
236    }
237
238    @Override
239    public File getResourceFile(String key) {
240        return null;
241    }
242
243    @Override
244    public URL getResourceURL(String key) {
245        return ResourceContext.getContext().findEntry(key);
246    }
247
248}