001/*
002 * Copyright 2002-2020 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.context.support;
018
019import java.io.IOException;
020import java.util.Enumeration;
021import java.util.LinkedHashSet;
022import java.util.Set;
023import java.util.jar.JarEntry;
024import java.util.jar.JarFile;
025
026import javax.servlet.ServletContext;
027
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030
031import org.springframework.core.io.Resource;
032import org.springframework.core.io.ResourceLoader;
033import org.springframework.core.io.UrlResource;
034import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
035import org.springframework.util.ResourceUtils;
036import org.springframework.util.StringUtils;
037
038/**
039 * ServletContext-aware subclass of {@link PathMatchingResourcePatternResolver},
040 * able to find matching resources below the web application root directory
041 * via {@link ServletContext#getResourcePaths}. Falls back to the superclass'
042 * file system checking for other resources.
043 *
044 * @author Juergen Hoeller
045 * @since 1.1.2
046 */
047public class ServletContextResourcePatternResolver extends PathMatchingResourcePatternResolver {
048
049        private static final Log logger = LogFactory.getLog(ServletContextResourcePatternResolver.class);
050
051
052        /**
053         * Create a new ServletContextResourcePatternResolver.
054         * @param servletContext the ServletContext to load resources with
055         * @see ServletContextResourceLoader#ServletContextResourceLoader(javax.servlet.ServletContext)
056         */
057        public ServletContextResourcePatternResolver(ServletContext servletContext) {
058                super(new ServletContextResourceLoader(servletContext));
059        }
060
061        /**
062         * Create a new ServletContextResourcePatternResolver.
063         * @param resourceLoader the ResourceLoader to load root directories and
064         * actual resources with
065         */
066        public ServletContextResourcePatternResolver(ResourceLoader resourceLoader) {
067                super(resourceLoader);
068        }
069
070
071        /**
072         * Overridden version which checks for ServletContextResource
073         * and uses {@code ServletContext.getResourcePaths} to find
074         * matching resources below the web application root directory.
075         * In case of other resources, delegates to the superclass version.
076         * @see #doRetrieveMatchingServletContextResources
077         * @see ServletContextResource
078         * @see javax.servlet.ServletContext#getResourcePaths
079         */
080        @Override
081        protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
082                        throws IOException {
083
084                if (rootDirResource instanceof ServletContextResource) {
085                        ServletContextResource scResource = (ServletContextResource) rootDirResource;
086                        ServletContext sc = scResource.getServletContext();
087                        String fullPattern = scResource.getPath() + subPattern;
088                        Set<Resource> result = new LinkedHashSet<>(8);
089                        doRetrieveMatchingServletContextResources(sc, fullPattern, scResource.getPath(), result);
090                        return result;
091                }
092                else {
093                        return super.doFindPathMatchingFileResources(rootDirResource, subPattern);
094                }
095        }
096
097        /**
098         * Recursively retrieve ServletContextResources that match the given pattern,
099         * adding them to the given result set.
100         * @param servletContext the ServletContext to work on
101         * @param fullPattern the pattern to match against,
102         * with preprended root directory path
103         * @param dir the current directory
104         * @param result the Set of matching Resources to add to
105         * @throws IOException if directory contents could not be retrieved
106         * @see ServletContextResource
107         * @see javax.servlet.ServletContext#getResourcePaths
108         */
109        protected void doRetrieveMatchingServletContextResources(
110                        ServletContext servletContext, String fullPattern, String dir, Set<Resource> result)
111                        throws IOException {
112
113                Set<String> candidates = servletContext.getResourcePaths(dir);
114                if (candidates != null) {
115                        boolean dirDepthNotFixed = fullPattern.contains("**");
116                        int jarFileSep = fullPattern.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
117                        String jarFilePath = null;
118                        String pathInJarFile = null;
119                        if (jarFileSep > 0 && jarFileSep + ResourceUtils.JAR_URL_SEPARATOR.length() < fullPattern.length()) {
120                                jarFilePath = fullPattern.substring(0, jarFileSep);
121                                pathInJarFile = fullPattern.substring(jarFileSep + ResourceUtils.JAR_URL_SEPARATOR.length());
122                        }
123                        for (String currPath : candidates) {
124                                if (!currPath.startsWith(dir)) {
125                                        // Returned resource path does not start with relative directory:
126                                        // assuming absolute path returned -> strip absolute path.
127                                        int dirIndex = currPath.indexOf(dir);
128                                        if (dirIndex != -1) {
129                                                currPath = currPath.substring(dirIndex);
130                                        }
131                                }
132                                if (currPath.endsWith("/") && (dirDepthNotFixed || StringUtils.countOccurrencesOf(currPath, "/") <=
133                                                StringUtils.countOccurrencesOf(fullPattern, "/"))) {
134                                        // Search subdirectories recursively: ServletContext.getResourcePaths
135                                        // only returns entries for one directory level.
136                                        doRetrieveMatchingServletContextResources(servletContext, fullPattern, currPath, result);
137                                }
138                                if (jarFilePath != null && getPathMatcher().match(jarFilePath, currPath)) {
139                                        // Base pattern matches a jar file - search for matching entries within.
140                                        String absoluteJarPath = servletContext.getRealPath(currPath);
141                                        if (absoluteJarPath != null) {
142                                                doRetrieveMatchingJarEntries(absoluteJarPath, pathInJarFile, result);
143                                        }
144                                }
145                                if (getPathMatcher().match(fullPattern, currPath)) {
146                                        result.add(new ServletContextResource(servletContext, currPath));
147                                }
148                        }
149                }
150        }
151
152        /**
153         * Extract entries from the given jar by pattern.
154         * @param jarFilePath the path to the jar file
155         * @param entryPattern the pattern for jar entries to match
156         * @param result the Set of matching Resources to add to
157         */
158        private void doRetrieveMatchingJarEntries(String jarFilePath, String entryPattern, Set<Resource> result) {
159                if (logger.isDebugEnabled()) {
160                        logger.debug("Searching jar file [" + jarFilePath + "] for entries matching [" + entryPattern + "]");
161                }
162                try (JarFile jarFile = new JarFile(jarFilePath)) {
163                        for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
164                                JarEntry entry = entries.nextElement();
165                                String entryPath = entry.getName();
166                                if (getPathMatcher().match(entryPattern, entryPath)) {
167                                        result.add(new UrlResource(
168                                                        ResourceUtils.URL_PROTOCOL_JAR,
169                                                        ResourceUtils.FILE_URL_PREFIX + jarFilePath + ResourceUtils.JAR_URL_SEPARATOR + entryPath));
170                                }
171                        }
172                }
173                catch (IOException ex) {
174                        if (logger.isWarnEnabled()) {
175                                logger.warn("Cannot search for matching resources in jar file [" + jarFilePath +
176                                                "] because the jar cannot be opened through the file system", ex);
177                        }
178                }
179        }
180
181}