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