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