001    package de.deepamehta.core.impl;
002    
003    import de.deepamehta.core.service.SecurityHandler;
004    
005    import com.sun.jersey.api.core.DefaultResourceConfig;
006    import com.sun.jersey.api.core.ResourceConfig;
007    import com.sun.jersey.spi.container.servlet.ServletContainer;
008    
009    import org.osgi.framework.Bundle;
010    import org.osgi.service.http.HttpContext;
011    import org.osgi.service.http.HttpService;
012    
013    import javax.servlet.http.HttpServletRequest;
014    import javax.servlet.http.HttpServletResponse;
015    
016    import javax.ws.rs.Path;
017    import javax.ws.rs.WebApplicationException;
018    import javax.ws.rs.core.MultivaluedMap;
019    import javax.ws.rs.core.Response;
020    
021    import java.io.IOException;
022    import java.net.URL;
023    import java.util.List;
024    import java.util.Map;
025    import java.util.Set;
026    import java.util.logging.Level;
027    import java.util.logging.Logger;
028    
029    
030    
031    public class WebPublishingService {
032    
033        // ------------------------------------------------------------------------------------------------------- Constants
034    
035        // Note: OPS4J Pax Web needs "/*". Felix HTTP Jetty in contrast needs "/".
036        private static final String ROOT_APPLICATION_PATH = "/*";
037    
038        // ---------------------------------------------------------------------------------------------- Instance Variables
039    
040        private ResourceConfig jerseyApplication;
041        private int classCount = 0;         // counts DM root resource and provider classes
042        private int singletonCount = 0;     // counts DM root resource and provider singletons
043        // Note: we count DM resources separately as Jersey adds its own ones to the application.
044        // Once the total DM resource count reaches 0 the Jersey servlet is unregistered.
045    
046        private ServletContainer jerseyServlet;
047        private boolean isJerseyServletRegistered = false;
048    
049        private HttpService httpService;
050    
051        private EmbeddedService dms;
052    
053        private Logger logger = Logger.getLogger(getClass().getName());
054    
055        // ---------------------------------------------------------------------------------------------------- Constructors
056    
057        public WebPublishingService(EmbeddedService dms, HttpService httpService) {
058            try {
059                logger.info("Setting up the Web Publishing service");
060                this.dms = dms;
061                //
062                // create Jersey application
063                this.jerseyApplication = new DefaultResourceConfig();
064                //
065                // setup container filters
066                Map<String, Object> properties = jerseyApplication.getProperties();
067                properties.put(ResourceConfig.PROPERTY_CONTAINER_REQUEST_FILTERS, new JerseyRequestFilter(dms));
068                properties.put(ResourceConfig.PROPERTY_CONTAINER_RESPONSE_FILTERS, new JerseyResponseFilter(dms));
069                properties.put(ResourceConfig.PROPERTY_RESOURCE_FILTER_FACTORIES, new TransactionFactory(dms));
070                //
071                // deploy Jersey application in container
072                this.jerseyServlet = new ServletContainer(jerseyApplication);
073                this.httpService = httpService;
074            } catch (Exception e) {
075                // unregister...();     // ### TODO?
076                throw new RuntimeException("Setting up the Web Publishing service failed", e);
077            }
078        }
079    
080        // ----------------------------------------------------------------------------------------- Package Private Methods
081    
082    
083    
084        // === Static Resources ===
085    
086        /**
087         * Publishes the static resources of the given bundle's /web directory.
088         */
089        StaticResources publishStaticResources(Bundle bundle, String uriNamespace) {
090            try {
091                // Note: registerResources() throws org.osgi.service.http.NamespaceException
092                httpService.registerResources(uriNamespace, "/web", new BundleHTTPContext(bundle));
093                return new StaticResources(uriNamespace);
094            } catch (Exception e) {
095                throw new RuntimeException(e);
096            }
097        }
098    
099        void unpublishStaticResources(StaticResources staticResources) {
100            httpService.unregister(staticResources.uriNamespace);
101        }
102    
103        // ---
104    
105        /**
106         * Publishes a directory of the server's file system.
107         */
108        StaticResources publishStaticResources(String directoryPath, String uriNamespace, SecurityHandler securityHandler) {
109            try {
110                // Note: registerResources() throws org.osgi.service.http.NamespaceException
111                httpService.registerResources(uriNamespace, "/", new DirectoryHTTPContext(directoryPath, securityHandler));
112                return new StaticResources(uriNamespace);
113            } catch (Exception e) {
114                throw new RuntimeException(e);
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 this method prevents creation of multiple Jersey servlet instances due to parallel plugin
127         * 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 RestResources publishRestResources(List<Object> singletons, List<Class<?>> classes) {
133            RestResources restResources = new RestResources(singletons, classes);
134            try {
135                addToApplication(restResources);
136                //
137                // Note: we must register the Jersey servlet lazily, that is not before any root resources are added.
138                // An "empty" application would fail (com.sun.jersey.api.container.ContainerException:
139                // The ResourceConfig instance does not contain any root resource classes).
140                if (!isJerseyServletRegistered) {
141                    // Note: we must not register the servlet as long as no root resources are added yet.
142                    // A plugin may contain just provider classes.
143                    if (hasRootResources()) {
144                        registerJerseyServlet();
145                    }
146                } else {
147                    reloadJerseyServlet();
148                }
149                //
150                return restResources;
151            } catch (Exception e) {
152                unpublishRestResources(restResources);
153                throw new RuntimeException("Adding classes/singletons to Jersey application failed", e);
154            }
155        }
156    
157        synchronized void unpublishRestResources(RestResources restResources) {
158            removeFromApplication(restResources);
159            //
160            // Note: once all root resources are removed we must unregister the Jersey servlet.
161            // Reloading it with an "empty" application would fail (com.sun.jersey.api.container.ContainerException:
162            // The ResourceConfig instance does not contain any root resource classes).
163            if (!hasRootResources()) {
164                unregisterJerseyServlet();
165            } else {
166                reloadJerseyServlet();
167            }
168        }
169    
170        // ---
171    
172        boolean isRootResource(Object object) {
173            return getUriNamespace(object) != null;
174        }
175    
176        String getUriNamespace(Object object) {
177            Path path = object.getClass().getAnnotation(Path.class);
178            return path != null ? path.value() : null;
179        }
180    
181        // ------------------------------------------------------------------------------------------------- Private Methods
182    
183    
184    
185        // === Jersey application ===
186    
187        private void addToApplication(RestResources restResources) {
188            getClasses().addAll(restResources.classes);
189            getSingletons().addAll(restResources.singletons);
190            //
191            classCount     += restResources.classes.size();
192            singletonCount += restResources.singletons.size();
193            //
194            logResourceInfo();
195        }
196    
197        private void removeFromApplication(RestResources restResources) {
198            getClasses().removeAll(restResources.classes);
199            getSingletons().removeAll(restResources.singletons);
200            //
201            classCount -= restResources.classes.size();
202            singletonCount -= restResources.singletons.size();
203            //
204            logResourceInfo();
205        }
206    
207        // ---
208    
209        private boolean hasRootResources() {
210            return singletonCount > 0;
211        }
212    
213        private void logResourceInfo() {
214            logger.fine("##### DM Classes: " + classCount + ", All: " + getClasses().size() + " " + getClasses());
215            logger.fine("##### DM Singletons: " + singletonCount + ", All: " + getSingletons().size() + " " +
216                getSingletons());
217        }
218    
219        // ---
220    
221        private Set<Class<?>> getClasses() {
222            return jerseyApplication.getClasses();
223        }
224    
225        private Set<Object> getSingletons() {
226            return jerseyApplication.getSingletons();
227        }
228    
229    
230    
231        // === Jersey Servlet ===
232    
233        private void registerJerseyServlet() {
234            try {
235                logger.fine("########## Registering Jersey servlet at HTTP service (URI namespace=\"" +
236                    ROOT_APPLICATION_PATH + "\")");
237                // Note: registerServlet() throws javax.servlet.ServletException
238                httpService.registerServlet(ROOT_APPLICATION_PATH, jerseyServlet, null, null);
239                isJerseyServletRegistered = true;
240            } catch (Exception e) {
241                // unregister...();     // ### TODO?
242                throw new RuntimeException("Registering Jersey servlet at HTTP service failed", e);
243            }
244        }
245    
246        private void unregisterJerseyServlet() {
247            logger.fine("########## Unregistering Jersey servlet at HTTP service (URI namespace=\"" +
248                ROOT_APPLICATION_PATH + "\")");
249            httpService.unregister(ROOT_APPLICATION_PATH);
250            isJerseyServletRegistered = false;
251        }
252    
253        // ---
254    
255        private void reloadJerseyServlet() {
256            logger.fine("##### Reloading Jersey servlet");
257            jerseyServlet.reload();
258        }
259    
260    
261    
262        // === Resource Request Filter ===
263    
264        private boolean resourceRequestFilter(HttpServletRequest request, HttpServletResponse response) throws IOException {
265            try {
266                dms.fireEvent(CoreEvent.RESOURCE_REQUEST_FILTER, request);
267                return true;
268            } catch (WebApplicationException e) {
269                sendError(response, e.getResponse());
270                return false;
271            } catch (Exception e) {
272                logger.log(Level.SEVERE, "Resource request filtering failed for " + request.getRequestURI(), e);
273                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
274                return false;
275            }
276        }
277    
278        private void sendError(HttpServletResponse servletResponse, Response response) throws IOException {
279            // transfer headers
280            MultivaluedMap<String, Object> metadata = response.getMetadata();
281            for (String header : metadata.keySet()) {
282                for (Object value : metadata.get(header)) {
283                    servletResponse.addHeader(header, (String) value);
284                }
285            }
286            //
287            servletResponse.sendError(response.getStatus(), (String) response.getEntity());   // throws IOException
288        }
289    
290    
291    
292        // ------------------------------------------------------------------------------------------------- Private Classes
293    
294        private class BundleHTTPContext implements HttpContext {
295    
296            private Bundle bundle;
297    
298            private BundleHTTPContext(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 resource from context bundle
316                return bundle.getResource(name);
317            }
318    
319            @Override
320            public String getMimeType(String name) {
321                return null;
322            }
323    
324            @Override
325            public boolean handleSecurity(HttpServletRequest request, HttpServletResponse response)
326                                                                                throws java.io.IOException {
327                return resourceRequestFilter(request, response);
328            }
329        }
330    
331        private class DirectoryHTTPContext implements HttpContext {
332    
333            private String directoryPath;
334            private SecurityHandler securityHandler;
335    
336            private DirectoryHTTPContext(String directoryPath, SecurityHandler securityHandler) {
337                this.directoryPath = directoryPath;
338                this.securityHandler = securityHandler;
339            }
340    
341            // ---
342    
343            @Override
344            public URL getResource(String name) {
345                try {
346                    URL url = new URL("file:" + directoryPath + "/" + name);    // throws java.net.MalformedURLException
347                    logger.info("### Mapping resource name \"" + name + "\" to URL \"" + url + "\"");
348                    return url;
349                } catch (Exception e) {
350                    throw new RuntimeException("Mapping resource name \"" + name + "\" to URL failed", e);
351                }
352            }
353    
354            @Override
355            public String getMimeType(String name) {
356                return null;
357            }
358    
359            @Override
360            public boolean handleSecurity(HttpServletRequest request, HttpServletResponse response)
361                                                                                throws java.io.IOException {
362                boolean doService = resourceRequestFilter(request, response);
363                if (doService) {
364                    if (securityHandler != null) {
365                        return securityHandler.handleSecurity(request, response);
366                    } else {
367                        return true;
368                    }
369                }
370                return false;
371            }
372        }
373    }