<version>1.9.5</version>
</dependency>
<dependency>
- <groupId>org.codehaus.sonar</groupId>
- <artifactId>sonar-home</artifactId>
- <version>5.2-SNAPSHOT</version>
- </dependency>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ <version>1.10</version>
+ </dependency>
</dependencies>
</dependencyManagement>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>
- <dependency>
- <groupId>org.codehaus.sonar</groupId>
- <artifactId>sonar-home</artifactId>
- </dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
- <version>1.7.1</version>
+ <version>2.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<artifactId>commons-lang</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <!-- used to compare results -->
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
*/
package org.sonar.runner.api;
+import org.sonar.runner.cache.Logger;
+
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;
-import org.sonar.home.cache.Logger;
class Dirs {
*/
package org.sonar.runner.api;
+import org.sonar.runner.cache.Logger;
+
import org.sonar.runner.impl.ClassloadRules;
import java.nio.charset.Charset;
import javax.annotation.Nullable;
-import org.sonar.home.cache.Logger;
import org.sonar.runner.batch.IsolatedLauncher;
import org.sonar.runner.impl.InternalProperties;
import org.sonar.runner.impl.IsolatedLauncherFactory;
*/
package org.sonar.runner.api;
-import org.sonar.home.cache.Logger;
+
+import org.sonar.runner.cache.Logger;
import java.io.PrintWriter;
import java.io.StringWriter;
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+package org.sonar.runner.cache;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class DeleteFileOnCloseInputStream extends InputStream {
+ private final InputStream is;
+ private final Path p;
+
+ public DeleteFileOnCloseInputStream(InputStream stream, Path p) {
+ this.is = stream;
+ this.p = p;
+ }
+
+ @Override
+ public int read() throws IOException {
+ return is.read();
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ return is.read(b);
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ return is.read(b, off, len);
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ return is.skip(n);
+ }
+
+ @Override
+ public synchronized void mark(int readlimit) {
+ is.mark(readlimit);
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ is.reset();
+ }
+
+ @Override
+ public int available() throws IOException {
+ return is.available();
+ }
+
+ @Override
+ public boolean markSupported() {
+ return is.markSupported();
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ super.close();
+ } finally {
+ Files.delete(p);
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+package org.sonar.runner.cache;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.RandomAccessFile;
+import java.io.StringWriter;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class DirectoryLock {
+ static final String LOCK_FILE_NAME = ".sonar_lock";
+ private final Path lockFilePath;
+ private final Logger logger;
+
+ private RandomAccessFile lockRandomAccessFile;
+ private FileChannel lockChannel;
+ private FileLock lockFile;
+
+ public DirectoryLock(Path directory, Logger logger) {
+ this.logger = logger;
+ this.lockFilePath = directory.resolve(LOCK_FILE_NAME).toAbsolutePath();
+ }
+
+ public String getFileLockName() {
+ return LOCK_FILE_NAME;
+ }
+
+ public void lock() {
+ try {
+ lockRandomAccessFile = new RandomAccessFile(lockFilePath.toFile(), "rw");
+ lockChannel = lockRandomAccessFile.getChannel();
+ lockFile = lockChannel.lock(0, 1024, false);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to create lock in " + lockFilePath.toString(), e);
+ }
+ }
+
+ public boolean tryLock() {
+ try {
+ lockRandomAccessFile = new RandomAccessFile(lockFilePath.toFile(), "rw");
+ lockChannel = lockRandomAccessFile.getChannel();
+ lockFile = lockChannel.tryLock(0, 1024, false);
+
+ return lockFile != null;
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to create lock in " + lockFilePath.toString(), e);
+ }
+ }
+
+ public void unlock() {
+ if (lockFile != null) {
+ try {
+ lockFile.release();
+ lockFile = null;
+ } catch (IOException e) {
+ logger.error("Error releasing lock", e);
+ }
+ }
+ if (lockChannel != null) {
+ try {
+ lockChannel.close();
+ lockChannel = null;
+ } catch (IOException e) {
+ logger.error("Error closing file channel", e);
+ }
+ }
+ if (lockRandomAccessFile != null) {
+ try {
+ lockRandomAccessFile.close();
+ lockRandomAccessFile = null;
+ } catch (IOException e) {
+ logger.error("Error closing file", e);
+ }
+ }
+
+ try {
+ Files.delete(lockFilePath);
+ } catch (IOException e) {
+ // ignore, as an error happens if another process just started to acquire the same lock
+ StringWriter errors = new StringWriter();
+ e.printStackTrace(new PrintWriter(errors));
+ logger.debug("Couldn't delete lock file: " + lockFilePath.toString() + " " + errors.toString());
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+package org.sonar.runner.cache;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import javax.annotation.CheckForNull;
+
+/**
+ * This class is responsible for managing Sonar batch file cache. You can put file into cache and
+ * later try to retrieve them. MD5 is used to differentiate files (name is not secure as files may come
+ * from different Sonar servers and have same name but be actually different, and same for SNAPSHOTs).
+ */
+public class FileCache {
+
+ /** Maximum loop count when creating temp directories. */
+ private static final int TEMP_DIR_ATTEMPTS = 10000;
+
+ private final File dir;
+ private final File tmpDir;
+ private final FileHashes hashes;
+ private final Logger logger;
+
+ FileCache(File dir, FileHashes fileHashes, Logger logger) {
+ this.hashes = fileHashes;
+ this.logger = logger;
+ this.dir = createDir(dir, "user cache");
+ logger.info(String.format("User cache: %s", dir.getAbsolutePath()));
+ this.tmpDir = createDir(new File(dir, "_tmp"), "temp dir");
+ }
+
+ public static FileCache create(File dir, Logger logger) {
+ return new FileCache(dir, new FileHashes(), logger);
+ }
+
+ public File getDir() {
+ return dir;
+ }
+
+ /**
+ * Look for a file in the cache by its filename and md5 checksum. If the file is not
+ * present then return null.
+ */
+ @CheckForNull
+ public File get(String filename, String hash) {
+ File cachedFile = new File(new File(dir, hash), filename);
+ if (cachedFile.exists()) {
+ return cachedFile;
+ }
+ logger.debug(String.format("No file found in the cache with name %s and hash %s", filename, hash));
+ return null;
+ }
+
+ public interface Downloader {
+ void download(String filename, File toFile) throws IOException;
+ }
+
+ public File get(String filename, String hash, Downloader downloader) {
+ // Does not fail if another process tries to create the directory at the same time.
+ File hashDir = hashDir(hash);
+ File targetFile = new File(hashDir, filename);
+ if (!targetFile.exists()) {
+ File tempFile = newTempFile();
+ download(downloader, filename, tempFile);
+ String downloadedHash = hashes.of(tempFile);
+ if (!hash.equals(downloadedHash)) {
+ throw new IllegalStateException("INVALID HASH: File " + tempFile.getAbsolutePath() + " was expected to have hash " + hash
+ + " but was downloaded with hash " + downloadedHash);
+ }
+ mkdirQuietly(hashDir);
+ renameQuietly(tempFile, targetFile);
+ }
+ return targetFile;
+ }
+
+ private void download(Downloader downloader, String filename, File tempFile) {
+ try {
+ downloader.download(filename, tempFile);
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to download " + filename + " to " + tempFile, e);
+ }
+ }
+
+ private void renameQuietly(File sourceFile, File targetFile) {
+ boolean rename = sourceFile.renameTo(targetFile);
+ // Check if the file was cached by another process during download
+ if (!rename && !targetFile.exists()) {
+ logger.warn(String.format("Unable to rename %s to %s", sourceFile.getAbsolutePath(), targetFile.getAbsolutePath()));
+ logger.warn("A copy/delete will be tempted but with no guarantee of atomicity");
+ try {
+ Files.move(sourceFile.toPath(), targetFile.toPath());
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to move " + sourceFile.getAbsolutePath() + " to " + targetFile, e);
+ }
+ }
+ }
+
+ private File hashDir(String hash) {
+ return new File(dir, hash);
+ }
+
+ private static void mkdirQuietly(File hashDir) {
+ try {
+ Files.createDirectories(hashDir.toPath());
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to create cache directory: " + hashDir, e);
+ }
+ }
+
+ private File newTempFile() {
+ try {
+ return File.createTempFile("fileCache", null, tmpDir);
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to create temp file in " + tmpDir, e);
+ }
+ }
+
+ public File createTempDir() {
+ String baseName = System.currentTimeMillis() + "-";
+
+ for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
+ File tempDir = new File(tmpDir, baseName + counter);
+ if (tempDir.mkdir()) {
+ return tempDir;
+ }
+ }
+ throw new IllegalStateException("Failed to create directory in " + tmpDir);
+ }
+
+ private File createDir(File dir, String debugTitle) {
+ if (!dir.isDirectory() || !dir.exists()) {
+ logger.debug("Create : " + dir.getAbsolutePath());
+ try {
+ Files.createDirectories(dir.toPath());
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to create " + debugTitle + dir.getAbsolutePath(), e);
+ }
+ }
+ return dir;
+ }
+}
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+package org.sonar.runner.cache;
+
+import java.io.File;
+
+import javax.annotation.Nullable;
+
+public class FileCacheBuilder {
+ private final Logger logger;
+ private File userHome;
+
+ public FileCacheBuilder(Logger logger) {
+ this.logger = logger;
+ }
+
+ public FileCacheBuilder setUserHome(File d) {
+ this.userHome = d;
+ return this;
+ }
+
+ public FileCacheBuilder setUserHome(@Nullable String path) {
+ this.userHome = (path == null) ? null : new File(path);
+ return this;
+ }
+
+ public FileCache build() {
+ if (userHome == null) {
+ userHome = findHome();
+ }
+ File cacheDir = new File(userHome, "cache");
+ return FileCache.create(cacheDir, logger);
+ }
+
+ private File findHome() {
+ String path = System.getenv("SONAR_USER_HOME");
+ if (path == null) {
+ // Default
+ path = System.getProperty("user.home") + File.separator + ".sonar";
+ }
+ return new File(path);
+ }
+}
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+package org.sonar.runner.cache;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.security.MessageDigest;
+
+/**
+ * Hashes used to store files in the cache directory.
+ *
+ * @since 3.5
+ */
+public class FileHashes {
+
+ private static final int STREAM_BUFFER_LENGTH = 1024;
+
+ public String of(File file) {
+ try {
+ return of(new FileInputStream(file));
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to compute hash of: " + file.getAbsolutePath(), e);
+ }
+ }
+
+ /**
+ * Computes the hash of given stream. The stream is closed by this method.
+ */
+ public String of(InputStream input) {
+ try(InputStream is = input) {
+ MessageDigest digest = MessageDigest.getInstance("MD5");
+ byte[] hash = digest(is, digest);
+ return toHex(hash);
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to compute hash", e);
+ }
+ }
+
+ private static byte[] digest(InputStream input, MessageDigest digest) throws IOException {
+ final byte[] buffer = new byte[STREAM_BUFFER_LENGTH];
+ int read = input.read(buffer, 0, STREAM_BUFFER_LENGTH);
+ while (read > -1) {
+ digest.update(buffer, 0, read);
+ read = input.read(buffer, 0, STREAM_BUFFER_LENGTH);
+ }
+ return digest.digest();
+ }
+
+ static String toHex(byte[] bytes) {
+ BigInteger bi = new BigInteger(1, bytes);
+ return String.format("%0" + (bytes.length << 1) + "x", bi);
+ }
+}
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+package org.sonar.runner.cache;
+
+public interface Logger {
+
+ void debug(String msg);
+
+ void info(String msg);
+
+ void warn(String msg);
+
+ void error(String msg);
+
+ void error(String msg, Throwable t);
+
+}
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+package org.sonar.runner.cache;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+import static java.nio.file.StandardOpenOption.WRITE;
+
+public class PersistentCache {
+ private static final char[] hexArray = "0123456789ABCDEF".toCharArray();
+ private static final Charset ENCODING = StandardCharsets.UTF_8;
+ private static final String DIGEST_ALGO = "MD5";
+
+ private final PersistentCacheInvalidation invalidation;
+ private final Logger logger;
+ private final Path dir;
+ private DirectoryLock lock;
+
+ public PersistentCache(Path dir, PersistentCacheInvalidation invalidation, Logger logger, DirectoryLock lock) {
+ this.dir = dir;
+ this.invalidation = invalidation;
+ this.logger = logger;
+ this.lock = lock;
+
+ reconfigure();
+ logger.debug("cache: " + dir);
+ }
+
+ public synchronized void reconfigure() {
+ try {
+ Files.createDirectories(dir);
+ } catch (IOException e) {
+ throw new IllegalStateException("failed to create cache dir", e);
+ }
+ }
+
+ public Path getDirectory() {
+ return dir;
+ }
+
+ @CheckForNull
+ public synchronized String getString(@Nonnull String obj) throws IOException {
+ byte[] cached = get(obj);
+
+ if (cached == null) {
+ return null;
+ }
+
+ return new String(cached, ENCODING);
+ }
+
+ @CheckForNull
+ public synchronized InputStream getStream(@Nonnull String obj) throws IOException {
+ String key = getKey(obj);
+
+ try {
+ lock();
+ Path path = getCacheCopy(key);
+ if (path == null) {
+ return null;
+ }
+ return new DeleteFileOnCloseInputStream(new FileInputStream(path.toFile()), path);
+
+ } finally {
+ unlock();
+ }
+ }
+
+ @CheckForNull
+ public synchronized byte[] get(@Nonnull String obj) throws IOException {
+ String key = getKey(obj);
+
+ try {
+ lock();
+
+ byte[] cached = getCache(key);
+
+ if (cached != null) {
+ logger.debug("cache hit for " + obj + " -> " + key);
+ return cached;
+ }
+
+ logger.debug("cache miss for " + obj + " -> " + key);
+ } finally {
+ unlock();
+ }
+
+ return null;
+ }
+
+ public synchronized void put(@Nonnull String obj, @Nonnull InputStream stream) throws IOException {
+ String key = getKey(obj);
+ try {
+ lock();
+ putCache(key, stream);
+ } finally {
+ unlock();
+ }
+ }
+
+ public synchronized void put(@Nonnull String obj, @Nonnull byte[] value) throws IOException {
+ String key = getKey(obj);
+ try {
+ lock();
+ putCache(key, value);
+ } finally {
+ unlock();
+ }
+ }
+
+ /**
+ * Deletes all cache entries
+ */
+ public synchronized void clear() {
+ logger.info("cache: clearing");
+ try {
+ lock();
+ deleteCacheEntries(new DirectoryClearFilter());
+ } catch (IOException e) {
+ logger.error("Error clearing cache", e);
+ } finally {
+ unlock();
+ }
+ }
+
+ /**
+ * Deletes cache entries that are no longer valid according to the default expiration time period.
+ */
+ public synchronized void clean() {
+ logger.info("cache: cleaning");
+ try {
+ lock();
+ deleteCacheEntries(new DirectoryCleanFilter());
+ } catch (IOException e) {
+ logger.error("Error cleaning cache", e);
+ } finally {
+ unlock();
+ }
+ }
+
+ private void lock() throws IOException {
+ lock.lock();
+ }
+
+ private void unlock() {
+ lock.unlock();
+ }
+
+ private static String getKey(String uri) {
+ try {
+ String key = uri;
+ MessageDigest digest = MessageDigest.getInstance(DIGEST_ALGO);
+ digest.update(key.getBytes(StandardCharsets.UTF_8));
+ return byteArrayToHex(digest.digest());
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("Couldn't create hash", e);
+ }
+ }
+
+ private void deleteCacheEntries(DirectoryStream.Filter<Path> filter) throws IOException {
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, filter)) {
+ for (Path p : stream) {
+ try {
+ Files.delete(p);
+ } catch (Exception e) {
+ logger.error("Error deleting " + p, e);
+ }
+ }
+ }
+ }
+
+ private class DirectoryClearFilter implements DirectoryStream.Filter<Path> {
+ @Override
+ public boolean accept(Path entry) throws IOException {
+ return !lock.getFileLockName().equals(entry.getFileName().toString());
+ }
+ }
+
+ private class DirectoryCleanFilter implements DirectoryStream.Filter<Path> {
+ @Override
+ public boolean accept(Path entry) throws IOException {
+ if (lock.getFileLockName().equals(entry.getFileName().toString())) {
+ return false;
+ }
+
+ return invalidation.test(entry);
+ }
+ }
+
+ private void putCache(String key, byte[] value) throws IOException {
+ Path cachePath = getCacheEntryPath(key);
+ Files.write(cachePath, value, CREATE, WRITE, TRUNCATE_EXISTING);
+ }
+
+ private void putCache(String key, InputStream stream) throws IOException {
+ Path cachePath = getCacheEntryPath(key);
+ Files.copy(stream, cachePath, StandardCopyOption.REPLACE_EXISTING);
+ }
+
+ private byte[] getCache(String key) throws IOException {
+ Path cachePath = getCacheEntryPath(key);
+
+ if (!validateCacheEntry(cachePath)) {
+ return null;
+ }
+
+ return Files.readAllBytes(cachePath);
+ }
+
+ private Path getCacheCopy(String key) throws IOException {
+ Path cachePath = getCacheEntryPath(key);
+
+ if (!validateCacheEntry(cachePath)) {
+ return null;
+ }
+
+ Path temp = Files.createTempFile("sonar_cache", null);
+ Files.copy(cachePath, temp, StandardCopyOption.REPLACE_EXISTING);
+ return temp;
+ }
+
+ private boolean validateCacheEntry(Path cacheEntryPath) throws IOException {
+ if (!Files.exists(cacheEntryPath)) {
+ return false;
+ }
+
+ if (invalidation.test(cacheEntryPath)) {
+ logger.debug("cache: evicting entry");
+ Files.delete(cacheEntryPath);
+ return false;
+ }
+
+ return true;
+ }
+
+ private Path getCacheEntryPath(String key) {
+ return dir.resolve(key);
+ }
+
+ public static String byteArrayToHex(byte[] bytes) {
+ char[] hexChars = new char[bytes.length * 2];
+ for (int j = 0; j < bytes.length; j++) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = hexArray[v >>> 4];
+ hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+}
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+package org.sonar.runner.cache;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nullable;
+
+/**
+ * Cache files will be placed in 3 areas:
+ * <pre>
+ * <sonar_home>/ws_cache/<server_url>-<version>/projects/<project>/
+ * <sonar_home>/ws_cache/<server_url>-<version>/global/
+ * <sonar_home>/ws_cache/<server_url>-<version>/local/
+ * </pre>
+ */
+public class PersistentCacheBuilder {
+ private static final long DEFAULT_EXPIRE_DURATION = TimeUnit.MILLISECONDS.convert(9999L, TimeUnit.DAYS);
+ private static final String DIR_NAME = "ws_cache";
+
+ private Path cacheBasePath;
+ private Path relativePath;
+ private final Logger logger;
+
+ public PersistentCacheBuilder(Logger logger) {
+ this.logger = logger;
+ }
+
+ public PersistentCacheBuilder setAreaForProject(String serverUrl, String serverVersion, String projectKey) {
+ relativePath = Paths.get(sanitizeFilename(serverUrl))
+ .resolve(sanitizeFilename(serverVersion))
+ .resolve("projects")
+ .resolve(sanitizeFilename(projectKey));
+ return this;
+ }
+
+ public PersistentCacheBuilder setAreaForGlobal(String serverUrl) {
+ relativePath = Paths.get(sanitizeFilename(serverUrl))
+ .resolve("global");
+ return this;
+ }
+
+ public PersistentCacheBuilder setAreaForLocalProject(String serverUrl, String serverVersion) {
+ relativePath = Paths.get(sanitizeFilename(serverUrl))
+ .resolve(sanitizeFilename(serverVersion))
+ .resolve("local");
+ return this;
+ }
+
+ public PersistentCacheBuilder setSonarHome(@Nullable Path p) {
+ if (p != null) {
+ this.cacheBasePath = p.resolve(DIR_NAME);
+ }
+ return this;
+ }
+
+ public PersistentCache build() {
+ if (relativePath == null) {
+ throw new IllegalStateException("area must be set before building");
+ }
+ if (cacheBasePath == null) {
+ setSonarHome(findHome());
+ }
+ Path cachePath = cacheBasePath.resolve(relativePath);
+ DirectoryLock lock = new DirectoryLock(cacheBasePath, logger);
+ PersistentCacheInvalidation criteria = new TTLCacheInvalidation(DEFAULT_EXPIRE_DURATION);
+ return new PersistentCache(cachePath, criteria, logger, lock);
+ }
+
+ private static Path findHome() {
+ String home = System.getenv("SONAR_USER_HOME");
+
+ if (home != null) {
+ return Paths.get(home);
+ }
+
+ home = System.getProperty("user.home");
+ return Paths.get(home, ".sonar");
+ }
+
+ private static String sanitizeFilename(String name) {
+ try {
+ return URLEncoder.encode(name, StandardCharsets.UTF_8.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalStateException("Couldn't sanitize filename: " + name, e);
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+package org.sonar.runner.cache;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+public interface PersistentCacheInvalidation {
+ boolean test(Path cacheEntryPath) throws IOException;
+}
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+package org.sonar.runner.cache;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+
+public class TTLCacheInvalidation implements PersistentCacheInvalidation {
+ private final long durationToExpireMs;
+
+ public TTLCacheInvalidation(long durationToExpireMs) {
+ this.durationToExpireMs = durationToExpireMs;
+ }
+
+ @Override
+ public boolean test(Path cacheEntryPath) throws IOException {
+ BasicFileAttributes attr = Files.readAttributes(cacheEntryPath, BasicFileAttributes.class);
+ long modTime = attr.lastModifiedTime().toMillis();
+
+ long age = System.currentTimeMillis() - modTime;
+
+ if (age > durationToExpireMs) {
+ return true;
+ }
+
+ return false;
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+
+@ParametersAreNonnullByDefault
+package org.sonar.runner.cache;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
*/
package org.sonar.runner.impl;
+import org.sonar.runner.batch.IsolatedLauncher;
+import org.sonar.runner.cache.Logger;
+import org.sonar.runner.cache.PersistentCache;
+import org.sonar.runner.cache.PersistentCacheBuilder;
+
import java.io.File;
import java.nio.file.Paths;
import java.security.AccessController;
import java.util.List;
import java.util.Properties;
-import org.sonar.home.cache.Logger;
-import org.sonar.home.cache.PersistentCache;
-import org.sonar.home.cache.PersistentCacheBuilder;
-import org.sonar.runner.batch.IsolatedLauncher;
-
public class IsolatedLauncherFactory {
static final String ISOLATED_LAUNCHER_IMPL = "org.sonar.runner.batch.BatchIsolatedLauncher";
private final TempCleaning tempCleaning;
*/
package org.sonar.runner.impl;
+import org.sonar.runner.cache.Logger;
+
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
-import org.sonar.home.cache.Logger;
public class IsolatedLauncherProxy implements InvocationHandler {
private final Object proxied;
*/
package org.sonar.runner.impl;
+import org.sonar.runner.cache.Logger;
+
import java.io.File;
import java.util.List;
import java.util.Properties;
-import org.sonar.home.cache.Logger;
-
class JarDownloader {
private final ServerConnection serverConnection;
private final Logger logger;
*/
package org.sonar.runner.impl;
+import org.sonar.runner.cache.FileCache;
+import org.sonar.runner.cache.FileCacheBuilder;
+import org.sonar.runner.cache.Logger;
+
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
-import org.sonar.home.cache.FileCache;
-import org.sonar.home.cache.FileCacheBuilder;
-import org.sonar.home.cache.Logger;
-
class Jars {
private static final String BOOTSTRAP_INDEX_PATH = "/batch_bootstrap/index";
static final String BATCH_PATH = "/batch/";
*/
package org.sonar.runner.impl;
-import com.github.kevinsawicki.http.HttpRequest.HttpRequestException;
-
import com.github.kevinsawicki.http.HttpRequest;
+import com.github.kevinsawicki.http.HttpRequest.HttpRequestException;
+import org.apache.commons.io.FileUtils;
+import org.sonar.runner.cache.Logger;
+import org.sonar.runner.cache.PersistentCache;
import java.io.File;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import org.apache.commons.io.FileUtils;
-import org.sonar.home.cache.Logger;
-import org.sonar.home.cache.PersistentCache;
-
class ServerConnection {
private static final String SONAR_SERVER_CAN_NOT_BE_REACHED = "SonarQube server ''{0}'' can not be reached";
*/
package org.sonar.runner.impl;
-import org.sonar.home.cache.Logger;
-
-import javax.annotation.Nullable;
-
+import org.sonar.runner.batch.IsolatedLauncher;
import org.sonar.runner.batch.IssueListener;
import org.sonar.runner.batch.LogOutput;
+import org.sonar.runner.cache.Logger;
+
+import javax.annotation.Nullable;
import java.io.File;
import java.io.FileOutputStream;
import java.util.List;
import java.util.Properties;
-import org.sonar.runner.batch.IsolatedLauncher;
-
public class SimulatedLauncher implements IsolatedLauncher {
private final String version;
private final Logger logger;
*/
package org.sonar.runner.impl;
-import java.io.File;
-import java.util.Collection;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.AgeFileFilter;
import org.apache.commons.io.filefilter.AndFileFilter;
import org.apache.commons.io.filefilter.PrefixFileFilter;
-import org.sonar.home.cache.Logger;
+import org.sonar.runner.cache.Logger;
+
+import java.io.File;
+import java.util.Collection;
/**
* The file sonar-runner-batch.jar is locked by the classloader on Windows and can't be dropped at the end of the execution.
*/
package org.sonar.runner.api;
+import org.sonar.runner.cache.Logger;
+
import java.io.File;
import java.util.Properties;
+
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
-import org.sonar.home.cache.Logger;
-
import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.Mockito.mock;
*/
package org.sonar.runner.api;
-import org.sonar.runner.impl.ClassloadRules;
-
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.ArgumentMatcher;
+import org.sonar.runner.batch.IsolatedLauncher;
+import org.sonar.runner.cache.Logger;
+import org.sonar.runner.impl.ClassloadRules;
+import org.sonar.runner.impl.IsolatedLauncherFactory;
import java.io.File;
import java.io.FileInputStream;
import java.util.List;
import java.util.Properties;
-import static org.mockito.Matchers.eq;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.mockito.ArgumentMatcher;
-import org.sonar.home.cache.Logger;
-import org.sonar.runner.batch.IsolatedLauncher;
-import org.sonar.runner.impl.IsolatedLauncherFactory;
import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.argThat;
+import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
expectedException.expectMessage("started");
runner.runAnalysis(new Properties());
}
-
+
@Test
public void cannot_start_twice() {
runner.start();
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+package org.sonar.runner.cache;
+
+import static org.mockito.Mockito.mock;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.rules.ExpectedException;
+
+import java.nio.channels.OverlappingFileLockException;
+import java.nio.file.Paths;
+
+import org.junit.Test;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+
+public class DirectoryLockTest {
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+ private DirectoryLock lock;
+
+ @Before
+ public void setUp() {
+ lock = new DirectoryLock(temp.getRoot().toPath(), mock(Logger.class));
+ }
+
+ @Test
+ public void lock() {
+ assertThat(temp.getRoot().list()).isEmpty();
+ lock.lock();
+ assertThat(temp.getRoot().toPath().resolve(".sonar_lock")).exists();
+ lock.unlock();
+ assertThat(temp.getRoot().list()).isEmpty();
+ }
+
+ @Test
+ public void tryLock() {
+ assertThat(temp.getRoot().list()).isEmpty();
+ lock.tryLock();
+ assertThat(temp.getRoot().toPath().resolve(".sonar_lock")).exists();
+ lock.unlock();
+ assertThat(temp.getRoot().list()).isEmpty();
+ }
+
+ @Test(expected = OverlappingFileLockException.class)
+ public void error_2locks() {
+ assertThat(temp.getRoot().list()).isEmpty();
+ lock.lock();
+ lock.lock();
+ }
+
+ @Test
+ public void unlockWithoutLock() {
+ lock.unlock();
+ }
+
+ @Test
+ public void errorCreatingLock() {
+ lock = new DirectoryLock(Paths.get("non", "existing", "path"), mock(Logger.class));
+
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("Failed to create lock");
+ lock.lock();
+ }
+
+ @Test
+ public void errorTryLock() {
+ lock = new DirectoryLock(Paths.get("non", "existing", "path"), mock(Logger.class));
+
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("Failed to create lock");
+ lock.tryLock();
+ }
+}
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+package org.sonar.runner.cache;
+
+import java.io.File;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class FileCacheBuilderTest {
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Test
+ public void setUserHome() throws Exception {
+ File userHome = temp.newFolder();
+ FileCache cache = new FileCacheBuilder(mock(Logger.class)).setUserHome(userHome).build();
+
+ assertThat(cache.getDir()).isDirectory().exists();
+ assertThat(cache.getDir().getName()).isEqualTo("cache");
+ assertThat(cache.getDir().getParentFile()).isEqualTo(userHome);
+ }
+
+ @Test
+ public void user_home_property_can_be_null() {
+ FileCache cache = new FileCacheBuilder(mock(Logger.class)).setUserHome((String) null).build();
+
+ // does not fail. It uses default path or env variable
+ assertThat(cache.getDir()).isDirectory().exists();
+ assertThat(cache.getDir().getName()).isEqualTo("cache");
+ }
+
+ @Test
+ public void use_default_path_or_env_variable() {
+ FileCache cache = new FileCacheBuilder(mock(Logger.class)).build();
+
+ assertThat(cache.getDir()).isDirectory().exists();
+ assertThat(cache.getDir().getName()).isEqualTo("cache");
+ }
+}
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+package org.sonar.runner.cache;
+
+import java.io.File;
+import java.io.IOException;
+import org.apache.commons.io.FileUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class FileCacheTest {
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void not_in_cache() throws IOException {
+ FileCache cache = FileCache.create(tempFolder.newFolder(), mock(Logger.class));
+ assertThat(cache.get("sonar-foo-plugin-1.5.jar", "ABCDE")).isNull();
+ }
+
+ @Test
+ public void found_in_cache() throws IOException {
+ FileCache cache = FileCache.create(tempFolder.newFolder(), mock(Logger.class));
+
+ // populate the cache. Assume that hash is correct.
+ File cachedFile = new File(new File(cache.getDir(), "ABCDE"), "sonar-foo-plugin-1.5.jar");
+ FileUtils.write(cachedFile, "body");
+
+ assertThat(cache.get("sonar-foo-plugin-1.5.jar", "ABCDE")).isNotNull().exists().isEqualTo(cachedFile);
+ }
+
+ @Test
+ public void download_and_add_to_cache() throws IOException {
+ FileHashes hashes = mock(FileHashes.class);
+ FileCache cache = new FileCache(tempFolder.newFolder(), hashes, mock(Logger.class));
+ when(hashes.of(any(File.class))).thenReturn("ABCDE");
+
+ FileCache.Downloader downloader = new FileCache.Downloader() {
+ public void download(String filename, File toFile) throws IOException {
+ FileUtils.write(toFile, "body");
+ }
+ };
+ File cachedFile = cache.get("sonar-foo-plugin-1.5.jar", "ABCDE", downloader);
+ assertThat(cachedFile).isNotNull().exists().isFile();
+ assertThat(cachedFile.getName()).isEqualTo("sonar-foo-plugin-1.5.jar");
+ assertThat(cachedFile.getParentFile().getParentFile()).isEqualTo(cache.getDir());
+ assertThat(FileUtils.readFileToString(cachedFile)).isEqualTo("body");
+ }
+
+ @Test
+ public void download_corrupted_file() throws IOException {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("INVALID HASH");
+
+ FileHashes hashes = mock(FileHashes.class);
+ FileCache cache = new FileCache(tempFolder.newFolder(), hashes, mock(Logger.class));
+ when(hashes.of(any(File.class))).thenReturn("VWXYZ");
+
+ FileCache.Downloader downloader = new FileCache.Downloader() {
+ public void download(String filename, File toFile) throws IOException {
+ FileUtils.write(toFile, "corrupted body");
+ }
+ };
+ cache.get("sonar-foo-plugin-1.5.jar", "ABCDE", downloader);
+ }
+
+ @Test
+ public void concurrent_download() throws IOException {
+ FileHashes hashes = mock(FileHashes.class);
+ when(hashes.of(any(File.class))).thenReturn("ABCDE");
+ final FileCache cache = new FileCache(tempFolder.newFolder(), hashes, mock(Logger.class));
+
+ FileCache.Downloader downloader = new FileCache.Downloader() {
+ public void download(String filename, File toFile) throws IOException {
+ // Emulate a concurrent download that adds file to cache before
+ File cachedFile = new File(new File(cache.getDir(), "ABCDE"), "sonar-foo-plugin-1.5.jar");
+ FileUtils.write(cachedFile, "downloaded by other");
+
+ FileUtils.write(toFile, "downloaded by me");
+ }
+ };
+
+ // do not fail
+ File cachedFile = cache.get("sonar-foo-plugin-1.5.jar", "ABCDE", downloader);
+ assertThat(cachedFile).isNotNull().exists().isFile();
+ assertThat(cachedFile.getName()).isEqualTo("sonar-foo-plugin-1.5.jar");
+ assertThat(cachedFile.getParentFile().getParentFile()).isEqualTo(cache.getDir());
+ assertThat(FileUtils.readFileToString(cachedFile)).contains("downloaded by");
+ }
+}
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+package org.sonar.runner.cache;
+
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.security.SecureRandom;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class FileHashesTest {
+
+ SecureRandom secureRandom = new SecureRandom();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Test
+ public void test_md5_hash() {
+ assertThat(hash("sonar")).isEqualTo("d85e336d61f5344395c42126fac239bc");
+
+ // compare results with commons-codec
+ for (int index = 0; index < 100; index++) {
+ String random = randomString();
+ assertThat(hash(random)).as(random).isEqualTo(
+ DigestUtils.md5Hex(random).toLowerCase()
+ );
+ }
+ }
+
+ @Test
+ public void test_hash_file() throws IOException {
+ File f = temp.newFile();
+ Files.write(f.toPath(), "sonar".getBytes(StandardCharsets.UTF_8));
+ assertThat(hashFile(f)).isEqualTo("d85e336d61f5344395c42126fac239bc");
+ }
+
+ @Test
+ public void test_toHex() {
+ // lower-case
+ assertThat(FileHashes.toHex("aloa_bi_bop_a_loula".getBytes())).isEqualTo("616c6f615f62695f626f705f615f6c6f756c61");
+
+ // compare results with commons-codec
+ for (int index = 0; index < 100; index++) {
+ String random = randomString();
+ assertThat(FileHashes.toHex(random.getBytes())).as(random).isEqualTo(
+ Hex.encodeHexString(random.getBytes()).toLowerCase()
+ );
+ }
+ }
+
+ @Test
+ public void fail_if_file_does_not_exist() throws IOException {
+ File file = temp.newFile("does_not_exist");
+ FileUtils.forceDelete(file);
+
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("Fail to compute hash of: " + file.getAbsolutePath());
+
+ new FileHashes().of(file);
+ }
+
+ @Test
+ public void fail_if_stream_is_closed() throws Exception {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("Fail to compute hash");
+
+ InputStream input = mock(InputStream.class);
+ when(input.read(any(byte[].class), anyInt(), anyInt())).thenThrow(new IllegalThreadStateException());
+ new FileHashes().of(input);
+ }
+
+ private String randomString() {
+ return new BigInteger(130, secureRandom).toString(32);
+ }
+
+ private String hash(String s) {
+ InputStream in = new ByteArrayInputStream(s.getBytes());
+ try {
+ return new FileHashes().of(in);
+ } finally {
+ IOUtils.closeQuietly(in);
+ }
+ }
+
+ private String hashFile(File f) {
+ return new FileHashes().of(f);
+ }
+}
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+package org.sonar.runner.cache;
+
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.Mockito.mock;
+
+public class PersistentCacheBuilderTest {
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Test
+ public void user_home_property_can_be_null() {
+ PersistentCache cache = new PersistentCacheBuilder(mock(Logger.class)).setSonarHome(null).setAreaForGlobal("url").build();
+ assertTrue(Files.isDirectory(cache.getDirectory()));
+ assertThat(cache.getDirectory()).endsWith(Paths.get("url", "global"));
+ }
+
+ @Test
+ public void set_user_home() {
+ PersistentCache cache = new PersistentCacheBuilder(mock(Logger.class)).setSonarHome(temp.getRoot().toPath()).setAreaForGlobal("url").build();
+
+ assertThat(cache.getDirectory()).isDirectory();
+ assertThat(cache.getDirectory()).startsWith(temp.getRoot().toPath());
+ assertTrue(Files.isDirectory(cache.getDirectory()));
+ }
+
+ @Test
+ public void read_system_env() {
+ assumeTrue(System.getenv("SONAR_USER_HOME") == null);
+
+ System.setProperty("user.home", temp.getRoot().getAbsolutePath());
+
+ PersistentCache cache = new PersistentCacheBuilder(mock(Logger.class)).setAreaForGlobal("url").build();
+ assertTrue(Files.isDirectory(cache.getDirectory()));
+ assertThat(cache.getDirectory()).startsWith(temp.getRoot().toPath());
+ }
+
+ @Test
+ public void directories() {
+ System.setProperty("user.home", temp.getRoot().getAbsolutePath());
+
+ PersistentCache cache = new PersistentCacheBuilder(mock(Logger.class)).setAreaForProject("url", "0", "proj").build();
+ assertThat(cache.getDirectory()).endsWith(Paths.get(".sonar", "ws_cache", "url", "0", "projects", "proj"));
+
+ cache = new PersistentCacheBuilder(mock(Logger.class)).setAreaForLocalProject("url", "0").build();
+ assertThat(cache.getDirectory()).endsWith(Paths.get(".sonar", "ws_cache", "url", "0", "local"));
+
+ cache = new PersistentCacheBuilder(mock(Logger.class)).setAreaForGlobal("url").build();
+ assertThat(cache.getDirectory()).endsWith(Paths.get(".sonar", "ws_cache", "url", "global"));
+ }
+}
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+package org.sonar.runner.cache;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.mockito.Matchers.any;
+
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.verify;
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class PersistentCacheTest {
+ private final static String URI = "key1";
+ private final static String VALUE = "cache content";
+ private PersistentCache cache = null;
+ private DirectoryLock lock = null;
+ private PersistentCacheInvalidation invalidation = null;
+
+ @Rule
+ public TemporaryFolder tmp = new TemporaryFolder();
+
+ @Before
+ public void setUp() throws IOException {
+ invalidation = mock(PersistentCacheInvalidation.class);
+ when(invalidation.test(any(Path.class))).thenReturn(false);
+ lock = mock(DirectoryLock.class);
+ when(lock.getFileLockName()).thenReturn("lock");
+ cache = new PersistentCache(tmp.getRoot().toPath(), invalidation, mock(Logger.class), lock);
+ }
+
+ @Test
+ public void testCacheMiss() throws Exception {
+ assertCacheHit(false);
+ }
+
+ @Test
+ public void testClean() throws Exception {
+ Path lockFile = cache.getDirectory().resolve("lock");
+ // puts entry
+ cache.put(URI, VALUE.getBytes(StandardCharsets.UTF_8));
+ Files.write(lockFile, "test".getBytes(StandardCharsets.UTF_8));
+ assertCacheHit(true);
+ when(invalidation.test(any(Path.class))).thenReturn(true);
+ cache.clean();
+ when(invalidation.test(any(Path.class))).thenReturn(false);
+ assertCacheHit(false);
+ // lock file should not get deleted
+ assertThat(new String(Files.readAllBytes(lockFile), StandardCharsets.UTF_8)).isEqualTo("test");
+ }
+
+ @Test
+ public void testStream() throws IOException {
+ cache.put("id", "test".getBytes());
+ InputStream stream = cache.getStream("id");
+ assertThat(IOUtils.toString(stream)).isEqualTo("test");
+
+ assertThat(cache.getStream("non existing")).isNull();
+ }
+
+ @Test
+ public void testClear() throws Exception {
+ Path lockFile = cache.getDirectory().resolve("lock");
+ cache.put(URI, VALUE.getBytes(StandardCharsets.UTF_8));
+ Files.write(lockFile, "test".getBytes(StandardCharsets.UTF_8));
+ assertCacheHit(true);
+ cache.clear();
+ assertCacheHit(false);
+ // lock file should not get deleted
+ assertThat(new String(Files.readAllBytes(lockFile), StandardCharsets.UTF_8)).isEqualTo("test");
+ }
+
+ @Test
+ public void testCacheHit() throws Exception {
+ cache.put(URI, VALUE.getBytes(StandardCharsets.UTF_8));
+ assertCacheHit(true);
+ }
+
+ @Test
+ public void testReconfigure() throws Exception {
+ assertCacheHit(false);
+ cache.put(URI, VALUE.getBytes(StandardCharsets.UTF_8));
+ assertCacheHit(true);
+
+ File root = tmp.getRoot();
+ FileUtils.deleteQuietly(root);
+
+ // should re-create cache directory and start using the cache
+ cache.reconfigure();
+ assertThat(root).exists();
+
+ assertCacheHit(false);
+ cache.put(URI, VALUE.getBytes(StandardCharsets.UTF_8));
+ assertCacheHit(true);
+ }
+
+ @Test
+ public void testExpiration() throws Exception {
+ when(invalidation.test(any(Path.class))).thenReturn(true);
+ cache.put(URI, VALUE.getBytes(StandardCharsets.UTF_8));
+ assertCacheHit(false);
+ }
+
+ private void assertCacheHit(boolean hit) throws Exception {
+ assertCacheHit(cache, hit);
+ }
+
+ private void assertCacheHit(PersistentCache pCache, boolean hit) throws Exception {
+ String expected = hit ? VALUE : null;
+ assertThat(pCache.getString(URI)).isEqualTo(expected);
+ verify(lock, atLeast(1)).unlock();
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube Runner - API
+ * Copyright (C) 2011 SonarSource
+ * sonarqube@googlegroups.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 02
+ */
+package org.sonar.runner.cache;
+
+import org.junit.Before;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Test;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+
+public class TTLCacheInvalidationTest {
+ private Path testFile;
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Before
+ public void setUp() throws IOException {
+ testFile = temp.newFile().toPath();
+ }
+
+ @Test
+ public void testExpired() throws IOException {
+ TTLCacheInvalidation invalidation = new TTLCacheInvalidation(-100);
+ assertThat(invalidation.test(testFile)).isEqualTo(true);
+ }
+
+ @Test
+ public void testValid() throws IOException {
+ TTLCacheInvalidation invalidation = new TTLCacheInvalidation(100_000);
+ assertThat(invalidation.test(testFile)).isEqualTo(false);
+ }
+}
*/
package org.sonar.runner.impl;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.runner.batch.IsolatedLauncher;
import org.sonar.runner.batch.IssueListener;
+import org.sonar.runner.batch.LogOutput;
+import org.sonar.runner.cache.Logger;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
-import org.junit.Before;
-import org.junit.Test;
-import org.sonar.home.cache.Logger;
-import org.sonar.runner.batch.IsolatedLauncher;
-import org.sonar.runner.batch.LogOutput;
import static org.fest.assertions.Fail.fail;
import static org.mockito.Mockito.mock;
*/
package org.sonar.runner.impl;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.util.concurrent.Callable;
import org.junit.Before;
import org.junit.Test;
-import org.sonar.home.cache.Logger;
import org.sonar.runner.batch.BatchIsolatedLauncher;
+import org.sonar.runner.cache.Logger;
+
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.concurrent.Callable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
*/
package org.sonar.runner.impl;
+import org.junit.Test;
+import org.sonar.runner.cache.Logger;
+
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
-import org.junit.Test;
-import org.sonar.home.cache.Logger;
import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.Mockito.doReturn;
*/
package org.sonar.runner.impl;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.runner.cache.FileCache;
+import org.sonar.runner.cache.Logger;
+
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Properties;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.sonar.home.cache.FileCache;
-import org.sonar.home.cache.Logger;
import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.Fail.fail;
import static org.mockito.Matchers.any;
package org.sonar.runner.impl;
import com.github.kevinsawicki.http.HttpRequest;
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.runner.cache.Logger;
+import org.sonar.runner.cache.PersistentCache;
+import org.sonar.runner.cache.PersistentCacheBuilder;
import java.io.File;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.util.Properties;
-import static org.mockito.Matchers.startsWith;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyString;
-import static org.junit.Assert.*;
-import org.apache.commons.io.FileUtils;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.sonar.home.cache.Logger;
-import org.sonar.home.cache.PersistentCache;
-import org.sonar.home.cache.PersistentCacheBuilder;
import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.Fail.fail;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.startsWith;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
public class ServerConnectionTest {
*/
package org.sonar.runner.impl;
-import org.junit.Rule;
-import org.junit.rules.TemporaryFolder;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
-import org.sonar.home.cache.Logger;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.runner.cache.Logger;
import java.io.File;
import java.io.FileInputStream;
*/
package org.sonar.runner.impl;
-import java.io.File;
import org.apache.commons.io.FileUtils;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
-import org.sonar.home.cache.Logger;
+import org.sonar.runner.cache.Logger;
+
+import java.io.File;
import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.Mockito.mock;