diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/annotations/GeneratedFieldType.java b/model/build-processor/src/main/java/org/keycloak/models/map/annotations/GeneratedFieldType.java new file mode 100644 index 0000000000..f320ed7424 --- /dev/null +++ b/model/build-processor/src/main/java/org/keycloak/models/map/annotations/GeneratedFieldType.java @@ -0,0 +1,43 @@ +/* + * 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.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specifies the default implementation with a no-args constructor for + * a container property (e.g. a {@code} List} or a {@code Map}). + *

+ * Applicable to a setter of a single key from the map (e.g. {@code setAttribute}) or an adder to + * a collection (e.g. {@code addWebOrigin}). This is used to override default type generated by the + * generator in case the entry does not exist yet and a new container needs to be instantiated. + * + * Example: + *

+ *  @GeneratedFieldType(HashSet) void addWebOrigin() { ... }
+ * 
+ * + * @author hmlnarik + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.METHOD) +public @interface GeneratedFieldType { + Class value() default Void.class; +} diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/annotations/GenerateEnumMapFieldType.java b/model/build-processor/src/main/java/org/keycloak/models/map/annotations/IgnoreForEntityImplementationGenerator.java similarity index 89% rename from model/build-processor/src/main/java/org/keycloak/models/map/annotations/GenerateEnumMapFieldType.java rename to model/build-processor/src/main/java/org/keycloak/models/map/annotations/IgnoreForEntityImplementationGenerator.java index 80a3f9beaf..d8fd5cc7ea 100644 --- a/model/build-processor/src/main/java/org/keycloak/models/map/annotations/GenerateEnumMapFieldType.java +++ b/model/build-processor/src/main/java/org/keycloak/models/map/annotations/IgnoreForEntityImplementationGenerator.java @@ -26,7 +26,6 @@ import java.lang.annotation.Target; * @author hmlnarik */ @Retention(RetentionPolicy.SOURCE) -@Target(ElementType.METHOD) -public @interface GenerateEnumMapFieldType { - Class value() default Void.class; +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface IgnoreForEntityImplementationGenerator { } 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 0720760bb1..610267bf4c 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 @@ -50,9 +50,14 @@ import static org.keycloak.models.map.processor.FieldAccessorType.*; import static org.keycloak.models.map.processor.Util.getGenericsDeclaration; import static org.keycloak.models.map.processor.Util.isSetType; import static org.keycloak.models.map.processor.Util.methodParameters; +import java.util.Collection; import java.util.Comparator; import java.util.IdentityHashMap; import java.util.Optional; +import java.util.TreeSet; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; @@ -70,8 +75,10 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { private Elements elements; private Types types; + private Collection cloners = new TreeSet<>(); private final Generator[] generators = new Generator[] { + new ClonerGenerator(), new DelegateGenerator(), new FieldsGenerator(), new ImplGenerator() @@ -89,8 +96,30 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { .forEach(this::processTypeElement); } + if (! cloners.isEmpty() && ! annotations.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("// 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(" static {"); + cloners.forEach(pw::println); + pw.println(" }"); + pw.println("}"); + } + } catch (IOException ex) { + Logger.getLogger(GenerateEntityImplementationsProcessor.class.getName()).log(Level.SEVERE, null, ex); + } + } + return true; } + private static final String FQN_DEEP_CLONER = "org.keycloak.models.map.common.DeepCloner"; private void processTypeElement(TypeElement e) { if (e.getKind() != ElementKind.INTERFACE) { @@ -99,7 +128,11 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { } // Find all properties - Map> methodsPerAttribute = e.getEnclosedElements().stream() + 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)) @@ -189,6 +222,15 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { } } + private String deepClone(TypeMirror fieldType, String parameterName) { + if (isKnownCollectionOfImmutableFinalTypes(fieldType)) { + TypeElement typeElement = elements.getTypeElement(types.erasure(fieldType).toString()); + return parameterName + " == null ? null : " + interfaceToImplementation(typeElement, parameterName); + } else { + return "deepClone(" + parameterName + ")"; + } + } + private boolean isPrimitiveType(TypeMirror fieldType) { try { types.getPrimitiveType(fieldType.getKind()); @@ -223,6 +265,26 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { } } + private static class NameFirstComparator implements Comparator { + private static final Comparator ID_INSTANCE = new NameFirstComparator("id").thenComparing(Comparator.naturalOrder()); + private static final Comparator GET_ID_INSTANCE = new NameFirstComparator("getId").thenComparing(Comparator.naturalOrder()); + private final String name; + public NameFirstComparator(String name) { + this.name = name; + } + @Override + public int compare(String o1, String o2) { + return Objects.equals(o1, o2) + ? 0 + : name.equalsIgnoreCase(o1) + ? -1 + : name.equalsIgnoreCase(o2) + ? 1 + : 0; + } + + } + private class FieldsGenerator implements Generator { @Override public void generate(TypeElement e, Map> methodsPerAttribute) throws IOException { @@ -245,7 +307,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { pw.println("public enum " + mapSimpleFieldsClassName + " {"); methodsPerAttribute.keySet().stream() - .sorted() + .sorted(NameFirstComparator.ID_INSTANCE) .map(GenerateEntityImplementationsProcessor::toEnumConstant) .forEach(key -> pw.println(" " + key + ",")); pw.println("}"); @@ -274,6 +336,10 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { 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 needsDeepClone = fieldGetters(methodsPerAttribute) + .map(ExecutableElement::getReturnType) + .anyMatch(fieldType -> ! isKnownCollectionOfImmutableFinalTypes(fieldType) && ! isImmutableFinalType(fieldType)); JavaFileObject file = processingEnv.getFiler().createSourceFile(mapImplClassName); try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) { @@ -282,23 +348,48 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { } pw.println("import java.util.Objects;"); + pw.println("import " + FQN_DEEP_CLONER + ";"); pw.println("// DO NOT CHANGE THIS CLASS, IT IS GENERATED AUTOMATICALLY BY " + GenerateEntityImplementationsProcessor.class.getSimpleName()); pw.println("public class " + mapSimpleClassName + (an.inherits().isEmpty() ? "" : " extends " + an.inherits()) + " implements " + className + " {"); -// pw.println(" private final EnumMap values = new EnumMap<>(Field.class);"); -// pw.println(" protected Object get(Field field) { return values.get(field); }"); -// pw.println(" protected Object set(Field field, Object p0) { return values.put(field, p0); }"); + + // Constructors + allMembers.stream() + .filter(ExecutableElement.class::isInstance) + .map(ExecutableElement.class::cast) + .filter((ExecutableElement ee) -> ee.getKind() == ElementKind.CONSTRUCTOR) + .forEach((ExecutableElement ee) -> { + if (hasDeepClone || ! needsDeepClone) { + pw.println(" " + + ee.getModifiers().stream().map(Object::toString).collect(Collectors.joining(" ")) + + " " + mapSimpleClassName + "(" + methodParameters(ee.getParameters()) + ") { super(" + ee.getParameters() + "); }" + ); + } else if (needsDeepClone) { + pw.println(" /**"); + pw.println(" * @deprecated This constructor uses a {@link DeepCloner#DUMB_CLONER} that does not clone anything. Use {@link #" + mapSimpleClassName + "(DeepCloner)} variant instead"); + pw.println(" */"); + pw.println(" " + + ee.getModifiers().stream().map(Object::toString).collect(Collectors.joining(" ")) + + " " + + mapSimpleClassName + "(" + methodParameters(ee.getParameters()) + ") { this(DeepCloner.DUMB_CLONER" + (ee.getParameters().isEmpty() ? "" : ", ") + ee.getParameters() + "); }" + ); + pw.println(" " + + ee.getModifiers().stream().map(Object::toString).collect(Collectors.joining(" ")) + + " " + + mapSimpleClassName + "(DeepCloner cloner" + (ee.getParameters().isEmpty() ? "" : ", ") + methodParameters(ee.getParameters()) + ") { super(" + ee.getParameters() + "); this.cloner = cloner; }" + ); + } + }); + + // equals, hashCode, toString pw.println(" @Override public boolean equals(Object o) {"); pw.println(" if (o == this) return true; "); pw.println(" if (! (o instanceof " + mapSimpleClassName + ")) return false; "); pw.println(" " + mapSimpleClassName + " other = (" + mapSimpleClassName + ") o; "); pw.println(" return " - + methodsPerAttribute.entrySet().stream() - .map(me -> FieldAccessorType.getMethod(GETTER, me.getValue(), me.getKey(), types, determineFieldType(me.getKey(), me.getValue()))) - .filter(Optional::isPresent) - .map(Optional::get) + + fieldGetters(methodsPerAttribute) .map(ExecutableElement::getSimpleName) .map(Name::toString) - .sorted() + .sorted(NameFirstComparator.GET_ID_INSTANCE) .map(v -> "Objects.equals(" + v + "(), other." + v + "())") .collect(Collectors.joining("\n && ")) + ";"); @@ -308,14 +399,11 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { + (hasId ? "(getId() == null ? super.hashCode() : getId().hashCode())" : "Objects.hash(" - + methodsPerAttribute.entrySet().stream() // generate hashcode from simple-typed properties (no collections etc.) - .map(me -> FieldAccessorType.getMethod(GETTER, me.getValue(), me.getKey(), types, determineFieldType(me.getKey(), me.getValue()))) - .filter(Optional::isPresent) - .map(Optional::get) + + fieldGetters(methodsPerAttribute) .filter(ee -> isImmutableFinalType(ee.getReturnType())) .map(ExecutableElement::getSimpleName) .map(Name::toString) - .sorted() + .sorted(NameFirstComparator.GET_ID_INSTANCE) .map(v -> v + "()") .collect(Collectors.joining(",\n ")) + ")") @@ -325,16 +413,16 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { pw.println(" return String.format(\"%s@%08x\", " + (hasId ? "getId()" : "\"" + mapSimpleClassName + "\"" ) + ", System.identityHashCode(this));"); pw.println(" }"); - // Constructors - allMembers.stream() - .filter(ExecutableElement.class::isInstance) - .map(ExecutableElement.class::cast) - .filter((ExecutableElement ee) -> ee.getKind() == ElementKind.CONSTRUCTOR) - .forEach((ExecutableElement ee) -> pw.println(" " - + ee.getModifiers().stream().map(Object::toString).collect(Collectors.joining(" ")) - + " " + mapSimpleClassName + "(" + methodParameters(ee.getParameters()) + ") { super(" + ee.getParameters() + "); }")); + // deepClone + if (! hasDeepClone && needsDeepClone) { + pw.println(" private final DeepCloner cloner;"); + pw.println(" public V deepClone(V obj) {"); + pw.println(" return cloner.from(obj);"); + pw.println(" }"); + } - methodsPerAttribute.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)).forEach(me -> { + // fields, getters, setters + methodsPerAttribute.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey, NameFirstComparator.ID_INSTANCE)).forEach(me -> { HashSet methods = me.getValue(); TypeMirror fieldType = determineFieldType(me.getKey(), methods); if (fieldType == null) { @@ -364,6 +452,13 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { } } + private Stream fieldGetters(Map> methodsPerAttribute) { + return methodsPerAttribute.entrySet().stream() + .map(me -> FieldAccessorType.getMethod(GETTER, me.getValue(), me.getKey(), types, determineFieldType(me.getKey(), me.getValue()))) + .filter(Optional::isPresent) + .map(Optional::get); + } + private boolean printMethodBody(PrintWriter pw, FieldAccessorType accessorType, ExecutableElement method, String fieldName, TypeMirror fieldType) { TypeMirror firstParameterType = method.getParameters().isEmpty() ? types.getNullType() @@ -426,15 +521,6 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { return false; } - - private String deepClone(TypeMirror fieldType, String parameterName) { - if (isKnownCollectionOfImmutableFinalTypes(fieldType)) { - TypeElement typeElement = elements.getTypeElement(types.erasure(fieldType).toString()); - return parameterName + " == null ? null : " + interfaceToImplementation(typeElement, parameterName); - } else { - return "deepClone(" + parameterName + ")"; - } - } } private class DelegateGenerator implements Generator { @@ -504,4 +590,102 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { } } } + + private class ClonerGenerator implements Generator { + + @Override + public void generate(TypeElement e, Map> methodsPerAttribute) throws IOException { + String className = e.getQualifiedName().toString(); + String packageName = null; + int lastDot = className.lastIndexOf('.'); + if (lastDot > 0) { + packageName = className.substring(0, lastDot); + } + + String simpleClassName = className.substring(lastDot + 1); + String clonerImplClassName = className + "Cloner"; + String clonerSimpleClassName = simpleClassName + "Cloner"; + + JavaFileObject enumFile = processingEnv.getFiler().createSourceFile(clonerImplClassName); + try (PrintWriter pw = new PrintWriter(enumFile.openWriter()) { + @Override + public void println(String x) { + super.println(x == null ? x : x.replaceAll("java.lang.", "")); + } + }) { + if (packageName != null) { + pw.println("package " + packageName + ";"); + } + pw.println("import " + FQN_DEEP_CLONER + ";"); + pw.println("// DO NOT CHANGE THIS CLASS, IT IS GENERATED AUTOMATICALLY BY " + GenerateEntityImplementationsProcessor.class.getSimpleName()); + pw.println("public class " + clonerSimpleClassName + " {"); + pw.println(" public static " + className + " deepClone(" + className + " original, " + className + " target) {"); + + methodsPerAttribute.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)).forEach(me -> { + final String fieldName = me.getKey(); + HashSet methods = me.getValue(); + TypeMirror fieldType = determineFieldType(fieldName, methods); + if (fieldType == null) { + return; + } + + cloneField(e, fieldName, methods, fieldType, pw); + }); + pw.println(" target.clearUpdatedFlag();"); + pw.println(" return target;"); + pw.println(" }"); + + cloners.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) {"); + + methodsPerAttribute.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)).forEach(me -> { + final String fieldName = me.getKey(); + HashSet methods = me.getValue(); + TypeMirror fieldType = determineFieldType(fieldName, methods); + if (fieldType == null || "Id".equals(fieldName)) { + return; + } + + cloneField(e, fieldName, methods, fieldType, pw); + }); + pw.println(" target.clearUpdatedFlag();"); + pw.println(" return target;"); + pw.println(" }"); + + cloners.add(" CLONERS_WITHOUT_ID.put(" + className + ".class, (Cloner<" + className + ">) " + clonerImplClassName + "::deepCloneNoId);"); + } + pw.println("}"); + } + } + + private void cloneField(TypeElement e, final String fieldName, HashSet methods, TypeMirror fieldType, final PrintWriter pw) { + ExecutableElement getter = FieldAccessorType.getMethod(GETTER, methods, fieldName, types, fieldType).orElse(null); + if (getter == null) { + processingEnv.getMessager().printMessage(Kind.WARNING, "Could not determine getter for " + fieldName + " property"); + return; + } + + Optional setter = FieldAccessorType.getMethod(SETTER, methods, fieldName, types, fieldType); + Optional addToCollection = FieldAccessorType.getMethod(COLLECTION_ADD, methods, fieldName, types, fieldType); + Optional updateMap = FieldAccessorType.getMethod(MAP_ADD, methods, fieldName, types, fieldType); + + if (setter.isPresent()) { + final Name setterName = setter.get().getSimpleName(); + // Setter always deep-clones whatever comes from the original, so we don't clone the value here. + pw.println(" target." + setterName + "(original." + getter.getSimpleName() + "());"); + } else if (addToCollection.isPresent()) { + pw.println(" if (original." + getter.getSimpleName() + "() != null) {"); + pw.println(" original." + getter.getSimpleName() + "().forEach(target::" + addToCollection.get().getSimpleName() + ");"); + pw.println(" }"); + } else if (updateMap.isPresent()) { + pw.println(" if (original." + getter.getSimpleName() + "() != null) {"); + pw.println(" original." + getter.getSimpleName() + "().forEach(target::" + updateMap.get().getSimpleName() + ");"); + pw.println(" }"); + } else { + processingEnv.getMessager().printMessage(Kind.ERROR, "Could not determine way to clone " + fieldName + " property", e); + } + } + } } 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 86996ac138..bc82b92987 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 @@ -16,6 +16,7 @@ */ package org.keycloak.models.map.processor; +import org.keycloak.models.map.annotations.IgnoreForEntityImplementationGenerator; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashSet; @@ -24,6 +25,7 @@ import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; +import javax.lang.model.element.Element; import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; @@ -65,4 +67,15 @@ public class Util { return SET_TYPES.contains(name.toString()); } + public static boolean isNotIgnored(Element el) { + do { + IgnoreForEntityImplementationGenerator a = el.getAnnotation(IgnoreForEntityImplementationGenerator.class); + if (a != null) { + return false; + } + el = el.getEnclosingElement(); + } while (el != null); + return true; + } + } diff --git a/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionAdapter.java b/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionAdapter.java index 81b06a7ad8..147faaee37 100644 --- a/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionAdapter.java +++ b/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionAdapter.java @@ -119,7 +119,7 @@ public class MapRootAuthenticationSessionAdapter extends AbstractRootAuthenticat } public void setUpdated(boolean updated) { - entity.updated |= updated; + entity.signalUpdated(updated); } private String generateTabId() { diff --git a/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionEntity.java b/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionEntity.java index 73ec32aac3..149809a002 100644 --- a/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionEntity.java @@ -26,7 +26,7 @@ import java.util.concurrent.ConcurrentHashMap; /** * @author Martin Kanis */ -public class MapRootAuthenticationSessionEntity implements AbstractEntity, UpdatableEntity { +public class MapRootAuthenticationSessionEntity extends UpdatableEntity.Impl implements AbstractEntity { private String id; private String realmId; @@ -34,11 +34,10 @@ public class MapRootAuthenticationSessionEntity implements AbstractEntity, Updat /** * Flag signalizing that any of the setters has been meaningfully used. */ - protected boolean updated; private int timestamp; private Map authenticationSessions = new ConcurrentHashMap<>(); - protected MapRootAuthenticationSessionEntity() {} + public MapRootAuthenticationSessionEntity() {} public MapRootAuthenticationSessionEntity(String id, String realmId) { this.id = id; @@ -57,11 +56,6 @@ public class MapRootAuthenticationSessionEntity implements AbstractEntity, Updat this.updated |= id != null; } - @Override - public boolean isUpdated() { - return this.updated; - } - public String getRealmId() { return realmId; } @@ -103,4 +97,8 @@ public class MapRootAuthenticationSessionEntity implements AbstractEntity, Updat this.updated |= !this.authenticationSessions.isEmpty(); this.authenticationSessions.clear(); } + + void signalUpdated(boolean updated) { + this.updated |= updated; + } } diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapPermissionTicketEntity.java b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapPermissionTicketEntity.java index 88bb276047..1b500bd571 100644 --- a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapPermissionTicketEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapPermissionTicketEntity.java @@ -22,7 +22,7 @@ import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.UpdatableEntity; import java.util.Objects; -public class MapPermissionTicketEntity implements AbstractEntity, UpdatableEntity { +public class MapPermissionTicketEntity extends UpdatableEntity.Impl implements AbstractEntity { private String id; private String owner; @@ -33,7 +33,6 @@ public class MapPermissionTicketEntity implements AbstractEntity, UpdatableEntit private String scopeId; private String resourceServerId; private String policyId; - private boolean updated = false; public MapPermissionTicketEntity(String id) { this.id = id; @@ -125,11 +124,6 @@ public class MapPermissionTicketEntity implements AbstractEntity, UpdatableEntit this.policyId = policyId; } - @Override - public boolean isUpdated() { - return updated; - } - @Override public String toString() { return String.format("%s@%08x", getId(), System.identityHashCode(this)); diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapPolicyEntity.java b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapPolicyEntity.java index 16395b8adf..42f21e0541 100644 --- a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapPolicyEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapPolicyEntity.java @@ -28,7 +28,7 @@ import java.util.Set; import java.util.Map; import java.util.Objects; -public class MapPolicyEntity implements AbstractEntity, UpdatableEntity { +public class MapPolicyEntity extends UpdatableEntity.Impl implements AbstractEntity { private String id; private String name; @@ -42,7 +42,6 @@ public class MapPolicyEntity implements AbstractEntity, UpdatableEntity { private final Set resourceIds = new HashSet<>(); private final Set scopeIds = new HashSet<>(); private String owner; - private boolean updated = false; public MapPolicyEntity(String id) { this.id = id; @@ -187,11 +186,6 @@ public class MapPolicyEntity implements AbstractEntity, UpdatableEntity { this.updated |= id != null; } - @Override - public boolean isUpdated() { - return updated; - } - @Override public String toString() { return String.format("%s@%08x", getId(), System.identityHashCode(this)); diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapResourceEntity.java b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapResourceEntity.java index 0d20073562..2be129707e 100644 --- a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapResourceEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapResourceEntity.java @@ -27,7 +27,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -public class MapResourceEntity implements AbstractEntity, UpdatableEntity { +public class MapResourceEntity extends UpdatableEntity.Impl implements AbstractEntity { private String id; private String name; @@ -41,7 +41,6 @@ public class MapResourceEntity implements AbstractEntity, UpdatableEntity { private final Set scopeIds = new HashSet<>(); private final Set policyIds = new HashSet<>(); private final Map> attributes = new HashMap<>(); - private boolean updated = false; public MapResourceEntity(String id) { this.id = id; @@ -178,11 +177,6 @@ public class MapResourceEntity implements AbstractEntity, UpdatableEntity { this.updated |= this.attributes.remove(name) != null; } - @Override - public boolean isUpdated() { - return updated; - } - @Override public String toString() { return String.format("%s@%08x", getId(), System.identityHashCode(this)); diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapResourceServerEntity.java b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapResourceServerEntity.java index c35ccefa13..76dc83ae55 100644 --- a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapResourceServerEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapResourceServerEntity.java @@ -24,10 +24,9 @@ import org.keycloak.representations.idm.authorization.PolicyEnforcementMode; import java.util.Objects; -public class MapResourceServerEntity implements AbstractEntity, UpdatableEntity { +public class MapResourceServerEntity extends UpdatableEntity.Impl implements AbstractEntity { private String id; - private boolean updated = false; private boolean allowRemoteResourceManagement; private PolicyEnforcementMode policyEnforcementMode = PolicyEnforcementMode.ENFORCING; @@ -78,11 +77,6 @@ public class MapResourceServerEntity implements AbstractEntity, UpdatableEntity this.decisionStrategy = decisionStrategy; } - @Override - public boolean isUpdated() { - return updated; - } - @Override public String toString() { return String.format("%s@%08x", getId(), System.identityHashCode(this)); diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapScopeEntity.java b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapScopeEntity.java index fec2f5dace..96aff466d5 100644 --- a/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapScopeEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/entity/MapScopeEntity.java @@ -22,14 +22,13 @@ import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.UpdatableEntity; import java.util.Objects; -public class MapScopeEntity implements AbstractEntity, UpdatableEntity { +public class MapScopeEntity extends UpdatableEntity.Impl implements AbstractEntity { private String id; private String name; private String displayName; private String iconUri; private String resourceServerId; - private boolean updated = false; public MapScopeEntity(String id) { this.id = id; @@ -85,11 +84,6 @@ public class MapScopeEntity implements AbstractEntity, UpdatableEntity { this.resourceServerId = resourceServerId; } - @Override - public boolean isUpdated() { - return updated; - } - @Override public String toString() { return String.format("%s@%08x", getId(), System.identityHashCode(this)); diff --git a/model/map/src/main/java/org/keycloak/models/map/client/MapClientEntity.java b/model/map/src/main/java/org/keycloak/models/map/client/MapClientEntity.java index 2bbb494c87..69e6be9d07 100644 --- a/model/map/src/main/java/org/keycloak/models/map/client/MapClientEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/client/MapClientEntity.java @@ -26,27 +26,22 @@ import java.util.Objects; import java.util.Set; import java.util.stream.Stream; import org.keycloak.models.map.annotations.GenerateEntityImplementations; -import org.keycloak.models.map.common.Serialization; +import org.keycloak.models.map.common.DeepCloner; /** * * @author hmlnarik */ -@GenerateEntityImplementations(inherits="org.keycloak.models.map.client.MapClientEntity.AbstractClientEntity") +@GenerateEntityImplementations( + inherits = "org.keycloak.models.map.client.MapClientEntity.AbstractClientEntity" +) +@DeepCloner.Root public interface MapClientEntity extends AbstractEntity, UpdatableEntity { public abstract class AbstractClientEntity extends UpdatableEntity.Impl implements MapClientEntity { - /** - * Flag signalizing that any of the setters has been meaningfully used. - */ + private String id; - protected AbstractClientEntity() {} - - public AbstractClientEntity(String id) { - this.id = id; - } - @Override public String getId() { return this.id; @@ -59,10 +54,6 @@ public interface MapClientEntity extends AbstractEntity, UpdatableEntity { this.updated |= id != null; } - public V deepClone(V obj) { - return Serialization.from(obj); - } - @Override public Stream getClientScopes(boolean defaultScope) { final Map clientScopes = getClientScopes(); diff --git a/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java b/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java index 576ec6584a..a5d0981edb 100644 --- a/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java @@ -140,7 +140,8 @@ public class MapClientProvider implements ClientProvider { public ClientModel addClient(RealmModel realm, String id, String clientId) { LOG.tracef("addClient(%s, %s, %s)%s", realm, id, clientId, getShortStackTrace()); - MapClientEntity entity = new MapClientEntityImpl(id); + MapClientEntity entity = new MapClientEntityImpl(); + entity.setId(id); entity.setRealmId(realm.getId()); entity.setClientId(clientId); entity.setEnabled(true); diff --git a/model/map/src/main/java/org/keycloak/models/map/client/MapProtocolMapperEntity.java b/model/map/src/main/java/org/keycloak/models/map/client/MapProtocolMapperEntity.java index 300f979852..eeee874dcb 100644 --- a/model/map/src/main/java/org/keycloak/models/map/client/MapProtocolMapperEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/client/MapProtocolMapperEntity.java @@ -17,6 +17,7 @@ package org.keycloak.models.map.client; import org.keycloak.models.map.annotations.GenerateEntityImplementations; +import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.UpdatableEntity; import java.util.Map; @@ -25,6 +26,7 @@ import java.util.Map; * @author hmlnarik */ @GenerateEntityImplementations +@DeepCloner.Root public interface MapProtocolMapperEntity extends UpdatableEntity { String getId(); diff --git a/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeEntity.java b/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeEntity.java index ff4b594a32..dd2d800512 100644 --- a/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeEntity.java @@ -31,7 +31,7 @@ import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.UpdatableEntity; -public class MapClientScopeEntity implements AbstractEntity, UpdatableEntity { +public class MapClientScopeEntity extends UpdatableEntity.Impl implements AbstractEntity { private String id; private String realmId; @@ -47,9 +47,8 @@ public class MapClientScopeEntity implements AbstractEntity, UpdatableEntity { /** * Flag signalizing that any of the setters has been meaningfully used. */ - protected boolean updated; - protected MapClientScopeEntity() {} + public MapClientScopeEntity() {} public MapClientScopeEntity(String id, String realmId) { this.id = id; @@ -68,11 +67,6 @@ public class MapClientScopeEntity implements AbstractEntity, UpdatableEntity { this.updated |= id != null; } - @Override - public boolean isUpdated() { - return this.updated; - } - public String getName() { return name; } 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 new file mode 100644 index 0000000000..ba96d7be0b --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/common/DeepCloner.java @@ -0,0 +1,384 @@ +/* + * 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.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Stack; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import org.jboss.logging.Logger; + +/** + * Helper class for deep cloning and fine-grained instantiation per interface and deep copying their properties. + *

+ * This class is intended to be used by individual map storage implementations for copying + * over entities into their native implementations. + *

+ * For example, a {@code MapClientEntity} interface could be implemented by {@code MapClientEntityImpl} + * (used by a file-based storage in this example) and an {@code HotRodClientEntityImpl} (for Infinispan). + * Say that the Infinispan is stacked on top of the file-based storage to provide caching layer. + * Upon first read, a {@code MapClientEntityImpl} could be obtained from file-based storage and passed + * to Infinispan layer for caching. Infinispan, regardless of the actual implementation, need to store + * the {@code MapClientEntity} data in a form that can be processed and sent over the wire in Infinispan + * (say in an {@code InfinispanClientEntityImpl}). To achieve this, the Infinispan store has to clone + * the file entity values from the {@code MapClientEntityImpl} to {@code InfinispanClientEntityImpl}, + * i.e. it performs deep cloning, using this helper class. + *

+ * Broader context: + * In tree store, map storages are agnostic to their neighbours. Therefore each implementation can be + * provided with a record (a {@code MapClientEntity} instance in the example above) originating from + * any other implementation. For a map storage to process the record (beyond read-only mode), + * it needs to be able to clone it into its own entity. Each of the storages thus can benefit from + * the {@code DeepCloner} capabilities. + * + * @author hmlnarik + */ +public class DeepCloner { + + /** + * Marker for interfaces that could be requested for instantiation and cloning. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface Root {} + + /** + * Function that clones properties from {@code original} object to a {@code target} object and returns + * the cloned object (usually the same as the {@code target}). + * @param Object class + */ + @FunctionalInterface + public interface Cloner { + /** + * Function that clones properties from {@code original} object to a {@code target} object and returns + * the cloned object (usually the same as the {@code target}). + */ + V clone(V original, V target); + } + + public static final DeepCloner DUMB_CLONER = new Builder().build(); + + /** + * Builder for the {@code DeepCloner} helper class. + */ + 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 Cloner genericCloner = (from, to) -> { throw new IllegalStateException("Cloner not found for class " + (from == null ? "" : from.getClass())); }; + + /** + * Returns a {@link DeepCloner} initialized with the respective constructors and cloners. + * @return + */ + public DeepCloner build() { + return new DeepCloner(parameterlessConstructors, constructors, clonersWithId, clonersWithoutId, genericCloner); + } + + private void forThisClassAndAllMarkedParentsAndInterfaces(Class rootClazz, Consumer> action) { + action.accept(rootClazz); + + Stack> c = new Stack<>(); + c.push(rootClazz); + while (! c.isEmpty()) { + Class cl = c.pop(); + if (cl == null) { + continue; + } + + c.push(cl.getSuperclass()); + for (Class iface : cl.getInterfaces()) { + c.push(iface); + } + + if (cl.getAnnotation(Root.class) != null) { + action.accept(cl); + } + } + } + + /** + * 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}. + * If {@code null}, parameterless constructor is not available. + * @return This builder. + */ + public Builder constructor(Class clazz, Supplier constructorNoParameters) { + if (constructorNoParameters != null) { + forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.parameterlessConstructors.put(cl, constructorNoParameters)); + } + return this; + } + + /** + * 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 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 constructorDC(Class clazz, Function constructor) { + if (constructor != null) { + forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.constructors.put(cl, constructor)); + } + return this; + } + + /** + * Adds a method that copies (as in a deep copy) an object properties from one object to another + * + * @param Class or interface whose instance would be copied over to another instance by the given cloner + * @param clazz Class or interface whose instance would be copied over to another instance by the given cloner + * @param cloner A method for cloning with the following signature: {@code V deepClone(V from, V to)} which + * copies properties of an object {@code from} onto the object {@code to}. This + * function usually returns {@code to} + * @return This builder. + */ + public Builder cloner(Class clazz, Cloner cloner) { + if (cloner != null) { + forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.clonersWithId.put(cl, cloner)); + } + return this; + } + + /** + * Adds a method that copies (as in a deep copy) an object properties from one object to another + * + * @param Class or interface whose instance would be copied over to another instance by the given cloner + * @param clazz Class or interface whose instance would be copied over to another instance by the given cloner + * @param clonerWithId A method for cloning with the following signature: {@code V deepClone(V from, V to)} which + * copies properties of an object {@code from} onto the object {@code to}. This + * function usually returns {@code to} + * @return This builder. + */ + public Builder cloner(Class clazz, Cloner clonerWithId, Cloner clonerWithoutId) { + if (clonerWithId != null) { + forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.clonersWithId.put(cl, clonerWithId)); + } + if (clonerWithoutId != null) { + forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.clonersWithoutId.put(cl, clonerWithoutId)); + } + return this; + } + + /** + * 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} + * @return This builder. + */ + public Builder genericCloner(Cloner genericCloner) { + this.genericCloner = genericCloner; + return this; + } + } + + private static final Logger LOG = Logger.getLogger(DeepCloner.class); + + private final Map, Supplier> parameterlessConstructors; + private final Map, Function> constructors; + private final Map, Cloner> clonersWithId; + private final Map, Cloner> clonersWithoutId; + private final Cloner genericCloner; + + private DeepCloner(Map, Supplier> parameterlessConstructors, + Map, Function> constructors, + Map, Cloner> clonersWithId, + Map, Cloner> clonersWithoutId, + Cloner genericCloner) { + this.parameterlessConstructors = parameterlessConstructors; + this.constructors = constructors; + this.clonersWithId = clonersWithId; + this.clonersWithoutId = clonersWithoutId; + this.genericCloner = genericCloner; + } + + private V getFromClassRespectingHierarchy(Map, V> map, Class clazz) { + // fast lookup + V res = map.get(clazz); + if (res != null) { + return res; + } + + // BFS on implemented supertypes and interfaces. Skip clazz as it has been looked up already + LinkedList> ll = new LinkedList<>(); + ll.push(clazz.getSuperclass()); + for (Class iface : clazz.getInterfaces()) { + ll.push(iface); + } + + while (! ll.isEmpty()) { + Class cl = ll.pollFirst(); + if (cl == null) { + continue; + } + + res = map.get(cl); + if (res != null) { + map.put(clazz, res); // Wire clazz with the result for fast lookup next time + return res; + } + + ll.push(cl.getSuperclass()); + ll.addAll(Arrays.asList(cl.getInterfaces())); + } + return null; + } + + /** + * 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 + * @param clazz Type (class or a {@code @Root} interface) to create a new instance + * @return A new instance + * @throws IllegalStateException When the constructor is not known. + */ + public V newInstance(Class clazz) { + if (clazz == null) { + return null; + } + + V res; + @SuppressWarnings("unchecked") + Function c = (Function) getFromClassRespectingHierarchy(this.constructors, clazz); + if (c == null) { + @SuppressWarnings("unchecked") + Supplier s = (Supplier) getFromClassRespectingHierarchy(this.parameterlessConstructors, clazz); + if (s == null) { + try { + res = clazz.newInstance(); + } catch (InstantiationException | IllegalAccessException ex) { + res = null; + } + } else { + res = s.get(); + } + } else { + res = c.apply(this); + } + + if (res == null) { + throw new IllegalStateException("Cannot instantiate " + clazz); + } + + return res; + } + + /** + * Deeply clones properties from the {@code from} instance to the {@code to} instance. + * @param Type (class or a {@code @Root} interface) to clone the instance + * @param from Original instance + * @param to Instance to copy the properties onto + * @return Instance which has all the properties same as the {@code from}. Preferably, {@code to} is returned. + * However {@code from} is returned if the cloner is not known and generic cloner is not available. + */ + public V deepClone(V from, V to) { + return deepClone(from, to, this.clonersWithId); + } + + /** + * Deeply clones properties from the {@code from} instance to the {@code to} instance excluding the ID field. + * @param Type (class or a {@code @Root} interface) to clone the instance + * @param from Original instance + * @param to Instance to copy the properties onto + * @return Instance which has all the properties same as the {@code from}. Preferably, {@code to} is returned. + * However {@code from} is returned if the cloner is not known and generic cloner is not available. + */ + public V deepCloneNoId(V from, V to) { + return deepClone(from, to, this.clonersWithoutId); + } + + @SuppressWarnings("unchecked") + 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); + } + + if (genericCloner != null) { + LOG.debugf("Using generic cloner for %s", from.getClass()); + final V res = ((Cloner) genericCloner).clone(from, to); + + if (res instanceof UpdatableEntity) { + ((UpdatableEntity) res).clearUpdatedFlag(); + } + + return res; + } + + return warnCloneNotSupported(from); + } + + /** + * Creates a new instance of the given type and copies its properties from the {@code from} instance + * @param Type (class or a {@code @Root} interface) to create a new instance and clone properties from + * @param newId ID of the new object + * @param from Original instance + * @return Newly created instance or {@code null} if {@code from} is {@code null}. + */ + @SuppressWarnings("unchecked") + public V from(String newId, V from) { + if (from == null) { + return null; + } + final V res = newInstance((Class) from.getClass()); + if (newId != null) { + res.setId(newId); + } + return deepCloneNoId(from, res); + } + + /** + * Creates a new instance of the given type and copies its properties from the {@code from} instance + * @param Type (class or a {@code @Root} interface) to create a new instance and clone properties from + * @param from Original instance + * @return Newly created instance or {@code null} if {@code from} is {@code null}. + */ + public V from(V from) { + return from == null ? null : deepClone(from, newInstance((Class) from.getClass())); + } + + /** + * Issues warning in the logs and returns the input parameter {@code o} + * @param o + * @return The {@code o} object + */ + public static T warnCloneNotSupported(T o) { + if (o != null) { + LOG.warnf("Cloning not supported for %s, returning the same instance!", o.getClass()); + } + return o; + } + +} diff --git a/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java b/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java index 1d33f1d452..c6781f906a 100644 --- a/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java +++ b/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java @@ -19,6 +19,7 @@ package org.keycloak.models.map.common; import org.keycloak.common.util.reflections.Reflections; import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreType; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo.As; @@ -54,11 +55,15 @@ public class Serialization { .setVisibility(PropertyAccessor.FIELD, Visibility.ANY) .activateDefaultTyping(new LaissezFaireSubTypeValidator() /* TODO - see javadoc */, ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS, JsonTypeInfo.As.PROPERTY) .addMixIn(UpdatableEntity.class, IgnoreUpdatedMixIn.class) + .addMixIn(DeepCloner.class, IgnoredTypeMixIn.class) ; public static final ConcurrentHashMap, ObjectReader> READERS = new ConcurrentHashMap<>(); public static final ConcurrentHashMap, ObjectWriter> WRITERS = new ConcurrentHashMap<>(); + @JsonIgnoreType + class IgnoredTypeMixIn {} + abstract class IgnoreUpdatedMixIn { @JsonIgnore public abstract boolean isUpdated(); } @@ -90,4 +95,28 @@ public class Serialization { throw new IllegalStateException(ex); } } + + public static T from(T orig, T target) { + if (orig == null) { + return null; + } + @SuppressWarnings("unchecked") + final Class origClass = (Class) orig.getClass(); + + // Naive solution but will do. + try { + ObjectReader reader = MAPPER.readerForUpdating(target); + ObjectWriter writer = WRITERS.computeIfAbsent(origClass, MAPPER::writerFor); + final T res; + res = reader.readValue(writer.writeValueAsBytes(orig)); + + if (res != target) { + throw new IllegalStateException("Should clone into desired target"); + } + + return res; + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + } } diff --git a/model/map/src/main/java/org/keycloak/models/map/common/UpdatableEntity.java b/model/map/src/main/java/org/keycloak/models/map/common/UpdatableEntity.java index a5d9da303f..a8b28cdb04 100644 --- a/model/map/src/main/java/org/keycloak/models/map/common/UpdatableEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/common/UpdatableEntity.java @@ -16,6 +16,9 @@ */ package org.keycloak.models.map.common; +import org.keycloak.models.map.annotations.IgnoreForEntityImplementationGenerator; + +@IgnoreForEntityImplementationGenerator public interface UpdatableEntity { public static class Impl implements UpdatableEntity { diff --git a/model/map/src/main/java/org/keycloak/models/map/group/MapGroupEntity.java b/model/map/src/main/java/org/keycloak/models/map/group/MapGroupEntity.java index 4809cd39ad..c755225a5d 100644 --- a/model/map/src/main/java/org/keycloak/models/map/group/MapGroupEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/group/MapGroupEntity.java @@ -31,7 +31,7 @@ import java.util.Set; * * @author mhajas */ -public class MapGroupEntity implements AbstractEntity, UpdatableEntity { +public class MapGroupEntity extends UpdatableEntity.Impl implements AbstractEntity { private String id; private String realmId; @@ -44,9 +44,8 @@ public class MapGroupEntity implements AbstractEntity, UpdatableEntity { /** * Flag signalizing that any of the setters has been meaningfully used. */ - protected boolean updated; - protected MapGroupEntity() {} + public MapGroupEntity() {} public MapGroupEntity(String id, String realmId) { this.id = id; @@ -65,12 +64,6 @@ public class MapGroupEntity implements AbstractEntity, UpdatableEntity { this.updated |= id != null; } - @Override - public boolean isUpdated() { - return this.updated; - } - - public String getName() { return name; } diff --git a/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureEntity.java b/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureEntity.java index ec1ffba306..67ce55e8af 100644 --- a/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureEntity.java @@ -24,16 +24,11 @@ import java.util.Objects; /** * @author Martin Kanis */ -public class MapUserLoginFailureEntity implements AbstractEntity, UpdatableEntity { +public class MapUserLoginFailureEntity extends UpdatableEntity.Impl implements AbstractEntity { private String id; private String realmId; private String userId; - /** - * Flag signalizing that any of the setters has been meaningfully used. - */ - protected boolean updated; - private int failedLoginNotBefore; private int numFailures; private long lastFailure; @@ -59,11 +54,6 @@ public class MapUserLoginFailureEntity implements AbstractEntity, UpdatableEntit this.updated |= id != null; } - @Override - public boolean isUpdated() { - return this.updated; - } - public String getRealmId() { return realmId; } diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmEntity.java index 1d20189557..5755f699de 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmEntity.java @@ -43,7 +43,7 @@ import org.keycloak.models.map.realm.entity.MapRequiredActionProviderEntity; import org.keycloak.models.map.realm.entity.MapRequiredCredentialEntity; import org.keycloak.models.map.realm.entity.MapWebAuthnPolicyEntity; -public class MapRealmEntity implements AbstractEntity, UpdatableEntity { +public class MapRealmEntity extends UpdatableEntity.Impl implements AbstractEntity { private String id; private String name; @@ -128,9 +128,8 @@ public class MapRealmEntity implements AbstractEntity, UpdatableEntity { /** * Flag signalizing that any of the setters has been meaningfully used. */ - protected boolean updated; - protected MapRealmEntity() {} + public MapRealmEntity() {} public MapRealmEntity(String id) { this.id = id; diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticationExecutionEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticationExecutionEntity.java index c2fd77e3c4..8f5e44293c 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticationExecutionEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticationExecutionEntity.java @@ -22,7 +22,7 @@ import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.utils.KeycloakModelUtils; -public class MapAuthenticationExecutionEntity implements UpdatableEntity { +public class MapAuthenticationExecutionEntity extends UpdatableEntity.Impl { private String id; private String authenticator; @@ -33,7 +33,6 @@ public class MapAuthenticationExecutionEntity implements UpdatableEntity { private Boolean autheticatorFlow = false; private Integer priority = 0; - private boolean updated; private MapAuthenticationExecutionEntity() {} @@ -66,11 +65,6 @@ public class MapAuthenticationExecutionEntity implements UpdatableEntity { return model; } - @Override - public boolean isUpdated() { - return updated; - } - public String getId() { return id; } diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticationFlowEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticationFlowEntity.java index 0f78cf863a..3aed620431 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticationFlowEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticationFlowEntity.java @@ -22,7 +22,7 @@ import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.utils.KeycloakModelUtils; -public class MapAuthenticationFlowEntity implements UpdatableEntity { +public class MapAuthenticationFlowEntity extends UpdatableEntity.Impl { private String id; private String alias; @@ -31,7 +31,6 @@ public class MapAuthenticationFlowEntity implements UpdatableEntity { private Boolean builtIn = false; private Boolean topLevel = false; - private boolean updated; private MapAuthenticationFlowEntity() {} @@ -61,11 +60,6 @@ public class MapAuthenticationFlowEntity implements UpdatableEntity { return model; } - @Override - public boolean isUpdated() { - return updated; - } - public String getId() { return id; } diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticatorConfigEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticatorConfigEntity.java index 884ced587b..7243c86bad 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticatorConfigEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticatorConfigEntity.java @@ -24,13 +24,12 @@ import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.utils.KeycloakModelUtils; -public class MapAuthenticatorConfigEntity implements UpdatableEntity { +public class MapAuthenticatorConfigEntity extends UpdatableEntity.Impl { private String id; private String alias; private Map config = new HashMap<>(); - private boolean updated; private MapAuthenticatorConfigEntity() {} @@ -53,11 +52,6 @@ public class MapAuthenticatorConfigEntity implements UpdatableEntity { return model; } - @Override - public boolean isUpdated() { - return updated; - } - public String getId() { return id; } diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapClientInitialAccessEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapClientInitialAccessEntity.java index 4fc0cd8a52..a0299e054e 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapClientInitialAccessEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapClientInitialAccessEntity.java @@ -23,7 +23,7 @@ import org.keycloak.models.ClientInitialAccessModel; import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.utils.KeycloakModelUtils; -public class MapClientInitialAccessEntity implements UpdatableEntity { +public class MapClientInitialAccessEntity extends UpdatableEntity.Impl { private String id; private Integer timestamp = 0; @@ -31,7 +31,6 @@ public class MapClientInitialAccessEntity implements UpdatableEntity { private Integer count = 0; private Integer remainingCount = 0; - private boolean updated; private MapClientInitialAccessEntity() {} @@ -58,11 +57,6 @@ public class MapClientInitialAccessEntity implements UpdatableEntity { return model; } - @Override - public boolean isUpdated() { - return updated; - } - public String getId() { return id; } diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapComponentEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapComponentEntity.java index a64867291b..f1dbd08d4d 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapComponentEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapComponentEntity.java @@ -23,7 +23,7 @@ import org.keycloak.component.ComponentModel; import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.utils.KeycloakModelUtils; -public class MapComponentEntity implements UpdatableEntity { +public class MapComponentEntity extends UpdatableEntity.Impl { private String id; private String name; @@ -33,7 +33,6 @@ public class MapComponentEntity implements UpdatableEntity { private String parentId; private MultivaluedHashMap config = new MultivaluedHashMap<>(); - private boolean updated; private MapComponentEntity() {} @@ -64,11 +63,6 @@ public class MapComponentEntity implements UpdatableEntity { return model; } - @Override - public boolean isUpdated() { - return updated; - } - public String getId() { return id; } diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapIdentityProviderEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapIdentityProviderEntity.java index 949cf37d39..9cf4ad504a 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapIdentityProviderEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapIdentityProviderEntity.java @@ -24,7 +24,7 @@ import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.utils.KeycloakModelUtils; -public class MapIdentityProviderEntity implements UpdatableEntity { +public class MapIdentityProviderEntity extends UpdatableEntity.Impl { private String id; private String alias; @@ -40,7 +40,6 @@ public class MapIdentityProviderEntity implements UpdatableEntity { private Boolean authenticateByDefault = false; private Map config = new HashMap<>(); - private boolean updated; private MapIdentityProviderEntity() {} @@ -83,11 +82,6 @@ public class MapIdentityProviderEntity implements UpdatableEntity { return model; } - @Override - public boolean isUpdated() { - return updated; - } - public String getId() { return id; } diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapIdentityProviderMapperEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapIdentityProviderMapperEntity.java index 2234b3483c..fc15143d6c 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapIdentityProviderMapperEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapIdentityProviderMapperEntity.java @@ -24,7 +24,7 @@ import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.utils.KeycloakModelUtils; -public class MapIdentityProviderMapperEntity implements UpdatableEntity { +public class MapIdentityProviderMapperEntity extends UpdatableEntity.Impl { private String id; private String name; @@ -32,7 +32,6 @@ public class MapIdentityProviderMapperEntity implements UpdatableEntity { private String identityProviderMapper; private Map config = new HashMap<>(); - private boolean updated; private MapIdentityProviderMapperEntity() {} @@ -59,11 +58,6 @@ public class MapIdentityProviderMapperEntity implements UpdatableEntity { return model; } - @Override - public boolean isUpdated() { - return updated; - } - public String getId() { return id; } diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapOTPPolicyEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapOTPPolicyEntity.java index dfd5dd9add..30c5e6c059 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapOTPPolicyEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapOTPPolicyEntity.java @@ -21,7 +21,7 @@ import java.util.Objects; import org.keycloak.models.OTPPolicy; import org.keycloak.models.map.common.UpdatableEntity; -public class MapOTPPolicyEntity implements UpdatableEntity { +public class MapOTPPolicyEntity extends UpdatableEntity.Impl { private Integer otpPolicyInitialCounter = 0; private Integer otpPolicyDigits = 0; @@ -30,7 +30,6 @@ public class MapOTPPolicyEntity implements UpdatableEntity { private String otpPolicyType; private String otpPolicyAlgorithm; - private boolean updated; private MapOTPPolicyEntity() {} @@ -58,11 +57,6 @@ public class MapOTPPolicyEntity implements UpdatableEntity { return model; } - @Override - public boolean isUpdated() { - return updated; - } - public Integer getOtpPolicyInitialCounter() { return otpPolicyInitialCounter; } diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapRequiredActionProviderEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapRequiredActionProviderEntity.java index 6a7089bc7c..31b7d2d11a 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapRequiredActionProviderEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapRequiredActionProviderEntity.java @@ -24,7 +24,7 @@ import org.keycloak.models.RequiredActionProviderModel; import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.utils.KeycloakModelUtils; -public class MapRequiredActionProviderEntity implements UpdatableEntity { +public class MapRequiredActionProviderEntity extends UpdatableEntity.Impl { private String id; private String alias; @@ -35,7 +35,6 @@ public class MapRequiredActionProviderEntity implements UpdatableEntity { private Boolean defaultAction = false; private Map config = new HashMap<>(); - private boolean updated; private MapRequiredActionProviderEntity() {} @@ -68,11 +67,6 @@ public class MapRequiredActionProviderEntity implements UpdatableEntity { return model; } - @Override - public boolean isUpdated() { - return updated; - } - public String getId() { return id; } diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapRequiredCredentialEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapRequiredCredentialEntity.java index decf792f22..422bf5f25a 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapRequiredCredentialEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapRequiredCredentialEntity.java @@ -21,14 +21,13 @@ import java.util.Objects; import org.keycloak.models.RequiredCredentialModel; import org.keycloak.models.map.common.UpdatableEntity; -public class MapRequiredCredentialEntity implements UpdatableEntity { +public class MapRequiredCredentialEntity extends UpdatableEntity.Impl { private String type; private String formLabel; private Boolean input = false; private Boolean secret = false; - private boolean updated; private MapRequiredCredentialEntity() {} @@ -52,11 +51,6 @@ public class MapRequiredCredentialEntity implements UpdatableEntity { return model; } - @Override - public boolean isUpdated() { - return updated; - } - public String getType() { return type; } diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapWebAuthnPolicyEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapWebAuthnPolicyEntity.java index cdbcccf37a..73dd14980a 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapWebAuthnPolicyEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapWebAuthnPolicyEntity.java @@ -25,7 +25,7 @@ import org.keycloak.models.Constants; import org.keycloak.models.WebAuthnPolicy; import org.keycloak.models.map.common.UpdatableEntity; -public class MapWebAuthnPolicyEntity implements UpdatableEntity { +public class MapWebAuthnPolicyEntity extends UpdatableEntity.Impl { // mandatory private String rpEntityName; @@ -41,7 +41,6 @@ public class MapWebAuthnPolicyEntity implements UpdatableEntity { private Boolean avoidSameAuthenticatorRegister = false; private List acceptableAaguids = new LinkedList<>(); - private boolean updated; private MapWebAuthnPolicyEntity() {} @@ -92,11 +91,6 @@ public class MapWebAuthnPolicyEntity implements UpdatableEntity { return entity; } - @Override - public boolean isUpdated() { - return updated; - } - public String getRpEntityName() { return rpEntityName; } diff --git a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleEntity.java b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleEntity.java index fdb9dc5b1c..ab48ab14f3 100644 --- a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleEntity.java @@ -25,7 +25,7 @@ import java.util.Set; import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.UpdatableEntity; -public class MapRoleEntity implements AbstractEntity, UpdatableEntity { +public class MapRoleEntity extends UpdatableEntity.Impl implements AbstractEntity { private String id; private String realmId; @@ -40,9 +40,8 @@ public class MapRoleEntity implements AbstractEntity, UpdatableEntity { /** * Flag signalizing that any of the setters has been meaningfully used. */ - protected boolean updated; - protected MapRoleEntity() {} + public MapRoleEntity() {} public MapRoleEntity(String id, String realmId) { this.id = id; @@ -61,11 +60,6 @@ public class MapRoleEntity implements AbstractEntity, UpdatableEntity { this.updated |= id != null; } - @Override - public boolean isUpdated() { - return this.updated; - } - public String getName() { return name; } diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java index ce6aa25755..3efa21ef37 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java @@ -18,7 +18,7 @@ package org.keycloak.models.map.storage.chm; import org.keycloak.models.map.common.StringKeyConvertor; import org.keycloak.models.map.common.AbstractEntity; -import org.keycloak.models.map.common.Serialization; +import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.UpdatableEntity; import java.util.Iterator; import java.util.LinkedHashMap; @@ -40,19 +40,21 @@ public class ConcurrentHashMapKeycloakTransaction tasks = new LinkedHashMap<>(); - private final MapStorage map; - private final StringKeyConvertor keyConvertor; + protected boolean active; + protected boolean rollback; + protected final Map tasks = new LinkedHashMap<>(); + protected final MapStorage map; + protected final StringKeyConvertor keyConvertor; + protected final DeepCloner cloner; enum MapOperation { CREATE, UPDATE, DELETE, } - public ConcurrentHashMapKeycloakTransaction(MapStorage map, StringKeyConvertor keyConvertor) { + public ConcurrentHashMapKeycloakTransaction(MapStorage map, StringKeyConvertor keyConvertor, DeepCloner cloner) { this.map = map; this.keyConvertor = keyConvertor; + this.cloner = cloner; } @Override @@ -119,7 +121,7 @@ public class ConcurrentHashMapKeycloakTransaction e.isUpdated()); } @@ -212,10 +214,11 @@ public class ConcurrentHashMapKeycloakTransaction implements MapStorage { - private final ConcurrentMap store = new ConcurrentHashMap<>(); + protected final ConcurrentMap store = new ConcurrentHashMap<>(); - private final Map, UpdatePredicatesFunc> fieldPredicates; - private final StringKeyConvertor keyConvertor; + protected final Map, UpdatePredicatesFunc> fieldPredicates; + protected final StringKeyConvertor keyConvertor; + protected final DeepCloner cloner; @SuppressWarnings("unchecked") - public ConcurrentHashMapStorage(Class modelClass, StringKeyConvertor keyConvertor) { + public ConcurrentHashMapStorage(Class modelClass, StringKeyConvertor keyConvertor, DeepCloner cloner) { this.fieldPredicates = MapFieldPredicates.getPredicates(modelClass); this.keyConvertor = keyConvertor; + this.cloner = cloner; } @Override public V create(V value) { K key = keyConvertor.fromStringSafe(value.getId()); if (key == null) { - value = Serialization.from(value); key = keyConvertor.yieldNewUniqueKey(); - value.setId(keyConvertor.keyToString(key)); + value = cloner.from(keyConvertor.keyToString(key), value); } store.putIfAbsent(key, value); return value; @@ -98,6 +99,7 @@ public class ConcurrentHashMapStorage b = criteria.unwrap(MapModelCriteriaBuilder.class); if (b == null) { throw new IllegalStateException("Incompatible class: " + criteria.getClass()); @@ -130,7 +132,7 @@ public class ConcurrentHashMapStorage createTransaction(KeycloakSession session) { MapKeycloakTransaction sessionTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class); - return sessionTransaction == null ? new ConcurrentHashMapKeycloakTransaction<>(this, keyConvertor) : sessionTransaction; + return sessionTransaction == null ? new ConcurrentHashMapKeycloakTransaction<>(this, keyConvertor, cloner) : sessionTransaction; } public StringKeyConvertor getKeyConvertor() { @@ -146,6 +148,7 @@ public class ConcurrentHashMapStorage> stream = store.entrySet().stream(); + @SuppressWarnings("unchecked") MapModelCriteriaBuilder b = criteria.unwrap(MapModelCriteriaBuilder.class); if (b == null) { throw new IllegalStateException("Incompatible class: " + criteria.getClass()); diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java index 1666c406a8..691cafef8c 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java @@ -44,8 +44,11 @@ import org.keycloak.models.map.authorization.entity.MapResourceServerEntity; import org.keycloak.models.map.authorization.entity.MapScopeEntity; import org.keycloak.models.map.client.MapClientEntity; import org.keycloak.models.map.client.MapClientEntityImpl; +import org.keycloak.models.map.client.MapProtocolMapperEntity; +import org.keycloak.models.map.client.MapProtocolMapperEntityImpl; import org.keycloak.models.map.clientscope.MapClientScopeEntity; import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.Serialization; import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.map.group.MapGroupEntity; @@ -96,6 +99,12 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide private StringKeyConvertor defaultKeyConvertor; + private final static DeepCloner CLONER = new DeepCloner.Builder() + .genericCloner(Serialization::from) + .constructorDC(MapClientEntityImpl.class, MapClientEntityImpl::new) + .constructor(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl::new) + .build(); + public static final Map, String> MODEL_TO_NAME = new HashMap<>(); static { MODEL_TO_NAME.put(AuthenticatedClientSessionModel.class, "client-sessions"); @@ -224,13 +233,13 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide storages.forEach(this::storeMap); } + @SuppressWarnings("unchecked") private void storeMap(String mapName, ConcurrentHashMapStorage store) { if (mapName != null) { File f = getFile(mapName); try { if (storageDirectory != null) { LOG.debugf("Storing contents to %s", f.getCanonicalPath()); - @SuppressWarnings("unchecked") final ModelCriteriaBuilder readAllCriteria = store.createCriteriaBuilder(); Serialization.MAPPER.writeValue(f, store.read(withCriteria(readAllCriteria))); } else { @@ -242,24 +251,24 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide } } + @SuppressWarnings("unchecked") private ConcurrentHashMapStorage loadMap(String mapName, Class modelType, EnumSet flags) { final StringKeyConvertor kc = keyConvertors.getOrDefault(mapName, defaultKeyConvertor); Class valueType = MODEL_TO_VALUE_TYPE.get(modelType); LOG.debugf("Initializing new map storage: %s", mapName); - @SuppressWarnings("unchecked") ConcurrentHashMapStorage store; if (modelType == UserSessionModel.class) { ConcurrentHashMapStorage clientSessionStore = getStorage(AuthenticatedClientSessionModel.class); - store = new UserSessionConcurrentHashMapStorage(clientSessionStore, kc) { + store = new UserSessionConcurrentHashMapStorage(clientSessionStore, kc, CLONER) { @Override public String toString() { return "ConcurrentHashMapStorage(" + mapName + suffix + ")"; } }; } else { - store = new ConcurrentHashMapStorage(modelType, kc) { + store = new ConcurrentHashMapStorage(modelType, kc, CLONER) { @Override public String toString() { return "ConcurrentHashMapStorage(" + mapName + suffix + ")"; diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/CriteriaOperator.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/CriteriaOperator.java index 9de970b572..79d7da15a6 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/CriteriaOperator.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/CriteriaOperator.java @@ -144,10 +144,10 @@ class CriteriaOperator { if (value.length == 1) { final Object value0 = value[0]; if (value0 instanceof Collection) { - operand = (Collection) value0; + operand = (Collection) value0; } else if (value0 instanceof Stream) { - try (Stream valueS = (Stream) value0) { - operand = (Set) valueS.collect(Collectors.toSet()); + try (Stream valueS = (Stream) value0) { + operand = valueS.collect(Collectors.toSet()); } } else { operand = Collections.singleton(value0); diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java index be2f323afd..34f4625196 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java @@ -21,6 +21,7 @@ import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserSessionModel; 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.ModelCriteriaBuilder; import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; @@ -46,8 +47,8 @@ public class UserSessionConcurrentHashMapStorage extends ConcurrentHashMapSto private final MapKeycloakTransaction clientSessionTr; - public Transaction(MapKeycloakTransaction clientSessionTr, StringKeyConvertor keyConvertor) { - super(UserSessionConcurrentHashMapStorage.this, keyConvertor); + public Transaction(MapKeycloakTransaction clientSessionTr, StringKeyConvertor keyConvertor, DeepCloner cloner) { + super(UserSessionConcurrentHashMapStorage.this, keyConvertor, cloner); this.clientSessionTr = clientSessionTr; } @@ -70,8 +71,8 @@ public class UserSessionConcurrentHashMapStorage extends ConcurrentHashMapSto @SuppressWarnings("unchecked") public UserSessionConcurrentHashMapStorage(ConcurrentHashMapStorage clientSessionStore, - StringKeyConvertor keyConvertor) { - super(UserSessionModel.class, keyConvertor); + StringKeyConvertor keyConvertor, DeepCloner cloner) { + super(UserSessionModel.class, keyConvertor, cloner); this.clientSessionStore = clientSessionStore; } @@ -79,6 +80,6 @@ public class UserSessionConcurrentHashMapStorage extends ConcurrentHashMapSto @SuppressWarnings("unchecked") public MapKeycloakTransaction createTransaction(KeycloakSession session) { MapKeycloakTransaction sessionTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class); - return sessionTransaction == null ? new Transaction(clientSessionStore.createTransaction(session), clientSessionStore.getKeyConvertor()) : sessionTransaction; + return sessionTransaction == null ? new Transaction(clientSessionStore.createTransaction(session), clientSessionStore.getKeyConvertor(), cloner) : sessionTransaction; } } diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java index 63ffab1968..2eab99a6aa 100644 --- a/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java @@ -40,7 +40,7 @@ import java.util.stream.Stream; * * @author mhajas */ -public class MapUserEntity implements AbstractEntity, UpdatableEntity { +public class MapUserEntity extends UpdatableEntity.Impl implements AbstractEntity { private String id; private String realmId; @@ -69,9 +69,8 @@ public class MapUserEntity implements AbstractEntity, UpdatableEntity { /** * Flag signalizing that any of the setters has been meaningfully used. */ - protected boolean updated; - protected MapUserEntity() {} + public MapUserEntity() {} public MapUserEntity(String id, String realmId) { this.id = id; diff --git a/model/map/src/main/java/org/keycloak/models/map/user/UserConsentEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/UserConsentEntity.java index 966d38832a..7b2a3f0da1 100644 --- a/model/map/src/main/java/org/keycloak/models/map/user/UserConsentEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/user/UserConsentEntity.java @@ -31,13 +31,12 @@ import java.util.Objects; import java.util.Set; -public class UserConsentEntity implements UpdatableEntity { +public class UserConsentEntity extends UpdatableEntity.Impl { private String clientId; private final Set grantedClientScopesIds = new HashSet<>(); private Long createdDate; private Long lastUpdatedDate; - private boolean updated; private UserConsentEntity() {} @@ -78,11 +77,6 @@ public class UserConsentEntity implements UpdatableEntity { return model; } - @Override - public boolean isUpdated() { - return updated; - } - public String getClientId() { return clientId; } diff --git a/model/map/src/main/java/org/keycloak/models/map/user/UserCredentialEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/UserCredentialEntity.java index 68df6f012a..c674925fa7 100644 --- a/model/map/src/main/java/org/keycloak/models/map/user/UserCredentialEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/user/UserCredentialEntity.java @@ -23,7 +23,7 @@ import org.keycloak.models.map.common.UpdatableEntity; import java.util.Objects; -public class UserCredentialEntity implements UpdatableEntity { +public class UserCredentialEntity extends UpdatableEntity.Impl { private String id; private String type; @@ -31,7 +31,6 @@ public class UserCredentialEntity implements UpdatableEntity { private Long createdDate; private String secretData; private String credentialData; - private boolean updated; UserCredentialEntity() {} @@ -112,9 +111,4 @@ public class UserCredentialEntity implements UpdatableEntity { this.updated |= !Objects.equals(this.credentialData, credentialData); this.credentialData = credentialData; } - - @Override - public boolean isUpdated() { - return updated; - } } diff --git a/model/map/src/main/java/org/keycloak/models/map/user/UserFederatedIdentityEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/UserFederatedIdentityEntity.java index a57f96aca7..c73393e6b7 100644 --- a/model/map/src/main/java/org/keycloak/models/map/user/UserFederatedIdentityEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/user/UserFederatedIdentityEntity.java @@ -22,12 +22,11 @@ import org.keycloak.models.map.common.UpdatableEntity; import java.util.Objects; -public class UserFederatedIdentityEntity implements UpdatableEntity { +public class UserFederatedIdentityEntity extends UpdatableEntity.Impl { private String token; private String userId; private String identityProvider; private String userName; - private boolean updated; private UserFederatedIdentityEntity() {} @@ -82,9 +81,4 @@ public class UserFederatedIdentityEntity implements UpdatableEntity { this.updated |= !Objects.equals(this.userName, userName); this.userName = userName; } - - @Override - public boolean isUpdated() { - return updated; - } } diff --git a/model/map/src/main/java/org/keycloak/models/map/userSession/MapAuthenticatedClientSessionEntity.java b/model/map/src/main/java/org/keycloak/models/map/userSession/MapAuthenticatedClientSessionEntity.java index 1e8dde041e..33ad54b89b 100644 --- a/model/map/src/main/java/org/keycloak/models/map/userSession/MapAuthenticatedClientSessionEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/userSession/MapAuthenticatedClientSessionEntity.java @@ -27,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap; /** * @author Martin Kanis */ -public class MapAuthenticatedClientSessionEntity implements AbstractEntity, UpdatableEntity { +public class MapAuthenticatedClientSessionEntity extends UpdatableEntity.Impl implements AbstractEntity { private String id; private String userSessionId; @@ -37,7 +37,6 @@ public class MapAuthenticatedClientSessionEntity implements AbstractEntity, Upda /** * Flag signalizing that any of the setters has been meaningfully used. */ - protected boolean updated; private String authMethod; private String redirectUri; @@ -75,11 +74,6 @@ public class MapAuthenticatedClientSessionEntity implements AbstractEntity, Upda this.updated |= id != null; } - @Override - public boolean isUpdated() { - return this.updated; - } - public String getRealmId() { return realmId; } diff --git a/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionEntity.java b/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionEntity.java index b63f1a29fc..edba96570f 100644 --- a/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionEntity.java @@ -30,7 +30,7 @@ import java.util.concurrent.ConcurrentHashMap; /** * @author Martin Kanis */ -public class MapUserSessionEntity implements AbstractEntity, UpdatableEntity { +public class MapUserSessionEntity extends UpdatableEntity.Impl implements AbstractEntity { private String id; private String realmId; @@ -38,7 +38,6 @@ public class MapUserSessionEntity implements AbstractEntity, UpdatableEntity { /** * Flag signalizing that any of the setters has been meaningfully used. */ - protected boolean updated; private String userId; @@ -105,11 +104,6 @@ public class MapUserSessionEntity implements AbstractEntity, UpdatableEntity { this.updated |= id != null; } - @Override - public boolean isUpdated() { - return this.updated; - } - public String getRealmId() { return realmId; } 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 new file mode 100644 index 0000000000..aed292b0fa --- /dev/null +++ b/model/map/src/test/java/org/keycloak/models/map/client/MapClientEntityClonerTest.java @@ -0,0 +1,115 @@ +/* + * 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.client; + +import org.keycloak.models.map.common.DeepCloner; +import java.util.Arrays; +import java.util.HashMap; +import org.junit.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; + +/** + * + * @author hmlnarik + */ +public class MapClientEntityClonerTest { + + private final static DeepCloner CLONER = new DeepCloner.Builder() + .constructorDC(MapClientEntityImpl.class, MapClientEntityImpl::new) + .constructor(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl::new) + .build(); + + @Test + public void testNewInstance() { + MapClientEntity newInstance = CLONER.newInstance(MapClientEntity.class); + assertThat(newInstance, instanceOf(MapClientEntityImpl.class)); + assertThat(newInstance.getId(), nullValue()); + } + + @Test + public void testNewInstanceWithId() { + MapClientEntity newInstance = CLONER.newInstance(MapClientEntity.class); + newInstance.setId("my-id"); + assertThat(newInstance, instanceOf(MapClientEntityImpl.class)); + assertThat(newInstance.getId(), is("my-id")); + } + + @Test + public void testCloneAsNewInstance() { + MapClientEntity newInstance = CLONER.newInstance(MapClientEntity.class); + newInstance.setId("my-id"); + newInstance.setClientId("a-client-id"); + newInstance.setAttribute("attr", Arrays.asList("aa", "bb", "cc")); + + MapClientEntity clonedInstance = CLONER.from(newInstance); + assertThat(clonedInstance, instanceOf(MapClientEntityImpl.class)); + assertThat(clonedInstance.getId(), is("my-id")); + 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.getAuthenticationFlowBindingOverrides(), nullValue()); + assertThat(clonedInstance.getRegistrationToken(), nullValue()); + } + + @Test + public void testCloneToExistingInstance() { + MapClientEntity newInstance = CLONER.newInstance(MapClientEntity.class); + 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/MapStorageTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/MapStorageTest.java index 0f00ffc20a..218def8c25 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/MapStorageTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/MapStorageTest.java @@ -117,11 +117,14 @@ public class MapStorageTest extends KeycloakModelTest { assertClientDoesNotExist(storage2, idMain, kcMain, kc2); assertClientDoesNotExist(storage2, id1, kc1, kc2); - MapClientEntity clientMain = new MapClientEntityImpl(idMain); + MapClientEntity clientMain = new MapClientEntityImpl(); + clientMain.setId(idMain); clientMain.setRealmId(realmId); - MapClientEntity client1 = new MapClientEntityImpl(id1); + MapClientEntity client1 = new MapClientEntityImpl(); + client1.setId(id1); client1.setRealmId(realmId); - MapClientEntity client2 = new MapClientEntityImpl(id2); + MapClientEntity client2 = new MapClientEntityImpl(); + client2.setId(id2); client2.setRealmId(realmId); clientMain = storageMain.create(clientMain);