Create delegates and empty instances in DeepCloner

Fixes: #9030
This commit is contained in:
Hynek Mlnarik 2021-11-02 12:32:55 +01:00 committed by Hynek Mlnařík
parent 02b1932413
commit 3c7e5c8440
15 changed files with 1099 additions and 59 deletions

View file

@ -45,13 +45,20 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import static org.keycloak.models.map.processor.FieldAccessorType.GETTER; import static org.keycloak.models.map.processor.FieldAccessorType.GETTER;
import static org.keycloak.models.map.processor.Util.getGenericsDeclaration; import static org.keycloak.models.map.processor.Util.getGenericsDeclaration;
import static org.keycloak.models.map.processor.Util.isMapType; import static org.keycloak.models.map.processor.Util.isMapType;
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public abstract class AbstractGenerateEntityImplementationsProcessor extends AbstractProcessor { public abstract class AbstractGenerateEntityImplementationsProcessor extends AbstractProcessor {
protected static final String FQN_DEEP_CLONER = "org.keycloak.models.map.common.DeepCloner"; protected static final String FQN_DEEP_CLONER = "org.keycloak.models.map.common.DeepCloner";
protected static final String FQN_ENTITY_FIELD = "org.keycloak.models.map.common.EntityField";
protected static final String FQN_HAS_ENTITY_FIELD_DELEGATE = "org.keycloak.models.map.common.delegate.HasEntityFieldDelegate";
protected static final String FQN_ENTITY_FIELD_DELEGATE = "org.keycloak.models.map.common.delegate.EntityFieldDelegate";
protected Elements elements; protected Elements elements;
protected Types types; protected Types types;
@ -100,16 +107,19 @@ public abstract class AbstractGenerateEntityImplementationsProcessor extends Abs
// ); // );
} }
protected Map<String, HashSet<ExecutableElement>> methodsPerAttributeMapping(TypeElement e) { protected Stream<ExecutableElement> getAllAbstractMethods(TypeElement e) {
final List<? extends Element> allMembers = elements.getAllMembers(e); return elements.getAllMembers(e).stream()
Map<String, HashSet<ExecutableElement>> methodsPerAttribute = allMembers.stream()
.filter(el -> el.getKind() == ElementKind.METHOD) .filter(el -> el.getKind() == ElementKind.METHOD)
.filter(el -> el.getModifiers().contains(Modifier.ABSTRACT)) .filter(el -> el.getModifiers().contains(Modifier.ABSTRACT))
.filter(Util::isNotIgnored)
.filter(ExecutableElement.class::isInstance) .filter(ExecutableElement.class::isInstance)
.map(ExecutableElement.class::cast) .map(ExecutableElement.class::cast);
}
protected Map<String, HashSet<ExecutableElement>> methodsPerAttributeMapping(TypeElement e) {
Map<String, HashSet<ExecutableElement>> methodsPerAttribute = getAllAbstractMethods(e)
.filter(Util::isNotIgnored)
.filter(ee -> ! (ee.getReceiverType() instanceof NoType)) .filter(ee -> ! (ee.getReceiverType() instanceof NoType))
.collect(Collectors.toMap(this::determineAttributeFromMethodName, v -> new HashSet(Arrays.asList(v)), (a, b) -> { a.addAll(b); return a; })); .collect(Collectors.toMap(this::determineAttributeFromMethodName, v -> new HashSet<>(Arrays.asList(v)), (a,b) -> { a.addAll(b); return a; }));
// Merge plurals with singulars // Merge plurals with singulars
methodsPerAttribute.keySet().stream() methodsPerAttribute.keySet().stream()
@ -129,7 +139,7 @@ public abstract class AbstractGenerateEntityImplementationsProcessor extends Abs
FORBIDDEN_PREFIXES.put("delete", "remove"); FORBIDDEN_PREFIXES.put("delete", "remove");
} }
private String determineAttributeFromMethodName(ExecutableElement e) { protected String determineAttributeFromMethodName(ExecutableElement e) {
Name name = e.getSimpleName(); Name name = e.getSimpleName();
Matcher m = BEAN_NAME.matcher(name.toString()); Matcher m = BEAN_NAME.matcher(name.toString());
if (m.matches()) { if (m.matches()) {

View file

@ -17,12 +17,12 @@
package org.keycloak.models.map.processor; package org.keycloak.models.map.processor;
import org.keycloak.models.map.annotations.GenerateEntityImplementations; import org.keycloak.models.map.annotations.GenerateEntityImplementations;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion; import javax.annotation.processing.SupportedSourceVersion;
@ -47,6 +47,7 @@ import java.util.Optional;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.stream.Stream;
import javax.lang.model.element.VariableElement; import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeKind;
@ -58,30 +59,36 @@ import javax.lang.model.type.TypeKind;
@SupportedSourceVersion(SourceVersion.RELEASE_8) @SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GenerateEntityImplementationsProcessor extends AbstractGenerateEntityImplementationsProcessor { public class GenerateEntityImplementationsProcessor extends AbstractGenerateEntityImplementationsProcessor {
private Collection<String> cloners = new TreeSet<>(); private final Collection<String> autogenerated = new TreeSet<>();
private final Generator[] generators = new Generator[] { private final Generator[] generators = new Generator[] {
new ClonerGenerator(), new ClonerGenerator(),
new DelegateGenerator(), new DelegateGenerator(),
new FieldsGenerator(), new FieldsGenerator(),
new ImplGenerator() new FieldDelegateGenerator(),
new ImplGenerator(),
}; };
@Override @Override
protected void afterAnnotationProcessing() { protected void afterAnnotationProcessing() {
if (! cloners.isEmpty()) { if (! autogenerated.isEmpty()) {
try { try {
JavaFileObject file = processingEnv.getFiler().createSourceFile("org.keycloak.models.map.common.AutogeneratedCloners"); JavaFileObject file = processingEnv.getFiler().createSourceFile("org.keycloak.models.map.common.AutogeneratedCloners");
try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) { try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) {
pw.println("package org.keycloak.models.map.common;"); pw.println("package org.keycloak.models.map.common;");
pw.println("import org.keycloak.models.map.common.DeepCloner.Cloner;"); pw.println("import " + FQN_DEEP_CLONER + ".Cloner;");
pw.println("import " + FQN_DEEP_CLONER + ".DelegateCreator;");
pw.println("import " + FQN_DEEP_CLONER + ".EntityFieldDelegateCreator;");
pw.println("// DO NOT CHANGE THIS CLASS, IT IS GENERATED AUTOMATICALLY BY " + GenerateEntityImplementationsProcessor.class.getSimpleName()); pw.println("// DO NOT CHANGE THIS CLASS, IT IS GENERATED AUTOMATICALLY BY " + GenerateEntityImplementationsProcessor.class.getSimpleName());
pw.println("public final class AutogeneratedCloners {"); pw.println("public final class AutogeneratedCloners {");
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_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<?>, 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(" static {"); pw.println(" static {");
cloners.forEach(pw::println); autogenerated.forEach(pw::println);
pw.println(" }"); pw.println(" }");
pw.println("}"); pw.println("}");
} }
@ -111,6 +118,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
} }
private class FieldsGenerator implements Generator { private class FieldsGenerator implements Generator {
@Override @Override
public void generate(TypeElement e) throws IOException { public void generate(TypeElement e) throws IOException {
Map<String, HashSet<ExecutableElement>> methodsPerAttribute = methodsPerAttributeMapping(e); Map<String, HashSet<ExecutableElement>> methodsPerAttribute = methodsPerAttributeMapping(e);
@ -131,14 +139,104 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
pw.println("package " + packageName + ";"); pw.println("package " + packageName + ";");
} }
pw.println("public enum " + mapSimpleFieldsClassName + " {"); pw.println("public enum " + mapSimpleFieldsClassName + " implements " + FQN_ENTITY_FIELD + "<" + className + "> {");
methodsPerAttribute.keySet().stream() methodsPerAttribute.keySet().stream()
.sorted(NameFirstComparator.ID_INSTANCE) .sorted(NameFirstComparator.ID_INSTANCE)
.map(GenerateEntityImplementationsProcessor::toEnumConstant) .forEach(key -> {
.forEach(key -> pw.println(" " + key + ",")); pw.println(" " + toEnumConstant(key) + " {");
printEntityFieldMethods(pw, className, key, methodsPerAttribute.get(key));
pw.println(" },");
});
pw.println("}"); pw.println("}");
} }
} }
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(" @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(" }");
FieldAccessorType.getMethod(FieldAccessorType.COLLECTION_ADD, methods, fieldName, types, fieldType).ifPresent(method -> {
TypeMirror firstParameterType = method.getParameters().get(0).asType();
pw.println(" @SuppressWarnings(\"unchecked\") @Override public Class<?> getCollectionElementClass() {");
pw.println(" return " + types.erasure(firstParameterType) + ".class;");
pw.println(" }");
});
FieldAccessorType.getMethod(FieldAccessorType.MAP_ADD, methods, fieldName, types, fieldType).ifPresent(method -> {
TypeMirror firstParameterType = method.getParameters().get(0).asType();
TypeMirror secondParameterType = method.getParameters().get(1).asType();
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 (" + method.getReturnType() + ") e." + method.getSimpleName() + "((" + firstParameterType + ") p0);");
}
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 { private class ImplGenerator implements Generator {
@ -151,7 +249,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
if (parentTypeElement == null) { if (parentTypeElement == null) {
return; return;
} }
final List<? extends Element> allMembers = elements.getAllMembers(parentTypeElement); final List<? extends Element> allParentMembers = elements.getAllMembers(parentTypeElement);
String className = e.getQualifiedName().toString(); String className = e.getQualifiedName().toString();
String packageName = null; String packageName = null;
int lastDot = className.lastIndexOf('.'); int lastDot = className.lastIndexOf('.');
@ -162,8 +260,8 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
String simpleClassName = className.substring(lastDot + 1); String simpleClassName = className.substring(lastDot + 1);
String mapImplClassName = className + "Impl"; String mapImplClassName = className + "Impl";
String mapSimpleClassName = simpleClassName + "Impl"; String mapSimpleClassName = simpleClassName + "Impl";
boolean hasId = methodsPerAttribute.containsKey("Id") || allMembers.stream().anyMatch(el -> "getId".equals(el.getSimpleName().toString())); boolean hasId = methodsPerAttribute.containsKey("Id") || allParentMembers.stream().anyMatch(el -> "getId".equals(el.getSimpleName().toString()));
boolean hasDeepClone = allMembers.stream().filter(el -> el.getKind() == ElementKind.METHOD).anyMatch(el -> "deepClone".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) boolean needsDeepClone = fieldGetters(methodsPerAttribute)
.map(ExecutableElement::getReturnType) .map(ExecutableElement::getReturnType)
.anyMatch(fieldType -> ! isKnownCollectionOfImmutableFinalTypes(fieldType) && ! isImmutableFinalType(fieldType)); .anyMatch(fieldType -> ! isKnownCollectionOfImmutableFinalTypes(fieldType) && ! isImmutableFinalType(fieldType));
@ -180,7 +278,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
pw.println("public class " + mapSimpleClassName + (an.inherits().isEmpty() ? "" : " extends " + an.inherits()) + " implements " + className + " {"); pw.println("public class " + mapSimpleClassName + (an.inherits().isEmpty() ? "" : " extends " + an.inherits()) + " implements " + className + " {");
// Constructors // Constructors
allMembers.stream() allParentMembers.stream()
.filter(ExecutableElement.class::isInstance) .filter(ExecutableElement.class::isInstance)
.map(ExecutableElement.class::cast) .map(ExecutableElement.class::cast)
.filter((ExecutableElement ee) -> ee.getKind() == ElementKind.CONSTRUCTOR) .filter((ExecutableElement ee) -> ee.getKind() == ElementKind.CONSTRUCTOR)
@ -261,12 +359,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
for (ExecutableElement method : methods) { for (ExecutableElement method : methods) {
FieldAccessorType fat = FieldAccessorType.determineType(method, me.getKey(), types, fieldType); FieldAccessorType fat = FieldAccessorType.determineType(method, me.getKey(), types, fieldType);
Optional<ExecutableElement> parentMethod = allMembers.stream() Optional<ExecutableElement> parentMethod = Util.findParentMethodImplementation(allParentMembers, method);
.filter(ExecutableElement.class::isInstance)
.map(ExecutableElement.class::cast)
.filter(ee -> Objects.equals(ee.toString(), method.toString()))
.filter((ExecutableElement ee) -> ! ee.getModifiers().contains(Modifier.ABSTRACT))
.findAny();
if (parentMethod.isPresent()) { if (parentMethod.isPresent()) {
processingEnv.getMessager().printMessage(Kind.OTHER, "Method " + method + " is declared in a parent class.", method); processingEnv.getMessager().printMessage(Kind.OTHER, "Method " + method + " is declared in a parent class.", method);
@ -275,6 +368,44 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
} }
} }
}); });
// 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);");
pw.println("}"); pw.println("}");
} }
} }
@ -343,6 +474,122 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
} }
} }
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 + ";");
}
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(" }");
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);
if (ee.getReturnType().getKind() == TypeKind.BOOLEAN && "isUpdated".equals(ee.getSimpleName().toString())) {
pw.println(" return entityFieldDelegate.isUpdated();");
pw.println(" }");
} else if (ee.getReturnType().getKind() == TypeKind.VOID && "clearUpdatedFlag".equals(ee.getSimpleName().toString())) {
pw.println(" return entityFieldDelegate.clearUpdatedFlag();");
pw.println(" }");
} else {
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 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 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 { private class DelegateGenerator implements Generator {
@Override @Override
public void generate(TypeElement e) throws IOException { public void generate(TypeElement e) throws IOException {
@ -364,7 +611,6 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
if (parentTypeElement == null) { if (parentTypeElement == null) {
return; return;
} }
final List<? extends Element> allMembers = elements.getAllMembers(e);
JavaFileObject file = processingEnv.getFiler().createSourceFile(mapClassName); JavaFileObject file = processingEnv.getFiler().createSourceFile(mapClassName);
IdentityHashMap<ExecutableElement, String> m2field = new IdentityHashMap<>(); IdentityHashMap<ExecutableElement, String> m2field = new IdentityHashMap<>();
@ -380,11 +626,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
pw.println(" this.delegateProvider = delegateProvider;"); pw.println(" this.delegateProvider = delegateProvider;");
pw.println(" }"); pw.println(" }");
allMembers.stream() getAllAbstractMethods(e)
.filter(m -> m.getKind() == ElementKind.METHOD)
.filter(ExecutableElement.class::isInstance)
.map(ExecutableElement.class::cast)
.filter(ee -> ee.getModifiers().contains(Modifier.ABSTRACT))
.forEach(ee -> { .forEach(ee -> {
pw.println(" @Override " pw.println(" @Override "
+ ee.getModifiers().stream().filter(m -> m != Modifier.ABSTRACT).map(Object::toString).collect(Collectors.joining(" ")) + ee.getModifiers().stream().filter(m -> m != Modifier.ABSTRACT).map(Object::toString).collect(Collectors.joining(" "))
@ -396,11 +638,15 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
if (ee.getReturnType().getKind() == TypeKind.BOOLEAN && "isUpdated".equals(ee.getSimpleName().toString())) { if (ee.getReturnType().getKind() == TypeKind.BOOLEAN && "isUpdated".equals(ee.getSimpleName().toString())) {
pw.println(" return delegateProvider.isUpdated();"); pw.println(" return delegateProvider.isUpdated();");
} else if (ee.getReturnType().getKind() == TypeKind.VOID) { // write operation } else if (ee.getReturnType().getKind() == TypeKind.VOID) { // write operation
pw.println(" delegateProvider.getDelegate(false, " + field + ")." + ee.getSimpleName() + "(" 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(", ")) + ee.getParameters().stream().map(VariableElement::getSimpleName).collect(Collectors.joining(", "))
+ ");"); + ");");
} else { } else {
pw.println(" return delegateProvider.getDelegate(true, " + field + ")." + ee.getSimpleName() + "(" 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(", ")) + ee.getParameters().stream().map(VariableElement::getSimpleName).collect(Collectors.joining(", "))
+ ");"); + ");");
} }
@ -408,6 +654,8 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
}); });
pw.println("}"); pw.println("}");
autogenerated.add(" DELEGATE_CREATORS.put(" + className + ".class, (DelegateCreator<" + className + ">) " + mapClassName + "::new);");
} }
} }
} }
@ -457,7 +705,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
pw.println(" return target;"); pw.println(" return target;");
pw.println(" }"); pw.println(" }");
cloners.add(" CLONERS_WITH_ID.put(" + className + ".class, (Cloner<" + className + ">) " + clonerImplClassName + "::deepClone);"); autogenerated.add(" CLONERS_WITH_ID.put(" + className + ".class, (Cloner<" + className + ">) " + clonerImplClassName + "::deepClone);");
if (methodsPerAttribute.containsKey("Id")) { if (methodsPerAttribute.containsKey("Id")) {
pw.println(" public static " + className + " deepCloneNoId(" + className + " original, " + className + " target) {"); pw.println(" public static " + className + " deepCloneNoId(" + className + " original, " + className + " target) {");
@ -476,7 +724,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
pw.println(" return target;"); pw.println(" return target;");
pw.println(" }"); pw.println(" }");
cloners.add(" CLONERS_WITHOUT_ID.put(" + className + ".class, (Cloner<" + className + ">) " + clonerImplClassName + "::deepCloneNoId);"); autogenerated.add(" CLONERS_WITHOUT_ID.put(" + className + ".class, (Cloner<" + className + ">) " + clonerImplClassName + "::deepCloneNoId);");
} }
pw.println("}"); pw.println("}");
} }

View file

@ -24,10 +24,14 @@ import java.util.LinkedHashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.lang.model.element.Element; import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name; import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement; import javax.lang.model.element.VariableElement;
@ -86,4 +90,13 @@ public class Util {
return true; return true;
} }
protected static Optional<ExecutableElement> findParentMethodImplementation(List<? extends Element> allParentMembers, ExecutableElement method) {
return allParentMembers.stream()
.filter(ExecutableElement.class::isInstance)
.map(ExecutableElement.class::cast)
.filter(ee -> Objects.equals(ee.toString(), method.toString()))
.filter((ExecutableElement ee) -> ! ee.getModifiers().contains(Modifier.ABSTRACT))
.findAny();
}
} }

View file

@ -16,6 +16,8 @@
*/ */
package org.keycloak.models.map.common; package org.keycloak.models.map.common;
import org.keycloak.models.map.common.delegate.DelegateProvider;
import org.keycloak.models.map.common.delegate.EntityFieldDelegate;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@ -79,6 +81,30 @@ public class DeepCloner {
V clone(V original, V target); V clone(V original, V target);
} }
/**
* Function that instantiates a delegation object of type {@code V} with the given delegate provider
* @param <V> Object class
*/
@FunctionalInterface
public interface DelegateCreator<V> {
/**
* Function that instantiates a delegation object of type {@code V} with the given delegate provider.
*/
V create(DelegateProvider<V> delegateProvider);
}
/**
* Function that instantiates a delegation object of type {@code V} with the given per-field delegate provider
* @param <V> Object class
*/
@FunctionalInterface
public interface EntityFieldDelegateCreator<V> {
/**
* Function that instantiates a delegation object of type {@code V} with the given per-field delegate provider.
*/
V create(EntityFieldDelegate<V> entityDelegateProvider);
}
public static final DeepCloner DUMB_CLONER = new Builder().build(); public static final DeepCloner DUMB_CLONER = new Builder().build();
/** /**
@ -87,8 +113,10 @@ public class DeepCloner {
public static class Builder { public static class Builder {
private final Map<Class<?>, Supplier<?>> parameterlessConstructors = new HashMap<>(); private final Map<Class<?>, Supplier<?>> parameterlessConstructors = new HashMap<>();
private final Map<Class<?>, Function<DeepCloner, ?>> constructors = new HashMap<>(); private final Map<Class<?>, Function<DeepCloner, ?>> constructors = new HashMap<>();
private final Map<Class<?>, Cloner> clonersWithId = new HashMap<>(org.keycloak.models.map.common.AutogeneratedCloners.CLONERS_WITH_ID); private final Map<Class<?>, Cloner<?>> clonersWithId = new HashMap<>(org.keycloak.models.map.common.AutogeneratedCloners.CLONERS_WITH_ID);
private final Map<Class<?>, Cloner> clonersWithoutId = new HashMap<>(org.keycloak.models.map.common.AutogeneratedCloners.CLONERS_WITHOUT_ID); private final Map<Class<?>, Cloner<?>> clonersWithoutId = new HashMap<>(org.keycloak.models.map.common.AutogeneratedCloners.CLONERS_WITHOUT_ID);
private final Map<Class<?>, DelegateCreator<?>> delegateCreators = new HashMap<>(org.keycloak.models.map.common.AutogeneratedCloners.DELEGATE_CREATORS);
private final Map<Class<?>, EntityFieldDelegateCreator<?>> entityFieldDelegateCreators = new HashMap<>(org.keycloak.models.map.common.AutogeneratedCloners.ENTITY_FIELD_DELEGATE_CREATORS);
private Cloner<?> genericCloner = (from, to) -> { throw new IllegalStateException("Cloner not found for class " + (from == null ? "<null>" : from.getClass())); }; private Cloner<?> genericCloner = (from, to) -> { throw new IllegalStateException("Cloner not found for class " + (from == null ? "<null>" : from.getClass())); };
/** /**
@ -96,7 +124,7 @@ public class DeepCloner {
* @return * @return
*/ */
public DeepCloner build() { public DeepCloner build() {
return new DeepCloner(parameterlessConstructors, constructors, clonersWithId, clonersWithoutId, genericCloner); return new DeepCloner(parameterlessConstructors, constructors, delegateCreators, entityFieldDelegateCreators, clonersWithId, clonersWithoutId, genericCloner);
} }
private <V> void forThisClassAndAllMarkedParentsAndInterfaces(Class<V> rootClazz, Consumer<Class<?>> action) { private <V> void forThisClassAndAllMarkedParentsAndInterfaces(Class<V> rootClazz, Consumer<Class<?>> action) {
@ -153,6 +181,38 @@ public class DeepCloner {
return this; return this;
} }
/**
* Adds a method that instantiates an per-field delegate of type {@code V}.
*
* @param <V> Class or interface that would be instantiated by the given methods
* @param clazz Class or interface that would be instantiated by the given methods
* @param constructor Function that creates a new instance of class {@code V}.
* If {@code null}, such a single-parameter constructor is not available.
* @return This builder.
*/
public <V> Builder delegateCreator(Class<V> clazz, EntityFieldDelegateCreator<V> delegateCreator) {
if (delegateCreator != null) {
forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.entityFieldDelegateCreators.put(cl, delegateCreator));
}
return this;
}
/**
* Adds a method, often a constructor, that instantiates a delegate of type {@code V}.
*
* @param <V> Class or interface that would be instantiated by the given methods
* @param clazz Class or interface that would be instantiated by the given methods
* @param constructor Function that creates a new instance of class {@code V}.
* If {@code null}, such a single-parameter constructor is not available.
* @return This builder.
*/
public <V> Builder delegateCreator(Class<V> clazz, DelegateCreator<V> delegateCreator) {
if (delegateCreator != null) {
forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.delegateCreators.put(cl, delegateCreator));
}
return this;
}
/** /**
* Adds a method that copies (as in a deep copy) an object properties from one object to another * Adds a method that copies (as in a deep copy) an object properties from one object to another
* *
@ -163,7 +223,7 @@ public class DeepCloner {
* function usually returns {@code to} * function usually returns {@code to}
* @return This builder. * @return This builder.
*/ */
public <V> Builder cloner(Class<? extends V> clazz, Cloner cloner) { public <V> Builder cloner(Class<? extends V> clazz, Cloner<?> cloner) {
if (cloner != null) { if (cloner != null) {
forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.clonersWithId.put(cl, cloner)); forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.clonersWithId.put(cl, cloner));
} }
@ -180,7 +240,7 @@ public class DeepCloner {
* function usually returns {@code to} * function usually returns {@code to}
* @return This builder. * @return This builder.
*/ */
public <V> Builder cloner(Class<? extends V> clazz, Cloner clonerWithId, Cloner clonerWithoutId) { public <V> Builder cloner(Class<? extends V> clazz, Cloner<?> clonerWithId, Cloner<?> clonerWithoutId) {
if (clonerWithId != null) { if (clonerWithId != null) {
forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.clonersWithId.put(cl, clonerWithId)); forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.clonersWithId.put(cl, clonerWithId));
} }
@ -209,20 +269,27 @@ public class DeepCloner {
private final Map<Class<?>, Supplier<?>> parameterlessConstructors; private final Map<Class<?>, Supplier<?>> parameterlessConstructors;
private final Map<Class<?>, Function<DeepCloner, ?>> constructors; private final Map<Class<?>, Function<DeepCloner, ?>> constructors;
private final Map<Class<?>, Cloner> clonersWithId; private final Map<Class<?>, Cloner<?>> clonersWithId;
private final Map<Class<?>, Cloner> clonersWithoutId; private final Map<Class<?>, Cloner<?>> clonersWithoutId;
private final Map<Class<?>, DelegateCreator<?>> delegateCreators;
private final Map<Class<?>, EntityFieldDelegateCreator<?>> entityFieldDelegateCreators;
private final Cloner<?> genericCloner; private final Cloner<?> genericCloner;
private final Map<Class<?>, Object> emptyInstances = new HashMap<>(AutogeneratedCloners.EMPTY_INSTANCES);
private DeepCloner(Map<Class<?>, Supplier<?>> parameterlessConstructors, private DeepCloner(Map<Class<?>, Supplier<?>> parameterlessConstructors,
Map<Class<?>, Function<DeepCloner, ?>> constructors, Map<Class<?>, Function<DeepCloner, ?>> constructors,
Map<Class<?>, Cloner> clonersWithId, Map<Class<?>, DelegateCreator<?>> delegateCreators,
Map<Class<?>, Cloner> clonersWithoutId, Map<Class<?>, EntityFieldDelegateCreator<?>> entityFieldDelegateCreators,
Map<Class<?>, Cloner<?>> clonersWithId,
Map<Class<?>, Cloner<?>> clonersWithoutId,
Cloner<?> genericCloner) { Cloner<?> genericCloner) {
this.parameterlessConstructors = parameterlessConstructors; this.parameterlessConstructors = parameterlessConstructors;
this.constructors = constructors; this.constructors = constructors;
this.clonersWithId = clonersWithId; this.clonersWithId = clonersWithId;
this.clonersWithoutId = clonersWithoutId; this.clonersWithoutId = clonersWithoutId;
this.delegateCreators = delegateCreators;
this.genericCloner = genericCloner; this.genericCloner = genericCloner;
this.entityFieldDelegateCreators = entityFieldDelegateCreators;
} }
private <V> V getFromClassRespectingHierarchy(Map<Class<?>, V> map, Class<?> clazz) { private <V> V getFromClassRespectingHierarchy(Map<Class<?>, V> map, Class<?> clazz) {
@ -257,6 +324,43 @@ public class DeepCloner {
return null; return null;
} }
@SuppressWarnings("unchecked")
public <D, V extends D> D delegate(V delegate, DelegateProvider<D> delegateProvider) {
return delegate((Class<V>) delegate.getClass(), delegateProvider);
}
public <D, V extends D> D delegate(Class<V> delegateClass, DelegateProvider<D> delegateProvider) {
@SuppressWarnings("unchecked")
DelegateCreator<D> delegateCreator = (DelegateCreator<D>) getFromClassRespectingHierarchy(delegateCreators, delegateClass);
if (delegateCreator != null) {
return delegateCreator.create(delegateProvider);
}
throw new IllegalStateException("Cannot create delegate for " + delegateClass);
}
@SuppressWarnings("unchecked")
public <V> V entityFieldDelegate(V delegate, EntityFieldDelegate<V> delegateProvider) {
return entityFieldDelegate((Class<V>) delegate.getClass(), delegateProvider);
}
public <V> V entityFieldDelegate(Class<V> delegateClass, EntityFieldDelegate<V> delegateProvider) {
@SuppressWarnings("unchecked")
EntityFieldDelegateCreator<V> delegateCreator = (EntityFieldDelegateCreator<V>) getFromClassRespectingHierarchy(entityFieldDelegateCreators, delegateClass);
if (delegateCreator != null) {
return delegateCreator.create(delegateProvider);
}
throw new IllegalStateException("Cannot create delegate for " + delegateClass);
}
public <V> V emptyInstance(Class<V> instanceClass) {
@SuppressWarnings("unchecked")
V emptyInstance = (V) getFromClassRespectingHierarchy(emptyInstances, instanceClass);
if (emptyInstance != null) {
return emptyInstance;
}
throw new IllegalStateException("Cannot create empty instance for " + instanceClass);
}
/** /**
* Creates a new instance of the given class or interface if the parameterless constructor for that type is known. * Creates a new instance of the given class or interface if the parameterless constructor for that type is known.
* @param <V> Type (class or a {@code @Root} interface) to create a new instance * @param <V> Type (class or a {@code @Root} interface) to create a new instance
@ -301,6 +405,7 @@ public class DeepCloner {
* @param clazz Type (class or a {@code @Root} interface) to create a new instance * @param clazz Type (class or a {@code @Root} interface) to create a new instance
* @return See description * @return See description
*/ */
@SuppressWarnings("unchecked")
public <V> Class<? extends V> newInstanceType(Class<V> valueType) { public <V> Class<? extends V> newInstanceType(Class<V> valueType) {
if (valueType == null) { if (valueType == null) {
return null; return null;
@ -338,7 +443,7 @@ public class DeepCloner {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <V> V deepClone(V from, V to, Map<Class<?>, Cloner> cloners) { private <V> V deepClone(V from, V to, Map<Class<?>, Cloner<?>> cloners) {
Cloner<V> cloner = (Cloner<V>) getFromClassRespectingHierarchy(cloners, from.getClass()); Cloner<V> cloner = (Cloner<V>) getFromClassRespectingHierarchy(cloners, from.getClass());
if (cloner != null) { if (cloner != null) {
return cloner.clone(from, to); return cloner.clone(from, to);
@ -383,6 +488,7 @@ public class DeepCloner {
* @param from Original instance * @param from Original instance
* @return Newly created instance or {@code null} if {@code from} is {@code null}. * @return Newly created instance or {@code null} if {@code from} is {@code null}.
*/ */
@SuppressWarnings("unchecked")
public <V> V from(V from) { public <V> V from(V from) {
return from == null ? null : deepClone(from, newInstance((Class<V>) from.getClass())); return from == null ? null : deepClone(from, newInstance((Class<V>) from.getClass()));
} }

View file

@ -0,0 +1,127 @@
package org.keycloak.models.map.common;
import java.util.Collection;
import java.util.Map;
/**
* Represents a field in an entity with appropriate accessors.
*
* @author hmlnarik
* @param <E>
*/
public interface EntityField<E> {
/**
* Returns name of this field with no spaces where each word starts with a capital letter.
* @return
*/
String getName();
/**
* Returns name of this field in lowercase with words separated by a dash ({@code -}).
* @return
*/
String getNameDashed();
/**
* Returns the value of this field.
*
* @param e Entity
* @return Value of the field
*/
Object get(E e);
/**
* Sets the value of this field. Does nothing by default. If you want to have a field set, override this method.
* @param <T>
* @param e Entity
* @param value Value of the field
*/
default <T> void set(E e, T value) {};
/**
* Adds an element to the collection stored in this field.
* @param e Entity
* @param value Value to be added to the collection
* @throws ClassCastException If this field is not a collection.
*/
default <T> void collectionAdd(E e, T value) {
@SuppressWarnings("unchecked")
Collection<T> c = (Collection<T>) get(e);
if (c != null) {
c.add(value);
}
}
/**
* Removes an element from the collection stored in this field.
* @param e Entity
* @param value Value to be added to the collection
* @return Defined by the underlying field. Preferrably it should return deleted object, but it can return
* {@code true / false} indication of removal, or just {@code null}.
* @throws ClassCastException If this field is not a collection.
*/
default <T> Object collectionRemove(E e, T value) {
Collection<?> c = (Collection<?>) get(e);
return c == null ? null : c.remove(value);
}
/**
* Retrieves a value from the map stored in this field.
* @param e Entity
* @param key Requested key
* @return Object mapped to this key
* @throws ClassCastException If this field is not a map.
*/
default <K> Object mapGet(E e, K key) {
@SuppressWarnings("unchecked")
Map<K, ?> m = (Map<K, ?>) get(e);
return m == null ? null : m.get(key);
}
/**
* Adds a mapping to the map stored in this field.
* @param e Entity
* @param key Key to map
* @param value Mapped value
* @throws ClassCastException If this field is not a map.
*/
default <K, T> void mapPut(E e, K key, T value) {
@SuppressWarnings("unchecked")
Map<K, T> m = (Map<K, T>) get(e);
if (m != null) {
m.put(key, value);
}
}
/**
* Removes a mapping from the map stored in this field.
* @param e Entity
* @param key Key to remove
* @return Object mapped to this key
* @throws ClassCastException If this field is not a map.
*/
default <K> Object mapRemove(E e, K key) {
@SuppressWarnings("unchecked")
Map<K, ?> m = (Map<K, ?>) get(e);
if (m != null) {
return m.remove(key);
}
return null;
}
/**
* @return Returns the most specific type of this field.
*/
default Class<?> getFieldClass() { return Object.class; }
/**
* @return If this field is a collection, returns type of its elements; otherwise returns {@code Void} class.
*/
default Class<?> getCollectionElementClass() { return Void.class; }
/**
* @return If this field is a map, returns type of its keys; otherwise returns {@code Void} class.
*/
default Class<?> getMapKeyClass() { return Void.class; }
/**
* @return If this field is a map, returns type of its values; otherwise returns {@code Void} class.
*/
default Class<?> getMapValueClass() { return Void.class; }
}

View file

@ -17,7 +17,8 @@
package org.keycloak.models.map.common.delegate; package org.keycloak.models.map.common.delegate;
/** /**
* * Interface for a provider of a delegate of type {@code T}, optionally
* providing the flag on the object been updated.
* @author hmlnarik * @author hmlnarik
*/ */
public interface DelegateProvider<T> { public interface DelegateProvider<T> {
@ -27,7 +28,7 @@ public interface DelegateProvider<T> {
* @param field Identification of the field this delegates operates on. * @param field Identification of the field this delegates operates on.
* @return * @return
*/ */
T getDelegate(boolean isRead, Object field); T getDelegate(boolean isRead, Object field, Object... parameters);
default boolean isUpdated() { return false; } default boolean isUpdated() { return false; }
} }

View file

@ -0,0 +1,56 @@
package org.keycloak.models.map.common.delegate;
import java.util.Collection;
import java.util.Map;
import org.keycloak.models.map.common.EntityField;
import org.keycloak.models.map.common.UpdatableEntity;
public interface EntityFieldDelegate<E> extends UpdatableEntity {
// Non-collection values
Object get(EntityField<E> field);
default <T> void set(EntityField<E> field, T value) {}
default <T> void collectionAdd(EntityField<E> field, T value) {
@SuppressWarnings("unchecked")
Collection<T> c = (Collection<T>) get(field);
if (c != null) {
c.add(value);
}
}
default <T> Object collectionRemove(EntityField<E> field, T value) {
Collection<?> c = (Collection<?>) get(field);
return c == null ? null : c.remove(value);
}
/**
*
* @param <K> Key type
* @param <T> Value type
* @param field Field identifier. Should be one of the generated {@code *Fields} enum constants.
* @param key Key
* @param valueClass class of the value
* @return
*/
default <K> Object mapGet(EntityField<E> field, K key) {
@SuppressWarnings("unchecked")
Map<K, ?> m = (Map<K, ?>) get(field);
return m == null ? null : m.get(key);
}
default <K, T> void mapPut(EntityField<E> field, K key, T value) {
@SuppressWarnings("unchecked")
Map<K, T> m = (Map<K, T>) get(field);
if (m != null) {
m.put(key, value);
}
}
default <K> Object mapRemove(EntityField<E> field, K key) {
@SuppressWarnings("unchecked")
Map<K, ?> m = (Map<K, ?>) get(field);
if (m != null) {
return m.remove(key);
}
return null;
}
}

View file

@ -0,0 +1,5 @@
package org.keycloak.models.map.common.delegate;
public interface HasEntityFieldDelegate<E> {
EntityFieldDelegate<E> getEntityFieldDelegate();
}

View file

@ -35,7 +35,7 @@ public class LazyDelegateProvider<T extends UpdatableEntity> implements Delegate
} }
@Override @Override
public T getDelegate(boolean isRead, Object field) { public T getDelegate(boolean isRead, Object field, Object... parameters) {
if (! isDelegateInitialized()) { if (! isDelegateInitialized()) {
delegate.compareAndSet(null, delegateSupplier == null ? null : delegateSupplier.get(), false, true); delegate.compareAndSet(null, delegateSupplier == null ? null : delegateSupplier.get(), false, true);
} }

View file

@ -31,7 +31,7 @@ public class SimpleDelegateProvider<T extends UpdatableEntity> implements Delega
} }
@Override @Override
public T getDelegate(boolean isRead, Object field) { public T getDelegate(boolean isRead, Object field, Object... parameters) {
return this.delegate; return this.delegate;
} }

View file

@ -0,0 +1,100 @@
/*
* 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.storage.tree;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.MapStorage;
import org.keycloak.models.map.storage.QueryParameters;
import java.util.stream.Stream;
/**
*
* @author hmlnarik
*/
public class EmptyMapStorage<V extends AbstractEntity, M> implements MapStorage<V, M> {
private static final EmptyMapStorage<?, ?> INSTANCE = new EmptyMapStorage<>();
@SuppressWarnings("unchecked")
public static <V extends AbstractEntity, M> EmptyMapStorage<V, M> getInstance() {
return (EmptyMapStorage<V, M>) INSTANCE;
}
@Override
public MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session) {
return new MapKeycloakTransaction<V, M>() {
@Override
public V create(V value) {
return null;
}
@Override
public V read(String key) {
return null;
}
@Override
public Stream<V> read(QueryParameters<M> queryParameters) {
return Stream.empty();
}
@Override
public long getCount(QueryParameters<M> queryParameters) {
return 0;
}
@Override
public boolean delete(String key) {
return false;
}
@Override
public long delete(QueryParameters<M> queryParameters) {
return 0;
}
@Override
public void begin() {
}
@Override
public void commit() {
}
@Override
public void rollback() {
}
@Override
public void setRollbackOnly() {
}
@Override
public boolean getRollbackOnly() {
return false;
}
@Override
public boolean isActive() {
return true;
}
};
}
}

View file

@ -29,6 +29,7 @@ import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance; import static org.hamcrest.Matchers.sameInstance;
import static org.keycloak.models.map.common.DeepCloner.DUMB_CLONER;
/** /**
* *
@ -112,4 +113,40 @@ public class MapClientEntityClonerTest {
assertThat(clonedInstance.getAuthenticationFlowBindingOverrides(), nullValue()); assertThat(clonedInstance.getAuthenticationFlowBindingOverrides(), nullValue());
assertThat(clonedInstance.getRegistrationToken(), nullValue()); assertThat(clonedInstance.getRegistrationToken(), nullValue());
} }
@Test
public void testCloneToExistingInstanceDumb() {
MapClientEntity newInstance = new MapClientEntityImpl();
newInstance.setId("my-id");
newInstance.setClientId("a-client-id");
newInstance.setAttribute("attr", Arrays.asList("aa", "bb", "cc"));
MapProtocolMapperEntity pmm = new MapProtocolMapperEntityImpl();
pmm.setId("pmm-id");
pmm.setConfig(new HashMap<>());
pmm.getConfig().put("key1", "value1");
pmm.getConfig().put("key2", "value2");
newInstance.setProtocolMapper("pmm-id", pmm);
newInstance.setAttribute("attr", Arrays.asList("aa", "bb", "cc"));
MapClientEntity clonedInstance = CLONER.newInstance(MapClientEntity.class);
assertThat(CLONER.deepCloneNoId(newInstance, clonedInstance), sameInstance(clonedInstance));
assertThat(clonedInstance, instanceOf(MapClientEntityImpl.class));
clonedInstance.setId("my-id2");
assertThat(clonedInstance.getId(), is("my-id2"));
assertThat(clonedInstance.getClientId(), is("a-client-id"));
assertThat(clonedInstance.getAttributes(), not(sameInstance(newInstance.getAttributes())));
assertThat(clonedInstance.getAttributes().keySet(), containsInAnyOrder("attr"));
assertThat(clonedInstance.getAttributes().get("attr"), contains("aa", "bb", "cc"));
assertThat(clonedInstance.getAttributes().get("attr"), not(sameInstance(newInstance.getAttributes().get("attr"))));
assertThat(clonedInstance.getProtocolMappers(), not(sameInstance(newInstance.getProtocolMappers())));
assertThat(clonedInstance.getProtocolMapper("pmm-id"), not(sameInstance(newInstance.getProtocolMapper("pmm-id"))));
assertThat(clonedInstance.getProtocolMapper("pmm-id"), equalTo(newInstance.getProtocolMapper("pmm-id")));
assertThat(clonedInstance.getProtocolMapper("pmm-id").getConfig(), not(sameInstance(newInstance.getProtocolMapper("pmm-id").getConfig())));
assertThat(clonedInstance.getProtocolMapper("pmm-id").getConfig(), equalTo(newInstance.getProtocolMapper("pmm-id").getConfig()));
assertThat(clonedInstance.getAuthenticationFlowBindingOverrides(), nullValue());
assertThat(clonedInstance.getRegistrationToken(), nullValue());
}
} }

View file

@ -0,0 +1,157 @@
/*
* 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.testsuite.model.storage.tree.sample;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.keycloak.models.map.client.MapClientEntity;
import org.keycloak.models.map.client.MapClientEntityFieldDelegate;
import org.keycloak.models.map.client.MapClientEntityFields;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.EntityField;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.map.common.delegate.EntityFieldDelegate;
import org.keycloak.models.map.common.delegate.HasEntityFieldDelegate;
/**
*
* @author hmlnarik
*/
public class Dict<E> extends UpdatableEntity.Impl implements EntityFieldDelegate<E> {
public static final String CLIENT_FIELD_LOGO = "LOGO";
public static final String CLIENT_FIELD_ENABLED = "ENABLED";
public static final String CLIENT_FIELD_NAME = "NAME";
private static final Set<String> CLIENT_ALLOWED_KEYS = new HashSet<>(Arrays.asList(CLIENT_FIELD_NAME, CLIENT_FIELD_ENABLED, CLIENT_FIELD_LOGO));
public static MapClientEntity clientDelegate() {
// To be replaced by dynamic mapper config
Map<String, String> fieldName2key = new HashMap<>();
fieldName2key.put(MapClientEntityFields.ID.getName(), CLIENT_FIELD_NAME);
fieldName2key.put(MapClientEntityFields.CLIENT_ID.getName(), CLIENT_FIELD_NAME);
fieldName2key.put(MapClientEntityFields.ENABLED.getName(), CLIENT_FIELD_ENABLED);
Map<String, String> attributeName2key = new HashMap<>();
attributeName2key.put("logo", CLIENT_FIELD_LOGO);
Dict<MapClientEntity> dict = new Dict<>(CLIENT_ALLOWED_KEYS, fieldName2key, attributeName2key);
return DeepCloner.DUMB_CLONER.entityFieldDelegate(MapClientEntity.class, dict);
}
@SuppressWarnings("unchecked")
public static <E> Dict<E> asDict(E entity) {
return (entity instanceof HasEntityFieldDelegate && ((HasEntityFieldDelegate<?>) entity).getEntityFieldDelegate() instanceof Dict)
? (Dict<E>) ((HasEntityFieldDelegate<E>) entity).getEntityFieldDelegate()
: null;
}
private final Set<String> allowedKeys;
private final Map<String, Object> contents = new HashMap<>();
private final Map<String, String> fieldName2key;
private final Map<String, String> attributeName2key;
public Dict(Set<String> allowedKeys, Map<String, String> fieldName2key, Map<String, String> attributeName2key) {
this.allowedKeys = allowedKeys;
this.fieldName2key = fieldName2key;
this.attributeName2key = attributeName2key;
}
@Override
public Object get(EntityField<E> field) {
if ("Attributes".equals(field.getName())) {
return attributeName2key.entrySet().stream()
.filter(me -> get(me.getValue()) != null)
.collect(Collectors.toMap(me -> me.getKey(), me -> Collections.singletonList(get(me.getValue()))));
}
String key = fieldName2key.get(field.getName());
if (key != null) {
return get(key);
}
return null;
}
@Override
public <T> void set(EntityField<E> field, T value) {
String key = fieldName2key.get(field.getName());
if (key != null) {
put(key, value);
}
}
@Override
public <K> Object mapGet(EntityField<E> field, K key) {
if ("Attributes".equals(field.getName()) && attributeName2key.containsKey(key)) {
Object v = get(attributeName2key.get(key));
return v == null ? null : Collections.singletonList(get(attributeName2key.get(key)));
}
return null;
}
@Override
public <K, T> void mapPut(EntityField<E> field, K key, T value) {
if ("Attributes".equals(field.getName()) && attributeName2key.containsKey(key) && (value instanceof List)) {
List<?> l = (List<?>) value;
if (l.isEmpty()) {
remove(attributeName2key.get(key));
} else {
put(attributeName2key.get(key), l.get(0));
}
}
}
@Override
public <K> Object mapRemove(EntityField<E> field, K key) {
if ("Attributes".equals(field.getName()) && attributeName2key.containsKey(key)) {
Object o = remove(attributeName2key.get(key));
return o == null ? null : Collections.singletonList(o);
}
return null;
}
protected boolean isKeyAllowed(String key) {
return allowedKeys.contains(key);
}
public Object get(String key) {
return isKeyAllowed(key) ? contents.get(key) : null;
}
public void put(String key, Object value) {
if (isKeyAllowed(key)) {
updated |= ! Objects.equals(contents.put(key, value), value);
}
}
public Object remove(String key) {
key = key == null ? null : key.toUpperCase();
if (isKeyAllowed(key)) {
Object res = contents.remove(key);
updated |= res != null;
return res;
}
return null;
}
}

View file

@ -0,0 +1,120 @@
/*
* 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.testsuite.model.storage.tree.sample;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.MapStorage;
import org.keycloak.models.map.storage.QueryParameters;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
/**
*
* @author hmlnarik
*/
public class DictStorage<V extends AbstractEntity, M> implements MapStorage<V, M> {
private final DeepCloner cloner;
private final List<V> store;
public DictStorage(DeepCloner cloner, List<V> store) {
this.cloner = cloner;
this.store = store;
}
List<V> getStore() {
return store;
}
private final class Transaction implements MapKeycloakTransaction<V, M> {
@Override
public V create(V value) {
V res = cloner.from(value);
store.add(res);
return res;
}
@Override
public V read(String key) {
return store.stream()
.filter(e -> Objects.equals(e.getId(), key))
.findFirst()
.orElse(null);
}
@Override
public Stream<V> read(QueryParameters<M> queryParameters) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public long getCount(QueryParameters<M> queryParameters) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public boolean delete(String key) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public long delete(QueryParameters<M> queryParameters) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public void begin() {
}
@Override
public void commit() {
}
@Override
public void rollback() {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public void setRollbackOnly() {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public boolean getRollbackOnly() {
return false;
}
@Override
public boolean isActive() {
return true;
}
}
@Override
public MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session) {
return new Transaction();
}
}

View file

@ -0,0 +1,60 @@
package org.keycloak.testsuite.model.storage.tree.sample;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import java.util.Arrays;
import org.junit.Test;
import org.keycloak.models.map.client.MapClientEntity;
public class DictTest {
@Test
public void testDictClientFromMap() {
MapClientEntity mce = Dict.clientDelegate();
assertThat(mce.getClientId(), nullValue());
assertThat(mce.isEnabled(), nullValue());
assertThat(mce.getAttribute("logo"), nullValue());
assertThat(mce.getAttributes().keySet(), is(empty()));
Dict.asDict(mce).put(Dict.CLIENT_FIELD_NAME, "name");
Dict.asDict(mce).put(Dict.CLIENT_FIELD_ENABLED, false);
Dict.asDict(mce).put(Dict.CLIENT_FIELD_LOGO, "thisShouldBeBase64Logo");
Dict.asDict(mce).put("nonexistent", "nonexistent");
assertThat(mce.getId(), is("name"));
assertThat(mce.getClientId(), is("name"));
assertThat(mce.isEnabled(), is(false));
assertThat(mce.getAttribute("logo"), hasItems("thisShouldBeBase64Logo"));
assertThat(mce.getAttributes().keySet(), hasItems("logo"));
}
@Test
public void testDictClientFromEntity() {
MapClientEntity mce = Dict.clientDelegate();
assertThat(Dict.asDict(mce).get(Dict.CLIENT_FIELD_NAME), nullValue());
assertThat(Dict.asDict(mce).get(Dict.CLIENT_FIELD_ENABLED), nullValue());
assertThat(Dict.asDict(mce).get(Dict.CLIENT_FIELD_LOGO), nullValue());
mce.setClientId("name");
mce.setEnabled(false);
mce.setAttribute("logo", Arrays.asList("thisShouldBeBase64Logo"));
mce.setAttribute("blah", Arrays.asList("thisShouldBeBase64Logofdas"));
assertThat(mce.getAttributes().keySet(), hasItems("logo"));
assertThat(Dict.asDict(mce).get(Dict.CLIENT_FIELD_NAME), is("name"));
assertThat(Dict.asDict(mce).get(Dict.CLIENT_FIELD_ENABLED), is(false));
assertThat(Dict.asDict(mce).get(Dict.CLIENT_FIELD_LOGO), is("thisShouldBeBase64Logo"));
mce.setAttribute("logo", Arrays.asList("thisShouldBeAnotherBase64Logo"));
assertThat(Dict.asDict(mce).get(Dict.CLIENT_FIELD_LOGO), is("thisShouldBeAnotherBase64Logo"));
mce.removeAttribute("logo");
assertThat(Dict.asDict(mce).get(Dict.CLIENT_FIELD_LOGO), nullValue());
}
}