/* * SonarQube Java * Copyright (C) 2012-2016 SonarSource SA * mailto:contact AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.java; import com.google.common.base.Splitter; import org.apache.commons.lang.StringUtils; import org.sonar.api.batch.BatchSide; import org.sonar.api.batch.fs.FileSystem; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.config.Settings; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonarsource.api.sonarlint.SonarLintSide; import java.io.File; import java.io.IOException; import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream.Filter; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @BatchSide @SonarLintSide public abstract class AbstractJavaClasspath { private static final char SEPARATOR = ','; private static final char UNIX_SEPARATOR = '/'; private static final char WINDOWS_SEPARATOR = '\\'; private static final Logger LOG = Loggers.get(AbstractJavaClasspath.class); protected final Settings settings; protected final FileSystem fs; private final InputFile.Type fileType; private static final Path[] STANDARD_CLASSES_DIRS = {Paths.get("target", "classes"), Paths.get("target", "test-classes")}; protected List<File> binaries; protected List<File> elements; protected boolean validateLibraries; protected boolean initialized; public AbstractJavaClasspath(Settings settings, FileSystem fs, InputFile.Type fileType) { this.settings = settings; this.fs = fs; this.fileType = fileType; initialized = false; } protected abstract void init(); protected Set<File> getFilesFromProperty(String property) { Set<File> result = new LinkedHashSet<>(); String fileList = settings.getString(property); if (StringUtils.isNotEmpty(fileList)) { Iterable<String> fileNames = Splitter.on(SEPARATOR).omitEmptyStrings().split(fileList); File baseDir = fs.baseDir(); boolean hasJavaSources = hasJavaSources(); boolean validateLibs = validateLibraries; boolean isLibraryProperty = property.endsWith("libraries"); for (String pathPattern : fileNames) { Set<File> libraryFilesForPattern = getFilesForPattern(baseDir.toPath(), pathPattern, isLibraryProperty); if (validateLibraries && libraryFilesForPattern.isEmpty() && hasJavaSources) { LOG.error("Invalid value for " + property); String message = "No files nor directories matching '" + pathPattern + "'"; throw new IllegalStateException(message); } validateLibraries = validateLibs; result.addAll(libraryFilesForPattern); } } return result; } protected boolean hasJavaSources() { return fs.hasFiles(fs.predicates().and(fs.predicates().hasLanguage("java"), fs.predicates().hasType(fileType))); } private Set<File> getFilesForPattern(Path baseDir, String pathPattern, boolean libraryProperty) { try { Path filePath = resolvePath(baseDir, pathPattern); File file = filePath.toFile(); if(file.isFile()) { return getMatchingFile(pathPattern, file); } if (file.isDirectory()) { return getMatchesInDir(filePath, libraryProperty); } } catch (IOException | InvalidPathException e) { // continue } String dirPath = sanitizeWildcards(pathPattern); String fileNamePattern = pathPattern; int lastPathSeparator = Math.max(dirPath.lastIndexOf(UNIX_SEPARATOR), dirPath.lastIndexOf(WINDOWS_SEPARATOR)); if (lastPathSeparator == -1) { dirPath = "."; } else { dirPath = pathPattern.substring(0, lastPathSeparator); fileNamePattern = pathPattern.substring(lastPathSeparator + 1); } Path dir = resolvePath(baseDir, dirPath); return getFilesInDir(dir, fileNamePattern, libraryProperty); } private static Set<File> getFilesInDir(Path dir, String fileNamePattern, boolean libraryProperty) { if (!dir.toFile().isDirectory()) { return Collections.emptySet(); } try { if (libraryProperty) { return getMatchingLibraries(fileNamePattern, dir); } else { return getMatchingDirs(fileNamePattern, dir); } } catch (IOException e) { throw new IllegalStateException(e); } } private static String sanitizeWildcards(String pathPattern) { int wildcardIndex = pathPattern.indexOf('*'); if (wildcardIndex >= 0) { return pathPattern.substring(0, wildcardIndex); } return pathPattern; } private Set<File> getMatchingFile(String pathPattern, File file) { if (pathPattern.endsWith(".jar") || pathPattern.endsWith(".zip") || pathPattern.endsWith(".aar")) { return Collections.singleton(file); } LOG.debug("File " + file.getAbsolutePath() + " was ignored from java classpath"); validateLibraries = false; return Collections.emptySet(); } private static Set<File> getMatchingDirs(String pattern, Path dir) throws IOException { if (!StringUtils.isEmpty(pattern)) { // find all dirs and subdirs that match the pattern PathMatcher matcher = FileSystems.getDefault().getPathMatcher(getGlob(dir, pattern)); return new DirFinder().find(dir, matcher); } else { // no pattern, so we just return dir return Collections.singleton(dir.toFile()); } } private static Set<File> getMatchesInDir(Path dirPath, boolean isLibraryProperty) throws IOException { if (isLibraryProperty) { for (Path end : STANDARD_CLASSES_DIRS) { if (dirPath.endsWith(end)) { // don't scan these, as they should only contain .classes with paths starting from the root return Collections.singleton(dirPath.toFile()); } } Set<File> matches = new LibraryFinder().find(dirPath, p -> true); matches.add(dirPath.toFile()); return matches; } else { return Collections.singleton(dirPath.toFile()); } } private static String separatorsToUnix(final String path) { return path.replace(WINDOWS_SEPARATOR, UNIX_SEPARATOR); } private static String getGlob(Path dir, String pattern) { // globs work with unix separators return "glob:" + separatorsToUnix(dir.toString()) + UNIX_SEPARATOR + separatorsToUnix(pattern); } private static Set<File> getMatchingLibraries(String pattern, Path dir) throws IOException { Set<File> matches = new LinkedHashSet<>(); Set<File> dirs = getMatchingDirs(pattern, dir); PathMatcher matcher = FileSystems.getDefault().getPathMatcher(getGlob(dir, pattern)); for (File d : dirs) { matches.addAll(getLibs(d.toPath())); } matches.addAll(dirs); matches.addAll(new LibraryFinder().find(dir, matcher)); if(pattern.startsWith("**/")) { // match jar in the base dir when using wildcard matches.addAll(new LibraryFinder().find(dir, FileSystems.getDefault().getPathMatcher(getGlob(dir, pattern.substring(3))))); } return matches; } private static List<File> getLibs(Path dir) throws IOException { Filter<Path> filter = path -> { String name = path.getFileName().toString(); return name.endsWith(".jar") || name.endsWith(".zip") || name.endsWith(".aar"); }; List<File> files = new ArrayList<>(); try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, filter)) { stream.forEach(p -> files.add(p.toFile())); } return files; } private abstract static class AbstractFileFinder extends SimpleFileVisitor<Path> { protected Set<File> matchedFiles = new LinkedHashSet<>(); protected PathMatcher matcher; Set<File> find(Path dir, PathMatcher matcher) throws IOException { this.matcher = matcher; Files.walkFileTree(dir, this); return matchedFiles; } } private static class DirFinder extends AbstractFileFinder { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { if (matcher.matches(dir)) { matchedFiles.add(dir.toFile()); } return FileVisitResult.CONTINUE; } } private static class LibraryFinder extends AbstractFileFinder { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attr) { String name = file.getFileName().toString(); if ((name.endsWith(".jar") || name.endsWith(".zip")) && matcher.matches(file)) { matchedFiles.add(file.toFile()); } return FileVisitResult.CONTINUE; } } private static Path resolvePath(Path baseDir, String fileName) { Path filePath = Paths.get(fileName); if (!filePath.isAbsolute()) { filePath = baseDir.resolve(fileName); } return filePath.normalize(); } public List<File> getElements() { init(); return elements; } public List<File> getBinaryDirs() { init(); return binaries; } }