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