KEYCLOAK-19482 Generate map entity cloners
This commit is contained in:
parent
73f0474008
commit
8ee992e638
46 changed files with 907 additions and 283 deletions
|
@ -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;
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -119,7 +119,7 @@ public class MapRootAuthenticationSessionAdapter extends AbstractRootAuthenticat
|
|||
}
|
||||
|
||||
public void setUpdated(boolean updated) {
|
||||
entity.updated |= updated;
|
||||
entity.signalUpdated(updated);
|
||||
}
|
||||
|
||||
private String generateTabId() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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 + ")";
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue