GenerateEntityImplementationsProcessor.java
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.models.map.processor;
import org.keycloak.models.map.annotations.GenerateEntityImplementations;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.Diagnostic.Kind;
import javax.tools.JavaFileObject;
import static org.keycloak.models.map.processor.FieldAccessorType.*;
import static org.keycloak.models.map.processor.Util.isSetType;
import static org.keycloak.models.map.processor.Util.methodParameters;
import java.util.Collection;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.Optional;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
/**
*
* @author hmlnarik
*/
@SupportedAnnotationTypes("org.keycloak.models.map.annotations.GenerateEntityImplementations")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class GenerateEntityImplementationsProcessor extends AbstractGenerateEntityImplementationsProcessor {
private static final Collection<String> autogenerated = new TreeSet<>();
private static final String ID_FIELD_NAME = "Id";
private final Generator[] generators = new Generator[] {
new ClonerGenerator(),
new DelegateGenerator(),
new FieldsGenerator(),
new FieldDelegateGenerator(),
new ImplGenerator(),
};
@Override
protected void afterAnnotationProcessing() {
if (! autogenerated.isEmpty()) {
try {
JavaFileObject file = processingEnv.getFiler().createSourceFile("org.keycloak.models.map.common.AutogeneratedClasses");
try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) {
pw.println("package org.keycloak.models.map.common;");
pw.println("import " + FQN_DEEP_CLONER + ".Cloner;");
pw.println("import " + FQN_DEEP_CLONER + ".DelegateCreator;");
pw.println("import java.util.function.Function;");
pw.println("import " + FQN_DEEP_CLONER + ".EntityFieldDelegateCreator;");
pw.println("// DO NOT CHANGE THIS CLASS, IT IS GENERATED AUTOMATICALLY BY " + GenerateEntityImplementationsProcessor.class.getSimpleName());
generatedAnnotation(pw);
pw.println("public final class AutogeneratedClasses {");
pw.println(" public static final java.util.Map<Class<?>, Cloner<?>> CLONERS_WITH_ID = new java.util.HashMap<>();");
pw.println(" public static final java.util.Map<Class<?>, Cloner<?>> CLONERS_WITHOUT_ID = new java.util.HashMap<>();");
pw.println(" public static final java.util.Map<Class<?>, DelegateCreator<?>> DELEGATE_CREATORS = new java.util.HashMap<>();");
pw.println(" public static final java.util.Map<Class<?>, EntityFieldDelegateCreator<?>> ENTITY_FIELD_DELEGATE_CREATORS = new java.util.HashMap<>();");
pw.println(" public static final java.util.Map<Class<?>, Object> EMPTY_INSTANCES = new java.util.HashMap<>();");
pw.println(" public static final java.util.Map<Class<?>, EntityField<?>[]> ENTITY_FIELDS = new java.util.HashMap<>();");
pw.println(" public static final java.util.Map<Class<?>, Function<DeepCloner, ?>> CONSTRUCTORS_DC = new java.util.HashMap<>();");
pw.println(" static {");
autogenerated.forEach(pw::println);
pw.println(" }");
pw.println("}");
}
} catch (IOException ex) {
Logger.getLogger(GenerateEntityImplementationsProcessor.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
@Override
protected Generator[] getGenerators() {
return this.generators;
}
@Override
protected boolean testAnnotationElement(TypeElement e) {
if (e.getKind() != ElementKind.INTERFACE) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Annotation @GenerateEntityImplementations is only applicable to an interface", e);
return false;
}
return true;
}
protected static String toEnumConstant(String key) {
return key.replaceAll("([a-z])([A-Z])", "$1_$2").toUpperCase();
}
private class FieldsGenerator implements Generator {
@Override
public void generate(TypeElement e) throws IOException {
Map<String, HashSet<ExecutableElement>> methodsPerAttribute = methodsPerAttributeMapping(e);
String className = e.getQualifiedName().toString();
String packageName = null;
int lastDot = className.lastIndexOf('.');
if (lastDot > 0) {
packageName = className.substring(0, lastDot);
}
String simpleClassName = className.substring(lastDot + 1);
String mapFieldsClassName = className + "Fields";
String mapSimpleFieldsClassName = simpleClassName + "Fields";
JavaFileObject file = processingEnv.getFiler().createSourceFile(mapFieldsClassName);
try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) {
if (packageName != null) {
pw.println("package " + packageName + ";");
}
generatedAnnotation(pw);
pw.println("public enum " + mapSimpleFieldsClassName + " implements " + FQN_ENTITY_FIELD + "<" + className + "> {");
methodsPerAttribute.keySet().stream()
.sorted(NameFirstComparator.ID_INSTANCE)
.forEach(key -> {
pw.println(" " + toEnumConstant(key) + " {");
printEntityFieldMethods(pw, className, key, methodsPerAttribute.get(key));
pw.println(" },");
});
pw.println("}");
autogenerated.add(" ENTITY_FIELDS.put(" + className + ".class, " + mapFieldsClassName + ".values());");
}
}
private void printEntityFieldMethods(PrintWriter pw, String className, String fieldName, HashSet<ExecutableElement> methods) {
TypeMirror fieldType = determineFieldType(fieldName, methods);
pw.println(" public static final String FIELD_NAME = \"" + fieldName + "\";");
pw.println(" public static final String FIELD_NAME_DASHED = \"" + fieldName.replaceAll("([^_A-Z])([A-Z])", "$1-$2").toLowerCase() + "\";");
pw.println(" public static final String FIELD_NAME_CAMEL_CASE = \"" + fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1) + "\";");
pw.println(" @SuppressWarnings(\"unchecked\") @Override public Class<?> getFieldClass() {");
pw.println(" return " + types.erasure(fieldType) + ".class;");
pw.println(" }");
pw.println(" @Override public String getName() {");
pw.println(" return FIELD_NAME;");
pw.println(" }");
pw.println(" @Override public String getNameDashed() {");
pw.println(" return FIELD_NAME_DASHED;");
pw.println(" }");
pw.println(" @Override public String getNameCamelCase() {");
pw.println(" return FIELD_NAME_CAMEL_CASE;");
pw.println(" }");
FieldAccessorType.getMethod(FieldAccessorType.GETTER, methods, fieldName, types, fieldType).ifPresent(method -> {
if (Util.isCollectionType((TypeElement) types.asElement(types.erasure(fieldType)))) {
TypeMirror firstParameterType = Util.getGenericsDeclaration(method.getReturnType()).get(0);
pw.println(" @SuppressWarnings(\"unchecked\") @Override public Class<?> getCollectionElementClass() {");
pw.println(" return " + types.erasure(firstParameterType) + ".class;");
pw.println(" }");
} else if (Util.isMapType((TypeElement) types.asElement(types.erasure(fieldType)))) {
TypeMirror firstParameterType = Util.getGenericsDeclaration(method.getReturnType()).get(0);
TypeMirror secondParameterType = Util.getGenericsDeclaration(method.getReturnType()).get(1);
pw.println(" @SuppressWarnings(\"unchecked\") @Override public Class<?> getMapKeyClass() {");
pw.println(" return " + types.erasure(firstParameterType) + ".class;");
pw.println(" }");
pw.println(" @SuppressWarnings(\"unchecked\") @Override public Class<?> getMapValueClass() {");
pw.println(" return " + types.erasure(secondParameterType) + ".class;");
pw.println(" }");
}
});
for (ExecutableElement ee : methods) {
FieldAccessorType fat = FieldAccessorType.determineType(ee, fieldName, types, fieldType);
printMethodBody(pw, fat, ee, className, fieldType);
}
}
private void printMethodBody(PrintWriter pw, FieldAccessorType accessorType, ExecutableElement method, String className, TypeMirror fieldType) {
TypeMirror firstParameterType = method.getParameters().isEmpty()
? types.getNullType()
: method.getParameters().get(0).asType();
switch (accessorType) {
case GETTER:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " get(" + className + " e) {");
pw.println(" return (" + fieldType + ") e." + method.getSimpleName() + "();");
pw.println(" }");
return;
case SETTER:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public <T> void set(" + className + " e, T value) {");
pw.println(" e." + method.getSimpleName() + "((" + firstParameterType + ") value);");
pw.println(" }");
return;
case COLLECTION_ADD:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public <T> void collectionAdd(" + className + " e, T value) {");
pw.println(" e." + method.getSimpleName() + "((" + firstParameterType + ") value);");
pw.println(" }");
return;
case COLLECTION_DELETE:
{
String returnType = method.getReturnType().getKind() == TypeKind.VOID ? "Void" : method.getReturnType().toString();
TypeElement fieldTypeElement = elements.getTypeElement(types.erasure(fieldType).toString());
if (Util.isMapType(fieldTypeElement)) {
pw.println(" @SuppressWarnings(\"unchecked\") @Override public <K> " + returnType + " mapRemove(" + className + " e, K p0) {");
} else {
pw.println(" @SuppressWarnings(\"unchecked\") @Override public <T> " + returnType + " collectionRemove(" + className + " e, T p0) {");
}
if (method.getReturnType().getKind() == TypeKind.VOID) {
pw.println(" e." + method.getSimpleName() + "((" + firstParameterType + ") p0); return null;");
} else {
pw.println(" return e." + method.getSimpleName() + "((" + firstParameterType + ") p0);");
}
pw.println(" }");
return;
}
case COLLECTION_DELETE_BY_ID:
{
String returnType = method.getReturnType().getKind() == TypeKind.VOID ? "Void" : method.getReturnType().toString();
pw.println(" @SuppressWarnings(\"unchecked\") @Override public <K> " + returnType + " mapRemove(" + className + " e, K p0) {");
if (method.getReturnType().getKind() == TypeKind.VOID) {
pw.println(" e." + method.getSimpleName() + "((String) p0); return null;");
} else {
pw.println(" return e." + method.getSimpleName() + "((String) p0);");
}
pw.println(" }");
return;
}
case COLLECTION_GET_BY_ID:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public <K> " + method.getReturnType() + " mapGet(" + className + " e, K key) {");
pw.println(" return e." + method.getSimpleName() + "((" + firstParameterType + ") key);");
pw.println(" }");
return;
case MAP_ADD:
TypeMirror secondParameterType = method.getParameters().get(1).asType();
pw.println(" @SuppressWarnings(\"unchecked\") @Override public <K, T> void mapPut(" + className + " e, K key, T value) {");
pw.println(" e." + method.getSimpleName() + "((" + firstParameterType + ") key, (" + secondParameterType + ") value);");
pw.println(" }");
return;
case MAP_GET:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public <K> " + method.getReturnType() + " mapGet(" + className + " e, K key) {");
pw.println(" return (" + method.getReturnType() + ") e." + method.getSimpleName() + "((" + firstParameterType + ") key);");
pw.println(" }");
}
}
}
private class ImplGenerator implements Generator {
@Override
public void generate(TypeElement e) throws IOException {
Map<String, HashSet<ExecutableElement>> methodsPerAttribute = methodsPerAttributeMapping(e);
GenerateEntityImplementations an = e.getAnnotation(GenerateEntityImplementations.class);
TypeElement parentTypeElement = elements.getTypeElement((an.inherits() == null || an.inherits().isEmpty()) ? "void" : an.inherits());
if (parentTypeElement == null) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Unable to find type " + an.inherits() + " for inherits parameter for annotation " + GenerateEntityImplementations.class.getTypeName(), e);
}
final List<? extends Element> allParentMembers = elements.getAllMembers(parentTypeElement);
String className = e.getQualifiedName().toString();
String packageName = null;
int lastDot = className.lastIndexOf('.');
if (lastDot > 0) {
packageName = className.substring(0, lastDot);
}
String simpleClassName = className.substring(lastDot + 1);
String mapImplClassName = className + "Impl";
String mapSimpleClassName = simpleClassName + "Impl";
boolean hasId = methodsPerAttribute.containsKey(ID_FIELD_NAME) || allParentMembers.stream().anyMatch(el -> "getId".equals(el.getSimpleName().toString()));
boolean hasDeepClone = allParentMembers.stream().filter(el -> el.getKind() == ElementKind.METHOD).anyMatch(el -> "deepClone".equals(el.getSimpleName().toString()));
boolean needsDeepClone = fieldGetters(methodsPerAttribute)
.map(ExecutableElement::getReturnType)
.anyMatch(fieldType -> ! isKnownCollectionOfImmutableFinalTypes(fieldType) && ! isImmutableFinalType(fieldType));
boolean usingGeneratedCloner = ! hasDeepClone && needsDeepClone;
JavaFileObject file = processingEnv.getFiler().createSourceFile(mapImplClassName);
try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) {
if (packageName != null) {
pw.println("package " + packageName + ";");
}
pw.println("import java.util.Objects;");
pw.println("import java.util.Optional;");
pw.println("import " + FQN_DEEP_CLONER + ";");
pw.println("// DO NOT CHANGE THIS CLASS, IT IS GENERATED AUTOMATICALLY BY " + GenerateEntityImplementationsProcessor.class.getSimpleName());
generatedAnnotation(pw);
pw.println("public class " + mapSimpleClassName + (an.inherits().isEmpty() ? "" : " extends " + an.inherits()) + " implements " + className + " {");
pw.println(" "
+ "private "
+ mapSimpleClassName + "() { this(DeepCloner.DUMB_CLONER); } // Nullary constructor only for Jackson deserialization"
);
pw.println(" "
+ "public "
+ mapSimpleClassName + "(DeepCloner cloner) { super(); " + (!usingGeneratedCloner ? "" : "this.cloner = cloner;") + "}"
);
// equals, hashCode, toString
pw.println(" @Override public boolean equals(Object o) {");
pw.println(" if (o == this) return true; ");
pw.println(" if (! (o instanceof " + mapSimpleClassName + ")) return false; ");
pw.println(" " + mapSimpleClassName + " other = (" + mapSimpleClassName + ") o; ");
pw.println(" return "
+ fieldGetters(methodsPerAttribute)
.map(ExecutableElement::getSimpleName)
.map(Name::toString)
.sorted(NameFirstComparator.GET_ID_INSTANCE)
.map(v -> "Objects.equals(" + v + "(), other." + v + "())")
.collect(Collectors.joining("\n && "))
+ ";");
pw.println(" }");
pw.println(" @Override public int hashCode() {");
pw.println(" return "
+ (hasId
? "(getId() == null ? super.hashCode() : getId().hashCode())"
: "Objects.hash("
+ fieldGetters(methodsPerAttribute)
.filter(ee -> isImmutableFinalType(ee.getReturnType()))
.map(ExecutableElement::getSimpleName)
.map(Name::toString)
.sorted(NameFirstComparator.GET_ID_INSTANCE)
.map(v -> v + "()")
.collect(Collectors.joining(",\n "))
+ ")")
+ ";");
pw.println(" }");
pw.println(" @Override public String toString() {");
pw.println(" return String.format(\"%s@%08x\", " + (hasId ? "getId()" : "\"" + mapSimpleClassName + "\"" ) + ", System.identityHashCode(this));");
pw.println(" }");
// deepClone
if (usingGeneratedCloner) {
pw.println(" private final DeepCloner cloner;");
pw.println(" public <V> V deepClone(V obj) {");
pw.println(" return cloner.from(obj);");
pw.println(" }");
}
// fields, getters, setters
methodsPerAttribute.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey, NameFirstComparator.ID_INSTANCE)).forEach(me -> {
HashSet<ExecutableElement> methods = me.getValue();
TypeMirror fieldType = determineFieldType(me.getKey(), methods);
if (fieldType == null) {
return;
}
pw.println("");
pw.println(" private " + fieldType + " f" + me.getKey() + ";");
for (ExecutableElement method : methods) {
FieldAccessorType fat = FieldAccessorType.determineType(method, me.getKey(), types, fieldType);
Optional<ExecutableElement> parentMethod = Util.findParentMethodImplementation(allParentMembers, method);
if (parentMethod.isPresent()) {
processingEnv.getMessager().printMessage(Kind.OTHER, "Method " + method + " is declared in a parent class.", method);
} else if (fat == FieldAccessorType.UNKNOWN || ! printMethodBody(pw, fat, method, "f" + me.getKey(), fieldType)) {
processingEnv.getMessager().printMessage(Kind.WARNING, "Could not determine desired semantics of method from its signature", method);
}
}
});
// Read-only class overrides setters to be no-op
pw.println(" public static class Empty " + (an.inherits().isEmpty() ? "" : " extends " + an.inherits()) + " implements " + className + " {");
pw.println(" public static final Empty INSTANCE = new Empty();");
methodsPerAttribute.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey, NameFirstComparator.ID_INSTANCE))
.map(Map.Entry::getValue)
.flatMap(Collection::stream)
.forEach(ee -> {
pw.println(" @Override "
+ ee.getModifiers().stream().filter(m -> m != Modifier.ABSTRACT).map(Object::toString).collect(Collectors.joining(" "))
+ " " + ee.getReturnType()
+ " " + ee.getSimpleName()
+ "(" + methodParameters(ee.getParameters()) + ") {");
if (ee.getReturnType().getKind() == TypeKind.VOID) {
pw.println(" }");
} else {
pw.println(" return null;");
pw.println(" }");
}
});
elements.getAllMembers(e).stream()
.filter(ee -> ee.getSimpleName().contentEquals("isUpdated"))
.filter(ExecutableElement.class::isInstance)
.map(ExecutableElement.class::cast)
.filter(ee -> ee.getReturnType().getKind() == TypeKind.BOOLEAN)
.forEach(ee -> {
pw.println(" @Override "
+ ee.getModifiers().stream().filter(m -> m != Modifier.ABSTRACT).map(Object::toString).collect(Collectors.joining(" "))
+ " " + ee.getReturnType()
+ " " + ee.getSimpleName()
+ "(" + methodParameters(ee.getParameters()) + ") {");
pw.println(" return false;");
pw.println(" }");
});
pw.println(" }");
autogenerated.add(" EMPTY_INSTANCES.put(" + className + ".class, " + mapImplClassName + ".Empty.INSTANCE);");
autogenerated.add(" CONSTRUCTORS_DC.put(" + className + ".class, " + mapImplClassName + "::new);");
pw.println("}");
}
}
private boolean printMethodBody(PrintWriter pw, FieldAccessorType accessorType, ExecutableElement method, String fieldName, TypeMirror fieldType) {
TypeMirror firstParameterType = method.getParameters().isEmpty()
? types.getNullType()
: method.getParameters().get(0).asType();
TypeElement typeElement = elements.getTypeElement(types.erasure(fieldType).toString());
switch (accessorType) {
case GETTER:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method + " {");
pw.println(" return " + fieldName + ";");
pw.println(" }");
return true;
case SETTER:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
if (! isImmutableFinalType(fieldType)) {
pw.println(" p0 = " + deepClone(firstParameterType, "p0") + ";");
}
if (isCollection(firstParameterType)) {
pw.println(" if (p0 != null) {");
pw.println(" " + removeUndefined(firstParameterType, "p0") + ";");
pw.println(" if (" + isUndefined("p0") + ") p0 = null;");
pw.println(" }");
}
pw.println(" updated |= ! Objects.equals(" + fieldName + ", p0);");
pw.println(" " + fieldName + " = p0;");
pw.println(" }");
return true;
case COLLECTION_ADD:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
if (! isImmutableFinalType(firstParameterType)) {
pw.println(" p0 = " + deepClone(firstParameterType, "p0") + ";");
}
if (isCollection(firstParameterType)) {
pw.println(" if (p0 != null) " + removeUndefined(firstParameterType, "p0") + ";");
}
pw.println(" if (" + isUndefined("p0") + ") return;");
pw.println(" if (" + fieldName + " == null) { " + fieldName + " = " + interfaceToImplementation(typeElement, "") + "; }");
if (isSetType(typeElement)) {
pw.println(" updated |= " + fieldName + ".add(p0);");
} else {
pw.println(" " + fieldName + ".add(p0);");
pw.println(" updated = true;");
}
pw.println(" }");
return true;
case COLLECTION_DELETE:
{
boolean needsReturn = method.getReturnType().getKind() != TypeKind.VOID;
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
pw.println(" if (" + fieldName + " == null) { return" + (needsReturn ? " false" : "") + "; }");
pw.println(" boolean removed = " + fieldName + ".remove(p0)" + ("java.util.Map".equals(typeElement.getQualifiedName().toString()) ? " != null" : "") + ";");
pw.println(" updated |= removed;");
if (needsReturn) pw.println(" return removed;");
pw.println(" }");
return true;
}
case COLLECTION_DELETE_BY_ID:
{
boolean needsReturn = method.getReturnType().getKind() != TypeKind.VOID;
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(String p0) {");
pw.println(" boolean removed = " + fieldName + " != null && " + fieldName + ".removeIf(o -> Objects.equals(o." + getCollectionKey(fieldType, method) + ", p0));");
pw.println(" updated |= removed;");
if (needsReturn) pw.println(" return removed;");
pw.println(" }");
return true;
}
case COLLECTION_GET_BY_ID:
{
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(String p0) {");
pw.println(" if (" + fieldName + " == null || " + fieldName + ".isEmpty()) return Optional.empty();");
pw.println(" return " + fieldName + ".stream().filter(o -> Objects.equals(o." + getCollectionKey(fieldType, method) + ", p0)).findFirst();");
pw.println(" }");
return true;
}
case MAP_ADD:
TypeMirror secondParameterType = method.getParameters().get(1).asType();
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0, " + secondParameterType + " p1) {");
if (! isImmutableFinalType(secondParameterType)) {
pw.println(" p1 = " + deepClone(secondParameterType, "p1") + ";");
}
if (isCollection(secondParameterType)) {
pw.println(" if (p1 != null) " + removeUndefined(secondParameterType, "p1") + ";");
}
pw.println(" boolean valueUndefined = " + isUndefined("p1") + ";");
pw.println(" if (valueUndefined) { if (" + fieldName + " != null) { updated |= " + fieldName + ".remove(p0) != null; } return; }");
pw.println(" if (" + fieldName + " == null) { " + fieldName + " = " + interfaceToImplementation(typeElement, "") + "; }");
pw.println(" Object v = " + fieldName + ".put(p0, p1);");
pw.println(" updated |= ! Objects.equals(v, p1);");
pw.println(" }");
return true;
case MAP_GET:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
pw.println(" return " + fieldName + " == null ? null : " + fieldName + ".get(p0);");
pw.println(" }");
return true;
}
return false;
}
}
private class FieldDelegateGenerator implements Generator {
@Override
public void generate(TypeElement e) throws IOException {
Map<String, HashSet<ExecutableElement>> methodsPerAttribute = methodsPerAttributeMapping(e);
String className = e.getQualifiedName().toString();
String packageName = null;
int lastDot = className.lastIndexOf('.');
if (lastDot > 0) {
packageName = className.substring(0, lastDot);
}
String simpleClassName = className.substring(lastDot + 1);
String mapClassName = className + "FieldDelegate";
String mapSimpleClassName = simpleClassName + "FieldDelegate";
String fieldsClassName = className + "Fields";
GenerateEntityImplementations an = e.getAnnotation(GenerateEntityImplementations.class);
TypeElement parentTypeElement = elements.getTypeElement((an.inherits() == null || an.inherits().isEmpty()) ? "void" : an.inherits());
if (parentTypeElement == null) {
return;
}
JavaFileObject file = processingEnv.getFiler().createSourceFile(mapClassName);
IdentityHashMap<ExecutableElement, String> m2field = new IdentityHashMap<>();
methodsPerAttribute.forEach((f, s) -> s.forEach(m -> m2field.put(m, f))); // Create reverse map
try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) {
if (packageName != null) {
pw.println("package " + packageName + ";");
}
generatedAnnotation(pw);
pw.println("public class " + mapSimpleClassName + (an.inherits().isEmpty() ? "" : " extends " + an.inherits()) + " implements " + className + ", " + FQN_HAS_ENTITY_FIELD_DELEGATE + "<" + className + ">" + " {");
pw.println(" private final " + FQN_ENTITY_FIELD_DELEGATE + "<" + className + "> entityFieldDelegate;");
pw.println(" public " + mapSimpleClassName + "(" + FQN_ENTITY_FIELD_DELEGATE + "<" + className + "> entityFieldDelegate) {");
pw.println(" this.entityFieldDelegate = entityFieldDelegate;");
pw.println(" }");
pw.println(" public " + FQN_ENTITY_FIELD_DELEGATE + "<" + className + "> getEntityFieldDelegate() {");
pw.println(" return this.entityFieldDelegate;");
pw.println(" }");
pw.println(" @Override public boolean isUpdated() {");
pw.println(" return entityFieldDelegate.isUpdated();");
pw.println(" }");
pw.println(" @Override public void markUpdatedFlag() {");
pw.println(" entityFieldDelegate.markUpdatedFlag();");
pw.println(" }");
pw.println(" @Override public void clearUpdatedFlag() {");
pw.println(" entityFieldDelegate.clearUpdatedFlag();");
pw.println(" }");
pw.println(" @Override public String toString() {");
pw.println(" return \"%\" + String.valueOf(entityFieldDelegate);");
pw.println(" }");
getAllAbstractMethods(e)
.forEach(ee -> {
String originalField = m2field.get(ee);
if (originalField == null) {
return;
}
TypeMirror fieldType = determineFieldType(originalField, methodsPerAttribute.get(originalField));
String field = fieldsClassName + "." + toEnumConstant(originalField);
FieldAccessorType fat = FieldAccessorType.determineType(ee, originalField, types, fieldType);
printMethodBody(pw, fat, ee, field, fieldType);
});
autogenerated.add(" ENTITY_FIELD_DELEGATE_CREATORS.put(" + className + ".class, (EntityFieldDelegateCreator<" + className + ">) " + mapClassName + "::new);");
pw.println("}");
}
}
private boolean printMethodBody(PrintWriter pw, FieldAccessorType accessorType, ExecutableElement method, String fieldName, TypeMirror fieldType) {
TypeMirror firstParameterType = method.getParameters().isEmpty()
? types.getNullType()
: method.getParameters().get(0).asType();
switch (accessorType) {
case GETTER:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method + " {");
pw.println(" return (" + fieldType + ") entityFieldDelegate.get(" + fieldName + ");");
pw.println(" }");
return true;
case SETTER:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
pw.println(" entityFieldDelegate.set(" + fieldName + ", p0);");
pw.println(" }");
return true;
case COLLECTION_ADD:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
pw.println(" entityFieldDelegate.collectionAdd(" + fieldName + ", p0);");
pw.println(" }");
return true;
case COLLECTION_DELETE:
{
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
TypeElement fieldTypeElement = elements.getTypeElement(types.erasure(fieldType).toString());
String removeMethod = Util.isMapType(fieldTypeElement) ? "mapRemove" : "collectionRemove";
if (method.getReturnType().getKind() == TypeKind.VOID) {
pw.println(" entityFieldDelegate." + removeMethod + "(" + fieldName + ", p0);");
} else {
pw.println(" return (" + method.getReturnType() + ") entityFieldDelegate." + removeMethod + "(" + fieldName + ", p0);");
}
pw.println(" }");
return true;
}
case COLLECTION_DELETE_BY_ID:
{
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(String p0) {");
if (method.getReturnType().getKind() == TypeKind.VOID) {
pw.println(" entityFieldDelegate.mapRemove(" + fieldName + ", p0);");
} else {
pw.println(" return (" + method.getReturnType() + ") entityFieldDelegate.mapRemove(" + fieldName + ", p0);");
}
pw.println(" }");
return true;
}
case MAP_ADD:
TypeMirror secondParameterType = method.getParameters().get(1).asType();
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0, " + secondParameterType + " p1) {");
pw.println(" entityFieldDelegate.mapPut(" + fieldName + ", p0, p1);");
pw.println(" }");
return true;
case COLLECTION_GET_BY_ID:
case MAP_GET:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
pw.println(" return (" + method.getReturnType() + ") entityFieldDelegate.mapGet(" + fieldName + ", p0);");
pw.println(" }");
return true;
}
return false;
}
}
private class DelegateGenerator implements Generator {
@Override
public void generate(TypeElement e) throws IOException {
Map<String, HashSet<ExecutableElement>> methodsPerAttribute = methodsPerAttributeMapping(e);
String className = e.getQualifiedName().toString();
String packageName = null;
int lastDot = className.lastIndexOf('.');
if (lastDot > 0) {
packageName = className.substring(0, lastDot);
}
String simpleClassName = className.substring(lastDot + 1);
String mapClassName = className + "Delegate";
String mapSimpleClassName = simpleClassName + "Delegate";
String fieldsClassName = className + "Fields";
GenerateEntityImplementations an = e.getAnnotation(GenerateEntityImplementations.class);
TypeElement parentTypeElement = elements.getTypeElement((an.inherits() == null || an.inherits().isEmpty()) ? "void" : an.inherits());
if (parentTypeElement == null) {
return;
}
JavaFileObject file = processingEnv.getFiler().createSourceFile(mapClassName);
IdentityHashMap<ExecutableElement, String> m2field = new IdentityHashMap<>();
methodsPerAttribute.forEach((f, s) -> s.forEach(m -> m2field.put(m, f))); // Create reverse map
try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) {
if (packageName != null) {
pw.println("package " + packageName + ";");
}
generatedAnnotation(pw);
pw.println("public class " + mapSimpleClassName + " implements " + className + ", org.keycloak.models.map.common.delegate.HasDelegateProvider<" + className + "> {");
pw.println(" private final org.keycloak.models.map.common.delegate.DelegateProvider<" + className + "> delegateProvider;");
pw.println(" public " + mapSimpleClassName + "(org.keycloak.models.map.common.delegate.DelegateProvider<" + className + "> delegateProvider) {");
pw.println(" this.delegateProvider = delegateProvider;");
pw.println(" }");
pw.println(" public org.keycloak.models.map.common.delegate.DelegateProvider<" + className + "> getDelegateProvider() {");
pw.println(" return this.delegateProvider;");
pw.println(" }");
pw.println(" @Override public String toString() {");
pw.println(" return \"/\" + String.valueOf(this.delegateProvider);");
pw.println(" }");
getAllAbstractMethods(e)
.forEach(ee -> {
printMethodHeader(pw, ee);
String field = m2field.get(ee);
field = field == null ? "null" : fieldsClassName + "." + toEnumConstant(field);
if (ee.getReturnType().getKind() == TypeKind.BOOLEAN && "isUpdated".equals(ee.getSimpleName().toString())) {
pw.println(" return delegateProvider.isUpdated();");
} else if (ee.getReturnType().getKind() == TypeKind.VOID) { // write operation
pw.println(" delegateProvider.getDelegate(false, "
+ Stream.concat(Stream.of(field), ee.getParameters().stream().map(VariableElement::getSimpleName)).collect(Collectors.joining(", "))
+ ")." + ee.getSimpleName() + "("
+ ee.getParameters().stream().map(VariableElement::getSimpleName).collect(Collectors.joining(", "))
+ ");");
} else {
pw.println(" return delegateProvider.getDelegate(true, "
+ Stream.concat(Stream.of(field), ee.getParameters().stream().map(VariableElement::getSimpleName)).collect(Collectors.joining(", "))
+ ")." + ee.getSimpleName() + "("
+ ee.getParameters().stream().map(VariableElement::getSimpleName).collect(Collectors.joining(", "))
+ ");");
}
pw.println(" }");
});
pw.println("}");
autogenerated.add(" DELEGATE_CREATORS.put(" + className + ".class, (DelegateCreator<" + className + ">) " + mapClassName + "::new);");
}
}
}
protected void printMethodHeader(final PrintWriter pw, ExecutableElement ee) {
pw.println(" @Override "
+ ee.getModifiers().stream().filter(m -> m != Modifier.ABSTRACT).map(Object::toString).collect(Collectors.joining(" "))
+ " " + ee.getReturnType()
+ " " + ee.getSimpleName()
+ "(" + methodParameters(ee.getParameters()) + ") {");
}
private class ClonerGenerator implements Generator {
@Override
public void generate(TypeElement e) throws IOException {
Map<String, HashSet<ExecutableElement>> methodsPerAttribute = methodsPerAttributeMapping(e);
String className = e.getQualifiedName().toString();
String packageName = null;
int lastDot = className.lastIndexOf('.');
if (lastDot > 0) {
packageName = className.substring(0, lastDot);
}
String simpleClassName = className.substring(lastDot + 1);
String clonerImplClassName = className + "Cloner";
String clonerSimpleClassName = simpleClassName + "Cloner";
JavaFileObject enumFile = processingEnv.getFiler().createSourceFile(clonerImplClassName);
try (PrintWriter pw = new PrintWriter(enumFile.openWriter()) {
@Override
public void println(String x) {
super.println(x == null ? x : x.replaceAll("java.lang.", ""));
}
}) {
if (packageName != null) {
pw.println("package " + packageName + ";");
}
pw.println("import " + FQN_DEEP_CLONER + ";");
pw.println("// DO NOT CHANGE THIS CLASS, IT IS GENERATED AUTOMATICALLY BY " + GenerateEntityImplementationsProcessor.class.getSimpleName());
generatedAnnotation(pw);
pw.println("public class " + clonerSimpleClassName + " {");
if (methodsPerAttribute.containsKey(ID_FIELD_NAME)) {
pw.println(" public static " + className + " deepClone(" + className + " original, " + className + " target) {");
// If the entity has an ID, set the ID first and then set all other attributes.
// This was important when working with Jpa storage as the ID is the one field needed to persist an entity.
HashSet<ExecutableElement> idMethods = methodsPerAttribute.get(ID_FIELD_NAME);
TypeMirror idFieldType = determineFieldType(ID_FIELD_NAME, idMethods);
cloneField(e, ID_FIELD_NAME, idMethods, idFieldType, pw);
pw.println(" return deepCloneNoId(original, target);");
pw.println(" }");
autogenerated.add(" CLONERS_WITH_ID.put(" + className + ".class, (Cloner<" + className + ">) " + clonerImplClassName + "::deepClone);");
pw.println(" public static " + className + " deepCloneNoId(" + className + " original, " + className + " target) {");
methodsPerAttribute.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)).forEach(me -> {
final String fieldName = me.getKey();
HashSet<ExecutableElement> methods = me.getValue();
TypeMirror fieldType = determineFieldType(fieldName, methods);
if (fieldType == null || ID_FIELD_NAME.equals(fieldName)) {
return;
}
cloneField(e, fieldName, methods, fieldType, pw);
});
pw.println(" target.clearUpdatedFlag();");
pw.println(" return target;");
pw.println(" }");
autogenerated.add(" CLONERS_WITHOUT_ID.put(" + className + ".class, (Cloner<" + className + ">) " + clonerImplClassName + "::deepCloneNoId);");
} else {
pw.println(" public static " + className + " deepClone(" + className + " original, " + className + " target) {");
methodsPerAttribute.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)).forEach(me -> {
final String fieldName = me.getKey();
HashSet<ExecutableElement> methods = me.getValue();
TypeMirror fieldType = determineFieldType(fieldName, methods);
if (fieldType == null) {
return;
}
cloneField(e, fieldName, methods, fieldType, pw);
});
pw.println(" target.clearUpdatedFlag();");
pw.println(" return target;");
pw.println(" }");
autogenerated.add(" CLONERS_WITH_ID.put(" + className + ".class, (Cloner<" + className + ">) " + clonerImplClassName + "::deepClone);");
}
pw.println("}");
}
}
private void cloneField(TypeElement e, final String fieldName, HashSet<ExecutableElement> methods, TypeMirror fieldType, final PrintWriter pw) {
ExecutableElement getter = FieldAccessorType.getMethod(GETTER, methods, fieldName, types, fieldType).orElse(null);
if (getter == null) {
processingEnv.getMessager().printMessage(Kind.WARNING, "Could not determine getter for " + fieldName + " property");
return;
}
Optional<ExecutableElement> setter = FieldAccessorType.getMethod(SETTER, methods, fieldName, types, fieldType);
Optional<ExecutableElement> addToCollection = FieldAccessorType.getMethod(COLLECTION_ADD, methods, fieldName, types, fieldType);
Optional<ExecutableElement> updateMap = FieldAccessorType.getMethod(MAP_ADD, methods, fieldName, types, fieldType);
if (setter.isPresent()) {
final Name setterName = setter.get().getSimpleName();
// Setter always deep-clones whatever comes from the original, so we don't clone the value here.
pw.println(" target." + setterName + "(original." + getter.getSimpleName() + "());");
} else if (addToCollection.isPresent()) {
pw.println(" if (original." + getter.getSimpleName() + "() != null) {");
pw.println(" original." + getter.getSimpleName() + "().forEach(target::" + addToCollection.get().getSimpleName() + ");");
pw.println(" }");
} else if (updateMap.isPresent()) {
pw.println(" if (original." + getter.getSimpleName() + "() != null) {");
pw.println(" original." + getter.getSimpleName() + "().forEach(target::" + updateMap.get().getSimpleName() + ");");
pw.println(" }");
} else {
processingEnv.getMessager().printMessage(Kind.ERROR, "Could not determine way to clone " + fieldName + " property", e);
}
}
}
}