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