/* * Copyright (C) 2015 Sebastian Daschner, sebastian-daschner.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.sebastian_daschner.jaxrs_analyzer.analysis; import com.sebastian_daschner.jaxrs_analyzer.LogProvider; import com.sebastian_daschner.jaxrs_analyzer.analysis.bytecode.BytecodeAnalyzer; import com.sebastian_daschner.jaxrs_analyzer.analysis.classes.ContextClassReader; import com.sebastian_daschner.jaxrs_analyzer.analysis.classes.JAXRSClassVisitor; import com.sebastian_daschner.jaxrs_analyzer.analysis.javadoc.JavaDocAnalyzer; import com.sebastian_daschner.jaxrs_analyzer.analysis.results.ResultInterpreter; import com.sebastian_daschner.jaxrs_analyzer.model.rest.Resources; import com.sebastian_daschner.jaxrs_analyzer.model.results.ClassResult; import com.sebastian_daschner.jaxrs_analyzer.utils.Pair; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import javax.ws.rs.ApplicationPath; import java.io.File; import java.io.IOException; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Enumeration; import java.util.HashSet; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.jar.JarEntry; import java.util.jar.JarFile; import static com.sebastian_daschner.jaxrs_analyzer.model.JavaUtils.isAnnotationPresent; /** * Analyzes the JAX-RS project. This class is thread-safe. * * @author Sebastian Daschner */ public class ProjectAnalyzer { // TODO test following scenario: // 2 Maven modules -> a, b; a needs b // b contains interface with @Path & resource methods // a contains impl of iface without annotations // b should have result private final Lock lock = new ReentrantLock(); private final Set<String> classes = new HashSet<>(); private final Set<String> packages = new HashSet<>(); private final Set<Path> classPool = new HashSet<>(); private final ResultInterpreter resultInterpreter = new ResultInterpreter(); private final BytecodeAnalyzer bytecodeAnalyzer = new BytecodeAnalyzer(); private final JavaDocAnalyzer javaDocAnalyzer = new JavaDocAnalyzer(); /** * Creates a project analyzer with given class path locations where to search for classes. * * @param classPaths The locations of additional class paths (can be directories or jar-files) */ public ProjectAnalyzer(final Set<Path> classPaths) { classPaths.forEach(this::addToClassPool); final Path lib = Paths.get(System.getProperty("java.home"), "..", "lib", "tools.jar"); addToClassPool(lib); addToSystemClassLoader(lib); } /** * Analyzes all classes in the given project path. * * @param projectClassPaths The project class paths * @param projectSourcePaths The project source file paths * @return The REST resource representations */ public Resources analyze(final Set<Path> projectClassPaths, final Set<Path> projectSourcePaths) { lock.lock(); try { projectClassPaths.forEach(this::addProjectPath); // analyze relevant classes final JobRegistry jobRegistry = JobRegistry.getInstance(); final Set<ClassResult> classResults = new HashSet<>(); classes.stream().filter(this::isJAXRSRootResource).forEach(c -> jobRegistry.analyzeResourceClass(c, new ClassResult())); Pair<String, ClassResult> classResultPair; while ((classResultPair = jobRegistry.nextUnhandledClass()) != null) { final ClassResult classResult = classResultPair.getRight(); classResults.add(classResult); analyzeClass(classResultPair.getLeft(), classResult); bytecodeAnalyzer.analyzeBytecode(classResult); } javaDocAnalyzer.analyze(classResults, packages, projectSourcePaths, classPool); return resultInterpreter.interpret(classResults); } finally { lock.unlock(); } } private boolean isJAXRSRootResource(String className) { try { final Class<?> clazz = ContextClassReader.getClassLoader().loadClass(className); return isAnnotationPresent(clazz, javax.ws.rs.Path.class) || isAnnotationPresent(clazz, ApplicationPath.class); } catch (ClassNotFoundException e) { LogProvider.error("The class " + className + " could not be loaded!"); LogProvider.debug(e); return false; } } private void analyzeClass(final String className, ClassResult classResult) { try { final ClassReader classReader = new ContextClassReader(className); final ClassVisitor visitor = new JAXRSClassVisitor(classResult); classReader.accept(visitor, ClassReader.EXPAND_FRAMES); } catch (IOException e) { LogProvider.error("The class " + className + " could not be loaded!"); LogProvider.debug(e); } } /** * Adds the location to the class pool. * * @param location The location of a jar file or a directory */ private void addToClassPool(final Path location) { if (!location.toFile().exists()) throw new IllegalArgumentException("The location '" + location + "' does not exist!"); classPool.add(location); try { ContextClassReader.addClassPath(location.toUri().toURL()); } catch (Exception e) { throw new IllegalArgumentException("The location '" + location + "' could not be loaded to the class path!", e); } } private void addToSystemClassLoader(final Path location) { try { final Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); method.setAccessible(true); method.invoke(ClassLoader.getSystemClassLoader(), location.toUri().toURL()); } catch (Exception e) { throw new IllegalArgumentException("The location '" + location + "' could not be loaded to the class path!", e); } } /** * Adds the project paths and loads all classes. * * @param path The project path */ private void addProjectPath(final Path path) { addToClassPool(path); if (path.toFile().isFile() && path.toString().endsWith(".jar")) { addJarClasses(path); } else if (path.toFile().isDirectory()) { addDirectoryClasses(path, Paths.get("")); } else { throw new IllegalArgumentException("The project path '" + path + "' must be a jar file or a directory"); } } /** * Adds all classes in the given jar-file location to the set of known classes. * * @param location The location of the jar-file */ private void addJarClasses(final Path location) { try (final JarFile jarFile = new JarFile(location.toFile())) { final Enumeration<JarEntry> entries = jarFile.entries(); while (entries.hasMoreElements()) { final JarEntry entry = entries.nextElement(); final String entryName = entry.getName(); if (entryName.endsWith(".class")) classes.add(toQualifiedClassName(entryName)); else if (entry.isDirectory()) packages.add(entryName); } } catch (IOException e) { throw new IllegalArgumentException("Could not read jar-file '" + location + "', reason: " + e.getMessage()); } } /** * Adds all classes in the given directory location to the set of known classes. * * @param location The location of the current directory * @param subPath The sub-path which is relevant for the package names or {@code null} if currently in the root directory */ private void addDirectoryClasses(final Path location, final Path subPath) { for (final File file : location.toFile().listFiles()) { if (file.isDirectory()) addDirectoryClasses(location.resolve(file.getName()), subPath.resolve(file.getName())); else if (file.isFile() && file.getName().endsWith(".class")) { packages.add(toQualifiedPackageName(subPath.toString())); final String classFileName = subPath.resolve(file.getName()).toString(); classes.add(toQualifiedClassName(classFileName)); } } } /** * Converts the given file name of a class-file to the fully-qualified class name. * * @param fileName The file name (e.g. a/package/AClass.class) * @return The fully-qualified class name (e.g. a.package.AClass) */ private static String toQualifiedClassName(final String fileName) { final String replacedSeparators = fileName.replace(File.separatorChar, '.'); return replacedSeparators.substring(0, replacedSeparators.length() - ".class".length()); } /** * Converts the given path name of a directory to the fully-qualified package name. * * @param pathName The directory name (e.g. a/package/) * @return The fully-qualified package name (e.g. a.package) */ private static String toQualifiedPackageName(final String pathName) { return pathName.replace(File.separatorChar, '.'); } }