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