001// Copyright 2007, 2008, 2010, 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.func.F;
018import org.apache.tapestry5.func.Mapper;
019import org.apache.tapestry5.ioc.services.ClassNameLocator;
020import org.apache.tapestry5.ioc.services.ClasspathMatcher;
021import org.apache.tapestry5.ioc.services.ClasspathScanner;
022
023import java.io.IOException;
024import java.util.Collection;
025import java.util.regex.Pattern;
026
027public class ClassNameLocatorImpl implements ClassNameLocator
028{
029    private final ClasspathScanner scanner;
030
031    // This matches normal class files but not inner class files (which contain a '$'.
032
033    private final Pattern CLASS_NAME_PATTERN = Pattern.compile("^\\p{javaJavaIdentifierStart}[\\p{javaJavaIdentifierPart}&&[^\\$]]*\\.class$", Pattern.CASE_INSENSITIVE);
034
035    /**
036     * Matches paths that are classes, but not for inner classes, or the package-info.class psuedo-class (used for package-level annotations).
037     */
038    private final ClasspathMatcher CLASS_NAME_MATCHER = new ClasspathMatcher()
039    {
040        @Override
041        public boolean matches(String packagePath, String fileName)
042        {
043            if (!CLASS_NAME_PATTERN.matcher(fileName).matches())
044            {
045                return false;
046            }
047
048            // Filter out inner classes.
049
050            if (fileName.contains("$") || fileName.equals("package-info.class"))
051            {
052                return false;
053            }
054
055            return true;
056        }
057    };
058
059    /**
060     * Maps a path name ("foo/bar/Baz.class") to a class name ("foo.bar.Baz").
061     */
062    private final Mapper<String, String> CLASS_NAME_MAPPER = new Mapper<String, String>()
063    {
064        @Override
065        public String map(String element)
066        {
067            return element.substring(0, element.length() - 6).replace('/', '.');
068        }
069    };
070
071
072    public ClassNameLocatorImpl(ClasspathScanner scanner)
073    {
074        this.scanner = scanner;
075    }
076
077    /**
078     * Synchronization should not be necessary, but perhaps the underlying ClassLoader's are sensitive to threading.
079     */
080    @Override
081    public synchronized Collection<String> locateClassNames(String packageName)
082    {
083        String packagePath = packageName.replace('.', '/') + "/";
084
085        try
086        {
087            Collection<String> matches = scanner.scan(packagePath, CLASS_NAME_MATCHER);
088
089            return F.flow(matches).map(CLASS_NAME_MAPPER).toSet();
090
091        } catch (IOException ex)
092        {
093            throw new RuntimeException(ex);
094        }
095    }
096
097
098}