001package de.deepamehta.core.impl;
002
003import de.deepamehta.core.osgi.CoreActivator;
004import de.deepamehta.core.service.DeepaMehtaService;
005import de.deepamehta.core.util.UniversalExceptionMapper;
006
007import com.sun.jersey.api.core.DefaultResourceConfig;
008import com.sun.jersey.api.core.ResourceConfig;
009import com.sun.jersey.spi.container.servlet.ServletContainer;
010
011import org.osgi.framework.Bundle;
012import org.osgi.service.http.HttpContext;
013import org.osgi.service.http.HttpService;
014import org.osgi.service.http.NamespaceException;
015
016import javax.servlet.http.HttpServletRequest;
017import javax.servlet.http.HttpServletResponse;
018
019import javax.ws.rs.Path;
020import javax.ws.rs.WebApplicationException;
021import javax.ws.rs.core.Response;
022import javax.ws.rs.ext.Provider;
023
024import java.io.IOException;
025import java.net.URL;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029import java.util.logging.Level;
030import java.util.logging.Logger;
031
032
033
034class WebPublishingService {
035
036    // ------------------------------------------------------------------------------------------------------- Constants
037
038    // Note: OPS4J Pax Web needs "/*". Felix HTTP Jetty in contrast needs "/".
039    private static final String ROOT_APPLICATION_PATH = System.getProperty("dm4.webservice.path", "/");
040    // Note: the default value is required in case no config file is in effect. This applies when DM is started
041    // via feature:install from Karaf. The default values must match the values defined in global POM.
042
043    // ---------------------------------------------------------------------------------------------- Instance Variables
044
045    private ResourceConfig jerseyApplication;
046    private int classCount = 0;         // counts DM root resource and provider classes
047    private int singletonCount = 0;     // counts DM root resource and provider singletons
048    // Note: we count DM resources separately as Jersey adds its own ones to the application.
049    // Once the total DM resource count reaches 0 the Jersey servlet is unregistered.
050
051    private ServletContainer jerseyServlet;
052    private boolean isJerseyServletRegistered = false;
053
054    private DeepaMehtaService dms;
055
056    private Logger logger = Logger.getLogger(getClass().getName());
057
058    // ---------------------------------------------------------------------------------------------------- Constructors
059
060    WebPublishingService(DeepaMehtaService dms) {
061        try {
062            logger.info("Setting up the WebPublishingService");
063            this.dms = dms;
064            //
065            // create Jersey application
066            this.jerseyApplication = new DefaultResourceConfig();
067            //
068            // setup container filters
069            Map<String, Object> properties = jerseyApplication.getProperties();
070            properties.put(ResourceConfig.PROPERTY_CONTAINER_REQUEST_FILTERS, new JerseyRequestFilter(dms));
071            properties.put(ResourceConfig.PROPERTY_CONTAINER_RESPONSE_FILTERS, new JerseyResponseFilter(dms));
072            properties.put(ResourceConfig.PROPERTY_RESOURCE_FILTER_FACTORIES, new TransactionFactory(dms));
073            //
074            // deploy Jersey application in container
075            this.jerseyServlet = new ServletContainer(jerseyApplication);
076        } catch (Exception e) {
077            // unregister...();     // ### TODO?
078            throw new RuntimeException("Setting up the WebPublishingService failed", e);
079        }
080    }
081
082    // ----------------------------------------------------------------------------------------- Package Private Methods
083
084
085
086    // === Static Resources ===
087
088    /**
089     * Publishes the bundle's web resources.
090     * Web resources are found in the bundle's /web directory.
091     */
092    StaticResourcesPublication publishWebResources(String uriNamespace, Bundle bundle) throws NamespaceException {
093        getHttpService().registerResources(uriNamespace, "/web", new BundleHTTPContext(bundle));
094        return new StaticResourcesPublication(uriNamespace, this);
095    }
096
097    /**
098     * Publishes a directory of the server's file system.
099     *
100     * @param   path    An absolute path to the directory to be published.
101     */
102    StaticResourcesPublication publishFileSystem(String uriNamespace, String path) throws NamespaceException {
103        getHttpService().registerResources(uriNamespace, "/", new FileSystemHTTPContext(path));
104        return new StaticResourcesPublication(uriNamespace, this);
105    }
106
107    void unpublishStaticResources(String uriNamespace) {
108        HttpService httpService = getHttpService();
109        if (httpService != null) {
110            httpService.unregister(uriNamespace);
111        } else {
112            logger.warning("HTTP service is already gone");
113        }
114    }
115
116
117
118    // === REST Resources ===
119
120    /**
121     * Publishes REST resources. This is done by adding JAX-RS root resource and provider classes/singletons
122     * to the Jersey application and reloading the Jersey servlet.
123     * <p>
124     * Note: synchronizing prevents creation of multiple Jersey servlet instances due to parallel plugin initialization.
125     *
126     * @param   singletons  the set of root resource and provider singletons, may be empty.
127     * @param   classes     the set of root resource and provider classes, may be empty.
128     */
129    synchronized RestResourcesPublication publishRestResources(List<Object> singletons, List<Class<?>> classes) {
130        try {
131            addToApplication(singletons, classes);
132            //
133            // Note: we must register the Jersey servlet lazily, that is not before any root resources are added.
134            // An "empty" application would fail (com.sun.jersey.api.container.ContainerException:
135            // The ResourceConfig instance does not contain any root resource classes).
136            if (!isJerseyServletRegistered) {
137                // Note: we must not register the servlet as long as no root resources are added yet.
138                // A plugin may contain just provider classes.
139                if (hasRootResources()) {
140                    registerJerseyServlet();
141                }
142            } else {
143                reloadJerseyServlet();
144            }
145            //
146            return new RestResourcesPublication(singletons, classes, this);
147        } catch (Exception e) {
148            unpublishRestResources(singletons, classes);
149            throw new RuntimeException("Adding classes/singletons to Jersey application failed", e);
150        }
151    }
152
153    synchronized void unpublishRestResources(List<Object> singletons, List<Class<?>> classes) {
154        removeFromApplication(singletons, classes);
155        //
156        // Note: once all root resources are removed we must unregister the Jersey servlet.
157        // Reloading it with an "empty" application would fail (com.sun.jersey.api.container.ContainerException:
158        // The ResourceConfig instance does not contain any root resource classes).
159        if (!hasRootResources()) {
160            unregisterJerseyServlet();
161        } else {
162            reloadJerseyServlet();
163        }
164    }
165
166    // ---
167
168    boolean isRootResource(Object object) {
169        return getUriNamespace(object) != null;
170    }
171
172    String getUriNamespace(Object object) {
173        Path path = object.getClass().getAnnotation(Path.class);
174        return path != null ? path.value() : null;
175    }
176
177    boolean isProviderClass(Class clazz) {
178        return clazz.isAnnotationPresent(Provider.class);
179    }
180
181    // ------------------------------------------------------------------------------------------------- Private Methods
182
183    private HttpService getHttpService() {
184        return CoreActivator.getHttpService();
185    }
186
187
188
189    // === Jersey application ===
190
191    private void addToApplication(List<Object> singletons, List<Class<?>> classes) {
192        getClasses().addAll(classes);
193        getSingletons().addAll(singletons);
194        //
195        classCount     += classes.size();
196        singletonCount += singletons.size();
197        //
198        logResourceInfo();
199    }
200
201    private void removeFromApplication(List<Object> singletons, List<Class<?>> classes) {
202        getClasses().removeAll(classes);
203        getSingletons().removeAll(singletons);
204        //
205        classCount -= classes.size();
206        singletonCount -= singletons.size();
207        //
208        logResourceInfo();
209    }
210
211    // ---
212
213    private boolean hasRootResources() {
214        return singletonCount > 0;
215    }
216
217    private void logResourceInfo() {
218        logger.fine("##### DM Classes: " + classCount + ", All: " + getClasses().size() + " " + getClasses());
219        logger.fine("##### DM Singletons: " + singletonCount + ", All: " + getSingletons().size() + " " +
220            getSingletons());
221    }
222
223    // ---
224
225    private Set<Class<?>> getClasses() {
226        return jerseyApplication.getClasses();
227    }
228
229    private Set<Object> getSingletons() {
230        return jerseyApplication.getSingletons();
231    }
232
233
234
235    // === Jersey Servlet ===
236
237    private void registerJerseyServlet() {
238        try {
239            logger.fine("########## Registering Jersey servlet at HTTP service (URI namespace=\"" +
240                ROOT_APPLICATION_PATH + "\")");
241            // Note: registerServlet() throws javax.servlet.ServletException
242            getHttpService().registerServlet(ROOT_APPLICATION_PATH, jerseyServlet, null, null);
243            isJerseyServletRegistered = true;
244        } catch (Exception e) {
245            throw new RuntimeException("Registering Jersey servlet at HTTP service failed (URI namespace=\"" +
246                ROOT_APPLICATION_PATH + "\")", e);
247        }
248    }
249
250    private void unregisterJerseyServlet() {
251        logger.fine("########## Unregistering Jersey servlet at HTTP service (URI namespace=\"" +
252            ROOT_APPLICATION_PATH + "\")");
253        HttpService httpService = getHttpService();
254        if (httpService != null) {
255            httpService.unregister(ROOT_APPLICATION_PATH);
256        } else {
257            logger.warning("HTTP service is already gone");
258        }
259        isJerseyServletRegistered = false;
260    }
261
262    // ---
263
264    private void reloadJerseyServlet() {
265        logger.fine("##### Reloading Jersey servlet");
266        jerseyServlet.reload();
267    }
268
269
270
271    // === Resource Request Filter ===
272
273    private boolean resourceRequestFilter(HttpServletRequest request, HttpServletResponse response) throws IOException {
274        try {
275            dms.fireEvent(CoreEvent.RESOURCE_REQUEST_FILTER, request);
276            return true;
277        } catch (Throwable e) {
278            // Note: resourceRequestFilter() is called from an OSGi HTTP service static resource HttpContext.
279            // JAX-RS is not involved here. No JAX-RS exception mapper kicks in. Though the application's
280            // ResourceRequestFilterListener can throw a WebApplicationException (which is JAX-RS API)
281            // in order to provide error response info.
282            new UniversalExceptionMapper(e, request).initResponse(response);
283            return false;
284        }
285    }
286
287
288
289    // ------------------------------------------------------------------------------------------------- Private Classes
290
291    private class BundleHTTPContext implements HttpContext {
292
293        private Bundle bundle;
294
295        private BundleHTTPContext(Bundle bundle) {
296            this.bundle = bundle;
297        }
298
299        // ---
300
301        @Override
302        public URL getResource(String name) {
303            // 1) map "/" to "/index.html"
304            //
305            // Note: for the bundle's web root resource Pax Web passes "/web/" or "/web",
306            // depending whether the request URL has a slash at the end or not.
307            // Felix HTTP Jetty 2.2.0 in contrast passes "web/" and version 2.3.0 passes "/web/"
308            // (regardless whether the request URL has a slash at the end or not).
309            if (name.equals("/web") || name.equals("/web/")) {
310                name = "/web/index.html";
311            }
312            // 2) access resource from context bundle
313            return bundle.getResource(name);
314        }
315
316        @Override
317        public String getMimeType(String name) {
318            return null;
319        }
320
321        @Override
322        public boolean handleSecurity(HttpServletRequest request, HttpServletResponse response)
323                                                                            throws java.io.IOException {
324            return resourceRequestFilter(request, response);
325        }
326    }
327
328    private class FileSystemHTTPContext implements HttpContext {
329
330        private String path;
331
332        /**
333         * @param   path    An absolute path to a directory.
334         */
335        private FileSystemHTTPContext(String path) {
336            this.path = path;
337        }
338
339        // ---
340
341        @Override
342        public URL getResource(String name) {
343            try {
344                URL url = new URL("file:" + path + "/" + name);     // throws java.net.MalformedURLException
345                logger.info("### Mapping resource name \"" + name + "\" to URL \"" + url + "\"");
346                return url;
347            } catch (Exception e) {
348                throw new RuntimeException("Mapping resource name \"" + name + "\" to URL failed", e);
349            }
350        }
351
352        @Override
353        public String getMimeType(String name) {
354            return null;
355        }
356
357        @Override
358        public boolean handleSecurity(HttpServletRequest request, HttpServletResponse response)
359                                                                            throws java.io.IOException {
360            return resourceRequestFilter(request, response);
361        }
362    }
363}