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}