private void unlock() {
    if (lock != null) {
      try {
        lock.release();
      } catch (IOException e) {
        log.error("Error releasing lock", e);
      }
    }
    if (lock_fc != null) {
      try {
        lock_fc.close();
      } catch (IOException e) {
        log.error("Error closing file channel", e);
      }
    }
    if (lock_raf != null) {
      try {
        lock_raf.close();
      } catch (IOException e) {
        log.error("Error closing file", e);
      }
    }

    lock = null;
    lock_raf = null;
    lock_fc = null;
  }
  @CheckForNull
  public synchronized byte[] get(@Nonnull String obj, @Nullable Callable<byte[]> valueLoader)
      throws Exception {
    String key = getKey(obj);

    try {
      lock();
      if (!forceUpdate) {
        byte[] cached = getCache(key);

        if (cached != null) {
          log.debug("cache hit for " + obj + " -> " + key);
          return cached;
        }

        log.debug("cache miss for " + obj + " -> " + key);
      } else {
        log.debug("cache force update for " + obj + " -> " + key);
      }

      if (valueLoader != null) {
        byte[] value = valueLoader.call();
        if (value != null) {
          putCache(key, value);
        }
        return value;
      }
    } finally {
      unlock();
    }

    return null;
  }
 /**
  * Deletes cache entries that are no longer valid according to the default expiration time period.
  */
 public synchronized void clean() {
   log.info("cache: cleaning");
   try {
     lock();
     deleteCacheEntries(createCleanFilter());
   } catch (IOException e) {
     log.error("Error cleaning cache", e);
   } finally {
     unlock();
   }
 }
  public PersistentCache(
      Path baseDir, long defaultDurationToExpireMs, Log log, boolean forceUpdate) {
    this.baseDir = baseDir;
    this.defaultDurationToExpireMs = defaultDurationToExpireMs;
    this.log = log;

    reconfigure(forceUpdate);
    log.debug("cache: " + baseDir + ", default expiration time (ms): " + defaultDurationToExpireMs);
  }
 private void deleteCacheEntries(DirectoryStream.Filter<Path> filter) throws IOException {
   try (DirectoryStream<Path> stream = Files.newDirectoryStream(baseDir, filter)) {
     for (Path p : stream) {
       try {
         Files.delete(p);
       } catch (Exception e) {
         log.error("Error deleting " + p, e);
       }
     }
   }
 }
  public void reconfigure(boolean forceUpdate) {
    this.forceUpdate = forceUpdate;

    if (forceUpdate) {
      log.debug("cache: forcing update");
    }

    try {
      Files.createDirectories(baseDir);
    } catch (IOException e) {
      throw new IllegalStateException("failed to create cache dir", e);
    }
  }
  private boolean validateCacheEntry(Path cacheEntryPath, long durationToExpireMs)
      throws IOException {
    if (!Files.exists(cacheEntryPath)) {
      return false;
    }

    if (isCacheEntryExpired(cacheEntryPath, durationToExpireMs)) {
      log.debug("cache: expiring entry");
      Files.delete(cacheEntryPath);
      return false;
    }

    return true;
  }