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