All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonar.runner.cache.PersistentCache Maven / Gradle / Ivy

/*
 * SonarQube Runner - API
 * Copyright (C) 2011 SonarSource
 * [email protected]
 *
 * 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;

  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 filter) throws IOException {
    try (DirectoryStream 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 {
    @Override
    public boolean accept(Path entry) throws IOException {
      return !lock.getFileLockName().equals(entry.getFileName().toString());
    }
  }

  private class DirectoryCleanFilter implements DirectoryStream.Filter {
    @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);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy