    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}