View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.tools.plugin.extractor.annotations;
20  
21  import java.io.Closeable;
22  import java.io.File;
23  import java.io.IOException;
24  import java.lang.reflect.Method;
25  import java.net.URL;
26  import java.net.URLClassLoader;
27  import java.nio.charset.Charset;
28  import java.nio.file.Files;
29  import java.nio.file.Path;
30  import java.util.ArrayList;
31  import java.util.Collection;
32  import java.util.Collections;
33  import java.util.Enumeration;
34  import java.util.LinkedHashMap;
35  import java.util.LinkedHashSet;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.Optional;
39  import java.util.Set;
40  import java.util.jar.JarEntry;
41  import java.util.jar.JarFile;
42  import java.util.stream.Collectors;
43  import java.util.stream.Stream;
44  
45  import com.github.javaparser.ParseResult;
46  import com.github.javaparser.ParserConfiguration;
47  import com.github.javaparser.ast.CompilationUnit;
48  import com.github.javaparser.ast.Node;
49  import com.github.javaparser.ast.body.TypeDeclaration;
50  import com.github.javaparser.ast.modules.ModuleExportsDirective;
51  import com.github.javaparser.resolution.TypeSolver;
52  import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration;
53  import com.github.javaparser.resolution.model.SymbolReference;
54  import com.github.javaparser.symbolsolver.JavaSymbolSolver;
55  import com.github.javaparser.symbolsolver.resolution.typesolvers.ClassLoaderTypeSolver;
56  import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver;
57  import com.github.javaparser.symbolsolver.resolution.typesolvers.JarTypeSolver;
58  import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver;
59  import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;
60  import com.github.javaparser.utils.SourceRoot;
61  import org.slf4j.Logger;
62  import org.slf4j.LoggerFactory;
63  
64  /** Source declarations and type resolution used while extracting Javadocs. */
65  public final class JavaSourceModel implements Closeable {
66  
67      private static final Logger LOGGER = LoggerFactory.getLogger(JavaSourceModel.class);
68  
69      private static final Set<String> JAVA_RUNTIME_PACKAGES = javaRuntimePackages();
70  
71      private final Charset encoding;
72      private final Set<Path> sourceDirectories = new LinkedHashSet<>();
73      private final Set<Path> classPathEntries = new LinkedHashSet<>();
74      private final Map<String, TypeDeclaration<?>> types = new LinkedHashMap<>();
75      private final Map<String, String> modulesByExportedPackage = new LinkedHashMap<>();
76      private final Set<String> packages = new LinkedHashSet<>(JAVA_RUNTIME_PACKAGES);
77      private final Set<String> internalPackages = new LinkedHashSet<>();
78  
79      private ParserConfiguration parserConfiguration;
80      private CombinedTypeSolver typeSolver;
81      private URLClassLoader classPathLoader;
82      private boolean parsed;
83  
84      public JavaSourceModel(Charset encoding) {
85          this.encoding = encoding;
86      }
87  
88      public void addSourceDirectory(File directory) throws IOException {
89          if (directory != null && directory.isDirectory()) {
90              sourceDirectories.add(directory.toPath().toRealPath());
91          }
92      }
93  
94      public void addClassPathEntry(File entry) throws IOException {
95          if (entry != null && entry.exists()) {
96              classPathEntries.add(entry.toPath().toRealPath());
97          }
98      }
99  
100     public void parse() throws IOException {
101         if (parsed) {
102             return;
103         }
104         parsed = true;
105 
106         parserConfiguration = new ParserConfiguration()
107                 .setCharacterEncoding(encoding)
108                 .setLanguageLevel(ParserConfiguration.LanguageLevel.BLEEDING_EDGE);
109         typeSolver = new CombinedTypeSolver();
110         for (Path sourceDirectory : sourceDirectories) {
111             typeSolver.add(new JavaParserTypeSolver(sourceDirectory, parserConfiguration));
112         }
113 
114         List<URL> classPathUrls = new ArrayList<>();
115         for (Path classPathEntry : classPathEntries) {
116             if (Files.isDirectory(classPathEntry)) {
117                 classPathUrls.add(classPathEntry.toUri().toURL());
118                 indexClassDirectory(classPathEntry);
119             } else if (classPathEntry.getFileName().toString().endsWith(".jar")) {
120                 classPathUrls.add(classPathEntry.toUri().toURL());
121                 JarTypeSolver jarTypeSolver = new JarTypeSolver(classPathEntry);
122                 typeSolver.add(jarTypeSolver);
123                 jarTypeSolver.getKnownClasses().stream()
124                         .map(JavaSourceModel::packageName)
125                         .filter(name -> !name.isEmpty())
126                         .forEach(packages::add);
127             }
128         }
129         if (!classPathUrls.isEmpty()) {
130             classPathLoader = new URLClassLoader(classPathUrls.toArray(new URL[0]), ClassLoader.getSystemClassLoader());
131             typeSolver.add(new ClassLoaderTypeSolver(classPathLoader));
132         }
133         typeSolver.add(new ReflectionTypeSolver(ReflectionTypeSolver.JCL_ONLY));
134         parserConfiguration.setSymbolResolver(new JavaSymbolSolver(typeSolver));
135 
136         for (Path sourceDirectory : sourceDirectories) {
137             SourceRoot sourceRoot = new SourceRoot(sourceDirectory, parserConfiguration);
138             for (ParseResult<CompilationUnit> result : sourceRoot.tryToParse()) {
139                 if (!result.isSuccessful()) {
140                     String path = sourcePath(result, sourceDirectory);
141                     String problems = result.getProblems().stream()
142                             .map(Object::toString)
143                             .collect(Collectors.joining(System.lineSeparator()));
144                     LOGGER.warn(
145                             "Unable to parse {}. Javadoc from this source file will be skipped.{}",
146                             path,
147                             problems.isEmpty() ? "" : System.lineSeparator() + problems);
148                     continue;
149                 }
150                 if (result.getResult().isPresent()) {
151                     index(result.getResult().get());
152                 } else {
153                     LOGGER.warn(
154                             "Parser returned no compilation unit for {}. Javadoc from this source file will be skipped.",
155                             sourcePath(result, sourceDirectory));
156                 }
157             }
158         }
159     }
160 
161     private static String sourcePath(ParseResult<CompilationUnit> result, Path sourceDirectory) {
162         return result.getSourcePath().map(Path::toString).orElse(sourceDirectory.toString());
163     }
164 
165     private void indexClassDirectory(Path directory) throws IOException {
166         indexClassDirectory(directory, packages);
167     }
168 
169     private static void indexClassDirectory(Path directory, Set<String> result) throws IOException {
170         try (Stream<Path> entries = Files.walk(directory)) {
171             entries.filter(Files::isRegularFile)
172                     .map(directory::relativize)
173                     .filter(path -> path.getFileName().toString().endsWith(".class"))
174                     .map(Path::getParent)
175                     .filter(path -> path != null)
176                     .map(Path::toString)
177                     .map(name -> name.replace(File.separatorChar, '.'))
178                     .filter(name -> !name.isEmpty())
179                     .forEach(result::add);
180         }
181     }
182 
183     private void index(CompilationUnit unit) {
184         unit.getPackageDeclaration()
185                 .map(declaration -> declaration.getName().asString())
186                 .ifPresent(name -> {
187                     packages.add(name);
188                     internalPackages.add(name);
189                 });
190         for (TypeDeclaration<?> type : unit.findAll(TypeDeclaration.class)) {
191             type.getFullyQualifiedName().ifPresent(name -> types.putIfAbsent(name, type));
192         }
193         unit.getModule()
194                 .ifPresent(module -> module.getDirectives().stream()
195                         .filter(ModuleExportsDirective.class::isInstance)
196                         .map(ModuleExportsDirective.class::cast)
197                         .forEach(exports -> modulesByExportedPackage.putIfAbsent(
198                                 exports.getName().asString(), module.getName().asString())));
199     }
200 
201     public Collection<TypeDeclaration<?>> getTypes() {
202         return Collections.unmodifiableCollection(types.values());
203     }
204 
205     public Optional<TypeDeclaration<?>> getType(String fullyQualifiedName) {
206         return Optional.ofNullable(types.get(fullyQualifiedName));
207     }
208 
209     public Optional<TypeDeclaration<?>> getType(ResolvedReferenceTypeDeclaration declaration) {
210         return getType(declaration.getQualifiedName());
211     }
212 
213     public Optional<String> getModuleName(String packageName) {
214         return Optional.ofNullable(modulesByExportedPackage.get(packageName));
215     }
216 
217     public boolean hasPackage(String packageName) {
218         return packages.contains(packageName);
219     }
220 
221     public boolean isInternal(ResolvedReferenceTypeDeclaration declaration) {
222         return types.containsKey(declaration.getQualifiedName());
223     }
224 
225     public boolean isInternalPackage(String packageName) {
226         return internalPackages.contains(packageName);
227     }
228 
229     public Optional<ResolvedReferenceTypeDeclaration> resolveType(String fullyQualifiedName) {
230         ensureParsed();
231         SymbolReference<ResolvedReferenceTypeDeclaration> reference = typeSolver.tryToSolveType(fullyQualifiedName);
232         return reference.isSolved() ? Optional.of(reference.getCorrespondingDeclaration()) : Optional.empty();
233     }
234 
235     public TypeSolver getTypeSolver() {
236         ensureParsed();
237         return typeSolver;
238     }
239 
240     public String getLocation(Node node, int fallbackLine) {
241         int line = fallbackLine > 0
242                 ? fallbackLine
243                 : node.getBegin().map(position -> position.line).orElse(0);
244         return node.findCompilationUnit()
245                 .flatMap(CompilationUnit::getStorage)
246                 .map(storage -> java.nio.file.Paths.get("")
247                                 .toAbsolutePath()
248                                 .toUri()
249                                 .relativize(storage.getPath().toUri())
250                                 .toString()
251                         + ":" + line)
252                 .orElse("unknown:" + line);
253     }
254 
255     private void ensureParsed() {
256         if (!parsed) {
257             throw new IllegalStateException("Java source model has not been parsed");
258         }
259     }
260 
261     private static String packageName(String className) {
262         int separator = className.lastIndexOf('.');
263         return separator > 0 ? className.substring(0, separator) : "";
264     }
265 
266     private static Set<String> javaRuntimePackages() {
267         Set<String> result = new LinkedHashSet<>();
268         try {
269             Class<?> moduleLayerClass = Class.forName("java.lang.ModuleLayer");
270             Class<?> moduleClass = Class.forName("java.lang.Module");
271             Object bootLayer = moduleLayerClass.getMethod("boot").invoke(null);
272             Collection<?> modules =
273                     (Collection<?>) moduleLayerClass.getMethod("modules").invoke(bootLayer);
274             Method getPackages = moduleClass.getMethod("getPackages");
275             for (Object module : modules) {
276                 for (Object packageName : (Set<?>) getPackages.invoke(module)) {
277                     result.add((String) packageName);
278                 }
279             }
280         } catch (ClassNotFoundException e) {
281             indexBootClassPathPackages(result);
282         } catch (ReflectiveOperationException e) {
283             LOGGER.warn("Could not index packages from the Java runtime module layer", e);
284         }
285         return Collections.unmodifiableSet(result);
286     }
287 
288     private static void indexBootClassPathPackages(Set<String> result) {
289         String bootClassPath = System.getProperty("sun.boot.class.path");
290         if (bootClassPath == null) {
291             return;
292         }
293         for (String entry : bootClassPath.split(java.util.regex.Pattern.quote(File.pathSeparator))) {
294             Path path = new File(entry).toPath();
295             try {
296                 if (Files.isDirectory(path)) {
297                     indexClassDirectory(path, result);
298                 } else if (Files.isRegularFile(path)) {
299                     indexJar(path, result);
300                 }
301             } catch (IOException e) {
302                 LOGGER.debug("Could not index Java runtime packages from {}", path, e);
303             }
304         }
305     }
306 
307     private static void indexJar(Path jar, Set<String> result) throws IOException {
308         try (JarFile jarFile = new JarFile(jar.toFile())) {
309             Enumeration<JarEntry> entries = jarFile.entries();
310             while (entries.hasMoreElements()) {
311                 JarEntry entry = entries.nextElement();
312                 String name = entry.getName();
313                 if (!entry.isDirectory() && name.endsWith(".class")) {
314                     int separator = name.lastIndexOf('/');
315                     if (separator > 0) {
316                         result.add(name.substring(0, separator).replace('/', '.'));
317                     }
318                 }
319             }
320         }
321     }
322 
323     @Override
324     public void close() throws IOException {
325         if (classPathLoader != null) {
326             classPathLoader.close();
327         }
328     }
329 }