org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping.java

Source

/*
 * Copyright 2012-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.rest.webmvc;

import lombok.NonNull;
import lombok.RequiredArgsConstructor;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.webmvc.support.JpaHelper;
import org.springframework.data.util.ProxyUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPatternParser;

/**
 * {@link RequestMappingHandlerMapping} implementation that will only find a handler method if a
 * {@link org.springframework.data.repository.Repository} is exported under that URL path segment. Also ensures the
 * {@link OpenEntityManagerInViewInterceptor} is registered in the application context. The OEMIVI is required for the
 * REST exporter to function properly.
 *
 * @author Jon Brisbin
 * @author Oliver Gierke
 * @author Mark Paluch
 */
public class RepositoryRestHandlerMapping extends BasePathAwareHandlerMapping {

    private static final PathPatternParser PARSER = new PathPatternParser();
    static final String EFFECTIVE_LOOKUP_PATH_ATTRIBUTE = RepositoryRestHandlerMapping.class.getName()
            + ".EFFECTIVE_REPOSITORY_RESOURCE_LOOKUP_PATH";

    private final ResourceMappings mappings;
    private final RepositoryRestConfiguration configuration;
    private final Optional<Repositories> repositories;

    private RepositoryCorsConfigurationAccessor corsConfigurationAccessor;
    private Optional<JpaHelper> jpaHelper = Optional.empty();

    /**
     * Creates a new {@link RepositoryRestHandlerMapping} for the given {@link ResourceMappings} and
     * {@link RepositoryRestConfiguration}.
     *
     * @param mappings must not be {@literal null}.
     * @param config must not be {@literal null}.
     */
    public RepositoryRestHandlerMapping(ResourceMappings mappings, RepositoryRestConfiguration config) {
        this(mappings, config, Optional.empty());
    }

    /**
     * Creates a new {@link RepositoryRestHandlerMapping} for the given {@link ResourceMappings}
     * {@link RepositoryRestConfiguration} and {@link Repositories}.
     *
     * @param mappings must not be {@literal null}.
     * @param config must not be {@literal null}.
     * @param repositories must not be {@literal null}.
     */
    public RepositoryRestHandlerMapping(ResourceMappings mappings, RepositoryRestConfiguration config,
            Repositories repositories) {

        this(mappings, config, Optional.of(repositories));
    }

    private RepositoryRestHandlerMapping(ResourceMappings mappings, RepositoryRestConfiguration config,
            Optional<Repositories> repositories) {

        super(config);

        Assert.notNull(mappings, "ResourceMappings must not be null!");
        Assert.notNull(config, "RepositoryRestConfiguration must not be null!");
        Assert.notNull(repositories, "Repositories must not be null!");

        this.mappings = mappings;
        this.configuration = config;
        this.repositories = repositories;
        this.corsConfigurationAccessor = new RepositoryCorsConfigurationAccessor(mappings,
                NoOpStringValueResolver.INSTANCE, repositories);
    }

    /**
     * @param jpaHelper the jpaHelper to set
     */
    public void setJpaHelper(JpaHelper jpaHelper) {
        this.jpaHelper = Optional.ofNullable(jpaHelper);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#setEmbeddedValueResolver(org.springframework.util.StringValueResolver)
     */
    @Override
    public void setEmbeddedValueResolver(StringValueResolver resolver) {

        super.setEmbeddedValueResolver(resolver);

        this.corsConfigurationAccessor = new RepositoryCorsConfigurationAccessor(mappings,
                resolver == null ? NoOpStringValueResolver.INSTANCE : resolver, repositories);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod(java.lang.String, javax.servlet.http.HttpServletRequest)
     */
    @Override
    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {

        HandlerMethod handlerMethod = super.lookupHandlerMethod(lookupPath, request);

        if (handlerMethod == null) {
            return null;
        }

        String repositoryLookupPath = new BaseUri(configuration.getBaseUri()).getRepositoryLookupPath(lookupPath);

        // Repository root resource
        if (!StringUtils.hasText(repositoryLookupPath)) {
            return handlerMethod;
        }

        String repositoryBasePath = getRepositoryBasePath(repositoryLookupPath);

        if (!mappings.exportsTopLevelResourceFor(repositoryBasePath)) {
            return null;
        }

        exposeEffectiveLookupPathKey(handlerMethod, request, repositoryBasePath);

        return handlerMethod;
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping#handleNoMatch(java.util.Set, java.lang.String, javax.servlet.http.HttpServletRequest)
     */
    @Override
    protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> requestMappingInfos, String lookupPath,
            HttpServletRequest request) throws ServletException {
        return null;
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#isHandler(java.lang.Class)
     */
    @Override
    protected boolean isHandler(Class<?> beanType) {

        Class<?> type = ProxyUtils.getUserClass(beanType);

        return AnnotationUtils.findAnnotation(type, RepositoryRestController.class) != null;
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.web.servlet.handler.AbstractHandlerMapping#extendInterceptors(java.util.List)
     */
    @Override
    protected void extendInterceptors(List<Object> interceptors) {

        jpaHelper.map(JpaHelper::getInterceptors) //
                .orElseGet(() -> Collections.emptyList()) //
                .forEach(interceptors::add);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.rest.webmvc.BasePathAwareHandlerMapping#process(org.springframework.web.servlet.mvc.condition.ProducesRequestCondition)
     */
    @Override
    protected ProducesRequestCondition customize(ProducesRequestCondition condition) {

        if (!condition.isEmpty()) {
            return condition;
        }

        HashSet<String> mediaTypes = new LinkedHashSet<String>();
        mediaTypes.add(configuration.getDefaultMediaType().toString());
        mediaTypes.add(MediaType.APPLICATION_JSON_VALUE);

        return new ProducesRequestCondition(mediaTypes.toArray(new String[mediaTypes.size()]));
    }

    /* (non-Javadoc)
     * @see org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getCorsConfiguration(java.lang.Object, javax.servlet.http.HttpServletRequest)
     */
    @Override
    protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) {

        String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
        String repositoryLookupPath = new BaseUri(configuration.getBaseUri()).getRepositoryLookupPath(lookupPath);
        CorsConfiguration corsConfiguration = super.getCorsConfiguration(handler, request);

        return repositories.filter(it -> StringUtils.hasText(repositoryLookupPath))//
                .flatMap(it -> corsConfigurationAccessor.findCorsConfiguration(repositoryLookupPath))
                .map(it -> it.combine(corsConfiguration))//
                .orElse(corsConfiguration);
    }

    /**
     * Returns the first segment of the given repository lookup path.
     *
     * @param repositoryLookupPath must not be {@literal null}.
     * @return
     */
    private static String getRepositoryBasePath(String repositoryLookupPath) {

        int secondSlashIndex = repositoryLookupPath.indexOf('/', repositoryLookupPath.startsWith("/") ? 1 : 0);
        return secondSlashIndex == -1 ? repositoryLookupPath : repositoryLookupPath.substring(0, secondSlashIndex);
    }

    /**
     * Exposes the effective repository resource lookup path as request attribute via
     * {@link #EFFECTIVE_LOOKUP_PATH_ATTRIBUTE}, i.e. {@code /people/search/\{search\}} instead of
     * {@code /\{repository\}/search/\{search\}}.
     *
     * @param method must not be {@literal null}.
     * @param request must not be {@literal null}.
     * @param repositoryBasePath must not be {@literal null}.
     */
    private void exposeEffectiveLookupPathKey(HandlerMethod method, HttpServletRequest request,
            String repositoryBasePath) {

        RequestMappingInfo mappingInfo = getMappingForMethod(method.getMethod(), method.getBeanType());

        if (mappingInfo == null) {
            return;
        }

        String pattern = mappingInfo.getPatternsCondition() //
                .getMatchingCondition(request)//
                .getPatterns() //
                .iterator().next();

        request.setAttribute(EFFECTIVE_LOOKUP_PATH_ATTRIBUTE,
                PARSER.parse(pattern.replace("/{repository}", repositoryBasePath)));
    }

    /**
     * No-op {@link StringValueResolver} that returns the given {@link String} value as is.
     *
     * @author Oliver Gierke
     * @since 2.6
     */
    enum NoOpStringValueResolver implements StringValueResolver {

        INSTANCE;

        /*
         * (non-Javadoc)
         * @see org.springframework.util.StringValueResolver#resolveStringValue(java.lang.String)
         */
        @Override
        public String resolveStringValue(String value) {
            return value;
        }
    }

    /**
     * Accessor to obtain {@link CorsConfiguration} for exposed repositories.
     * <p>
     * Exported repository classes can be annotated with {@link CrossOrigin} to configure CORS for a specific repository.
     *
     * @author Mark Paluch
     * @author Oliver Gierke
     * @since 2.6
     */
    @RequiredArgsConstructor
    static class RepositoryCorsConfigurationAccessor {

        private final @NonNull ResourceMappings mappings;
        private final @NonNull StringValueResolver embeddedValueResolver;
        private final @NonNull Optional<Repositories> repositories;

        Optional<CorsConfiguration> findCorsConfiguration(String lookupPath) {

            return getResourceMetadata(getRepositoryBasePath(lookupPath))//
                    .flatMap(it -> repositories.flatMap(foo -> foo.getRepositoryInformationFor(it.getDomainType())))//
                    .map(it -> it.getRepositoryInterface())//
                    .map(it -> createConfiguration(it));
        }

        private Optional<ResourceMetadata> getResourceMetadata(String basePath) {

            if (!mappings.exportsTopLevelResourceFor(basePath)) {
                return Optional.empty();
            }

            return mappings.stream()//
                    .filter(it -> it.getPath().matches(basePath) && it.isExported())//
                    .findFirst();
        }

        /**
         * Creates {@link CorsConfiguration} from a repository interface.
         *
         * @param repositoryInterface the repository interface
         * @return {@link CorsConfiguration} or {@literal null}.
         */
        protected CorsConfiguration createConfiguration(Class<?> repositoryInterface) {

            CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(repositoryInterface,
                    CrossOrigin.class);

            if (typeAnnotation == null) {
                return null;
            }

            CorsConfiguration config = new CorsConfiguration();
            updateCorsConfig(config, typeAnnotation);

            if (CollectionUtils.isEmpty(config.getAllowedOrigins())) {
                config.setAllowedOrigins(Arrays.asList(CrossOrigin.DEFAULT_ORIGINS));
            }

            if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
                for (HttpMethod httpMethod : HttpMethod.values()) {
                    config.addAllowedMethod(httpMethod);
                }
            }

            if (CollectionUtils.isEmpty(config.getAllowedHeaders())) {
                config.setAllowedHeaders(Arrays.asList(CrossOrigin.DEFAULT_ALLOWED_HEADERS));
            }

            if (config.getAllowCredentials() == null) {
                config.setAllowCredentials(CrossOrigin.DEFAULT_ALLOW_CREDENTIALS);
            }

            if (config.getMaxAge() == null) {
                config.setMaxAge(CrossOrigin.DEFAULT_MAX_AGE);
            }

            return config;
        }

        private void updateCorsConfig(CorsConfiguration config, CrossOrigin annotation) {

            for (String origin : annotation.origins()) {
                config.addAllowedOrigin(resolveCorsAnnotationValue(origin));
            }

            for (RequestMethod method : annotation.methods()) {
                config.addAllowedMethod(method.name());
            }

            for (String header : annotation.allowedHeaders()) {
                config.addAllowedHeader(resolveCorsAnnotationValue(header));
            }

            for (String header : annotation.exposedHeaders()) {
                config.addExposedHeader(resolveCorsAnnotationValue(header));
            }

            String allowCredentials = resolveCorsAnnotationValue(annotation.allowCredentials());

            if ("true".equalsIgnoreCase(allowCredentials)) {
                config.setAllowCredentials(true);
            } else if ("false".equalsIgnoreCase(allowCredentials)) {
                config.setAllowCredentials(false);
            } else if (!allowCredentials.isEmpty()) {
                throw new IllegalStateException(
                        "@CrossOrigin's allowCredentials value must be \"true\", \"false\", "
                                + "or an empty string (\"\"): current value is [" + allowCredentials + "]");
            }

            if (annotation.maxAge() >= 0 && config.getMaxAge() == null) {
                config.setMaxAge(annotation.maxAge());
            }
        }

        private String resolveCorsAnnotationValue(String value) {
            return this.embeddedValueResolver.resolveStringValue(value);
        }
    }
}