001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.http.modules; 014 015import java.io.BufferedReader; 016import java.io.IOException; 017import java.io.InputStream; 018import java.io.Reader; 019import java.util.List; 020import java.util.Map; 021 022import javax.servlet.ServletContext; 023import javax.servlet.http.HttpServletRequest; 024import javax.servlet.http.HttpServletResponse; 025 026import org.apache.commons.io.IOUtils; 027import org.apache.tapestry5.commons.MappedConfiguration; 028import org.apache.tapestry5.commons.OrderedConfiguration; 029import org.apache.tapestry5.commons.internal.util.TapestryException; 030import org.apache.tapestry5.commons.services.CoercionTuple; 031import org.apache.tapestry5.http.OptimizedSessionPersistedObject; 032import org.apache.tapestry5.http.TapestryHttpSymbolConstants; 033import org.apache.tapestry5.http.internal.AsyncRequestService; 034import org.apache.tapestry5.http.internal.TypeCoercerHttpRequestBodyConverter; 035import org.apache.tapestry5.http.internal.gzip.GZipFilter; 036import org.apache.tapestry5.http.internal.services.ApplicationGlobalsImpl; 037import org.apache.tapestry5.http.internal.services.AsyncRequestServiceImpl; 038import org.apache.tapestry5.http.internal.services.BaseURLSourceImpl; 039import org.apache.tapestry5.http.internal.services.ContextImpl; 040import org.apache.tapestry5.http.internal.services.DefaultSessionPersistedObjectAnalyzer; 041import org.apache.tapestry5.http.internal.services.OptimizedSessionPersistedObjectAnalyzer; 042import org.apache.tapestry5.http.internal.services.RequestGlobalsImpl; 043import org.apache.tapestry5.http.internal.services.RequestImpl; 044import org.apache.tapestry5.http.internal.services.ResponseCompressionAnalyzerImpl; 045import org.apache.tapestry5.http.internal.services.ResponseImpl; 046import org.apache.tapestry5.http.internal.services.RestSupportImpl; 047import org.apache.tapestry5.http.internal.services.TapestrySessionFactory; 048import org.apache.tapestry5.http.internal.services.TapestrySessionFactoryImpl; 049import org.apache.tapestry5.http.services.ApplicationGlobals; 050import org.apache.tapestry5.http.services.ApplicationInitializer; 051import org.apache.tapestry5.http.services.ApplicationInitializerFilter; 052import org.apache.tapestry5.http.services.BaseURLSource; 053import org.apache.tapestry5.http.services.Context; 054import org.apache.tapestry5.http.services.Dispatcher; 055import org.apache.tapestry5.http.services.HttpRequestBodyConverter; 056import org.apache.tapestry5.http.services.HttpServletRequestFilter; 057import org.apache.tapestry5.http.services.HttpServletRequestHandler; 058import org.apache.tapestry5.http.services.Request; 059import org.apache.tapestry5.http.services.RequestFilter; 060import org.apache.tapestry5.http.services.RequestGlobals; 061import org.apache.tapestry5.http.services.RequestHandler; 062import org.apache.tapestry5.http.services.Response; 063import org.apache.tapestry5.http.services.ResponseCompressionAnalyzer; 064import org.apache.tapestry5.http.services.RestSupport; 065import org.apache.tapestry5.http.services.ServletApplicationInitializer; 066import org.apache.tapestry5.http.services.ServletApplicationInitializerFilter; 067import org.apache.tapestry5.http.services.SessionPersistedObjectAnalyzer; 068import org.apache.tapestry5.ioc.ServiceBinder; 069import org.apache.tapestry5.ioc.annotations.Autobuild; 070import org.apache.tapestry5.ioc.annotations.Marker; 071import org.apache.tapestry5.ioc.annotations.Primary; 072import org.apache.tapestry5.ioc.annotations.Symbol; 073import org.apache.tapestry5.ioc.services.ChainBuilder; 074import org.apache.tapestry5.ioc.services.PipelineBuilder; 075import org.apache.tapestry5.ioc.services.PropertyShadowBuilder; 076import org.apache.tapestry5.ioc.services.StrategyBuilder; 077import org.slf4j.Logger; 078 079/** 080 * The Tapestry module for HTTP handling classes. 081 */ 082public final class TapestryHttpModule { 083 084 final private PropertyShadowBuilder shadowBuilder; 085 final private RequestGlobals requestGlobals; 086 final private PipelineBuilder pipelineBuilder; 087 final private ApplicationGlobals applicationGlobals; 088 089 public TapestryHttpModule(PropertyShadowBuilder shadowBuilder, 090 RequestGlobals requestGlobals, PipelineBuilder pipelineBuilder, 091 ApplicationGlobals applicationGlobals) 092 { 093 this.shadowBuilder = shadowBuilder; 094 this.requestGlobals = requestGlobals; 095 this.pipelineBuilder = pipelineBuilder; 096 this.applicationGlobals = applicationGlobals; 097 } 098 099 public static void bind(ServiceBinder binder) 100 { 101 binder.bind(RequestGlobals.class, RequestGlobalsImpl.class); 102 binder.bind(ApplicationGlobals.class, ApplicationGlobalsImpl.class); 103 binder.bind(TapestrySessionFactory.class, TapestrySessionFactoryImpl.class); 104 binder.bind(BaseURLSource.class, BaseURLSourceImpl.class); 105 binder.bind(ResponseCompressionAnalyzer.class, ResponseCompressionAnalyzerImpl.class); 106 binder.bind(RestSupport.class, RestSupportImpl.class); 107 binder.bind(AsyncRequestService.class, AsyncRequestServiceImpl.class); 108 } 109 110 /** 111 * Contributes factory defaults that may be overridden. 112 */ 113 public static void contributeFactoryDefaults(MappedConfiguration<String, Object> configuration) 114 { 115 configuration.add(TapestryHttpSymbolConstants.SESSION_LOCKING_ENABLED, true); 116 configuration.add(TapestryHttpSymbolConstants.CLUSTERED_SESSIONS, true); 117 configuration.add(TapestryHttpSymbolConstants.CHARSET, "UTF-8"); 118 configuration.add(TapestryHttpSymbolConstants.APPLICATION_VERSION, "0.0.1"); 119 configuration.add(TapestryHttpSymbolConstants.GZIP_COMPRESSION_ENABLED, true); 120 configuration.add(TapestryHttpSymbolConstants.MIN_GZIP_SIZE, 100); 121 122 // The default values denote "use values from request" 123 configuration.add(TapestryHttpSymbolConstants.HOSTNAME, ""); 124 configuration.add(TapestryHttpSymbolConstants.HOSTPORT, 0); 125 configuration.add(TapestryHttpSymbolConstants.HOSTPORT_SECURE, 0); 126 } 127 128 /** 129 * Builds a shadow of the RequestGlobals.request property. Note again that 130 * the shadow can be an ordinary singleton, 131 * even though RequestGlobals is perthread. 132 */ 133 public Request buildRequest(PropertyShadowBuilder shadowBuilder) 134 { 135 return shadowBuilder.build(requestGlobals, "request", Request.class); 136 } 137 138 /** 139 * Builds a shadow of the RequestGlobals.HTTPServletRequest property. 140 * Generally, you should inject the {@link Request} service instead, as 141 * future version of Tapestry may operate beyond just the servlet API. 142 */ 143 public HttpServletRequest buildHttpServletRequest() 144 { 145 return shadowBuilder.build(requestGlobals, "HTTPServletRequest", HttpServletRequest.class); 146 } 147 148 /** 149 * @since 5.1.0.0 150 */ 151 public HttpServletResponse buildHttpServletResponse() 152 { 153 return shadowBuilder.build(requestGlobals, "HTTPServletResponse", HttpServletResponse.class); 154 } 155 156 /** 157 * Builds a shadow of the RequestGlobals.response property. Note again that 158 * the shadow can be an ordinary singleton, 159 * even though RequestGlobals is perthread. 160 */ 161 public Response buildResponse() 162 { 163 return shadowBuilder.build(requestGlobals, "response", Response.class); 164 } 165 166 /** 167 * Ordered contributions to the MasterDispatcher service allow different URL 168 * matching strategies to occur. 169 */ 170 @Marker(Primary.class) 171 public Dispatcher buildMasterDispatcher(List<Dispatcher> configuration, 172 ChainBuilder chainBuilder) 173 { 174 return chainBuilder.build(Dispatcher.class, configuration); 175 } 176 177 /** 178 * The master SessionPersistedObjectAnalyzer. 179 * 180 * @since 5.1.0.0 181 */ 182 @SuppressWarnings("rawtypes") 183 @Marker(Primary.class) 184 public SessionPersistedObjectAnalyzer buildSessionPersistedObjectAnalyzer( 185 Map<Class, SessionPersistedObjectAnalyzer> configuration, 186 StrategyBuilder strategyBuilder) 187 { 188 return strategyBuilder.build(SessionPersistedObjectAnalyzer.class, configuration); 189 } 190 191 /** 192 * Identifies String, Number and Boolean as immutable objects, a catch-all 193 * handler for Object (that understands 194 * the {@link org.apache.tapestry5.http.annotations.ImmutableSessionPersistedObject} annotation), 195 * and a handler for {@link org.apache.tapestry5.http.OptimizedSessionPersistedObject}. 196 * 197 * @since 5.1.0.0 198 */ 199 @SuppressWarnings("rawtypes") 200 public static void contributeSessionPersistedObjectAnalyzer( 201 MappedConfiguration<Class, SessionPersistedObjectAnalyzer> configuration) 202 { 203 configuration.add(Object.class, new DefaultSessionPersistedObjectAnalyzer()); 204 205 SessionPersistedObjectAnalyzer<Object> immutable = new SessionPersistedObjectAnalyzer<Object>() 206 { 207 public boolean checkAndResetDirtyState(Object sessionPersistedObject) 208 { 209 return false; 210 } 211 }; 212 213 configuration.add(String.class, immutable); 214 configuration.add(Number.class, immutable); 215 configuration.add(Boolean.class, immutable); 216 217 configuration.add(OptimizedSessionPersistedObject.class, new OptimizedSessionPersistedObjectAnalyzer()); 218 } 219 220 /** 221 * Initializes the application, using a pipeline of {@link org.apache.tapestry5.http.services.ApplicationInitializer}s. 222 */ 223 @Marker(Primary.class) 224 public ApplicationInitializer buildApplicationInitializer(Logger logger, 225 List<ApplicationInitializerFilter> configuration) 226 { 227 ApplicationInitializer terminator = new ApplicationInitializerTerminator(); 228 229 return pipelineBuilder.build(logger, ApplicationInitializer.class, ApplicationInitializerFilter.class, 230 configuration, terminator); 231 } 232 233 public HttpServletRequestHandler buildHttpServletRequestHandler(Logger logger, 234 235 List<HttpServletRequestFilter> configuration, 236 237 @Primary 238 RequestHandler handler, 239 240 @Symbol(TapestryHttpSymbolConstants.CHARSET) 241 String applicationCharset, 242 243 TapestrySessionFactory sessionFactory) 244 { 245 HttpServletRequestHandler terminator = new HttpServletRequestHandlerTerminator(handler, applicationCharset, 246 sessionFactory); 247 248 return pipelineBuilder.build(logger, HttpServletRequestHandler.class, HttpServletRequestFilter.class, 249 configuration, terminator); 250 } 251 252 @Marker(Primary.class) 253 public RequestHandler buildRequestHandler(Logger logger, List<RequestFilter> configuration, 254 255 @Primary 256 Dispatcher masterDispatcher) 257 { 258 RequestHandler terminator = new RequestHandlerTerminator(masterDispatcher); 259 260 return pipelineBuilder.build(logger, RequestHandler.class, RequestFilter.class, configuration, terminator); 261 } 262 263 public ServletApplicationInitializer buildServletApplicationInitializer(Logger logger, 264 List<ServletApplicationInitializerFilter> configuration, 265 266 @Primary 267 ApplicationInitializer initializer) 268 { 269 ServletApplicationInitializer terminator = new ServletApplicationInitializerTerminator(initializer); 270 271 return pipelineBuilder.build(logger, ServletApplicationInitializer.class, 272 ServletApplicationInitializerFilter.class, configuration, terminator); 273 } 274 275 /** 276 * <dl> 277 * <dt>StoreIntoGlobals</dt> 278 * <dd>Stores the request and response into {@link org.apache.tapestry5.http.services.RequestGlobals} at the start of the 279 * pipeline</dd> 280 * <dt>IgnoredPaths</dt> 281 * <dd>Identifies requests that are known (via the IgnoredPathsFilter service's configuration) to be mapped to other 282 * applications</dd> 283 * <dt>GZip</dt> 284 * <dd>Handles GZIP compression of response streams (if supported by client)</dd> 285 * </dl> 286 */ 287 public void contributeHttpServletRequestHandler(OrderedConfiguration<HttpServletRequestFilter> configuration, 288 @Symbol(TapestryHttpSymbolConstants.GZIP_COMPRESSION_ENABLED) boolean gzipCompressionEnabled, 289 @Autobuild GZipFilter gzipFilter) 290 { 291 292 HttpServletRequestFilter storeIntoGlobals = new HttpServletRequestFilter() 293 { 294 public boolean service(HttpServletRequest request, HttpServletResponse response, 295 HttpServletRequestHandler handler) throws IOException 296 { 297 requestGlobals.storeServletRequestResponse(request, response); 298 299 return handler.service(request, response); 300 } 301 }; 302 303 configuration.add("StoreIntoGlobals", storeIntoGlobals, "before:*"); 304 305 configuration.add("GZIP", gzipCompressionEnabled ? gzipFilter : null); 306 307 } 308 309 public static HttpRequestBodyConverter buildHttpRequestBodyConverter( 310 final List<HttpRequestBodyConverter> converters, 311 final ChainBuilder chainBuilder) 312 { 313 return chainBuilder.build(HttpRequestBodyConverter.class, converters); 314 } 315 316 public static void contributeHttpRequestBodyConverter( 317 final OrderedConfiguration<HttpRequestBodyConverter> configuration) 318 { 319 configuration.addInstance("TypeCoercer", TypeCoercerHttpRequestBodyConverter.class, "after:*"); 320 } 321 322 @SuppressWarnings("rawtypes") 323 public static void contributeTypeCoercer(MappedConfiguration<CoercionTuple.Key, CoercionTuple> configuration) 324 { 325 CoercionTuple.add(configuration, HttpServletRequest.class, String.class, TapestryHttpModule::toString); 326 CoercionTuple.add(configuration, HttpServletRequest.class, byte[].class, TapestryHttpModule::toByteArray); 327 CoercionTuple.add(configuration, HttpServletRequest.class, InputStream.class, TapestryHttpModule::toInputStream); 328 CoercionTuple.add(configuration, HttpServletRequest.class, Reader.class, TapestryHttpModule::toBufferedReader); 329 CoercionTuple.add(configuration, HttpServletRequest.class, BufferedReader.class, TapestryHttpModule::toBufferedReader); 330 } 331 332 private final static InputStream toInputStream(HttpServletRequest request) 333 { 334 try 335 { 336 return request.getInputStream(); 337 } catch (IOException e) { 338 throw new RuntimeException(e); 339 } 340 } 341 342 private final static BufferedReader toBufferedReader(HttpServletRequest request) 343 { 344 try 345 { 346 return request.getReader(); 347 } catch (IOException e) { 348 throw new RuntimeException(e); 349 } 350 } 351 352 private final static String toString(HttpServletRequest request) 353 { 354 try (Reader reader = request.getReader()) 355 { 356 String string = IOUtils.toString(reader); 357 return string.isEmpty() ? null : string; 358 } 359 catch (IOException e) { 360 throw new TapestryException( 361 "Exception converting body from HttpServletRequest (getReader()) to String", e); 362 } 363 } 364 365 private final static byte[] toByteArray(HttpServletRequest request) 366 { 367 try (InputStream inputStream = request.getInputStream()) 368 { 369 byte[] byteArray = IOUtils.toByteArray(inputStream); 370 return byteArray.length == 0 ? null : byteArray; 371 } catch (IOException e) { 372 throw new TapestryException( 373 "Exception converting from HttpServletRequest (getInputStream()) to String", e); 374 } 375 } 376 377 // A bunch of classes "promoted" from inline inner class to nested classes, 378 // just so that the stack trace would be more readable. Most of these 379 // are terminators for pipeline services. 380 381 /** 382 * @since 5.1.0.0 383 */ 384 private class ApplicationInitializerTerminator implements ApplicationInitializer 385 { 386 public void initializeApplication(Context context) 387 { 388 applicationGlobals.storeContext(context); 389 } 390 } 391 392 /** 393 * @since 5.1.0.0 394 */ 395 private class HttpServletRequestHandlerTerminator implements HttpServletRequestHandler 396 { 397 private final RequestHandler handler; 398 private final String applicationCharset; 399 private final TapestrySessionFactory sessionFactory; 400 401 public HttpServletRequestHandlerTerminator(RequestHandler handler, String applicationCharset, 402 TapestrySessionFactory sessionFactory) 403 { 404 this.handler = handler; 405 this.applicationCharset = applicationCharset; 406 this.sessionFactory = sessionFactory; 407 } 408 409 public boolean service(HttpServletRequest servletRequest, HttpServletResponse servletResponse) 410 throws IOException 411 { 412 requestGlobals.storeServletRequestResponse(servletRequest, servletResponse); 413 414 // Should have started doing this a long time ago: recoding attributes into 415 // the request for things that may be needed downstream, without having to extend 416 // Request. 417 418 servletRequest.setAttribute("servletAPI.protocol", servletRequest.getProtocol()); 419 servletRequest.setAttribute("servletAPI.characterEncoding", servletRequest.getCharacterEncoding()); 420 servletRequest.setAttribute("servletAPI.contentLength", servletRequest.getContentLength()); 421 servletRequest.setAttribute("servletAPI.authType", servletRequest.getAuthType()); 422 servletRequest.setAttribute("servletAPI.contentType", servletRequest.getContentType()); 423 servletRequest.setAttribute("servletAPI.scheme", servletRequest.getScheme()); 424 425 Request request = new RequestImpl(servletRequest, applicationCharset, sessionFactory); 426 Response response = new ResponseImpl(servletRequest, servletResponse); 427 428 // TAP5-257: Make sure that the "initial guess" for request/response 429 // is available, even ifsome filter in the RequestHandler pipeline replaces them. 430 // Which just goes to show that there should have been only one way to access the Request/Response: 431 // either functionally (via parameters) or global (via ReqeuestGlobals) but not both. 432 // That ship has sailed. 433 434 requestGlobals.storeRequestResponse(request, response); 435 436 // Transition from the Servlet API-based pipeline, to the 437 // Tapestry-based pipeline. 438 439 return handler.service(request, response); 440 } 441 } 442 443 /** 444 * @since 5.1.0.0 445 */ 446 private class RequestHandlerTerminator implements RequestHandler 447 { 448 private final Dispatcher masterDispatcher; 449 450 public RequestHandlerTerminator(Dispatcher masterDispatcher) 451 { 452 this.masterDispatcher = masterDispatcher; 453 } 454 455 public boolean service(Request request, Response response) throws IOException 456 { 457 // Update RequestGlobals with the current request/response (in case 458 // some filter replaced the 459 // normal set). 460 requestGlobals.storeRequestResponse(request, response); 461 462 return masterDispatcher.dispatch(request, response); 463 } 464 } 465 466 /** 467 * @since 5.1.0.0 468 */ 469 private class ServletApplicationInitializerTerminator implements ServletApplicationInitializer 470 { 471 private final ApplicationInitializer initializer; 472 473 public ServletApplicationInitializerTerminator(ApplicationInitializer initializer) 474 { 475 this.initializer = initializer; 476 } 477 478 public void initializeApplication(ServletContext servletContext) 479 { 480 applicationGlobals.storeServletContext(servletContext); 481 482 // And now, down the (Web) ApplicationInitializer pipeline ... 483 484 ContextImpl context = new ContextImpl(servletContext); 485 486 applicationGlobals.storeContext(context); 487 488 initializer.initializeApplication(context); 489 } 490 } 491 492}