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