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                OutputStream out = resp.getOutputStream();
178                byte[] bytes = new byte[1024 * 64];
179                int r = in.read(bytes);
180                while (r > -1) {
181                    if (r > 0) {
182                        out.write(bytes, 0, r);
183                    }
184                    r = in.read(bytes);
185                }
186                out.flush();
187            } finally {
188                in.close();
189            }
190        }
191    }
192
193    protected RenderingEngine initRendering(ServletConfig config) throws ServletException {
194        RenderingEngine rendering;
195        try {
196            String v = config.getInitParameter(RenderingEngine.class.getName());
197            if (v != null) {
198                rendering = (RenderingEngine) Utils.getClassRef(v, bundle).newInstance();
199            } else { // default settings
200                rendering = new FreemarkerEngine();
201                ((FreemarkerEngine) rendering).getConfiguration().setClassicCompatible(false);
202            }
203            rendering.setResourceLocator(this);
204            return rendering;
205        } catch (ReflectiveOperationException | BundleNotFoundException e) {
206            throw new ServletException(e);
207        }
208    }
209
210    protected void destroyRendering() {
211        // do nothing
212    }
213
214    protected void initContainer(ServletConfig config) throws ServletException {
215        container.init(getServletConfig());
216    }
217
218    protected void destroyContainer() {
219        container.destroy();
220        container = null;
221    }
222
223    protected synchronized void reloadContainer() throws ServletException {
224        // reload is not working correctly since old classes are still referenced
225        // for this to work we need a custom ResourceConfig but all fields in jersey
226        // classes are private so we cannot set it ...
227        try {
228            container.destroy();
229            container = createServletContainer(app);
230            container.init(getServletConfig());
231        } finally {
232            isDirty = false;
233        }
234    }
235
236    protected ServletContainer createServletContainer(ApplicationHost app) {
237        ApplicationAdapter adapter = new ApplicationAdapter(app);
238        // disable wadl since we got class loader pb in JAXB under equinox
239        adapter.getFeatures().put(ResourceConfig.FEATURE_DISABLE_WADL, Boolean.TRUE);
240        // copy all features recorded in app
241        adapter.getFeatures().putAll(app.getFeatures());
242        return new ServletContainer(adapter);
243    }
244
245    @Override
246    public File getResourceFile(String key) {
247        return null;
248    }
249
250    @Override
251    public URL getResourceURL(String key) {
252        return ResourceContext.getContext().findEntry(key);
253    }
254
255}