//////////////////////////////////////////////////////////////////////////////// // checkstyle: Checks Java source code for adherence to a set of rules. // Copyright (C) 2001-2017 the original author or authors. // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //////////////////////////////////////////////////////////////////////////////// package com.puppycrawl.tools.checkstyle.checks; import java.util.HashSet; import java.util.Set; /** * Utility class to resolve a class name to an actual class. Note that loaded * classes are not initialized. * <p>Limitations: this does not handle inner classes very well.</p> * * @author Oliver Burn */ public class ClassResolver { /** Period literal. */ private static final String PERIOD = "."; /** Dollar sign literal. */ private static final String DOLLAR_SIGN = "$"; /** Name of the package to check if the class belongs to. **/ private final String pkg; /** Set of imports to check against. **/ private final Set<String> imports; /** Use to load classes. **/ private final ClassLoader loader; /** * Creates a new {@code ClassResolver} instance. * * @param loader the ClassLoader to load classes with. * @param pkg the name of the package the class may belong to * @param imports set of imports to check if the class belongs to */ public ClassResolver(ClassLoader loader, String pkg, Set<String> imports) { this.loader = loader; this.pkg = pkg; this.imports = new HashSet<>(imports); this.imports.add("java.lang.*"); } /** * Attempts to resolve the Class for a specified name. The algorithm is * to check: * - fully qualified name * - explicit imports * - enclosing package * - star imports * @param name name of the class to resolve * @param currentClass name of current class (for inner classes). * @return the resolved class * @throws ClassNotFoundException if unable to resolve the class */ // -@cs[ForbidWildcardAsReturnType] This method can return any type, so no way to avoid wildcard public Class<?> resolve(String name, String currentClass) throws ClassNotFoundException { // See if the class is full qualified Class<?> clazz = resolveQualifiedName(name); if (clazz == null) { // try matching explicit imports clazz = resolveMatchingExplicitImport(name); if (clazz == null) { // See if in the package clazz = resolveInPackage(name); if (clazz == null) { // see if inner class of this class clazz = resolveInnerClass(name, currentClass); if (clazz == null) { clazz = resolveByStarImports(name); // -@cs[NestedIfDepth] it is better to have single return point from method if (clazz == null) { // Giving up, the type is unknown, so load the class to generate an // exception clazz = safeLoad(name); } } } } } return clazz; } /** * Try to find class by search in package. * @param name class name * @return class object */ private Class<?> resolveInPackage(String name) { Class<?> clazz = null; if (pkg != null && !pkg.isEmpty()) { final Class<?> classFromQualifiedName = resolveQualifiedName(pkg + PERIOD + name); if (classFromQualifiedName != null) { clazz = classFromQualifiedName; } } return clazz; } /** * Try to find class by matching explicit Import. * @param name class name * @return class object */ private Class<?> resolveMatchingExplicitImport(String name) { Class<?> clazz = null; for (String imp : imports) { // Very important to add the "." in the check below. Otherwise you // when checking for "DataException", it will match on // "SecurityDataException". This has been the cause of a very // difficult bug to resolve! if (imp.endsWith(PERIOD + name)) { clazz = resolveQualifiedName(imp); if (clazz != null) { break; } } } return clazz; } /** * See if inner class of this class. * @param name name of the search Class to search * @param currentClass class where search in * @return class if found , or null if not resolved * @throws ClassNotFoundException if an error occurs */ private Class<?> resolveInnerClass(String name, String currentClass) throws ClassNotFoundException { Class<?> clazz = null; if (!currentClass.isEmpty()) { String innerClass = currentClass + DOLLAR_SIGN + name; if (!pkg.isEmpty()) { innerClass = pkg + PERIOD + innerClass; } if (isLoadable(innerClass)) { clazz = safeLoad(innerClass); } } return clazz; } /** * Try star imports. * @param name name of the Class to search * @return class if found , or null if not resolved */ private Class<?> resolveByStarImports(String name) { Class<?> clazz = null; for (String imp : imports) { if (imp.endsWith(".*")) { final String fqn = imp.substring(0, imp.lastIndexOf('.') + 1) + name; clazz = resolveQualifiedName(fqn); if (clazz != null) { break; } } } return clazz; } /** * @param name name of the class to check * @return whether a specified class is loadable with safeLoad(). */ public boolean isLoadable(String name) { boolean result; try { safeLoad(name); result = true; } catch (final ClassNotFoundException | NoClassDefFoundError ignored) { result = false; } return result; } /** * Will load a specified class is such a way that it will NOT be * initialised. * @param name name of the class to load * @return the {@code Class} for the specified class * @throws ClassNotFoundException if an error occurs * @throws NoClassDefFoundError if an error occurs */ // -@cs[ForbidWildcardAsReturnType] The class is deprecated and will be removed soon. private Class<?> safeLoad(String name) throws ClassNotFoundException, NoClassDefFoundError { // The next line will load the class using the specified class // loader. The magic is having the "false" parameter. This means the // class will not be initialised. Very, very important. return Class.forName(name, false, loader); } /** * Tries to resolve a class for fully-specified name. * @param name a given name of class. * @return Class object for the given name or null. */ private Class<?> resolveQualifiedName(final String name) { Class<?> classObj = null; try { if (isLoadable(name)) { classObj = safeLoad(name); } else { //Perhaps it's fully-qualified inner class final int dot = name.lastIndexOf('.'); if (dot != -1) { final String innerName = name.substring(0, dot) + DOLLAR_SIGN + name.substring(dot + 1); classObj = resolveQualifiedName(innerName); } } } catch (final ClassNotFoundException ex) { // we shouldn't get this exception here, // so this is unexpected runtime exception throw new IllegalStateException(ex); } return classObj; } }