001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.maven.tools.plugin.extractor.annotations; 020 021import java.io.Closeable; 022import java.io.File; 023import java.io.IOException; 024import java.lang.reflect.Method; 025import java.net.URL; 026import java.net.URLClassLoader; 027import java.nio.charset.Charset; 028import java.nio.file.Files; 029import java.nio.file.Path; 030import java.util.ArrayList; 031import java.util.Collection; 032import java.util.Collections; 033import java.util.Enumeration; 034import java.util.LinkedHashMap; 035import java.util.LinkedHashSet; 036import java.util.List; 037import java.util.Map; 038import java.util.Optional; 039import java.util.Set; 040import java.util.jar.JarEntry; 041import java.util.jar.JarFile; 042import java.util.stream.Collectors; 043import java.util.stream.Stream; 044 045import com.github.javaparser.ParseResult; 046import com.github.javaparser.ParserConfiguration; 047import com.github.javaparser.ast.CompilationUnit; 048import com.github.javaparser.ast.Node; 049import com.github.javaparser.ast.body.TypeDeclaration; 050import com.github.javaparser.ast.modules.ModuleExportsDirective; 051import com.github.javaparser.resolution.TypeSolver; 052import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; 053import com.github.javaparser.resolution.model.SymbolReference; 054import com.github.javaparser.symbolsolver.JavaSymbolSolver; 055import com.github.javaparser.symbolsolver.resolution.typesolvers.ClassLoaderTypeSolver; 056import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver; 057import com.github.javaparser.symbolsolver.resolution.typesolvers.JarTypeSolver; 058import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver; 059import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver; 060import com.github.javaparser.utils.SourceRoot; 061import org.slf4j.Logger; 062import org.slf4j.LoggerFactory; 063 064/** Source declarations and type resolution used while extracting Javadocs. */ 065public final class JavaSourceModel implements Closeable { 066 067 private static final Logger LOGGER = LoggerFactory.getLogger(JavaSourceModel.class); 068 069 private static final Set<String> JAVA_RUNTIME_PACKAGES = javaRuntimePackages(); 070 071 private final Charset encoding; 072 private final Set<Path> sourceDirectories = new LinkedHashSet<>(); 073 private final Set<Path> classPathEntries = new LinkedHashSet<>(); 074 private final Map<String, TypeDeclaration<?>> types = new LinkedHashMap<>(); 075 private final Map<String, String> modulesByExportedPackage = new LinkedHashMap<>(); 076 private final Set<String> packages = new LinkedHashSet<>(JAVA_RUNTIME_PACKAGES); 077 private final Set<String> internalPackages = new LinkedHashSet<>(); 078 079 private ParserConfiguration parserConfiguration; 080 private CombinedTypeSolver typeSolver; 081 private URLClassLoader classPathLoader; 082 private boolean parsed; 083 084 public JavaSourceModel(Charset encoding) { 085 this.encoding = encoding; 086 } 087 088 public void addSourceDirectory(File directory) throws IOException { 089 if (directory != null && directory.isDirectory()) { 090 sourceDirectories.add(directory.toPath().toRealPath()); 091 } 092 } 093 094 public void addClassPathEntry(File entry) throws IOException { 095 if (entry != null && entry.exists()) { 096 classPathEntries.add(entry.toPath().toRealPath()); 097 } 098 } 099 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}