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    }