    001// Copyright 2012 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.commons.util.CollectionFactory;
    018import org.apache.tapestry5.commons.util.Stack;
    019import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    020import org.apache.tapestry5.ioc.services.ClasspathMatcher;
    021import org.apache.tapestry5.ioc.services.ClasspathScanner;
    022import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
    023
    024import java.io.*;
    025import java.net.JarURLConnection;
    026import java.net.URL;
    027import java.net.URLConnection;
    028import java.util.Enumeration;
    029import java.util.Set;
    030import java.util.jar.JarEntry;
    031import java.util.jar.JarFile;
    032import java.util.regex.Pattern;
    033
    034public class ClasspathScannerImpl implements ClasspathScanner
    035{
    036    private final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
    037
    038    private final ClasspathURLConverter converter;
    039
    040    private static final Pattern FOLDER_NAME_PATTERN = Pattern.compile("^\\p{javaJavaIdentifierStart}[\\p{javaJavaIdentifierPart}]*$", Pattern.CASE_INSENSITIVE);
    041
    042
    043    public ClasspathScannerImpl(ClasspathURLConverter converter)
    044    {
    045        this.converter = converter;
    046    }
    047
    048    /**
    049     * Scans the indicated package path for matches.
    050     *
    051     * @param packagePath
    052     *         a package path (like a package name, but using '/' instead of '.', and ending with '/')
    053     * @param matcher
    054     *         passed a resource path from the package (or a sub-package), returns true if the provided
    055     *         path should be included in the returned collection
    056     * @return collection of matching paths, in no specified order
    057     * @throws java.io.IOException
    058     */
    059    @Override
    060    public Set<String> scan(String packagePath, ClasspathMatcher matcher) throws IOException
    061    {
    062        assert packagePath != null && packagePath.endsWith("/");
    063        assert matcher != null;
    064
    065        return new Job(matcher, contextClassLoader, converter).findMatches(packagePath);
    066    }
    067
    068    /**
    069     * Check whether container supports opening a stream on a dir/package to get a list of its contents.
    070     */
    071    private static boolean supportsDirStream(URL packageURL)
    072    {
    073        InputStream is = null;
    074
    075        try
    076        {
    077            is = packageURL.openStream();
    078
    079            return true;
    080        } catch (FileNotFoundException ex)
    081        {
    082            return false;
    083        } catch (IOException ex)
    084        {
    085            return false;
    086        } finally
    087        {
    088            InternalUtils.close(is);
    089        }
    090    }
    091
    092    /**
    093     * For URLs to JARs that do not use JarURLConnection - allowed by the servlet spec - attempt to produce a JarFile
    094     * object all the same. Known servlet engines that function like this include Weblogic and OC4J. This is not a full
    095     * solution, since an unpacked WAR or EAR will not have JAR "files" as such.
    096     *
    097     * @param url
    098     *         URL of jar
    099     * @return JarFile or null
    100     * @throws java.io.IOException
    101     *         If error occurs creating jar file
    102     */
    103    private static JarFile getAlternativeJarFile(URL url) throws IOException
    104    {
    105        String urlFile = url.getFile();
    106        // Trim off any suffix - which is prefixed by "!/" on Weblogic
    107        int separatorIndex = urlFile.indexOf("!/");
    108
    109        // OK, didn't find that. Try the less safe "!", used on OC4J
    110        if (separatorIndex == -1)
    111        {
    112            separatorIndex = urlFile.indexOf('!');
    113        }
    114
    115        if (separatorIndex != -1)
    116        {
    117            String jarFileUrl = urlFile.substring(0, separatorIndex);
    118            // And trim off any "file:" prefix.
    119            if (jarFileUrl.startsWith("file:"))
    120            {
    121                jarFileUrl = jarFileUrl.substring("file:".length());
    122            }
    123
    124            return new JarFile(jarFileUrl);
    125        }
    126
    127        return null;
    128    }
    129
    130    /**
    131     * Variation of {@link Runnable} that throws {@link IOException}.  Still think checked exceptions are a good idea?
    132     */
    133    interface IOWork
    134    {
    135        void run() throws IOException;
    136    }
    137
    138    /**
    139     * Encapsulates the data, result, and queue of deferred operations for performing the scan.
    140     */
    141    static class Job
    142    {
    143        final ClasspathMatcher matcher;
    144
    145        final ClasspathURLConverter converter;
    146
    147        final ClassLoader classloader;
    148
    149        final Set<String> matches = CollectionFactory.newSet();
    150
    151        /**
    152         * Explicit queue used to avoid deep tail-recursion.
    153         */
    154        final Stack<IOWork> queue = CollectionFactory.newStack();
    155
    156
    157        Job(ClasspathMatcher matcher, ClassLoader classloader, ClasspathURLConverter converter)
    158        {
    159            this.matcher = matcher;
    160            this.classloader = classloader;
    161            this.converter = converter;
    162        }
    163
    164        Set<String> findMatches(String packagePath) throws IOException
    165        {
    166
    167            Enumeration<URL> urls = classloader.getResources(packagePath);
    168
    169            while (urls.hasMoreElements())
    170            {
    171                URL url = urls.nextElement();
    172
    173                URL converted = converter.convert(url);
    174
    175                scanURL(packagePath, converted);
    176
    177                while (!queue.isEmpty())
    178                {
    179                    IOWork queued = queue.pop();
    180
    181                    queued.run();
    182                }
    183            }
    184
    185            return matches;
    186        }
    187
    188        void scanURL(final String packagePath, final URL url) throws IOException
    189        {
    190            URLConnection connection = url.openConnection();
    191
    192            JarFile jarFile;
    193
    194            if (connection instanceof JarURLConnection)
    195            {
    196                jarFile = ((JarURLConnection) connection).getJarFile();
    197            } else
    198            {
    199                jarFile = getAlternativeJarFile(url);
    200            }
    201
    202            if (jarFile != null)
    203            {
    204                scanJarFile(packagePath, jarFile);
    205            } else if (supportsDirStream(url))
    206            {
    207                queue.push(new IOWork()
    208                {
    209                    @Override
    210                    public void run() throws IOException
    211                    {
    212                        scanDirStream(packagePath, url);
    213                    }
    214                });
    215            } else
    216            {
    217                // Try scanning file system.
    218
    219                scanDir(packagePath, new File(url.getFile()));
    220            }
    221
    222        }
    223
    224        /**
    225         * Scan a dir for classes. Will recursively look in the supplied directory and all sub directories.
    226         *
    227         * @param packagePath
    228         *         Name of package that this directory corresponds to.
    229         * @param packageDir
    230         *         Dir to scan for classes.
    231         */
    232        private void scanDir(String packagePath, File packageDir)
    233        {
    234            if (packageDir.exists() && packageDir.isDirectory())
    235            {
    236                for (final File file : packageDir.listFiles())
    237                {
    238                    String fileName = file.getName();
    239
    240                    if (file.isDirectory())
    241                    {
    242                        final String nestedPackagePath = packagePath + fileName + "/";
    243
    244                        queue.push(new IOWork()
    245                        {
    246                            @Override
    247                            public void run() throws IOException
    248                            {
    249                                scanDir(nestedPackagePath, file);
    250                            }
    251                        });
    252                    }
    253
    254                    if (matcher.matches(packagePath, fileName))
    255                    {
    256                        matches.add(packagePath + fileName);
    257                    }
    258                }
    259            }
    260        }
    261
    262        private void scanDirStream(String packagePath, URL packageURL) throws IOException
    263        {
    264            InputStream is;
    265
    266            try
    267            {
    268                is = new BufferedInputStream(packageURL.openStream());
    269            } catch (FileNotFoundException ex)
    270            {
    271                // This can happen for certain application servers (JBoss 4.0.5 for example), that
    272                // export part of the exploded WAR for deployment, but leave part (WEB-INF/classes)
    273                // unexploded.
    274
    275                return;
    276            }
    277
    278            Reader reader = new InputStreamReader(is);
    279            LineNumberReader lineReader = new LineNumberReader(reader);
    280
    281            try
    282            {
    283                while (true)
    284                {
    285                    String line = lineReader.readLine();
    286
    287                    if (line == null) break;
    288
    289                    if (matcher.matches(packagePath, line))
    290                    {
    291                        matches.add(packagePath + line);
    292                    } else
    293                    {
    294
    295                        // This should match just directories.  It may also match files that have no extension;
    296                        // when we read those, none of the lines should look like class files.
    297
    298                        if (FOLDER_NAME_PATTERN.matcher(line).matches())
    299                        {
    300                            final URL newURL = new URL(packageURL.toExternalForm() + line + "/");
    301                            final String nestedPackagePath = packagePath + line + "/";
    302
    303                            queue.push(new IOWork()
    304                            {
    305                                @Override
    306                                public void run() throws IOException
    307                                {
    308                                    scanURL(nestedPackagePath, newURL);
    309                                }
    310                            });
    311                        }
    312                    }
    313                }
    314
    315                lineReader.close();
    316                lineReader = null;
    317            } finally
    318            {
    319                InternalUtils.close(lineReader);
    320            }
    321
    322        }
    323
    324        private void scanJarFile(String packagePath, JarFile jarFile)
    325        {
    326            Enumeration<JarEntry> e = jarFile.entries();
    327
    328            while (e.hasMoreElements())
    329            {
    330                String name = e.nextElement().getName();
    331
    332                if (!name.startsWith(packagePath)) continue;
    333
    334                int lastSlashx = name.lastIndexOf('/');
    335
    336                String filePackagePath = name.substring(0, lastSlashx + 1);
    337                String fileName = name.substring(lastSlashx + 1);
    338
    339                if (matcher.matches(filePackagePath, fileName))
    340                {
    341                    matches.add(name);
    342                }
    343            }
    344        }
    345    }
    346}