001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2018 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 * @author Oliver Burn
031 */
032public class ClassResolver {
033
034    /** Period literal. */
035    private static final String PERIOD = ".";
036    /** Dollar sign literal. */
037    private static final String DOLLAR_SIGN = "$";
038
039    /** Name of the package to check if the class belongs to. **/
040    private final String pkg;
041    /** Set of imports to check against. **/
042    private final Set<String> imports;
043    /** Use to load classes. **/
044    private final ClassLoader loader;
045
046    /**
047     * Creates a new {@code ClassResolver} instance.
048     *
049     * @param loader the ClassLoader to load classes with.
050     * @param pkg the name of the package the class may belong to
051     * @param imports set of imports to check if the class belongs to
052     */
053    public ClassResolver(ClassLoader loader, String pkg, Set<String> imports) {
054        this.loader = loader;
055        this.pkg = pkg;
056        this.imports = new HashSet<>(imports);
057        this.imports.add("java.lang.*");
058    }
059
060    /**
061     * Attempts to resolve the Class for a specified name. The algorithm is
062     * to check:
063     * - fully qualified name
064     * - explicit imports
065     * - enclosing package
066     * - star imports
067     * @param name name of the class to resolve
068     * @param currentClass name of current class (for inner classes).
069     * @return the resolved class
070     * @throws ClassNotFoundException if unable to resolve the class
071     */
072    // -@cs[ForbidWildcardAsReturnType] This method can return any type, so no way to avoid wildcard
073    public Class<?> resolve(String name, String currentClass)
074            throws ClassNotFoundException {
075        // See if the class is full qualified
076        Class<?> clazz = resolveQualifiedName(name);
077        if (clazz == null) {
078            // try matching explicit imports
079            clazz = resolveMatchingExplicitImport(name);
080
081            if (clazz == null) {
082                // See if in the package
083                clazz = resolveInPackage(name);
084
085                if (clazz == null) {
086                    // see if inner class of this class
087                    clazz = resolveInnerClass(name, currentClass);
088
089                    if (clazz == null) {
090                        clazz = resolveByStarImports(name);
091                        // -@cs[NestedIfDepth] it is better to have single return point from method
092                        if (clazz == null) {
093                            // Giving up, the type is unknown, so load the class to generate an
094                            // exception
095                            clazz = safeLoad(name);
096                        }
097                    }
098                }
099            }
100        }
101        return clazz;
102    }
103
104    /**
105     * Try to find class by search in package.
106     * @param name class name
107     * @return class object
108     */
109    private Class<?> resolveInPackage(String name) {
110        Class<?> clazz = null;
111        if (pkg != null && !pkg.isEmpty()) {
112            final Class<?> classFromQualifiedName = resolveQualifiedName(pkg + PERIOD + name);
113            if (classFromQualifiedName != null) {
114                clazz = classFromQualifiedName;
115            }
116        }
117        return clazz;
118    }
119
120    /**
121     * Try to find class by matching explicit Import.
122     * @param name class name
123     * @return class object
124     */
125    private Class<?> resolveMatchingExplicitImport(String name) {
126        Class<?> clazz = null;
127        for (String imp : imports) {
128            // Very important to add the "." in the check below. Otherwise you
129            // when checking for "DataException", it will match on
130            // "SecurityDataException". This has been the cause of a very
131            // difficult bug to resolve!
132            if (imp.endsWith(PERIOD + name)) {
133                clazz = resolveQualifiedName(imp);
134                if (clazz != null) {
135                    break;
136                }
137            }
138        }
139        return clazz;
140    }
141
142    /**
143     * See if inner class of this class.
144     * @param name name of the search Class to search
145     * @param currentClass class where search in
146     * @return class if found , or null if not resolved
147     * @throws ClassNotFoundException  if an error occurs
148     */
149    private Class<?> resolveInnerClass(String name, String currentClass)
150            throws ClassNotFoundException {
151        Class<?> clazz = null;
152        if (!currentClass.isEmpty()) {
153            String innerClass = currentClass + DOLLAR_SIGN + name;
154
155            if (!pkg.isEmpty()) {
156                innerClass = pkg + PERIOD + innerClass;
157            }
158
159            if (isLoadable(innerClass)) {
160                clazz = safeLoad(innerClass);
161            }
162        }
163        return clazz;
164    }
165
166    /**
167     * Try star imports.
168     * @param name name of the Class to search
169     * @return  class if found , or null if not resolved
170     */
171    private Class<?> resolveByStarImports(String name) {
172        Class<?> clazz = null;
173        for (String imp : imports) {
174            if (imp.endsWith(".*")) {
175                final String fqn = imp.substring(0, imp.lastIndexOf('.') + 1) + name;
176                clazz = resolveQualifiedName(fqn);
177                if (clazz != null) {
178                    break;
179                }
180            }
181        }
182        return clazz;
183    }
184
185    /**
186     * Checks if the given class name can be loaded.
187     * @param name name of the class to check
188     * @return whether a specified class is loadable with safeLoad().
189     */
190    public boolean isLoadable(String name) {
191        boolean result;
192        try {
193            safeLoad(name);
194            result = true;
195        }
196        catch (final ClassNotFoundException | NoClassDefFoundError ignored) {
197            result = false;
198        }
199        return result;
200    }
201
202    /**
203     * Will load a specified class is such a way that it will NOT be
204     * initialised.
205     * @param name name of the class to load
206     * @return the {@code Class} for the specified class
207     * @throws ClassNotFoundException if an error occurs
208     * @throws NoClassDefFoundError if an error occurs
209     */
210    // -@cs[ForbidWildcardAsReturnType] The class is deprecated and will be removed soon.
211    private Class<?> safeLoad(String name) throws ClassNotFoundException, NoClassDefFoundError {
212        // The next line will load the class using the specified class
213        // loader. The magic is having the "false" parameter. This means the
214        // class will not be initialised. Very, very important.
215        return Class.forName(name, false, loader);
216    }
217
218    /**
219     * Tries to resolve a class for fully-specified name.
220     * @param name a given name of class.
221     * @return Class object for the given name or null.
222     */
223    private Class<?> resolveQualifiedName(final String name) {
224        Class<?> classObj = null;
225        try {
226            if (isLoadable(name)) {
227                classObj = safeLoad(name);
228            }
229            else {
230                //Perhaps it's fully-qualified inner class
231                final int dot = name.lastIndexOf('.');
232                if (dot != -1) {
233                    final String innerName =
234                        name.substring(0, dot) + DOLLAR_SIGN + name.substring(dot + 1);
235                    classObj = resolveQualifiedName(innerName);
236                }
237            }
238        }
239        catch (final ClassNotFoundException ex) {
240            // we shouldn't get this exception here,
241            // so this is unexpected runtime exception
242            throw new IllegalStateException(ex);
243        }
244        return classObj;
245    }
246
247}