Protecting Pages

Let's say you would like Tapestry to protect particular pages from being accessed by users who have not logged in...

In JumpStart we do this with 2 parts. First, we create an annotation, @ProtectedPage, which we add to pages we want to protect.
You should consider doing the opposite: create an annotation @PublicPage to put on the pages you want to be public and protect all others.

Second, we create a ComponentRequestFilter, called PageProtectionFilter, which we contribute to the application in AppModule. This filter inspects every page render request and component event request as it comes in, determines which page is involved, and whether the page has the annotation. If it does and the user is not logged in, then the filter redirects the browser to the Login page. It also tells the Login page which page you were trying to reach.
Here's a link to a "protected" page: View admin user. Try it!
For more elaborate security try tapestry-security and Security in Tapestry5HowTos.

References: Request Processing diagram, Securing Tapestry Pages with Annotations, ComponentRequestFilter, Request.

Home


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like &nbsp; 
     We use a "strict" DTD to make IE follow the alignment rules. -->
     
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd">
<body class="container">
    <h3>Protecting Pages</h3>
    
    Let's say you would like Tapestry to protect particular pages from being accessed by users who have not logged in...<br/><br/>
    
    In JumpStart we do this with 2 parts. First, we create an annotation, @ProtectedPage, which we add to pages we want to protect.<br/> 
    You should consider doing the opposite: create an annotation @PublicPage to put on the pages you want to be public and protect all others.<br/><br/>
    
    Second, we create a ComponentRequestFilter, called PageProtectionFilter, which we contribute to the application in AppModule. 
    This filter inspects every <a href="http://tapestry.apache.org/page-navigation.html#PageNavigation-PageRenderRequests">
    page render request</a> and <a href="http://tapestry.apache.org/page-navigation.html#PageNavigation-ComponentEventRequests&amp;Responses">
    component event request</a> as it comes in, determines which page is involved,
    and whether the page has the annotation. If it does and the user is not logged in, then the filter redirects the browser to 
    the Login page. It also tells the Login page which page you were trying to reach.

    <div class="eg">
        Here's a link to a "protected" page: 
        <t:pagelink page="theapp/userview" context="literal:2">View admin user</t:pagelink>. Try it!
    </div>
    
    For more elaborate security try <a href="http://www.tynamo.org/tapestry-security+guide">tapestry-security</a> 
    and <a href="http://wiki.apache.org/tapestry/Tapestry5HowTos#Security">Security in Tapestry5HowTos</a>.<br/><br/>
    
    References: 
    <a href="http://tapestry.apache.org/request-processing.html#RequestProcessing-Overview">Request Processing diagram</a>, 
    <a href="http://tapestryjava.blogspot.com/2009/12/securing-tapestry-pages-with.html">Securing Tapestry Pages with Annotations</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/ComponentRequestFilter.html">ComponentRequestFilter</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/Request.html">Request</a>.<br/><br/>
    
    <t:pagelink page="Index">Home</t:pagelink><br/><br/>
    
    <t:tabgroup>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/infrastructure/ProtectingPages.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/infrastructure/ProtectingPages.java"/>
        <t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/examples/olive.css"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/theapp/security/UserView.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/annotation/ProtectedPage.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/services/PageProtectionFilter.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/infra/PageDenied.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/infra/PageDenied.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/commons/IIntermediatePage.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/state/theapp/Visit.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/services/AppModule.java"/>
    </t:tabgroup>
</body>
</html>


package jumpstart.web.pages.examples.infrastructure;

import org.apache.tapestry5.annotations.Import;

@Import(stylesheet = "css/examples/olive.css")
public class ProtectingPages {
}


.eg {
                margin: 20px 0;
                padding: 14px;
                color: olive;
                border: 1px solid #ddd;
                border-radius: 6px;
                -webkit-border-radius: 6px;
                -mox-border-radius: 6px;
}


package jumpstart.web.pages.theapp.security;

import java.util.List;

import javax.ejb.EJB;

import jumpstart.business.commons.exception.BusinessException;
import jumpstart.business.commons.exception.DoesNotExistException;
import jumpstart.business.domain.security.User;
import jumpstart.business.domain.security.User.PageStyle;
import jumpstart.business.domain.security.UserRole;
import jumpstart.business.domain.security.iface.ISecurityFinderServiceLocal;
import jumpstart.web.annotation.ProtectedPage;
import jumpstart.web.base.theapp.SimpleBasePage;

import org.apache.tapestry5.Link;
import org.apache.tapestry5.annotations.InjectPage;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.services.TypeCoercer;
import org.apache.tapestry5.services.PageRenderLinkSource;
import org.apache.tapestry5.util.EnumValueEncoder;

@ProtectedPage
public class UserView extends SimpleBasePage {

    // Activation context

    private Long userId;

    // Screen fields

    @Property
    private User user;

    @Property
    private List<UserRole> userRoles;

    @Property
    private UserRole userRole;

    // Other pages

    @InjectPage
    private UserSearch userSearch;

    @InjectPage
    private UserRoleView viewPage;

    // Generally useful bits and pieces

    @EJB
    private ISecurityFinderServiceLocal securityFinderService;

    @Inject
    private PageRenderLinkSource pageRenderLinkSource;

    @Inject
    private TypeCoercer typeCoercer;

    // The code

    public void set(Long userId) {
        this.userId = userId;
    }

    void onActivate(Long userId) {
        this.userId = userId;
    }

    Long onPassivate() {
        return userId;
    }

    void setupRender() throws BusinessException {
        try {
            user = securityFinderService.findUser(userId);
        }
        catch (DoesNotExistException e) {
            // Handle null user in the template
        }

        userRoles = securityFinderService.findUserRolesShallowishByUser(userId);
    }

    void onRefresh() {
    }

    Link onCancel() {
        return userSearch.createLinkWithLastSearch();
    }

    Object onViewUserRole(Long id) {
        viewPage.set(id, createLinkToThisPage());
        return viewPage;
    }

    private Link createLinkToThisPage() {
        Link thisPageLink = pageRenderLinkSource.createPageRenderLinkWithContext(this.getClass(), onPassivate());
        return thisPageLink;
    }

    public PageStyle getBoxy() {
        return User.PageStyle.BOXY;
    }

    public PageStyle getWide() {
        return User.PageStyle.WIDE;
    }

    public EnumValueEncoder<PageStyle> getPageStyleEncoder() {
        return new EnumValueEncoder<PageStyle>(typeCoercer, User.PageStyle.class);
    }
}


// Based on http://wiki.apache.org/tapestry/Tapestry5HowToControlAccess
// When you apply this @ProtectedPage annotation to any page class that you want 

package jumpstart.web.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Specifies that the class is a "protected page", one that must not be accessible by users that are not logged in.
 * This annotation is applied to a Tapestry page class. The protection is provided by {@link PageProtectionFilter}. 
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ProtectedPage {
}


// Based on http://tapestryjava.blogspot.com/2009/12/securing-tapestry-pages-with.html

package jumpstart.web.services;

import java.io.IOException;
import java.io.OutputStream;
import java.util.List;

import javax.annotation.security.RolesAllowed;

import jumpstart.business.domain.security.User;
import jumpstart.business.domain.security.iface.ISecurityFinderServiceLocal;
import jumpstart.client.BusinessServicesLocator;
import jumpstart.client.IBusinessServicesLocator;
import jumpstart.web.annotation.ProtectedPage;
import jumpstart.web.commons.IIntermediatePage;
import jumpstart.web.pages.infra.PageDenied;
import jumpstart.web.pages.theapp.LogIn;
import jumpstart.web.state.theapp.Visit;

import org.apache.tapestry5.EventContext;
import org.apache.tapestry5.Link;
import org.apache.tapestry5.internal.EmptyEventContext;
import org.apache.tapestry5.runtime.Component;
import org.apache.tapestry5.services.ApplicationStateManager;
import org.apache.tapestry5.services.ComponentEventRequestParameters;
import org.apache.tapestry5.services.ComponentRequestFilter;
import org.apache.tapestry5.services.ComponentRequestHandler;
import org.apache.tapestry5.services.ComponentSource;
import org.apache.tapestry5.services.PageRenderLinkSource;
import org.apache.tapestry5.services.PageRenderRequestParameters;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.Response;
import org.slf4j.Logger;

/**
 * A service that protects pages annotated with {@link jumpstart.web.annotation.ProtectedPage}. It examines each
 * {@link org.apache.tapestry5.services.Request} and redirects it to the login page if the request is for a
 * ProtectedPage and the user is not logged in. If the page also has the {@link javax.annotation.security.RolesAllowed}
 * annotation then the user must belong to one of the listed roles.
 * <p>
 * To use this filter, contribute it to Tapestry's ComponentRequestHandler service as we do in AppModule.
 * 
 */
public class PageProtectionFilter implements ComponentRequestFilter {
    private static final String COMPONENT_PARAM_PREFIX = "t:";

    private final String autoLogInStr = System.getProperty("jumpstart.auto-login");

    private enum AuthCheckResult {
        AUTHENTICATE, AUTHORISED, DENY;
    }

    private final PageRenderLinkSource pageRenderLinkSource;
    private final ComponentSource componentSource;
    private final Request request;
    private final Response response;
    private ApplicationStateManager sessionStateManager;
    private final Logger logger;
    private IBusinessServicesLocator businessServicesLocator;

    /**
     * Receive all the services needed as constructor arguments. When we bind this service, T5 IoC will provide all the
     * services.
     */
    public PageProtectionFilter(PageRenderLinkSource pageRenderLinkSource, ComponentSource componentSource,
            Request request, Response response, ApplicationStateManager asm, Logger logger) {
        this.pageRenderLinkSource = pageRenderLinkSource;
        this.request = request;
        this.response = response;
        this.componentSource = componentSource;
        this.sessionStateManager = asm;
        this.logger = logger;
        this.businessServicesLocator = null;
    }

    @Override
    public void handlePageRender(PageRenderRequestParameters parameters, ComponentRequestHandler handler)
            throws IOException {

        AuthCheckResult result = checkAuthorityToPage(parameters.getLogicalPageName());

        if (result == AuthCheckResult.AUTHORISED) {
            handler.handlePageRender(parameters);
        }
        else if (result == AuthCheckResult.DENY) {

            // Redirect to the Denied page.

            Link pageProtectedLink = pageRenderLinkSource.createPageRenderLinkWithContext(PageDenied.class,
                    parameters.getLogicalPageName());
            response.sendRedirect(pageProtectedLink);
        }
        else if (result == AuthCheckResult.AUTHENTICATE) {

            // Redirect to the LogIn page, with memory of the request.

            Link requestedPageLink = createLinkToRequestedPage(parameters.getLogicalPageName(),
                    parameters.getActivationContext());
            Link logInPageLink = createLogInPageLinkWithMemory(requestedPageLink);

            response.sendRedirect(logInPageLink);
        }
        else {
            throw new IllegalStateException(result.toString());
        }

    }

    @Override
    public void handleComponentEvent(ComponentEventRequestParameters parameters, ComponentRequestHandler handler)
            throws IOException {

        AuthCheckResult result = checkAuthorityToPage(parameters.getActivePageName());

        if (result == AuthCheckResult.AUTHORISED) {
            handler.handleComponentEvent(parameters);
        }
        else if (result == AuthCheckResult.DENY) {

            // Redirect to the Denied page.

            Link pageProtectedLink = pageRenderLinkSource.createPageRenderLinkWithContext(PageDenied.class,
                    parameters.getActivePageName());
            response.sendRedirect(pageProtectedLink);
        }
        else if (result == AuthCheckResult.AUTHENTICATE) {

            // If AJAX request, return an AJAX response that reloads the page.

            if (request.isXHR()) {
                Link requestedPageLink = createLinkToRequestedPage(parameters.getActivePageName(),
                        parameters.getPageActivationContext());
                OutputStream os = response.getOutputStream("application/json;charset=UTF-8");
                // TODO - Using new, longer JSON string until this is fixed:
                // http://apache-tapestry-mailing-list-archives.1045711.n5.nabble.com/T5-4-cannot-redirect-on-session-timeout-tt5724697.html.
                // os.write(("{\"redirectURL\":\"" + requestedPageLink.toAbsoluteURI() + "\"}").getBytes());
                os.write(("{\"_tapestry\" : " + "{\"redirectURL\" : \"" + requestedPageLink.toAbsoluteURI() + "\"}" + " }")
                        .getBytes());
                os.close();
                return;
            }

            // Else, redirect to the LogIn page, with memory of the request.

            else {
                Link requestedPageLink = createLinkToRequestedPage(parameters.getActivePageName(),
                        parameters.getPageActivationContext());
                Link logInPageLink = createLogInPageLinkWithMemory(requestedPageLink);

                response.sendRedirect(logInPageLink);
            }
        }
        else {
            throw new IllegalStateException(result.toString());
        }

    }

    public AuthCheckResult checkAuthorityToPage(String requestedPageName) throws IOException {

        Component page = componentSource.getPage(requestedPageName);

        // Does the page have security annotations @ProtectedPage or @RolesAllowed?

        boolean protectedPage = page.getClass().getAnnotation(ProtectedPage.class) != null;
        RolesAllowed rolesAllowed = page.getClass().getAnnotation(RolesAllowed.class);

        // If the security annotations on the page conflict in meaning, then error

        if (!protectedPage && rolesAllowed != null) {
            throw new IllegalStateException("Page \"" + requestedPageName
                    + "\" is annotated with @RolesAllowed but not @ProtectedPage.");
        }
        else if (protectedPage && rolesAllowed == null) {
            // throw new IllegalStateException("Page \"" + requestedPageName
            // + "\" is annotated with @ProtectedPage but not @RolesAllowed.");
        }

        // If page is public (ie. not protected), then everyone is authorised to it so allow access

        if (!protectedPage) {
            return AuthCheckResult.AUTHORISED;
        }

        // If user has not been authenticated, disallow.

        if (!isAuthenticated()) {
            return AuthCheckResult.AUTHENTICATE;
        }

        // If user is authorised to the page, then all is well.

        if (isAuthorised(rolesAllowed)) {
            return AuthCheckResult.AUTHORISED;
        }

        // Fell through, so not authorised.

        return AuthCheckResult.DENY;

    }

    private Link createLinkToRequestedPage(String requestedPageName, EventContext eventContext) {

        // Create a link to the page you wanted.

        Link linkToRequestedPage;

        if (eventContext instanceof EmptyEventContext) {
            linkToRequestedPage = pageRenderLinkSource.createPageRenderLink(requestedPageName);
        }
        else {
            Object[] args = new String[eventContext.getCount()];
            for (int i = 0; i < eventContext.getCount(); i++) {
                args[i] = eventContext.get(String.class, i);
            }
            linkToRequestedPage = pageRenderLinkSource.createPageRenderLinkWithContext(requestedPageName, args);
        }

        // Add any activation request parameters (AKA query parameters).

        List<String> parameterNames = request.getParameterNames();

        for (String parameterName : parameterNames) {
            linkToRequestedPage.removeParameter(parameterName);
            if (!parameterName.startsWith(COMPONENT_PARAM_PREFIX)) {
                linkToRequestedPage.addParameter(parameterName, request.getParameter(parameterName));
            }
        }

        return linkToRequestedPage;
    }

    private boolean isAuthenticated() throws IOException {

        // TODO - is this test unnecessary?
        if (request.getSession(false) == null) {
            return false;
        }

        // If a Visit already exists in the session then you have already been authenticated

        if (sessionStateManager.exists(Visit.class)) {
            return true;
        }

        // Else if "auto-login" is on, try auto-logging in.
        // - this facility is for development environment only. It avoids getting you thrown out of the
        // app every time the session clears eg. when app is restarted.

        else {
            if (isAutoLogInOn()) {
                autoLogIn(1L);
                return true;
            }
        }

        return false;
    }

    private boolean isAuthorised(RolesAllowed rolesAllowed) throws IOException {
        boolean authorised = false;

        if (rolesAllowed == null) {
            authorised = true;
        }
        else {
            // Here we could check whether the user's role, or perhaps roles, include one of the rolesAllowed.
            // Typically we'd cache the user's roles in the Visit.
        }

        return authorised;
    }

    /**
     * Checks the value of system property jumpstart.auto-login. If "true" then returns true; if "false" then return
     * false; if not set then returns false.
     */
    private boolean isAutoLogInOn() {
        boolean autoLogIn = false;
        if (autoLogInStr == null) {
            autoLogIn = false;
        }
        else if (autoLogInStr.equalsIgnoreCase("true")) {
            autoLogIn = true;
        }
        else if (autoLogInStr.equalsIgnoreCase("false")) {
            autoLogIn = false;
        }
        else {
            throw new IllegalStateException(
                    "System property jumpstart.auto-login has been set to \""
                            + autoLogInStr
                            + "\".  Please set it to \"true\" or \"false\".  If not specified at all then it will default to \"false\".");
        }
        return autoLogIn;
    }

    /**
     * Automatically logs you in as the given user. Intended for use in development environment only.
     */
    private void autoLogIn(Long userId) {

        // Lazy-load the business services locator because it is only needed for auto-login

        if (businessServicesLocator == null) {
            businessServicesLocator = new BusinessServicesLocator(logger);
        }

        try {
            User user = getSecurityFinderService().findUser(userId);

            Visit visit = new Visit(user);
            logger.info(user.getLoginId() + " has been auto-logged-in.");

            sessionStateManager.set(Visit.class, visit);
        }
        catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    private Link createLogInPageLinkWithMemory(Link requestedPageLink) {

        IIntermediatePage logInPage = (IIntermediatePage) componentSource.getPage(LogIn.class);
        logInPage.setNextPageLink(requestedPageLink);
        Link logInPageLink = pageRenderLinkSource.createPageRenderLink(LogIn.class);

        return logInPageLink;
    }

    private ISecurityFinderServiceLocal getSecurityFinderService() {

        if (businessServicesLocator == null) {
            businessServicesLocator = new BusinessServicesLocator(logger);
        }

        return (ISecurityFinderServiceLocal) businessServicesLocator.getService(ISecurityFinderServiceLocal.class);
    }
}


package jumpstart.web.pages.infra;

import javax.servlet.http.HttpServletResponse;

import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.Response;

/**
 * Intended for use with PageProtectionFilter, this displays the path of the page to which you are not authorised.
 */
public class PageDenied {

    // Activation context

    @Property
    private String urlDenied;

    // Other useful bits and pieces

    @Inject
    private Response response;

    // The code

    void onActivate(String urlDenied) {
        this.urlDenied = urlDenied;
    }

    String onPassivate() {
        return urlDenied;
    }

    public void setupRender() {
        response.setStatus(HttpServletResponse.SC_NOT_FOUND);
    }

}


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like &nbsp; 
     We use a "strict" DTD to make IE follow the alignment rules. -->
     
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd">
<head>
    <title>Page Denied</title>
</head>
<body>
    <h1>Page Denied</h1>
    
    You are not authorised to page ${urlDenied}.<br/><br/>
    
    <a t:type="pagelink" t:page="Index">Home</a>
</body>
</html>


package jumpstart.web.commons;

import org.apache.tapestry5.Link;

public interface IIntermediatePage {
    
    void setNextPageLink(Link nextPageLink);

}


package jumpstart.web.state.theapp;

import java.io.Serializable;

import jumpstart.business.domain.security.User;
import jumpstart.business.domain.security.User.PageStyle;

@SuppressWarnings("serial")
public class Visit implements Serializable {

    private Long myUserId = null;
    private String myLoginId = null;
    private PageStyle pageStyle = null;
    private String dateInputPattern = null;
    private String dateViewPattern = null;
    private String dateListPattern = null;
    
    public Visit(User user) {
        myUserId = user.getId();
        cacheUsefulStuff(user);
    }

    public void noteChanges(User user) {
        if (user == null) {
            throw new IllegalArgumentException();
        }
        else if (user.getId().equals(myUserId)) {
            cacheUsefulStuff(user);
        }
    }

    private void cacheUsefulStuff(User user) {
        myLoginId = user.getLoginId();
        pageStyle = user.getPageStyle();
        dateInputPattern = user.getDateInputPattern();
        dateViewPattern = user.getDateViewPattern();
        dateListPattern = user.getDateListPattern();
    }

    public Long getMyUserId() {
        return myUserId;
    }

    public String getMyLoginId() {
        return myLoginId;
    }

    public PageStyle getPageStyle() {
        return pageStyle;
    }

    public String getDateInputPattern() {
        return dateInputPattern;
    }

    public String getDateViewPattern() {
        return dateViewPattern;
    }

    public String getDateListPattern() {
        return dateListPattern;
    }

}


package jumpstart.web.services;

import java.util.Map;

import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.Translator;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.beanvalidator.ClientConstraintDescriptor;
import org.apache.tapestry5.internal.beanvalidator.BaseCCD;
import org.apache.tapestry5.ioc.Configuration;
import org.apache.tapestry5.ioc.MappedConfiguration;
import org.apache.tapestry5.ioc.OrderedConfiguration;
import org.apache.tapestry5.ioc.ServiceBinder;
import org.apache.tapestry5.ioc.annotations.EagerLoad;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Primary;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
import org.apache.tapestry5.ioc.services.Coercion;
import org.apache.tapestry5.ioc.services.CoercionTuple;
import org.apache.tapestry5.ioc.services.ThreadLocale;
import org.apache.tapestry5.services.BeanBlockContribution;
import org.apache.tapestry5.services.ComponentRequestFilter;
import org.apache.tapestry5.services.DisplayBlockContribution;
import org.apache.tapestry5.services.EditBlockContribution;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.javascript.DataConstants;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;
import org.apache.tapestry5.services.security.WhitelistAnalyzer;
import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
import org.apache.tapestry5.upload.services.UploadSymbols;
import org.joda.time.DateMidnight;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.LocalTime;
import org.slf4j.Logger;

import jumpstart.business.validation.constraints.Letters;
import jumpstart.util.JodaTimeUtil;
import jumpstart.web.translators.MoneyTranslator;
import jumpstart.web.translators.YesNoTranslator;

/**
 * This module is automatically included as part of the Tapestry IoC Registry, it's a good place to configure and extend
 * Tapestry, or to place your own service definitions. See http://tapestry.apache.org/5.3.4/tapestry-ioc/module.html
 */
public class AppModule {
    private static final String UPLOADS_PATH = "jumpstart.upload-path";

    @Inject
    @Symbol(SymbolConstants.PRODUCTION_MODE)
    @Property(write = false)
    private static boolean productionMode;

    // Add 2 services to those provided by Tapestry.
    // - CountryNames, and SelectIdModelFactory are used by pages which ask Tapestry to @Inject them.

    public static void bind(ServiceBinder binder) {
        binder.bind(CountryNames.class);
        binder.bind(SelectIdModelFactory.class, SelectIdModelFactoryImpl.class);
    }

    // Tell Tapestry about our custom translators and their message file.
    // We do this by contributing configuration to Tapestry's TranslatorAlternatesSource service, FieldValidatorSource
    // service, and ComponentMessagesSource service.

    @SuppressWarnings("rawtypes")
    public static void contributeTranslatorAlternatesSource(MappedConfiguration<String, Translator> configuration,
            ThreadLocale threadLocale) {
        configuration.add("yesno", new YesNoTranslator("yesno"));
        configuration.add("money2", new MoneyTranslator("money2", 2, threadLocale));
    }

    public void contributeComponentMessagesSource(OrderedConfiguration<String> configuration) {
        configuration.add("myTranslationMessages", "jumpstart/web/translators/TranslationMessages");
    }

    // Tell Tapestry about the client-side (javascript) validators that corresponds to each server-side Bean Validator.

    public static void contributeClientConstraintDescriptorSource(final JavaScriptSupport javaScriptSupport,
            final Configuration<ClientConstraintDescriptor> configuration) {

        configuration.add(new BaseCCD(Letters.class) {

            public void applyClientValidation(MarkupWriter writer, String message, Map<String, Object> attributes) {
                javaScriptSupport.require("beanvalidation/letters");
                writer.attributes(DataConstants.VALIDATION_ATTRIBUTE, true, "data-validate-letters", true,
                        "data-letters-message", message);
            }

        });

    }

    // Tell Tapestry about our custom ValueEncoders.
    // We do this by contributing configuration to Tapestry's ValueEncoderSource service.

    // @SuppressWarnings("rawtypes")
    // public static void contributeValueEncoderSource(MappedConfiguration<Class, Object> configuration) {
    // configuration.addInstance(Person.class, PersonEncoder.class);
    // }

    // Tell Tapestry which locales we support, and tell Tapestry to use its jQuery implementation for its JavaScript.
    // We do this by contributing configuration to Tapestry's ApplicationDefaults service.

    public static void contributeApplicationDefaults(MappedConfiguration<String, String> configuration) {
        configuration.add(SymbolConstants.SUPPORTED_LOCALES, "en_US,en_GB,fr");
        configuration.add(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER, "jquery");
    }

    // Tell Tapestry how to detect and protect pages that require security.
    // We do this by contributing a custom ComponentRequestFilter to Tapestry's ComponentRequestHandler service.
    // - ComponentRequestHandler is shown in
    // http://tapestry.apache.org/request-processing.html#RequestProcessing-Overview
    // - Based on http://tapestryjava.blogspot.com/2009/12/securing-tapestry-pages-with.html

    public void contributeComponentRequestHandler(OrderedConfiguration<ComponentRequestFilter> configuration) {
        configuration.addInstance("PageProtectionFilter", PageProtectionFilter.class);
    }

    // Tell Tapestry how to handle WildFly 11's classpath URLs - WildFly uses a "virtual file system".
    // We do this by overriding Tapestry's ClasspathURLConverter service.
    // See "Running Tapestry on JBoss" (sic) in http://wiki.apache.org/tapestry/Tapestry5HowTos .

    @SuppressWarnings("rawtypes")
    public static void contributeServiceOverride(MappedConfiguration<Class, Object> configuration) {
        // configuration.add(ClasspathURLConverter.class, new ClasspathURLConverterJBoss7());
        configuration.add(ClasspathURLConverter.class, new ClasspathURLConverterWildFly11());
    }

    // Tell Tapestry how to handle @EJB in page and component classes.
    // We do this by contributing configuration to Tapestry's ComponentClassTransformWorker service.
    // - Based on http://wiki.apache.org/tapestry/JEE-Annotation.

    @Primary
    public static void contributeComponentClassTransformWorker(
            OrderedConfiguration<ComponentClassTransformWorker2> configuration) {
        configuration.addInstance("EJB", EJBAnnotationWorker.class, "before:Property");
    }

    // Tell Tapestry how to handle pages annotated with @WhitelistAccessOnly, eg. Tapestry's ServiceStatus and
    // PageCatalog.
    // The default WhitelistAnalyzer allows localhost only and only in non-production mode.
    // Our aim is to make the servicestatus page available to ALL clients when not in production mode.
    // We do this by contributing our own WhitelistAnalyzer to Tapestry's ClientWhitelist service.

    public static void contributeClientWhitelist(OrderedConfiguration<WhitelistAnalyzer> configuration) {
        if (!productionMode) {
            configuration.add("NonProductionWhitelistAnalyzer", new WhitelistAnalyzer() {
                @Override
                public boolean isRequestOnWhitelist(Request request) {
                    if (request.getPath().startsWith("/core/servicestatus")) {
                        return true;
                    }
                    else {
                        // This is copied from org.apache.tapestry5.internal.services.security.LocalhostOnly
                        String remoteHost = request.getRemoteHost();
                        return remoteHost.equals("localhost") || remoteHost.equals("127.0.0.1")
                                || remoteHost.equals("0:0:0:0:0:0:0:1%0") || remoteHost.equals("0:0:0:0:0:0:0:1");
                    }
                }
            }, "before:*");
        }
    }

    // Tell Tapestry how to build our Filer service (used in the FileUpload example).
    // Annotate it with EagerLoad to force resolution of symbols at startup rather than when it is first used.

    @EagerLoad
    public static IFiler buildFiler(Logger logger, @Inject @Symbol(UPLOADS_PATH) final String uploadsPath,
            @Inject @Symbol(UploadSymbols.FILESIZE_MAX) final long fileSizeMax) {
        return new Filer(logger, UPLOADS_PATH, uploadsPath, UploadSymbols.FILESIZE_MAX, fileSizeMax);
    }

    // Tell Tapestry how to coerce Joda Time types to and from Java Date types for the TypeCoercers example.
    // We do this by contributing configuration to Tapestry's TypeCoercer service.
    // - Based on http://tapestry.apache.org/typecoercer-service.html

    @SuppressWarnings("rawtypes")
    public static void contributeTypeCoercer(Configuration<CoercionTuple> configuration) {

        // From java.util.Date to DateMidnight

        Coercion<java.util.Date, DateMidnight> toDateMidnight = new Coercion<java.util.Date, DateMidnight>() {
            public DateMidnight coerce(java.util.Date input) {
                // TODO - confirm this conversion always works, esp. across timezones
                return JodaTimeUtil.toDateMidnight(input);
            }
        };

        configuration.add(new CoercionTuple<>(java.util.Date.class, DateMidnight.class, toDateMidnight));

        // From DateMidnight to java.util.Date

        Coercion<DateMidnight, java.util.Date> fromDateMidnight = new Coercion<DateMidnight, java.util.Date>() {
            public java.util.Date coerce(DateMidnight input) {
                // TODO - confirm this conversion always works, esp. across timezones
                return JodaTimeUtil.toJavaDate(input);
            }
        };

        configuration.add(new CoercionTuple<>(DateMidnight.class, java.util.Date.class, fromDateMidnight));

        // From java.util.Date to LocalDate

        Coercion<java.util.Date, LocalDate> toLocalDate = new Coercion<java.util.Date, LocalDate>() {
            public LocalDate coerce(java.util.Date input) {
                // TODO - confirm this conversion always works, esp. across timezones
                return JodaTimeUtil.toLocalDate(input);
            }
        };

        configuration.add(new CoercionTuple<>(java.util.Date.class, LocalDate.class, toLocalDate));

        // From LocalDate to java.util.Date

        Coercion<LocalDate, java.util.Date> fromLocalDate = new Coercion<LocalDate, java.util.Date>() {
            public java.util.Date coerce(LocalDate input) {
                // TODO - confirm this conversion always works, esp. across timezones
                return JodaTimeUtil.toJavaDate(input);
            }
        };

        configuration.add(new CoercionTuple<>(LocalDate.class, java.util.Date.class, fromLocalDate));
    }

    // Tell Tapestry how its BeanDisplay and BeanEditor can handle the JodaTime types.
    // We do this by contributing configuration to Tapestry's DefaultDataTypeAnalyzer and BeanBlockSource services.
    // - Based on http://tapestry.apache.org/beaneditform-guide.html .

    public static void contributeDefaultDataTypeAnalyzer(
            @SuppressWarnings("rawtypes") MappedConfiguration<Class, String> configuration) {
        configuration.add(DateTime.class, "dateTime");
        configuration.add(DateMidnight.class, "dateMidnight");
        configuration.add(LocalDateTime.class, "localDateTime");
        configuration.add(LocalDate.class, "localDate");
        configuration.add(LocalTime.class, "localTime");
    }

    public static void contributeBeanBlockSource(Configuration<BeanBlockContribution> configuration) {

        configuration.add(new DisplayBlockContribution("dateTime", "infra/AppPropertyDisplayBlocks", "dateTime"));
        configuration
                .add(new DisplayBlockContribution("dateMidnight", "infra/AppPropertyDisplayBlocks", "dateMidnight"));
        configuration
                .add(new DisplayBlockContribution("localDateTime", "infra/AppPropertyDisplayBlocks", "localDateTime"));
        configuration.add(new DisplayBlockContribution("localDate", "infra/AppPropertyDisplayBlocks", "localDate"));
        configuration.add(new DisplayBlockContribution("localTime", "infra/AppPropertyDisplayBlocks", "localTime"));

        configuration.add(new EditBlockContribution("dateMidnight", "infra/AppPropertyEditBlocks", "dateMidnight"));
        configuration.add(new EditBlockContribution("localDate", "infra/AppPropertyEditBlocks", "localDate"));

    }

}