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