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}