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