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