    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    }