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