001 // Copyright 2007, 2008, 2010 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 015 package org.apache.tapestry5.ioc.internal.services; 016 017 import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 018 import org.apache.tapestry5.ioc.internal.util.InternalUtils; 019 import org.apache.tapestry5.ioc.services.ClassNameLocator; 020 import org.apache.tapestry5.ioc.services.ClasspathURLConverter; 021 import org.apache.tapestry5.ioc.util.Stack; 022 023 import java.io.*; 024 import java.net.JarURLConnection; 025 import java.net.URL; 026 import java.net.URLConnection; 027 import java.util.Collection; 028 import java.util.Enumeration; 029 import java.util.jar.JarEntry; 030 import java.util.jar.JarFile; 031 import java.util.regex.Pattern; 032 033 public class ClassNameLocatorImpl implements ClassNameLocator 034 { 035 private static final String CLASS_SUFFIX = ".class"; 036 public static final String PACKAGE_INFO = "package-info.class"; 037 038 private final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); 039 040 private final ClasspathURLConverter converter; 041 042 // This matches normal class files but not inner class files (which contain a '$'. 043 044 private final Pattern CLASS_NAME_PATTERN = Pattern.compile("^\\p{javaJavaIdentifierStart}[\\p{javaJavaIdentifierPart}&&[^\\$]]*\\.class$", Pattern.CASE_INSENSITIVE); 045 046 private final Pattern FOLDER_NAME_PATTERN = Pattern.compile("^\\p{javaJavaIdentifierStart}[\\p{javaJavaIdentifierPart}]*$", Pattern.CASE_INSENSITIVE); 047 048 static class Queued 049 { 050 final URL packageURL; 051 052 final String packagePath; 053 054 public Queued(final URL packageURL, final String packagePath) 055 { 056 this.packageURL = packageURL; 057 this.packagePath = packagePath; 058 } 059 } 060 061 public ClassNameLocatorImpl(ClasspathURLConverter converter) 062 { 063 this.converter = converter; 064 } 065 066 /** 067 * Synchronization should not be necessary, but perhaps the underlying ClassLoader's are sensitive to threading. 068 */ 069 public synchronized Collection<String> locateClassNames(String packageName) 070 { 071 String packagePath = packageName.replace('.', '/') + "/"; 072 073 try 074 { 075 076 return findClassesWithinPath(packagePath); 077 078 } catch (IOException ex) 079 { 080 throw new RuntimeException(ex); 081 } 082 } 083 084 private Collection<String> findClassesWithinPath(String packagePath) throws IOException 085 { 086 Collection<String> result = CollectionFactory.newList(); 087 088 Enumeration<URL> urls = contextClassLoader.getResources(packagePath); 089 090 while (urls.hasMoreElements()) 091 { 092 URL url = urls.nextElement(); 093 094 URL converted = converter.convert(url); 095 096 scanURL(packagePath, result, converted); 097 } 098 099 return result; 100 } 101 102 private void scanURL(String packagePath, Collection<String> componentClassNames, URL url) throws IOException 103 { 104 URLConnection connection = url.openConnection(); 105 106 JarFile jarFile; 107 108 if (connection instanceof JarURLConnection) 109 { 110 jarFile = ((JarURLConnection) connection).getJarFile(); 111 } else 112 { 113 jarFile = getAlternativeJarFile(url); 114 } 115 116 if (jarFile != null) 117 { 118 scanJarFile(packagePath, componentClassNames, jarFile); 119 } else if (supportsDirStream(url)) 120 { 121 Stack<Queued> queue = CollectionFactory.newStack(); 122 123 queue.push(new Queued(url, packagePath)); 124 125 while (!queue.isEmpty()) 126 { 127 Queued queued = queue.pop(); 128 129 scanDirStream(queued.packagePath, queued.packageURL, componentClassNames, queue); 130 } 131 } else 132 { 133 // Try scanning file system. 134 String packageName = packagePath.replace("/", "."); 135 if (packageName.endsWith(".")) 136 { 137 packageName = packageName.substring(0, packageName.length() - 1); 138 } 139 scanDir(packageName, new File(url.getFile()), componentClassNames); 140 } 141 142 } 143 144 /** 145 * Check whether container supports opening a stream on a dir/package to get a list of its contents. 146 * 147 * @param packageURL 148 * @return 149 */ 150 private boolean supportsDirStream(URL packageURL) 151 { 152 InputStream is = null; 153 try 154 { 155 is = packageURL.openStream(); 156 return true; 157 } catch (FileNotFoundException ex) 158 { 159 return false; 160 } catch (IOException e) 161 { 162 return false; 163 } finally 164 { 165 InternalUtils.close(is); 166 } 167 } 168 169 private void scanDirStream(String packagePath, URL packageURL, Collection<String> componentClassNames, 170 Stack<Queued> queue) throws IOException 171 { 172 InputStream is; 173 174 try 175 { 176 is = new BufferedInputStream(packageURL.openStream()); 177 } catch (FileNotFoundException ex) 178 { 179 // This can happen for certain application servers (JBoss 4.0.5 for example), that 180 // export part of the exploded WAR for deployment, but leave part (WEB-INF/classes) 181 // unexploded. 182 183 return; 184 } 185 186 Reader reader = new InputStreamReader(is); 187 LineNumberReader lineReader = new LineNumberReader(reader); 188 189 String packageName = null; 190 191 try 192 { 193 while (true) 194 { 195 String line = lineReader.readLine(); 196 197 if (line == null) break; 198 199 if (CLASS_NAME_PATTERN.matcher(line).matches()) 200 { 201 if (packageName == null) 202 { 203 packageName = packagePath.replace('/', '.'); 204 } 205 206 // packagePath ends with '/', packageName ends with '.' 207 208 String fileName = line.substring(0, line.length() - CLASS_SUFFIX.length()); 209 210 if (!fileName.equals("package-info")) 211 { 212 String fullClassName = packageName + fileName; 213 214 componentClassNames.add(fullClassName); 215 } 216 217 continue; 218 } 219 220 // This should match just directories. It may also match files that have no extension; 221 // when we read those, none of the lines should look like class files. 222 223 if (FOLDER_NAME_PATTERN.matcher(line).matches()) 224 { 225 URL newURL = new URL(packageURL.toExternalForm() + line + "/"); 226 String newPackagePath = packagePath + line + "/"; 227 228 queue.push(new Queued(newURL, newPackagePath)); 229 } 230 } 231 232 lineReader.close(); 233 lineReader = null; 234 } finally 235 { 236 InternalUtils.close(lineReader); 237 } 238 239 } 240 241 private void scanJarFile(String packagePath, Collection<String> componentClassNames, JarFile jarFile) 242 { 243 Enumeration<JarEntry> e = jarFile.entries(); 244 245 while (e.hasMoreElements()) 246 { 247 String name = e.nextElement().getName(); 248 249 if (!name.startsWith(packagePath)) continue; 250 251 252 int lastSlashx = name.lastIndexOf('/'); 253 254 String fileName = name.substring(lastSlashx + 1); 255 256 if (isClassName(fileName)) 257 { 258 259 // Strip off .class and convert the slashes back to periods. 260 String className = 261 name.substring(0, lastSlashx + 1).replace('/', '.') + 262 fileName.substring(0, fileName.length() - CLASS_SUFFIX.length()); 263 264 265 componentClassNames.add(className); 266 } 267 } 268 } 269 270 /** 271 * Scan a dir for classes. Will recursively look in the supplied directory and all sub directories. 272 * 273 * @param packageName Name of package that this directory corresponds to. 274 * @param dir Dir to scan for clases. 275 * @param componentClassNames List of class names that have been found. 276 */ 277 private void scanDir(String packageName, File dir, Collection<String> componentClassNames) 278 { 279 if (dir.exists() && dir.isDirectory()) 280 { 281 for (File file : dir.listFiles()) 282 { 283 String fileName = file.getName(); 284 if (file.isDirectory()) 285 { 286 scanDir(packageName + "." + fileName, file, componentClassNames); 287 } 288 // https://issues.apache.org/jira/browse/TAP5-1737 289 // Use of package-info.java leaves these package-info.class files around. 290 else if (isClassName(fileName)) 291 { 292 String className = packageName + "." + fileName.substring(0, 293 fileName.length() - CLASS_SUFFIX.length()); 294 componentClassNames.add(className); 295 } 296 } 297 } 298 } 299 300 private boolean isClassName(String fileName) 301 { 302 return fileName.endsWith(CLASS_SUFFIX) && !fileName.equals(PACKAGE_INFO) && !fileName.contains("$"); 303 } 304 305 /** 306 * For URLs to JARs that do not use JarURLConnection - allowed by the servlet spec - attempt to produce a JarFile 307 * object all the same. Known servlet engines that function like this include Weblogic and OC4J. This is not a full 308 * solution, since an unpacked WAR or EAR will not have JAR "files" as such. 309 * 310 * @param url URL of jar 311 * @return JarFile or null 312 * @throws java.io.IOException If error occurs creating jar file 313 */ 314 private JarFile getAlternativeJarFile(URL url) throws IOException 315 { 316 String urlFile = url.getFile(); 317 // Trim off any suffix - which is prefixed by "!/" on Weblogic 318 int separatorIndex = urlFile.indexOf("!/"); 319 320 // OK, didn't find that. Try the less safe "!", used on OC4J 321 if (separatorIndex == -1) 322 { 323 separatorIndex = urlFile.indexOf('!'); 324 } 325 if (separatorIndex != -1) 326 { 327 String jarFileUrl = urlFile.substring(0, separatorIndex); 328 // And trim off any "file:" prefix. 329 if (jarFileUrl.startsWith("file:")) 330 { 331 jarFileUrl = jarFileUrl.substring("file:".length()); 332 } 333 return new JarFile(jarFileUrl); 334 } 335 return null; 336 } 337 338 }