1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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 }