alfio.config.MvcConfiguration.java Source code

Java tutorial

Introduction

Here is the source code for alfio.config.MvcConfiguration.java

Source

/**
 * This file is part of alf.io.
 *
 * alf.io is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * alf.io is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with alf.io.  If not, see <http://www.gnu.org/licenses/>.
 */
package alfio.config;

import alfio.controller.decorator.EventDescriptor;
import alfio.manager.i18n.I18nManager;
import alfio.manager.system.ConfigurationManager;
import alfio.model.ContentLanguage;
import alfio.model.Event;
import alfio.model.system.Configuration.ConfigurationPathKey;
import alfio.util.MustacheCustomTagInterceptor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.samskivert.mustache.Mustache;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
import org.springframework.web.servlet.*;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.servlet.view.mustache.MustacheViewResolver;
import org.springframework.web.servlet.view.mustache.jmustache.JMustacheTemplateFactory;
import org.springframework.web.servlet.view.mustache.jmustache.JMustacheTemplateLoader;
import org.springframework.web.servlet.view.mustache.jmustache.LocalizationMessageInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static alfio.model.system.ConfigurationKeys.*;

@Configuration
@ComponentScan(basePackages = { "alfio.controller", "alfio.config" })
@EnableWebMvc
public class MvcConfiguration extends WebMvcConfigurerAdapter {

    private final MessageSource messageSource;
    private final JMustacheTemplateLoader templateLoader;
    private final I18nManager i18nManager;
    private final ConfigurationManager configurationManager;
    private final Environment environment;

    @Autowired
    public MvcConfiguration(MessageSource messageSource, JMustacheTemplateLoader templateLoader,
            I18nManager i18nManager, ConfigurationManager configurationManager, Environment environment) {
        this.messageSource = messageSource;
        this.templateLoader = templateLoader;
        this.i18nManager = i18nManager;
        this.configurationManager = configurationManager;
        this.environment = environment;
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        ResourceHandlerRegistration reg = registry.addResourceHandler("/resources/**")
                .addResourceLocations("/resources/");
        int cacheMinutes = environment.acceptsProfiles(Initializer.PROFILE_LIVE) ? 15 : 0;
        reg.setCachePeriod(cacheMinutes * 60);
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/admin/partials/index.html").setViewName("/admin/partials/main");
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(getLocaleChangeInterceptor());
        registry.addInterceptor(getEventLocaleSetterInterceptor());
        registry.addInterceptor(getTemplateMessagesInterceptor());
        registry.addInterceptor(new MustacheCustomTagInterceptor());
        registry.addInterceptor(getCsrfInterceptor());
        registry.addInterceptor(getCSPInterceptor());
        registry.addInterceptor(getDefaultTemplateObjectsFiller());
    }

    @Bean
    public HandlerInterceptor getEventLocaleSetterInterceptor() {
        return new HandlerInterceptorAdapter() {
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
                    throws Exception {

                if (handler instanceof HandlerMethod) {
                    HandlerMethod handlerMethod = ((HandlerMethod) handler);
                    RequestMapping reqMapping = handlerMethod.getMethodAnnotation(RequestMapping.class);

                    //check if the request mapping value has the form "/event/{something}"
                    Pattern eventPattern = Pattern.compile("^/event/\\{(\\w+)}/{0,1}.*");
                    if (reqMapping != null && reqMapping.value().length == 1
                            && eventPattern.matcher(reqMapping.value()[0]).matches()) {

                        Matcher m = eventPattern.matcher(reqMapping.value()[0]);
                        m.matches();

                        String pathVariableName = m.group(1);

                        //extract the parameter name
                        Arrays.stream(handlerMethod.getMethodParameters())
                                .map(methodParameter -> methodParameter.getParameterAnnotation(PathVariable.class))
                                .filter(Objects::nonNull).map(PathVariable::value).filter(pathVariableName::equals)
                                .findFirst().ifPresent((val) -> {

                                    //fetch the parameter value
                                    @SuppressWarnings("unchecked")
                                    String eventName = Optional.ofNullable(((Map<String, Object>) request
                                            .getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE))
                                                    .get(val))
                                            .orElse("").toString();

                                    LocaleResolver resolver = RequestContextUtils.getLocaleResolver(request);
                                    Locale locale = resolver.resolveLocale(request);
                                    List<ContentLanguage> cl = i18nManager.getEventLanguages(eventName);

                                    request.setAttribute("ALFIO_EVENT_NAME", eventName);

                                    if (cl.stream().noneMatch(
                                            contentLanguage -> contentLanguage.getLanguage().equals(Optional
                                                    .ofNullable(locale).orElse(Locale.ENGLISH).getLanguage()))) {
                                        //override the user locale if it's not in the one permitted by the event
                                        resolver.setLocale(request, response, cl.stream().findFirst()
                                                .map(ContentLanguage::getLocale).orElse(Locale.ENGLISH));
                                    } else {
                                        resolver.setLocale(request, response, locale);
                                    }
                                });
                    }
                }
                return true;
            }
        };
    }

    @Bean
    public HandlerInterceptorAdapter getDefaultTemplateObjectsFiller() {
        return new HandlerInterceptorAdapter() {
            @Override
            public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                    ModelAndView modelAndView) throws Exception {
                Optional.ofNullable(modelAndView)
                        .filter(mv -> !StringUtils.startsWith(mv.getViewName(), "redirect:")).ifPresent(mv -> {
                            mv.addObject("request", request);
                            final ModelMap modelMap = mv.getModelMap();

                            boolean demoModeEnabled = environment.acceptsProfiles(Initializer.PROFILE_DEMO);

                            modelMap.put("demoModeEnabled", demoModeEnabled);

                            Optional.ofNullable(request.getAttribute("ALFIO_EVENT_NAME")).map(Object::toString)
                                    .ifPresent(eventName -> {

                                        List<?> availableLanguages = i18nManager.getEventLanguages(eventName);

                                        modelMap.put("showAvailableLanguagesInPageTop",
                                                availableLanguages.size() > 1);
                                        modelMap.put("availableLanguages", availableLanguages);
                                    });

                            modelMap.putIfAbsent("event", null);
                            modelMap.putIfAbsent("pageTitle", "empty");
                            Event event = modelMap.get("event") == null ? null
                                    : modelMap.get("event") instanceof Event ? (Event) modelMap.get("event")
                                            : ((EventDescriptor) modelMap.get("event")).getEvent();
                            ConfigurationPathKey googleAnalyticsKey = Optional.ofNullable(event)
                                    .map(e -> alfio.model.system.Configuration.from(e.getOrganizationId(),
                                            e.getId(), GOOGLE_ANALYTICS_KEY))
                                    .orElseGet(() -> alfio.model.system.Configuration
                                            .getSystemConfiguration(GOOGLE_ANALYTICS_KEY));
                            modelMap.putIfAbsent("analyticsEnabled", StringUtils
                                    .isNotBlank(configurationManager.getStringConfigValue(googleAnalyticsKey, "")));

                            if (demoModeEnabled) {
                                modelMap.putIfAbsent("paypalTestUsername",
                                        configurationManager.getStringConfigValue(alfio.model.system.Configuration
                                                .getSystemConfiguration(PAYPAL_DEMO_MODE_USERNAME), "<missing>"));
                                modelMap.putIfAbsent("paypalTestPassword",
                                        configurationManager.getStringConfigValue(alfio.model.system.Configuration
                                                .getSystemConfiguration(PAYPAL_DEMO_MODE_PASSWORD), "<missing>"));
                            }
                        });
            }
        };
    }

    @Bean
    public HandlerInterceptor getLocaleChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        return localeChangeInterceptor;
    }

    private HandlerInterceptor getCSPInterceptor() {
        return new HandlerInterceptorAdapter() {
            @Override
            public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                    ModelAndView modelAndView) throws Exception {
                // http://www.html5rocks.com/en/tutorials/security/content-security-policy/
                // lockdown policy
                response.addHeader("Content-Security-Policy", "default-src 'none'; "//block all by default
                        + " script-src 'self' https://js.stripe.com/ https://api.stripe.com/ https://ssl.google-analytics.com/ https://www.google.com/recaptcha/api.js https://www.gstatic.com/recaptcha/api2/ https://maps.googleapis.com/;"//
                        + " style-src 'self' 'unsafe-inline';" // unsafe-inline for style is acceptable...
                        + " img-src 'self' https: data:;"//
                        + " child-src 'self';"//webworker
                        + " frame-src 'self' https://js.stripe.com https://www.google.com;" + " font-src 'self';"//
                        + " media-src blob: 'self';"//for loading camera api
                        + " connect-src 'self' https://api.stripe.com https://maps.googleapis.com/ https://geocoder.cit.api.here.com;" //<- currently stripe.js use jsonp but if they switch to xmlhttprequest+cors we will be ready
                        + (environment.acceptsProfiles(Initializer.PROFILE_DEBUG_CSP)
                                ? " report-uri /report-csp-violation"
                                : ""));
            }
        };
    }

    @Bean
    public HandlerInterceptor getCsrfInterceptor() {
        return new HandlerInterceptorAdapter() {
            @Override
            public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                    ModelAndView modelAndView) throws Exception {
                Optional.ofNullable(modelAndView).ifPresent(mv -> mv.addObject(WebSecurityConfig.CSRF_PARAM_NAME,
                        request.getAttribute(CsrfToken.class.getName())));
            }
        };
    }

    @Bean
    public LocalizationMessageInterceptor getTemplateMessagesInterceptor() {
        LocalizationMessageInterceptor interceptor = new LocalizationMessageInterceptor();
        interceptor.setLocaleResolver(getLocaleResolver());
        interceptor.setMessageSource(messageSource);
        return interceptor;
    }

    @Bean(name = "localeResolver")
    public LocaleResolver getLocaleResolver() {
        return new SessionLocaleResolver();
    }

    @Bean
    public ViewResolver getViewResolver(Environment env) throws Exception {
        MustacheViewResolver viewResolver = new MustacheViewResolver();
        viewResolver.setSuffix("");
        viewResolver.setTemplateFactory(getTemplateFactory());
        viewResolver.setOrder(1);
        //disable caching if we are in dev mode
        viewResolver.setCache(env.acceptsProfiles(Initializer.PROFILE_LIVE));
        viewResolver.setContentType("text/html;charset=UTF-8");
        return viewResolver;
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(jacksonMessageConverter());
        StringHttpMessageConverter converter = new StringHttpMessageConverter();
        converter.setSupportedMediaTypes(Collections.singletonList(MediaType.ALL));
        converters.add(converter);
    }

    @Bean
    public JMustacheTemplateFactory getTemplateFactory() throws Exception {
        final JMustacheTemplateFactory templateFactory = new JMustacheTemplateFactory();

        templateFactory.setPrefix("/WEB-INF/templates");
        templateFactory.setSuffix(".ms");
        templateFactory.setTemplateLoader(templateLoader);
        templateFactory.setCompiler(Mustache.compiler().escapeHTML(true).standardsMode(false).defaultValue("")
                .nullValue("").withFormatter((o) -> {
                    if (o instanceof ZonedDateTime) {
                        return DateTimeFormatter.ISO_ZONED_DATE_TIME.format((ZonedDateTime) o);
                    } else if (o instanceof DefaultMessageSourceResolvable) {
                        DefaultMessageSourceResolvable m = ((DefaultMessageSourceResolvable) o);
                        return m.getCode() + " "
                                + Arrays.stream(Optional.ofNullable(m.getArguments()).orElse(new Object[] {}))
                                        .map(x -> "[" + x.toString() + "]").collect(Collectors.joining(" "));
                    } else {
                        return String.valueOf(o);
                    }
                }).withLoader(templateLoader));

        templateFactory.afterPropertiesSet();
        return templateFactory;
    }

    @Bean
    public MappingJackson2HttpMessageConverter jacksonMessageConverter() {
        final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        converter.setObjectMapper(objectMapper());
        return converter;
    }

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        return mapper;
    }

    @Bean
    public CommonsMultipartResolver multipartResolver() {
        return new CommonsMultipartResolver();
    }

}