////////////////////////////////////////////////////////////////////////////////
// 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;
}
}