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