001/*
002 * Copyright 2012-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.autoconfigure.security.servlet;
018
019import java.util.EnumSet;
020import java.util.LinkedHashSet;
021import java.util.List;
022import java.util.Set;
023import java.util.function.Supplier;
024import java.util.stream.Collectors;
025import java.util.stream.Stream;
026
027import javax.servlet.http.HttpServletRequest;
028
029import org.springframework.boot.autoconfigure.security.StaticResourceLocation;
030import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath;
031import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher;
032import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
033import org.springframework.security.web.util.matcher.OrRequestMatcher;
034import org.springframework.security.web.util.matcher.RequestMatcher;
035import org.springframework.util.Assert;
036
037/**
038 * Used to create a {@link RequestMatcher} for static resources in commonly used
039 * locations. Returned by {@link PathRequest#toStaticResources()}.
040 *
041 * @author Madhura Bhave
042 * @author Phillip Webb
043 * @since 2.0.0
044 * @see PathRequest
045 */
046public final class StaticResourceRequest {
047
048        static final StaticResourceRequest INSTANCE = new StaticResourceRequest();
049
050        private StaticResourceRequest() {
051        }
052
053        /**
054         * Returns a matcher that includes all commonly used {@link StaticResourceLocation
055         * Locations}. The
056         * {@link StaticResourceRequestMatcher#excluding(StaticResourceLocation, StaticResourceLocation...)
057         * excluding} method can be used to remove specific locations if required. For
058         * example: <pre class="code">
059         * PathRequest.toStaticResources().atCommonLocations().excluding(StaticResourceLocation.CSS)
060         * </pre>
061         * @return the configured {@link RequestMatcher}
062         */
063        public StaticResourceRequestMatcher atCommonLocations() {
064                return at(EnumSet.allOf(StaticResourceLocation.class));
065        }
066
067        /**
068         * Returns a matcher that includes the specified {@link StaticResourceLocation
069         * Locations}. For example: <pre class="code">
070         * PathRequest.toStaticResources().at(StaticResourceLocation.CSS, StaticResourceLocation.JAVA_SCRIPT)
071         * </pre>
072         * @param first the first location to include
073         * @param rest additional locations to include
074         * @return the configured {@link RequestMatcher}
075         */
076        public StaticResourceRequestMatcher at(StaticResourceLocation first,
077                        StaticResourceLocation... rest) {
078                return at(EnumSet.of(first, rest));
079        }
080
081        /**
082         * Returns a matcher that includes the specified {@link StaticResourceLocation
083         * Locations}. For example: <pre class="code">
084         * PathRequest.toStaticResources().at(locations)
085         * </pre>
086         * @param locations the locations to include
087         * @return the configured {@link RequestMatcher}
088         */
089        public StaticResourceRequestMatcher at(Set<StaticResourceLocation> locations) {
090                Assert.notNull(locations, "Locations must not be null");
091                return new StaticResourceRequestMatcher(new LinkedHashSet<>(locations));
092        }
093
094        /**
095         * The request matcher used to match against resource {@link StaticResourceLocation
096         * Locations}.
097         */
098        public static final class StaticResourceRequestMatcher
099                        extends ApplicationContextRequestMatcher<DispatcherServletPath> {
100
101                private final Set<StaticResourceLocation> locations;
102
103                private volatile RequestMatcher delegate;
104
105                private StaticResourceRequestMatcher(Set<StaticResourceLocation> locations) {
106                        super(DispatcherServletPath.class);
107                        this.locations = locations;
108                }
109
110                /**
111                 * Return a new {@link StaticResourceRequestMatcher} based on this one but
112                 * excluding the specified locations.
113                 * @param first the first location to exclude
114                 * @param rest additional locations to exclude
115                 * @return a new {@link StaticResourceRequestMatcher}
116                 */
117                public StaticResourceRequestMatcher excluding(StaticResourceLocation first,
118                                StaticResourceLocation... rest) {
119                        return excluding(EnumSet.of(first, rest));
120                }
121
122                /**
123                 * Return a new {@link StaticResourceRequestMatcher} based on this one but
124                 * excluding the specified locations.
125                 * @param locations the locations to exclude
126                 * @return a new {@link StaticResourceRequestMatcher}
127                 */
128                public StaticResourceRequestMatcher excluding(
129                                Set<StaticResourceLocation> locations) {
130                        Assert.notNull(locations, "Locations must not be null");
131                        Set<StaticResourceLocation> subset = new LinkedHashSet<>(this.locations);
132                        subset.removeAll(locations);
133                        return new StaticResourceRequestMatcher(subset);
134                }
135
136                @Override
137                protected void initialized(
138                                Supplier<DispatcherServletPath> dispatcherServletPath) {
139                        this.delegate = new OrRequestMatcher(
140                                        getDelegateMatchers(dispatcherServletPath.get()));
141                }
142
143                private List<RequestMatcher> getDelegateMatchers(
144                                DispatcherServletPath dispatcherServletPath) {
145                        return getPatterns(dispatcherServletPath).map(AntPathRequestMatcher::new)
146                                        .collect(Collectors.toList());
147                }
148
149                private Stream<String> getPatterns(DispatcherServletPath dispatcherServletPath) {
150                        return this.locations.stream().flatMap(StaticResourceLocation::getPatterns)
151                                        .map(dispatcherServletPath::getRelativePath);
152                }
153
154                @Override
155                protected boolean matches(HttpServletRequest request,
156                                Supplier<DispatcherServletPath> context) {
157                        return this.delegate.matches(request);
158                }
159
160        }
161
162}