1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.javadoc;
21  
22  import java.util.HashSet;
23  import java.util.Set;
24  
25  /**
26   * Utility class to resolve a class name to an actual class. Note that loaded
27   * classes are not initialized.
28   * <p>Limitations: this does not handle inner classes very well.</p>
29   *
30   */
31  public class ClassResolver {
32  
33      /** Period literal. */
34      private static final String PERIOD = ".";
35      /** Dollar sign literal. */
36      private static final String DOLLAR_SIGN = "$";
37  
38      /** Name of the package to check if the class belongs to. **/
39      private final String pkg;
40      /** Set of imports to check against. **/
41      private final Set<String> imports;
42      /** Use to load classes. **/
43      private final ClassLoader loader;
44  
45      /**
46       * Creates a new {@code ClassResolver} instance.
47       *
48       * @param loader the ClassLoader to load classes with.
49       * @param pkg the name of the package the class may belong to
50       * @param imports set of imports to check if the class belongs to
51       */
52      public ClassResolver(ClassLoader loader, String pkg, Set<String> imports) {
53          this.loader = loader;
54          this.pkg = pkg;
55          this.imports = new HashSet<>(imports);
56          this.imports.add("java.lang.*");
57      }
58  
59      /**
60       * Attempts to resolve the Class for a specified name. The algorithm is
61       * to check:
62       * - fully qualified name
63       * - explicit imports
64       * - enclosing package
65       * - star imports
66       * @param name name of the class to resolve
67       * @param currentClass name of current class (for inner classes).
68       * @return the resolved class
69       * @throws ClassNotFoundException if unable to resolve the class
70       */
71      // -@cs[ForbidWildcardAsReturnType] This method can return any type, so no way to avoid wildcard
72      public Class<?> resolve(String name, String currentClass)
73              throws ClassNotFoundException {
74          // See if the class is full qualified
75          Class<?> clazz = resolveQualifiedName(name);
76          if (clazz == null) {
77              // try matching explicit imports
78              clazz = resolveMatchingExplicitImport(name);
79  
80              if (clazz == null) {
81                  // See if in the package
82                  clazz = resolveInPackage(name);
83  
84                  if (clazz == null) {
85                      // see if inner class of this class
86                      clazz = resolveInnerClass(name, currentClass);
87  
88                      if (clazz == null) {
89                          clazz = resolveByStarImports(name);
90                          // -@cs[NestedIfDepth] it is better to have single return point from method
91                          if (clazz == null) {
92                              throw new ClassNotFoundException(name);
93                          }
94                      }
95                  }
96              }
97          }
98          return clazz;
99      }
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 }