diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/processor/AbstractGenerateEntityImplementationsProcessor.java b/model/build-processor/src/main/java/org/keycloak/models/map/processor/AbstractGenerateEntityImplementationsProcessor.java index 3e5fa013ee..b2c3563055 100644 --- a/model/build-processor/src/main/java/org/keycloak/models/map/processor/AbstractGenerateEntityImplementationsProcessor.java +++ b/model/build-processor/src/main/java/org/keycloak/models/map/processor/AbstractGenerateEntityImplementationsProcessor.java @@ -45,13 +45,20 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; 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.Util.getGenericsDeclaration; import static org.keycloak.models.map.processor.Util.isMapType; +@SupportedSourceVersion(SourceVersion.RELEASE_8) public abstract class AbstractGenerateEntityImplementationsProcessor extends AbstractProcessor { 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 Types types; @@ -100,16 +107,19 @@ public abstract class AbstractGenerateEntityImplementationsProcessor extends Abs // ); } + protected Stream getAllAbstractMethods(TypeElement e) { + return elements.getAllMembers(e).stream() + .filter(el -> el.getKind() == ElementKind.METHOD) + .filter(el -> el.getModifiers().contains(Modifier.ABSTRACT)) + .filter(ExecutableElement.class::isInstance) + .map(ExecutableElement.class::cast); + } + protected Map> methodsPerAttributeMapping(TypeElement e) { - final List allMembers = elements.getAllMembers(e); - Map> methodsPerAttribute = allMembers.stream() - .filter(el -> el.getKind() == ElementKind.METHOD) - .filter(el -> el.getModifiers().contains(Modifier.ABSTRACT)) - .filter(Util::isNotIgnored) - .filter(ExecutableElement.class::isInstance) - .map(ExecutableElement.class::cast) - .filter(ee -> ! (ee.getReceiverType() instanceof NoType)) - .collect(Collectors.toMap(this::determineAttributeFromMethodName, v -> new HashSet(Arrays.asList(v)), (a, b) -> { a.addAll(b); return a; })); + Map> methodsPerAttribute = getAllAbstractMethods(e) + .filter(Util::isNotIgnored) + .filter(ee -> ! (ee.getReceiverType() instanceof NoType)) + .collect(Collectors.toMap(this::determineAttributeFromMethodName, v -> new HashSet<>(Arrays.asList(v)), (a,b) -> { a.addAll(b); return a; })); // Merge plurals with singulars methodsPerAttribute.keySet().stream() @@ -129,7 +139,7 @@ public abstract class AbstractGenerateEntityImplementationsProcessor extends Abs FORBIDDEN_PREFIXES.put("delete", "remove"); } - private String determineAttributeFromMethodName(ExecutableElement e) { + protected String determineAttributeFromMethodName(ExecutableElement e) { Name name = e.getSimpleName(); Matcher m = BEAN_NAME.matcher(name.toString()); if (m.matches()) { diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateEntityImplementationsProcessor.java b/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateEntityImplementationsProcessor.java index 4742d23b24..3c82a773e5 100644 --- a/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateEntityImplementationsProcessor.java +++ b/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateEntityImplementationsProcessor.java @@ -17,12 +17,12 @@ 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.Objects; import java.util.stream.Collectors; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedSourceVersion; @@ -47,6 +47,7 @@ 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; @@ -58,30 +59,36 @@ import javax.lang.model.type.TypeKind; @SupportedSourceVersion(SourceVersion.RELEASE_8) public class GenerateEntityImplementationsProcessor extends AbstractGenerateEntityImplementationsProcessor { - private Collection cloners = new TreeSet<>(); + private final Collection autogenerated = new TreeSet<>(); private final Generator[] generators = new Generator[] { new ClonerGenerator(), new DelegateGenerator(), new FieldsGenerator(), - new ImplGenerator() + new FieldDelegateGenerator(), + new ImplGenerator(), }; @Override protected void afterAnnotationProcessing() { - if (! cloners.isEmpty()) { + if (! autogenerated.isEmpty()) { try { JavaFileObject file = processingEnv.getFiler().createSourceFile("org.keycloak.models.map.common.AutogeneratedCloners"); try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) { 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("public final class AutogeneratedCloners {"); pw.println(" public static final java.util.Map, Cloner> CLONERS_WITH_ID = new java.util.HashMap<>();"); pw.println(" public static final java.util.Map, Cloner> CLONERS_WITHOUT_ID = new java.util.HashMap<>();"); + pw.println(" public static final java.util.Map, DelegateCreator> DELEGATE_CREATORS = new java.util.HashMap<>();"); + pw.println(" public static final java.util.Map, EntityFieldDelegateCreator> ENTITY_FIELD_DELEGATE_CREATORS = new java.util.HashMap<>();"); + pw.println(" public static final java.util.Map, Object> EMPTY_INSTANCES = new java.util.HashMap<>();"); pw.println(" static {"); - cloners.forEach(pw::println); + autogenerated.forEach(pw::println); pw.println(" }"); pw.println("}"); } @@ -111,6 +118,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti } private class FieldsGenerator implements Generator { + @Override public void generate(TypeElement e) throws IOException { Map> methodsPerAttribute = methodsPerAttributeMapping(e); @@ -131,14 +139,104 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti pw.println("package " + packageName + ";"); } - pw.println("public enum " + mapSimpleFieldsClassName + " {"); + pw.println("public enum " + mapSimpleFieldsClassName + " implements " + FQN_ENTITY_FIELD + "<" + className + "> {"); methodsPerAttribute.keySet().stream() .sorted(NameFirstComparator.ID_INSTANCE) - .map(GenerateEntityImplementationsProcessor::toEnumConstant) - .forEach(key -> pw.println(" " + key + ",")); + .forEach(key -> { + pw.println(" " + toEnumConstant(key) + " {"); + printEntityFieldMethods(pw, className, key, methodsPerAttribute.get(key)); + pw.println(" },"); + }); pw.println("}"); } } + + private void printEntityFieldMethods(PrintWriter pw, String className, String fieldName, HashSet 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 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 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 " + returnType + " mapRemove(" + className + " e, K p0) {"); + } else { + pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + 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 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 " + method.getReturnType() + " mapGet(" + className + " e, K key) {"); + pw.println(" return (" + method.getReturnType() + ") e." + method.getSimpleName() + "((" + firstParameterType + ") key);"); + pw.println(" }"); + } + } } private class ImplGenerator implements Generator { @@ -151,7 +249,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti if (parentTypeElement == null) { return; } - final List allMembers = elements.getAllMembers(parentTypeElement); + final List allParentMembers = elements.getAllMembers(parentTypeElement); String className = e.getQualifiedName().toString(); String packageName = null; int lastDot = className.lastIndexOf('.'); @@ -162,8 +260,8 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti String simpleClassName = className.substring(lastDot + 1); String mapImplClassName = className + "Impl"; String mapSimpleClassName = simpleClassName + "Impl"; - boolean hasId = methodsPerAttribute.containsKey("Id") || allMembers.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 hasId = methodsPerAttribute.containsKey("Id") || 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)); @@ -180,7 +278,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti pw.println("public class " + mapSimpleClassName + (an.inherits().isEmpty() ? "" : " extends " + an.inherits()) + " implements " + className + " {"); // Constructors - allMembers.stream() + allParentMembers.stream() .filter(ExecutableElement.class::isInstance) .map(ExecutableElement.class::cast) .filter((ExecutableElement ee) -> ee.getKind() == ElementKind.CONSTRUCTOR) @@ -222,7 +320,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti + ";"); pw.println(" }"); pw.println(" @Override public int hashCode() {"); - pw.println(" return " + pw.println(" return " + (hasId ? "(getId() == null ? super.hashCode() : getId().hashCode())" : "Objects.hash(" @@ -261,12 +359,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti for (ExecutableElement method : methods) { FieldAccessorType fat = FieldAccessorType.determineType(method, me.getKey(), types, fieldType); - Optional parentMethod = allMembers.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(); + Optional parentMethod = Util.findParentMethodImplementation(allParentMembers, method); if (parentMethod.isPresent()) { 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("}"); } } @@ -343,6 +474,122 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti } } + private class FieldDelegateGenerator implements Generator { + + @Override + public void generate(TypeElement e) throws IOException { + Map> 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 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 { @Override public void generate(TypeElement e) throws IOException { @@ -364,7 +611,6 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti if (parentTypeElement == null) { return; } - final List allMembers = elements.getAllMembers(e); JavaFileObject file = processingEnv.getFiler().createSourceFile(mapClassName); IdentityHashMap m2field = new IdentityHashMap<>(); @@ -380,11 +626,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti pw.println(" this.delegateProvider = delegateProvider;"); pw.println(" }"); - allMembers.stream() - .filter(m -> m.getKind() == ElementKind.METHOD) - .filter(ExecutableElement.class::isInstance) - .map(ExecutableElement.class::cast) - .filter(ee -> ee.getModifiers().contains(Modifier.ABSTRACT)) + getAllAbstractMethods(e) .forEach(ee -> { pw.println(" @Override " + 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())) { pw.println(" return delegateProvider.isUpdated();"); } 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(", ")) + ");"); } 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(", ")) + ");"); } @@ -408,6 +654,8 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti }); 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(" }"); - 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")) { 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(" }"); - 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("}"); } diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/processor/Util.java b/model/build-processor/src/main/java/org/keycloak/models/map/processor/Util.java index b0c4ecdc17..d7090c091f 100644 --- a/model/build-processor/src/main/java/org/keycloak/models/map/processor/Util.java +++ b/model/build-processor/src/main/java/org/keycloak/models/map/processor/Util.java @@ -24,10 +24,14 @@ import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; 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.TypeElement; import javax.lang.model.element.VariableElement; @@ -86,4 +90,13 @@ public class Util { return true; } + protected static Optional findParentMethodImplementation(List 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(); + } + } diff --git a/model/map/src/main/java/org/keycloak/models/map/common/DeepCloner.java b/model/map/src/main/java/org/keycloak/models/map/common/DeepCloner.java index 3261b76234..fd6bcf3745 100644 --- a/model/map/src/main/java/org/keycloak/models/map/common/DeepCloner.java +++ b/model/map/src/main/java/org/keycloak/models/map/common/DeepCloner.java @@ -1,13 +1,13 @@ /* * 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. @@ -16,6 +16,8 @@ */ 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.Retention; import java.lang.annotation.RetentionPolicy; @@ -79,6 +81,30 @@ public class DeepCloner { V clone(V original, V target); } + /** + * Function that instantiates a delegation object of type {@code V} with the given delegate provider + * @param Object class + */ + @FunctionalInterface + public interface DelegateCreator { + /** + * Function that instantiates a delegation object of type {@code V} with the given delegate provider. + */ + V create(DelegateProvider delegateProvider); + } + + /** + * Function that instantiates a delegation object of type {@code V} with the given per-field delegate provider + * @param Object class + */ + @FunctionalInterface + public interface EntityFieldDelegateCreator { + /** + * Function that instantiates a delegation object of type {@code V} with the given per-field delegate provider. + */ + V create(EntityFieldDelegate entityDelegateProvider); + } + public static final DeepCloner DUMB_CLONER = new Builder().build(); /** @@ -87,8 +113,10 @@ public class DeepCloner { public static class Builder { private final Map, Supplier> parameterlessConstructors = new HashMap<>(); private final Map, Function> constructors = new HashMap<>(); - private final Map, Cloner> clonersWithId = new HashMap<>(org.keycloak.models.map.common.AutogeneratedCloners.CLONERS_WITH_ID); - private final Map, Cloner> clonersWithoutId = new HashMap<>(org.keycloak.models.map.common.AutogeneratedCloners.CLONERS_WITHOUT_ID); + private final Map, Cloner> clonersWithId = new HashMap<>(org.keycloak.models.map.common.AutogeneratedCloners.CLONERS_WITH_ID); + private final Map, Cloner> clonersWithoutId = new HashMap<>(org.keycloak.models.map.common.AutogeneratedCloners.CLONERS_WITHOUT_ID); + private final Map, DelegateCreator> delegateCreators = new HashMap<>(org.keycloak.models.map.common.AutogeneratedCloners.DELEGATE_CREATORS); + private final Map, 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 ? "" : from.getClass())); }; /** @@ -96,7 +124,7 @@ public class DeepCloner { * @return */ public DeepCloner build() { - return new DeepCloner(parameterlessConstructors, constructors, clonersWithId, clonersWithoutId, genericCloner); + return new DeepCloner(parameterlessConstructors, constructors, delegateCreators, entityFieldDelegateCreators, clonersWithId, clonersWithoutId, genericCloner); } private void forThisClassAndAllMarkedParentsAndInterfaces(Class rootClazz, Consumer> action) { @@ -123,7 +151,7 @@ public class DeepCloner { /** * Adds a method, often a constructor, that instantiates a record of type {@code V}. - * + * * @param 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 constructorNoParameters Parameterless function that creates a new instance of class {@code V}. @@ -153,6 +181,38 @@ public class DeepCloner { return this; } + /** + * Adds a method that instantiates an per-field delegate of type {@code V}. + * + * @param 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 Builder delegateCreator(Class clazz, EntityFieldDelegateCreator 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 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 Builder delegateCreator(Class clazz, DelegateCreator 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 * @@ -163,7 +223,7 @@ public class DeepCloner { * function usually returns {@code to} * @return This builder. */ - public Builder cloner(Class clazz, Cloner cloner) { + public Builder cloner(Class clazz, Cloner cloner) { if (cloner != null) { forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.clonersWithId.put(cl, cloner)); } @@ -180,7 +240,7 @@ public class DeepCloner { * function usually returns {@code to} * @return This builder. */ - public Builder cloner(Class clazz, Cloner clonerWithId, Cloner clonerWithoutId) { + public Builder cloner(Class clazz, Cloner clonerWithId, Cloner clonerWithoutId) { if (clonerWithId != null) { forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.clonersWithId.put(cl, clonerWithId)); } @@ -193,7 +253,7 @@ public class DeepCloner { /** * Adds a method that copies (as in a deep copy) an object properties to another object for any class * that is not covered by a specific cloner set via {@link #cloner(Class, BiFunction)} method. - * + * * @param Class or interface whose instance would be copied over to another instance by the given cloner * @param genericCloner A method for cloning which copies properties of an object onto another object. This * function usually returns {@code to} @@ -209,20 +269,27 @@ public class DeepCloner { private final Map, Supplier> parameterlessConstructors; private final Map, Function> constructors; - private final Map, Cloner> clonersWithId; - private final Map, Cloner> clonersWithoutId; + private final Map, Cloner> clonersWithId; + private final Map, Cloner> clonersWithoutId; + private final Map, DelegateCreator> delegateCreators; + private final Map, EntityFieldDelegateCreator> entityFieldDelegateCreators; private final Cloner genericCloner; + private final Map, Object> emptyInstances = new HashMap<>(AutogeneratedCloners.EMPTY_INSTANCES); private DeepCloner(Map, Supplier> parameterlessConstructors, Map, Function> constructors, - Map, Cloner> clonersWithId, - Map, Cloner> clonersWithoutId, + Map, DelegateCreator> delegateCreators, + Map, EntityFieldDelegateCreator> entityFieldDelegateCreators, + Map, Cloner> clonersWithId, + Map, Cloner> clonersWithoutId, Cloner genericCloner) { this.parameterlessConstructors = parameterlessConstructors; this.constructors = constructors; this.clonersWithId = clonersWithId; this.clonersWithoutId = clonersWithoutId; + this.delegateCreators = delegateCreators; this.genericCloner = genericCloner; + this.entityFieldDelegateCreators = entityFieldDelegateCreators; } private V getFromClassRespectingHierarchy(Map, V> map, Class clazz) { @@ -257,6 +324,43 @@ public class DeepCloner { return null; } + @SuppressWarnings("unchecked") + public D delegate(V delegate, DelegateProvider delegateProvider) { + return delegate((Class) delegate.getClass(), delegateProvider); + } + + public D delegate(Class delegateClass, DelegateProvider delegateProvider) { + @SuppressWarnings("unchecked") + DelegateCreator delegateCreator = (DelegateCreator) getFromClassRespectingHierarchy(delegateCreators, delegateClass); + if (delegateCreator != null) { + return delegateCreator.create(delegateProvider); + } + throw new IllegalStateException("Cannot create delegate for " + delegateClass); + } + + @SuppressWarnings("unchecked") + public V entityFieldDelegate(V delegate, EntityFieldDelegate delegateProvider) { + return entityFieldDelegate((Class) delegate.getClass(), delegateProvider); + } + + public V entityFieldDelegate(Class delegateClass, EntityFieldDelegate delegateProvider) { + @SuppressWarnings("unchecked") + EntityFieldDelegateCreator delegateCreator = (EntityFieldDelegateCreator) getFromClassRespectingHierarchy(entityFieldDelegateCreators, delegateClass); + if (delegateCreator != null) { + return delegateCreator.create(delegateProvider); + } + throw new IllegalStateException("Cannot create delegate for " + delegateClass); + } + + public V emptyInstance(Class 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. * @param 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 * @return See description */ + @SuppressWarnings("unchecked") public Class newInstanceType(Class valueType) { if (valueType == null) { return null; @@ -338,7 +443,7 @@ public class DeepCloner { } @SuppressWarnings("unchecked") - private V deepClone(V from, V to, Map, Cloner> cloners) { + private V deepClone(V from, V to, Map, Cloner> cloners) { Cloner cloner = (Cloner) getFromClassRespectingHierarchy(cloners, from.getClass()); if (cloner != null) { return cloner.clone(from, to); @@ -383,6 +488,7 @@ public class DeepCloner { * @param from Original instance * @return Newly created instance or {@code null} if {@code from} is {@code null}. */ + @SuppressWarnings("unchecked") public V from(V from) { return from == null ? null : deepClone(from, newInstance((Class) from.getClass())); } diff --git a/model/map/src/main/java/org/keycloak/models/map/common/EntityField.java b/model/map/src/main/java/org/keycloak/models/map/common/EntityField.java new file mode 100644 index 0000000000..b7b87a7ded --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/common/EntityField.java @@ -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 + */ +public interface EntityField { + + /** + * 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 + * @param e Entity + * @param value Value of the field + */ + default 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 void collectionAdd(E e, T value) { + @SuppressWarnings("unchecked") + Collection c = (Collection) 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 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 Object mapGet(E e, K key) { + @SuppressWarnings("unchecked") + Map m = (Map) 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 void mapPut(E e, K key, T value) { + @SuppressWarnings("unchecked") + Map m = (Map) 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 Object mapRemove(E e, K key) { + @SuppressWarnings("unchecked") + Map m = (Map) 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; } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/common/delegate/DelegateProvider.java b/model/map/src/main/java/org/keycloak/models/map/common/delegate/DelegateProvider.java index 6d33dc4205..b487c416af 100644 --- a/model/map/src/main/java/org/keycloak/models/map/common/delegate/DelegateProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/common/delegate/DelegateProvider.java @@ -17,7 +17,8 @@ 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 */ public interface DelegateProvider { @@ -27,7 +28,7 @@ public interface DelegateProvider { * @param field Identification of the field this delegates operates on. * @return */ - T getDelegate(boolean isRead, Object field); + T getDelegate(boolean isRead, Object field, Object... parameters); default boolean isUpdated() { return false; } } diff --git a/model/map/src/main/java/org/keycloak/models/map/common/delegate/EntityFieldDelegate.java b/model/map/src/main/java/org/keycloak/models/map/common/delegate/EntityFieldDelegate.java new file mode 100644 index 0000000000..386a4a749e --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/common/delegate/EntityFieldDelegate.java @@ -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 extends UpdatableEntity { + // Non-collection values + Object get(EntityField field); + default void set(EntityField field, T value) {} + + default void collectionAdd(EntityField field, T value) { + @SuppressWarnings("unchecked") + Collection c = (Collection) get(field); + if (c != null) { + c.add(value); + } + } + default Object collectionRemove(EntityField field, T value) { + Collection c = (Collection) get(field); + return c == null ? null : c.remove(value); + } + + /** + * + * @param Key type + * @param 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 Object mapGet(EntityField field, K key) { + @SuppressWarnings("unchecked") + Map m = (Map) get(field); + return m == null ? null : m.get(key); + } + default void mapPut(EntityField field, K key, T value) { + @SuppressWarnings("unchecked") + Map m = (Map) get(field); + if (m != null) { + m.put(key, value); + } + } + default Object mapRemove(EntityField field, K key) { + @SuppressWarnings("unchecked") + Map m = (Map) get(field); + if (m != null) { + return m.remove(key); + } + return null; + } + +} diff --git a/model/map/src/main/java/org/keycloak/models/map/common/delegate/HasEntityFieldDelegate.java b/model/map/src/main/java/org/keycloak/models/map/common/delegate/HasEntityFieldDelegate.java new file mode 100644 index 0000000000..1b2aadf562 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/common/delegate/HasEntityFieldDelegate.java @@ -0,0 +1,5 @@ +package org.keycloak.models.map.common.delegate; + +public interface HasEntityFieldDelegate { + EntityFieldDelegate getEntityFieldDelegate(); +} diff --git a/model/map/src/main/java/org/keycloak/models/map/common/delegate/LazyDelegateProvider.java b/model/map/src/main/java/org/keycloak/models/map/common/delegate/LazyDelegateProvider.java index ab72f6e6af..5c06025762 100644 --- a/model/map/src/main/java/org/keycloak/models/map/common/delegate/LazyDelegateProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/common/delegate/LazyDelegateProvider.java @@ -35,7 +35,7 @@ public class LazyDelegateProvider implements Delegate } @Override - public T getDelegate(boolean isRead, Object field) { + public T getDelegate(boolean isRead, Object field, Object... parameters) { if (! isDelegateInitialized()) { delegate.compareAndSet(null, delegateSupplier == null ? null : delegateSupplier.get(), false, true); } diff --git a/model/map/src/main/java/org/keycloak/models/map/common/delegate/SimpleDelegateProvider.java b/model/map/src/main/java/org/keycloak/models/map/common/delegate/SimpleDelegateProvider.java index d3218c95a8..f717b3f62f 100644 --- a/model/map/src/main/java/org/keycloak/models/map/common/delegate/SimpleDelegateProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/common/delegate/SimpleDelegateProvider.java @@ -31,7 +31,7 @@ public class SimpleDelegateProvider implements Delega } @Override - public T getDelegate(boolean isRead, Object field) { + public T getDelegate(boolean isRead, Object field, Object... parameters) { return this.delegate; } diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/tree/EmptyMapStorage.java b/model/map/src/main/java/org/keycloak/models/map/storage/tree/EmptyMapStorage.java new file mode 100644 index 0000000000..5fe1a268bb --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/storage/tree/EmptyMapStorage.java @@ -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 implements MapStorage { + + private static final EmptyMapStorage INSTANCE = new EmptyMapStorage<>(); + + @SuppressWarnings("unchecked") + public static EmptyMapStorage getInstance() { + return (EmptyMapStorage) INSTANCE; + } + + @Override + public MapKeycloakTransaction createTransaction(KeycloakSession session) { + return new MapKeycloakTransaction() { + @Override + public V create(V value) { + return null; + } + + @Override + public V read(String key) { + return null; + } + + @Override + public Stream read(QueryParameters queryParameters) { + return Stream.empty(); + } + + @Override + public long getCount(QueryParameters queryParameters) { + return 0; + } + + @Override + public boolean delete(String key) { + return false; + } + + @Override + public long delete(QueryParameters 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; + } + }; + } + +} diff --git a/model/map/src/test/java/org/keycloak/models/map/client/MapClientEntityClonerTest.java b/model/map/src/test/java/org/keycloak/models/map/client/MapClientEntityClonerTest.java index aed292b0fa..166053231d 100644 --- a/model/map/src/test/java/org/keycloak/models/map/client/MapClientEntityClonerTest.java +++ b/model/map/src/test/java/org/keycloak/models/map/client/MapClientEntityClonerTest.java @@ -29,6 +29,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; 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.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()); + } } diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/storage/tree/sample/Dict.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/storage/tree/sample/Dict.java new file mode 100644 index 0000000000..55464a3d1c --- /dev/null +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/storage/tree/sample/Dict.java @@ -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 extends UpdatableEntity.Impl implements EntityFieldDelegate { + + 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 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 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 attributeName2key = new HashMap<>(); + attributeName2key.put("logo", CLIENT_FIELD_LOGO); + + Dict dict = new Dict<>(CLIENT_ALLOWED_KEYS, fieldName2key, attributeName2key); + return DeepCloner.DUMB_CLONER.entityFieldDelegate(MapClientEntity.class, dict); + } + + @SuppressWarnings("unchecked") + public static Dict asDict(E entity) { + return (entity instanceof HasEntityFieldDelegate && ((HasEntityFieldDelegate) entity).getEntityFieldDelegate() instanceof Dict) + ? (Dict) ((HasEntityFieldDelegate) entity).getEntityFieldDelegate() + : null; + } + + private final Set allowedKeys; + private final Map contents = new HashMap<>(); + private final Map fieldName2key; + private final Map attributeName2key; + + public Dict(Set allowedKeys, Map fieldName2key, Map attributeName2key) { + this.allowedKeys = allowedKeys; + this.fieldName2key = fieldName2key; + this.attributeName2key = attributeName2key; + } + + @Override + public Object get(EntityField 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 void set(EntityField field, T value) { + String key = fieldName2key.get(field.getName()); + if (key != null) { + put(key, value); + } + } + + @Override + public Object mapGet(EntityField 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 void mapPut(EntityField 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 Object mapRemove(EntityField 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; + } +} diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/storage/tree/sample/DictStorage.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/storage/tree/sample/DictStorage.java new file mode 100644 index 0000000000..e29e7ccd20 --- /dev/null +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/storage/tree/sample/DictStorage.java @@ -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 implements MapStorage { + + private final DeepCloner cloner; + + private final List store; + + public DictStorage(DeepCloner cloner, List store) { + this.cloner = cloner; + this.store = store; + } + + List getStore() { + return store; + } + + private final class Transaction implements MapKeycloakTransaction { + + @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 read(QueryParameters queryParameters) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public long getCount(QueryParameters 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 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 createTransaction(KeycloakSession session) { + return new Transaction(); + } + +} diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/storage/tree/sample/DictTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/storage/tree/sample/DictTest.java new file mode 100644 index 0000000000..a24fa3749f --- /dev/null +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/storage/tree/sample/DictTest.java @@ -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()); + } +}