/*
* Copyright 2015 Ben Manes. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.github.benmanes.caffeine.cache;
import static com.github.benmanes.caffeine.cache.Specifications.BOUNDED_LOCAL_CACHE;
import static com.github.benmanes.caffeine.cache.Specifications.BUILDER_PARAM;
import static com.github.benmanes.caffeine.cache.Specifications.CACHE_LOADER_PARAM;
import static com.github.benmanes.caffeine.cache.Specifications.kTypeVar;
import static com.github.benmanes.caffeine.cache.Specifications.vTypeVar;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Year;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;
import javax.lang.model.element.Modifier;
import com.github.benmanes.caffeine.cache.local.AddConstructor;
import com.github.benmanes.caffeine.cache.local.AddDeques;
import com.github.benmanes.caffeine.cache.local.AddExpirationTicker;
import com.github.benmanes.caffeine.cache.local.AddExpireAfterAccess;
import com.github.benmanes.caffeine.cache.local.AddExpireAfterWrite;
import com.github.benmanes.caffeine.cache.local.AddFastPath;
import com.github.benmanes.caffeine.cache.local.AddKeyValueStrength;
import com.github.benmanes.caffeine.cache.local.AddMaximum;
import com.github.benmanes.caffeine.cache.local.AddRefreshAfterWrite;
import com.github.benmanes.caffeine.cache.local.AddRemovalListener;
import com.github.benmanes.caffeine.cache.local.AddStats;
import com.github.benmanes.caffeine.cache.local.AddSubtype;
import com.github.benmanes.caffeine.cache.local.AddWriteBuffer;
import com.github.benmanes.caffeine.cache.local.Finalize;
import com.github.benmanes.caffeine.cache.local.LocalCacheContext;
import com.github.benmanes.caffeine.cache.local.LocalCacheRule;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.common.io.Resources;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
/**
* Generates a factory that creates the cache optimized for the user specified configuration.
*
* @author ben.manes@gmail.com (Ben Manes)
*/
public final class LocalCacheFactoryGenerator {
final Feature[] featureByIndex = new Feature[] {null, null, Feature.LISTENING,
Feature.STATS, Feature.MAXIMUM_SIZE, Feature.MAXIMUM_WEIGHT, Feature.EXPIRE_ACCESS,
Feature.EXPIRE_WRITE, Feature.REFRESH_WRITE};
final List<LocalCacheRule> rules = ImmutableList.of(new AddSubtype(), new AddConstructor(),
new AddKeyValueStrength(), new AddRemovalListener(), new AddStats(),
new AddExpirationTicker(), new AddMaximum(), new AddFastPath(), new AddDeques(),
new AddExpireAfterAccess(), new AddExpireAfterWrite(), new AddRefreshAfterWrite(),
new AddWriteBuffer(), new Finalize());
final NavigableMap<String, ImmutableSet<Feature>> classNameToFeatures;
final Path directory;
TypeSpec.Builder factory;
public LocalCacheFactoryGenerator(Path directory) {
this.directory = requireNonNull(directory);
this.classNameToFeatures = new TreeMap<>();
}
void generate() throws IOException {
factory = TypeSpec.classBuilder("LocalCacheFactory")
.addModifiers(Modifier.FINAL)
.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "{$S, $S, $S, $S}", "unchecked", "unused", "PMD", "MissingOverride")
.build());
addClassJavaDoc();
generateLocalCaches();
addFactoryMethods();
writeJavaFile();
}
private void addFactoryMethods() {
factory.addMethod(newBoundedLocalCache());
}
private MethodSpec newBoundedLocalCache() {
Preconditions.checkState(!classNameToFeatures.isEmpty(), "Must generate all cache types first");
return MethodSpec.methodBuilder("newBoundedLocalCache")
.addTypeVariable(kTypeVar)
.addTypeVariable(vTypeVar)
.returns(BOUNDED_LOCAL_CACHE)
.addModifiers(Modifier.STATIC)
.addCode(LocalCacheSelectorCode.get(classNameToFeatures.keySet()))
.addParameter(BUILDER_PARAM)
.addParameter(CACHE_LOADER_PARAM)
.addParameter(boolean.class, "async")
.addJavadoc("Returns a cache optimized for this configuration.\n")
.build();
}
private void writeJavaFile() throws IOException {
String header = Resources.toString(Resources.getResource("license.txt"), UTF_8).trim();
JavaFile.builder(getClass().getPackage().getName(), factory.build())
.addFileComment(header, Year.now())
.indent(" ")
.build()
.writeTo(directory);
}
private void generateLocalCaches() {
fillClassNameToFeatures();
classNameToFeatures.forEach((className, features) -> {
String higherKey = classNameToFeatures.higherKey(className);
boolean isLeaf = (higherKey == null) || !higherKey.startsWith(className);
addLocalCacheSpec(className, isLeaf, features);
});
}
private void fillClassNameToFeatures() {
for (List<Object> combination : combinations()) {
Set<Feature> features = new LinkedHashSet<>();
features.add(((Boolean) combination.get(0)) ? Feature.STRONG_KEYS : Feature.WEAK_KEYS);
features.add(((Boolean) combination.get(1)) ? Feature.STRONG_VALUES : Feature.INFIRM_VALUES);
for (int i = 2; i < combination.size(); i++) {
if ((Boolean) combination.get(i)) {
features.add(featureByIndex[i]);
}
}
if (features.contains(Feature.MAXIMUM_WEIGHT)) {
features.remove(Feature.MAXIMUM_SIZE);
}
String className = encode(Feature.makeClassName(features));
classNameToFeatures.put(className, ImmutableSet.copyOf(features));
}
}
private Set<List<Object>> combinations() {
Set<Boolean> options = ImmutableSet.of(true, false);
List<Set<Boolean>> sets = new ArrayList<>();
for (int i = 0; i < featureByIndex.length; i++) {
sets.add(options);
}
return Sets.cartesianProduct(sets);
}
private void addLocalCacheSpec(String className, boolean isFinal, Set<Feature> features) {
TypeName superClass;
Set<Feature> parentFeatures;
Set<Feature> generateFeatures;
if (features.size() == 2) {
parentFeatures = ImmutableSet.of();
generateFeatures = features;
superClass = BOUNDED_LOCAL_CACHE;
} else {
parentFeatures = ImmutableSet.copyOf(Iterables.limit(features, features.size() - 1));
generateFeatures = ImmutableSet.of(Iterables.getLast(features));
superClass = ParameterizedTypeName.get(ClassName.bestGuess(
encode(Feature.makeClassName(parentFeatures))), kTypeVar, vTypeVar);
}
LocalCacheContext context = new LocalCacheContext(
superClass, className, isFinal, parentFeatures, generateFeatures);
for (LocalCacheRule rule : rules) {
rule.accept(context);
}
factory.addType(context.cache.build());
}
private void addClassJavaDoc() {
factory.addJavadoc("<em>WARNING: GENERATED CODE</em>\n\n")
.addJavadoc("A factory for caches optimized for a particular configuration.\n")
.addJavadoc("\n@author ben.manes@gmail.com (Ben Manes)\n");
}
/** Returns an encoded form of the class name for compact use. */
private static String encode(String className) {
return Feature.makeEnumName(className)
.replaceFirst("STRONG_KEYS", "S")
.replaceFirst("WEAK_KEYS", "W")
.replaceFirst("_STRONG_VALUES", "S")
.replaceFirst("_INFIRM_VALUES", "I")
.replaceFirst("_LISTENING", "Li")
.replaceFirst("_STATS", "S")
.replaceFirst("_MAXIMUM", "M")
.replaceFirst("_WEIGHT", "W")
.replaceFirst("_SIZE", "S")
.replaceFirst("_EXPIRE_ACCESS", "A")
.replaceFirst("_EXPIRE_WRITE", "W")
.replaceFirst("_REFRESH_WRITE", "R");
}
public static void main(String[] args) throws IOException {
new LocalCacheFactoryGenerator(Paths.get(args[0])).generate();
}
}