001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2019 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.javadoc;
021
022import java.util.HashSet;
023import java.util.Set;
024
025/**
026 * Utility class to resolve a class name to an actual class. Note that loaded
027 * classes are not initialized.
028 * <p>Limitations: this does not handle inner classes very well.</p>
029 *
030 */
031public class ClassResolver {
032
033    /** Period literal. */
034    private static final String PERIOD = ".";
035    /** Dollar sign literal. */
036    private static final String DOLLAR_SIGN = "$";
037
038    /** Name of the package to check if the class belongs to. **/
039    private final String pkg;
040    /** Set of imports to check against. **/
041    private final Set<String> imports;
042    /** Use to load classes. **/
043    private final ClassLoader loader;
044
045    /**
046     * Creates a new {@code ClassResolver} instance.
047     *
048     * @param loader the ClassLoader to load classes with.
049     * @param pkg the name of the package the class may belong to
050     * @param imports set of imports to check if the class belongs to
051     */
052    public ClassResolver(ClassLoader loader, String pkg, Set<String> imports) {
053        this.loader = loader;
054        this.pkg = pkg;
055        this.imports = new HashSet<>(imports);
056        this.imports.add("java.lang.*");
057    }
058
059    /**
060     * Attempts to resolve the Class for a specified name. The algorithm is
061     * to check:
062     * - fully qualified name
063     * - explicit imports
064     * - enclosing package
065     * - star imports
066     * @param name name of the class to resolve
067     * @param currentClass name of current class (for inner classes).
068     * @return the resolved class
069     * @throws ClassNotFoundException if unable to resolve the class
070     */
071    // -@cs[ForbidWildcardAsReturnType] This method can return any type, so no way to avoid wildcard
072    public Class<?> resolve(String name, String currentClass)
073            throws ClassNotFoundException {
074        // See if the class is full qualified
075        Class<?> clazz = resolveQualifiedName(name);
076        if (clazz == null) {
077            // try matching explicit imports
078            clazz = resolveMatchingExplicitImport(name);
079
080            if (clazz == null) {
081                // See if in the package
082                clazz = resolveInPackage(name);
083
084                if (clazz == null) {
085                    // see if inner class of this class
086                    clazz = resolveInnerClass(name, currentClass);
087
088                    if (clazz == null) {
089                        clazz = resolveByStarImports(name);
090                        // -@cs[NestedIfDepth] it is better to have single return point from method
091                        if (clazz == null) {
092                            throw new ClassNotFoundException(name);
093                        }
094                    }
095                }
096            }
097        }
098        return clazz;
099    }
100
101    /**
102     * Try to find class by search in package.
103     * @param name class name
104     * @return class object
105     */
106    private Class<?> resolveInPackage(String name) {
107        Class<?> clazz = null;
108        if (pkg != null && !pkg.isEmpty()) {
109            final Class<?> classFromQualifiedName = resolveQualifiedName(pkg + PERIOD + name);
110            if (classFromQualifiedName != null) {
111                clazz = classFromQualifiedName;
112            }
113        }
114        return clazz;
115    }
116
117    /**
118     * Try to find class by matching explicit Import.
119     * @param name class name
120     * @return class object
121     */
122    private Class<?> resolveMatchingExplicitImport(String name) {
123        Class<?> clazz = null;
124        for (String imp : imports) {
125            // Very important to add the "." in the check below. Otherwise you
126            // when checking for "DataException", it will match on
127            // "SecurityDataException". This has been the cause of a very
128            // difficult bug to resolve!
129            if (imp.endsWith(PERIOD + name)) {
130                clazz = resolveQualifiedName(imp);
131                if (clazz != null) {
132                    break;
133                }
134            }
135        }
136        return clazz;
137    }
138
139    /**
140     * See if inner class of this class.
141     * @param name name of the search Class to search
142     * @param currentClass class where search in
143     * @return class if found , or null if not resolved
144     * @throws ClassNotFoundException  if an error occurs
145     */
146    private Class<?> resolveInnerClass(String name, String currentClass)
147            throws ClassNotFoundException {
148        Class<?> clazz = null;
149        if (!currentClass.isEmpty()) {
150            String innerClass = currentClass + DOLLAR_SIGN + name;
151
152            if (!pkg.isEmpty()) {
153                innerClass = pkg + PERIOD + innerClass;
154            }
155
156            if (isLoadable(innerClass)) {
157                clazz = safeLoad(innerClass);
158            }
159        }
160        return clazz;
161    }
162
163    /**
164     * Try star imports.
165     * @param name name of the Class to search
166     * @return  class if found , or null if not resolved
167     */
168    private Class<?> resolveByStarImports(String name) {
169        Class<?> clazz = null;
170        for (String imp : imports) {
171            if (imp.endsWith(".*")) {
172                final String fqn = imp.substring(0, imp.lastIndexOf('.') + 1) + name;
173                clazz = resolveQualifiedName(fqn);
174                if (clazz != null) {
175                    break;
176                }
177            }
178        }
179        return clazz;
180    }
181
182    /**
183     * Checks if the given class name can be loaded.
184     * @param name name of the class to check
185     * @return whether a specified class is loadable with safeLoad().
186     */
187    public boolean isLoadable(String name) {
188        boolean result;
189        try {
190            safeLoad(name);
191            result = true;
192        }
193        catch (final ClassNotFoundException | NoClassDefFoundError ignored) {
194            result = false;
195        }
196        return result;
197    }
198
199    /**
200     * Will load a specified class is such a way that it will NOT be
201     * initialised.
202     * @param name name of the class to load
203     * @return the {@code Class} for the specified class
204     * @throws ClassNotFoundException if an error occurs
205     * @throws NoClassDefFoundError if an error occurs
206     */
207    // -@cs[ForbidWildcardAsReturnType] The class is deprecated and will be removed soon.
208    private Class<?> safeLoad(String name) throws ClassNotFoundException, NoClassDefFoundError {
209        // The next line will load the class using the specified class
210        // loader. The magic is having the "false" parameter. This means the
211        // class will not be initialised. Very, very important.
212        return Class.forName(name, false, loader);
213    }
214
215    /**
216     * Tries to resolve a class for fully-specified name.
217     * @param name a given name of class.
218     * @return Class object for the given name or null.
219     */
220    private Class<?> resolveQualifiedName(final String name) {
221        Class<?> classObj = null;
222        try {
223            if (isLoadable(name)) {
224                classObj = safeLoad(name);
225            }
226            else {
227                //Perhaps it's fully-qualified inner class
228                final int dot = name.lastIndexOf('.');
229                if (dot != -1) {
230                    final String innerName =
231                        name.substring(0, dot) + DOLLAR_SIGN + name.substring(dot + 1);
232                    classObj = resolveQualifiedName(innerName);
233                }
234            }
235        }
236        catch (final ClassNotFoundException ex) {
237            // we shouldn't get this exception here,
238            // so this is unexpected runtime exception
239            throw new IllegalStateException(ex);
240        }
241        return classObj;
242    }
243
244}