APLCache.java

210 lines | 7.929 kB Blame History Raw Download
/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package br.ufrgs.inf.prosoft.aplcache.caching;

import br.ufrgs.inf.prosoft.cache.Caffeine;
import br.ufrgs.inf.prosoft.cache.KeyNotFoundException;
import br.ufrgs.inf.prosoft.jsonserialiser.JSONSerialiser;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class APLCache<V> {

  private static final Logger LOGGER = Logger.getLogger(APLCache.class.getName());
  private static final Map<String, Map<String, Long>> APLCACHE_METHOD_HAS_CACHEABLE_PARAMETERS = new HashMap<>();
  private static final String APLCACHE_LOG = System.getenv("APLCACHE_LOG") != null && !System.getenv("APLCACHE_LOG").isEmpty() && !System.getenv("APLCACHE_LOG").equals("null")
    ? System.getenv("APLCACHE_LOG") : null;
  private static String APLCACHE_CACHEABLE_PARAMETERS = System.getenv("APLCACHE_CACHEABLE_PARAMETERS");
  private final Caffeine<String, V> caffeine;

  public APLCache() {
    this.caffeine = new Caffeine<>();
  }

  public APLCache(String name) {
    this.caffeine = new Caffeine<>(name);
  }

  public APLCache(long ttl) {
    this.caffeine = new Caffeine<>(ttl);
  }

  public APLCache(String name, long ttl) {
    this.caffeine = new Caffeine<>(name, ttl);
  }

  private synchronized static void loadCacheableParameters() {
    if (APLCACHE_CACHEABLE_PARAMETERS == null) return;
    try (FileReader fileReader = new FileReader(APLCACHE_CACHEABLE_PARAMETERS)) {
      JsonParser jsonParser = new JsonParser();
      JsonObject jsonObject = jsonParser.parse(fileReader).getAsJsonObject();
      jsonObject.entrySet().forEach(entry -> {
        Map<String, Long> inputsHasTTL = new HashMap<>();
        if (entry.getValue().isJsonArray()) entry.getValue().getAsJsonArray().forEach(parameter -> inputsHasTTL.put(parameter.getAsString(), 0L));
        else entry.getValue().getAsJsonObject().entrySet().forEach(innerEntry -> inputsHasTTL.put(innerEntry.getKey(), innerEntry.getValue().getAsLong()));
        APLCACHE_METHOD_HAS_CACHEABLE_PARAMETERS.put(entry.getKey(), inputsHasTTL);
      });
      LOGGER.log(Level.INFO, "cache file loaded");
    } catch (IOException ex) {
      LOGGER.log(Level.SEVERE, "invalid cache file");
    }
    APLCACHE_CACHEABLE_PARAMETERS = null;
  }

  private static void log(String message) {
    if (APLCACHE_LOG == null) {
      return;
    }
    try (FileWriter fileWriter = new FileWriter(APLCACHE_LOG, true)) {
      fileWriter.write(message + "\n");
    } catch (IOException ex) {
    }
  }

  public boolean isCacheable(Thread currentThread, Object... parameters) {
    StackTraceElement[] stackTrace = currentThread.getStackTrace();
    StackTraceElement stackTraceElement;
    try {
      stackTraceElement = stackTrace[2];
    } catch (ArrayIndexOutOfBoundsException ex) {
      stackTraceElement = stackTrace[0];
    }
    return isCacheable(stackTraceElement, parameters);
  }

  public boolean isCacheable(StackTraceElement stackTraceElement, Object... parameters) {
    try {
      String methodName = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName();
      methodName = methodName.replace("$", ".");
      return isCacheable(methodName, parameters);
    } catch (Exception ex) {
      return false;
    }
  }

  public boolean isCacheable(String method, Object... parameters) {
    loadCacheableParameters();
    if (APLCACHE_METHOD_HAS_CACHEABLE_PARAMETERS.isEmpty()) {
      LOGGER.log(Level.WARNING, "no method is cacheable");
      return false;
    }
    Map<String, Long> cacheableInputsHasTTL = APLCACHE_METHOD_HAS_CACHEABLE_PARAMETERS.get(method);
    if (cacheableInputsHasTTL == null) {
      LOGGER.log(Level.WARNING, "method not cacheable: {0}", method);
      return false;
    }
    String serialisedParameters = Stream.of(parameters).map(JSONSerialiser::serialise)
      .collect(Collectors.joining(",", "[", "]"));
    if (cacheableInputsHasTTL.containsKey(serialisedParameters)) {
      return true;
    }
    log(this.caffeine.getCachePerformance().getName() + " : " + serialisedParameters);
    return false;
  }

  public boolean isCacheable(String method, String parameters) {
    loadCacheableParameters();
    if (APLCACHE_METHOD_HAS_CACHEABLE_PARAMETERS.isEmpty()) {
      LOGGER.log(Level.WARNING, "no method is cacheable");
      return false;
    }
    Map<String, Long> cacheableParameters = APLCACHE_METHOD_HAS_CACHEABLE_PARAMETERS.get(method);
    if (cacheableParameters == null) {
      LOGGER.log(Level.WARNING, "method not cacheable: {0}", method);
      return false;
    }
    if (cacheableParameters.containsKey(parameters)) {
      return true;
    }
    log(this.caffeine.getCachePerformance().getName() + " : " + parameters);
    return false;
  }

  public long getTTLforInput(String method, String input) {
    loadCacheableParameters();
    if (APLCACHE_METHOD_HAS_CACHEABLE_PARAMETERS.isEmpty()) {
      LOGGER.log(Level.WARNING, "no method is cacheable");
      return 0;
    }
    Map<String, Long> cacheableInputsHasTTL = APLCACHE_METHOD_HAS_CACHEABLE_PARAMETERS.get(method);
    if (cacheableInputsHasTTL == null) {
      LOGGER.log(Level.WARNING, "method not cacheable: {0}", method);
      return 0;
    }
    return cacheableInputsHasTTL.get(input);
  }

  public V computeIfAbsent(Thread currentThread, Object[] parameters, Supplier<V> supplier, long timeToLive) {
    StackTraceElement[] stackTrace = currentThread.getStackTrace();
    StackTraceElement stackTraceElement;
    try {
      stackTraceElement = stackTrace[2];
    } catch (ArrayIndexOutOfBoundsException ex) {
      stackTraceElement = stackTrace[0];
    }
    String methodName = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName();
    methodName = methodName.replace("$", ".");

    String serialisedParameters = Stream.of(parameters).map(JSONSerialiser::serialise)
      .collect(Collectors.joining(",", "[", "]"));
    synchronized (serialisedParameters) {
      try {
        return this.caffeine.get(serialisedParameters);
      } catch (KeyNotFoundException ex) {
        V get = supplier.get();
        if (isCacheable(methodName, serialisedParameters)) {
          long recommendedTTLforInput = getTTLforInput(methodName, serialisedParameters);
          if (recommendedTTLforInput == 0) this.caffeine.put(serialisedParameters, get, timeToLive);
          else this.caffeine.put(serialisedParameters, get, recommendedTTLforInput);
        }
        return get;
      }
    }
  }

  public V computeIfAbsent(Thread currentThread, Object[] parameters, Supplier<V> supplier) {
    StackTraceElement[] stackTrace = currentThread.getStackTrace();
    StackTraceElement stackTraceElement;
    try {
      stackTraceElement = stackTrace[2];
    } catch (ArrayIndexOutOfBoundsException ex) {
      stackTraceElement = stackTrace[0];
    }
    String methodName = stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName();
    methodName = methodName.replace("$", ".");

    String serialisedParameters = Stream.of(parameters).map(JSONSerialiser::serialise)
      .collect(Collectors.joining(",", "[", "]"));
    synchronized (serialisedParameters) {
      try {
        return this.caffeine.get(serialisedParameters);
      } catch (KeyNotFoundException ex) {
        V get = supplier.get();
        if (isCacheable(methodName, serialisedParameters)) {
          long ttLforInput = getTTLforInput(methodName, serialisedParameters);
          if (ttLforInput == 0) {
            this.caffeine.put(serialisedParameters, get);
          } else {
            this.caffeine.put(serialisedParameters, get, ttLforInput);
          }
        }
        return get;
      }
    }
  }
}