diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/annotations/GenerateEntityImplementations.java b/model/build-processor/src/main/java/org/keycloak/models/map/annotations/GenerateEntityImplementations.java index f36cf901fb..1a8e3c6d0b 100644 --- a/model/build-processor/src/main/java/org/keycloak/models/map/annotations/GenerateEntityImplementations.java +++ b/model/build-processor/src/main/java/org/keycloak/models/map/annotations/GenerateEntityImplementations.java @@ -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"; } diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/processor/FieldAccessorType.java b/model/build-processor/src/main/java/org/keycloak/models/map/processor/FieldAccessorType.java new file mode 100644 index 0000000000..90389d73ea --- /dev/null +++ b/model/build-processor/src/main/java/org/keycloak/models/map/processor/FieldAccessorType.java @@ -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 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 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 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 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 getMethod(FieldAccessorType type, + HashSet 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; + } +} diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateEntityImplementationsProcessor.java b/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateEntityImplementationsProcessor.java index aaed0f2ce2..0720760bb1 100644 --- a/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateEntityImplementationsProcessor.java +++ b/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateEntityImplementationsProcessor.java @@ -17,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> 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 annotations, RoundEnvironment roundEnv) { + elements = processingEnv.getElementUtils(); + types = processingEnv.getTypeUtils(); + for (TypeElement annotation : annotations) { Set 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> 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 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 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> me : methodsPerAttribute.entrySet()) { - String enumConstant = toEnumConstant(me.getKey()); - HashSet 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 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 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 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> 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 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> 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 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 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 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 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> 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 allMembers = elements.getAllMembers(e); + + JavaFileObject file = processingEnv.getFiler().createSourceFile(mapClassName); + IdentityHashMap m2field = new IdentityHashMap<>(); + methodsPerAttribute.forEach((f, s) -> s.forEach(m -> m2field.put(m, f))); // Create reverse map + try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) { + if (packageName != null) { + pw.println("package " + packageName + ";"); + } + + pw.println("public class " + mapSimpleClassName + " 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("}"); + } + } } } diff --git a/model/map/src/main/java/org/keycloak/models/map/client/MapClientEntityDelegate.java b/model/build-processor/src/main/java/org/keycloak/models/map/processor/PrintWriterNoJavaLang.java similarity index 67% rename from model/map/src/main/java/org/keycloak/models/map/client/MapClientEntityDelegate.java rename to model/build-processor/src/main/java/org/keycloak/models/map/processor/PrintWriterNoJavaLang.java index fad13b6414..99caca4f8c 100644 --- a/model/map/src/main/java/org/keycloak/models/map/client/MapClientEntityDelegate.java +++ b/model/build-processor/src/main/java/org/keycloak/models/map/processor/PrintWriterNoJavaLang.java @@ -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.", "")); } + } diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/processor/Util.java b/model/build-processor/src/main/java/org/keycloak/models/map/processor/Util.java new file mode 100644 index 0000000000..86996ac138 --- /dev/null +++ b/model/build-processor/src/main/java/org/keycloak/models/map/processor/Util.java @@ -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 SET_TYPES = new HashSet<>(Arrays.asList(Set.class.getCanonicalName(), TreeSet.class.getCanonicalName(), HashSet.class.getCanonicalName(), LinkedHashSet.class.getCanonicalName())); + + public static List getGenericsDeclaration(TypeMirror fieldType) { + List res = new LinkedList<>(); + + fieldType.accept(new SimpleTypeVisitor8>() { + @Override + public Void visitDeclared(DeclaredType t, List p) { + List typeArguments = t.getTypeArguments(); + res.addAll(typeArguments); + return null; + } + }, res); + + return res; + } + + public static String methodParameters(List 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()); + } + +} diff --git a/model/map/src/main/java/org/keycloak/models/map/client/MapClientEntity.java b/model/map/src/main/java/org/keycloak/models/map/client/MapClientEntity.java index 867307c536..2bbb494c87 100644 --- a/model/map/src/main/java/org/keycloak/models/map/client/MapClientEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/client/MapClientEntity.java @@ -26,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 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 webOrigins); - default List getAttribute(String name) { return getAttributes().get(name); } + default List getAttribute(String name) { return getAttributes() == null ? null : getAttributes().get(name); } Map> getAttributes(); void removeAttribute(String name); void setAttribute(String name, List values); diff --git a/model/map/src/main/java/org/keycloak/models/map/client/MapClientEntityLazyDelegate.java b/model/map/src/main/java/org/keycloak/models/map/client/MapClientEntityLazyDelegate.java deleted file mode 100644 index 5ca21853f0..0000000000 --- a/model/map/src/main/java/org/keycloak/models/map/client/MapClientEntityLazyDelegate.java +++ /dev/null @@ -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 delegateSupplier; - - private final AtomicMarkableReference delegate = new AtomicMarkableReference<>(null, false); - - public MapClientEntityLazyDelegate(Supplier 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 getAttribute(String name) { - return getReadDelegate().getAttribute(name); - } - - @Override - public Map> getAttributes() { - return getReadDelegate().getAttributes(); - } - - @Override - public Map getAuthFlowBindings() { - return getReadDelegate().getAuthFlowBindings(); - } - - @Override - public String getAuthenticationFlowBindingOverride(String binding) { - return getReadDelegate().getAuthenticationFlowBindingOverride(binding); - } - - @Override - public Map 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 getClientScopes(boolean defaultScope) { - return getReadDelegate().getClientScopes(defaultScope); - } - - @Override - public Map 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 getProtocolMappers() { - return getReadDelegate().getProtocolMappers(); - } - - @Override - public String getRealmId() { - return getReadDelegate().getRealmId(); - } - - @Override - public void setRealmId(String realmId) { - getWriteDelegate().setRealmId(realmId); - } - - @Override - public Set getRedirectUris() { - return getReadDelegate().getRedirectUris(); - } - - @Override - public String getRegistrationToken() { - return getReadDelegate().getRegistrationToken(); - } - - @Override - public String getRootUrl() { - return getReadDelegate().getRootUrl(); - } - - @Override - public Set getScope() { - return getReadDelegate().getScope(); - } - - @Override - public Collection getScopeMappings() { - return getReadDelegate().getScopeMappings(); - } - - @Override - public String getSecret() { - return getReadDelegate().getSecret(); - } - - @Override - public Set 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 values) { - getWriteDelegate().setAttribute(name, values); - } - - @Override - public void setAuthFlowBindings(Map 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 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 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 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(); - } - -} diff --git a/model/map/src/main/java/org/keycloak/models/map/client/MapProtocolMapperEntity.java b/model/map/src/main/java/org/keycloak/models/map/client/MapProtocolMapperEntity.java index b8ff1399cf..300f979852 100644 --- a/model/map/src/main/java/org/keycloak/models/map/client/MapProtocolMapperEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/client/MapProtocolMapperEntity.java @@ -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); diff --git a/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java b/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java index ae6aba86ab..1d33f1d452 100644 --- a/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java +++ b/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java @@ -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 from(T orig) { + public static T from(T orig) { if (orig == null) { return null; } diff --git a/model/map/src/main/java/org/keycloak/models/map/common/UpdatableEntity.java b/model/map/src/main/java/org/keycloak/models/map/common/UpdatableEntity.java index 1ac1861425..a5d9da303f 100644 --- a/model/map/src/main/java/org/keycloak/models/map/common/UpdatableEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/common/UpdatableEntity.java @@ -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() { } } diff --git a/model/map/src/main/java/org/keycloak/models/map/common/delegate/DelegateProvider.java b/model/map/src/main/java/org/keycloak/models/map/common/delegate/DelegateProvider.java new file mode 100644 index 0000000000..6d33dc4205 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/common/delegate/DelegateProvider.java @@ -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 { + /** + * 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; } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/common/delegate/LazyDelegateProvider.java b/model/map/src/main/java/org/keycloak/models/map/common/delegate/LazyDelegateProvider.java new file mode 100644 index 0000000000..ab72f6e6af --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/common/delegate/LazyDelegateProvider.java @@ -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 implements DelegateProvider { + + private final Supplier delegateSupplier; + + private final AtomicMarkableReference delegate = new AtomicMarkableReference<>(null, false); + + public LazyDelegateProvider(Supplier 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; + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/common/delegate/SimpleDelegateProvider.java b/model/map/src/main/java/org/keycloak/models/map/common/delegate/SimpleDelegateProvider.java new file mode 100644 index 0000000000..d3218c95a8 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/common/delegate/SimpleDelegateProvider.java @@ -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 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(); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java index c0dbbaca2a..ce6aa25755 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java @@ -212,6 +212,7 @@ public class ConcurrentHashMapKeycloakTransaction 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 values = Serialization.MAPPER.readValue(f, type); values.forEach((V mce) -> store.create(mce));