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