]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-2291 move management of file cache to the new module sonar-home
authorSimon Brandhof <simon.brandhof@gmail.com>
Tue, 5 Feb 2013 11:43:45 +0000 (12:43 +0100)
committerSimon Brandhof <simon.brandhof@gmail.com>
Tue, 5 Feb 2013 11:43:45 +0000 (12:43 +0100)
33 files changed:
pom.xml
sonar-batch/pom.xml
sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchSonarCache.java [deleted file]
sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapModule.java
sonar-batch/src/main/java/org/sonar/batch/bootstrap/FileCacheProvider.java [new file with mode: 0644]
sonar-batch/src/main/java/org/sonar/batch/bootstrap/JdbcDriverHolder.java
sonar-batch/src/main/java/org/sonar/batch/bootstrap/PluginDownloader.java
sonar-batch/src/main/java/org/sonar/batch/cache/SonarCache.java [deleted file]
sonar-batch/src/test/java/org/sonar/batch/bootstrap/FileCacheProviderTest.java [new file with mode: 0644]
sonar-batch/src/test/java/org/sonar/batch/bootstrap/JdbcDriverHolderTest.java
sonar-batch/src/test/java/org/sonar/batch/bootstrap/PluginDownloaderTest.java
sonar-batch/src/test/java/org/sonar/batch/cache/SonarCacheTest.java [deleted file]
sonar-core/pom.xml
sonar-core/src/main/java/org/sonar/core/plugins/RemotePlugin.java
sonar-core/src/main/java/org/sonar/core/plugins/RemotePluginFile.java
sonar-core/src/test/java/org/sonar/core/plugins/RemotePluginTest.java
sonar-home/pom.xml [new file with mode: 0644]
sonar-home/src/main/java/org/sonar/home/cache/FileCache.java [new file with mode: 0644]
sonar-home/src/main/java/org/sonar/home/cache/FileCacheBuilder.java [new file with mode: 0644]
sonar-home/src/main/java/org/sonar/home/cache/FileHashes.java [new file with mode: 0644]
sonar-home/src/main/java/org/sonar/home/cache/package-info.java [new file with mode: 0644]
sonar-home/src/main/java/org/sonar/home/log/Log.java [new file with mode: 0644]
sonar-home/src/main/java/org/sonar/home/log/Slf4jLog.java [new file with mode: 0644]
sonar-home/src/main/java/org/sonar/home/log/StandardLog.java [new file with mode: 0644]
sonar-home/src/main/java/org/sonar/home/log/package-info.java [new file with mode: 0644]
sonar-home/src/test/java/org/sonar/home/cache/FileCacheBuilderTest.java [new file with mode: 0644]
sonar-home/src/test/java/org/sonar/home/cache/FileCacheTest.java [new file with mode: 0644]
sonar-home/src/test/java/org/sonar/home/cache/FileHashesTest.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java
sonar-server/pom.xml
sonar-server/src/main/java/org/sonar/server/startup/GenerateBootstrapIndex.java
sonar-server/src/main/java/org/sonar/server/startup/JdbcDriverDeployer.java
sonar-server/src/test/java/org/sonar/server/startup/JdbcDriverDeployerTest.java

diff --git a/pom.xml b/pom.xml
index 5386fca70ea0cc59ad2197b818c12c7438d6713c..44946a6575142ee1d122fb037c08fb4c6bda8100 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -23,6 +23,7 @@
     <module>sonar-deprecated</module>
     <module>sonar-duplications</module>
     <module>sonar-graph</module>
+    <module>sonar-home</module>
     <module>sonar-java-api</module>
     <module>sonar-markdown</module>
     <module>sonar-maven-plugin</module>
         <artifactId>sonar-duplications</artifactId>
         <version>${project.version}</version>
       </dependency>
+      <dependency>
+        <groupId>org.codehaus.sonar</groupId>
+        <artifactId>sonar-home</artifactId>
+        <version>${project.version}</version>
+      </dependency>
       <dependency>
         <groupId>org.codehaus.sonar</groupId>
         <artifactId>sonar-graph</artifactId>
         <artifactId>guava</artifactId>
         <version>10.0.1</version>
       </dependency>
+      <dependency>
+        <groupId>com.google.code.findbugs</groupId>
+        <artifactId>jsr305</artifactId>
+        <version>1.3.9</version>
+      </dependency>
       <dependency>
         <groupId>commons-io</groupId>
         <artifactId>commons-io</artifactId>
index 8fcf5b43847b8cd6afca45533ee08f05a9c4fadf..9e95782e6d30640d9e41f5c27fdaf40da6e0e8db 100644 (file)
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
     <groupId>org.codehaus.sonar</groupId>
@@ -9,7 +10,6 @@
 
   <groupId>org.codehaus.sonar</groupId>
   <artifactId>sonar-batch</artifactId>
-  <packaging>jar</packaging>
   <name>Sonar :: Batch</name>
 
   <dependencies>
       <groupId>org.codehaus.sonar</groupId>
       <artifactId>sonar-deprecated</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.codehaus.sonar</groupId>
+      <artifactId>sonar-home</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.codehaus.sonar</groupId>
       <artifactId>sonar-java-api</artifactId>
diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchSonarCache.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchSonarCache.java
deleted file mode 100644 (file)
index d656c21..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Sonar, open source software quality management tool.
- * Copyright (C) 2008-2012 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * Sonar 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.
- *
- * Sonar 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 Sonar; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
- */
-package org.sonar.batch.bootstrap;
-
-import org.apache.commons.lang.StringUtils;
-import org.sonar.api.CoreProperties;
-import org.sonar.api.config.Settings;
-import org.sonar.api.task.TaskExtension;
-import org.sonar.batch.cache.SonarCache;
-
-import java.io.File;
-
-public class BatchSonarCache implements TaskExtension {
-
-  private Settings settings;
-  private SonarCache cache;
-
-  public BatchSonarCache(Settings settings) {
-    this.settings = settings;
-  }
-
-  public void start() {
-    // Try to get Sonar user home from property
-    String sonarUserHome = settings.getString(CoreProperties.SONAR_USER_HOME_PROPERTY);
-    if (StringUtils.isBlank(sonarUserHome)) {
-      // Try to get Sonar user home from environment variable
-      sonarUserHome = settings.getString(CoreProperties.SONAR_USER_HOME);
-    }
-    if (StringUtils.isBlank(sonarUserHome)) {
-      // Default
-      sonarUserHome = System.getProperty("user.home") + File.separator + ".sonar";
-    }
-
-    SonarCache.Builder builder = SonarCache.create(new File(sonarUserHome));
-    this.cache = builder.build();
-  }
-
-  public SonarCache getCache() {
-    return cache;
-  }
-}
index 39614208ea11158b549622d355073654f9ae3765..9e4d54b201f9ace40c42bfe4be0712502e159b33 100644 (file)
@@ -74,7 +74,7 @@ public class BootstrapModule extends Module {
     container.addSingleton(HttpDownloader.class);
     container.addSingleton(UriReader.class);
     container.addSingleton(PluginDownloader.class);
-    container.addSingleton(BatchSonarCache.class);
+    container.addPicoAdapter(new FileCacheProvider());
     for (Object component : boostrapperComponents) {
       if (component != null) {
         container.addSingleton(component);
diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/FileCacheProvider.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/FileCacheProvider.java
new file mode 100644 (file)
index 0000000..610083d
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.batch.bootstrap;
+
+import org.picocontainer.injectors.ProviderAdapter;
+import org.sonar.api.config.Settings;
+import org.sonar.home.cache.FileCache;
+import org.sonar.home.cache.FileCacheBuilder;
+import org.sonar.home.log.Slf4jLog;
+
+public class FileCacheProvider extends ProviderAdapter {
+  private FileCache cache;
+
+  public FileCache provide(Settings settings) {
+    if (cache == null) {
+      String home = settings.getString("sonar.userHome");
+      cache = new FileCacheBuilder().setLog(new Slf4jLog(FileCache.class)).setUserHome(home).build();
+    }
+    return cache;
+  }
+}
index 06b36e2b9c68f930f3c5d9b0fa12a5b72c041dee..4db53d7db31fb1d6a95d60b0006c4013c5796efa 100644 (file)
@@ -25,7 +25,7 @@ import org.slf4j.LoggerFactory;
 import org.sonar.api.CoreProperties;
 import org.sonar.api.config.Settings;
 import org.sonar.api.utils.SonarException;
-import org.sonar.batch.cache.SonarCache;
+import org.sonar.home.cache.FileCache;
 
 import java.io.File;
 import java.io.IOException;
@@ -44,43 +44,38 @@ public class JdbcDriverHolder {
 
   private ServerClient serverClient;
   private Settings settings;
-  private BatchSonarCache batchCache;
+  private FileCache fileCache;
 
   // initialized in start()
   private JdbcDriverClassLoader classLoader = null;
 
-  public JdbcDriverHolder(BatchSonarCache batchCache, Settings settings, ServerClient serverClient) {
+  public JdbcDriverHolder(FileCache fileCache, Settings settings, ServerClient serverClient) {
     this.serverClient = serverClient;
     this.settings = settings;
-    this.batchCache = batchCache;
+    this.fileCache = fileCache;
   }
 
   public void start() {
     if (!settings.getBoolean(CoreProperties.DRY_RUN)) {
       try {
         LOG.info("Install JDBC driver");
-        String[] nameAndMd5 = downloadJdbcDriverIndex();
-        String filename = nameAndMd5[0];
-        String remoteMd5 = nameAndMd5[1];
-        File driverInCache = getSonarCache().getFileFromCache(filename, remoteMd5);
-        if (driverInCache == null) {
-          File tmpDownloadFile = getSonarCache().getTemporaryFile();
-          String url = "/deploy/" + filename;
-          if (LOG.isDebugEnabled()) {
-            LOG.debug("Downloading {} to {}", url, tmpDownloadFile.getAbsolutePath());
+        String[] nameAndHash = downloadJdbcDriverIndex();
+        String filename = nameAndHash[0];
+        String hash = nameAndHash[1];
+
+        File jdbcDriver = fileCache.get(filename, hash, new FileCache.Downloader() {
+          public void download(String filename, File toFile) throws IOException {
+            String url = "/deploy/" + filename;
+            if (LOG.isDebugEnabled()) {
+              LOG.debug("Download {} to {}", url, toFile.getAbsolutePath());
+            } else {
+              LOG.info("Download {}", filename);
+            }
+            serverClient.download(url, toFile);
           }
-          else {
-            LOG.info("Downloading {}", filename);
-          }
-          serverClient.download(url, tmpDownloadFile);
-          String md5 = getSonarCache().cacheFile(tmpDownloadFile, filename);
-          driverInCache = getSonarCache().getFileFromCache(filename, md5);
-          if (!md5.equals(remoteMd5)) {
-            throw new SonarException("INVALID CHECKSUM: File " + driverInCache.getAbsolutePath() + " was expected to have checksum " + remoteMd5
-              + " but was downloaded with checksum " + md5);
-          }
-        }
-        classLoader = initClassloader(driverInCache);
+        });
+
+        classLoader = initClassloader(jdbcDriver);
       } catch (SonarException e) {
         throw e;
       } catch (Exception e) {
@@ -89,10 +84,6 @@ public class JdbcDriverHolder {
     }
   }
 
-  private SonarCache getSonarCache() {
-    return batchCache.getCache();
-  }
-
   @VisibleForTesting
   JdbcDriverClassLoader getClassLoader() {
     return classLoader;
@@ -148,7 +139,7 @@ public class JdbcDriverHolder {
   private String[] downloadJdbcDriverIndex() {
     String url = "/deploy/jdbc-driver.txt";
     try {
-      LOG.debug("Downloading index of jdbc-driver");
+      LOG.debug("Download index of jdbc-driver");
       String indexContent = serverClient.request(url);
       return indexContent.split("\\|");
     } catch (Exception e) {
@@ -159,7 +150,7 @@ public class JdbcDriverHolder {
   static class JdbcDriverClassLoader extends URLClassLoader {
 
     public JdbcDriverClassLoader(URL jdbcDriver, ClassLoader parent) {
-      super(new URL[] {jdbcDriver}, parent);
+      super(new URL[]{jdbcDriver}, parent);
     }
 
     public void clearReferencesJdbc() {
index dadabefd1d8a0d02e3b489605d9e915f318ac5d7..f037413a2788517901ffbba4e16197d1729a5580 100644 (file)
@@ -26,11 +26,12 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.sonar.api.BatchComponent;
 import org.sonar.api.utils.SonarException;
-import org.sonar.batch.cache.SonarCache;
 import org.sonar.core.plugins.RemotePlugin;
 import org.sonar.core.plugins.RemotePluginFile;
+import org.sonar.home.cache.FileCache;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.List;
 
 public class PluginDownloader implements BatchComponent {
@@ -38,45 +39,32 @@ public class PluginDownloader implements BatchComponent {
   private static final Logger LOG = LoggerFactory.getLogger(PluginDownloader.class);
 
   private ServerClient server;
-  private BatchSonarCache batchCache;
+  private FileCache fileCache;
 
-  public PluginDownloader(BatchSonarCache batchCache, ServerClient server) {
+  public PluginDownloader(FileCache fileCache, ServerClient server) {
     this.server = server;
-    this.batchCache = batchCache;
+    this.fileCache = fileCache;
   }
 
-  private SonarCache getSonarCache() {
-    return batchCache.getCache();
-  }
-
-  public List<File> downloadPlugin(RemotePlugin remote) {
+  public List<File> downloadPlugin(final RemotePlugin remote) {
     try {
       List<File> files = Lists.newArrayList();
-      for (RemotePluginFile file : remote.getFiles()) {
-        File fileInCache = getSonarCache().getFileFromCache(file.getFilename(), file.getMd5());
-        if (fileInCache == null) {
-          File tmpDownloadFile = getSonarCache().getTemporaryFile();
-          String url = "/deploy/plugins/" + remote.getKey() + "/" + file.getFilename();
-          if (LOG.isDebugEnabled()) {
-            LOG.debug("Downloading {} to {}", url, tmpDownloadFile.getAbsolutePath());
-          }
-          else {
-            LOG.info("Downloading {}", file.getFilename());
-          }
-          server.download(url, tmpDownloadFile);
-          String md5 = getSonarCache().cacheFile(tmpDownloadFile, file.getFilename());
-          fileInCache = getSonarCache().getFileFromCache(file.getFilename(), md5);
-          if (!md5.equals(file.getMd5())) {
-            throw new SonarException("INVALID CHECKSUM: File " + fileInCache.getAbsolutePath() + " was expected to have checksum " + file.getMd5()
-              + " but was downloaded with checksum " + md5);
+      for (final RemotePluginFile file : remote.getFiles()) {
+        File cachedFile = fileCache.get(file.getFilename(), file.getHash(), new FileCache.Downloader() {
+          public void download(String filename, File toFile) throws IOException {
+            String url = "/deploy/plugins/" + remote.getKey() + "/" + file.getFilename();
+            if (LOG.isDebugEnabled()) {
+              LOG.debug("Download {} to {}", url, toFile.getAbsolutePath());
+            } else {
+              LOG.info("Download {}", file.getFilename());
+            }
+            server.download(url, toFile);
           }
-        }
-        files.add(fileInCache);
+        });
+        files.add(cachedFile);
       }
       return files;
 
-    } catch (SonarException e) {
-      throw e;
     } catch (Exception e) {
       throw new SonarException("Fail to download plugin: " + remote.getKey(), e);
     }
@@ -85,7 +73,7 @@ public class PluginDownloader implements BatchComponent {
   public List<RemotePlugin> downloadPluginIndex() {
     String url = "/deploy/plugins/index.txt";
     try {
-      LOG.debug("Downloading index of plugins");
+      LOG.debug("Download index of plugins");
       String indexContent = server.request(url);
       String[] rows = StringUtils.split(indexContent, CharUtils.LF);
       List<RemotePlugin> remoteLocations = Lists.newArrayList();
diff --git a/sonar-batch/src/main/java/org/sonar/batch/cache/SonarCache.java b/sonar-batch/src/main/java/org/sonar/batch/cache/SonarCache.java
deleted file mode 100644 (file)
index 7749942..0000000
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Sonar, open source software quality management tool.
- * Copyright (C) 2008-2012 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * Sonar 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.
- *
- * Sonar 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 Sonar; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
- */
-package org.sonar.batch.cache;
-
-import com.google.common.io.Closeables;
-import com.google.common.io.Files;
-import org.apache.commons.codec.digest.DigestUtils;
-import org.apache.commons.io.FileUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.api.utils.SonarException;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-
-/**
- * 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).
- * Default location of cache is 
- * @author Julien HENRY
- *
- */
-public class SonarCache {
-
-  private static final Logger LOG = LoggerFactory.getLogger(SonarCache.class);
-
-  private static final int TEMP_FILE_ATTEMPTS = 10000;
-
-  private File cacheLocation;
-  /**
-   * Temporary directory where files should be stored before be inserted in the cache.
-   * Having a temporary close to the final location (read on same FS) will assure
-   * the move will be atomic.
-   */
-  private File tmpDir;
-
-  private SonarCache(File cacheLocation) {
-    this.cacheLocation = cacheLocation;
-    tmpDir = new File(cacheLocation, "tmp");
-    if (!cacheLocation.exists()) {
-      LOG.debug("Creating cache directory: {}", cacheLocation.getAbsolutePath());
-      try {
-        FileUtils.forceMkdir(cacheLocation);
-      } catch (IOException e) {
-        throw new RuntimeException("Unable to create cache directory " + cacheLocation.getAbsolutePath(), e);
-      }
-    }
-  }
-
-  public static class Builder {
-
-    private File sonarUserHomeLocation;
-    private File cacheLocation;
-
-    public Builder(File sonarUserHomeLocation) {
-      this.sonarUserHomeLocation = sonarUserHomeLocation;
-    }
-
-    public Builder setCacheLocation(File cacheLocation) {
-      this.cacheLocation = cacheLocation;
-      return this;
-    }
-
-    public SonarCache build() {
-      if (cacheLocation == null) {
-        return new SonarCache(new File(sonarUserHomeLocation, "cache"));
-      }
-      else {
-        return new SonarCache(cacheLocation);
-      }
-    }
-
-  }
-
-  public static Builder create(File sonarUserHomeLocation) {
-    if (sonarUserHomeLocation == null) {
-      throw new SonarException("Sonar user home directory should not be null");
-    }
-    return new Builder(sonarUserHomeLocation);
-  }
-
-  /**
-   * Move the given file inside the cache. Return the MD5 of the cached file.
-   * @param sourceFile
-   * @throws IOException 
-   */
-  public String cacheFile(File sourceFile, String filename) throws IOException {
-    LOG.debug("Trying to cache file {} with filename {}", sourceFile.getAbsolutePath(), filename);
-    File tmpFileName = null;
-    try {
-      if (!sourceFile.getParentFile().equals(getTmpDir())) {
-        // Provided file is not close to the cache so we will move it first in a temporary file (could be non atomic)
-        tmpFileName = getTemporaryFile();
-        Files.move(sourceFile, tmpFileName);
-      }
-      else {
-        tmpFileName = sourceFile;
-      }
-      // Now compute the md5 to find the final destination
-      String md5;
-      FileInputStream fis = null;
-      try {
-        fis = new FileInputStream(tmpFileName);
-        md5 = DigestUtils.md5Hex(fis);
-      } finally {
-        Closeables.closeQuietly(fis);
-      }
-      File finalDir = new File(cacheLocation, md5);
-      File finalFileName = new File(finalDir, filename);
-      // Try to create final destination folder
-      FileUtils.forceMkdir(finalDir);
-      // Now try to move the file from temporary folder to final location
-      boolean rename = tmpFileName.renameTo(finalFileName);
-      if (!rename) {
-        // Check if the file was already in cache
-        if (!finalFileName.exists()) {
-          LOG.warn("Unable to rename {} to {}", tmpFileName.getAbsolutePath(), finalFileName.getAbsolutePath());
-          LOG.warn("A copy/delete will be tempted but with no garantee of atomicity");
-          FileUtils.moveFile(tmpFileName, finalFileName);
-        }
-      }
-      LOG.debug("File cached at {}", finalFileName.getAbsolutePath());
-      return md5;
-    } finally {
-      FileUtils.deleteQuietly(tmpFileName);
-    }
-
-  }
-
-  /**
-   * Look for a file in the cache by its filename and md5 checksum. If the file is not
-   * present then return null.
-   */
-  public File getFileFromCache(String filename, String md5) {
-    File location = new File(new File(cacheLocation, md5), filename);
-    LOG.debug("Looking for {}", location.getAbsolutePath());
-    if (location.exists()) {
-      return location;
-    }
-    LOG.debug("No file found in the cache with name {} and checksum {}", filename, md5);
-    return null;
-  }
-
-  /**
-   * Return a temporary file that caller can use to store file content before
-   * asking for caching it with {@link #cacheFile(File)}.
-   * This is to avoid extra copy.
-   * @return
-   * @throws IOException 
-   */
-  public File getTemporaryFile() throws IOException {
-    return createTempFile(getTmpDir());
-  }
-
-  /**
-   * Create a temporary file in the given directory.
-   * @param baseDir
-   * @return
-   * @throws IOException 
-   */
-  private static File createTempFile(File baseDir) throws IOException {
-    String baseName = System.currentTimeMillis() + "-";
-
-    for (int counter = 0; counter < TEMP_FILE_ATTEMPTS; counter++) {
-      File tempFile = new File(baseDir, baseName + counter);
-      if (tempFile.createNewFile()) {
-        return tempFile;
-      }
-    }
-    throw new IOException("Failed to create temporary file in " + baseDir.getAbsolutePath() + " within "
-      + TEMP_FILE_ATTEMPTS + " attempts (tried "
-      + baseName + "0 to " + baseName + (TEMP_FILE_ATTEMPTS - 1) + ')');
-  }
-
-  public File getTmpDir() {
-    if (!tmpDir.exists()) {
-      LOG.debug("Creating temporary cache directory: {}", tmpDir.getAbsolutePath());
-      try {
-        FileUtils.forceMkdir(tmpDir);
-      } catch (IOException e) {
-        throw new RuntimeException("Unable to create temporary cache directory " + tmpDir.getAbsolutePath(), e);
-      }
-    }
-    return tmpDir;
-  }
-
-  public File getCacheLocation() {
-    return cacheLocation;
-  }
-}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/bootstrap/FileCacheProviderTest.java b/sonar-batch/src/test/java/org/sonar/batch/bootstrap/FileCacheProviderTest.java
new file mode 100644 (file)
index 0000000..305e6c9
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.batch.bootstrap;
+
+import org.junit.Test;
+import org.sonar.api.config.Settings;
+import org.sonar.home.cache.FileCache;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class FileCacheProviderTest {
+  @Test
+  public void provide() {
+    FileCacheProvider provider = new FileCacheProvider();
+    FileCache cache = provider.provide(new Settings());
+
+    assertThat(cache).isNotNull();
+    assertThat(cache.getDir()).isNotNull().exists();
+  }
+
+  @Test
+  public void keep_singleton_instance() {
+    FileCacheProvider provider = new FileCacheProvider();
+    Settings settings = new Settings();
+    FileCache cache1 = provider.provide(settings);
+    FileCache cache2 = provider.provide(settings);
+
+    assertThat(cache1).isSameAs(cache2);
+  }
+}
index 0047141fb2db38ebecfc939f69a25070da9b1353..df091fab76bcd6be768493afd15072d07fe3a19b 100644 (file)
@@ -25,15 +25,15 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.rules.TemporaryFolder;
-import org.mockito.Mockito;
 import org.sonar.api.CoreProperties;
 import org.sonar.api.config.Settings;
-import org.sonar.api.utils.SonarException;
-import org.sonar.batch.cache.SonarCache;
+import org.sonar.home.cache.FileCache;
 
 import java.io.File;
 
 import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
@@ -60,15 +60,10 @@ public class JdbcDriverHolderTest {
 
   @Test
   public void should_extend_classloader_with_jdbc_driver() throws Exception {
-    SonarCache cache = mock(SonarCache.class);
-    BatchSonarCache batchCache = mock(BatchSonarCache.class);
-    when(batchCache.getCache()).thenReturn(cache);
+    FileCache cache = mock(FileCache.class);
 
     File fakeDriver = new File(getClass().getResource("/org/sonar/batch/bootstrap/JdbcDriverHolderTest/jdbc-driver.jar").toURI());
-    when(cache.cacheFile(Mockito.any(File.class), Mockito.anyString())).thenReturn("fakemd5");
-    when(cache.getFileFromCache(Mockito.anyString(), Mockito.anyString()))
-        .thenReturn(null)
-        .thenReturn(fakeDriver);
+    when(cache.get(eq("ojdbc14.jar"), eq("fakemd5"), any(FileCache.Downloader.class))).thenReturn(fakeDriver);
 
     /* jdbc-driver.jar has just one file /foo/foo.txt */
     assertThat(Thread.currentThread().getContextClassLoader().getResource("foo/foo.txt")).isNull();
@@ -77,7 +72,7 @@ public class JdbcDriverHolderTest {
     when(server.request("/deploy/jdbc-driver.txt")).thenReturn("ojdbc14.jar|fakemd5");
     when(server.request("/deploy/ojdbc14.jar")).thenReturn("fakecontent");
 
-    JdbcDriverHolder holder = new JdbcDriverHolder(batchCache, new Settings(), server);
+    JdbcDriverHolder holder = new JdbcDriverHolder(cache, new Settings(), server);
     holder.start();
 
     assertThat(holder.getClassLoader().getResource("foo/foo.txt")).isNotNull();
@@ -89,35 +84,12 @@ public class JdbcDriverHolderTest {
     assertThat(holder.getClassLoader()).isNull();
   }
 
-  @Test
-  public void should_fail_if_checksum_mismatch() throws Exception {
-    SonarCache cache = mock(SonarCache.class);
-    BatchSonarCache batchCache = mock(BatchSonarCache.class);
-    when(batchCache.getCache()).thenReturn(cache);
-
-    File fakeDriver = new File(getClass().getResource("/org/sonar/batch/bootstrap/JdbcDriverHolderTest/jdbc-driver.jar").toURI());
-    when(cache.cacheFile(Mockito.any(File.class), Mockito.anyString())).thenReturn("anotherfakemd5");
-    when(cache.getFileFromCache(Mockito.anyString(), Mockito.anyString()))
-        .thenReturn(null)
-        .thenReturn(fakeDriver);
-
-    ServerClient server = mock(ServerClient.class);
-    when(server.request("/deploy/jdbc-driver.txt")).thenReturn("ojdbc14.jar|fakemd5");
-    when(server.request("/deploy/ojdbc14.jar")).thenReturn("fakecontent");
-
-    JdbcDriverHolder holder = new JdbcDriverHolder(batchCache, new Settings(), server);
-
-    thrown.expect(SonarException.class);
-    thrown.expectMessage("INVALID CHECKSUM");
-
-    holder.start();
-  }
-
   @Test
   public void should_be_disabled_if_dry_run() {
+    FileCache cache = mock(FileCache.class);
     Settings settings = new Settings().setProperty(CoreProperties.DRY_RUN, true);
     ServerClient server = mock(ServerClient.class);
-    JdbcDriverHolder holder = new JdbcDriverHolder(new BatchSonarCache(new Settings()), settings, server);
+    JdbcDriverHolder holder = new JdbcDriverHolder(cache, settings, server);
 
     holder.start();
 
index 86fca9177d41477ed50c0ca9b947f5be1f8fc7e3..d1b21b0fde965c3c396eeb732002f0966c52b939 100644 (file)
@@ -23,20 +23,18 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.rules.TemporaryFolder;
-import org.mockito.Mockito;
-import org.sonar.api.config.Settings;
 import org.sonar.api.utils.SonarException;
-import org.sonar.batch.cache.SonarCache;
 import org.sonar.core.plugins.RemotePlugin;
+import org.sonar.home.cache.FileCache;
 
 import java.io.File;
 import java.util.List;
 
 import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 public class PluginDownloaderTest {
@@ -49,9 +47,10 @@ public class PluginDownloaderTest {
 
   @Test
   public void should_request_list_of_plugins() {
+    FileCache cache = mock(FileCache.class);
     ServerClient server = mock(ServerClient.class);
     when(server.request("/deploy/plugins/index.txt")).thenReturn("checkstyle,true\nsqale,false");
-    PluginDownloader downloader = new PluginDownloader(new BatchSonarCache(new Settings()), server);
+    PluginDownloader downloader = new PluginDownloader(cache, server);
 
     List<RemotePlugin> plugins = downloader.downloadPluginIndex();
     assertThat(plugins).hasSize(2);
@@ -62,52 +61,23 @@ public class PluginDownloaderTest {
   }
 
   @Test
-  public void should_download_plugin_if_not_cached() throws Exception {
-    SonarCache cache = mock(SonarCache.class);
-    BatchSonarCache batchCache = mock(BatchSonarCache.class);
-    when(batchCache.getCache()).thenReturn(cache);
-
-    File fileInCache = temp.newFile();
-    when(cache.cacheFile(Mockito.any(File.class), Mockito.anyString())).thenReturn("fakemd51").thenReturn("fakemd52");
-    when(cache.getFileFromCache(Mockito.anyString(), Mockito.anyString()))
-        .thenReturn(null)
-        .thenReturn(fileInCache)
-        .thenReturn(null)
-        .thenReturn(fileInCache);
-    ServerClient server = mock(ServerClient.class);
-    PluginDownloader downloader = new PluginDownloader(batchCache, server);
-
-    RemotePlugin plugin = new RemotePlugin("checkstyle", true)
-        .addFile("checkstyle-plugin.jar", "fakemd51")
-        .addFile("checkstyle-extensions.jar", "fakemd52");
-    List<File> files = downloader.downloadPlugin(plugin);
+  public void should_download_plugin() throws Exception {
+    FileCache cache = mock(FileCache.class);
 
-    assertThat(files).hasSize(2);
-    verify(server).download(Mockito.eq("/deploy/plugins/checkstyle/checkstyle-plugin.jar"), Mockito.any(File.class));
-    verify(server).download(Mockito.eq("/deploy/plugins/checkstyle/checkstyle-extensions.jar"), Mockito.any(File.class));
-  }
+    File pluginJar = temp.newFile();
+    File extensionJar = temp.newFile();
+    when(cache.get(eq("checkstyle-plugin.jar"), eq("fakemd5_1"), any(FileCache.Downloader.class))).thenReturn(pluginJar);
+    when(cache.get(eq("checkstyle-extensions.jar"), eq("fakemd5_2"), any(FileCache.Downloader.class))).thenReturn(extensionJar);
 
-  @Test
-  public void should_not_download_plugin_if_cached() throws Exception {
-    SonarCache cache = mock(SonarCache.class);
-    BatchSonarCache batchCache = mock(BatchSonarCache.class);
-    when(batchCache.getCache()).thenReturn(cache);
-
-    File fileInCache = temp.newFile();
-    when(cache.getFileFromCache(Mockito.anyString(), Mockito.anyString()))
-        .thenReturn(fileInCache)
-        .thenReturn(fileInCache);
     ServerClient server = mock(ServerClient.class);
-    PluginDownloader downloader = new PluginDownloader(batchCache, server);
+    PluginDownloader downloader = new PluginDownloader(cache, server);
 
     RemotePlugin plugin = new RemotePlugin("checkstyle", true)
-        .addFile("checkstyle-plugin.jar", "fakemd51")
-        .addFile("checkstyle-extensions.jar", "fakemd52");
+      .addFile("checkstyle-plugin.jar", "fakemd5_1")
+      .addFile("checkstyle-extensions.jar", "fakemd5_2");
     List<File> files = downloader.downloadPlugin(plugin);
 
     assertThat(files).hasSize(2);
-    verify(server, never()).download(Mockito.anyString(), Mockito.any(File.class));
-    verify(cache, never()).cacheFile(Mockito.any(File.class), Mockito.anyString());
   }
 
   @Test
@@ -117,28 +87,6 @@ public class PluginDownloaderTest {
     ServerClient server = mock(ServerClient.class);
     doThrow(new SonarException()).when(server).request("/deploy/plugins/index.txt");
 
-    new PluginDownloader(new BatchSonarCache(new Settings()), server).downloadPluginIndex();
-  }
-
-  @Test
-  public void should_fail_if_invalid_checksum() throws Exception {
-    thrown.expect(SonarException.class);
-    thrown.expectMessage("INVALID CHECKSUM");
-
-    SonarCache cache = mock(SonarCache.class);
-    BatchSonarCache batchCache = mock(BatchSonarCache.class);
-    when(batchCache.getCache()).thenReturn(cache);
-
-    File fileInCache = temp.newFile();
-    when(cache.cacheFile(Mockito.any(File.class), Mockito.anyString())).thenReturn("fakemd51diff");
-    when(cache.getFileFromCache(Mockito.anyString(), Mockito.anyString()))
-        .thenReturn(null)
-        .thenReturn(fileInCache);
-    ServerClient server = mock(ServerClient.class);
-    PluginDownloader downloader = new PluginDownloader(batchCache, server);
-
-    RemotePlugin plugin = new RemotePlugin("checkstyle", true)
-        .addFile("checkstyle-plugin.jar", "fakemd51");
-    downloader.downloadPlugin(plugin);
+    new PluginDownloader(mock(FileCache.class), server).downloadPluginIndex();
   }
 }
diff --git a/sonar-batch/src/test/java/org/sonar/batch/cache/SonarCacheTest.java b/sonar-batch/src/test/java/org/sonar/batch/cache/SonarCacheTest.java
deleted file mode 100644 (file)
index dce64ba..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Sonar, open source software quality management tool.
- * Copyright (C) 2008-2012 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * Sonar 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.
- *
- * Sonar 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 Sonar; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
- */
-package org.sonar.batch.cache;
-
-import org.apache.commons.io.FileUtils;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-
-import java.io.File;
-import java.io.IOException;
-
-import static org.fest.assertions.Assertions.assertThat;
-
-public class SonarCacheTest {
-
-  @Rule
-  public TemporaryFolder tempFolder = new TemporaryFolder();
-
-  private SonarCache cache;
-
-  @Before
-  public void prepare() throws IOException {
-    cache = SonarCache.create(tempFolder.newFolder()).build();
-  }
-
-  @Test
-  public void testCacheExternalFile() throws IOException {
-    // Create a file outside the cache
-    File fileToCache = tempFolder.newFile();
-    FileUtils.write(fileToCache, "Sample data");
-    // Put it in the cache
-    String md5 = cache.cacheFile(fileToCache, "foo.txt");
-    // Verify the temporary location was created to do the copy in the cache in 2 stages
-    File tmpCache = new File(cache.getCacheLocation(), "tmp");
-    assertThat(tmpCache).exists();
-    // The tmp location should be empty as the file was moved inside the cache
-    assertThat(tmpCache.list()).isEmpty();
-    // Verify it is present in the cache folder
-    File fileInCache = new File(new File(cache.getCacheLocation(), md5), "foo.txt");
-    assertThat(fileInCache).exists();
-    String content = FileUtils.readFileToString(fileInCache);
-    assertThat(content).isEqualTo("Sample data");
-    // Now retrieve from cache API
-    File fileFromCache = cache.getFileFromCache("foo.txt", md5);
-    assertThat(fileFromCache.getCanonicalPath()).isEqualTo(fileInCache.getCanonicalPath());
-  }
-
-  @Test
-  public void testCacheInternalFile() throws IOException {
-    // Create a file in the cache temp location
-    File fileToCache = cache.getTemporaryFile();
-    // Verify the temporary location was created
-    File tmpCache = new File(cache.getCacheLocation(), "tmp");
-    assertThat(tmpCache).exists();
-    assertThat(tmpCache.list().length).isEqualTo(1);
-
-    FileUtils.write(fileToCache, "Sample data");
-    String md5 = cache.cacheFile(fileToCache, "foo.txt");
-    // Verify it is present in the cache folder
-    File fileInCache = new File(new File(cache.getCacheLocation(), md5), "foo.txt");
-    assertThat(fileInCache).exists();
-    String content = FileUtils.readFileToString(fileInCache);
-    assertThat(content).isEqualTo("Sample data");
-    // Now retrieve from cache API
-    File fileFromCache = cache.getFileFromCache("foo.txt", md5);
-    assertThat(fileFromCache.getCanonicalPath()).isEqualTo(fileInCache.getCanonicalPath());
-  }
-
-  @Test
-  public void testGetFileNotInCache() throws IOException {
-    File fileFromCache = cache.getFileFromCache("foo.txt", "mockmd5");
-    assertThat(fileFromCache).isNull();
-  }
-
-}
index 1ec0ff424822e2d4e1c4459b66fdc813199a449b..249bf40f4600338ec1149891f625c828761fd8f4 100644 (file)
       <groupId>org.codehaus.sonar</groupId>
       <artifactId>sonar-update-center-common</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.codehaus.sonar</groupId>
+      <artifactId>sonar-home</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.hibernate</groupId>
       <artifactId>hibernate-core</artifactId>
index 75be9c70a8a0f7ad8ea93c6dfe11fc9d541166c7..dc53d64f0eaa9e8e519e7cb31a6ef253c5f2060b 100644 (file)
 package org.sonar.core.plugins;
 
 import com.google.common.collect.Lists;
-import com.google.common.io.Closeables;
-import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.commons.lang.StringUtils;
+import org.sonar.home.cache.FileHashes;
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.util.List;
 
 public class RemotePlugin {
@@ -52,8 +50,8 @@ public class RemotePlugin {
     RemotePlugin result = new RemotePlugin(fields[0], Boolean.parseBoolean(fields[1]));
     if (fields.length > 2) {
       for (int index = 2; index < fields.length; index++) {
-        String[] nameAndMd5 = StringUtils.split(fields[index], "|");
-        result.addFile(nameAndMd5[0], nameAndMd5.length > 1 ? nameAndMd5[1] : null);
+        String[] nameAndHash = StringUtils.split(fields[index], "|");
+        result.addFile(nameAndHash[0], nameAndHash.length > 1 ? nameAndHash[1] : null);
       }
     }
     return result;
@@ -64,10 +62,7 @@ public class RemotePlugin {
     sb.append(pluginKey).append(",");
     sb.append(String.valueOf(core));
     for (RemotePluginFile file : files) {
-      sb.append(",").append(file.getFilename());
-      if (StringUtils.isNotBlank(file.getMd5())) {
-        sb.append("|").append(file.getMd5());
-      }
+      sb.append(",").append(file.getFilename()).append("|").append(file.getHash());
     }
     return sb.toString();
   }
@@ -80,23 +75,13 @@ public class RemotePlugin {
     return core;
   }
 
-  public RemotePlugin addFile(String filename, String md5) {
-    files.add(new RemotePluginFile(filename, md5));
+  public RemotePlugin addFile(String filename, String hash) {
+    files.add(new RemotePluginFile(filename, hash));
     return this;
   }
 
   public RemotePlugin addFile(File f) {
-    String md5;
-    FileInputStream fis = null;
-    try {
-      fis = new FileInputStream(f);
-      md5 = DigestUtils.md5Hex(fis);
-    } catch (Exception e) {
-      md5 = null;
-    } finally {
-      Closeables.closeQuietly(fis);
-    }
-    return this.addFile(f.getName(), md5);
+    return this.addFile(f.getName(), f.exists() ? new FileHashes().of(f) : null);
   }
 
   public List<RemotePluginFile> getFiles() {
index 88d85c3485b3fefa19a8466d6c7de666d8a18269..87c9495cacb7add1e4ca73dea7e7572d4c8961d4 100644 (file)
@@ -23,18 +23,18 @@ package org.sonar.core.plugins;
 public class RemotePluginFile {
 
   private String filename;
-  private String md5;
+  private String hash;
 
-  public RemotePluginFile(String filename, String md5) {
+  public RemotePluginFile(String filename, String hash) {
     this.filename = filename;
-    this.md5 = md5;
+    this.hash = hash;
   }
 
   public String getFilename() {
     return filename;
   }
 
-  public String getMd5() {
-    return md5;
+  public String getHash() {
+    return hash;
   }
 }
index b62dc1d3ad447fbc0225c1e9791966254fe95ede..d6b44ab6f753631031ff4d5d13ededab56f885c0 100644 (file)
@@ -60,7 +60,7 @@ public class RemotePluginTest {
     assertThat(clirr.isCore(), is(false));
     assertThat(clirr.getFiles().size(), is(1));
     assertThat(clirr.getFiles().get(0).getFilename(), is("clirr-1.1.jar"));
-    assertThat(clirr.getFiles().get(0).getMd5(), is("fakemd5"));
+    assertThat(clirr.getFiles().get(0).getHash(), is("fakemd5"));
 
   }
 
diff --git a/sonar-home/pom.xml b/sonar-home/pom.xml
new file mode 100644 (file)
index 0000000..d1e9b1b
--- /dev/null
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.codehaus.sonar</groupId>
+    <artifactId>sonar</artifactId>
+    <version>3.5-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>sonar-home</artifactId>
+
+  <name>Sonar :: Home</name>
+  <description>Access the user home directory that contains cache of files</description>
+
+  <dependencies>
+    <dependency>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>com.google.code.findbugs</groupId>
+      <artifactId>jsr305</artifactId>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.easytesting</groupId>
+      <artifactId>fest-assert</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <!-- used to compare results -->
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-all</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+  </dependencies>
+
+</project>
diff --git a/sonar-home/src/main/java/org/sonar/home/cache/FileCache.java b/sonar-home/src/main/java/org/sonar/home/cache/FileCache.java
new file mode 100644 (file)
index 0000000..5e57d82
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.home.cache;
+
+import org.apache.commons.io.FileUtils;
+import org.sonar.home.log.Log;
+
+import javax.annotation.CheckForNull;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * 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 {
+
+  private final File dir;
+  private final FileHashes hashes;
+  private final Log log;
+
+  FileCache(File dir, Log log, FileHashes fileHashes) {
+    this.dir = dir;
+    this.hashes = fileHashes;
+    this.log = log;
+    if (!dir.exists()) {
+      log.debug(String.format("Create cache directory: %s", dir.getAbsolutePath()));
+      try {
+        FileUtils.forceMkdir(dir);
+      } catch (IOException e) {
+        throw new IllegalStateException("Unable to create cache directory " + dir.getAbsolutePath(), e);
+      }
+    }
+    log.info(String.format("User cache: %s", dir.getAbsolutePath()));
+  }
+
+  public static FileCache create(File dir, Log log) {
+    return new FileCache(dir, log, new FileHashes());
+  }
+
+  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;
+    }
+    log.debug(String.format("No file found in the cache with name %s and hash %s", filename, hash));
+    return null;
+  }
+
+  public static 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(filename);
+      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 File newTempFile(String filename) {
+    try {
+      return File.createTempFile(filename, ".tmp");
+    } catch (IOException e) {
+      throw new IllegalStateException("Fail to create temp file", e);
+    }
+  }
+
+  private void renameQuietly(File sourceFile, File targetFile) {
+    boolean rename = sourceFile.renameTo(targetFile);
+    if (!rename) {
+      // Check if the file was cached by another process during download
+      if (!targetFile.exists()) {
+        log.warn(String.format("Unable to rename %s to %s", sourceFile.getAbsolutePath(), targetFile.getAbsolutePath()));
+        log.warn(String.format("A copy/delete will be tempted but with no garantee of atomicity"));
+        try {
+          FileUtils.moveFile(sourceFile, targetFile);
+        } 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 void mkdirQuietly(File hashDir) {
+    try {
+      FileUtils.forceMkdir(hashDir);
+    } catch (IOException e) {
+      throw new IllegalStateException("Fail to create cache directory: " + hashDir, e);
+    }
+  }
+}
diff --git a/sonar-home/src/main/java/org/sonar/home/cache/FileCacheBuilder.java b/sonar-home/src/main/java/org/sonar/home/cache/FileCacheBuilder.java
new file mode 100644 (file)
index 0000000..79c88b4
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.home.cache;
+
+import org.sonar.home.log.Log;
+import org.sonar.home.log.StandardLog;
+
+import javax.annotation.Nullable;
+
+import java.io.File;
+
+public class FileCacheBuilder {
+
+  private File userHome;
+  private Log log = new StandardLog();
+
+  public FileCacheBuilder setUserHome(File d) {
+    this.userHome = d;
+    return this;
+  }
+
+  public FileCacheBuilder setLog(Log log) {
+    this.log = log;
+    return this;
+  }
+
+  public FileCacheBuilder setUserHome(@Nullable String path) {
+    this.userHome = (path == null ? null : new File(path));
+    return this;
+  }
+
+  public FileCache build() {
+    if (userHome == null) {
+      String path = System.getenv("SONAR_USER_HOME");
+      if (path == null) {
+        // Default
+        path = System.getProperty("user.home") + File.separator + ".sonar";
+      }
+      userHome = new File(path);
+    }
+    File cacheDir = new File(userHome, "cache");
+    return FileCache.create(cacheDir, log);
+  }
+}
diff --git a/sonar-home/src/main/java/org/sonar/home/cache/FileHashes.java b/sonar-home/src/main/java/org/sonar/home/cache/FileHashes.java
new file mode 100644 (file)
index 0000000..89a973f
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.home.cache;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+
+/**
+ * Hashes used to store files in the cache directory.
+ *
+ * @since 3.5
+ */
+public class FileHashes {
+
+  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) {
+    DigestInputStream digestInputStream = null;
+    try {
+      MessageDigest digest = MessageDigest.getInstance("MD5");
+      digestInputStream = new DigestInputStream(input, digest);
+      while (digestInputStream.read() != -1) {
+      }
+      byte[] hash = digest.digest();
+      return toHex(hash);
+
+    } catch (Exception e) {
+      throw new IllegalStateException("Fail to compute hash", e);
+
+    } finally {
+      IOUtils.closeQuietly(digestInputStream);
+      IOUtils.closeQuietly(input);
+    }
+  }
+
+  static String toHex(byte[] bytes) {
+    BigInteger bi = new BigInteger(1, bytes);
+    return String.format("%0" + (bytes.length << 1) + "X", bi);
+  }
+}
\ No newline at end of file
diff --git a/sonar-home/src/main/java/org/sonar/home/cache/package-info.java b/sonar-home/src/main/java/org/sonar/home/cache/package-info.java
new file mode 100644 (file)
index 0000000..ca0cf64
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+
+@ParametersAreNonnullByDefault
+package org.sonar.home.cache;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-home/src/main/java/org/sonar/home/log/Log.java b/sonar-home/src/main/java/org/sonar/home/log/Log.java
new file mode 100644 (file)
index 0000000..21c6a0e
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.home.log;
+
+public interface Log {
+  void debug(String s);
+
+  void info(String s);
+
+  void warn(String s);
+
+  void error(String s, Throwable throwable);
+
+}
diff --git a/sonar-home/src/main/java/org/sonar/home/log/Slf4jLog.java b/sonar-home/src/main/java/org/sonar/home/log/Slf4jLog.java
new file mode 100644 (file)
index 0000000..736e624
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.home.log;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Slf4jLog implements Log {
+
+  private final Logger logger;
+
+  public Slf4jLog(Logger logger) {
+    this.logger = logger;
+  }
+
+  public Slf4jLog(Class loggerClass) {
+    this.logger = LoggerFactory.getLogger(loggerClass);
+  }
+
+  public boolean isDebugEnabled() {
+    return logger.isDebugEnabled();
+  }
+
+  public void debug(String s) {
+    logger.debug(s);
+  }
+
+  public void info(String s) {
+    logger.info(s);
+  }
+
+  public void warn(String s) {
+    logger.warn(s);
+  }
+
+  public void error(String s, Throwable throwable) {
+    logger.error(s, throwable);
+  }
+
+
+}
diff --git a/sonar-home/src/main/java/org/sonar/home/log/StandardLog.java b/sonar-home/src/main/java/org/sonar/home/log/StandardLog.java
new file mode 100644 (file)
index 0000000..264d5df
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.home.log;
+
+public class StandardLog implements Log {
+  public void debug(String s) {
+  }
+
+  public void info(String s) {
+  }
+
+  public void warn(String s) {
+  }
+
+  public void error(String s, Throwable throwable) {
+  }
+}
diff --git a/sonar-home/src/main/java/org/sonar/home/log/package-info.java b/sonar-home/src/main/java/org/sonar/home/log/package-info.java
new file mode 100644 (file)
index 0000000..fee20fa
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+
+@ParametersAreNonnullByDefault
+package org.sonar.home.log;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-home/src/test/java/org/sonar/home/cache/FileCacheBuilderTest.java b/sonar-home/src/test/java/org/sonar/home/cache/FileCacheBuilderTest.java
new file mode 100644 (file)
index 0000000..658180c
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.home.cache;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class FileCacheBuilderTest {
+  @Rule
+  public TemporaryFolder temp = new TemporaryFolder();
+
+  @Test
+  public void setUserHome() throws Exception {
+    File userHome = temp.newFolder();
+    FileCache cache = new FileCacheBuilder().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() throws Exception {
+    FileCache cache = new FileCacheBuilder().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() throws Exception {
+    FileCache cache = new FileCacheBuilder().build();
+
+    assertThat(cache.getDir()).isDirectory().exists();
+    assertThat(cache.getDir().getName()).isEqualTo("cache");
+  }
+}
diff --git a/sonar-home/src/test/java/org/sonar/home/cache/FileCacheTest.java b/sonar-home/src/test/java/org/sonar/home/cache/FileCacheTest.java
new file mode 100644 (file)
index 0000000..86382f6
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.home.cache;
+
+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 org.sonar.home.log.Slf4jLog;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.fest.assertions.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();
+
+  private Slf4jLog log = new Slf4jLog(FileCacheTest.class);
+
+  @Test
+  public void not_in_cache() throws IOException {
+    FileCache cache = FileCache.create(tempFolder.newFolder(), log);
+    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(), log);
+
+    // 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(), log, hashes);
+    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(), log, hashes);
+    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(), log, hashes);
+
+    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");
+  }
+}
diff --git a/sonar-home/src/test/java/org/sonar/home/cache/FileHashesTest.java b/sonar-home/src/test/java/org/sonar/home/cache/FileHashesTest.java
new file mode 100644 (file)
index 0000000..08f7227
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.home.cache;
+
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.IOUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.security.SecureRandom;
+
+import static org.fest.assertions.Assertions.assertThat;
+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();
+
+  @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).toUpperCase()
+      );
+    }
+  }
+
+  @Test
+  public void test_toHex() {
+    // upper-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()).toUpperCase()
+      );
+    }
+  }
+
+  @Test
+  public void fail_if_file_does_not_exist() {
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("Fail to compute hash of: /does/not/exist");
+
+    new FileHashes().of(new File("/does/not/exist"));
+  }
+
+  @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()).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);
+    }
+  }
+}
index 57c69dd389a06850a7ae6ca9bf741e358d070022..3186a6ca6d73425a410df4200b88a7ef9a948b6c 100644 (file)
@@ -376,12 +376,6 @@ public interface CoreProperties {
    */
   String TASK = "sonar.task";
 
-  /**
-   * @since 3.5
-   */
-  String SONAR_USER_HOME = "SONAR_USER_HOME";
-  String SONAR_USER_HOME_PROPERTY = "sonar.userHome";
-
   /**
    * @deprecated replaced in v3.4 by properties specific to languages, for example sonar.java.coveragePlugin
    * See http://jira.codehaus.org/browse/SONARJAVA-39 for more details.
index 4417542fa202eaa41e930e9f9bfe9eacdd8ca135..da24a99a4ea82f0188d81eeec442492625a748a3 100644 (file)
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
     <groupId>org.codehaus.sonar</groupId>
       <groupId>org.codehaus.sonar</groupId>
       <artifactId>sonar-deprecated</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.codehaus.sonar</groupId>
+      <artifactId>sonar-home</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.codehaus.sonar</groupId>
       <artifactId>sonar-java-api</artifactId>
index e449ad76500e00e82b88bffe2d2c2f50584e9f8d..1c04c970baf88badb99ee04e4df4da28d8c0de9b 100644 (file)
 package org.sonar.server.startup;
 
 import com.google.common.collect.Lists;
-import com.google.common.io.Closeables;
-import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang.CharUtils;
 import org.apache.commons.lang.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.sonar.home.cache.FileHashes;
 import org.sonar.server.platform.DefaultServerFileSystem;
 
 import javax.servlet.ServletContext;
@@ -44,8 +41,6 @@ import java.util.Set;
  */
 public final class GenerateBootstrapIndex {
 
-  private static final Logger LOG = LoggerFactory.getLogger(GenerateBootstrapIndex.class);
-
   private final ServletContext servletContext;
   private final DefaultServerFileSystem fileSystem;
 
@@ -64,16 +59,8 @@ public final class GenerateBootstrapIndex {
     try {
       for (String path : getLibs(servletContext)) {
         writer.append(path);
-        // Compute MD5
         InputStream is = servletContext.getResourceAsStream("/WEB-INF/lib/" + path);
-        try {
-          String md5 = DigestUtils.md5Hex(is);
-          writer.append("|").append(md5);
-        } catch (IOException e) {
-          LOG.warn("Unable to compute checksum of {}", path, e);
-        } finally {
-          Closeables.closeQuietly(is);
-        }
+        writer.append("|").append(new FileHashes().of(is));
         writer.append(CharUtils.LF);
       }
       writer.flush();
index 1e1a8a8b52bf3d8249aef516ed85159f0c77d87d..4bf99bcff4e044e7b97f5a6bf96b36f7755d00c1 100644 (file)
  */
 package org.sonar.server.startup;
 
-import com.google.common.io.Closeables;
-import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.commons.io.FileUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.sonar.home.cache.FileHashes;
 import org.sonar.server.platform.DefaultServerFileSystem;
 
 import java.io.File;
 import java.io.IOException;
-import java.io.InputStream;
 
 public class JdbcDriverDeployer {
 
-  private static final Logger LOG = LoggerFactory.getLogger(JdbcDriverDeployer.class);
-
   private final DefaultServerFileSystem fileSystem;
 
   public JdbcDriverDeployer(DefaultServerFileSystem fileSystem) {
@@ -43,7 +37,7 @@ public class JdbcDriverDeployer {
   public void start() {
     File driver = fileSystem.getJdbcDriver();
     File deployedDriver = new File(fileSystem.getDeployDir(), driver.getName());
-    if (deployedDriver == null || !deployedDriver.exists() || deployedDriver.length() != driver.length()) {
+    if (!deployedDriver.exists() || deployedDriver.length() != driver.length()) {
       try {
         FileUtils.copyFile(driver, deployedDriver);
 
@@ -52,16 +46,11 @@ public class JdbcDriverDeployer {
       }
     }
     File deployedDriverIndex = fileSystem.getDeployedJdbcDriverIndex();
-    // Compute MD5
-    InputStream is = null;
     try {
-      is = FileUtils.openInputStream(deployedDriver);
-      String md5 = DigestUtils.md5Hex(is);
-      FileUtils.writeStringToFile(deployedDriverIndex, deployedDriver.getName() + "|" + md5);
+      String hash = new FileHashes().of(deployedDriver);
+      FileUtils.writeStringToFile(deployedDriverIndex, deployedDriver.getName() + "|" + hash);
     } catch (IOException e) {
-      throw new RuntimeException("Can not generate index of JDBC driver", e);
-    } finally {
-      Closeables.closeQuietly(is);
+      throw new IllegalStateException("Can not generate index of JDBC driver", e);
     }
   }
 }
index 4cac71128704f1a49cef4354c2282053cd5a05fd..eeb5a76ef2eeee123d378f02562f2bc569c7e63b 100644 (file)
@@ -25,18 +25,15 @@ import org.sonar.server.platform.DefaultServerFileSystem;
 import org.sonar.test.TestUtils;
 
 import java.io.File;
-import java.io.IOException;
 
 import static org.fest.assertions.Assertions.assertThat;
-import static org.hamcrest.core.Is.is;
-import static org.junit.Assert.assertThat;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 public class JdbcDriverDeployerTest {
 
   @Test
-  public void testDeploy() throws IOException {
+  public void test_deploy() throws Exception {
     DefaultServerFileSystem fs = mock(DefaultServerFileSystem.class);
     File initialDriver = TestUtils.getResource(getClass(), "deploy/my-driver.jar");
     when(fs.getJdbcDriver()).thenReturn(initialDriver);
@@ -54,7 +51,7 @@ public class JdbcDriverDeployerTest {
 
     assertThat(deployedIndex).exists();
     assertThat(deployedFile).exists();
-    assertThat(deployedFile.length(), is(initialDriver.length()));
-    assertThat(FileUtils.readFileToString(deployedIndex)).isEqualTo("my-driver.jar|02b97f7bc37b2b68fc847fcc3fc1c156");
+    assertThat(deployedFile).hasSize(initialDriver.length());
+    assertThat(FileUtils.readFileToString(deployedIndex)).isEqualTo("my-driver.jar|02B97F7BC37B2B68FC847FCC3FC1C156");
   }
 }