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 }