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