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.commons.util.CollectionFactory; 018import org.apache.tapestry5.commons.util.Stack; 019import org.apache.tapestry5.ioc.internal.util.InternalUtils; 020import org.apache.tapestry5.ioc.services.ClasspathMatcher; 021import org.apache.tapestry5.ioc.services.ClasspathScanner; 022import org.apache.tapestry5.ioc.services.ClasspathURLConverter; 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 static 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 @Override 060 public Set<String> scan(String packagePath, ClasspathMatcher matcher) throws IOException 061 { 062 assert packagePath != null && packagePath.endsWith("/"); 063 assert matcher != null; 064 065 return new Job(matcher, contextClassLoader, converter).findMatches(packagePath); 066 } 067 068 /** 069 * Check whether container supports opening a stream on a dir/package to get a list of its contents. 070 */ 071 private static boolean supportsDirStream(URL packageURL) 072 { 073 InputStream is = null; 074 075 try 076 { 077 is = packageURL.openStream(); 078 079 return true; 080 } catch (FileNotFoundException ex) 081 { 082 return false; 083 } catch (IOException ex) 084 { 085 return false; 086 } finally 087 { 088 InternalUtils.close(is); 089 } 090 } 091 092 /** 093 * For URLs to JARs that do not use JarURLConnection - allowed by the servlet spec - attempt to produce a JarFile 094 * object all the same. Known servlet engines that function like this include Weblogic and OC4J. This is not a full 095 * solution, since an unpacked WAR or EAR will not have JAR "files" as such. 096 * 097 * @param url 098 * URL of jar 099 * @return JarFile or null 100 * @throws java.io.IOException 101 * If error occurs creating jar file 102 */ 103 private static JarFile getAlternativeJarFile(URL url) throws IOException 104 { 105 String urlFile = url.getFile(); 106 // Trim off any suffix - which is prefixed by "!/" on Weblogic 107 int separatorIndex = urlFile.indexOf("!/"); 108 109 // OK, didn't find that. Try the less safe "!", used on OC4J 110 if (separatorIndex == -1) 111 { 112 separatorIndex = urlFile.indexOf('!'); 113 } 114 115 if (separatorIndex != -1) 116 { 117 String jarFileUrl = urlFile.substring(0, separatorIndex); 118 // And trim off any "file:" prefix. 119 if (jarFileUrl.startsWith("file:")) 120 { 121 jarFileUrl = jarFileUrl.substring("file:".length()); 122 } 123 124 return new JarFile(jarFileUrl); 125 } 126 127 return null; 128 } 129 130 /** 131 * Variation of {@link Runnable} that throws {@link IOException}. Still think checked exceptions are a good idea? 132 */ 133 interface IOWork 134 { 135 void run() throws IOException; 136 } 137 138 /** 139 * Encapsulates the data, result, and queue of deferred operations for performing the scan. 140 */ 141 static class Job 142 { 143 final ClasspathMatcher matcher; 144 145 final ClasspathURLConverter converter; 146 147 final ClassLoader classloader; 148 149 final Set<String> matches = CollectionFactory.newSet(); 150 151 /** 152 * Explicit queue used to avoid deep tail-recursion. 153 */ 154 final Stack<IOWork> queue = CollectionFactory.newStack(); 155 156 157 Job(ClasspathMatcher matcher, ClassLoader classloader, ClasspathURLConverter converter) 158 { 159 this.matcher = matcher; 160 this.classloader = classloader; 161 this.converter = converter; 162 } 163 164 Set<String> findMatches(String packagePath) throws IOException 165 { 166 167 Enumeration<URL> urls = classloader.getResources(packagePath); 168 169 while (urls.hasMoreElements()) 170 { 171 URL url = urls.nextElement(); 172 173 URL converted = converter.convert(url); 174 175 scanURL(packagePath, converted); 176 177 while (!queue.isEmpty()) 178 { 179 IOWork queued = queue.pop(); 180 181 queued.run(); 182 } 183 } 184 185 return matches; 186 } 187 188 void scanURL(final String packagePath, final URL url) throws IOException 189 { 190 URLConnection connection = url.openConnection(); 191 192 JarFile jarFile; 193 194 if (connection instanceof JarURLConnection) 195 { 196 jarFile = ((JarURLConnection) connection).getJarFile(); 197 } else 198 { 199 jarFile = getAlternativeJarFile(url); 200 } 201 202 if (jarFile != null) 203 { 204 scanJarFile(packagePath, jarFile); 205 } else if (supportsDirStream(url)) 206 { 207 queue.push(new IOWork() 208 { 209 @Override 210 public void run() throws IOException 211 { 212 scanDirStream(packagePath, url); 213 } 214 }); 215 } else 216 { 217 // Try scanning file system. 218 219 scanDir(packagePath, new File(url.getFile())); 220 } 221 222 } 223 224 /** 225 * Scan a dir for classes. Will recursively look in the supplied directory and all sub directories. 226 * 227 * @param packagePath 228 * Name of package that this directory corresponds to. 229 * @param packageDir 230 * Dir to scan for classes. 231 */ 232 private void scanDir(String packagePath, File packageDir) 233 { 234 if (packageDir.exists() && packageDir.isDirectory()) 235 { 236 for (final File file : packageDir.listFiles()) 237 { 238 String fileName = file.getName(); 239 240 if (file.isDirectory()) 241 { 242 final String nestedPackagePath = packagePath + fileName + "/"; 243 244 queue.push(new IOWork() 245 { 246 @Override 247 public void run() throws IOException 248 { 249 scanDir(nestedPackagePath, file); 250 } 251 }); 252 } 253 254 if (matcher.matches(packagePath, fileName)) 255 { 256 matches.add(packagePath + fileName); 257 } 258 } 259 } 260 } 261 262 private void scanDirStream(String packagePath, URL packageURL) throws IOException 263 { 264 InputStream is; 265 266 try 267 { 268 is = new BufferedInputStream(packageURL.openStream()); 269 } catch (FileNotFoundException ex) 270 { 271 // This can happen for certain application servers (JBoss 4.0.5 for example), that 272 // export part of the exploded WAR for deployment, but leave part (WEB-INF/classes) 273 // unexploded. 274 275 return; 276 } 277 278 Reader reader = new InputStreamReader(is); 279 LineNumberReader lineReader = new LineNumberReader(reader); 280 281 try 282 { 283 while (true) 284 { 285 String line = lineReader.readLine(); 286 287 if (line == null) break; 288 289 if (matcher.matches(packagePath, line)) 290 { 291 matches.add(packagePath + line); 292 } else 293 { 294 295 // This should match just directories. It may also match files that have no extension; 296 // when we read those, none of the lines should look like class files. 297 298 if (FOLDER_NAME_PATTERN.matcher(line).matches()) 299 { 300 final URL newURL = new URL(packageURL.toExternalForm() + line + "/"); 301 final String nestedPackagePath = packagePath + line + "/"; 302 303 queue.push(new IOWork() 304 { 305 @Override 306 public void run() throws IOException 307 { 308 scanURL(nestedPackagePath, newURL); 309 } 310 }); 311 } 312 } 313 } 314 315 lineReader.close(); 316 lineReader = null; 317 } finally 318 { 319 InternalUtils.close(lineReader); 320 } 321 322 } 323 324 private void scanJarFile(String packagePath, JarFile jarFile) 325 { 326 Enumeration<JarEntry> e = jarFile.entries(); 327 328 while (e.hasMoreElements()) 329 { 330 String name = e.nextElement().getName(); 331 332 if (!name.startsWith(packagePath)) continue; 333 334 int lastSlashx = name.lastIndexOf('/'); 335 336 String filePackagePath = name.substring(0, lastSlashx + 1); 337 String fileName = name.substring(lastSlashx + 1); 338 339 if (matcher.matches(filePackagePath, fileName)) 340 { 341 matches.add(name); 342 } 343 } 344 } 345 } 346}