KEYCLOAK-19570 Add annotation processing for HotRod clients

This commit is contained in:
Michal Hajas 2021-11-23 16:46:17 +01:00 committed by Hynek Mlnařík
parent 95614e8b40
commit 7aaa33739b
24 changed files with 1259 additions and 929 deletions

View file

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

View file

@ -25,7 +25,7 @@ import java.lang.annotation.Target;
* *
* @author hmlnarik * @author hmlnarik
*/ */
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE, ElementType.METHOD}) @Target({ElementType.TYPE, ElementType.METHOD})
public @interface IgnoreForEntityImplementationGenerator { public @interface IgnoreForEntityImplementationGenerator {
} }

View file

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

View file

@ -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<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
elements = processingEnv.getElementUtils();
types = processingEnv.getTypeUtils();
for (TypeElement annotation : annotations) {
Set<? extends Element> 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<String, HashSet<ExecutableElement>> methodsPerAttributeMapping(TypeElement e) {
final List<? extends Element> allMembers = elements.getAllMembers(e);
Map<String, HashSet<ExecutableElement>> methodsPerAttribute = allMembers.stream()
.filter(el -> el.getKind() == ElementKind.METHOD)
.filter(el -> el.getModifiers().contains(Modifier.ABSTRACT))
.filter(Util::isNotIgnored)
.filter(ExecutableElement.class::isInstance)
.map(ExecutableElement.class::cast)
.filter(ee -> ! (ee.getReceiverType() instanceof NoType))
.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<ExecutableElement> 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<String, String> 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<ExecutableElement> fieldGetters(Map<String, HashSet<ExecutableElement>> methodsPerAttribute) {
return methodsPerAttribute.entrySet().stream()
.map(me -> FieldAccessorType.getMethod(GETTER, me.getValue(), me.getKey(), types, determineFieldType(me.getKey(), me.getValue())))
.filter(Optional::isPresent)
.map(Optional::get);
}
protected boolean isImmutableFinalType(TypeMirror fieldType) {
return isPrimitiveType(fieldType) || isBoxedPrimitiveType(fieldType) || Objects.equals("java.lang.String", fieldType.toString());
}
protected boolean isKnownCollectionOfImmutableFinalTypes(TypeMirror fieldType) {
List<TypeMirror> 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<ExecutableElement> 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<String> {
protected static final Comparator<String> ID_INSTANCE = new NameFirstComparator("id").thenComparing(Comparator.naturalOrder());
protected static final Comparator<String> GET_ID_INSTANCE = new NameFirstComparator("getId").thenComparing(Comparator.naturalOrder());
private final String name;
public NameFirstComparator(String name) {
this.name = name;
}
@Override
public int compare(String o1, String o2) {
return Objects.equals(o1, o2)
? 0
: name.equalsIgnoreCase(o1)
? -1
: name.equalsIgnoreCase(o2)
? 1
: 0;
}
}
}

View file

@ -19,18 +19,11 @@ package org.keycloak.models.map.processor;
import org.keycloak.models.map.annotations.GenerateEntityImplementations; import org.keycloak.models.map.annotations.GenerateEntityImplementations;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion; import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion; 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.Modifier;
import javax.lang.model.element.Name; import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeElement;
import javax.lang.model.type.NoType;
import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements; import javax.tools.Diagnostic;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic.Kind; import javax.tools.Diagnostic.Kind;
import javax.tools.JavaFileObject; import javax.tools.JavaFileObject;
import static org.keycloak.models.map.processor.FieldAccessorType.*; 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.isSetType;
import static org.keycloak.models.map.processor.Util.methodParameters; import static org.keycloak.models.map.processor.Util.methodParameters;
import java.util.Collection; import java.util.Collection;
@ -57,7 +47,6 @@ import java.util.Optional;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.stream.Stream;
import javax.lang.model.element.VariableElement; import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeKind;
@ -67,14 +56,8 @@ import javax.lang.model.type.TypeKind;
*/ */
@SupportedAnnotationTypes("org.keycloak.models.map.annotations.GenerateEntityImplementations") @SupportedAnnotationTypes("org.keycloak.models.map.annotations.GenerateEntityImplementations")
@SupportedSourceVersion(SourceVersion.RELEASE_8) @SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GenerateEntityImplementationsProcessor extends AbstractProcessor { public class GenerateEntityImplementationsProcessor extends AbstractGenerateEntityImplementationsProcessor {
private static interface Generator {
void generate(TypeElement e, Map<String, HashSet<ExecutableElement>> methodsPerAttribute) throws IOException;
}
private Elements elements;
private Types types;
private Collection<String> cloners = new TreeSet<>(); private Collection<String> cloners = new TreeSet<>();
private final Generator[] generators = new Generator[] { private final Generator[] generators = new Generator[] {
@ -85,23 +68,13 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor {
}; };
@Override @Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { protected void afterAnnotationProcessing() {
elements = processingEnv.getElementUtils(); if (! cloners.isEmpty()) {
types = processingEnv.getTypeUtils();
for (TypeElement annotation : annotations) {
Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(annotation);
annotatedElements.stream()
.map(TypeElement.class::cast)
.forEach(this::processTypeElement);
}
if (! cloners.isEmpty() && ! annotations.isEmpty()) {
try { try {
JavaFileObject file = processingEnv.getFiler().createSourceFile("org.keycloak.models.map.common.AutogeneratedCloners"); JavaFileObject file = processingEnv.getFiler().createSourceFile("org.keycloak.models.map.common.AutogeneratedCloners");
try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) { try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) {
pw.println("package org.keycloak.models.map.common;"); pw.println("package org.keycloak.models.map.common;");
pw.println("import org.keycloak.models.map.common.DeepCloner.Cloner;"); 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("// DO NOT CHANGE THIS CLASS, IT IS GENERATED AUTOMATICALLY BY " + GenerateEntityImplementationsProcessor.class.getSimpleName());
pw.println("public final class AutogeneratedCloners {"); 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); 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; 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<? extends Element> allMembers = elements.getAllMembers(e);
Map<String, HashSet<ExecutableElement>> methodsPerAttribute = allMembers.stream()
.filter(el -> el.getKind() == ElementKind.METHOD)
.filter(el -> el.getModifiers().contains(Modifier.ABSTRACT))
.filter(Util::isNotIgnored)
.filter(ExecutableElement.class::isInstance)
.map(ExecutableElement.class::cast)
.filter(ee -> ! (ee.getReceiverType() instanceof NoType))
.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<ExecutableElement> 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<String, String> 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) { protected static String toEnumConstant(String key) {
return key.replaceAll("([a-z])([A-Z])", "$1_$2").toUpperCase(); return key.replaceAll("([a-z])([A-Z])", "$1_$2").toUpperCase();
} }
private TypeMirror determineFieldType(String fieldName, HashSet<ExecutableElement> 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<TypeMirror> 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<String> {
private static final Comparator<String> ID_INSTANCE = new NameFirstComparator("id").thenComparing(Comparator.naturalOrder());
private static final Comparator<String> GET_ID_INSTANCE = new NameFirstComparator("getId").thenComparing(Comparator.naturalOrder());
private final String name;
public NameFirstComparator(String name) {
this.name = name;
}
@Override
public int compare(String o1, String o2) {
return Objects.equals(o1, o2)
? 0
: name.equalsIgnoreCase(o1)
? -1
: name.equalsIgnoreCase(o2)
? 1
: 0;
}
}
private class FieldsGenerator implements Generator { private class FieldsGenerator implements Generator {
@Override @Override
public void generate(TypeElement e, Map<String, HashSet<ExecutableElement>> methodsPerAttribute) throws IOException { public void generate(TypeElement e) throws IOException {
Map<String, HashSet<ExecutableElement>> methodsPerAttribute = methodsPerAttributeMapping(e);
String className = e.getQualifiedName().toString(); String className = e.getQualifiedName().toString();
String packageName = null; String packageName = null;
int lastDot = className.lastIndexOf('.'); int lastDot = className.lastIndexOf('.');
@ -318,7 +144,8 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor {
private class ImplGenerator implements Generator { private class ImplGenerator implements Generator {
@Override @Override
public void generate(TypeElement e, Map<String, HashSet<ExecutableElement>> methodsPerAttribute) throws IOException { public void generate(TypeElement e) throws IOException {
Map<String, HashSet<ExecutableElement>> methodsPerAttribute = methodsPerAttributeMapping(e);
GenerateEntityImplementations an = e.getAnnotation(GenerateEntityImplementations.class); GenerateEntityImplementations an = e.getAnnotation(GenerateEntityImplementations.class);
TypeElement parentTypeElement = elements.getTypeElement((an.inherits() == null || an.inherits().isEmpty()) ? "void" : an.inherits()); TypeElement parentTypeElement = elements.getTypeElement((an.inherits() == null || an.inherits().isEmpty()) ? "void" : an.inherits());
if (parentTypeElement == null) { if (parentTypeElement == null) {
@ -452,13 +279,6 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor {
} }
} }
private Stream<ExecutableElement> fieldGetters(Map<String, HashSet<ExecutableElement>> methodsPerAttribute) {
return methodsPerAttribute.entrySet().stream()
.map(me -> FieldAccessorType.getMethod(GETTER, me.getValue(), me.getKey(), types, determineFieldType(me.getKey(), me.getValue())))
.filter(Optional::isPresent)
.map(Optional::get);
}
private boolean printMethodBody(PrintWriter pw, FieldAccessorType accessorType, ExecutableElement method, String fieldName, TypeMirror fieldType) { private boolean printMethodBody(PrintWriter pw, FieldAccessorType accessorType, ExecutableElement method, String fieldName, TypeMirror fieldType) {
TypeMirror firstParameterType = method.getParameters().isEmpty() TypeMirror firstParameterType = method.getParameters().isEmpty()
? types.getNullType() ? types.getNullType()
@ -525,7 +345,8 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor {
private class DelegateGenerator implements Generator { private class DelegateGenerator implements Generator {
@Override @Override
public void generate(TypeElement e, Map<String, HashSet<ExecutableElement>> methodsPerAttribute) throws IOException { public void generate(TypeElement e) throws IOException {
Map<String, HashSet<ExecutableElement>> methodsPerAttribute = methodsPerAttributeMapping(e);
String className = e.getQualifiedName().toString(); String className = e.getQualifiedName().toString();
String packageName = null; String packageName = null;
int lastDot = className.lastIndexOf('.'); int lastDot = className.lastIndexOf('.');
@ -594,7 +415,8 @@ public class GenerateEntityImplementationsProcessor extends AbstractProcessor {
private class ClonerGenerator implements Generator { private class ClonerGenerator implements Generator {
@Override @Override
public void generate(TypeElement e, Map<String, HashSet<ExecutableElement>> methodsPerAttribute) throws IOException { public void generate(TypeElement e) throws IOException {
Map<String, HashSet<ExecutableElement>> methodsPerAttribute = methodsPerAttributeMapping(e);
String className = e.getQualifiedName().toString(); String className = e.getQualifiedName().toString();
String packageName = null; String packageName = null;
int lastDot = className.lastIndexOf('.'); int lastDot = className.lastIndexOf('.');

View file

@ -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<String, HashSet<ExecutableElement>> methodsPerAttribute = methodsPerAttributeMapping(parentInterfaceElement);
final List<? extends Element> 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> 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<ExecutableElement> 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<VariableElement> 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<ExecutableElement> parentMethod = allMembers.stream()
.filter(ExecutableElement.class::isInstance)
.map(ExecutableElement.class::cast)
.filter(ee -> Objects.equals(ee.toString(), method.toString()))
.filter((ExecutableElement ee) -> ! ee.getModifiers().contains(Modifier.ABSTRACT))
.findAny();
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<ExecutableElement> 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<TypeMirror> fromTypes, List<TypeMirror> 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;
}
}
}

View file

@ -18,10 +18,12 @@ package org.keycloak.models.map.processor;
import org.keycloak.models.map.annotations.IgnoreForEntityImplementationGenerator; import org.keycloak.models.map.annotations.IgnoreForEntityImplementationGenerator;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -40,6 +42,7 @@ import javax.lang.model.util.SimpleTypeVisitor8;
public class Util { public class Util {
private static final HashSet<String> SET_TYPES = new HashSet<>(Arrays.asList(Set.class.getCanonicalName(), TreeSet.class.getCanonicalName(), HashSet.class.getCanonicalName(), LinkedHashSet.class.getCanonicalName())); private static final HashSet<String> SET_TYPES = new HashSet<>(Arrays.asList(Set.class.getCanonicalName(), TreeSet.class.getCanonicalName(), HashSet.class.getCanonicalName(), LinkedHashSet.class.getCanonicalName()));
private static final HashSet<String> MAP_TYPES = new HashSet<>(Arrays.asList(Map.class.getCanonicalName(), HashMap.class.getCanonicalName()));
public static List<TypeMirror> getGenericsDeclaration(TypeMirror fieldType) { public static List<TypeMirror> getGenericsDeclaration(TypeMirror fieldType) {
List<TypeMirror> res = new LinkedList<>(); List<TypeMirror> res = new LinkedList<>();
@ -67,6 +70,11 @@ public class Util {
return SET_TYPES.contains(name.toString()); 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) { public static boolean isNotIgnored(Element el) {
do { do {
IgnoreForEntityImplementationGenerator a = el.getAnnotation(IgnoreForEntityImplementationGenerator.class); IgnoreForEntityImplementationGenerator a = el.getAnnotation(IgnoreForEntityImplementationGenerator.class);

View file

@ -58,6 +58,44 @@
<artifactId>hamcrest</artifactId> <artifactId>hamcrest</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-build-processor</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
</dependencies> </dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.infinispan.protostream</groupId>
<artifactId>protostream-processor</artifactId>
<version>${infinispan.protostream.processor.version}</version>
</path>
<path>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-build-processor</artifactId>
<version>${project.version}</version>
</path>
<path>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>${javax.annotation-api.version}</version>
</path>
</annotationProcessorPaths>
<annotationProcessors>
<annotationProcessor>org.infinispan.protostream.annotations.impl.processor.AutoProtoSchemaBuilderAnnotationProcessor</annotationProcessor>
<annotationProcessor>org.keycloak.models.map.processor.GenerateHotRodEntityImplementationsProcessor</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
</plugins>
</build>
</project> </project>

View file

@ -26,9 +26,10 @@ import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.DeepCloner; 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.storage.hotRod.common.HotRodEntityDescriptor;
import org.keycloak.models.map.common.StringKeyConvertor; 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.MapKeycloakTransaction;
import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.MapStorage;
import org.keycloak.models.map.storage.QueryParameters; import org.keycloak.models.map.storage.QueryParameters;
@ -42,6 +43,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Spliterators; import java.util.Spliterators;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import java.util.stream.StreamSupport; 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.models.map.storage.hotRod.common.HotRodUtils.paginateQuery;
import static org.keycloak.utils.StreamsUtil.closing; import static org.keycloak.utils.StreamsUtil.closing;
public class HotRodMapStorage<K, V extends AbstractEntity & UpdatableEntity, M> implements MapStorage<V, M>, ConcurrentHashMapCrudOperations<V, M> { public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends HotRodEntityDelegate<E>, M> implements MapStorage<V, M>, ConcurrentHashMapCrudOperations<V, M> {
private static final Logger LOG = Logger.getLogger(HotRodMapStorage.class); private static final Logger LOG = Logger.getLogger(HotRodMapStorage.class);
private final RemoteCache<K, V> remoteCache; private final RemoteCache<K, E> remoteCache;
private final StringKeyConvertor<K> keyConvertor; private final StringKeyConvertor<K> keyConvertor;
private final HotRodEntityDescriptor<V> storedEntityDescriptor; private final HotRodEntityDescriptor<E, V> storedEntityDescriptor;
private final Function<E, V> delegateProducer;
private final DeepCloner cloner; private final DeepCloner cloner;
public HotRodMapStorage(RemoteCache<K, V> remoteCache, StringKeyConvertor<K> keyConvertor, HotRodEntityDescriptor<V> storedEntityDescriptor, DeepCloner cloner) { public HotRodMapStorage(RemoteCache<K, E> remoteCache, StringKeyConvertor<K> keyConvertor, HotRodEntityDescriptor<E, V> storedEntityDescriptor, DeepCloner cloner) {
this.remoteCache = remoteCache; this.remoteCache = remoteCache;
this.keyConvertor = keyConvertor; this.keyConvertor = keyConvertor;
this.storedEntityDescriptor = storedEntityDescriptor; this.storedEntityDescriptor = storedEntityDescriptor;
this.cloner = cloner; this.cloner = cloner;
this.delegateProducer = storedEntityDescriptor.getHotRodDelegateProvider();
} }
@Override @Override
@ -73,7 +77,7 @@ public class HotRodMapStorage<K, V extends AbstractEntity & UpdatableEntity, M>
value = cloner.from(keyConvertor.keyToString(key), value); value = cloner.from(keyConvertor.keyToString(key), value);
} }
remoteCache.putIfAbsent(key, value); remoteCache.putIfAbsent(key, value.getHotRodEntity());
return value; return value;
} }
@ -82,13 +86,13 @@ public class HotRodMapStorage<K, V extends AbstractEntity & UpdatableEntity, M>
public V read(String key) { public V read(String key) {
Objects.requireNonNull(key, "Key must be non-null"); Objects.requireNonNull(key, "Key must be non-null");
K k = keyConvertor.fromStringSafe(key); K k = keyConvertor.fromStringSafe(key);
return remoteCache.get(k); return delegateProducer.apply(remoteCache.get(k));
} }
@Override @Override
public V update(V value) { public V update(V value) {
K key = keyConvertor.fromStringSafe(value.getId()); K key = keyConvertor.fromStringSafe(value.getId());
return remoteCache.replace(key, value); return delegateProducer.apply(remoteCache.replace(key, value.getHotRodEntity()));
} }
@Override @Override
@ -120,14 +124,15 @@ public class HotRodMapStorage<K, V extends AbstractEntity & UpdatableEntity, M>
QueryFactory queryFactory = Search.getQueryFactory(remoteCache); QueryFactory queryFactory = Search.getQueryFactory(remoteCache);
Query<V> query = paginateQuery(queryFactory.create(queryString), queryParameters.getOffset(), Query<E> query = paginateQuery(queryFactory.create(queryString), queryParameters.getOffset(),
queryParameters.getLimit()); queryParameters.getLimit());
query.setParameters(iqmcb.getParameters()); query.setParameters(iqmcb.getParameters());
CloseableIterator<V> iterator = query.iterator(); CloseableIterator<E> iterator = query.iterator();
return closing(StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false)) return closing(StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false))
.onClose(iterator::close); .onClose(iterator::close)
.map(this.delegateProducer);
} }
@Override @Override

View file

@ -19,9 +19,10 @@ package org.keycloak.models.map.storage.hotRod;
import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.DeepCloner; 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.storage.hotRod.common.HotRodEntityDescriptor;
import org.keycloak.models.map.common.StringKeyConvertor; 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.hotRod.connections.HotRodConnectionProvider;
import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.MapStorage;
import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.MapStorageProvider;
@ -46,8 +47,8 @@ public class HotRodMapStorageProvider implements MapStorageProvider {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <V extends AbstractEntity & UpdatableEntity, M> HotRodMapStorage<String, V, M> getHotRodStorage(Class<M> modelType, MapStorageProviderFactory.Flag... flags) { public <E extends AbstractHotRodEntity, V extends HotRodEntityDelegate<E>, M> HotRodMapStorage<String, E, V, M> getHotRodStorage(Class<M> modelType, MapStorageProviderFactory.Flag... flags) {
HotRodEntityDescriptor<V> entityDescriptor = (HotRodEntityDescriptor<V>) factory.getEntityDescriptor(modelType); HotRodEntityDescriptor<E, V> entityDescriptor = (HotRodEntityDescriptor<E, V>) factory.getEntityDescriptor(modelType);
return new HotRodMapStorage<>(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConvertor.StringKey.INSTANCE, entityDescriptor, cloner); return new HotRodMapStorage<>(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConvertor.StringKey.INSTANCE, entityDescriptor, cloner);
} }

View file

@ -24,10 +24,9 @@ import org.keycloak.component.AmphibianProviderFactory;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; 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.HotRodClientEntity;
import org.keycloak.models.map.storage.hotRod.client.HotRodProtocolMapperEntity; import org.keycloak.models.map.storage.hotRod.client.HotRodClientEntityDelegate;
import org.keycloak.models.map.storage.hotRod.common.HotRodPair; import org.keycloak.models.map.storage.hotRod.client.HotRodProtocolMapperEntityDelegate;
import org.keycloak.models.map.client.MapClientEntity; import org.keycloak.models.map.client.MapClientEntity;
import org.keycloak.models.map.client.MapProtocolMapperEntity; import org.keycloak.models.map.client.MapProtocolMapperEntity;
import org.keycloak.models.map.common.DeepCloner; 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.models.map.storage.MapStorageProviderFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.provider.EnvironmentDependentProviderFactory;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -47,18 +45,17 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory
private static final Logger LOG = Logger.getLogger(HotRodMapStorageProviderFactory.class); private static final Logger LOG = Logger.getLogger(HotRodMapStorageProviderFactory.class);
private final static DeepCloner CLONER = new DeepCloner.Builder() private final static DeepCloner CLONER = new DeepCloner.Builder()
.constructorDC(MapClientEntity.class, HotRodClientEntity::new) .constructorDC(MapClientEntity.class, HotRodClientEntityDelegate::new)
.constructor(MapProtocolMapperEntity.class, HotRodProtocolMapperEntity::new) .constructor(MapProtocolMapperEntity.class, HotRodProtocolMapperEntityDelegate::new)
.build(); .build();
public static final Map<Class<?>, HotRodEntityDescriptor<?>> ENTITY_DESCRIPTOR_MAP = new HashMap<>(); public static final Map<Class<?>, HotRodEntityDescriptor<?, ?>> ENTITY_DESCRIPTOR_MAP = new HashMap<>();
static { static {
// Clients descriptor // Clients descriptor
ENTITY_DESCRIPTOR_MAP.put(ClientModel.class, ENTITY_DESCRIPTOR_MAP.put(ClientModel.class,
new HotRodEntityDescriptor<>(ClientModel.class, new HotRodEntityDescriptor<>(ClientModel.class,
MapClientEntity.class, HotRodClientEntity.class,
Arrays.asList(HotRodClientEntity.class, HotRodAttributeEntity.class, HotRodProtocolMapperEntity.class, HotRodPair.class), HotRodClientEntityDelegate::new));
"clients"));
} }
@Override @Override
@ -72,7 +69,7 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory
return new HotRodMapStorageProvider(this, cacheProvider, CLONER); return new HotRodMapStorageProvider(this, cacheProvider, CLONER);
} }
public HotRodEntityDescriptor<?> getEntityDescriptor(Class<?> c) { public HotRodEntityDescriptor<?, ?> getEntityDescriptor(Class<?> c) {
return ENTITY_DESCRIPTOR_MAP.get(c); return ENTITY_DESCRIPTOR_MAP.get(c);
} }

View file

@ -18,25 +18,26 @@
package org.keycloak.models.map.storage.hotRod.client; package org.keycloak.models.map.storage.hotRod.client;
import org.infinispan.protostream.annotations.ProtoField; import org.infinispan.protostream.annotations.ProtoField;
import org.keycloak.models.map.client.MapClientEntity; import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation;
import org.keycloak.models.map.client.MapProtocolMapperEntity; import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity;
import org.keycloak.models.map.common.DeepCloner; 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.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.Collection;
import java.util.Collections; import java.util.LinkedList;
import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream; 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) @ProtoField(number = 1, required = true)
public int entityVersion = 1; public int entityVersion = 1;
@ -57,7 +58,7 @@ public class HotRodClientEntity implements MapClientEntity, Versioned {
public String description; public String description;
@ProtoField(number = 7) @ProtoField(number = 7)
public Set<String> redirectUris = new HashSet<>(); public Set<String> redirectUris;
@ProtoField(number = 8) @ProtoField(number = 8)
public Boolean enabled; public Boolean enabled;
@ -78,10 +79,10 @@ public class HotRodClientEntity implements MapClientEntity, Versioned {
public String protocol; public String protocol;
@ProtoField(number = 14) @ProtoField(number = 14)
public Set<HotRodAttributeEntity> attributes = new HashSet<>(); public Set<HotRodAttributeEntity> attributes;
@ProtoField(number = 15) @ProtoField(number = 15)
public Set<HotRodPair<String, String>> authFlowBindings = new HashSet<>(); public Set<HotRodPair<String, String>> authenticationFlowBindingOverrides;
@ProtoField(number = 16) @ProtoField(number = 16)
public Boolean publicClient; public Boolean publicClient;
@ -96,19 +97,19 @@ public class HotRodClientEntity implements MapClientEntity, Versioned {
public Integer notBefore; public Integer notBefore;
@ProtoField(number = 20) @ProtoField(number = 20)
public Set<String> scope = new HashSet<>(); public Set<String> scope;
@ProtoField(number = 21) @ProtoField(number = 21)
public Set<String> webOrigins = new HashSet<>(); public Set<String> webOrigins;
@ProtoField(number = 22) @ProtoField(number = 22)
public Set<HotRodProtocolMapperEntity> protocolMappers = new HashSet<>(); public Set<HotRodProtocolMapperEntity> protocolMappers;
@ProtoField(number = 23) @ProtoField(number = 23)
public Set<HotRodPair<String, Boolean>> clientScopes = new HashSet<>(); public Set<HotRodPair<String, Boolean>> clientScopes;
@ProtoField(number = 24) @ProtoField(number = 24, collectionImplementation = LinkedList.class)
public Set<String> scopeMappings = new HashSet<>(); public Collection<String> scopeMappings;
@ProtoField(number = 25) @ProtoField(number = 25)
public Boolean surrogateAuthRequired; public Boolean surrogateAuthRequired;
@ -143,572 +144,37 @@ public class HotRodClientEntity implements MapClientEntity, Versioned {
@ProtoField(number = 35) @ProtoField(number = 35)
public Integer nodeReRegistrationTimeout; public Integer nodeReRegistrationTimeout;
private boolean updated = false; public static abstract class AbstractHotRodClientEntityDelegate extends UpdatableEntity.Impl implements HotRodEntityDelegate<HotRodClientEntity>, MapClientEntity {
private final DeepCloner cloner; @Override
public String getId() {
public HotRodClientEntity() { return getHotRodEntity().id;
this(DeepCloner.DUMB_CLONER);
}
public HotRodClientEntity(DeepCloner cloner) {
this.cloner = cloner;
}
@Override
public int getEntityVersion() {
return entityVersion;
}
@Override
public List<String> getAttribute(String name) {
return attributes.stream()
.filter(attributeEntity -> Objects.equals(attributeEntity.getName(), name))
.findFirst()
.map(HotRodAttributeEntity::getValues)
.orElse(Collections.emptyList());
}
@Override
public Map<String, List<String>> getAttributes() {
return attributes.stream().collect(Collectors.toMap(HotRodAttributeEntity::getName, HotRodAttributeEntity::getValues));
}
@Override
public void setAttribute(String name, List<String> values) {
boolean valueUndefined = values == null || values.isEmpty();
Optional<HotRodAttributeEntity> 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;
} }
// do not create attributes if empty or null @Override
if (valueUndefined) { public void setId(String id) {
return; 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); @Override
updated |= attributes.add(newAttributeEntity); public Stream<String> getClientScopes(boolean defaultScope) {
} final Map<String, Boolean> clientScopes = getClientScopes();
return clientScopes == null ? Stream.empty() : clientScopes.entrySet().stream()
@Override .filter(me -> Objects.equals(me.getValue(), defaultScope))
public void removeAttribute(String name) { .map(Map.Entry::getKey);
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<String> getRedirectUris() {
return redirectUris;
}
@Override
public void setRedirectUris(Set<String> 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<String, String> getAuthFlowBindings() {
return authFlowBindings.stream().collect(Collectors.toMap(HotRodPair::getFirst, HotRodPair::getSecond));
}
@Override
public void setAuthFlowBindings(Map<String, String> 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<String> getScope() {
return scope;
}
@Override
public void setScope(Set<String> 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<String> getWebOrigins() {
return webOrigins;
}
@Override
public void setWebOrigins(Set<String> 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<String, MapProtocolMapperEntity> 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<String, String> 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<String> getScopeMappings() {
return scopeMappings;
}
@Override
public void addScopeMapping(String id) {
if (id != null) {
updated = true;
scopeMappings.add(id);
} }
} }
@Override @Override
public void removeScopeMapping(String id) { public boolean equals(Object o) {
updated |= scopeMappings.remove(id); return HotRodClientEntityDelegate.entityEquals(this, o);
} }
@Override @Override
public Map<String, Boolean> getClientScopes() { public int hashCode() {
return this.clientScopes.stream().collect(Collectors.toMap(HotRodPair::getFirst, HotRodPair::getSecond)); return HotRodClientEntityDelegate.entityHashCode(this);
}
@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<String> 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;
} }
} }

View file

@ -18,16 +18,15 @@
package org.keycloak.models.map.storage.hotRod.client; package org.keycloak.models.map.storage.hotRod.client;
import org.infinispan.protostream.annotations.ProtoField; 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 org.keycloak.models.map.storage.hotRod.common.HotRodPair;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; 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) @ProtoField(number = 1)
public String id; public String id;
@ProtoField(number = 2) @ProtoField(number = 2)
@ -41,73 +40,15 @@ public class HotRodProtocolMapperEntity implements MapProtocolMapperEntity {
// @ProtoField(number = 5) // @ProtoField(number = 5)
// public String consentText; // public String consentText;
@ProtoField(number = 5) @ProtoField(number = 5)
public Set<HotRodPair<String, String>> config = new LinkedHashSet<>(); public Set<HotRodPair<String, String>> config;
private boolean updated;
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; return HotRodProtocolMapperEntityDelegate.entityEquals(this, o);
if (o == null || getClass() != o.getClass()) return false;
HotRodProtocolMapperEntity entity = (HotRodProtocolMapperEntity) o;
return id.equals(entity.id);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return id.hashCode(); return HotRodProtocolMapperEntityDelegate.entityHashCode(this);
}
@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<String, String> getConfig() {
return config.stream().collect(Collectors.toMap(HotRodPair::getFirst, HotRodPair::getSecond));
}
@Override
public void setConfig(Map<String, String> 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;
} }
} }

View file

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

View file

@ -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<E> extends AbstractEntity, UpdatableEntity {
E getHotRodEntity();
}

View file

@ -17,35 +17,34 @@
package org.keycloak.models.map.storage.hotRod.common; package org.keycloak.models.map.storage.hotRod.common;
import java.util.List; import org.keycloak.models.map.storage.ModelEntityUtil;
import java.util.stream.Stream;
public class HotRodEntityDescriptor<EntityType> { import java.util.function.Function;
public class HotRodEntityDescriptor<E, D extends HotRodEntityDelegate<E>> {
private final Class<?> modelTypeClass; private final Class<?> modelTypeClass;
private final Class<EntityType> entityTypeClass; private final Class<E> entityTypeClass;
private final List<Class<?>> hotRodClasses; private final Function<E, D> hotRodDelegateProvider;
private final String cacheName;
public HotRodEntityDescriptor(Class<?> modelTypeClass, Class<EntityType> entityTypeClass, List<Class<?>> hotRodClasses, String cacheName) { public HotRodEntityDescriptor(Class<?> modelTypeClass, Class<E> entityTypeClass, Function<E, D> hotRodDelegateProvider) {
this.modelTypeClass = modelTypeClass; this.modelTypeClass = modelTypeClass;
this.entityTypeClass = entityTypeClass; this.entityTypeClass = entityTypeClass;
this.hotRodClasses = hotRodClasses; this.hotRodDelegateProvider = hotRodDelegateProvider;
this.cacheName = cacheName;
} }
public Class<?> getModelTypeClass() { public Class<?> getModelTypeClass() {
return modelTypeClass; return modelTypeClass;
} }
public Class<EntityType> getEntityTypeClass() { public Class<E> getEntityTypeClass() {
return entityTypeClass; return entityTypeClass;
} }
public Stream<Class<?>> getHotRodClasses() { public String getCacheName() {
return hotRodClasses.stream(); return ModelEntityUtil.getModelName(modelTypeClass);
} }
public String getCacheName() { public Function<E, D> getHotRodDelegateProvider() {
return cacheName; return hotRodDelegateProvider;
} }
} }

View file

@ -20,33 +20,48 @@ package org.keycloak.models.map.storage.hotRod.common;
import org.infinispan.protostream.WrappedMessage; import org.infinispan.protostream.WrappedMessage;
import org.infinispan.protostream.annotations.ProtoField; import org.infinispan.protostream.annotations.ProtoField;
import java.util.Objects;
public class HotRodPair<T, V> { public class HotRodPair<T, V> {
@ProtoField(number = 1) @ProtoField(number = 1)
public WrappedMessage firstWrapped; public WrappedMessage key;
@ProtoField(number = 2) @ProtoField(number = 2)
public WrappedMessage secondWrapped; public WrappedMessage value;
public HotRodPair() {} public HotRodPair() {}
public HotRodPair(T first, V second) { public HotRodPair(T first, V second) {
this.firstWrapped = new WrappedMessage(first); this.key = new WrappedMessage(first);
this.secondWrapped = new WrappedMessage(second); this.value = new WrappedMessage(second);
} }
public T getFirst() { public T getKey() {
return firstWrapped == null ? null : (T) firstWrapped.getValue(); return key == null ? null : (T) key.getValue();
} }
public V getSecond() { public V getValue() {
return secondWrapped == null ? null : (V) secondWrapped.getValue(); return value == null ? null : (V) value.getValue();
} }
public void setFirst(T first) { public void setKey(T first) {
this.firstWrapped = new WrappedMessage(first); this.key = new WrappedMessage(first);
} }
public void setSecond(V second) { public void setValue(V second) {
this.secondWrapped = new WrappedMessage(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);
} }
} }

View file

@ -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 <MapKey, MapValue, SetValue> Set<SetValue> migrateMapToSet(Map<MapKey, MapValue> map, Function<Map.Entry<MapKey, MapValue>, SetValue> creator) {
return map == null ? null : map.entrySet()
.stream()
.map(creator)
.collect(Collectors.toSet());
}
public static <MapKey, MapValue, SetValue> Map<MapKey, MapValue> migrateSetToMap(Set<SetValue> set, Function<SetValue, MapKey> keyProducer, Function<SetValue, MapValue> valueProducer) {
return set == null ? null : set.stream().collect(Collectors.toMap(keyProducer, valueProducer));
}
public static <T, V> HotRodPair<T, V> createHotRodPairFromMapEntry(Map.Entry<T, V> entry) {
return new HotRodPair<>(entry.getKey(), entry.getValue());
}
public static <SetType, KeyType> boolean removeFromSetByMapKey(Set<SetType> set, KeyType key, Function<SetType, KeyType> 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 <SetType, MapKey, MapValue> MapValue getMapValueFromSet(Set<SetType> set, MapKey key, Function<SetType, MapKey> keyGetter, Function<SetType, MapValue> valueGetter) {
return set == null ? null : set.stream().filter(entry -> Objects.equals(keyGetter.apply(entry), key)).findFirst().map(valueGetter).orElse(null);
}
public static <K, V> K getKey(HotRodPair<K, V> hotRodPair) {
return hotRodPair.getKey();
}
public static <K, V> V getValue(HotRodPair<K, V> hotRodPair) {
return hotRodPair.getValue();
}
public static String getKey(HotRodAttributeEntity attributeEntity) {
return attributeEntity.name;
}
public static List<String> getValue(HotRodAttributeEntity attributeEntity) {
return attributeEntity.values;
}
public static String getKey(AbstractEntity entity) {
return entity.getId();
}
}

View file

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

View file

@ -20,6 +20,7 @@ package org.keycloak.models.map.storage.hotRod;
import org.junit.Test; import org.junit.Test;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.map.storage.hotRod.client.HotRodClientEntity; 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.ModelCriteriaBuilder;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; 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 { public class IckleQueryMapModelCriteriaBuilderTest {
@Test @Test
public void testSimpleIckleQuery() { public void testSimpleIckleQuery() {
IckleQueryMapModelCriteriaBuilder<String, HotRodClientEntity, ClientModel> v = new IckleQueryMapModelCriteriaBuilder<>(); IckleQueryMapModelCriteriaBuilder<String, HotRodClientEntityDelegate, ClientModel> v = new IckleQueryMapModelCriteriaBuilder<>();
IckleQueryMapModelCriteriaBuilder<String, HotRodClientEntity, ClientModel> mcb = v.compare(CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, 3); IckleQueryMapModelCriteriaBuilder<String, HotRodClientEntityDelegate, ClientModel> 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.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().entrySet(), hasSize(1));
assertThat(mcb.getParameters(), hasEntry("clientId0", 3)); assertThat(mcb.getParameters(), hasEntry("clientId0", 3));
@ -55,7 +56,7 @@ public class IckleQueryMapModelCriteriaBuilderTest {
@Test @Test
public void testSimpleIckleQueryFlashedFromDefault() { public void testSimpleIckleQueryFlashedFromDefault() {
DefaultModelCriteria<ClientModel> v = criteria(); DefaultModelCriteria<ClientModel> v = criteria();
IckleQueryMapModelCriteriaBuilder<String, HotRodClientEntity, ClientModel> mcb = v.compare(CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, 3).flashToModelCriteriaBuilder(new IckleQueryMapModelCriteriaBuilder<>()); IckleQueryMapModelCriteriaBuilder<String, HotRodClientEntityDelegate, ClientModel> 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.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().entrySet(), hasSize(1));
assertThat(mcb.getParameters(), hasEntry("clientId0", 3)); assertThat(mcb.getParameters(), hasEntry("clientId0", 3));

View file

@ -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<String, String>) null, Map.Entry::getKey), nullValue());
Map<String, String> 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<String>) null, Function.identity(), Function.identity()), nullValue());
Set<String> s = new HashSet<>();
s.add("key#value");
Map<String, String> 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<String>) null, null, Function.identity()), is(false));
assertThat(removeFromSetByMapKey(Collections.emptySet(), null, Function.identity()), is(false));
Set<String> s = new HashSet<>();
s.add("key#value");
s.add("key1#value1");
s.add("key2#value2");
// Remove existing
Set<String> 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<String>) null, null, Function.identity(), Function.identity()), nullValue());
assertThat(getMapValueFromSet(Collections.emptySet(), "key", Function.identity(), Function.identity()), nullValue());
Set<String> 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());
}
}

View file

@ -92,9 +92,6 @@ public interface MapClientEntity extends AbstractEntity, UpdatableEntity {
void removeAttribute(String name); void removeAttribute(String name);
void setAttribute(String name, List<String> values); void setAttribute(String name, List<String> values);
Map<String, String> getAuthFlowBindings();
void setAuthFlowBindings(Map<String, String> authFlowBindings);
String getAuthenticationFlowBindingOverride(String binding); String getAuthenticationFlowBindingOverride(String binding);
Map<String, String> getAuthenticationFlowBindingOverrides(); Map<String, String> getAuthenticationFlowBindingOverrides();
void removeAuthenticationFlowBindingOverride(String binding); void removeAuthenticationFlowBindingOverride(String binding);

View file

@ -17,6 +17,7 @@
package org.keycloak.models.map.client; package org.keycloak.models.map.client;
import org.keycloak.models.map.annotations.GenerateEntityImplementations; 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.DeepCloner;
import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.map.common.UpdatableEntity;
import java.util.Map; import java.util.Map;
@ -27,10 +28,7 @@ import java.util.Map;
*/ */
@GenerateEntityImplementations @GenerateEntityImplementations
@DeepCloner.Root @DeepCloner.Root
public interface MapProtocolMapperEntity extends UpdatableEntity { public interface MapProtocolMapperEntity extends UpdatableEntity, AbstractEntity {
String getId();
void setId(String id);
String getName(); String getName();
void setName(String name); void setName(String name);

View file

@ -79,6 +79,7 @@
<hibernate.c3p0.version>5.3.20.Final</hibernate.c3p0.version> <hibernate.c3p0.version>5.3.20.Final</hibernate.c3p0.version>
<infinispan.version>11.0.9.Final</infinispan.version> <infinispan.version>11.0.9.Final</infinispan.version>
<infinispan.protostream.processor.version>4.3.4.Final</infinispan.protostream.processor.version> <infinispan.protostream.processor.version>4.3.4.Final</infinispan.protostream.processor.version>
<javax.annotation-api.version>1.3.2</javax.annotation-api.version>
<jackson.version>2.12.1</jackson.version> <jackson.version>2.12.1</jackson.version>
<jackson.databind.version>${jackson.version}</jackson.databind.version> <jackson.databind.version>${jackson.version}</jackson.databind.version>
<jackson.annotations.version>${jackson.databind.version}</jackson.annotations.version> <jackson.annotations.version>${jackson.databind.version}</jackson.annotations.version>