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