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