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}