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}