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        private static final String ROOT_APPLICATION_PATH = "/";
036    
037        // ---------------------------------------------------------------------------------------------- Instance Variables
038    
039        private ResourceConfig rootApplication;
040        private int classCount = 0;         // counts DM root resource and provider classes
041        private int singletonCount = 0;     // counts DM root resource and provider singletons
042        // Note: we count DM resources separately as Jersey adds its own ones to the application.
043        // Once the total DM resource count reaches 0 the Jersey servlet is unregistered.
044    
045        private ServletContainer jerseyServlet;
046        private boolean isJerseyServletRegistered = false;
047    
048        private HttpService httpService;
049    
050        private EmbeddedService dms;
051    
052        private Logger logger = Logger.getLogger(getClass().getName());
053    
054        // ---------------------------------------------------------------------------------------------------- Constructors
055    
056        public WebPublishingService(EmbeddedService dms, HttpService httpService) {
057            try {
058                logger.info("Setting up the Web Publishing service");
059                this.dms = dms;
060                //
061                // create web application
062                this.rootApplication = new DefaultResourceConfig();
063                //
064                // setup container filters
065                Map<String, Object> properties = rootApplication.getProperties();
066                properties.put(ResourceConfig.PROPERTY_CONTAINER_REQUEST_FILTERS, new JerseyRequestFilter(dms));
067                properties.put(ResourceConfig.PROPERTY_CONTAINER_RESPONSE_FILTERS, new JerseyResponseFilter(dms));
068                //
069                // deploy web application in container
070                this.jerseyServlet = new ServletContainer(rootApplication);
071                this.httpService = httpService;
072            } catch (Exception e) {
073                // unregister...();     // ### TODO?
074                throw new RuntimeException("Setting up the Web Publishing service failed", e);
075            }
076        }
077    
078        // ----------------------------------------------------------------------------------------- Package Private Methods
079    
080    
081    
082        // === Web Resources ===
083    
084        /**
085         * Publishes the /web resources directory of the given bundle to the web.
086         */
087        WebResources addWebResources(Bundle bundle, String uriNamespace) {
088            try {
089                // Note: registerResources() throws org.osgi.service.http.NamespaceException
090                httpService.registerResources(uriNamespace, "/web", new BundleHTTPContext(bundle));
091                return new WebResources(uriNamespace);
092            } catch (Exception e) {
093                throw new RuntimeException(e);
094            }
095        }
096    
097        void removeWebResources(WebResources webResources) {
098            httpService.unregister(webResources.uriNamespace);
099        }
100    
101        // ---
102    
103        /**
104         * Publishes a directory of the server's file system to the web.
105         */
106        WebResources addWebResources(String directoryPath, String uriNamespace, SecurityHandler securityHandler) {
107            try {
108                // Note: registerResources() throws org.osgi.service.http.NamespaceException
109                httpService.registerResources(uriNamespace, "/", new DirectoryHTTPContext(directoryPath, securityHandler));
110                return new WebResources(uriNamespace);
111            } catch (Exception e) {
112                throw new RuntimeException(e);
113            }
114        }
115    
116    
117    
118        // === REST Resources ===
119    
120        /**
121         * Note: synchronizing this method prevents creation of multiple Jersey servlet instances due to parallel plugin
122         * initialization.
123         *
124         * @param   resources           the set of root resource and provider singletons, may be empty.
125         * @param   providerClasses     the set of root resource and provider classes, may be empty.
126         */
127        synchronized RestResource addRestResource(List<Object> resources, List<Class<?>> providerClasses) {
128            addSingletons(resources);
129            addClasses(providerClasses);
130            logResourceInfo();
131            //
132            // Note: we must register the Jersey servlet lazily, that is not before any root resources are added.
133            // An "empty" application would fail (com.sun.jersey.api.container.ContainerException:
134            // The ResourceConfig instance does not contain any root resource classes).
135            if (!isJerseyServletRegistered) {
136                // Note: we must not register the servlet as long as no root resources are added yet.
137                // A plugin may contain just provider classes.
138                if (hasRootResources()) {
139                    registerJerseyServlet();
140                }
141            } else {
142                reloadJerseyServlet();
143            }
144            //
145            return new RestResource(resources, providerClasses);
146        }
147    
148        synchronized void removeRestResource(RestResource restResource) {
149            removeSingletons(restResource.resources);
150            removeClasses(restResource.providerClasses);
151            logResourceInfo();
152            //
153            // Note: once all root resources are removed we must unregister the Jersey servlet.
154            // Reloading it with an "empty" application would fail (com.sun.jersey.api.container.ContainerException:
155            // The ResourceConfig instance does not contain any root resource classes).
156            if (!hasRootResources()) {
157                unregisterJerseyServlet();
158            } else {
159                reloadJerseyServlet();
160            }
161        }
162    
163        // ---
164    
165        boolean isRootResource(Object object) {
166            return getUriNamespace(object) != null;
167        }
168    
169        String getUriNamespace(Object object) {
170            Path path = object.getClass().getAnnotation(Path.class);
171            return path != null ? path.value() : null;
172        }
173    
174        // ------------------------------------------------------------------------------------------------- Private Methods
175    
176    
177    
178        // === Jersey Servlet ===
179    
180        private Set<Class<?>> getClasses() {
181            return rootApplication.getClasses();
182        }
183    
184        private Set<Object> getSingletons() {
185            return rootApplication.getSingletons();
186        }
187    
188        // ---
189    
190        private void addClasses(List<Class<?>> classes) {
191            getClasses().addAll(classes);
192            classCount += classes.size();
193        }
194    
195        private void addSingletons(List<Object> singletons) {
196            getSingletons().addAll(singletons);
197            singletonCount += singletons.size();
198        }
199    
200        // ---
201    
202        private void removeClasses(List<Class<?>> classes) {
203            getClasses().removeAll(classes);
204            classCount -= classes.size();
205        }
206    
207        private void removeSingletons(List<Object> singletons) {
208            getSingletons().removeAll(singletons);
209            singletonCount -= singletons.size();
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 void registerJerseyServlet() {
227            try {
228                logger.fine("########## Registering Jersey servlet at HTTP service (URI namespace=\"" +
229                    ROOT_APPLICATION_PATH + "\")");
230                // Note: registerServlet() throws javax.servlet.ServletException
231                httpService.registerServlet(ROOT_APPLICATION_PATH, jerseyServlet, null, null);
232                isJerseyServletRegistered = true;
233            } catch (Exception e) {
234                // unregister...();     // ### TODO?
235                throw new RuntimeException("Registering Jersey servlet at HTTP service failed", e);
236            }
237        }
238    
239        private void unregisterJerseyServlet() {
240            logger.fine("########## Unregistering Jersey servlet at HTTP service (URI namespace=\"" +
241                ROOT_APPLICATION_PATH + "\")");
242            httpService.unregister(ROOT_APPLICATION_PATH);
243            isJerseyServletRegistered = false;
244        }
245    
246        // ---
247    
248        private void reloadJerseyServlet() {
249            logger.fine("##### Reloading Jersey servlet");
250            jerseyServlet.reload();
251        }
252    
253    
254    
255        // === Resource Request Filter ===
256    
257        private boolean resourceRequestFilter(HttpServletRequest request, HttpServletResponse response) throws IOException {
258            try {
259                dms.fireEvent(CoreEvent.RESOURCE_REQUEST_FILTER, request);
260                return true;
261            } catch (WebApplicationException e) {
262                sendError(response, e.getResponse());
263                return false;
264            } catch (Exception e) {
265                logger.log(Level.SEVERE, "Resource request filtering failed for " + request.getRequestURI(), e);
266                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
267                return false;
268            }
269        }
270    
271        private void sendError(HttpServletResponse servletResponse, Response response) throws IOException {
272            // transfer headers
273            MultivaluedMap<String, Object> metadata = response.getMetadata();
274            for (String header : metadata.keySet()) {
275                for (Object value : metadata.get(header)) {
276                    servletResponse.addHeader(header, (String) value);
277                }
278            }
279            //
280            servletResponse.sendError(response.getStatus(), (String) response.getEntity());   // throws IOException
281        }
282    
283    
284    
285        // ------------------------------------------------------------------------------------------------- Private Classes
286    
287        private class BundleHTTPContext implements HttpContext {
288    
289            private Bundle bundle;
290    
291            private BundleHTTPContext(Bundle bundle) {
292                this.bundle = bundle;
293            }
294    
295            // ---
296    
297            @Override
298            public URL getResource(String name) {
299                // map resource name "/" to "/index.html"
300                if (name.equals("web/")) {
301                    name = "/web/index.html";
302                }
303                //
304                return bundle.getResource(name);
305            }
306    
307            @Override
308            public String getMimeType(String name) {
309                return null;
310            }
311    
312            @Override
313            public boolean handleSecurity(HttpServletRequest request, HttpServletResponse response)
314                                                                                throws java.io.IOException {
315                return resourceRequestFilter(request, response);
316            }
317        }
318    
319        private class DirectoryHTTPContext implements HttpContext {
320    
321            private String directoryPath;
322            private SecurityHandler securityHandler;
323    
324            private DirectoryHTTPContext(String directoryPath, SecurityHandler securityHandler) {
325                this.directoryPath = directoryPath;
326                this.securityHandler = securityHandler;
327            }
328    
329            // ---
330    
331            @Override
332            public URL getResource(String name) {
333                try {
334                    URL url = new URL("file:" + directoryPath + "/" + name);    // throws java.net.MalformedURLException
335                    logger.info("### Mapping resource name \"" + name + "\" to URL \"" + url + "\"");
336                    return url;
337                } catch (Exception e) {
338                    throw new RuntimeException("Mapping resource name \"" + name + "\" to URL failed", e);
339                }
340            }
341    
342            @Override
343            public String getMimeType(String name) {
344                return null;
345            }
346    
347            @Override
348            public boolean handleSecurity(HttpServletRequest request, HttpServletResponse response)
349                                                                                throws java.io.IOException {
350                boolean doService = resourceRequestFilter(request, response);
351                if (doService) {
352                    if (securityHandler != null) {
353                        return securityHandler.handleSecurity(request, response);
354                    } else {
355                        return true;
356                    }
357                }
358                return false;
359            }
360        }
361    }