From eb37a1ed691381e60342bf31560fbd4d6582ba32 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Thu, 4 Feb 2021 19:52:02 -0300 Subject: [PATCH] [KEYCLOAK-17031] - ClientInvalidationClusterTest failing on Quarkus due to unreliable comparison --- .../common/util/reflections/Reflections.java | 54 +++++++++++++++++++ .../UnSetAccessiblePrivilegedAction.java | 41 ++++++++++++++ .../AbstractInvalidationClusterTest.java | 31 ++++++++++- 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 common/src/main/java/org/keycloak/common/util/reflections/UnSetAccessiblePrivilegedAction.java diff --git a/common/src/main/java/org/keycloak/common/util/reflections/Reflections.java b/common/src/main/java/org/keycloak/common/util/reflections/Reflections.java index b2d16f2ceb..c2131e83d2 100644 --- a/common/src/main/java/org/keycloak/common/util/reflections/Reflections.java +++ b/common/src/main/java/org/keycloak/common/util/reflections/Reflections.java @@ -35,6 +35,7 @@ import java.security.AccessController; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -409,6 +410,20 @@ public class Reflections { return member; } + /** + * Set the accessibility flag on the {@link AccessibleObject} to false as described in {@link + * AccessibleObject#setAccessible(boolean)} within the context of a {link PrivilegedAction}. + * + * @param member the accessible object type + * @param member the accessible object + * + * @return the accessible object after the accessible flag has been altered + */ + public static A unsetAccessible(A member) { + AccessController.doPrivileged(new UnSetAccessiblePrivilegedAction(member)); + return member; + } + private static String buildSetFieldValueErrorMessage(Field field, Object obj, Object value) { return String.format("Exception setting [%s] field on object [%s] to value [%s]", field.getName(), obj, value); } @@ -994,4 +1009,43 @@ public class Reflections { public static T newInstance(final Class type, final String fullQualifiedName) throws ClassNotFoundException, IllegalAccessException, InstantiationException { return (T) classForName(fullQualifiedName, type.getClassLoader()).newInstance(); } + + /** + *

Resolves the type of items for a {@link Field} declared as a {@link List}. + * + *

This method will first try to check the parametrized type of the field type. If none is defined, it will try to infer + * the type of items by looking at the value of the field for the given {@code instance}. + * + *

Make sure the field is accessible before invoking this method. + * + * @param field the field declared as {@link List} + * @param instance the instance that should be used to obtain infer the type in case no parametrized type is found in the field. + * @return if the field is not a {@link List}, it returns null. Otherwise the type of items of the list. If the type for items can not be inferred, the {@link Object} type is returned. + * @throws IllegalAccessException in case it fails to obtain the value of the field from the {@code instance} + */ + public static Class resolveListType(Field field, Object instance) throws IllegalAccessException { + if (!List.class.isAssignableFrom(field.getType())) { + return null; + } + + Type genericType = field.getGenericType(); + + if (genericType instanceof ParameterizedType) { + Type[] typeArguments = ParameterizedType.class.cast(genericType) + .getActualTypeArguments(); + + if (typeArguments[0] instanceof Class) { + return (Class) typeArguments[0]; + } + } else if (instance != null) { + // just in case the field is not parametrized + List item = List.class.cast(field.get(instance)); + + if (!item.isEmpty()) { + return item.get(0).getClass(); + } + } + + return Object.class; + } } \ No newline at end of file diff --git a/common/src/main/java/org/keycloak/common/util/reflections/UnSetAccessiblePrivilegedAction.java b/common/src/main/java/org/keycloak/common/util/reflections/UnSetAccessiblePrivilegedAction.java new file mode 100644 index 0000000000..cfdb1302e4 --- /dev/null +++ b/common/src/main/java/org/keycloak/common/util/reflections/UnSetAccessiblePrivilegedAction.java @@ -0,0 +1,41 @@ +/* + * Copyright 2016 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.common.util.reflections; + +import java.lang.reflect.AccessibleObject; +import java.security.PrivilegedAction; + +/** + * A {@link PrivilegedAction} that calls {@link AccessibleObject#setAccessible(boolean)} + */ +public class UnSetAccessiblePrivilegedAction implements PrivilegedAction { + + private final AccessibleObject member; + + public UnSetAccessiblePrivilegedAction(AccessibleObject member) { + this.member = member; + } + + public Void run() { + if (member.isAccessible()) { + member.setAccessible(false); + } + return null; + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractInvalidationClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractInvalidationClusterTest.java index 657ad5bab7..33d82970d3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractInvalidationClusterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractInvalidationClusterTest.java @@ -9,10 +9,15 @@ import org.junit.Test; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.arquillian.ContainerInfo; +import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import static org.junit.Assert.assertFalse; +import static org.keycloak.common.util.reflections.Reflections.resolveListType; +import static org.keycloak.common.util.reflections.Reflections.setAccessible; +import static org.keycloak.common.util.reflections.Reflections.unsetAccessible; /** * @@ -132,7 +137,8 @@ public abstract class AbstractInvalidationClusterTest extends AbstractClu boolean entityDiffers = false; for (ContainerInfo survivorNode : getCurrentSurvivorNodes()) { T testEntityOnSurvivorNode = readEntity(testEntityOnFailNode, survivorNode); - if (EqualsBuilder.reflectionEquals(testEntityOnSurvivorNode, testEntityOnFailNode, excludedComparisonFields)) { + + if (EqualsBuilder.reflectionEquals(sortFields(testEntityOnSurvivorNode), sortFields(testEntityOnFailNode), excludedComparisonFields)) { log.info(String.format("Verification of %s on survivor %s PASSED", getEntityType(testEntityOnFailNode), survivorNode)); } else { entityDiffers = true; @@ -149,6 +155,29 @@ public abstract class AbstractInvalidationClusterTest extends AbstractClu assertFalse(entityDiffers); } + private T sortFields(T entity) { + for (Field field : entity.getClass().getDeclaredFields()) { + try { + Class type = resolveListType(field, entity); + + if (type != null && Comparable.class.isAssignableFrom(type)) { + setAccessible(field); + Object value = field.get(entity); + + if (value != null) { + Collections.sort((List) value); + } + } + } catch (IllegalAccessException cause) { + throw new RuntimeException("Failed to sort field [" + field + "]", cause); + } finally { + unsetAccessible(field); + } + } + + return entity; + } + private void assertEntityOnSurvivorNodesIsDeleted(T testEntityOnFailNode) { // check if deleted from all survivor nodes boolean entityExists = false;