KEYCLOAK-19505 Generate map entity delegates

This commit is contained in:
Hynek Mlnarik 2021-10-07 17:59:27 +02:00 committed by Hynek Mlnařík
parent 5b0986e490
commit 675e1b0941
16 changed files with 717 additions and 681 deletions

View file

@ -28,5 +28,5 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface GenerateEntityImplementations {
String inherits() default "";
String inherits() default "org.keycloak.models.map.common.UpdatableEntity.Impl";
}

View file

@ -0,0 +1,121 @@
/*
* 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.processor;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Name;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import static org.keycloak.models.map.processor.Util.getGenericsDeclaration;
/**
*
* @author hmlnarik
*/
enum FieldAccessorType {
GETTER {
@Override
public boolean is(ExecutableElement method, String fieldName, Types types, TypeMirror fieldType) {
Pattern getter = Pattern.compile("(get|is)" + Pattern.quote(fieldName));
Name methodName = method.getSimpleName();
return getter.matcher(methodName).matches() && method.getParameters().isEmpty() && types.isSameType(fieldType, method.getReturnType());
}
},
SETTER {
@Override
public boolean is(ExecutableElement method, String fieldName, Types types, TypeMirror fieldType) {
String methodName = "set" + fieldName;
return Objects.equals(methodName, method.getSimpleName().toString())
&& method.getParameters().size() == 1
&& types.isSameType(fieldType, method.getParameters().get(0).asType());
}
},
COLLECTION_ADD {
@Override
public boolean is(ExecutableElement method, String fieldName, Types types, TypeMirror fieldType) {
String fieldNameSingular = fieldName.endsWith("s") ? fieldName.substring(0, fieldName.length() - 1) : fieldName;
String methodName = "add" + fieldNameSingular;
List<TypeMirror> res = getGenericsDeclaration(fieldType);
return Objects.equals(methodName, method.getSimpleName().toString())
&& res.size() == 1
&& types.isSameType(res.get(0), method.getParameters().get(0).asType());
}
},
COLLECTION_DELETE {
@Override
public boolean is(ExecutableElement method, String fieldName, Types types, TypeMirror fieldType) {
String fieldNameSingular = fieldName.endsWith("s") ? fieldName.substring(0, fieldName.length() - 1) : fieldName;
String removeFromCollection = "remove" + fieldNameSingular;
List<TypeMirror> res = getGenericsDeclaration(fieldType);
return Objects.equals(removeFromCollection, method.getSimpleName().toString())
&& method.getParameters().size() == 1
&& types.isSameType(res.get(0), method.getParameters().get(0).asType());
}
},
MAP_ADD {
@Override
public boolean is(ExecutableElement method, String fieldName, Types types, TypeMirror fieldType) {
String fieldNameSingular = fieldName.endsWith("s") ? fieldName.substring(0, fieldName.length() - 1) : fieldName;
String methodName = "set" + fieldNameSingular;
List<TypeMirror> res = getGenericsDeclaration(fieldType);
return Objects.equals(methodName, method.getSimpleName().toString())
&& res.size() == 2
&& types.isSameType(res.get(0), method.getParameters().get(0).asType())
&& types.isSameType(res.get(1), method.getParameters().get(1).asType());
}
},
MAP_GET {
@Override
public boolean is(ExecutableElement method, String fieldName, Types types, TypeMirror fieldType) {
String fieldNameSingular = fieldName.endsWith("s") ? fieldName.substring(0, fieldName.length() - 1) : fieldName;
String methodName = "get" + fieldNameSingular;
List<TypeMirror> res = getGenericsDeclaration(fieldType);
return Objects.equals(methodName, method.getSimpleName().toString())
&& res.size() == 2
&& types.isSameType(res.get(0), method.getParameters().get(0).asType());
}
},
UNKNOWN /* Must be the last */ {
@Override
public boolean is(ExecutableElement method, String fieldName, Types types, TypeMirror fieldType) {
return true;
}
}
;
public abstract boolean is(ExecutableElement method, String fieldName, Types types, TypeMirror fieldType);
public static Optional<ExecutableElement> getMethod(FieldAccessorType type,
HashSet<ExecutableElement> methods, String fieldName, Types types, TypeMirror fieldType) {
return methods.stream().filter(ee -> type.is(ee, fieldName, types, fieldType)).findAny();
}
public static FieldAccessorType determineType(ExecutableElement method, String fieldName, Types types, TypeMirror fieldType) {
for (FieldAccessorType fat : values()) {
if (fat.is(method, fieldName, types, fieldType)) {
return fat;
}
}
return UNKNOWN;
}
}

View file

@ -17,19 +17,15 @@
package org.keycloak.models.map.processor;
import org.keycloak.models.map.annotations.GenerateEntityImplementations;
import org.keycloak.models.map.annotations.GenerateEnumMapFieldType;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@ -44,13 +40,21 @@ import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.NoType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic.Kind;
import javax.tools.JavaFileObject;
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.Comparator;
import java.util.IdentityHashMap;
import java.util.Optional;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
/**
*
@ -60,8 +64,24 @@ import javax.tools.JavaFileObject;
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GenerateEntityImplementationsProcessor extends AbstractProcessor {
private static interface Generator {
void generate(TypeElement e, Map<String, HashSet<ExecutableElement>> methodsPerAttribute) throws IOException;
}
private Elements elements;
private Types types;
private final Generator[] generators = new Generator[] {
new DelegateGenerator(),
new FieldsGenerator(),
new ImplGenerator()
};
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
elements = processingEnv.getElementUtils();
types = processingEnv.getTypeUtils();
for (TypeElement annotation : annotations) {
Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(annotation);
annotatedElements.stream()
@ -74,7 +94,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor {
private void processTypeElement(TypeElement e) {
if (e.getKind() != ElementKind.INTERFACE) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Annotation @GenerateEntityImplementations is only applicable to interface", e);
processingEnv.getMessager().printMessage(Kind.ERROR, "Annotation @GenerateEntityImplementations is only applicable to an interface", e);
return;
}
@ -94,10 +114,12 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor {
methodsPerAttribute.get(key + "s").addAll(removed);
});
try {
generateImpl(e, methodsPerAttribute);
} catch (IOException ex) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Could not generate implementation for class", e);
for (Generator generator : this.generators) {
try {
generator.generate(e, methodsPerAttribute);
} catch (Exception ex) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Could not generate implementation for class: " + ex, e);
}
}
// methodsPerAttribute.entrySet().stream()
@ -130,83 +152,6 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor {
return null;
}
private void generateImpl(TypeElement e, Map<String, HashSet<ExecutableElement>> methodsPerAttribute) throws IOException {
GenerateEntityImplementations an = e.getAnnotation(GenerateEntityImplementations.class);
Elements elements = processingEnv.getElementUtils();
TypeElement parentTypeElement = elements.getTypeElement(an.inherits().isEmpty() ? "void" : an.inherits());
final List<? extends Element> allMembers = elements.getAllMembers(parentTypeElement);
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 mapImplClassName = className + "Impl";
String mapSimpleClassName = simpleClassName + "Impl";
JavaFileObject enumFile = processingEnv.getFiler().createSourceFile(mapImplClassName);
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 java.util.EnumMap;");
pw.println("import java.util.Objects;");
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(" public enum Field {");
methodsPerAttribute.keySet().stream()
.sorted()
.map(GenerateEntityImplementationsProcessor::toEnumConstant)
.forEach(key -> pw.println(" " + key + ","));
pw.println(" }");
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) -> pw.println(" public " + mapSimpleClassName + "(" + methodParameters(ee.getParameters()) + ") { super(" + ee.getParameters() + "); }"));
for (Entry<String, HashSet<ExecutableElement>> me : methodsPerAttribute.entrySet()) {
String enumConstant = toEnumConstant(me.getKey());
HashSet<ExecutableElement> methods = me.getValue();
TypeMirror fieldType = determineFieldType(me.getKey(), methods);
if (fieldType == null) {
continue;
}
for (ExecutableElement method : methods) {
if (! printMethodBody(pw, method, me.getKey(), enumConstant, fieldType)) {
List<ExecutableElement> parentMethods = allMembers.stream()
.filter(ExecutableElement.class::isInstance)
.map(ExecutableElement.class::cast)
.filter(ee -> Objects.equals(ee.toString(), method.toString()))
.filter((ExecutableElement ee) -> ! ee.getModifiers().contains(Modifier.ABSTRACT))
.collect(Collectors.toList());
if (! parentMethods.isEmpty()) {
processingEnv.getMessager().printMessage(Kind.OTHER, "Method " + method + " is declared in a parent class.");
} else {
processingEnv.getMessager().printMessage(Kind.WARNING, "Could not determine desired semantics of method from its signature", method);
}
}
}
}
pw.println("}");
}
}
protected static String toEnumConstant(String key) {
return key.replaceAll("([a-z])([A-Z])", "$1_$2").toUpperCase();
}
@ -220,109 +165,343 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor {
}
}
if (res == null) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Could not determine return type for field " + fieldName, methods.iterator().next());
processingEnv.getMessager().printMessage(Kind.ERROR, "Could not determine return type for the field " + fieldName, methods.iterator().next());
}
return res;
}
private boolean printMethodBody(PrintWriter pw, ExecutableElement method, String fieldName, String enumConstant, TypeMirror fieldType) {
Pattern getter = Pattern.compile("(get|is)" + Pattern.quote(fieldName));
Types types = processingEnv.getTypeUtils();
final String methodName = method.getSimpleName().toString();
String setter = "set" + fieldName;
TypeMirror firstParameterType = method.getParameters().isEmpty()
? types.getNullType()
: method.getParameters().get(0).asType();
String fieldNameSingular = fieldName.endsWith("s") ? fieldName.substring(0, fieldName.length() - 1) : fieldName;
String getFromMap = "get" + fieldNameSingular;
String addToCollection = "add" + fieldNameSingular;
String updateMap = "set" + fieldNameSingular;
String removeFromCollection = "remove" + fieldNameSingular;
Elements elements = processingEnv.getElementUtils();
TypeElement typeElement = elements.getTypeElement(types.erasure(fieldType).toString());
if (getter.matcher(methodName).matches() && method.getParameters().isEmpty() && types.isSameType(fieldType, method.getReturnType())) {
pw.println(" @Override public " + method.getReturnType() + " " + method + " {");
pw.println(" return (" + fieldType + ") get(Field." + enumConstant + ");");
pw.println(" }");
return true;
} else if (setter.equals(methodName) && types.isSameType(firstParameterType, fieldType)) {
pw.println(" @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
pw.println(" Object o = set(Field." + enumConstant + ", p0);");
pw.println(" updated |= ! Objects.equals(o, p0);");
pw.println(" }");
return true;
} else if (addToCollection.equals(methodName) && method.getParameters().size() == 1) {
pw.println(" @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
pw.println(" " + fieldType + " o = (" + fieldType + ") get(Field." + enumConstant + ");");
pw.println(" if (o == null) { o = " + interfaceToImplementation(typeElement) + "; set(Field." + enumConstant + ", o); }");
if (isSetType(typeElement)) {
pw.println(" updated |= o.add(p0);");
} else {
pw.println(" o.add(p0);");
pw.println(" updated = true;");
}
pw.println(" }");
return true;
} else if (removeFromCollection.equals(methodName) && method.getParameters().size() == 1) {
pw.println(" @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
pw.println(" " + fieldType + " o = (" + fieldType + ") get(Field." + enumConstant + ");");
pw.println(" if (o == null) { return; }");
pw.println(" boolean removed = o.remove(p0)" + ("java.util.Map".equals(typeElement.getQualifiedName().toString()) ? " != null" : "") + ";");
pw.println(" updated |= removed;");
pw.println(" }");
return true;
} else if (updateMap.equals(methodName) && method.getParameters().size() == 2) {
pw.println(" @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0, " + method.getParameters().get(1).asType() + " p1) {");
pw.println(" " + fieldType + " o = (" + fieldType + ") get(Field." + enumConstant + ");");
pw.println(" if (o == null) { o = " + interfaceToImplementation(typeElement) + "; set(Field." + enumConstant + ", o); }");
pw.println(" Object v = o.put(p0, p1);");
pw.println(" updated |= ! Objects.equals(v, p1);");
pw.println(" }");
return true;
} else if (getFromMap.equals(methodName) && method.getParameters().size() == 1) {
pw.println(" @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
pw.println(" " + fieldType + " o = (" + fieldType + ") get(Field." + enumConstant + ");");
pw.println(" return o == null ? null : o.get(p0);");
pw.println(" }");
return true;
}
return false;
private boolean isImmutableFinalType(TypeMirror fieldType) {
return isPrimitiveType(fieldType) || isBoxedPrimitiveType(fieldType) || Objects.equals("java.lang.String", fieldType.toString());
}
private String interfaceToImplementation(TypeElement typeElement) {
GenerateEnumMapFieldType an = typeElement.getAnnotation(GenerateEnumMapFieldType.class);
if (an != null) {
return "new " + an.value().getCanonicalName() + "<>()";
private boolean isKnownCollectionOfImmutableFinalTypes(TypeMirror fieldType) {
TypeElement typeElement = elements.getTypeElement(types.erasure(fieldType).toString());
switch (typeElement.getQualifiedName().toString()) {
case "java.util.List":
case "java.util.Map":
case "java.util.Set":
case "java.util.Collection":
case "org.keycloak.common.util.MultivaluedHashMap":
List<TypeMirror> res = getGenericsDeclaration(fieldType);
return res.stream().allMatch(tm -> isImmutableFinalType(tm) || isKnownCollectionOfImmutableFinalTypes(tm));
default:
return false;
}
}
private boolean isPrimitiveType(TypeMirror fieldType) {
try {
types.getPrimitiveType(fieldType.getKind());
return true;
} catch (IllegalArgumentException ex) {
return false;
}
}
private boolean isBoxedPrimitiveType(TypeMirror fieldType) {
try {
types.unboxedType(fieldType);
return true;
} catch (IllegalArgumentException ex) {
return false;
}
}
private String interfaceToImplementation(TypeElement typeElement, String parameter) {
Name parameterTypeQN = typeElement.getQualifiedName();
switch (parameterTypeQN.toString()) {
case "java.util.List":
return "new java.util.LinkedList<>()";
case "java.util.Map":
return "new java.util.HashMap<>()";
case "java.util.Set":
return "new java.util.HashSet<>()";
case "java.util.Collection":
return "new java.util.LinkedList<>()";
return "new java.util.LinkedList<>(" + parameter + ")";
case "java.util.Map":
return "new java.util.HashMap<>(" + parameter + ")";
case "java.util.Set":
return "new java.util.HashSet<>(" + parameter + ")";
default:
processingEnv.getMessager().printMessage(Kind.ERROR, "Could not determine implementation for type " + typeElement, typeElement);
return "TODO()";
}
}
private String methodParameters(List<? extends VariableElement> parameters) {
return parameters.stream()
.map(p -> p.asType() + " " + p.getSimpleName())
.collect(Collectors.joining(", "));
private class FieldsGenerator 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 mapFieldsClassName = className + "Fields";
String mapSimpleFieldsClassName = simpleClassName + "Fields";
JavaFileObject file = processingEnv.getFiler().createSourceFile(mapFieldsClassName);
try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) {
if (packageName != null) {
pw.println("package " + packageName + ";");
}
pw.println("public enum " + mapSimpleFieldsClassName + " {");
methodsPerAttribute.keySet().stream()
.sorted()
.map(GenerateEntityImplementationsProcessor::toEnumConstant)
.forEach(key -> pw.println(" " + key + ","));
pw.println("}");
}
}
}
private static final HashSet<String> SET_TYPES = new HashSet<>(Arrays.asList(Set.class.getCanonicalName(), TreeSet.class.getCanonicalName(), HashSet.class.getCanonicalName(), LinkedHashSet.class.getCanonicalName()));
private class ImplGenerator implements Generator {
private boolean isSetType(TypeElement typeElement) {
Name name = typeElement.getQualifiedName();
return SET_TYPES.contains(name.toString());
@Override
public void generate(TypeElement e, Map<String, HashSet<ExecutableElement>> methodsPerAttribute) throws IOException {
GenerateEntityImplementations an = e.getAnnotation(GenerateEntityImplementations.class);
TypeElement parentTypeElement = elements.getTypeElement((an.inherits() == null || an.inherits().isEmpty()) ? "void" : an.inherits());
if (parentTypeElement == null) {
return;
}
final List<? extends Element> allMembers = elements.getAllMembers(parentTypeElement);
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 mapImplClassName = className + "Impl";
String mapSimpleClassName = simpleClassName + "Impl";
boolean hasId = methodsPerAttribute.containsKey("Id") || allMembers.stream().anyMatch(el -> "getId".equals(el.getSimpleName().toString()));
JavaFileObject file = processingEnv.getFiler().createSourceFile(mapImplClassName);
try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) {
if (packageName != null) {
pw.println("package " + packageName + ";");
}
pw.println("import java.util.Objects;");
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); }");
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)
.map(ExecutableElement::getSimpleName)
.map(Name::toString)
.sorted()
.map(v -> "Objects.equals(" + v + "(), other." + v + "())")
.collect(Collectors.joining("\n && "))
+ ";");
pw.println(" }");
pw.println(" @Override public int hashCode() {");
pw.println(" return "
+ (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)
.filter(ee -> isImmutableFinalType(ee.getReturnType()))
.map(ExecutableElement::getSimpleName)
.map(Name::toString)
.sorted()
.map(v -> v + "()")
.collect(Collectors.joining(",\n "))
+ ")")
+ ";");
pw.println(" }");
pw.println(" @Override public String toString() {");
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() + "); }"));
methodsPerAttribute.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)).forEach(me -> {
HashSet<ExecutableElement> methods = me.getValue();
TypeMirror fieldType = determineFieldType(me.getKey(), methods);
if (fieldType == null) {
return;
}
pw.println("");
pw.println(" private " + fieldType + " f" + me.getKey() + ";");
for (ExecutableElement method : methods) {
FieldAccessorType fat = FieldAccessorType.determineType(method, me.getKey(), types, fieldType);
Optional<ExecutableElement> parentMethod = allMembers.stream()
.filter(ExecutableElement.class::isInstance)
.map(ExecutableElement.class::cast)
.filter(ee -> Objects.equals(ee.toString(), method.toString()))
.filter((ExecutableElement ee) -> ! ee.getModifiers().contains(Modifier.ABSTRACT))
.findAny();
if (parentMethod.isPresent()) {
processingEnv.getMessager().printMessage(Kind.OTHER, "Method " + method + " is declared in a parent class.", method);
} else if (fat != FieldAccessorType.UNKNOWN && ! printMethodBody(pw, fat, method, "f" + me.getKey(), fieldType)) {
processingEnv.getMessager().printMessage(Kind.WARNING, "Could not determine desired semantics of method from its signature", method);
}
}
});
pw.println("}");
}
}
private boolean printMethodBody(PrintWriter pw, FieldAccessorType accessorType, ExecutableElement method, String fieldName, TypeMirror fieldType) {
TypeMirror firstParameterType = method.getParameters().isEmpty()
? types.getNullType()
: method.getParameters().get(0).asType();
TypeElement typeElement = elements.getTypeElement(types.erasure(fieldType).toString());
switch (accessorType) {
case GETTER:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method + " {");
pw.println(" return " + fieldName + ";");
pw.println(" }");
return true;
case SETTER:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
if (! isImmutableFinalType(fieldType)) {
pw.println(" p0 = " + deepClone(fieldType, "p0") + ";");
}
pw.println(" updated |= ! Objects.equals(" + fieldName + ", p0);");
pw.println(" " + fieldName + " = p0;");
pw.println(" }");
return true;
case COLLECTION_ADD:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
pw.println(" if (" + fieldName + " == null) { " + fieldName + " = " + interfaceToImplementation(typeElement, "") + "; }");
if (! isImmutableFinalType(firstParameterType)) {
pw.println(" p0 = " + deepClone(fieldType, "p0") + ";");
}
if (isSetType(typeElement)) {
pw.println(" updated |= " + fieldName + ".add(p0);");
} else {
pw.println(" " + fieldName + ".add(p0);");
pw.println(" updated = true;");
}
pw.println(" }");
return true;
case COLLECTION_DELETE:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
pw.println(" if (" + fieldName + " == null) { return; }");
pw.println(" boolean removed = " + fieldName + ".remove(p0)" + ("java.util.Map".equals(typeElement.getQualifiedName().toString()) ? " != null" : "") + ";");
pw.println(" updated |= removed;");
pw.println(" }");
return true;
case MAP_ADD:
TypeMirror secondParameterType = method.getParameters().get(1).asType();
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0, " + secondParameterType + " p1) {");
pw.println(" if (" + fieldName + " == null) { " + fieldName + " = " + interfaceToImplementation(typeElement, "") + "; }");
if (! isImmutableFinalType(secondParameterType)) {
pw.println(" p1 = " + deepClone(secondParameterType, "p1") + ";");
}
pw.println(" Object v = " + fieldName + ".put(p0, p1);");
pw.println(" updated |= ! Objects.equals(v, p1);");
pw.println(" }");
return true;
case MAP_GET:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
pw.println(" return " + fieldName + " == null ? null : " + fieldName + ".get(p0);");
pw.println(" }");
return true;
}
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 {
@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 mapClassName = className + "Delegate";
String mapSimpleClassName = simpleClassName + "Delegate";
String fieldsClassName = className + "Fields";
GenerateEntityImplementations an = e.getAnnotation(GenerateEntityImplementations.class);
TypeElement parentTypeElement = elements.getTypeElement((an.inherits() == null || an.inherits().isEmpty()) ? "void" : an.inherits());
if (parentTypeElement == null) {
return;
}
final List<? extends Element> allMembers = elements.getAllMembers(e);
JavaFileObject file = processingEnv.getFiler().createSourceFile(mapClassName);
IdentityHashMap<ExecutableElement, String> m2field = new IdentityHashMap<>();
methodsPerAttribute.forEach((f, s) -> s.forEach(m -> m2field.put(m, f))); // Create reverse map
try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) {
if (packageName != null) {
pw.println("package " + packageName + ";");
}
pw.println("public class " + mapSimpleClassName + " implements " + className + " {");
pw.println(" private final org.keycloak.models.map.common.delegate.DelegateProvider<" + mapSimpleClassName + "> delegateProvider;");
pw.println(" public " + mapSimpleClassName + "(org.keycloak.models.map.common.delegate.DelegateProvider<" + mapSimpleClassName + "> delegateProvider) {");
pw.println(" this.delegateProvider = delegateProvider;");
pw.println(" }");
allMembers.stream()
.filter(m -> m.getKind() == ElementKind.METHOD)
.filter(ExecutableElement.class::isInstance)
.map(ExecutableElement.class::cast)
.filter(ee -> ee.getModifiers().contains(Modifier.ABSTRACT))
.forEach(ee -> {
pw.println(" @Override "
+ ee.getModifiers().stream().filter(m -> m != Modifier.ABSTRACT).map(Object::toString).collect(Collectors.joining(" "))
+ " " + ee.getReturnType()
+ " " + ee.getSimpleName()
+ "(" + methodParameters(ee.getParameters()) + ") {");
String field = m2field.get(ee);
field = field == null ? "null" : fieldsClassName + "." + toEnumConstant(field);
if (ee.getReturnType().getKind() == TypeKind.BOOLEAN && "isUpdated".equals(ee.getSimpleName().toString())) {
pw.println(" return delegateProvider.isUpdated();");
} else if (ee.getReturnType().getKind() == TypeKind.VOID) { // write operation
pw.println(" delegateProvider.getDelegate(false, " + field + ")." + ee.getSimpleName() + "("
+ ee.getParameters().stream().map(VariableElement::getSimpleName).collect(Collectors.joining(", "))
+ ");");
} else {
pw.println(" return delegateProvider.getDelegate(true, " + field + ")." + ee.getSimpleName() + "("
+ ee.getParameters().stream().map(VariableElement::getSimpleName).collect(Collectors.joining(", "))
+ ");");
}
pw.println(" }");
});
pw.println("}");
}
}
}
}

View file

@ -14,23 +14,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.client;
package org.keycloak.models.map.processor;
import java.io.PrintWriter;
import java.io.Writer;
/**
*
* @author hmlnarik
*/
public class MapClientEntityDelegate extends MapClientEntityLazyDelegate {
public class PrintWriterNoJavaLang extends PrintWriter {
private final MapClientEntity delegate;
public MapClientEntityDelegate(MapClientEntity delegate) {
super(null);
this.delegate = delegate;
public PrintWriterNoJavaLang(Writer out) {
super(out);
}
@Override
protected MapClientEntity getWriteDelegate() {
return delegate;
public void println(String x) {
super.println(x == null ? x : x.replaceAll("java.lang.", ""));
}
}

View file

@ -0,0 +1,68 @@
/*
* 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.processor;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.SimpleTypeVisitor8;
/**
*
* @author hmlnarik
*/
public class Util {
private static final HashSet<String> SET_TYPES = new HashSet<>(Arrays.asList(Set.class.getCanonicalName(), TreeSet.class.getCanonicalName(), HashSet.class.getCanonicalName(), LinkedHashSet.class.getCanonicalName()));
public static List<TypeMirror> getGenericsDeclaration(TypeMirror fieldType) {
List<TypeMirror> res = new LinkedList<>();
fieldType.accept(new SimpleTypeVisitor8<Void, List<TypeMirror>>() {
@Override
public Void visitDeclared(DeclaredType t, List<TypeMirror> p) {
List<? extends TypeMirror> typeArguments = t.getTypeArguments();
res.addAll(typeArguments);
return null;
}
}, res);
return res;
}
public static String methodParameters(List<? extends VariableElement> parameters) {
return parameters.stream()
.map(p -> p.asType() + " " + p.getSimpleName())
.collect(Collectors.joining(", "));
}
public static boolean isSetType(TypeElement typeElement) {
Name name = typeElement.getQualifiedName();
return SET_TYPES.contains(name.toString());
}
}

View file

@ -26,6 +26,7 @@ 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;
/**
*
@ -34,11 +35,10 @@ import org.keycloak.models.map.annotations.GenerateEntityImplementations;
@GenerateEntityImplementations(inherits="org.keycloak.models.map.client.MapClientEntity.AbstractClientEntity")
public interface MapClientEntity extends AbstractEntity, UpdatableEntity {
public abstract class AbstractClientEntity implements MapClientEntity {
public abstract class AbstractClientEntity extends UpdatableEntity.Impl implements MapClientEntity {
/**
* Flag signalizing that any of the setters has been meaningfully used.
*/
protected boolean updated;
private String id;
protected AbstractClientEntity() {}
@ -59,9 +59,8 @@ public interface MapClientEntity extends AbstractEntity, UpdatableEntity {
this.updated |= id != null;
}
@Override
public boolean isUpdated() {
return this.updated;
public <V> V deepClone(V obj) {
return Serialization.from(obj);
}
@Override
@ -97,7 +96,7 @@ public interface MapClientEntity extends AbstractEntity, UpdatableEntity {
void removeWebOrigin(String webOrigin);
void setWebOrigins(Set<String> webOrigins);
default List<String> getAttribute(String name) { return getAttributes().get(name); }
default List<String> getAttribute(String name) { return getAttributes() == null ? null : getAttributes().get(name); }
Map<String, List<String>> getAttributes();
void removeAttribute(String name);
void setAttribute(String name, List<String> values);

View file

@ -1,480 +0,0 @@
/*
* 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 java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicMarkableReference;
import java.util.function.Supplier;
import java.util.stream.Stream;
/**
*
* @author hmlnarik
*/
public class MapClientEntityLazyDelegate implements MapClientEntity {
private final Supplier<MapClientEntity> delegateSupplier;
private final AtomicMarkableReference<MapClientEntity> delegate = new AtomicMarkableReference<>(null, false);
public MapClientEntityLazyDelegate(Supplier<MapClientEntity> delegateSupplier) {
this.delegateSupplier = delegateSupplier;
}
protected MapClientEntity getWriteDelegate() {
if (! isWriteDelegateInitialized()) {
delegate.compareAndSet(null, delegateSupplier == null ? null : delegateSupplier.get(), false, true);
}
MapClientEntity ref = delegate.getReference();
if (ref == null) {
throw new IllegalStateException("Invalid delegate obtained");
}
return ref;
}
protected boolean isWriteDelegateInitialized() {
return delegate.isMarked();
}
protected MapClientEntity getReadDelegate() {
return getWriteDelegate();
}
@Override
public void setClientScope(String id, Boolean defaultScope) {
getWriteDelegate().setClientScope(id, defaultScope);
}
@Override
public void addRedirectUri(String redirectUri) {
getWriteDelegate().addRedirectUri(redirectUri);
}
@Override
public void addScopeMapping(String id) {
getWriteDelegate().addScopeMapping(id);
}
@Override
public void addWebOrigin(String webOrigin) {
getWriteDelegate().addWebOrigin(webOrigin);
}
@Override
public void removeScopeMapping(String id) {
getWriteDelegate().removeScopeMapping(id);
}
@Override
public List<String> getAttribute(String name) {
return getReadDelegate().getAttribute(name);
}
@Override
public Map<String, List<String>> getAttributes() {
return getReadDelegate().getAttributes();
}
@Override
public Map<String, String> getAuthFlowBindings() {
return getReadDelegate().getAuthFlowBindings();
}
@Override
public String getAuthenticationFlowBindingOverride(String binding) {
return getReadDelegate().getAuthenticationFlowBindingOverride(binding);
}
@Override
public Map<String, String> getAuthenticationFlowBindingOverrides() {
return getReadDelegate().getAuthenticationFlowBindingOverrides();
}
@Override
public String getBaseUrl() {
return getReadDelegate().getBaseUrl();
}
@Override
public String getClientAuthenticatorType() {
return getReadDelegate().getClientAuthenticatorType();
}
@Override
public String getClientId() {
return getReadDelegate().getClientId();
}
@Override
public Stream<String> getClientScopes(boolean defaultScope) {
return getReadDelegate().getClientScopes(defaultScope);
}
@Override
public Map<String, Boolean> getClientScopes() {
return getReadDelegate().getClientScopes();
}
@Override
public String getDescription() {
return getReadDelegate().getDescription();
}
@Override
public String getManagementUrl() {
return getReadDelegate().getManagementUrl();
}
@Override
public String getName() {
return getReadDelegate().getName();
}
@Override
public Integer getNodeReRegistrationTimeout() {
return getReadDelegate().getNodeReRegistrationTimeout();
}
@Override
public Integer getNotBefore() {
return getReadDelegate().getNotBefore();
}
@Override
public String getProtocol() {
return getReadDelegate().getProtocol();
}
@Override
public MapProtocolMapperEntity getProtocolMapper(String id) {
return getReadDelegate().getProtocolMapper(id);
}
@Override
public Map<String,MapProtocolMapperEntity> getProtocolMappers() {
return getReadDelegate().getProtocolMappers();
}
@Override
public String getRealmId() {
return getReadDelegate().getRealmId();
}
@Override
public void setRealmId(String realmId) {
getWriteDelegate().setRealmId(realmId);
}
@Override
public Set<String> getRedirectUris() {
return getReadDelegate().getRedirectUris();
}
@Override
public String getRegistrationToken() {
return getReadDelegate().getRegistrationToken();
}
@Override
public String getRootUrl() {
return getReadDelegate().getRootUrl();
}
@Override
public Set<String> getScope() {
return getReadDelegate().getScope();
}
@Override
public Collection<String> getScopeMappings() {
return getReadDelegate().getScopeMappings();
}
@Override
public String getSecret() {
return getReadDelegate().getSecret();
}
@Override
public Set<String> getWebOrigins() {
return getReadDelegate().getWebOrigins();
}
@Override
public Boolean isAlwaysDisplayInConsole() {
return getWriteDelegate().isAlwaysDisplayInConsole();
}
@Override
public Boolean isBearerOnly() {
return getWriteDelegate().isBearerOnly();
}
@Override
public Boolean isConsentRequired() {
return getWriteDelegate().isConsentRequired();
}
@Override
public Boolean isDirectAccessGrantsEnabled() {
return getWriteDelegate().isDirectAccessGrantsEnabled();
}
@Override
public Boolean isEnabled() {
return getWriteDelegate().isEnabled();
}
@Override
public Boolean isFrontchannelLogout() {
return getWriteDelegate().isFrontchannelLogout();
}
@Override
public Boolean isFullScopeAllowed() {
return getWriteDelegate().isFullScopeAllowed();
}
@Override
public Boolean isImplicitFlowEnabled() {
return getWriteDelegate().isImplicitFlowEnabled();
}
@Override
public Boolean isPublicClient() {
return getWriteDelegate().isPublicClient();
}
@Override
public Boolean isServiceAccountsEnabled() {
return getWriteDelegate().isServiceAccountsEnabled();
}
@Override
public Boolean isStandardFlowEnabled() {
return getWriteDelegate().isStandardFlowEnabled();
}
@Override
public Boolean isSurrogateAuthRequired() {
return getWriteDelegate().isSurrogateAuthRequired();
}
@Override
public void removeAttribute(String name) {
getWriteDelegate().removeAttribute(name);
}
@Override
public void removeAuthenticationFlowBindingOverride(String binding) {
getWriteDelegate().removeAuthenticationFlowBindingOverride(binding);
}
@Override
public void removeClientScope(String id) {
getWriteDelegate().removeClientScope(id);
}
@Override
public void removeProtocolMapper(String id) {
getWriteDelegate().removeProtocolMapper(id);
}
@Override
public void removeRedirectUri(String redirectUri) {
getWriteDelegate().removeRedirectUri(redirectUri);
}
@Override
public void removeWebOrigin(String webOrigin) {
getWriteDelegate().removeWebOrigin(webOrigin);
}
@Override
public void setAlwaysDisplayInConsole(Boolean alwaysDisplayInConsole) {
getWriteDelegate().setAlwaysDisplayInConsole(alwaysDisplayInConsole);
}
@Override
public void setAttribute(String name, List<String> values) {
getWriteDelegate().setAttribute(name, values);
}
@Override
public void setAuthFlowBindings(Map<String, String> authFlowBindings) {
getWriteDelegate().setAuthFlowBindings(authFlowBindings);
}
@Override
public void setAuthenticationFlowBindingOverride(String binding, String flowId) {
getWriteDelegate().setAuthenticationFlowBindingOverride(binding, flowId);
}
@Override
public void setBaseUrl(String baseUrl) {
getWriteDelegate().setBaseUrl(baseUrl);
}
@Override
public void setBearerOnly(Boolean bearerOnly) {
getWriteDelegate().setBearerOnly(bearerOnly);
}
@Override
public void setClientAuthenticatorType(String clientAuthenticatorType) {
getWriteDelegate().setClientAuthenticatorType(clientAuthenticatorType);
}
@Override
public void setClientId(String clientId) {
getWriteDelegate().setClientId(clientId);
}
@Override
public void setConsentRequired(Boolean consentRequired) {
getWriteDelegate().setConsentRequired(consentRequired);
}
@Override
public void setDescription(String description) {
getWriteDelegate().setDescription(description);
}
@Override
public void setDirectAccessGrantsEnabled(Boolean directAccessGrantsEnabled) {
getWriteDelegate().setDirectAccessGrantsEnabled(directAccessGrantsEnabled);
}
@Override
public void setEnabled(Boolean enabled) {
getWriteDelegate().setEnabled(enabled);
}
@Override
public void setFrontchannelLogout(Boolean frontchannelLogout) {
getWriteDelegate().setFrontchannelLogout(frontchannelLogout);
}
@Override
public void setFullScopeAllowed(Boolean fullScopeAllowed) {
getWriteDelegate().setFullScopeAllowed(fullScopeAllowed);
}
@Override
public void setImplicitFlowEnabled(Boolean implicitFlowEnabled) {
getWriteDelegate().setImplicitFlowEnabled(implicitFlowEnabled);
}
@Override
public void setManagementUrl(String managementUrl) {
getWriteDelegate().setManagementUrl(managementUrl);
}
@Override
public void setName(String name) {
getWriteDelegate().setName(name);
}
@Override
public void setNodeReRegistrationTimeout(Integer nodeReRegistrationTimeout) {
getWriteDelegate().setNodeReRegistrationTimeout(nodeReRegistrationTimeout);
}
@Override
public void setNotBefore(Integer notBefore) {
getWriteDelegate().setNotBefore(notBefore);
}
@Override
public void setProtocol(String protocol) {
getWriteDelegate().setProtocol(protocol);
}
@Override
public void setPublicClient(Boolean publicClient) {
getWriteDelegate().setPublicClient(publicClient);
}
@Override
public void setRedirectUris(Set<String> redirectUris) {
getWriteDelegate().setRedirectUris(redirectUris);
}
@Override
public void setRegistrationToken(String registrationToken) {
getWriteDelegate().setRegistrationToken(registrationToken);
}
@Override
public void setRootUrl(String rootUrl) {
getWriteDelegate().setRootUrl(rootUrl);
}
@Override
public void setScope(Set<String> scope) {
getWriteDelegate().setScope(scope);
}
@Override
public void setSecret(String secret) {
getWriteDelegate().setSecret(secret);
}
@Override
public void setServiceAccountsEnabled(Boolean serviceAccountsEnabled) {
getWriteDelegate().setServiceAccountsEnabled(serviceAccountsEnabled);
}
@Override
public void setStandardFlowEnabled(Boolean standardFlowEnabled) {
getWriteDelegate().setStandardFlowEnabled(standardFlowEnabled);
}
@Override
public void setSurrogateAuthRequired(Boolean surrogateAuthRequired) {
getWriteDelegate().setSurrogateAuthRequired(surrogateAuthRequired);
}
@Override
public void setWebOrigins(Set<String> webOrigins) {
getWriteDelegate().setWebOrigins(webOrigins);
}
@Override
public void setProtocolMapper(String id, MapProtocolMapperEntity mapping) {
getWriteDelegate().setProtocolMapper(id, mapping);
}
@Override
public String getId() {
return getReadDelegate().getId();
}
@Override
public void setId(String id) {
getWriteDelegate().setId(id);
}
@Override
public boolean isUpdated() {
return isWriteDelegateInitialized() && getWriteDelegate().isUpdated();
}
}

View file

@ -24,20 +24,9 @@ import java.util.Map;
*
* @author hmlnarik
*/
@GenerateEntityImplementations(
inherits = "org.keycloak.models.map.client.MapProtocolMapperEntity.AbstractProtocolMapperEntity"
)
@GenerateEntityImplementations
public interface MapProtocolMapperEntity extends UpdatableEntity {
public abstract class AbstractProtocolMapperEntity implements MapProtocolMapperEntity {
protected boolean updated;
@Override
public boolean isUpdated() {
return this.updated;
}
}
String getId();
void setId(String id);

View file

@ -52,7 +52,7 @@ public class Serialization {
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.setVisibility(PropertyAccessor.ALL, Visibility.NONE)
.setVisibility(PropertyAccessor.FIELD, Visibility.ANY)
.activateDefaultTyping(new LaissezFaireSubTypeValidator() /* TODO - see javadoc */, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT, JsonTypeInfo.As.PROPERTY)
.activateDefaultTyping(new LaissezFaireSubTypeValidator() /* TODO - see javadoc */, ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS, JsonTypeInfo.As.PROPERTY)
.addMixIn(UpdatableEntity.class, IgnoreUpdatedMixIn.class)
;
@ -71,7 +71,7 @@ public class Serialization {
}
public static <T extends AbstractEntity> T from(T orig) {
public static <T> T from(T orig) {
if (orig == null) {
return null;
}

View file

@ -18,9 +18,29 @@ package org.keycloak.models.map.common;
public interface UpdatableEntity {
public static class Impl implements UpdatableEntity {
protected boolean updated;
@Override
public boolean isUpdated() {
return this.updated;
}
@Override
public void clearUpdatedFlag() {
this.updated = false;
}
}
/**
* Flag signalizing that any of the setters has been meaningfully used.
* @return
*/
boolean isUpdated();
/**
* An optional operation clearing the updated flag. Right after using this method, the
* {@link #isUpdated()} would return {@code false}.
*/
default void clearUpdatedFlag() { }
}

View file

@ -0,0 +1,33 @@
/*
* 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.delegate;
/**
*
* @author hmlnarik
*/
public interface DelegateProvider<T> {
/**
* Returns a delegate for and entity for an operation on a field.
* @param isRead {@code true} when the delegate requested for a read operation, false otherwise
* @param field Identification of the field this delegates operates on.
* @return
*/
T getDelegate(boolean isRead, Object field);
default boolean isUpdated() { return false; }
}

View file

@ -0,0 +1,61 @@
/*
* 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.delegate;
import org.keycloak.models.map.common.UpdatableEntity;
import java.util.concurrent.atomic.AtomicMarkableReference;
import java.util.function.Supplier;
/**
*
* @author hmlnarik
*/
public class LazyDelegateProvider<T extends UpdatableEntity> implements DelegateProvider {
private final Supplier<T> delegateSupplier;
private final AtomicMarkableReference<T> delegate = new AtomicMarkableReference<>(null, false);
public LazyDelegateProvider(Supplier<T> delegateSupplier) {
this.delegateSupplier = delegateSupplier;
}
@Override
public T getDelegate(boolean isRead, Object field) {
if (! isDelegateInitialized()) {
delegate.compareAndSet(null, delegateSupplier == null ? null : delegateSupplier.get(), false, true);
}
T ref = delegate.getReference();
if (ref == null) {
throw new IllegalStateException("Invalid delegate obtained");
}
return ref;
}
protected boolean isDelegateInitialized() {
return delegate.isMarked();
}
@Override
public boolean isUpdated() {
if (isDelegateInitialized()) {
T d = getDelegate(true, this);
return d.isUpdated();
}
return false;
}
}

View file

@ -0,0 +1,42 @@
/*
* 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.delegate;
import org.keycloak.models.map.common.UpdatableEntity;
/**
*
* @author hmlnarik
*/
public class SimpleDelegateProvider<T extends UpdatableEntity> implements DelegateProvider {
private final T delegate;
public SimpleDelegateProvider(T delegate) {
this.delegate = delegate;
}
@Override
public T getDelegate(boolean isRead, Object field) {
return this.delegate;
}
@Override
public boolean isUpdated() {
return this.delegate.isUpdated();
}
}

View file

@ -212,6 +212,7 @@ 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);

View file

@ -61,6 +61,7 @@ public class ConcurrentHashMapStorage<K, V extends AbstractEntity & UpdatableEnt
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));
}

View file

@ -71,6 +71,7 @@ import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.sessions.RootAuthenticationSessionModel;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import static org.keycloak.models.map.storage.QueryParameters.withCriteria;
@ -272,7 +273,7 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide
try {
LOG.debugf("Restoring contents from %s", f.getCanonicalPath());
Class<?> valueImplType = INTERFACE_TO_IMPL.getOrDefault(valueType, valueType);
JavaType type = Serialization.MAPPER.getTypeFactory().constructCollectionType(List.class, valueImplType);
JavaType type = Serialization.MAPPER.getTypeFactory().constructCollectionType(LinkedList.class, valueImplType);
List<V> values = Serialization.MAPPER.readValue(f, type);
values.forEach((V mce) -> store.create(mce));