001// Copyright 2012 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// You may obtain a copy of the License at 006// 007// http://www.apache.org/licenses/LICENSE-2.0 008// 009// Unless required by applicable law or agreed to in writing, software 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014 015package org.apache.tapestry5.ioc.internal.services; 016 017import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 018import org.apache.tapestry5.ioc.internal.util.InternalUtils; 019import org.apache.tapestry5.ioc.services.ClasspathMatcher; 020import org.apache.tapestry5.ioc.services.ClasspathScanner; 021import org.apache.tapestry5.ioc.services.ClasspathURLConverter; 022import org.apache.tapestry5.ioc.util.Stack; 023 024import java.io.*; 025import java.net.JarURLConnection; 026import java.net.URL; 027import java.net.URLConnection; 028import java.util.Enumeration; 029import java.util.Set; 030import java.util.jar.JarEntry; 031import java.util.jar.JarFile; 032import java.util.regex.Pattern; 033 034public class ClasspathScannerImpl implements ClasspathScanner 035{ 036 private final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); 037 038 private final ClasspathURLConverter converter; 039 040 private final Pattern FOLDER_NAME_PATTERN = Pattern.compile("^\\p{javaJavaIdentifierStart}[\\p{javaJavaIdentifierPart}]*$", Pattern.CASE_INSENSITIVE); 041 042 043 public ClasspathScannerImpl(ClasspathURLConverter converter) 044 { 045 this.converter = converter; 046 } 047 048 /** 049 * Scans the indicated package path for matches. 050 * 051 * @param packagePath 052 * a package path (like a package name, but using '/' instead of '.', and ending with '/') 053 * @param matcher 054 * passed a resource path from the package (or a sub-package), returns true if the provided 055 * path should be included in the returned collection 056 * @return collection of matching paths, in no specified order 057 * @throws java.io.IOException 058 */ 059 public Set<String> scan(String packagePath, ClasspathMatcher matcher) throws IOException 060 { 061 assert packagePath != null && packagePath.endsWith("/"); 062 assert matcher != null; 063 064 return new Job(matcher).findMatches(packagePath); 065 } 066 067 /** 068 * Check whether container supports opening a stream on a dir/package to get a list of its contents. 069 */ 070 private boolean supportsDirStream(URL packageURL) 071 { 072 InputStream is = null; 073 074 try 075 { 076 is = packageURL.openStream(); 077 078 return true; 079 } catch (FileNotFoundException ex) 080 { 081 return false; 082 } catch (IOException ex) 083 { 084 return false; 085 } finally 086 { 087 InternalUtils.close(is); 088 } 089 } 090 091 /** 092 * For URLs to JARs that do not use JarURLConnection - allowed by the servlet spec - attempt to produce a JarFile 093 * object all the same. Known servlet engines that function like this include Weblogic and OC4J. This is not a full 094 * solution, since an unpacked WAR or EAR will not have JAR "files" as such. 095 * 096 * @param url 097 * URL of jar 098 * @return JarFile or null 099 * @throws java.io.IOException 100 * If error occurs creating jar file 101 */ 102 private JarFile getAlternativeJarFile(URL url) throws IOException 103 { 104 String urlFile = url.getFile(); 105 // Trim off any suffix - which is prefixed by "!/" on Weblogic 106 int separatorIndex = urlFile.indexOf("!/"); 107 108 // OK, didn't find that. Try the less safe "!", used on OC4J 109 if (separatorIndex == -1) 110 { 111 separatorIndex = urlFile.indexOf('!'); 112 } 113 114 if (separatorIndex != -1) 115 { 116 String jarFileUrl = urlFile.substring(0, separatorIndex); 117 // And trim off any "file:" prefix. 118 if (jarFileUrl.startsWith("file:")) 119 { 120 jarFileUrl = jarFileUrl.substring("file:".length()); 121 } 122 123 return new JarFile(jarFileUrl); 124 } 125 126 return null; 127 } 128 129 /** 130 * Variation of {@link Runnable} that throws {@link IOException}. Still think checked exceptions are a good idea? 131 */ 132 interface IOWork 133 { 134 void run() throws IOException; 135 } 136 137 /** 138 * Encapsulates the data, result, and queue of deferred operations for performing the scan. 139 */ 140 class Job 141 { 142 final ClasspathMatcher matcher; 143 144 final Set<String> matches = CollectionFactory.newSet(); 145 146 /** 147 * Explicit queue used to avoid deep tail-recursion. 148 */ 149 final Stack<IOWork> queue = CollectionFactory.newStack(); 150 151 152 Job(ClasspathMatcher matcher) 153 { 154 this.matcher = matcher; 155 } 156 157 Set<String> findMatches(String packagePath) throws IOException 158 { 159 160 Enumeration<URL> urls = contextClassLoader.getResources(packagePath); 161 162 while (urls.hasMoreElements()) 163 { 164 URL url = urls.nextElement(); 165 166 URL converted = converter.convert(url); 167 168 scanURL(packagePath, converted); 169 170 while (!queue.isEmpty()) 171 { 172 IOWork queued = queue.pop(); 173 174 queued.run(); 175 } 176 } 177 178 return matches; 179 } 180 181 void scanURL(final String packagePath, final URL url) throws IOException 182 { 183 URLConnection connection = url.openConnection(); 184 185 JarFile jarFile; 186 187 if (connection instanceof JarURLConnection) 188 { 189 jarFile = ((JarURLConnection) connection).getJarFile(); 190 } else 191 { 192 jarFile = getAlternativeJarFile(url); 193 } 194 195 if (jarFile != null) 196 { 197 scanJarFile(packagePath, jarFile); 198 } else if (supportsDirStream(url)) 199 { 200 queue.push(new IOWork() 201 { 202 public void run() throws IOException 203 { 204 scanDirStream(packagePath, url); 205 } 206 }); 207 } else 208 { 209 // Try scanning file system. 210 211 scanDir(packagePath, new File(url.getFile())); 212 } 213 214 } 215 216 /** 217 * Scan a dir for classes. Will recursively look in the supplied directory and all sub directories. 218 * 219 * @param packagePath 220 * Name of package that this directory corresponds to. 221 * @param packageDir 222 * Dir to scan for classes. 223 */ 224 private void scanDir(String packagePath, File packageDir) 225 { 226 if (packageDir.exists() && packageDir.isDirectory()) 227 { 228 for (final File file : packageDir.listFiles()) 229 { 230 String fileName = file.getName(); 231 232 if (file.isDirectory()) 233 { 234 final String nestedPackagePath = fileName + "/"; 235 236 queue.push(new IOWork() 237 { 238 public void run() throws IOException 239 { 240 scanDir(nestedPackagePath, file); 241 } 242 }); 243 } 244 245 if (matcher.matches(packagePath, fileName)) 246 { 247 matches.add(packagePath + fileName); 248 } 249 } 250 } 251 } 252 253 private void scanDirStream(String packagePath, URL packageURL) throws IOException 254 { 255 InputStream is; 256 257 try 258 { 259 is = new BufferedInputStream(packageURL.openStream()); 260 } catch (FileNotFoundException ex) 261 { 262 // This can happen for certain application servers (JBoss 4.0.5 for example), that 263 // export part of the exploded WAR for deployment, but leave part (WEB-INF/classes) 264 // unexploded. 265 266 return; 267 } 268 269 Reader reader = new InputStreamReader(is); 270 LineNumberReader lineReader = new LineNumberReader(reader); 271 272 try 273 { 274 while (true) 275 { 276 String line = lineReader.readLine(); 277 278 if (line == null) break; 279 280 if (matcher.matches(packagePath, line)) 281 { 282 matches.add(packagePath + line); 283 } else 284 { 285 286 // This should match just directories. It may also match files that have no extension; 287 // when we read those, none of the lines should look like class files. 288 289 if (FOLDER_NAME_PATTERN.matcher(line).matches()) 290 { 291 final URL newURL = new URL(packageURL.toExternalForm() + line + "/"); 292 final String nestedPackagePath = packagePath + line + "/"; 293 294 queue.push(new IOWork() 295 { 296 public void run() throws IOException 297 { 298 scanURL(nestedPackagePath, newURL); 299 } 300 }); 301 } 302 } 303 } 304 305 lineReader.close(); 306 lineReader = null; 307 } finally 308 { 309 InternalUtils.close(lineReader); 310 } 311 312 } 313 314 private void scanJarFile(String packagePath, JarFile jarFile) 315 { 316 Enumeration<JarEntry> e = jarFile.entries(); 317 318 while (e.hasMoreElements()) 319 { 320 String name = e.nextElement().getName(); 321 322 if (!name.startsWith(packagePath)) continue; 323 324 int lastSlashx = name.lastIndexOf('/'); 325 326 String filePackagePath = name.substring(0, lastSlashx + 1); 327 String fileName = name.substring(lastSlashx + 1); 328 329 if (matcher.matches(filePackagePath, fileName)) 330 { 331 matches.add(name); 332 } 333 } 334 } 335 } 336}