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
015package org.apache.tapestry5.ioc.internal.services;
016
017import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
018import org.apache.tapestry5.ioc.internal.util.InternalUtils;
019import org.apache.tapestry5.ioc.services.ClassNameLocator;
020import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
021import org.apache.tapestry5.ioc.util.Stack;
022
023import java.io.*;
024import java.net.JarURLConnection;
025import java.net.URL;
026import java.net.URLConnection;
027import java.util.Collection;
028import java.util.Enumeration;
029import java.util.jar.JarEntry;
030import java.util.jar.JarFile;
031import java.util.regex.Pattern;
032
033public 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}