KEYCLOAK-19482 Generate map entity cloners

This commit is contained in:
Hynek Mlnarik 2021-10-07 17:59:27 +02:00 committed by Hynek Mlnařík
parent 73f0474008
commit 8ee992e638
46 changed files with 907 additions and 283 deletions

View file

@ -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}).
* <p>
* 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:
* <pre>
* @GeneratedFieldType(HashSet) void addWebOrigin() { ... }
* </pre>
*
* @author hmlnarik
*/
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface GeneratedFieldType {
Class<?> value() default Void.class;
}

View file

@ -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 {
}

View file

@ -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<String> 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<Class<?>, Cloner<?>> CLONERS_WITH_ID = new java.util.HashMap<>();");
pw.println(" public static final java.util.Map<Class<?>, Cloner<?>> CLONERS_WITHOUT_ID = new java.util.HashMap<>();");
pw.println(" 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<String, HashSet<ExecutableElement>> methodsPerAttribute = e.getEnclosedElements().stream()
final List<? extends Element> allMembers = elements.getAllMembers(e);
Map<String, HashSet<ExecutableElement>> 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<String> {
private static final Comparator<String> ID_INSTANCE = new NameFirstComparator("id").thenComparing(Comparator.naturalOrder());
private static final Comparator<String> 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<String, HashSet<ExecutableElement>> 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<Field, Object> 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> 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<ExecutableElement> methods = me.getValue();
TypeMirror fieldType = determineFieldType(me.getKey(), methods);
if (fieldType == null) {
@ -364,6 +452,13 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor {
}
}
private Stream<ExecutableElement> fieldGetters(Map<String, HashSet<ExecutableElement>> 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<String, HashSet<ExecutableElement>> 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<ExecutableElement> methods = me.getValue();
TypeMirror fieldType = determineFieldType(fieldName, methods);
if (fieldType == null) {
return;
}
cloneField(e, fieldName, methods, fieldType, pw);
});
pw.println(" target.clearUpdatedFlag();");
pw.println(" return target;");
pw.println(" }");
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<ExecutableElement> 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<ExecutableElement> methods, TypeMirror fieldType, final PrintWriter pw) {
ExecutableElement getter = FieldAccessorType.getMethod(GETTER, methods, fieldName, types, fieldType).orElse(null);
if (getter == null) {
processingEnv.getMessager().printMessage(Kind.WARNING, "Could not determine getter for " + fieldName + " property");
return;
}
Optional<ExecutableElement> setter = FieldAccessorType.getMethod(SETTER, methods, fieldName, types, fieldType);
Optional<ExecutableElement> addToCollection = FieldAccessorType.getMethod(COLLECTION_ADD, methods, fieldName, types, fieldType);
Optional<ExecutableElement> updateMap = FieldAccessorType.getMethod(MAP_ADD, methods, fieldName, types, fieldType);
if (setter.isPresent()) {
final Name setterName = setter.get().getSimpleName();
// Setter always deep-clones whatever comes from the original, so we don't clone the value here.
pw.println(" target." + setterName + "(original." + getter.getSimpleName() + "());");
} else if (addToCollection.isPresent()) {
pw.println(" if (original." + getter.getSimpleName() + "() != null) {");
pw.println(" original." + getter.getSimpleName() + "().forEach(target::" + addToCollection.get().getSimpleName() + ");");
pw.println(" }");
} else if (updateMap.isPresent()) {
pw.println(" if (original." + getter.getSimpleName() + "() != null) {");
pw.println(" original." + getter.getSimpleName() + "().forEach(target::" + updateMap.get().getSimpleName() + ");");
pw.println(" }");
} else {
processingEnv.getMessager().printMessage(Kind.ERROR, "Could not determine way to clone " + fieldName + " property", e);
}
}
}
}

View file

@ -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;
}
}

View file

@ -119,7 +119,7 @@ public class MapRootAuthenticationSessionAdapter extends AbstractRootAuthenticat
}
public void setUpdated(boolean updated) {
entity.updated |= updated;
entity.signalUpdated(updated);
}
private String generateTabId() {

View file

@ -26,7 +26,7 @@ import java.util.concurrent.ConcurrentHashMap;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
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<String, MapAuthenticationSessionEntity> 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;
}
}

View file

@ -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));

View file

@ -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<String> resourceIds = new HashSet<>();
private final Set<String> 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));

View file

@ -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<String> scopeIds = new HashSet<>();
private final Set<String> policyIds = new HashSet<>();
private final Map<String, List<String>> 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));

View file

@ -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));

View file

@ -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));

View file

@ -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> V deepClone(V obj) {
return Serialization.from(obj);
}
@Override
public Stream<String> getClientScopes(boolean defaultScope) {
final Map<String, Boolean> clientScopes = getClientScopes();

View file

@ -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);

View file

@ -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();

View file

@ -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;
}

View file

@ -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.
* <p>
* This class is intended to be used by individual map storage implementations for copying
* over entities into their native implementations.
* <p>
* 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.
* <p>
* <i>Broader context:</i>
* 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 <V> Object class
*/
@FunctionalInterface
public interface Cloner<V> {
/**
* 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<Class<?>, Supplier<?>> parameterlessConstructors = new HashMap<>();
private final Map<Class<?>, Function<DeepCloner, ?>> constructors = new HashMap<>();
private final Map<Class<?>, Cloner> clonersWithId = new HashMap<>(org.keycloak.models.map.common.AutogeneratedCloners.CLONERS_WITH_ID);
private final Map<Class<?>, Cloner> 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 ? "<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 <V> void forThisClassAndAllMarkedParentsAndInterfaces(Class<V> rootClazz, Consumer<Class<?>> action) {
action.accept(rootClazz);
Stack<Class<?>> 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 <V> Class or interface that would be instantiated by the given methods
* @param clazz Class or interface that would be instantiated by the given methods
* @param constructorNoParameters Parameterless function that creates a new instance of class {@code V}.
* If {@code null}, parameterless constructor is not available.
* @return This builder.
*/
public <V> Builder constructor(Class<V> clazz, Supplier<? extends V> 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 <V> Class or interface that would be instantiated by the given methods
* @param clazz Class or interface that would be instantiated by the given methods
* @param constructor Function that creates a new instance of class {@code V}.
* If {@code null}, such a single-parameter constructor is not available.
* @return This builder.
*/
public <V> Builder constructorDC(Class<V> clazz, Function<DeepCloner, ? extends V> 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 <V> 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 <V> Builder cloner(Class<? extends V> 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 <V> 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 <V> Builder cloner(Class<? extends V> 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 <V> 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 <V> Builder genericCloner(Cloner<V> genericCloner) {
this.genericCloner = genericCloner;
return this;
}
}
private static final Logger LOG = Logger.getLogger(DeepCloner.class);
private final Map<Class<?>, Supplier<?>> parameterlessConstructors;
private final Map<Class<?>, Function<DeepCloner, ?>> constructors;
private final Map<Class<?>, Cloner> clonersWithId;
private final Map<Class<?>, Cloner> clonersWithoutId;
private final Cloner<?> genericCloner;
private DeepCloner(Map<Class<?>, Supplier<?>> parameterlessConstructors,
Map<Class<?>, Function<DeepCloner, ?>> constructors,
Map<Class<?>, Cloner> clonersWithId,
Map<Class<?>, Cloner> clonersWithoutId,
Cloner<?> genericCloner) {
this.parameterlessConstructors = parameterlessConstructors;
this.constructors = constructors;
this.clonersWithId = clonersWithId;
this.clonersWithoutId = clonersWithoutId;
this.genericCloner = genericCloner;
}
private <V> V getFromClassRespectingHierarchy(Map<Class<?>, 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<Class<?>> 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 <V> 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> V newInstance(Class<V> clazz) {
if (clazz == null) {
return null;
}
V res;
@SuppressWarnings("unchecked")
Function<DeepCloner, V> c = (Function<DeepCloner, V>) getFromClassRespectingHierarchy(this.constructors, clazz);
if (c == null) {
@SuppressWarnings("unchecked")
Supplier<V> s = (Supplier<V>) 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 <V> 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> 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 <V> 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> V deepCloneNoId(V from, V to) {
return deepClone(from, to, this.clonersWithoutId);
}
@SuppressWarnings("unchecked")
private <V> V deepClone(V from, V to, Map<Class<?>, Cloner> cloners) {
Cloner<V> cloner = (Cloner<V>) 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<V>) 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 <V> 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 extends AbstractEntity> V from(String newId, V from) {
if (from == null) {
return null;
}
final V res = newInstance((Class<V>) 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 <V> 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> V from(V from) {
return from == null ? null : deepClone(from, newInstance((Class<V>) from.getClass()));
}
/**
* Issues warning in the logs and returns the input parameter {@code o}
* @param o
* @return The {@code o} object
*/
public static <T> T warnCloneNotSupported(T o) {
if (o != null) {
LOG.warnf("Cloning not supported for %s, returning the same instance!", o.getClass());
}
return o;
}
}

View file

@ -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<Class<?>, ObjectReader> READERS = new ConcurrentHashMap<>();
public static final ConcurrentHashMap<Class<?>, 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> T from(T orig, T target) {
if (orig == null) {
return null;
}
@SuppressWarnings("unchecked")
final Class<T> origClass = (Class<T>) 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);
}
}
}

View file

@ -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 {

View file

@ -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;
}

View file

@ -24,16 +24,11 @@ import java.util.Objects;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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<String, String> 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;
}

View file

@ -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;
}

View file

@ -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<String, String> 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;
}

View file

@ -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<String, String> 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;
}

View file

@ -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<String, String> 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;
}

View file

@ -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;
}

View file

@ -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<String, String> 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;
}

View file

@ -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;
}

View file

@ -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<String> 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;
}

View file

@ -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;
}

View file

@ -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<K, V extends AbstractEntity &
private final static Logger log = Logger.getLogger(ConcurrentHashMapKeycloakTransaction.class);
private boolean active;
private boolean rollback;
private final Map<String, MapTaskWithValue> tasks = new LinkedHashMap<>();
private final MapStorage<V, M> map;
private final StringKeyConvertor<K> keyConvertor;
protected boolean active;
protected boolean rollback;
protected final Map<String, MapTaskWithValue> tasks = new LinkedHashMap<>();
protected final MapStorage<V, M> map;
protected final StringKeyConvertor<K> keyConvertor;
protected final DeepCloner cloner;
enum MapOperation {
CREATE, UPDATE, DELETE,
}
public ConcurrentHashMapKeycloakTransaction(MapStorage<V, M> map, StringKeyConvertor<K> keyConvertor) {
public ConcurrentHashMapKeycloakTransaction(MapStorage<V, M> map, StringKeyConvertor<K> keyConvertor, DeepCloner cloner) {
this.map = map;
this.keyConvertor = keyConvertor;
this.cloner = cloner;
}
@Override
@ -119,7 +121,7 @@ public class ConcurrentHashMapKeycloakTransaction<K, V extends AbstractEntity &
return current.getValue();
}
// Else enlist its copy in the transaction. Never return direct reference to the underlying map
final V res = Serialization.from(origEntity);
final V res = cloner.from(origEntity);
return updateIfChanged(res, e -> e.isUpdated());
}
@ -212,10 +214,11 @@ public class ConcurrentHashMapKeycloakTransaction<K, V extends AbstractEntity &
public V create(V value) {
String key = value.getId();
if (key == null) {
value = Serialization.from(value);
K newKey = keyConvertor.yieldNewUniqueKey();
key = keyConvertor.keyToString(newKey);
value.setId(key);
value = cloner.from(key, value);
} else {
value = cloner.from(value);
}
addTask(key, new CreateOperation(value));
return value;

View file

@ -20,7 +20,7 @@ import org.keycloak.models.map.common.StringKeyConvertor;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
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 org.keycloak.models.map.storage.MapStorage;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
@ -46,24 +46,25 @@ import static org.keycloak.utils.StreamsUtil.paginatedStream;
*/
public class ConcurrentHashMapStorage<K, V extends AbstractEntity & UpdatableEntity, M> implements MapStorage<V, M> {
private final ConcurrentMap<K, V> store = new ConcurrentHashMap<>();
protected final ConcurrentMap<K, V> store = new ConcurrentHashMap<>();
private final Map<SearchableModelField<M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates;
private final StringKeyConvertor<K> keyConvertor;
protected final Map<SearchableModelField<M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates;
protected final StringKeyConvertor<K> keyConvertor;
protected final DeepCloner cloner;
@SuppressWarnings("unchecked")
public ConcurrentHashMapStorage(Class<M> modelClass, StringKeyConvertor<K> keyConvertor) {
public ConcurrentHashMapStorage(Class<M> modelClass, StringKeyConvertor<K> 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<K, V extends AbstractEntity & UpdatableEnt
return res;
}
@SuppressWarnings("unchecked")
MapModelCriteriaBuilder<K, V, M> b = criteria.unwrap(MapModelCriteriaBuilder.class);
if (b == null) {
throw new IllegalStateException("Incompatible class: " + criteria.getClass());
@ -130,7 +132,7 @@ public class ConcurrentHashMapStorage<K, V extends AbstractEntity & UpdatableEnt
@SuppressWarnings("unchecked")
public MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session) {
MapKeycloakTransaction<V, M> 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<K> getKeyConvertor() {
@ -146,6 +148,7 @@ public class ConcurrentHashMapStorage<K, V extends AbstractEntity & UpdatableEnt
}
Stream<Entry<K, V>> stream = store.entrySet().stream();
@SuppressWarnings("unchecked")
MapModelCriteriaBuilder<K, V, M> b = criteria.unwrap(MapModelCriteriaBuilder.class);
if (b == null) {
throw new IllegalStateException("Incompatible class: " + criteria.getClass());

View file

@ -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<Class<?>, 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 <K, V extends AbstractEntity & UpdatableEntity, M> ConcurrentHashMapStorage<K, V, M> loadMap(String mapName,
Class<M> modelType, EnumSet<Flag> 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<K, V, M> 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 + ")";

View file

@ -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);

View file

@ -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<K> extends ConcurrentHashMapSto
private final MapKeycloakTransaction<MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionTr;
public Transaction(MapKeycloakTransaction<MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionTr, StringKeyConvertor<K> keyConvertor) {
super(UserSessionConcurrentHashMapStorage.this, keyConvertor);
public Transaction(MapKeycloakTransaction<MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionTr, StringKeyConvertor<K> keyConvertor, DeepCloner cloner) {
super(UserSessionConcurrentHashMapStorage.this, keyConvertor, cloner);
this.clientSessionTr = clientSessionTr;
}
@ -70,8 +71,8 @@ public class UserSessionConcurrentHashMapStorage<K> extends ConcurrentHashMapSto
@SuppressWarnings("unchecked")
public UserSessionConcurrentHashMapStorage(ConcurrentHashMapStorage<K, MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionStore,
StringKeyConvertor<K> keyConvertor) {
super(UserSessionModel.class, keyConvertor);
StringKeyConvertor<K> keyConvertor, DeepCloner cloner) {
super(UserSessionModel.class, keyConvertor, cloner);
this.clientSessionStore = clientSessionStore;
}
@ -79,6 +80,6 @@ public class UserSessionConcurrentHashMapStorage<K> extends ConcurrentHashMapSto
@SuppressWarnings("unchecked")
public MapKeycloakTransaction<MapUserSessionEntity, UserSessionModel> createTransaction(KeycloakSession session) {
MapKeycloakTransaction<MapUserSessionEntity, UserSessionModel> 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;
}
}

View file

@ -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;

View file

@ -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<String> 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;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -27,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
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;
}

View file

@ -30,7 +30,7 @@ import java.util.concurrent.ConcurrentHashMap;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
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;
}

View file

@ -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());
}
}

View file

@ -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);