001/*
002 * Copyright 2002-2019 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 *      https://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.web.servlet.resource;
018
019import java.io.IOException;
020import javax.servlet.FilterChain;
021import javax.servlet.ServletException;
022import javax.servlet.ServletRequest;
023import javax.servlet.ServletResponse;
024import javax.servlet.http.HttpServletRequest;
025import javax.servlet.http.HttpServletRequestWrapper;
026import javax.servlet.http.HttpServletResponse;
027import javax.servlet.http.HttpServletResponseWrapper;
028
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031
032import org.springframework.util.Assert;
033import org.springframework.web.filter.GenericFilterBean;
034import org.springframework.web.util.UrlPathHelper;
035
036/**
037 * A filter that wraps the {@link HttpServletResponse} and overrides its
038 * {@link HttpServletResponse#encodeURL(String) encodeURL} method in order to
039 * translate internal resource request URLs into public URL paths for external use.
040 *
041 * @author Jeremy Grelle
042 * @author Rossen Stoyanchev
043 * @author Sam Brannen
044 * @author Brian Clozel
045 * @since 4.1
046 */
047public class ResourceUrlEncodingFilter extends GenericFilterBean {
048
049        private static final Log logger = LogFactory.getLog(ResourceUrlEncodingFilter.class);
050
051
052        @Override
053        public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
054                        throws ServletException, IOException {
055
056                if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
057                        throw new ServletException("ResourceUrlEncodingFilter only supports HTTP requests");
058                }
059                ResourceUrlEncodingRequestWrapper wrappedRequest =
060                                new ResourceUrlEncodingRequestWrapper((HttpServletRequest) request);
061                ResourceUrlEncodingResponseWrapper wrappedResponse =
062                                new ResourceUrlEncodingResponseWrapper(wrappedRequest, (HttpServletResponse) response);
063                filterChain.doFilter(wrappedRequest, wrappedResponse);
064        }
065
066
067        private static class ResourceUrlEncodingRequestWrapper extends HttpServletRequestWrapper {
068
069                private ResourceUrlProvider resourceUrlProvider;
070
071                private Integer indexLookupPath;
072
073                private String prefixLookupPath;
074
075                ResourceUrlEncodingRequestWrapper(HttpServletRequest request) {
076                        super(request);
077                }
078
079                @Override
080                public void setAttribute(String name, Object value) {
081                        super.setAttribute(name, value);
082                        if (ResourceUrlProviderExposingInterceptor.RESOURCE_URL_PROVIDER_ATTR.equals(name)) {
083                                if (value instanceof ResourceUrlProvider) {
084                                        initLookupPath((ResourceUrlProvider) value);
085                                }
086                        }
087                }
088
089                private void initLookupPath(ResourceUrlProvider urlProvider) {
090                        this.resourceUrlProvider = urlProvider;
091                        if (this.indexLookupPath == null) {
092                                UrlPathHelper pathHelper = this.resourceUrlProvider.getUrlPathHelper();
093                                String requestUri = pathHelper.getRequestUri(this);
094                                String lookupPath = pathHelper.getLookupPathForRequest(this);
095                                this.indexLookupPath = requestUri.lastIndexOf(lookupPath);
096                                if (this.indexLookupPath == -1) {
097                                        throw new IllegalStateException(
098                                                        "Failed to find lookupPath '" + lookupPath + "' within requestUri '" + requestUri + "'. " +
099                                                        "Does the path have invalid encoded characters for characterEncoding '" +
100                                                        getRequest().getCharacterEncoding() + "'?");
101                                }
102                                this.prefixLookupPath = requestUri.substring(0, this.indexLookupPath);
103                                if ("/".equals(lookupPath) && !"/".equals(requestUri)) {
104                                        String contextPath = pathHelper.getContextPath(this);
105                                        if (requestUri.equals(contextPath)) {
106                                                this.indexLookupPath = requestUri.length();
107                                                this.prefixLookupPath = requestUri;
108                                        }
109                                }
110                        }
111                }
112
113                public String resolveUrlPath(String url) {
114                        if (this.resourceUrlProvider == null) {
115                                logger.trace("ResourceUrlProvider not available via request attribute " +
116                                                ResourceUrlProviderExposingInterceptor.RESOURCE_URL_PROVIDER_ATTR);
117                                return null;
118                        }
119                        if (this.indexLookupPath != null && url.startsWith(this.prefixLookupPath)) {
120                                int suffixIndex = getEndPathIndex(url);
121                                String suffix = url.substring(suffixIndex);
122                                String lookupPath = url.substring(this.indexLookupPath, suffixIndex);
123                                lookupPath = this.resourceUrlProvider.getForLookupPath(lookupPath);
124                                if (lookupPath != null) {
125                                        return this.prefixLookupPath + lookupPath + suffix;
126                                }
127                        }
128                        return null;
129                }
130
131                private int getEndPathIndex(String path) {
132                        int end = path.indexOf('?');
133                        int fragmentIndex = path.indexOf('#');
134                        if (fragmentIndex != -1 && (end == -1 || fragmentIndex < end)) {
135                                end = fragmentIndex;
136                        }
137                        if (end == -1) {
138                                end = path.length();
139                        }
140                        return end;
141                }
142        }
143
144
145        private static class ResourceUrlEncodingResponseWrapper extends HttpServletResponseWrapper {
146
147                private final ResourceUrlEncodingRequestWrapper request;
148
149                ResourceUrlEncodingResponseWrapper(ResourceUrlEncodingRequestWrapper request, HttpServletResponse wrapped) {
150                        super(wrapped);
151                        this.request = request;
152                }
153
154                @Override
155                public String encodeURL(String url) {
156                        String urlPath = this.request.resolveUrlPath(url);
157                        if (urlPath != null) {
158                                return super.encodeURL(urlPath);
159                        }
160                        return super.encodeURL(url);
161                }
162        }
163
164}