From 7aaa33739bc1543c4ef61ebbc993b24c7f0b1416 Mon Sep 17 00:00:00 2001 From: Michal Hajas Date: Tue, 23 Nov 2021 16:46:17 +0100 Subject: [PATCH] KEYCLOAK-19570 Add annotation processing for HotRod clients --- .../GenerateHotRodEntityImplementation.java | 30 + ...gnoreForEntityImplementationGenerator.java | 2 +- .../CannotMigrateTypeException.java | 37 ++ ...enerateEntityImplementationsProcessor.java | 253 +++++++ ...enerateEntityImplementationsProcessor.java | 230 +------ ...eHotRodEntityImplementationsProcessor.java | 508 +++++++++++++++ .../keycloak/models/map/processor/Util.java | 8 + model/map-hot-rod/pom.xml | 38 ++ .../map/storage/hotRod/HotRodMapStorage.java | 27 +- .../hotRod/HotRodMapStorageProvider.java | 7 +- .../HotRodMapStorageProviderFactory.java | 19 +- .../hotRod/client/HotRodClientEntity.java | 616 ++---------------- .../client/HotRodProtocolMapperEntity.java | 73 +-- .../hotRod/common/AbstractHotRodEntity.java | 21 + .../hotRod/common/HotRodEntityDelegate.java | 25 + .../hotRod/common/HotRodEntityDescriptor.java | 27 +- .../map/storage/hotRod/common/HotRodPair.java | 39 +- .../hotRod/common/HotRodTypesUtils.java | 79 +++ .../map/storage/hotRod/common/Versioned.java | 22 - ...IckleQueryMapModelCriteriaBuilderTest.java | 7 +- .../hotRod/common/HotRodTypesUtilsTest.java | 110 ++++ .../models/map/client/MapClientEntity.java | 3 - .../map/client/MapProtocolMapperEntity.java | 6 +- pom.xml | 1 + 24 files changed, 1259 insertions(+), 929 deletions(-) create mode 100644 model/build-processor/src/main/java/org/keycloak/models/map/annotations/GenerateHotRodEntityImplementation.java create mode 100644 model/build-processor/src/main/java/org/keycloak/models/map/exceptions/CannotMigrateTypeException.java create mode 100644 model/build-processor/src/main/java/org/keycloak/models/map/processor/AbstractGenerateEntityImplementationsProcessor.java create mode 100644 model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateHotRodEntityImplementationsProcessor.java create mode 100644 model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/AbstractHotRodEntity.java create mode 100644 model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodEntityDelegate.java create mode 100644 model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodTypesUtils.java create mode 100644 model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/common/HotRodTypesUtilsTest.java diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/annotations/GenerateHotRodEntityImplementation.java b/model/build-processor/src/main/java/org/keycloak/models/map/annotations/GenerateHotRodEntityImplementation.java new file mode 100644 index 0000000000..52acf954f0 --- /dev/null +++ b/model/build-processor/src/main/java/org/keycloak/models/map/annotations/GenerateHotRodEntityImplementation.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.map.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface GenerateHotRodEntityImplementation { + String implementInterface(); + String inherits() default "org.keycloak.models.map.common.UpdatableEntity.Impl"; +} diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/annotations/IgnoreForEntityImplementationGenerator.java b/model/build-processor/src/main/java/org/keycloak/models/map/annotations/IgnoreForEntityImplementationGenerator.java index d8fd5cc7ea..c3eebcacbd 100644 --- a/model/build-processor/src/main/java/org/keycloak/models/map/annotations/IgnoreForEntityImplementationGenerator.java +++ b/model/build-processor/src/main/java/org/keycloak/models/map/annotations/IgnoreForEntityImplementationGenerator.java @@ -25,7 +25,7 @@ import java.lang.annotation.Target; * * @author hmlnarik */ -@Retention(RetentionPolicy.SOURCE) +@Retention(RetentionPolicy.CLASS) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface IgnoreForEntityImplementationGenerator { } diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/exceptions/CannotMigrateTypeException.java b/model/build-processor/src/main/java/org/keycloak/models/map/exceptions/CannotMigrateTypeException.java new file mode 100644 index 0000000000..fd2e8de3e6 --- /dev/null +++ b/model/build-processor/src/main/java/org/keycloak/models/map/exceptions/CannotMigrateTypeException.java @@ -0,0 +1,37 @@ +/* + * 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.exceptions; + +import javax.lang.model.type.TypeMirror; +import java.util.Arrays; +import java.util.stream.Collectors; + +public class CannotMigrateTypeException extends RuntimeException { + private final TypeMirror toType; + private final TypeMirror[] fromType; + + public CannotMigrateTypeException(TypeMirror toType, TypeMirror[] fromType) { + this.toType = toType; + this.fromType = fromType; + } + + public String getFormattedMessage() { + return "Cannot migrate [" + Arrays.stream(fromType).map(TypeMirror::toString).collect(Collectors.joining(", ")) + "] to " + toType.toString(); + } + +} diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/processor/AbstractGenerateEntityImplementationsProcessor.java b/model/build-processor/src/main/java/org/keycloak/models/map/processor/AbstractGenerateEntityImplementationsProcessor.java new file mode 100644 index 0000000000..1dca4f21de --- /dev/null +++ b/model/build-processor/src/main/java/org/keycloak/models/map/processor/AbstractGenerateEntityImplementationsProcessor.java @@ -0,0 +1,253 @@ +/* + * 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 javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +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.type.NoType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import javax.tools.Diagnostic; +import java.io.IOException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.keycloak.models.map.processor.FieldAccessorType.GETTER; +import static org.keycloak.models.map.processor.Util.getGenericsDeclaration; + +public abstract class AbstractGenerateEntityImplementationsProcessor extends AbstractProcessor { + + protected static final String FQN_DEEP_CLONER = "org.keycloak.models.map.common.DeepCloner"; + protected Elements elements; + protected Types types; + + protected static interface Generator { + void generate(TypeElement e) throws IOException; + } + + @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() + .map(TypeElement.class::cast) + .filter(this::testAnnotationElement) + .forEach(this::processTypeElement); + } + + if (!annotations.isEmpty()) { + afterAnnotationProcessing(); + } + + return true; + } + + protected boolean testAnnotationElement(TypeElement kind) { return true; } + protected void afterAnnotationProcessing() {} + protected abstract Generator[] getGenerators(); + + private void processTypeElement(TypeElement e) { + for (GenerateEntityImplementationsProcessor.Generator generator : getGenerators()) { + try { + generator.generate(e); + } catch (Exception ex) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not generate implementation for class: " + ex, e); + } + } + +// methodsPerAttribute.entrySet().stream() +// .sorted(Comparator.comparing(Map.Entry::getKey)) +// .forEach(me -> processingEnv.getMessager().printMessage( +// Diagnostic.Kind.NOTE, +// "** " + me.getKey() + ": " + me.getValue().stream().map(ExecutableElement::getSimpleName).sorted(Comparator.comparing(Object::toString)).collect(Collectors.joining(", "))) +// ); + } + + protected Map> methodsPerAttributeMapping(TypeElement e) { + final List allMembers = elements.getAllMembers(e); + Map> methodsPerAttribute = allMembers.stream() + .filter(el -> el.getKind() == ElementKind.METHOD) + .filter(el -> el.getModifiers().contains(Modifier.ABSTRACT)) + .filter(Util::isNotIgnored) + .filter(ExecutableElement.class::isInstance) + .map(ExecutableElement.class::cast) + .filter(ee -> ! (ee.getReceiverType() instanceof NoType)) + .collect(Collectors.toMap(this::determineAttributeFromMethodName, v -> new HashSet(Arrays.asList(v)), (a, b) -> { a.addAll(b); return a; })); + + // Merge plurals with singulars + methodsPerAttribute.keySet().stream() + .filter(key -> methodsPerAttribute.containsKey(key + "s")) + .collect(Collectors.toSet()) + .forEach(key -> { + HashSet removed = methodsPerAttribute.remove(key); + methodsPerAttribute.get(key + "s").addAll(removed); + }); + + return methodsPerAttribute; + } + + private static final Pattern BEAN_NAME = Pattern.compile("(get|set|is|delete|remove|add|update)([A-Z]\\S+)"); + private static final Map FORBIDDEN_PREFIXES = new HashMap<>(); + static { + FORBIDDEN_PREFIXES.put("delete", "remove"); + } + + private String determineAttributeFromMethodName(ExecutableElement e) { + Name name = e.getSimpleName(); + Matcher m = BEAN_NAME.matcher(name.toString()); + if (m.matches()) { + String prefix = m.group(1); + if (FORBIDDEN_PREFIXES.containsKey(prefix)) { + processingEnv.getMessager().printMessage( + Diagnostic.Kind.ERROR, + "Forbidden prefix " + prefix + "... detected, use " + FORBIDDEN_PREFIXES.get(prefix) + "... instead", e + ); + } + return m.group(2); + } + return null; + } + + protected Stream fieldGetters(Map> methodsPerAttribute) { + return methodsPerAttribute.entrySet().stream() + .map(me -> FieldAccessorType.getMethod(GETTER, me.getValue(), me.getKey(), types, determineFieldType(me.getKey(), me.getValue()))) + .filter(Optional::isPresent) + .map(Optional::get); + } + + protected boolean isImmutableFinalType(TypeMirror fieldType) { + return isPrimitiveType(fieldType) || isBoxedPrimitiveType(fieldType) || Objects.equals("java.lang.String", fieldType.toString()); + } + + protected boolean isKnownCollectionOfImmutableFinalTypes(TypeMirror fieldType) { + List res = getGenericsDeclaration(fieldType); + return isCollection(fieldType) && res.stream().allMatch(tm -> isImmutableFinalType(tm) || isKnownCollectionOfImmutableFinalTypes(tm)); + } + + protected boolean isCollection(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": + return true; + default: + return false; + } + } + + protected 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 + ")"; + } + } + + protected boolean isPrimitiveType(TypeMirror fieldType) { + try { + types.getPrimitiveType(fieldType.getKind()); + return true; + } catch (IllegalArgumentException ex) { + return false; + } + } + + protected boolean isBoxedPrimitiveType(TypeMirror fieldType) { + try { + types.unboxedType(fieldType); + return true; + } catch (IllegalArgumentException ex) { + return false; + } + } + + protected String interfaceToImplementation(TypeElement typeElement, String parameter) { + Name parameterTypeQN = typeElement.getQualifiedName(); + switch (parameterTypeQN.toString()) { + case "java.util.List": + case "java.util.Collection": + 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(Diagnostic.Kind.ERROR, "Could not determine implementation for type " + typeElement, typeElement); + return "TODO()"; + } + } + + protected TypeMirror determineFieldType(String fieldName, HashSet methods) { + Pattern getter = Pattern.compile("(get|is)" + Pattern.quote(fieldName)); + TypeMirror res = null; + for (ExecutableElement method : methods) { + if (getter.matcher(method.getSimpleName()).matches() && method.getParameters().isEmpty()) { + return method.getReturnType(); + } + } + if (res == null) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Could not determine return type for the field " + fieldName, methods.iterator().next()); + } + return res; + } + + protected static class NameFirstComparator implements Comparator { + protected static final Comparator ID_INSTANCE = new NameFirstComparator("id").thenComparing(Comparator.naturalOrder()); + protected static final Comparator GET_ID_INSTANCE = new NameFirstComparator("getId").thenComparing(Comparator.naturalOrder()); + private final String name; + public NameFirstComparator(String name) { + this.name = name; + } + @Override + public int compare(String o1, String o2) { + return Objects.equals(o1, o2) + ? 0 + : name.equalsIgnoreCase(o1) + ? -1 + : name.equalsIgnoreCase(o2) + ? 1 + : 0; + } + + } +} 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 a12747a6e1..4742d23b24 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 @@ -19,18 +19,11 @@ package org.keycloak.models.map.processor; import org.keycloak.models.map.annotations.GenerateEntityImplementations; import java.io.IOException; import java.io.PrintWriter; -import java.util.Arrays; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; -import javax.annotation.processing.AbstractProcessor; -import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; @@ -40,14 +33,11 @@ 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.type.NoType; import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.Elements; -import javax.lang.model.util.Types; +import javax.tools.Diagnostic; 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.Collection; @@ -57,7 +47,6 @@ import java.util.Optional; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Stream; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; @@ -67,14 +56,8 @@ import javax.lang.model.type.TypeKind; */ @SupportedAnnotationTypes("org.keycloak.models.map.annotations.GenerateEntityImplementations") @SupportedSourceVersion(SourceVersion.RELEASE_8) -public class GenerateEntityImplementationsProcessor extends AbstractProcessor { +public class GenerateEntityImplementationsProcessor extends AbstractGenerateEntityImplementationsProcessor { - private static interface Generator { - void generate(TypeElement e, Map> methodsPerAttribute) throws IOException; - } - - private Elements elements; - private Types types; private Collection cloners = new TreeSet<>(); private final Generator[] generators = new Generator[] { @@ -85,23 +68,13 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { }; @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() - .map(TypeElement.class::cast) - .forEach(this::processTypeElement); - } - - if (! cloners.isEmpty() && ! annotations.isEmpty()) { + protected void afterAnnotationProcessing() { + if (! cloners.isEmpty()) { try { JavaFileObject file = processingEnv.getFiler().createSourceFile("org.keycloak.models.map.common.AutogeneratedCloners"); try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) { pw.println("package org.keycloak.models.map.common;"); - + pw.println("import org.keycloak.models.map.common.DeepCloner.Cloner;"); pw.println("// DO NOT CHANGE THIS CLASS, IT IS GENERATED AUTOMATICALLY BY " + GenerateEntityImplementationsProcessor.class.getSimpleName()); pw.println("public final class AutogeneratedCloners {"); @@ -116,178 +89,31 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { Logger.getLogger(GenerateEntityImplementationsProcessor.class.getName()).log(Level.SEVERE, null, ex); } } + } + + @Override + protected Generator[] getGenerators() { + return this.generators; + } + + @Override + protected boolean testAnnotationElement(TypeElement e) { + if (e.getKind() != ElementKind.INTERFACE) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Annotation @GenerateEntityImplementations is only applicable to an interface", e); + return false; + } return true; } - private static final String FQN_DEEP_CLONER = "org.keycloak.models.map.common.DeepCloner"; - - private void processTypeElement(TypeElement e) { - if (e.getKind() != ElementKind.INTERFACE) { - processingEnv.getMessager().printMessage(Kind.ERROR, "Annotation @GenerateEntityImplementations is only applicable to an interface", e); - return; - } - - // Find all properties - final List allMembers = elements.getAllMembers(e); - Map> methodsPerAttribute = allMembers.stream() - .filter(el -> el.getKind() == ElementKind.METHOD) - .filter(el -> el.getModifiers().contains(Modifier.ABSTRACT)) - .filter(Util::isNotIgnored) - .filter(ExecutableElement.class::isInstance) - .map(ExecutableElement.class::cast) - .filter(ee -> ! (ee.getReceiverType() instanceof NoType)) - .collect(Collectors.toMap(this::determineAttributeFromMethodName, v -> new HashSet(Arrays.asList(v)), (a,b) -> { a.addAll(b); return a; })); - - // Merge plurals with singulars - methodsPerAttribute.keySet().stream() - .filter(key -> methodsPerAttribute.containsKey(key + "s")) - .collect(Collectors.toSet()) - .forEach(key -> { - HashSet removed = methodsPerAttribute.remove(key); - methodsPerAttribute.get(key + "s").addAll(removed); - }); - - 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() -// .sorted(Comparator.comparing(Map.Entry::getKey)) -// .forEach(me -> processingEnv.getMessager().printMessage( -// Diagnostic.Kind.NOTE, -// "** " + me.getKey() + ": " + me.getValue().stream().map(ExecutableElement::getSimpleName).sorted(Comparator.comparing(Object::toString)).collect(Collectors.joining(", "))) -// ); - } - - private static final Pattern BEAN_NAME = Pattern.compile("(get|set|is|delete|remove|add|update)([A-Z]\\S+)"); - private static final Map FORBIDDEN_PREFIXES = new HashMap<>(); - static { - FORBIDDEN_PREFIXES.put("delete", "remove"); - } - - private String determineAttributeFromMethodName(ExecutableElement e) { - Name name = e.getSimpleName(); - Matcher m = BEAN_NAME.matcher(name.toString()); - if (m.matches()) { - String prefix = m.group(1); - if (FORBIDDEN_PREFIXES.containsKey(prefix)) { - processingEnv.getMessager().printMessage( - Kind.ERROR, - "Forbidden prefix " + prefix + "... detected, use " + FORBIDDEN_PREFIXES.get(prefix) + "... instead", e - ); - } - return m.group(2); - } - return null; - } protected static String toEnumConstant(String key) { return key.replaceAll("([a-z])([A-Z])", "$1_$2").toUpperCase(); } - private TypeMirror determineFieldType(String fieldName, HashSet methods) { - Pattern getter = Pattern.compile("(get|is)" + Pattern.quote(fieldName)); - TypeMirror res = null; - for (ExecutableElement method : methods) { - if (getter.matcher(method.getSimpleName()).matches() && method.getParameters().isEmpty()) { - return method.getReturnType(); - } - } - if (res == null) { - processingEnv.getMessager().printMessage(Kind.ERROR, "Could not determine return type for the field " + fieldName, methods.iterator().next()); - } - return res; - } - - private boolean isImmutableFinalType(TypeMirror fieldType) { - return isPrimitiveType(fieldType) || isBoxedPrimitiveType(fieldType) || Objects.equals("java.lang.String", fieldType.toString()); - } - - 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 String deepClone(TypeMirror fieldType, String parameterName) { - if (isKnownCollectionOfImmutableFinalTypes(fieldType)) { - TypeElement typeElement = elements.getTypeElement(types.erasure(fieldType).toString()); - return parameterName + " == null ? null : " + interfaceToImplementation(typeElement, parameterName); - } else { - return "deepClone(" + parameterName + ")"; - } - } - - private boolean isPrimitiveType(TypeMirror fieldType) { - try { - types.getPrimitiveType(fieldType.getKind()); - 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": - case "java.util.Collection": - 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 static class NameFirstComparator implements Comparator { - private static final Comparator ID_INSTANCE = new NameFirstComparator("id").thenComparing(Comparator.naturalOrder()); - private static final Comparator GET_ID_INSTANCE = new NameFirstComparator("getId").thenComparing(Comparator.naturalOrder()); - private final String name; - public NameFirstComparator(String name) { - this.name = name; - } - @Override - public int compare(String o1, String o2) { - return Objects.equals(o1, o2) - ? 0 - : name.equalsIgnoreCase(o1) - ? -1 - : name.equalsIgnoreCase(o2) - ? 1 - : 0; - } - - } - private class FieldsGenerator implements Generator { @Override - public void generate(TypeElement e, Map> methodsPerAttribute) throws IOException { + public void generate(TypeElement e) throws IOException { + Map> methodsPerAttribute = methodsPerAttributeMapping(e); String className = e.getQualifiedName().toString(); String packageName = null; int lastDot = className.lastIndexOf('.'); @@ -318,7 +144,8 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { private class ImplGenerator implements Generator { @Override - public void generate(TypeElement e, Map> methodsPerAttribute) throws IOException { + public void generate(TypeElement e) throws IOException { + Map> methodsPerAttribute = methodsPerAttributeMapping(e); GenerateEntityImplementations an = e.getAnnotation(GenerateEntityImplementations.class); TypeElement parentTypeElement = elements.getTypeElement((an.inherits() == null || an.inherits().isEmpty()) ? "void" : an.inherits()); if (parentTypeElement == null) { @@ -452,13 +279,6 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { } } - private Stream fieldGetters(Map> methodsPerAttribute) { - return methodsPerAttribute.entrySet().stream() - .map(me -> FieldAccessorType.getMethod(GETTER, me.getValue(), me.getKey(), types, determineFieldType(me.getKey(), me.getValue()))) - .filter(Optional::isPresent) - .map(Optional::get); - } - private boolean printMethodBody(PrintWriter pw, FieldAccessorType accessorType, ExecutableElement method, String fieldName, TypeMirror fieldType) { TypeMirror firstParameterType = method.getParameters().isEmpty() ? types.getNullType() @@ -525,7 +345,8 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { private class DelegateGenerator implements Generator { @Override - public void generate(TypeElement e, Map> methodsPerAttribute) throws IOException { + public void generate(TypeElement e) throws IOException { + Map> methodsPerAttribute = methodsPerAttributeMapping(e); String className = e.getQualifiedName().toString(); String packageName = null; int lastDot = className.lastIndexOf('.'); @@ -594,7 +415,8 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor { private class ClonerGenerator implements Generator { @Override - public void generate(TypeElement e, Map> methodsPerAttribute) throws IOException { + public void generate(TypeElement e) throws IOException { + Map> methodsPerAttribute = methodsPerAttributeMapping(e); String className = e.getQualifiedName().toString(); String packageName = null; int lastDot = className.lastIndexOf('.'); diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateHotRodEntityImplementationsProcessor.java b/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateHotRodEntityImplementationsProcessor.java new file mode 100644 index 0000000000..f9a2fdead7 --- /dev/null +++ b/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateHotRodEntityImplementationsProcessor.java @@ -0,0 +1,508 @@ +/* + * 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 org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation; +import org.keycloak.models.map.exceptions.CannotMigrateTypeException; + +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +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.TypeMirror; +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.keycloak.models.map.processor.Util.getGenericsDeclaration; +import static org.keycloak.models.map.processor.Util.isMapType; +import static org.keycloak.models.map.processor.Util.isSetType; +import static org.keycloak.models.map.processor.Util.methodParameters; + +@SupportedAnnotationTypes("org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation") +@SupportedSourceVersion(SourceVersion.RELEASE_8) +public class GenerateHotRodEntityImplementationsProcessor extends AbstractGenerateEntityImplementationsProcessor { + + + @Override + protected Generator[] getGenerators() { + return new Generator[] { new HotRodGettersAndSettersDelegateGenerator() }; + } + + + private class HotRodGettersAndSettersDelegateGenerator implements Generator { + + private static final String ENTITY_VARIABLE = "hotRodEntity"; + private String hotRodSimpleClassName; + private TypeElement generalHotRodDelegate; + private TypeElement abstractEntity; + private TypeElement abstractHotRodEntity; + private TypeElement hotRodUtils; + + @Override + public void generate(TypeElement e) throws IOException { + GenerateHotRodEntityImplementation hotRodAnnotation = e.getAnnotation(GenerateHotRodEntityImplementation.class); + String interfaceClass = hotRodAnnotation.implementInterface(); + if (interfaceClass == null || interfaceClass.isEmpty()) return; + TypeElement parentClassElement = elements.getTypeElement(hotRodAnnotation.inherits()); + if (parentClassElement == null) return; + + TypeElement parentInterfaceElement = elements.getTypeElement(interfaceClass); + if (parentInterfaceElement == null) return; + Map> methodsPerAttribute = methodsPerAttributeMapping(parentInterfaceElement); + + + final List allMembers = elements.getAllMembers(parentClassElement); + 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 hotRodImplClassName = className + "Delegate"; + hotRodSimpleClassName = simpleClassName + "Delegate"; + generalHotRodDelegate = elements.getTypeElement("org.keycloak.models.map.storage.hotRod.common.HotRodEntityDelegate"); + abstractEntity = elements.getTypeElement("org.keycloak.models.map.common.AbstractEntity"); + abstractHotRodEntity = elements.getTypeElement("org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity"); + hotRodUtils = elements.getTypeElement("org.keycloak.models.map.storage.hotRod.common.HotRodTypesUtils"); + + boolean hasDeepClone = allMembers.stream().filter(el -> el.getKind() == ElementKind.METHOD).anyMatch(el -> "deepClone".equals(el.getSimpleName().toString())); + boolean needsDeepClone = fieldGetters(methodsPerAttribute) + .map(ExecutableElement::getReturnType) + .anyMatch(fieldType -> ! isKnownCollectionOfImmutableFinalTypes(fieldType) && ! isImmutableFinalType(fieldType)); + boolean hasId = methodsPerAttribute.containsKey("Id") || allMembers.stream().anyMatch(el -> "getId".equals(el.getSimpleName().toString())); + + JavaFileObject file = processingEnv.getFiler().createSourceFile(hotRodImplClassName); + try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) { + if (packageName != null) { + pw.println("package " + packageName + ";"); + } + + pw.println("import java.util.Objects;"); + pw.println("import " + FQN_DEEP_CLONER + ";"); + pw.println("import java.util.Optional;"); + pw.println("import java.util.stream.Collectors;"); + pw.println(); + pw.println("// DO NOT CHANGE THIS CLASS, IT IS GENERATED AUTOMATICALLY BY " + GenerateHotRodEntityImplementationsProcessor.class.getSimpleName()); + pw.println("public class " + hotRodSimpleClassName + " extends " + parentClassElement.getQualifiedName().toString() + " implements " + + parentInterfaceElement.getQualifiedName().toString() + + ", " + generalHotRodDelegate.getQualifiedName().toString() + "<" + e.getQualifiedName().toString() + ">" + + " {"); + pw.println(); + pw.println(" private final " + className + " " + ENTITY_VARIABLE + ";"); + pw.println(); + + // Constructors + allMembers.stream() + .filter(ExecutableElement.class::isInstance) + .map(ExecutableElement.class::cast) + .filter((ExecutableElement ee) -> ee.getKind() == ElementKind.CONSTRUCTOR) + .forEach((ExecutableElement ee) -> { + if (hasDeepClone || ! needsDeepClone) { + pw.println(" " + + ee.getModifiers().stream().map(Object::toString).collect(Collectors.joining(" ")) + + " " + hotRodSimpleClassName + "(" + methodParameters(ee.getParameters()) + ") {" + ); + pw.println(" super(" + ee.getParameters() + ");"); + pw.println(" this." + ENTITY_VARIABLE + " = new " + className + "();"); + pw.println(" }"); + } else if (needsDeepClone) { + pw.println(" /**"); + pw.println(" * @deprecated This constructor uses a {@link DeepCloner#DUMB_CLONER} that does not clone anything. Use {@link #" + hotRodSimpleClassName + "(DeepCloner)} variant instead"); + pw.println(" */"); + pw.println(" " + + ee.getModifiers().stream().map(Object::toString).collect(Collectors.joining(" ")) + + " " + + hotRodSimpleClassName + "(" + methodParameters(ee.getParameters()) + ") { this(DeepCloner.DUMB_CLONER" + (ee.getParameters().isEmpty() ? "" : ", ") + ee.getParameters() + "); }" + ); + pw.println(" " + + ee.getModifiers().stream().map(Object::toString).collect(Collectors.joining(" ")) + + " " + + hotRodSimpleClassName + "(DeepCloner cloner" + (ee.getParameters().isEmpty() ? "" : ", ") + methodParameters(ee.getParameters()) + ") {" + ); + pw.println(" super(" + ee.getParameters() + ");"); + pw.println(" this.cloner = cloner;"); + pw.println(" this." + ENTITY_VARIABLE + " = new " + className + "();"); + pw.println(" }"); + } + }); + + // Add constructor for setting HotRodEntity + pw.println(" " + + "public " + hotRodSimpleClassName + "(" + className + " " + ENTITY_VARIABLE + ") {" + ); + pw.println(" this." + ENTITY_VARIABLE + " = " + ENTITY_VARIABLE + ";"); + if (! hasDeepClone && needsDeepClone) { + pw.println(" this.cloner = DeepCloner.DUMB_CLONER;"); + } + pw.println(" }"); + + // equals, hashCode, toString + pw.println(" @Override public boolean equals(Object o) {"); + pw.println(" if (o == this) return true; "); + pw.println(" if (! (o instanceof " + hotRodSimpleClassName + ")) return false; "); + pw.println(" " + hotRodSimpleClassName + " other = (" + hotRodSimpleClassName + ") o; "); + pw.println(" return " + + fieldGetters(methodsPerAttribute) + .map(ExecutableElement::getSimpleName) + .map(Name::toString) + .sorted(NameFirstComparator.GET_ID_INSTANCE) + .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(" + + fieldGetters(methodsPerAttribute) + .filter(ee -> isImmutableFinalType(ee.getReturnType())) + .map(ExecutableElement::getSimpleName) + .map(Name::toString) + .sorted(GenerateEntityImplementationsProcessor.NameFirstComparator.GET_ID_INSTANCE) + .map(v -> v + "()") + .collect(Collectors.joining(",\n ")) + + ")") + + ";"); + pw.println(" }"); + pw.println(" @Override public String toString() {"); + pw.println(" return String.format(\"%s@%08x\", " + (hasId ? "getId()" : "\"" + hotRodSimpleClassName + "\"" ) + ", System.identityHashCode(this));"); + pw.println(" }"); + + pw.println(" public static boolean entityEquals(Object o1, Object o2) {"); + pw.println(" if (!(o1 instanceof " + className + ")) return false;"); + pw.println(" if (!(o2 instanceof " + className + ")) return false;"); + + pw.println(" if (o1 == o2) return true;"); + + pw.println(" " + className + " e1 = (" + className + ") o1;"); + pw.println(" " + className + " e2 = (" + className + ") o2;"); + + pw.print(" return "); + pw.println(elements.getAllMembers(e).stream() + .filter(VariableElement.class::isInstance) + .map(VariableElement.class::cast) + .map(var -> "Objects.equals(e1." + var.getSimpleName().toString() + ", e2." + var.getSimpleName().toString() + ")") + .collect(Collectors.joining("\n && "))); + pw.println(" ;"); + pw.println(" }"); + + pw.println(" public static int entityHashCode(" + className + " e) {"); + pw.println(" return " + + (hasId + ? "(e.id == null ? Objects.hash(e) : e.id.hashCode())" + : "Objects.hash(" + + elements.getAllMembers(e).stream() + .filter(VariableElement.class::isInstance) + .map(VariableElement.class::cast) + .map(var -> var.getSimpleName().toString()) + .collect(Collectors.joining(",\n ")) + + ")") + + ";" + ); + pw.println(" }"); + + // deepClone + if (! hasDeepClone && needsDeepClone) { + pw.println(" private final DeepCloner cloner;"); + pw.println(" public V deepClone(V obj) {"); + pw.println(" return cloner.from(obj);"); + pw.println(" }"); + } + + // getters, setters + methodsPerAttribute.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey, GenerateEntityImplementationsProcessor.NameFirstComparator.ID_INSTANCE)).forEach(me -> { + HashSet methods = me.getValue(); + TypeMirror fieldType = determineFieldType(me.getKey(), methods); + if (fieldType == null) { + return; + } + + // Determine HotRod entity field name by changing case of first letter + char[] c = me.getKey().toCharArray(); + c[0] = Character.toLowerCase(c[0]); + String hotRodEntityFieldName = new String(c); + + // Find corresponding variable in HotRod*Entity + Optional hotRodVariable = elements.getAllMembers(e).stream() + .filter(VariableElement.class::isInstance) + .map(VariableElement.class::cast) + .filter(variableElement -> variableElement.getSimpleName().toString().equals(hotRodEntityFieldName)) + .findFirst(); + + if (!hotRodVariable.isPresent()) { + // throw an error when no variable found + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Cannot find " + e.getSimpleName().toString() + " field for methods: [" + me.getValue().stream().map(ee -> ee.getSimpleName().toString()).collect(Collectors.joining(", ")) + "]", parentInterfaceElement); + return; + } + + // Implement each method + for (ExecutableElement method : methods) { + FieldAccessorType fat = FieldAccessorType.determineType(method, me.getKey(), types, fieldType); + + // Check if the parent class implements the method already + 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(); + + try { + if (parentMethod.isPresent()) { + // Do not implement the method if it is already implemented by the parent class + processingEnv.getMessager().printMessage(Diagnostic.Kind.OTHER, "Method " + method + " is declared in a parent class.", method); + } else if (fat != FieldAccessorType.UNKNOWN && !printMethodBody(pw, fat, method, hotRodEntityFieldName, fieldType, hotRodVariable.get().asType())) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "Could not determine desired semantics of method from its signature", method); + } + } catch (CannotMigrateTypeException ex) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, ex.getFormattedMessage(), method); + } + } + }); + + // Implement HotRodDelegate interface + pw.println(" public " + className + " getHotRodEntity() {"); + pw.println(" return this." + ENTITY_VARIABLE + ";"); + pw.println(" }"); + pw.println("}"); + } + } + + private String hotRodEntityField(String fieldName) { + return "this." + ENTITY_VARIABLE + "." + fieldName; + } + + private boolean printMethodBody(PrintWriter pw, FieldAccessorType accessorType, ExecutableElement method, String fieldName, TypeMirror fieldType, TypeMirror hotRodFieldType) { + 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 " + migrateToType(method.getReturnType(), hotRodFieldType, hotRodEntityField(fieldName)) + ";"); + pw.println(" }"); + return true; + case SETTER: + pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {"); + if (! isImmutableFinalType(firstParameterType)) { + pw.println(" p0 = " + deepClone(fieldType, "p0") + ";"); + } + pw.println(" " + hotRodFieldType.toString() + " migrated = " + migrateToType(hotRodFieldType, firstParameterType, "p0") + ";"); + pw.println(" updated |= ! Objects.equals(" + hotRodEntityField(fieldName) + ", migrated);"); + pw.println(" " + hotRodEntityField(fieldName) + " = migrated;"); + pw.println(" }"); + return true; + case COLLECTION_ADD: + TypeMirror collectionItemType = getGenericsDeclaration(hotRodFieldType).get(0); + pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {"); + pw.println(" if (" + hotRodEntityField(fieldName) + " == null) { " + hotRodEntityField(fieldName) + " = " + interfaceToImplementation(typeElement, "") + "; }"); + if (! isImmutableFinalType(firstParameterType)) { + pw.println(" p0 = " + deepClone(fieldType, "p0") + ";"); + } + pw.println(" " + collectionItemType.toString() + " migrated = " + migrateToType(collectionItemType, firstParameterType, "p0") + ";"); + if (isSetType(typeElement)) { + pw.println(" updated |= " + hotRodEntityField(fieldName) + ".add(migrated);"); + } else { + pw.println(" " + hotRodEntityField(fieldName) + ".add(migrated);"); + pw.println(" updated = true;"); + } + pw.println(" }"); + return true; + case COLLECTION_DELETE: + collectionItemType = getGenericsDeclaration(hotRodFieldType).get(0); + pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {"); + if (isMapType(typeElement)) { + // Maps are stored as sets + pw.println(" this.updated |= " + hotRodUtils.getQualifiedName().toString() + ".removeFromSetByMapKey(" + + hotRodEntityField(fieldName) + ", " + + "p0, " + + keyGetterReference(collectionItemType) + ");" + ); + } else { + pw.println(" if (" + hotRodEntityField(fieldName) + " == null) { return; }"); + pw.println(" boolean removed = " + hotRodEntityField(fieldName) + ".remove(p0);"); + pw.println(" updated |= removed;"); + } + pw.println(" }"); + return true; + case MAP_ADD: + collectionItemType = getGenericsDeclaration(hotRodFieldType).get(0); + TypeMirror secondParameterType = method.getParameters().get(1).asType(); + pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0, " + secondParameterType + " p1) {"); + pw.println(" if (" + hotRodEntityField(fieldName) + " == null) { " + hotRodEntityField(fieldName) + " = " + interfaceToImplementation((TypeElement) types.asElement(types.erasure(hotRodFieldType)), "") + "; }"); + pw.println(" boolean valueUndefined = p1 == null" + (isCollection(secondParameterType) ? " || p1.isEmpty()" : "") + ";"); + if (! isImmutableFinalType(secondParameterType)) { + pw.println(" p1 = " + deepClone(secondParameterType, "p1") + ";"); + } + pw.println(" this.updated |= " + hotRodUtils.getQualifiedName().toString() + ".removeFromSetByMapKey(" + + hotRodEntityField(fieldName) + ", " + + "p0, " + + keyGetterReference(collectionItemType) + ");" + ); + pw.println(" this.updated |= !valueUndefined && " + hotRodEntityField(fieldName) + + ".add(" + migrateToType(collectionItemType, new TypeMirror[]{firstParameterType, secondParameterType}, new String[]{"p0", "p1"}) + ");"); + pw.println(" }"); + return true; + case MAP_GET: + pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {"); + collectionItemType = getGenericsDeclaration(hotRodFieldType).get(0); + pw.println(" return " + hotRodUtils.getQualifiedName().toString() + ".getMapValueFromSet(" + + hotRodEntityField(fieldName) + ", " + + "p0, " + + keyGetterReference(collectionItemType) + ", " + + valueGetterReference(collectionItemType) + ");" + ); + pw.println(" }"); + return true; + } + + return false; + } + + private String migrateToType(TypeMirror toType, TypeMirror fromType, String fieldName) { + return migrateToType(toType, new TypeMirror[] {fromType}, new String[]{fieldName}); + } + + private String toSimpleName(TypeMirror typeMirror) { + TypeElement e = elements.getTypeElement(types.erasure(typeMirror).toString()); + return e.getSimpleName().toString(); + } + + private String keyGetterReference(TypeMirror type) { + if (types.isAssignable(type, abstractHotRodEntity.asType())) { + return "e -> e.id"; + } + return hotRodUtils.getQualifiedName().toString() + "::getKey"; + } + + private String valueGetterReference(TypeMirror type) { + if (types.isAssignable(type, abstractHotRodEntity.asType())) { + return toSimpleName(type) + "Delegate::new"; + } + return hotRodUtils.getQualifiedName().toString() + "::getValue"; + } + + private String migrateToType(TypeMirror toType, TypeMirror[] fromType, String[] fieldNames) { + // No migration needed, fromType is assignable to toType directly + if (fromType.length == 1 && types.isAssignable(types.erasure(fromType[0]), types.erasure(toType))) { + return fieldNames[0]; + } + + // HotRod entities are not allowed to use Maps, therefore we often need to migrate from Map to Set and the other way around + if (fromType.length == 1) { + if (isSetType((TypeElement) types.asElement(types.erasure(toType))) + && isMapType((TypeElement) types.asElement(types.erasure(fromType[0])))) { + TypeMirror setType = getGenericsDeclaration(toType).get(0); + + return hotRodUtils.getQualifiedName().toString() + ".migrateMapToSet(" + + fieldNames[0] + ", " + + hotRodUtils.getQualifiedName().toString() + "::create" + toSimpleName(setType) + "FromMapEntry)"; + } else if (isMapType((TypeElement) types.asElement(types.erasure(toType))) + && isSetType((TypeElement) types.asElement(types.erasure(fromType[0])))) { + TypeMirror setType = getGenericsDeclaration(fromType[0]).get(0); + + return hotRodUtils.getQualifiedName().toString() + ".migrateSetToMap(" + + fieldNames[0] + ", " + + keyGetterReference(setType) + ", " + + valueGetterReference(setType) + + ")"; + } + + } + + // Try to find constructor that can do the migration + if (findSuitableConstructor(toType, fromType).isPresent()) { + return "new " + toType.toString() + "(" + String.join(", ", fieldNames) + ")"; + } + + // Check if any of parameters is another Map*Entity + OptionalInt anotherMapEntityIndex = IntStream.range(0, fromType.length) + .filter(i -> types.isAssignable(fromType[i], abstractEntity.asType())) + .findFirst(); + + if (anotherMapEntityIndex.isPresent()) { + // If yes, we can be sure that it implements HotRodEntityDelegate (this is achieved by HotRod cloner settings) so we can just call getHotRodEntity method + return "((" + generalHotRodDelegate.getQualifiedName().toString() + "<" + toType.toString() + ">) " + fieldNames[anotherMapEntityIndex.getAsInt()] + ").getHotRodEntity()"; + } + + // Check if any of parameters is another HotRod*Entity + OptionalInt anotherHotRodEntityIndex = IntStream.range(0, fromType.length) + .filter(i -> types.isAssignable(fromType[i], abstractHotRodEntity.asType())) + .findFirst(); + + if (anotherHotRodEntityIndex.isPresent()) { + // If yes, we can be sure that it implements HotRodEntityDelegate (this is achieved by HotRod cloner settings) so we can just call getHotRodEntity method + return "new " + fromType[anotherHotRodEntityIndex.getAsInt()] + "Delegate(" + String.join(", ", fieldNames) + ")"; + } + + throw new CannotMigrateTypeException(toType, fromType); + } + + private Optional findSuitableConstructor(TypeMirror desiredType, TypeMirror[] parameters) { + // Try to find constructor that can do the migration + TypeElement type = (TypeElement) types.asElement(desiredType); + return elements.getAllMembers(type) + .stream() + .filter(ExecutableElement.class::isInstance) + .map(ExecutableElement.class::cast) + .filter(ee -> ee.getKind() == ElementKind.CONSTRUCTOR) + .filter(ee -> ee.getParameters().size() == parameters.length) + .filter(method -> IntStream.range(0, parameters.length).allMatch(i -> deepCompareTypes(parameters[i], method.getParameters().get(i).asType()))) + .findFirst(); + } + + + private boolean deepCompareTypes(TypeMirror fromType, TypeMirror toType) { + return types.isAssignable(types.erasure(fromType), types.erasure(toType)) + && deepCompareTypes(getGenericsDeclaration(fromType), getGenericsDeclaration(toType)); + } + + private boolean deepCompareTypes(List fromTypes, List toTypes) { + if (fromTypes.size() == 0 && toTypes.size() == 0) return true; + if (fromTypes.size() != toTypes.size()) return false; + + for (int i = 0; i < fromTypes.size(); i++) { + if (!deepCompareTypes(fromTypes.get(i), toTypes.get(i))) return false; + } + return true; + } + } +} diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/processor/Util.java b/model/build-processor/src/main/java/org/keycloak/models/map/processor/Util.java index bc82b92987..b0c4ecdc17 100644 --- a/model/build-processor/src/main/java/org/keycloak/models/map/processor/Util.java +++ b/model/build-processor/src/main/java/org/keycloak/models/map/processor/Util.java @@ -18,10 +18,12 @@ package org.keycloak.models.map.processor; import org.keycloak.models.map.annotations.IgnoreForEntityImplementationGenerator; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; @@ -40,6 +42,7 @@ import javax.lang.model.util.SimpleTypeVisitor8; 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())); + private static final HashSet MAP_TYPES = new HashSet<>(Arrays.asList(Map.class.getCanonicalName(), HashMap.class.getCanonicalName())); public static List getGenericsDeclaration(TypeMirror fieldType) { List res = new LinkedList<>(); @@ -67,6 +70,11 @@ public class Util { return SET_TYPES.contains(name.toString()); } + public static boolean isMapType(TypeElement typeElement) { + Name name = typeElement.getQualifiedName(); + return MAP_TYPES.contains(name.toString()); + } + public static boolean isNotIgnored(Element el) { do { IgnoreForEntityImplementationGenerator a = el.getAnnotation(IgnoreForEntityImplementationGenerator.class); diff --git a/model/map-hot-rod/pom.xml b/model/map-hot-rod/pom.xml index 6e9bc3b124..837062768e 100644 --- a/model/map-hot-rod/pom.xml +++ b/model/map-hot-rod/pom.xml @@ -58,6 +58,44 @@ hamcrest test + + org.keycloak + keycloak-model-build-processor + ${project.version} + true + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.infinispan.protostream + protostream-processor + ${infinispan.protostream.processor.version} + + + org.keycloak + keycloak-model-build-processor + ${project.version} + + + javax.annotation + javax.annotation-api + ${javax.annotation-api.version} + + + + org.infinispan.protostream.annotations.impl.processor.AutoProtoSchemaBuilderAnnotationProcessor + org.keycloak.models.map.processor.GenerateHotRodEntityImplementationsProcessor + + + + + + diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java index 0e3644ab3f..dd059ffe78 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java @@ -26,9 +26,10 @@ import org.jboss.logging.Logger; import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity; +import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDelegate; import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDescriptor; import org.keycloak.models.map.common.StringKeyConvertor; -import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.QueryParameters; @@ -42,6 +43,7 @@ import java.util.Map; import java.util.Objects; import java.util.Spliterators; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -49,20 +51,22 @@ import java.util.stream.StreamSupport; import static org.keycloak.models.map.storage.hotRod.common.HotRodUtils.paginateQuery; import static org.keycloak.utils.StreamsUtil.closing; -public class HotRodMapStorage implements MapStorage, ConcurrentHashMapCrudOperations { +public class HotRodMapStorage, M> implements MapStorage, ConcurrentHashMapCrudOperations { private static final Logger LOG = Logger.getLogger(HotRodMapStorage.class); - private final RemoteCache remoteCache; + private final RemoteCache remoteCache; private final StringKeyConvertor keyConvertor; - private final HotRodEntityDescriptor storedEntityDescriptor; + private final HotRodEntityDescriptor storedEntityDescriptor; + private final Function delegateProducer; private final DeepCloner cloner; - public HotRodMapStorage(RemoteCache remoteCache, StringKeyConvertor keyConvertor, HotRodEntityDescriptor storedEntityDescriptor, DeepCloner cloner) { + public HotRodMapStorage(RemoteCache remoteCache, StringKeyConvertor keyConvertor, HotRodEntityDescriptor storedEntityDescriptor, DeepCloner cloner) { this.remoteCache = remoteCache; this.keyConvertor = keyConvertor; this.storedEntityDescriptor = storedEntityDescriptor; this.cloner = cloner; + this.delegateProducer = storedEntityDescriptor.getHotRodDelegateProvider(); } @Override @@ -73,7 +77,7 @@ public class HotRodMapStorage value = cloner.from(keyConvertor.keyToString(key), value); } - remoteCache.putIfAbsent(key, value); + remoteCache.putIfAbsent(key, value.getHotRodEntity()); return value; } @@ -82,13 +86,13 @@ public class HotRodMapStorage public V read(String key) { Objects.requireNonNull(key, "Key must be non-null"); K k = keyConvertor.fromStringSafe(key); - return remoteCache.get(k); + return delegateProducer.apply(remoteCache.get(k)); } @Override public V update(V value) { K key = keyConvertor.fromStringSafe(value.getId()); - return remoteCache.replace(key, value); + return delegateProducer.apply(remoteCache.replace(key, value.getHotRodEntity())); } @Override @@ -120,14 +124,15 @@ public class HotRodMapStorage QueryFactory queryFactory = Search.getQueryFactory(remoteCache); - Query query = paginateQuery(queryFactory.create(queryString), queryParameters.getOffset(), + Query query = paginateQuery(queryFactory.create(queryString), queryParameters.getOffset(), queryParameters.getLimit()); query.setParameters(iqmcb.getParameters()); - CloseableIterator iterator = query.iterator(); + CloseableIterator iterator = query.iterator(); return closing(StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false)) - .onClose(iterator::close); + .onClose(iterator::close) + .map(this.delegateProducer); } @Override diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java index 92f45dad0e..438b4d7758 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java @@ -19,9 +19,10 @@ package org.keycloak.models.map.storage.hotRod; import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity; +import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDelegate; import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDescriptor; import org.keycloak.models.map.common.StringKeyConvertor; -import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionProvider; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.MapStorageProvider; @@ -46,8 +47,8 @@ public class HotRodMapStorageProvider implements MapStorageProvider { } @SuppressWarnings("unchecked") - public HotRodMapStorage getHotRodStorage(Class modelType, MapStorageProviderFactory.Flag... flags) { - HotRodEntityDescriptor entityDescriptor = (HotRodEntityDescriptor) factory.getEntityDescriptor(modelType); + public , M> HotRodMapStorage getHotRodStorage(Class modelType, MapStorageProviderFactory.Flag... flags) { + HotRodEntityDescriptor entityDescriptor = (HotRodEntityDescriptor) factory.getEntityDescriptor(modelType); return new HotRodMapStorage<>(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConvertor.StringKey.INSTANCE, entityDescriptor, cloner); } diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java index 3350918d3d..ede6dad3cb 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java @@ -24,10 +24,9 @@ import org.keycloak.component.AmphibianProviderFactory; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.map.storage.hotRod.client.HotRodAttributeEntity; import org.keycloak.models.map.storage.hotRod.client.HotRodClientEntity; -import org.keycloak.models.map.storage.hotRod.client.HotRodProtocolMapperEntity; -import org.keycloak.models.map.storage.hotRod.common.HotRodPair; +import org.keycloak.models.map.storage.hotRod.client.HotRodClientEntityDelegate; +import org.keycloak.models.map.storage.hotRod.client.HotRodProtocolMapperEntityDelegate; import org.keycloak.models.map.client.MapClientEntity; import org.keycloak.models.map.client.MapProtocolMapperEntity; import org.keycloak.models.map.common.DeepCloner; @@ -37,7 +36,6 @@ import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.MapStorageProviderFactory; import org.keycloak.provider.EnvironmentDependentProviderFactory; -import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -47,18 +45,17 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory private static final Logger LOG = Logger.getLogger(HotRodMapStorageProviderFactory.class); private final static DeepCloner CLONER = new DeepCloner.Builder() - .constructorDC(MapClientEntity.class, HotRodClientEntity::new) - .constructor(MapProtocolMapperEntity.class, HotRodProtocolMapperEntity::new) + .constructorDC(MapClientEntity.class, HotRodClientEntityDelegate::new) + .constructor(MapProtocolMapperEntity.class, HotRodProtocolMapperEntityDelegate::new) .build(); - public static final Map, HotRodEntityDescriptor> ENTITY_DESCRIPTOR_MAP = new HashMap<>(); + public static final Map, HotRodEntityDescriptor> ENTITY_DESCRIPTOR_MAP = new HashMap<>(); static { // Clients descriptor ENTITY_DESCRIPTOR_MAP.put(ClientModel.class, new HotRodEntityDescriptor<>(ClientModel.class, - MapClientEntity.class, - Arrays.asList(HotRodClientEntity.class, HotRodAttributeEntity.class, HotRodProtocolMapperEntity.class, HotRodPair.class), - "clients")); + HotRodClientEntity.class, + HotRodClientEntityDelegate::new)); } @Override @@ -72,7 +69,7 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory return new HotRodMapStorageProvider(this, cacheProvider, CLONER); } - public HotRodEntityDescriptor getEntityDescriptor(Class c) { + public HotRodEntityDescriptor getEntityDescriptor(Class c) { return ENTITY_DESCRIPTOR_MAP.get(c); } diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/client/HotRodClientEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/client/HotRodClientEntity.java index 3f2c75292c..bb6a8f7ec7 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/client/HotRodClientEntity.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/client/HotRodClientEntity.java @@ -18,25 +18,26 @@ package org.keycloak.models.map.storage.hotRod.client; import org.infinispan.protostream.annotations.ProtoField; -import org.keycloak.models.map.client.MapClientEntity; -import org.keycloak.models.map.client.MapProtocolMapperEntity; -import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation; +import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity; +import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDelegate; import org.keycloak.models.map.storage.hotRod.common.HotRodPair; -import org.keycloak.models.map.storage.hotRod.common.Versioned; +import org.keycloak.models.map.client.MapClientEntity; +import org.keycloak.models.map.common.UpdatableEntity; import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; +import java.util.LinkedList; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; import java.util.stream.Stream; -public class HotRodClientEntity implements MapClientEntity, Versioned { + +@GenerateHotRodEntityImplementation( + implementInterface = "org.keycloak.models.map.client.MapClientEntity", + inherits = "org.keycloak.models.map.storage.hotRod.client.HotRodClientEntity.AbstractHotRodClientEntityDelegate" +) +public class HotRodClientEntity implements AbstractHotRodEntity { @ProtoField(number = 1, required = true) public int entityVersion = 1; @@ -57,7 +58,7 @@ public class HotRodClientEntity implements MapClientEntity, Versioned { public String description; @ProtoField(number = 7) - public Set redirectUris = new HashSet<>(); + public Set redirectUris; @ProtoField(number = 8) public Boolean enabled; @@ -78,10 +79,10 @@ public class HotRodClientEntity implements MapClientEntity, Versioned { public String protocol; @ProtoField(number = 14) - public Set attributes = new HashSet<>(); + public Set attributes; @ProtoField(number = 15) - public Set> authFlowBindings = new HashSet<>(); + public Set> authenticationFlowBindingOverrides; @ProtoField(number = 16) public Boolean publicClient; @@ -96,19 +97,19 @@ public class HotRodClientEntity implements MapClientEntity, Versioned { public Integer notBefore; @ProtoField(number = 20) - public Set scope = new HashSet<>(); + public Set scope; @ProtoField(number = 21) - public Set webOrigins = new HashSet<>(); + public Set webOrigins; @ProtoField(number = 22) - public Set protocolMappers = new HashSet<>(); + public Set protocolMappers; @ProtoField(number = 23) - public Set> clientScopes = new HashSet<>(); + public Set> clientScopes; - @ProtoField(number = 24) - public Set scopeMappings = new HashSet<>(); + @ProtoField(number = 24, collectionImplementation = LinkedList.class) + public Collection scopeMappings; @ProtoField(number = 25) public Boolean surrogateAuthRequired; @@ -143,572 +144,37 @@ public class HotRodClientEntity implements MapClientEntity, Versioned { @ProtoField(number = 35) public Integer nodeReRegistrationTimeout; - private boolean updated = false; + public static abstract class AbstractHotRodClientEntityDelegate extends UpdatableEntity.Impl implements HotRodEntityDelegate, MapClientEntity { - private final DeepCloner cloner; - - public HotRodClientEntity() { - this(DeepCloner.DUMB_CLONER); - } - - public HotRodClientEntity(DeepCloner cloner) { - this.cloner = cloner; - } - - @Override - public int getEntityVersion() { - return entityVersion; - } - - @Override - public List getAttribute(String name) { - return attributes.stream() - .filter(attributeEntity -> Objects.equals(attributeEntity.getName(), name)) - .findFirst() - .map(HotRodAttributeEntity::getValues) - .orElse(Collections.emptyList()); - } - - @Override - public Map> getAttributes() { - return attributes.stream().collect(Collectors.toMap(HotRodAttributeEntity::getName, HotRodAttributeEntity::getValues)); - } - - @Override - public void setAttribute(String name, List values) { - boolean valueUndefined = values == null || values.isEmpty(); - - Optional first = attributes.stream() - .filter(attributeEntity -> Objects.equals(attributeEntity.getName(), name)) - .findFirst(); - - if (first.isPresent()) { - HotRodAttributeEntity attributeEntity = first.get(); - if (valueUndefined) { - this.updated = true; - removeAttribute(name); - } else { - this.updated |= !Objects.equals(attributeEntity.getValues(), values); - attributeEntity.setValues(values); - } - - return; + @Override + public String getId() { + return getHotRodEntity().id; } - // do not create attributes if empty or null - if (valueUndefined) { - return; + @Override + public void setId(String id) { + HotRodClientEntity entity = getHotRodEntity(); + if (entity.id != null) throw new IllegalStateException("Id cannot be changed"); + entity.id = id; + this.updated |= id != null; } - HotRodAttributeEntity newAttributeEntity = new HotRodAttributeEntity(name, values); - updated |= attributes.add(newAttributeEntity); - } - - @Override - public void removeAttribute(String name) { - attributes.stream() - .filter(attributeEntity -> Objects.equals(attributeEntity.getName(), name)) - .findFirst() - .ifPresent(attr -> { - this.updated |= attributes.remove(attr); - }); - } - - @Override - public String getClientId() { - return clientId; - } - - @Override - public void setClientId(String clientId) { - this.updated |= ! Objects.equals(this.clientId, clientId); - this.clientId = clientId; - } - - @Override - public String getName() { - return name; - } - - @Override - public void setName(String name) { - this.updated |= ! Objects.equals(this.name, name); - this.name = name; - } - - @Override - public String getDescription() { - return description; - } - - @Override - public void setDescription(String description) { - this.updated |= ! Objects.equals(this.description, description); - this.description = description; - } - - @Override - public Set getRedirectUris() { - return redirectUris; - } - - @Override - public void setRedirectUris(Set redirectUris) { - if (redirectUris == null || redirectUris.isEmpty()) { - this.updated |= !this.redirectUris.isEmpty(); - this.redirectUris.clear(); - return; - } - - this.updated |= ! Objects.equals(this.redirectUris, redirectUris); - this.redirectUris.clear(); - this.redirectUris.addAll(redirectUris); - } - - @Override - public Boolean isEnabled() { - return enabled; - } - - @Override - public void setEnabled(Boolean enabled) { - this.updated |= ! Objects.equals(this.enabled, enabled); - this.enabled = enabled; - } - - @Override - public Boolean isAlwaysDisplayInConsole() { - return alwaysDisplayInConsole; - } - - @Override - public void setAlwaysDisplayInConsole(Boolean alwaysDisplayInConsole) { - this.updated |= ! Objects.equals(this.alwaysDisplayInConsole, alwaysDisplayInConsole); - this.alwaysDisplayInConsole = alwaysDisplayInConsole; - } - - @Override - public String getClientAuthenticatorType() { - return clientAuthenticatorType; - } - - @Override - public void setClientAuthenticatorType(String clientAuthenticatorType) { - this.updated |= ! Objects.equals(this.clientAuthenticatorType, clientAuthenticatorType); - this.clientAuthenticatorType = clientAuthenticatorType; - } - - @Override - public String getSecret() { - return secret; - } - - @Override - public void setSecret(String secret) { - this.updated |= ! Objects.equals(this.secret, secret); - this.secret = secret; - } - - @Override - public String getRegistrationToken() { - return registrationToken; - } - - @Override - public void setRegistrationToken(String registrationToken) { - this.updated |= ! Objects.equals(this.registrationToken, registrationToken); - this.registrationToken = registrationToken; - } - - @Override - public String getProtocol() { - return protocol; - } - - @Override - public void setProtocol(String protocol) { - this.updated |= ! Objects.equals(this.protocol, protocol); - this.protocol = protocol; - } - - @Override - public Map getAuthFlowBindings() { - return authFlowBindings.stream().collect(Collectors.toMap(HotRodPair::getFirst, HotRodPair::getSecond)); - } - - @Override - public void setAuthFlowBindings(Map authFlowBindings) { - if (authFlowBindings == null || authFlowBindings.isEmpty()) { - this.updated |= !this.authFlowBindings.isEmpty(); - this.authFlowBindings.clear(); - return; - } - - this.updated = true; - this.authFlowBindings.clear(); - this.authFlowBindings.addAll(authFlowBindings.entrySet().stream().map(e -> new HotRodPair<>(e.getKey(), e.getValue())).collect(Collectors.toSet())); - } - - @Override - public Boolean isPublicClient() { - return publicClient; - } - - @Override - public void setPublicClient(Boolean publicClient) { - this.updated |= ! Objects.equals(this.publicClient, publicClient); - this.publicClient = publicClient; - } - - @Override - public void setRealmId(String realmId) { - this.realmId = realmId; - } - - @Override - public Boolean isFullScopeAllowed() { - return fullScopeAllowed; - } - - @Override - public void setFullScopeAllowed(Boolean fullScopeAllowed) { - this.updated |= ! Objects.equals(this.fullScopeAllowed, fullScopeAllowed); - this.fullScopeAllowed = fullScopeAllowed; - } - - @Override - public Boolean isFrontchannelLogout() { - return frontchannelLogout; - } - - @Override - public void setFrontchannelLogout(Boolean frontchannelLogout) { - this.updated |= ! Objects.equals(this.frontchannelLogout, frontchannelLogout); - this.frontchannelLogout = frontchannelLogout; - } - - @Override - public Integer getNotBefore() { - return notBefore; - } - - @Override - public void setNotBefore(Integer notBefore) { - this.updated |= ! Objects.equals(this.notBefore, notBefore); - this.notBefore = notBefore; - } - - @Override - public Set getScope() { - return scope; - } - - @Override - public void setScope(Set scope) { - if (scope == null || scope.isEmpty()) { - this.updated |= !this.scope.isEmpty(); - this.scope.clear(); - return; - } - - this.updated |= ! Objects.equals(this.scope, scope); - this.scope.clear(); - this.scope.addAll(scope); - } - - @Override - public Set getWebOrigins() { - return webOrigins; - } - - @Override - public void setWebOrigins(Set webOrigins) { - if (webOrigins == null || webOrigins.isEmpty()) { - this.updated |= !this.webOrigins.isEmpty(); - this.webOrigins.clear(); - return; - } - - this.updated |= ! Objects.equals(this.webOrigins, webOrigins); - this.webOrigins.clear(); - this.webOrigins.addAll(webOrigins); - } - - @Override - public Map getProtocolMappers() { - return protocolMappers.stream().collect(Collectors.toMap(HotRodProtocolMapperEntity::getId, Function.identity())); - } - - - @Override - public MapProtocolMapperEntity getProtocolMapper(String id) { - return protocolMappers.stream().filter(hotRodMapper -> Objects.equals(hotRodMapper.getId(), id)).findFirst().orElse(null); - } - - @Override - public void setProtocolMapper(String id, MapProtocolMapperEntity mapping) { - removeProtocolMapper(id); - - protocolMappers.add((HotRodProtocolMapperEntity) cloner.from(mapping)); // Workaround, will be replaced by cloners - this.updated = true; - } - - @Override - public void removeProtocolMapper(String id) { - protocolMappers.stream().filter(entity -> Objects.equals(id, entity.id)) - .findFirst() - .ifPresent(entity -> { - protocolMappers.remove(entity); - updated = true; - }); - } - - @Override - public Boolean isSurrogateAuthRequired() { - return surrogateAuthRequired; - } - - @Override - public void setSurrogateAuthRequired(Boolean surrogateAuthRequired) { - this.updated |= ! Objects.equals(this.surrogateAuthRequired, surrogateAuthRequired); - this.surrogateAuthRequired = surrogateAuthRequired; - } - - @Override - public String getManagementUrl() { - return managementUrl; - } - - @Override - public void setManagementUrl(String managementUrl) { - this.updated |= ! Objects.equals(this.managementUrl, managementUrl); - this.managementUrl = managementUrl; - } - - @Override - public String getRootUrl() { - return rootUrl; - } - - @Override - public void setRootUrl(String rootUrl) { - this.updated |= ! Objects.equals(this.rootUrl, rootUrl); - this.rootUrl = rootUrl; - } - - @Override - public String getBaseUrl() { - return baseUrl; - } - - @Override - public void setBaseUrl(String baseUrl) { - this.updated |= ! Objects.equals(this.baseUrl, baseUrl); - this.baseUrl = baseUrl; - } - - @Override - public Boolean isBearerOnly() { - return bearerOnly; - } - - @Override - public void setBearerOnly(Boolean bearerOnly) { - this.updated |= ! Objects.equals(this.bearerOnly, bearerOnly); - this.bearerOnly = bearerOnly; - } - - @Override - public Boolean isConsentRequired() { - return consentRequired; - } - - @Override - public void setConsentRequired(Boolean consentRequired) { - this.updated |= ! Objects.equals(this.consentRequired, consentRequired); - this.consentRequired = consentRequired; - } - - @Override - public Boolean isStandardFlowEnabled() { - return standardFlowEnabled; - } - - @Override - public void setStandardFlowEnabled(Boolean standardFlowEnabled) { - this.updated |= ! Objects.equals(this.standardFlowEnabled, standardFlowEnabled); - this.standardFlowEnabled = standardFlowEnabled; - } - - @Override - public Boolean isImplicitFlowEnabled() { - return implicitFlowEnabled; - } - - @Override - public void setImplicitFlowEnabled(Boolean implicitFlowEnabled) { - this.updated |= ! Objects.equals(this.implicitFlowEnabled, implicitFlowEnabled); - this.implicitFlowEnabled = implicitFlowEnabled; - } - - @Override - public Boolean isDirectAccessGrantsEnabled() { - return directAccessGrantsEnabled; - } - - @Override - public void setDirectAccessGrantsEnabled(Boolean directAccessGrantsEnabled) { - this.updated |= ! Objects.equals(this.directAccessGrantsEnabled, directAccessGrantsEnabled); - this.directAccessGrantsEnabled = directAccessGrantsEnabled; - } - - @Override - public Boolean isServiceAccountsEnabled() { - return serviceAccountsEnabled; - } - - @Override - public void setServiceAccountsEnabled(Boolean serviceAccountsEnabled) { - this.updated |= ! Objects.equals(this.serviceAccountsEnabled, serviceAccountsEnabled); - this.serviceAccountsEnabled = serviceAccountsEnabled; - } - - @Override - public Integer getNodeReRegistrationTimeout() { - return nodeReRegistrationTimeout; - } - - @Override - public void setNodeReRegistrationTimeout(Integer nodeReRegistrationTimeout) { - this.updated |= ! Objects.equals(this.nodeReRegistrationTimeout, nodeReRegistrationTimeout); - this.nodeReRegistrationTimeout = nodeReRegistrationTimeout; - } - - @Override - public void addWebOrigin(String webOrigin) { - updated = true; - this.webOrigins.add(webOrigin); - } - - @Override - public void removeWebOrigin(String webOrigin) { - updated |= this.webOrigins.remove(webOrigin); - } - - @Override - public void addRedirectUri(String redirectUri) { - this.updated |= ! this.redirectUris.contains(redirectUri); - this.redirectUris.add(redirectUri); - } - - @Override - public void removeRedirectUri(String redirectUri) { - updated |= this.redirectUris.remove(redirectUri); - } - - @Override - public String getAuthenticationFlowBindingOverride(String binding) { - return authFlowBindings.stream().filter(pair -> Objects.equals(pair.getFirst(), binding)).findFirst() - .map(HotRodPair::getSecond) - .orElse(null); - } - - @Override - public Map getAuthenticationFlowBindingOverrides() { - return this.authFlowBindings.stream().collect(Collectors.toMap(HotRodPair::getFirst, HotRodPair::getSecond)); - } - - @Override - public void removeAuthenticationFlowBindingOverride(String binding) { - this.authFlowBindings.stream().filter(pair -> Objects.equals(pair.getFirst(), binding)).findFirst() - .ifPresent(pair -> { - updated = true; - authFlowBindings.remove(pair); - }); - } - - @Override - public void setAuthenticationFlowBindingOverride(String binding, String flowId) { - this.updated = true; - - removeAuthenticationFlowBindingOverride(binding); - - this.authFlowBindings.add(new HotRodPair<>(binding, flowId)); - } - - @Override - public Collection getScopeMappings() { - return scopeMappings; - } - - @Override - public void addScopeMapping(String id) { - if (id != null) { - updated = true; - scopeMappings.add(id); + @Override + public Stream getClientScopes(boolean defaultScope) { + final Map clientScopes = getClientScopes(); + return clientScopes == null ? Stream.empty() : clientScopes.entrySet().stream() + .filter(me -> Objects.equals(me.getValue(), defaultScope)) + .map(Map.Entry::getKey); } } @Override - public void removeScopeMapping(String id) { - updated |= scopeMappings.remove(id); + public boolean equals(Object o) { + return HotRodClientEntityDelegate.entityEquals(this, o); } @Override - public Map getClientScopes() { - return this.clientScopes.stream().collect(Collectors.toMap(HotRodPair::getFirst, HotRodPair::getSecond)); - } - - @Override - public void setClientScope(String id, Boolean defaultScope) { - if (id != null) { - updated = true; - removeClientScope(id); - - this.clientScopes.add(new HotRodPair<>(id, defaultScope)); - } - } - - @Override - public void removeClientScope(String id) { - this.clientScopes.stream().filter(pair -> Objects.equals(pair.getFirst(), id)).findFirst() - .ifPresent(pair -> { - updated = true; - clientScopes.remove(pair); - }); - } - - @Override - public Stream getClientScopes(boolean defaultScope) { - return this.clientScopes.stream() - .filter(pair -> Objects.equals(pair.getSecond(), defaultScope)) - .map(HotRodPair::getFirst); - } - - @Override - public String getRealmId() { - return this.realmId; - } - - @Override - public String getId() { - return id; - } - - @Override - public void setId(String id) { - if (this.id != null) throw new IllegalStateException("Id cannot be changed"); - this.id = id; - this.updated |= id != null; - } - - @Override - public void clearUpdatedFlag() { - this.updated = false; - } - - @Override - public boolean isUpdated() { - return updated; + public int hashCode() { + return HotRodClientEntityDelegate.entityHashCode(this); } } diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/client/HotRodProtocolMapperEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/client/HotRodProtocolMapperEntity.java index 0b7a7e19dc..3d9aa3285b 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/client/HotRodProtocolMapperEntity.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/client/HotRodProtocolMapperEntity.java @@ -18,16 +18,15 @@ package org.keycloak.models.map.storage.hotRod.client; import org.infinispan.protostream.annotations.ProtoField; -import org.keycloak.models.map.client.MapProtocolMapperEntity; +import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation; +import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity; import org.keycloak.models.map.storage.hotRod.common.HotRodPair; -import java.util.LinkedHashSet; -import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.stream.Collectors; -public class HotRodProtocolMapperEntity implements MapProtocolMapperEntity { +@GenerateHotRodEntityImplementation(implementInterface = "org.keycloak.models.map.client.MapProtocolMapperEntity") +public class HotRodProtocolMapperEntity implements AbstractHotRodEntity { @ProtoField(number = 1) public String id; @ProtoField(number = 2) @@ -41,73 +40,15 @@ public class HotRodProtocolMapperEntity implements MapProtocolMapperEntity { // @ProtoField(number = 5) // public String consentText; @ProtoField(number = 5) - public Set> config = new LinkedHashSet<>(); - - private boolean updated; + public Set> config; @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - HotRodProtocolMapperEntity entity = (HotRodProtocolMapperEntity) o; - - return id.equals(entity.id); + return HotRodProtocolMapperEntityDelegate.entityEquals(this, o); } @Override public int hashCode() { - return id.hashCode(); - } - - @Override - public String getId() { - return id; - } - - @Override - public void setId(String id) { - updated |= !Objects.equals(this.id, id); - this.id = id; - } - - @Override - public String getName() { - return name; - } - - @Override - public void setName(String name) { - updated |= !Objects.equals(this.name, name); - this.name = name; - } - - @Override - public String getProtocolMapper() { - return protocolMapper; - } - - @Override - public void setProtocolMapper(String protocolMapper) { - updated |= !Objects.equals(this.protocolMapper, protocolMapper); - this.protocolMapper = protocolMapper; - } - - @Override - public Map getConfig() { - return config.stream().collect(Collectors.toMap(HotRodPair::getFirst, HotRodPair::getSecond)); - } - - @Override - public void setConfig(Map config) { - updated |= !Objects.equals(this.config, config); - this.config.clear(); - - config.entrySet().stream().map(entry -> new HotRodPair<>(entry.getKey(), entry.getValue())).forEach(this.config::add); - } - - @Override - public boolean isUpdated() { - return updated; + return HotRodProtocolMapperEntityDelegate.entityHashCode(this); } } diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/AbstractHotRodEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/AbstractHotRodEntity.java new file mode 100644 index 0000000000..85cfbb8036 --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/AbstractHotRodEntity.java @@ -0,0 +1,21 @@ +/* + * 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.storage.hotRod.common; + +public interface AbstractHotRodEntity { +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodEntityDelegate.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodEntityDelegate.java new file mode 100644 index 0000000000..b63d0db9a5 --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodEntityDelegate.java @@ -0,0 +1,25 @@ +/* + * 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.storage.hotRod.common; + +import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.common.UpdatableEntity; + +public interface HotRodEntityDelegate extends AbstractEntity, UpdatableEntity { + E getHotRodEntity(); +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodEntityDescriptor.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodEntityDescriptor.java index 5fd43f2889..c4b73777df 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodEntityDescriptor.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodEntityDescriptor.java @@ -17,35 +17,34 @@ package org.keycloak.models.map.storage.hotRod.common; -import java.util.List; -import java.util.stream.Stream; +import org.keycloak.models.map.storage.ModelEntityUtil; -public class HotRodEntityDescriptor { +import java.util.function.Function; + +public class HotRodEntityDescriptor> { private final Class modelTypeClass; - private final Class entityTypeClass; - private final List> hotRodClasses; - private final String cacheName; + private final Class entityTypeClass; + private final Function hotRodDelegateProvider; - public HotRodEntityDescriptor(Class modelTypeClass, Class entityTypeClass, List> hotRodClasses, String cacheName) { + public HotRodEntityDescriptor(Class modelTypeClass, Class entityTypeClass, Function hotRodDelegateProvider) { this.modelTypeClass = modelTypeClass; this.entityTypeClass = entityTypeClass; - this.hotRodClasses = hotRodClasses; - this.cacheName = cacheName; + this.hotRodDelegateProvider = hotRodDelegateProvider; } public Class getModelTypeClass() { return modelTypeClass; } - public Class getEntityTypeClass() { + public Class getEntityTypeClass() { return entityTypeClass; } - public Stream> getHotRodClasses() { - return hotRodClasses.stream(); + public String getCacheName() { + return ModelEntityUtil.getModelName(modelTypeClass); } - public String getCacheName() { - return cacheName; + public Function getHotRodDelegateProvider() { + return hotRodDelegateProvider; } } diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodPair.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodPair.java index d025e0d6d3..e893c26cd6 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodPair.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodPair.java @@ -20,33 +20,48 @@ package org.keycloak.models.map.storage.hotRod.common; import org.infinispan.protostream.WrappedMessage; import org.infinispan.protostream.annotations.ProtoField; +import java.util.Objects; + public class HotRodPair { @ProtoField(number = 1) - public WrappedMessage firstWrapped; + public WrappedMessage key; @ProtoField(number = 2) - public WrappedMessage secondWrapped; + public WrappedMessage value; public HotRodPair() {} public HotRodPair(T first, V second) { - this.firstWrapped = new WrappedMessage(first); - this.secondWrapped = new WrappedMessage(second); + this.key = new WrappedMessage(first); + this.value = new WrappedMessage(second); } - public T getFirst() { - return firstWrapped == null ? null : (T) firstWrapped.getValue(); + public T getKey() { + return key == null ? null : (T) key.getValue(); } - public V getSecond() { - return secondWrapped == null ? null : (V) secondWrapped.getValue(); + public V getValue() { + return value == null ? null : (V) value.getValue(); } - public void setFirst(T first) { - this.firstWrapped = new WrappedMessage(first); + public void setKey(T first) { + this.key = new WrappedMessage(first); } - public void setSecond(V second) { - this.secondWrapped = new WrappedMessage(second); + public void setValue(V second) { + this.value = new WrappedMessage(second); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + HotRodPair that = (HotRodPair) o; + return Objects.equals(key, that.key) && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(key, value); } } diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodTypesUtils.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodTypesUtils.java new file mode 100644 index 0000000000..0ff392761f --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodTypesUtils.java @@ -0,0 +1,79 @@ +/* + * 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.storage.hotRod.common; + +import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.storage.hotRod.client.HotRodAttributeEntity; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class HotRodTypesUtils { + + public static Set migrateMapToSet(Map map, Function, SetValue> creator) { + return map == null ? null : map.entrySet() + .stream() + .map(creator) + .collect(Collectors.toSet()); + } + + public static Map migrateSetToMap(Set set, Function keyProducer, Function valueProducer) { + return set == null ? null : set.stream().collect(Collectors.toMap(keyProducer, valueProducer)); + } + + public static HotRodPair createHotRodPairFromMapEntry(Map.Entry entry) { + return new HotRodPair<>(entry.getKey(), entry.getValue()); + } + + public static boolean removeFromSetByMapKey(Set set, KeyType key, Function keyGetter) { + if (set == null || set.isEmpty()) { return false; } + return set.stream() + .filter(entry -> Objects.equals(keyGetter.apply(entry), key)) + .findFirst() + .map(set::remove) + .orElse(false); + } + + public static MapValue getMapValueFromSet(Set set, MapKey key, Function keyGetter, Function valueGetter) { + return set == null ? null : set.stream().filter(entry -> Objects.equals(keyGetter.apply(entry), key)).findFirst().map(valueGetter).orElse(null); + } + + public static K getKey(HotRodPair hotRodPair) { + return hotRodPair.getKey(); + } + + public static V getValue(HotRodPair hotRodPair) { + return hotRodPair.getValue(); + } + + public static String getKey(HotRodAttributeEntity attributeEntity) { + return attributeEntity.name; + } + + public static List getValue(HotRodAttributeEntity attributeEntity) { + return attributeEntity.values; + } + + public static String getKey(AbstractEntity entity) { + return entity.getId(); + } +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/Versioned.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/Versioned.java index d099e4222f..e69de29bb2 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/Versioned.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/Versioned.java @@ -1,22 +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.storage.hotRod.common; - -public interface Versioned { - int getEntityVersion(); -} diff --git a/model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilderTest.java b/model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilderTest.java index c46419bf32..7bf32bb628 100644 --- a/model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilderTest.java +++ b/model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilderTest.java @@ -20,6 +20,7 @@ package org.keycloak.models.map.storage.hotRod; import org.junit.Test; import org.keycloak.models.ClientModel; import org.keycloak.models.map.storage.hotRod.client.HotRodClientEntity; +import org.keycloak.models.map.storage.hotRod.client.HotRodClientEntityDelegate; import org.keycloak.models.map.storage.ModelCriteriaBuilder; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; @@ -36,8 +37,8 @@ import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.crit public class IckleQueryMapModelCriteriaBuilderTest { @Test public void testSimpleIckleQuery() { - IckleQueryMapModelCriteriaBuilder v = new IckleQueryMapModelCriteriaBuilder<>(); - IckleQueryMapModelCriteriaBuilder mcb = v.compare(CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, 3); + IckleQueryMapModelCriteriaBuilder v = new IckleQueryMapModelCriteriaBuilder<>(); + IckleQueryMapModelCriteriaBuilder mcb = v.compare(CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, 3); assertThat(mcb.getIckleQuery(), is(equalTo("FROM org.keycloak.models.map.storage.hotrod.HotRodClientEntity c WHERE (c.clientId = :clientId0)"))); assertThat(mcb.getParameters().entrySet(), hasSize(1)); assertThat(mcb.getParameters(), hasEntry("clientId0", 3)); @@ -55,7 +56,7 @@ public class IckleQueryMapModelCriteriaBuilderTest { @Test public void testSimpleIckleQueryFlashedFromDefault() { DefaultModelCriteria v = criteria(); - IckleQueryMapModelCriteriaBuilder mcb = v.compare(CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, 3).flashToModelCriteriaBuilder(new IckleQueryMapModelCriteriaBuilder<>()); + IckleQueryMapModelCriteriaBuilder mcb = v.compare(CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, 3).flashToModelCriteriaBuilder(new IckleQueryMapModelCriteriaBuilder<>()); assertThat(mcb.getIckleQuery(), is(equalTo("FROM org.keycloak.models.map.storage.hotrod.HotRodClientEntity c WHERE (c.clientId = :clientId0)"))); assertThat(mcb.getParameters().entrySet(), hasSize(1)); assertThat(mcb.getParameters(), hasEntry("clientId0", 3)); diff --git a/model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/common/HotRodTypesUtilsTest.java b/model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/common/HotRodTypesUtilsTest.java new file mode 100644 index 0000000000..ff5cd09e47 --- /dev/null +++ b/model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/common/HotRodTypesUtilsTest.java @@ -0,0 +1,110 @@ +/* + * 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.storage.hotRod.common; + + +import org.junit.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasSize; +import static org.keycloak.models.map.storage.hotRod.common.HotRodTypesUtils.getMapValueFromSet; +import static org.keycloak.models.map.storage.hotRod.common.HotRodTypesUtils.migrateMapToSet; +import static org.keycloak.models.map.storage.hotRod.common.HotRodTypesUtils.migrateSetToMap; +import static org.keycloak.models.map.storage.hotRod.common.HotRodTypesUtils.removeFromSetByMapKey; + +public class HotRodTypesUtilsTest { + + @Test + public void testMigrateMapToSet() { + // Test null map + assertThat(migrateMapToSet((Map) null, Map.Entry::getKey), nullValue()); + + Map m = new HashMap<>(); + m.put("key", "value"); + + assertThat(migrateMapToSet(m, e -> e.getKey() + "#" + e.getValue()), + contains("key#value")); + } + + @Test + public void testMigrateSetToMap() { + // Test null map + assertThat(migrateSetToMap((Set) null, Function.identity(), Function.identity()), nullValue()); + + Set s = new HashSet<>(); + s.add("key#value"); + + Map result = HotRodTypesUtils.migrateSetToMap(s, e -> e.split("#")[0], e -> e.split("#")[1]); + + assertThat(result.keySet(), hasSize(1)); + assertThat(result, hasEntry("key", "value")); + } + + @Test + public void testRemoveFromSetByMapKey() { + assertThat(removeFromSetByMapKey((Set) null, null, Function.identity()), is(false)); + assertThat(removeFromSetByMapKey(Collections.emptySet(), null, Function.identity()), is(false)); + + Set s = new HashSet<>(); + s.add("key#value"); + s.add("key1#value1"); + s.add("key2#value2"); + + // Remove existing + Set testSet = new HashSet<>(s); + assertThat(removeFromSetByMapKey(testSet, "key", e -> e.split("#")[0]), is(true)); + assertThat(testSet, hasSize(2)); + assertThat(testSet, containsInAnyOrder("key1#value1", "key2#value2")); + + // Remove not existing + testSet = new HashSet<>(s); + assertThat(removeFromSetByMapKey(testSet, "key3", e -> e.split("#")[0]), is(false)); + assertThat(testSet, hasSize(3)); + assertThat(testSet, containsInAnyOrder("key#value", "key1#value1", "key2#value2")); + } + + @Test + public void testGetMapValueFromSet() { + assertThat(getMapValueFromSet((Set) null, null, Function.identity(), Function.identity()), nullValue()); + assertThat(getMapValueFromSet(Collections.emptySet(), "key", Function.identity(), Function.identity()), nullValue()); + + Set s = new HashSet<>(); + s.add("key#value"); + s.add("key1#value1"); + s.add("key2#value2"); + + // search existing + assertThat(getMapValueFromSet(s, "key", e -> e.split("#")[0], e -> e.split("#")[1]), is("value")); + assertThat(getMapValueFromSet(s, "key1", e -> e.split("#")[0], e -> e.split("#")[1]), is("value1")); + + // Search not existing + assertThat(getMapValueFromSet(s, "key3", e -> e.split("#")[0], e -> e.split("#")[1]), nullValue()); + } +} \ No newline at end of file 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 69e6be9d07..d87243d280 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 @@ -92,9 +92,6 @@ public interface MapClientEntity extends AbstractEntity, UpdatableEntity { void removeAttribute(String name); void setAttribute(String name, List values); - Map getAuthFlowBindings(); - void setAuthFlowBindings(Map authFlowBindings); - String getAuthenticationFlowBindingOverride(String binding); Map getAuthenticationFlowBindingOverrides(); void removeAuthenticationFlowBindingOverride(String binding); 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 eeee874dcb..25294a0158 100644 --- a/model/map/src/main/java/org/keycloak/models/map/client/MapProtocolMapperEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/client/MapProtocolMapperEntity.java @@ -17,6 +17,7 @@ package org.keycloak.models.map.client; import org.keycloak.models.map.annotations.GenerateEntityImplementations; +import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.UpdatableEntity; import java.util.Map; @@ -27,10 +28,7 @@ import java.util.Map; */ @GenerateEntityImplementations @DeepCloner.Root -public interface MapProtocolMapperEntity extends UpdatableEntity { - - String getId(); - void setId(String id); +public interface MapProtocolMapperEntity extends UpdatableEntity, AbstractEntity { String getName(); void setName(String name); diff --git a/pom.xml b/pom.xml index adee7393e4..f352f7f92c 100644 --- a/pom.xml +++ b/pom.xml @@ -79,6 +79,7 @@ 5.3.20.Final 11.0.9.Final 4.3.4.Final + 1.3.2 2.12.1 ${jackson.version} ${jackson.databind.version}