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}