/* * 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.BUILDER_PARAM; import static com.github.benmanes.caffeine.cache.Specifications.DEAD_STRONG_KEY; import static com.github.benmanes.caffeine.cache.Specifications.DEAD_WEAK_KEY; import static com.github.benmanes.caffeine.cache.Specifications.PACKAGE_NAME; import static com.github.benmanes.caffeine.cache.Specifications.RETIRED_STRONG_KEY; import static com.github.benmanes.caffeine.cache.Specifications.RETIRED_WEAK_KEY; import static com.github.benmanes.caffeine.cache.Specifications.kRefQueueType; import static com.github.benmanes.caffeine.cache.Specifications.kTypeVar; import static com.github.benmanes.caffeine.cache.Specifications.keyRefQueueSpec; import static com.github.benmanes.caffeine.cache.Specifications.keyRefSpec; import static com.github.benmanes.caffeine.cache.Specifications.keySpec; import static com.github.benmanes.caffeine.cache.Specifications.lookupKeyType; import static com.github.benmanes.caffeine.cache.Specifications.rawReferenceKeyType; import static com.github.benmanes.caffeine.cache.Specifications.referenceKeyType; import static com.github.benmanes.caffeine.cache.Specifications.vTypeVar; import static com.github.benmanes.caffeine.cache.Specifications.valueRefQueueSpec; import static com.github.benmanes.caffeine.cache.Specifications.valueSpec; import static com.google.common.base.Preconditions.checkState; 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.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.node.AddConstructors; import com.github.benmanes.caffeine.cache.node.AddDeques; import com.github.benmanes.caffeine.cache.node.AddExpiration; import com.github.benmanes.caffeine.cache.node.AddHealth; import com.github.benmanes.caffeine.cache.node.AddKey; import com.github.benmanes.caffeine.cache.node.AddMaximum; import com.github.benmanes.caffeine.cache.node.AddSubtype; import com.github.benmanes.caffeine.cache.node.AddToString; import com.github.benmanes.caffeine.cache.node.AddValue; import com.github.benmanes.caffeine.cache.node.Finalize; import com.github.benmanes.caffeine.cache.node.NodeContext; import com.github.benmanes.caffeine.cache.node.NodeRule; 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.FieldSpec; 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 the cache entry factory and specialized types. These entries are optimized for the * configuration to minimize memory use. An entry may have any of the following properties: * <ul> * <li>strong or weak keys * <li>strong, weak, or soft values * <li>access timestamp * <li>write timestamp * <li>weight * </ul> * <p> * If the cache has either a maximum size or expires after access, then the entry will also contain * prev/next references on a access ordered queue. If the cache expires after write, then the entry * will also contain prev/next on a write ordered queue. * * @author ben.manes@gmail.com (Ben Manes) */ public final class NodeFactoryGenerator { final Path directory; final NavigableMap<String, ImmutableSet<Feature>> classNameToFeatures; final List<NodeRule> rules = ImmutableList.of(new AddSubtype(), new AddConstructors(), new AddKey(), new AddValue(), new AddMaximum(), new AddExpiration(), new AddDeques(), new AddHealth(), new AddToString(), new Finalize()); TypeSpec.Builder nodeFactory; public NodeFactoryGenerator(Path directory) { this.directory = requireNonNull(directory); this.classNameToFeatures = new TreeMap<>(); } void generate() throws IOException { nodeFactory = TypeSpec.enumBuilder("NodeFactory") .addAnnotation(AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "{$S, $S, $S, $S}", "unchecked", "PMD", "GuardedByChecker", "MissingOverride") .build()); addClassJavaDoc(); addNodeStateStatics(); addKeyMethods(); generatedNodes(); addGetFactoryMethods(); writeJavaFile(); } private void writeJavaFile() throws IOException { String header = Resources.toString(Resources.getResource("license.txt"), UTF_8).trim(); JavaFile.builder(getClass().getPackage().getName(), nodeFactory.build()) .addFileComment(header, Year.now()) .indent(" ") .build() .writeTo(directory); } private void addClassJavaDoc() { nodeFactory.addJavadoc("<em>WARNING: GENERATED CODE</em>\n\n") .addJavadoc("A factory for cache nodes optimized for a particular configuration.\n") .addJavadoc("\n@author ben.manes@gmail.com (Ben Manes)\n"); } private void addNodeStateStatics() { Modifier[] modifiers = { Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL }; nodeFactory.addField(FieldSpec.builder(Object.class, RETIRED_STRONG_KEY, modifiers) .initializer("new Object()") .build()); nodeFactory.addField(FieldSpec.builder(Object.class, DEAD_STRONG_KEY, modifiers) .initializer("new Object()") .build()); nodeFactory.addField(FieldSpec.builder(rawReferenceKeyType, RETIRED_WEAK_KEY, modifiers) .initializer("new $T(null, null)", rawReferenceKeyType) .build()); nodeFactory.addField(FieldSpec.builder(rawReferenceKeyType, DEAD_WEAK_KEY, modifiers) .initializer("new $T(null, null)", rawReferenceKeyType) .build()); } private void addKeyMethods() { nodeFactory.addMethod(newNodeByKeyAbstractMethod()) .addMethod(newNodeByKeyRefAbstractMethod()) .addMethod(newLookupKeyMethod()) .addMethod(newReferenceKeyMethod()); } private MethodSpec newNodeByKeyAbstractMethod() { return newNodeByKey().addModifiers(Modifier.ABSTRACT) .addJavadoc("Returns a node optimized for the specified features.\n").build(); } private MethodSpec newNodeByKeyRefAbstractMethod() { return newNodeByKeyRef().addModifiers(Modifier.ABSTRACT) .addJavadoc("Returns a node optimized for the specified features.\n").build(); } private MethodSpec newLookupKeyMethod() { return MethodSpec.methodBuilder("newLookupKey") .addJavadoc("Returns a key suitable for looking up an entry in the cache. If the cache " + "holds keys strongly\nthen the key is returned. If the cache holds keys weakly " + "then a {@link $T}\nholding the key argument is returned.\n", lookupKeyType) .addTypeVariable(kTypeVar) .addParameter(kTypeVar, "key") .addStatement("return key") .returns(Object.class) .build(); } private MethodSpec newReferenceKeyMethod() { return MethodSpec.methodBuilder("newReferenceKey") .addJavadoc("Returns a key suitable for inserting into the cache. If the cache holds" + " keys strongly then\nthe key is returned. If the cache holds keys weakly " + "then a {@link $T}\nholding the key argument is returned.\n", referenceKeyType) .addTypeVariable(kTypeVar) .addParameter(kTypeVar, "key") .addParameter(kRefQueueType, "referenceQueue") .returns(Object.class) .addStatement("return $L", "key") .build(); } private void addGetFactoryMethods() { checkState(!classNameToFeatures.isEmpty(), "Must generate all cache types first"); nodeFactory.addMethod(MethodSpec.methodBuilder("getFactory") .addJavadoc("Returns a factory optimized for the specified features.\n") .addModifiers(Modifier.STATIC) .addTypeVariable(kTypeVar) .addTypeVariable(vTypeVar) .addParameter(BUILDER_PARAM) .addParameter(boolean.class, "isAsync") .addCode(NodeSelectorCode.get()) .returns(ClassName.bestGuess("NodeFactory")) .build()); nodeFactory.addMethod(MethodSpec.methodBuilder("weakValues") .addJavadoc("Returns whether this factory supports the weak values.\n") .addStatement("return name().matches($S)", ".W.*") .returns(boolean.class) .build()); nodeFactory.addMethod(MethodSpec.methodBuilder("softValues") .addJavadoc("Returns whether this factory supports the soft values.\n") .addStatement("return name().matches($S)", ".So.*") .returns(boolean.class) .build()); } private void generatedNodes() { fillClassNameToFeatures(); classNameToFeatures.forEach((className, features) -> { String higherKey = classNameToFeatures.higherKey(className); boolean isLeaf = (higherKey == null) || !higherKey.startsWith(className); addNodeSpec(className, isLeaf, features); }); } private void fillClassNameToFeatures() { Feature[] featureByIndex = new Feature[] { null, null, Feature.EXPIRE_ACCESS, Feature.EXPIRE_WRITE, Feature.REFRESH_WRITE, Feature.MAXIMUM_SIZE, Feature.MAXIMUM_WEIGHT }; for (List<Object> combination : combinations()) { Set<Feature> features = new LinkedHashSet<>(); features.add((Feature) combination.get(0)); features.add((Feature) combination.get(1)); 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 = Feature.makeClassName(features); classNameToFeatures.put(encode(className), ImmutableSet.copyOf(features)); } } private void addNodeSpec(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 = TypeName.OBJECT; } else { parentFeatures = ImmutableSet.copyOf(Iterables.limit(features, features.size() - 1)); generateFeatures = ImmutableSet.of(Iterables.getLast(features)); superClass = ParameterizedTypeName.get(ClassName.get(PACKAGE_NAME + ".NodeFactory", encode(Feature.makeClassName(parentFeatures))), kTypeVar, vTypeVar); } NodeContext context = new NodeContext(superClass, className, isFinal, parentFeatures, generateFeatures); for (NodeRule rule : rules) { rule.accept(context); } nodeFactory.addType(context.nodeSubtype.build()); addEnumConstant(className, features); } private void addEnumConstant(String className, Set<Feature> features) { String statementWithKey = makeFactoryStatementKey(); String statementWithKeyRef = makeFactoryStatementKeyRef(); TypeSpec.Builder typeSpec = TypeSpec.anonymousClassBuilder("") .addMethod(newNodeByKey() .addStatement(statementWithKey, className).build()) .addMethod(newNodeByKeyRef() .addStatement(statementWithKeyRef, className).build()); if (features.contains(Feature.WEAK_KEYS)) { typeSpec.addMethod(makeNewLookupKey()); typeSpec.addMethod(makeReferenceKey()); } nodeFactory.addEnumConstant(className, typeSpec.build()); } private String makeFactoryStatementKey() { return "return new $N<>(key, keyReferenceQueue, value, valueReferenceQueue, weight, now)"; } private String makeFactoryStatementKeyRef() { return "return new $N<>(keyReference, value, valueReferenceQueue, weight, now)"; } private MethodSpec makeNewLookupKey() { return MethodSpec.methodBuilder("newLookupKey") .addTypeVariable(kTypeVar) .addParameter(kTypeVar, "key") .addStatement("return new $T(key)", lookupKeyType) .returns(Object.class) .build(); } private MethodSpec makeReferenceKey() { return MethodSpec.methodBuilder("newReferenceKey") .addTypeVariable(kTypeVar) .addParameter(kTypeVar, "key") .addParameter(kRefQueueType, "referenceQueue") .addStatement("return new $T($L, $L)", referenceKeyType, "key", "referenceQueue") .returns(Object.class) .build(); } private Set<List<Object>> combinations() { Set<Feature> keyStrengths = ImmutableSet.of(Feature.STRONG_KEYS, Feature.WEAK_KEYS); Set<Feature> valueStrengths = ImmutableSet.of( Feature.STRONG_VALUES, Feature.WEAK_VALUES, Feature.SOFT_VALUES); Set<Boolean> expireAfterAccess = ImmutableSet.of(false, true); Set<Boolean> expireAfterWrite = ImmutableSet.of(false, true); Set<Boolean> refreshAfterWrite = ImmutableSet.of(false, true); Set<Boolean> maximumSize = ImmutableSet.of(false, true); Set<Boolean> weighed = ImmutableSet.of(false, true); @SuppressWarnings("unchecked") Set<List<Object>> combinations = Sets.cartesianProduct(keyStrengths, valueStrengths, expireAfterAccess, expireAfterWrite, refreshAfterWrite, maximumSize, weighed); return combinations; } private MethodSpec.Builder newNodeByKey() { return completeNewNode(MethodSpec.methodBuilder("newNode") .addParameter(keySpec) .addParameter(keyRefQueueSpec)); } private MethodSpec.Builder newNodeByKeyRef() { return completeNewNode(MethodSpec.methodBuilder("newNode").addParameter(keyRefSpec)); } private MethodSpec.Builder completeNewNode(MethodSpec.Builder method) { return method .addTypeVariable(kTypeVar) .addTypeVariable(vTypeVar) .addParameter(valueSpec) .addParameter(valueRefQueueSpec) .addParameter(int.class, "weight") .addParameter(long.class, "now") .returns(Specifications.NODE); } /** 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", "St") .replaceFirst("_WEAK_VALUES", "W") .replaceFirst("_SOFT_VALUES", "So") .replaceFirst("_EXPIRE_ACCESS", "A") .replaceFirst("_EXPIRE_WRITE", "W") .replaceFirst("_REFRESH_WRITE", "R") .replaceFirst("_MAXIMUM", "M") .replaceFirst("_WEIGHT", "W") .replaceFirst("_SIZE", "S"); } public static void main(String[] args) throws IOException { new NodeFactoryGenerator(Paths.get(args[0])).generate(); } }