clientScopes = getClientScopes();
diff --git a/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java b/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java
index 576ec6584a..a5d0981edb 100644
--- a/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java
+++ b/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java
@@ -140,7 +140,8 @@ public class MapClientProvider implements ClientProvider {
public ClientModel addClient(RealmModel realm, String id, String clientId) {
LOG.tracef("addClient(%s, %s, %s)%s", realm, id, clientId, getShortStackTrace());
- MapClientEntity entity = new MapClientEntityImpl(id);
+ MapClientEntity entity = new MapClientEntityImpl();
+ entity.setId(id);
entity.setRealmId(realm.getId());
entity.setClientId(clientId);
entity.setEnabled(true);
diff --git a/model/map/src/main/java/org/keycloak/models/map/client/MapProtocolMapperEntity.java b/model/map/src/main/java/org/keycloak/models/map/client/MapProtocolMapperEntity.java
index 300f979852..eeee874dcb 100644
--- a/model/map/src/main/java/org/keycloak/models/map/client/MapProtocolMapperEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/client/MapProtocolMapperEntity.java
@@ -17,6 +17,7 @@
package org.keycloak.models.map.client;
import org.keycloak.models.map.annotations.GenerateEntityImplementations;
+import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.UpdatableEntity;
import java.util.Map;
@@ -25,6 +26,7 @@ import java.util.Map;
* @author hmlnarik
*/
@GenerateEntityImplementations
+@DeepCloner.Root
public interface MapProtocolMapperEntity extends UpdatableEntity {
String getId();
diff --git a/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeEntity.java b/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeEntity.java
index ff4b594a32..dd2d800512 100644
--- a/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeEntity.java
@@ -31,7 +31,7 @@ import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.UpdatableEntity;
-public class MapClientScopeEntity implements AbstractEntity, UpdatableEntity {
+public class MapClientScopeEntity extends UpdatableEntity.Impl implements AbstractEntity {
private String id;
private String realmId;
@@ -47,9 +47,8 @@ public class MapClientScopeEntity implements AbstractEntity, UpdatableEntity {
/**
* Flag signalizing that any of the setters has been meaningfully used.
*/
- protected boolean updated;
- protected MapClientScopeEntity() {}
+ public MapClientScopeEntity() {}
public MapClientScopeEntity(String id, String realmId) {
this.id = id;
@@ -68,11 +67,6 @@ public class MapClientScopeEntity implements AbstractEntity, UpdatableEntity {
this.updated |= id != null;
}
- @Override
- public boolean isUpdated() {
- return this.updated;
- }
-
public String getName() {
return name;
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/common/DeepCloner.java b/model/map/src/main/java/org/keycloak/models/map/common/DeepCloner.java
new file mode 100644
index 0000000000..ba96d7be0b
--- /dev/null
+++ b/model/map/src/main/java/org/keycloak/models/map/common/DeepCloner.java
@@ -0,0 +1,384 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models.map.common;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Stack;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import org.jboss.logging.Logger;
+
+/**
+ * Helper class for deep cloning and fine-grained instantiation per interface and deep copying their properties.
+ *
+ * This class is intended to be used by individual map storage implementations for copying
+ * over entities into their native implementations.
+ *
+ * For example, a {@code MapClientEntity} interface could be implemented by {@code MapClientEntityImpl}
+ * (used by a file-based storage in this example) and an {@code HotRodClientEntityImpl} (for Infinispan).
+ * Say that the Infinispan is stacked on top of the file-based storage to provide caching layer.
+ * Upon first read, a {@code MapClientEntityImpl} could be obtained from file-based storage and passed
+ * to Infinispan layer for caching. Infinispan, regardless of the actual implementation, need to store
+ * the {@code MapClientEntity} data in a form that can be processed and sent over the wire in Infinispan
+ * (say in an {@code InfinispanClientEntityImpl}). To achieve this, the Infinispan store has to clone
+ * the file entity values from the {@code MapClientEntityImpl} to {@code InfinispanClientEntityImpl},
+ * i.e. it performs deep cloning, using this helper class.
+ *
+ * Broader context:
+ * In tree store, map storages are agnostic to their neighbours. Therefore each implementation can be
+ * provided with a record (a {@code MapClientEntity} instance in the example above) originating from
+ * any other implementation. For a map storage to process the record (beyond read-only mode),
+ * it needs to be able to clone it into its own entity. Each of the storages thus can benefit from
+ * the {@code DeepCloner} capabilities.
+ *
+ * @author hmlnarik
+ */
+public class DeepCloner {
+
+ /**
+ * Marker for interfaces that could be requested for instantiation and cloning.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ public @interface Root {}
+
+ /**
+ * Function that clones properties from {@code original} object to a {@code target} object and returns
+ * the cloned object (usually the same as the {@code target}).
+ * @param Object class
+ */
+ @FunctionalInterface
+ public interface Cloner {
+ /**
+ * Function that clones properties from {@code original} object to a {@code target} object and returns
+ * the cloned object (usually the same as the {@code target}).
+ */
+ V clone(V original, V target);
+ }
+
+ public static final DeepCloner DUMB_CLONER = new Builder().build();
+
+ /**
+ * Builder for the {@code DeepCloner} helper class.
+ */
+ public static class Builder {
+ private final Map, Supplier>> parameterlessConstructors = new HashMap<>();
+ private final Map, Function> constructors = new HashMap<>();
+ private final Map, Cloner> clonersWithId = new HashMap<>(org.keycloak.models.map.common.AutogeneratedCloners.CLONERS_WITH_ID);
+ private final Map, Cloner> clonersWithoutId = new HashMap<>(org.keycloak.models.map.common.AutogeneratedCloners.CLONERS_WITHOUT_ID);
+ private Cloner> genericCloner = (from, to) -> { throw new IllegalStateException("Cloner not found for class " + (from == null ? "" : from.getClass())); };
+
+ /**
+ * Returns a {@link DeepCloner} initialized with the respective constructors and cloners.
+ * @return
+ */
+ public DeepCloner build() {
+ return new DeepCloner(parameterlessConstructors, constructors, clonersWithId, clonersWithoutId, genericCloner);
+ }
+
+ private void forThisClassAndAllMarkedParentsAndInterfaces(Class rootClazz, Consumer> action) {
+ action.accept(rootClazz);
+
+ Stack> c = new Stack<>();
+ c.push(rootClazz);
+ while (! c.isEmpty()) {
+ Class> cl = c.pop();
+ if (cl == null) {
+ continue;
+ }
+
+ c.push(cl.getSuperclass());
+ for (Class> iface : cl.getInterfaces()) {
+ c.push(iface);
+ }
+
+ if (cl.getAnnotation(Root.class) != null) {
+ action.accept(cl);
+ }
+ }
+ }
+
+ /**
+ * Adds a method, often a constructor, that instantiates a record of type {@code V}.
+ *
+ * @param Class or interface that would be instantiated by the given methods
+ * @param clazz Class or interface that would be instantiated by the given methods
+ * @param constructorNoParameters Parameterless function that creates a new instance of class {@code V}.
+ * If {@code null}, parameterless constructor is not available.
+ * @return This builder.
+ */
+ public Builder constructor(Class clazz, Supplier extends V> constructorNoParameters) {
+ if (constructorNoParameters != null) {
+ forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.parameterlessConstructors.put(cl, constructorNoParameters));
+ }
+ return this;
+ }
+
+ /**
+ * Adds a method, often a constructor, that instantiates a record of type {@code V}.
+ *
+ * @param Class or interface that would be instantiated by the given methods
+ * @param clazz Class or interface that would be instantiated by the given methods
+ * @param constructor Function that creates a new instance of class {@code V}.
+ * If {@code null}, such a single-parameter constructor is not available.
+ * @return This builder.
+ */
+ public Builder constructorDC(Class clazz, Function constructor) {
+ if (constructor != null) {
+ forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.constructors.put(cl, constructor));
+ }
+ return this;
+ }
+
+ /**
+ * Adds a method that copies (as in a deep copy) an object properties from one object to another
+ *
+ * @param Class or interface whose instance would be copied over to another instance by the given cloner
+ * @param clazz Class or interface whose instance would be copied over to another instance by the given cloner
+ * @param cloner A method for cloning with the following signature: {@code V deepClone(V from, V to)} which
+ * copies properties of an object {@code from} onto the object {@code to}. This
+ * function usually returns {@code to}
+ * @return This builder.
+ */
+ public Builder cloner(Class extends V> clazz, Cloner cloner) {
+ if (cloner != null) {
+ forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.clonersWithId.put(cl, cloner));
+ }
+ return this;
+ }
+
+ /**
+ * Adds a method that copies (as in a deep copy) an object properties from one object to another
+ *
+ * @param Class or interface whose instance would be copied over to another instance by the given cloner
+ * @param clazz Class or interface whose instance would be copied over to another instance by the given cloner
+ * @param clonerWithId A method for cloning with the following signature: {@code V deepClone(V from, V to)} which
+ * copies properties of an object {@code from} onto the object {@code to}. This
+ * function usually returns {@code to}
+ * @return This builder.
+ */
+ public Builder cloner(Class extends V> clazz, Cloner clonerWithId, Cloner clonerWithoutId) {
+ if (clonerWithId != null) {
+ forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.clonersWithId.put(cl, clonerWithId));
+ }
+ if (clonerWithoutId != null) {
+ forThisClassAndAllMarkedParentsAndInterfaces(clazz, cl -> this.clonersWithoutId.put(cl, clonerWithoutId));
+ }
+ return this;
+ }
+
+ /**
+ * Adds a method that copies (as in a deep copy) an object properties to another object for any class
+ * that is not covered by a specific cloner set via {@link #cloner(Class, BiFunction)} method.
+ *
+ * @param Class or interface whose instance would be copied over to another instance by the given cloner
+ * @param genericCloner A method for cloning which copies properties of an object onto another object. This
+ * function usually returns {@code to}
+ * @return This builder.
+ */
+ public Builder genericCloner(Cloner genericCloner) {
+ this.genericCloner = genericCloner;
+ return this;
+ }
+ }
+
+ private static final Logger LOG = Logger.getLogger(DeepCloner.class);
+
+ private final Map, Supplier>> parameterlessConstructors;
+ private final Map, Function> constructors;
+ private final Map, Cloner> clonersWithId;
+ private final Map, Cloner> clonersWithoutId;
+ private final Cloner> genericCloner;
+
+ private DeepCloner(Map, Supplier>> parameterlessConstructors,
+ Map, Function> constructors,
+ Map, Cloner> clonersWithId,
+ Map, Cloner> clonersWithoutId,
+ Cloner> genericCloner) {
+ this.parameterlessConstructors = parameterlessConstructors;
+ this.constructors = constructors;
+ this.clonersWithId = clonersWithId;
+ this.clonersWithoutId = clonersWithoutId;
+ this.genericCloner = genericCloner;
+ }
+
+ private V getFromClassRespectingHierarchy(Map, V> map, Class> clazz) {
+ // fast lookup
+ V res = map.get(clazz);
+ if (res != null) {
+ return res;
+ }
+
+ // BFS on implemented supertypes and interfaces. Skip clazz as it has been looked up already
+ LinkedList> ll = new LinkedList<>();
+ ll.push(clazz.getSuperclass());
+ for (Class> iface : clazz.getInterfaces()) {
+ ll.push(iface);
+ }
+
+ while (! ll.isEmpty()) {
+ Class> cl = ll.pollFirst();
+ if (cl == null) {
+ continue;
+ }
+
+ res = map.get(cl);
+ if (res != null) {
+ map.put(clazz, res); // Wire clazz with the result for fast lookup next time
+ return res;
+ }
+
+ ll.push(cl.getSuperclass());
+ ll.addAll(Arrays.asList(cl.getInterfaces()));
+ }
+ return null;
+ }
+
+ /**
+ * Creates a new instance of the given class or interface if the parameterless constructor for that type is known.
+ * @param Type (class or a {@code @Root} interface) to create a new instance
+ * @param clazz Type (class or a {@code @Root} interface) to create a new instance
+ * @return A new instance
+ * @throws IllegalStateException When the constructor is not known.
+ */
+ public V newInstance(Class clazz) {
+ if (clazz == null) {
+ return null;
+ }
+
+ V res;
+ @SuppressWarnings("unchecked")
+ Function c = (Function) getFromClassRespectingHierarchy(this.constructors, clazz);
+ if (c == null) {
+ @SuppressWarnings("unchecked")
+ Supplier s = (Supplier) getFromClassRespectingHierarchy(this.parameterlessConstructors, clazz);
+ if (s == null) {
+ try {
+ res = clazz.newInstance();
+ } catch (InstantiationException | IllegalAccessException ex) {
+ res = null;
+ }
+ } else {
+ res = s.get();
+ }
+ } else {
+ res = c.apply(this);
+ }
+
+ if (res == null) {
+ throw new IllegalStateException("Cannot instantiate " + clazz);
+ }
+
+ return res;
+ }
+
+ /**
+ * Deeply clones properties from the {@code from} instance to the {@code to} instance.
+ * @param Type (class or a {@code @Root} interface) to clone the instance
+ * @param from Original instance
+ * @param to Instance to copy the properties onto
+ * @return Instance which has all the properties same as the {@code from}. Preferably, {@code to} is returned.
+ * However {@code from} is returned if the cloner is not known and generic cloner is not available.
+ */
+ public V deepClone(V from, V to) {
+ return deepClone(from, to, this.clonersWithId);
+ }
+
+ /**
+ * Deeply clones properties from the {@code from} instance to the {@code to} instance excluding the ID field.
+ * @param Type (class or a {@code @Root} interface) to clone the instance
+ * @param from Original instance
+ * @param to Instance to copy the properties onto
+ * @return Instance which has all the properties same as the {@code from}. Preferably, {@code to} is returned.
+ * However {@code from} is returned if the cloner is not known and generic cloner is not available.
+ */
+ public V deepCloneNoId(V from, V to) {
+ return deepClone(from, to, this.clonersWithoutId);
+ }
+
+ @SuppressWarnings("unchecked")
+ private V deepClone(V from, V to, Map, Cloner> cloners) {
+ Cloner cloner = (Cloner) getFromClassRespectingHierarchy(cloners, from.getClass());
+ if (cloner != null) {
+ return cloner.clone(from, to);
+ }
+
+ if (genericCloner != null) {
+ LOG.debugf("Using generic cloner for %s", from.getClass());
+ final V res = ((Cloner) genericCloner).clone(from, to);
+
+ if (res instanceof UpdatableEntity) {
+ ((UpdatableEntity) res).clearUpdatedFlag();
+ }
+
+ return res;
+ }
+
+ return warnCloneNotSupported(from);
+ }
+
+ /**
+ * Creates a new instance of the given type and copies its properties from the {@code from} instance
+ * @param Type (class or a {@code @Root} interface) to create a new instance and clone properties from
+ * @param newId ID of the new object
+ * @param from Original instance
+ * @return Newly created instance or {@code null} if {@code from} is {@code null}.
+ */
+ @SuppressWarnings("unchecked")
+ public V from(String newId, V from) {
+ if (from == null) {
+ return null;
+ }
+ final V res = newInstance((Class) from.getClass());
+ if (newId != null) {
+ res.setId(newId);
+ }
+ return deepCloneNoId(from, res);
+ }
+
+ /**
+ * Creates a new instance of the given type and copies its properties from the {@code from} instance
+ * @param Type (class or a {@code @Root} interface) to create a new instance and clone properties from
+ * @param from Original instance
+ * @return Newly created instance or {@code null} if {@code from} is {@code null}.
+ */
+ public V from(V from) {
+ return from == null ? null : deepClone(from, newInstance((Class) from.getClass()));
+ }
+
+ /**
+ * Issues warning in the logs and returns the input parameter {@code o}
+ * @param o
+ * @return The {@code o} object
+ */
+ public static T warnCloneNotSupported(T o) {
+ if (o != null) {
+ LOG.warnf("Cloning not supported for %s, returning the same instance!", o.getClass());
+ }
+ return o;
+ }
+
+}
diff --git a/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java b/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java
index 1d33f1d452..c6781f906a 100644
--- a/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java
+++ b/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java
@@ -19,6 +19,7 @@ package org.keycloak.models.map.common;
import org.keycloak.common.util.reflections.Reflections;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonIgnoreType;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
@@ -54,11 +55,15 @@ public class Serialization {
.setVisibility(PropertyAccessor.FIELD, Visibility.ANY)
.activateDefaultTyping(new LaissezFaireSubTypeValidator() /* TODO - see javadoc */, ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS, JsonTypeInfo.As.PROPERTY)
.addMixIn(UpdatableEntity.class, IgnoreUpdatedMixIn.class)
+ .addMixIn(DeepCloner.class, IgnoredTypeMixIn.class)
;
public static final ConcurrentHashMap, ObjectReader> READERS = new ConcurrentHashMap<>();
public static final ConcurrentHashMap, ObjectWriter> WRITERS = new ConcurrentHashMap<>();
+ @JsonIgnoreType
+ class IgnoredTypeMixIn {}
+
abstract class IgnoreUpdatedMixIn {
@JsonIgnore public abstract boolean isUpdated();
}
@@ -90,4 +95,28 @@ public class Serialization {
throw new IllegalStateException(ex);
}
}
+
+ public static T from(T orig, T target) {
+ if (orig == null) {
+ return null;
+ }
+ @SuppressWarnings("unchecked")
+ final Class origClass = (Class) orig.getClass();
+
+ // Naive solution but will do.
+ try {
+ ObjectReader reader = MAPPER.readerForUpdating(target);
+ ObjectWriter writer = WRITERS.computeIfAbsent(origClass, MAPPER::writerFor);
+ final T res;
+ res = reader.readValue(writer.writeValueAsBytes(orig));
+
+ if (res != target) {
+ throw new IllegalStateException("Should clone into desired target");
+ }
+
+ return res;
+ } catch (IOException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/common/UpdatableEntity.java b/model/map/src/main/java/org/keycloak/models/map/common/UpdatableEntity.java
index a5d9da303f..a8b28cdb04 100644
--- a/model/map/src/main/java/org/keycloak/models/map/common/UpdatableEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/common/UpdatableEntity.java
@@ -16,6 +16,9 @@
*/
package org.keycloak.models.map.common;
+import org.keycloak.models.map.annotations.IgnoreForEntityImplementationGenerator;
+
+@IgnoreForEntityImplementationGenerator
public interface UpdatableEntity {
public static class Impl implements UpdatableEntity {
diff --git a/model/map/src/main/java/org/keycloak/models/map/group/MapGroupEntity.java b/model/map/src/main/java/org/keycloak/models/map/group/MapGroupEntity.java
index 4809cd39ad..c755225a5d 100644
--- a/model/map/src/main/java/org/keycloak/models/map/group/MapGroupEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/group/MapGroupEntity.java
@@ -31,7 +31,7 @@ import java.util.Set;
*
* @author mhajas
*/
-public class MapGroupEntity implements AbstractEntity, UpdatableEntity {
+public class MapGroupEntity extends UpdatableEntity.Impl implements AbstractEntity {
private String id;
private String realmId;
@@ -44,9 +44,8 @@ public class MapGroupEntity implements AbstractEntity, UpdatableEntity {
/**
* Flag signalizing that any of the setters has been meaningfully used.
*/
- protected boolean updated;
- protected MapGroupEntity() {}
+ public MapGroupEntity() {}
public MapGroupEntity(String id, String realmId) {
this.id = id;
@@ -65,12 +64,6 @@ public class MapGroupEntity implements AbstractEntity, UpdatableEntity {
this.updated |= id != null;
}
- @Override
- public boolean isUpdated() {
- return this.updated;
- }
-
-
public String getName() {
return name;
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureEntity.java b/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureEntity.java
index ec1ffba306..67ce55e8af 100644
--- a/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureEntity.java
@@ -24,16 +24,11 @@ import java.util.Objects;
/**
* @author Martin Kanis
*/
-public class MapUserLoginFailureEntity implements AbstractEntity, UpdatableEntity {
+public class MapUserLoginFailureEntity extends UpdatableEntity.Impl implements AbstractEntity {
private String id;
private String realmId;
private String userId;
- /**
- * Flag signalizing that any of the setters has been meaningfully used.
- */
- protected boolean updated;
-
private int failedLoginNotBefore;
private int numFailures;
private long lastFailure;
@@ -59,11 +54,6 @@ public class MapUserLoginFailureEntity implements AbstractEntity, UpdatableEntit
this.updated |= id != null;
}
- @Override
- public boolean isUpdated() {
- return this.updated;
- }
-
public String getRealmId() {
return realmId;
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmEntity.java
index 1d20189557..5755f699de 100644
--- a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmEntity.java
@@ -43,7 +43,7 @@ import org.keycloak.models.map.realm.entity.MapRequiredActionProviderEntity;
import org.keycloak.models.map.realm.entity.MapRequiredCredentialEntity;
import org.keycloak.models.map.realm.entity.MapWebAuthnPolicyEntity;
-public class MapRealmEntity implements AbstractEntity, UpdatableEntity {
+public class MapRealmEntity extends UpdatableEntity.Impl implements AbstractEntity {
private String id;
private String name;
@@ -128,9 +128,8 @@ public class MapRealmEntity implements AbstractEntity, UpdatableEntity {
/**
* Flag signalizing that any of the setters has been meaningfully used.
*/
- protected boolean updated;
- protected MapRealmEntity() {}
+ public MapRealmEntity() {}
public MapRealmEntity(String id) {
this.id = id;
diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticationExecutionEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticationExecutionEntity.java
index c2fd77e3c4..8f5e44293c 100644
--- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticationExecutionEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticationExecutionEntity.java
@@ -22,7 +22,7 @@ import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
-public class MapAuthenticationExecutionEntity implements UpdatableEntity {
+public class MapAuthenticationExecutionEntity extends UpdatableEntity.Impl {
private String id;
private String authenticator;
@@ -33,7 +33,6 @@ public class MapAuthenticationExecutionEntity implements UpdatableEntity {
private Boolean autheticatorFlow = false;
private Integer priority = 0;
- private boolean updated;
private MapAuthenticationExecutionEntity() {}
@@ -66,11 +65,6 @@ public class MapAuthenticationExecutionEntity implements UpdatableEntity {
return model;
}
- @Override
- public boolean isUpdated() {
- return updated;
- }
-
public String getId() {
return id;
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticationFlowEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticationFlowEntity.java
index 0f78cf863a..3aed620431 100644
--- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticationFlowEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticationFlowEntity.java
@@ -22,7 +22,7 @@ import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
-public class MapAuthenticationFlowEntity implements UpdatableEntity {
+public class MapAuthenticationFlowEntity extends UpdatableEntity.Impl {
private String id;
private String alias;
@@ -31,7 +31,6 @@ public class MapAuthenticationFlowEntity implements UpdatableEntity {
private Boolean builtIn = false;
private Boolean topLevel = false;
- private boolean updated;
private MapAuthenticationFlowEntity() {}
@@ -61,11 +60,6 @@ public class MapAuthenticationFlowEntity implements UpdatableEntity {
return model;
}
- @Override
- public boolean isUpdated() {
- return updated;
- }
-
public String getId() {
return id;
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticatorConfigEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticatorConfigEntity.java
index 884ced587b..7243c86bad 100644
--- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticatorConfigEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapAuthenticatorConfigEntity.java
@@ -24,13 +24,12 @@ import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
-public class MapAuthenticatorConfigEntity implements UpdatableEntity {
+public class MapAuthenticatorConfigEntity extends UpdatableEntity.Impl {
private String id;
private String alias;
private Map config = new HashMap<>();
- private boolean updated;
private MapAuthenticatorConfigEntity() {}
@@ -53,11 +52,6 @@ public class MapAuthenticatorConfigEntity implements UpdatableEntity {
return model;
}
- @Override
- public boolean isUpdated() {
- return updated;
- }
-
public String getId() {
return id;
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapClientInitialAccessEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapClientInitialAccessEntity.java
index 4fc0cd8a52..a0299e054e 100644
--- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapClientInitialAccessEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapClientInitialAccessEntity.java
@@ -23,7 +23,7 @@ import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
-public class MapClientInitialAccessEntity implements UpdatableEntity {
+public class MapClientInitialAccessEntity extends UpdatableEntity.Impl {
private String id;
private Integer timestamp = 0;
@@ -31,7 +31,6 @@ public class MapClientInitialAccessEntity implements UpdatableEntity {
private Integer count = 0;
private Integer remainingCount = 0;
- private boolean updated;
private MapClientInitialAccessEntity() {}
@@ -58,11 +57,6 @@ public class MapClientInitialAccessEntity implements UpdatableEntity {
return model;
}
- @Override
- public boolean isUpdated() {
- return updated;
- }
-
public String getId() {
return id;
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapComponentEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapComponentEntity.java
index a64867291b..f1dbd08d4d 100644
--- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapComponentEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapComponentEntity.java
@@ -23,7 +23,7 @@ import org.keycloak.component.ComponentModel;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
-public class MapComponentEntity implements UpdatableEntity {
+public class MapComponentEntity extends UpdatableEntity.Impl {
private String id;
private String name;
@@ -33,7 +33,6 @@ public class MapComponentEntity implements UpdatableEntity {
private String parentId;
private MultivaluedHashMap config = new MultivaluedHashMap<>();
- private boolean updated;
private MapComponentEntity() {}
@@ -64,11 +63,6 @@ public class MapComponentEntity implements UpdatableEntity {
return model;
}
- @Override
- public boolean isUpdated() {
- return updated;
- }
-
public String getId() {
return id;
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapIdentityProviderEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapIdentityProviderEntity.java
index 949cf37d39..9cf4ad504a 100644
--- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapIdentityProviderEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapIdentityProviderEntity.java
@@ -24,7 +24,7 @@ import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
-public class MapIdentityProviderEntity implements UpdatableEntity {
+public class MapIdentityProviderEntity extends UpdatableEntity.Impl {
private String id;
private String alias;
@@ -40,7 +40,6 @@ public class MapIdentityProviderEntity implements UpdatableEntity {
private Boolean authenticateByDefault = false;
private Map config = new HashMap<>();
- private boolean updated;
private MapIdentityProviderEntity() {}
@@ -83,11 +82,6 @@ public class MapIdentityProviderEntity implements UpdatableEntity {
return model;
}
- @Override
- public boolean isUpdated() {
- return updated;
- }
-
public String getId() {
return id;
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapIdentityProviderMapperEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapIdentityProviderMapperEntity.java
index 2234b3483c..fc15143d6c 100644
--- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapIdentityProviderMapperEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapIdentityProviderMapperEntity.java
@@ -24,7 +24,7 @@ import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
-public class MapIdentityProviderMapperEntity implements UpdatableEntity {
+public class MapIdentityProviderMapperEntity extends UpdatableEntity.Impl {
private String id;
private String name;
@@ -32,7 +32,6 @@ public class MapIdentityProviderMapperEntity implements UpdatableEntity {
private String identityProviderMapper;
private Map config = new HashMap<>();
- private boolean updated;
private MapIdentityProviderMapperEntity() {}
@@ -59,11 +58,6 @@ public class MapIdentityProviderMapperEntity implements UpdatableEntity {
return model;
}
- @Override
- public boolean isUpdated() {
- return updated;
- }
-
public String getId() {
return id;
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapOTPPolicyEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapOTPPolicyEntity.java
index dfd5dd9add..30c5e6c059 100644
--- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapOTPPolicyEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapOTPPolicyEntity.java
@@ -21,7 +21,7 @@ import java.util.Objects;
import org.keycloak.models.OTPPolicy;
import org.keycloak.models.map.common.UpdatableEntity;
-public class MapOTPPolicyEntity implements UpdatableEntity {
+public class MapOTPPolicyEntity extends UpdatableEntity.Impl {
private Integer otpPolicyInitialCounter = 0;
private Integer otpPolicyDigits = 0;
@@ -30,7 +30,6 @@ public class MapOTPPolicyEntity implements UpdatableEntity {
private String otpPolicyType;
private String otpPolicyAlgorithm;
- private boolean updated;
private MapOTPPolicyEntity() {}
@@ -58,11 +57,6 @@ public class MapOTPPolicyEntity implements UpdatableEntity {
return model;
}
- @Override
- public boolean isUpdated() {
- return updated;
- }
-
public Integer getOtpPolicyInitialCounter() {
return otpPolicyInitialCounter;
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapRequiredActionProviderEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapRequiredActionProviderEntity.java
index 6a7089bc7c..31b7d2d11a 100644
--- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapRequiredActionProviderEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapRequiredActionProviderEntity.java
@@ -24,7 +24,7 @@ import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
-public class MapRequiredActionProviderEntity implements UpdatableEntity {
+public class MapRequiredActionProviderEntity extends UpdatableEntity.Impl {
private String id;
private String alias;
@@ -35,7 +35,6 @@ public class MapRequiredActionProviderEntity implements UpdatableEntity {
private Boolean defaultAction = false;
private Map config = new HashMap<>();
- private boolean updated;
private MapRequiredActionProviderEntity() {}
@@ -68,11 +67,6 @@ public class MapRequiredActionProviderEntity implements UpdatableEntity {
return model;
}
- @Override
- public boolean isUpdated() {
- return updated;
- }
-
public String getId() {
return id;
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapRequiredCredentialEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapRequiredCredentialEntity.java
index decf792f22..422bf5f25a 100644
--- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapRequiredCredentialEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapRequiredCredentialEntity.java
@@ -21,14 +21,13 @@ import java.util.Objects;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.map.common.UpdatableEntity;
-public class MapRequiredCredentialEntity implements UpdatableEntity {
+public class MapRequiredCredentialEntity extends UpdatableEntity.Impl {
private String type;
private String formLabel;
private Boolean input = false;
private Boolean secret = false;
- private boolean updated;
private MapRequiredCredentialEntity() {}
@@ -52,11 +51,6 @@ public class MapRequiredCredentialEntity implements UpdatableEntity {
return model;
}
- @Override
- public boolean isUpdated() {
- return updated;
- }
-
public String getType() {
return type;
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapWebAuthnPolicyEntity.java b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapWebAuthnPolicyEntity.java
index cdbcccf37a..73dd14980a 100644
--- a/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapWebAuthnPolicyEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/realm/entity/MapWebAuthnPolicyEntity.java
@@ -25,7 +25,7 @@ import org.keycloak.models.Constants;
import org.keycloak.models.WebAuthnPolicy;
import org.keycloak.models.map.common.UpdatableEntity;
-public class MapWebAuthnPolicyEntity implements UpdatableEntity {
+public class MapWebAuthnPolicyEntity extends UpdatableEntity.Impl {
// mandatory
private String rpEntityName;
@@ -41,7 +41,6 @@ public class MapWebAuthnPolicyEntity implements UpdatableEntity {
private Boolean avoidSameAuthenticatorRegister = false;
private List acceptableAaguids = new LinkedList<>();
- private boolean updated;
private MapWebAuthnPolicyEntity() {}
@@ -92,11 +91,6 @@ public class MapWebAuthnPolicyEntity implements UpdatableEntity {
return entity;
}
- @Override
- public boolean isUpdated() {
- return updated;
- }
-
public String getRpEntityName() {
return rpEntityName;
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleEntity.java b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleEntity.java
index fdb9dc5b1c..ab48ab14f3 100644
--- a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleEntity.java
@@ -25,7 +25,7 @@ import java.util.Set;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.UpdatableEntity;
-public class MapRoleEntity implements AbstractEntity, UpdatableEntity {
+public class MapRoleEntity extends UpdatableEntity.Impl implements AbstractEntity {
private String id;
private String realmId;
@@ -40,9 +40,8 @@ public class MapRoleEntity implements AbstractEntity, UpdatableEntity {
/**
* Flag signalizing that any of the setters has been meaningfully used.
*/
- protected boolean updated;
- protected MapRoleEntity() {}
+ public MapRoleEntity() {}
public MapRoleEntity(String id, String realmId) {
this.id = id;
@@ -61,11 +60,6 @@ public class MapRoleEntity implements AbstractEntity, UpdatableEntity {
this.updated |= id != null;
}
- @Override
- public boolean isUpdated() {
- return this.updated;
- }
-
public String getName() {
return name;
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java
index ce6aa25755..3efa21ef37 100644
--- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java
+++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java
@@ -18,7 +18,7 @@ package org.keycloak.models.map.storage.chm;
import org.keycloak.models.map.common.StringKeyConvertor;
import org.keycloak.models.map.common.AbstractEntity;
-import org.keycloak.models.map.common.Serialization;
+import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.UpdatableEntity;
import java.util.Iterator;
import java.util.LinkedHashMap;
@@ -40,19 +40,21 @@ public class ConcurrentHashMapKeycloakTransaction tasks = new LinkedHashMap<>();
- private final MapStorage map;
- private final StringKeyConvertor keyConvertor;
+ protected boolean active;
+ protected boolean rollback;
+ protected final Map tasks = new LinkedHashMap<>();
+ protected final MapStorage map;
+ protected final StringKeyConvertor keyConvertor;
+ protected final DeepCloner cloner;
enum MapOperation {
CREATE, UPDATE, DELETE,
}
- public ConcurrentHashMapKeycloakTransaction(MapStorage map, StringKeyConvertor keyConvertor) {
+ public ConcurrentHashMapKeycloakTransaction(MapStorage map, StringKeyConvertor keyConvertor, DeepCloner cloner) {
this.map = map;
this.keyConvertor = keyConvertor;
+ this.cloner = cloner;
}
@Override
@@ -119,7 +121,7 @@ public class ConcurrentHashMapKeycloakTransaction e.isUpdated());
}
@@ -212,10 +214,11 @@ public class ConcurrentHashMapKeycloakTransaction implements MapStorage {
- private final ConcurrentMap store = new ConcurrentHashMap<>();
+ protected final ConcurrentMap store = new ConcurrentHashMap<>();
- private final Map, UpdatePredicatesFunc> fieldPredicates;
- private final StringKeyConvertor keyConvertor;
+ protected final Map, UpdatePredicatesFunc> fieldPredicates;
+ protected final StringKeyConvertor keyConvertor;
+ protected final DeepCloner cloner;
@SuppressWarnings("unchecked")
- public ConcurrentHashMapStorage(Class modelClass, StringKeyConvertor keyConvertor) {
+ public ConcurrentHashMapStorage(Class modelClass, StringKeyConvertor keyConvertor, DeepCloner cloner) {
this.fieldPredicates = MapFieldPredicates.getPredicates(modelClass);
this.keyConvertor = keyConvertor;
+ this.cloner = cloner;
}
@Override
public V create(V value) {
K key = keyConvertor.fromStringSafe(value.getId());
if (key == null) {
- value = Serialization.from(value);
key = keyConvertor.yieldNewUniqueKey();
- value.setId(keyConvertor.keyToString(key));
+ value = cloner.from(keyConvertor.keyToString(key), value);
}
store.putIfAbsent(key, value);
return value;
@@ -98,6 +99,7 @@ public class ConcurrentHashMapStorage b = criteria.unwrap(MapModelCriteriaBuilder.class);
if (b == null) {
throw new IllegalStateException("Incompatible class: " + criteria.getClass());
@@ -130,7 +132,7 @@ public class ConcurrentHashMapStorage createTransaction(KeycloakSession session) {
MapKeycloakTransaction sessionTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class);
- return sessionTransaction == null ? new ConcurrentHashMapKeycloakTransaction<>(this, keyConvertor) : sessionTransaction;
+ return sessionTransaction == null ? new ConcurrentHashMapKeycloakTransaction<>(this, keyConvertor, cloner) : sessionTransaction;
}
public StringKeyConvertor getKeyConvertor() {
@@ -146,6 +148,7 @@ public class ConcurrentHashMapStorage> stream = store.entrySet().stream();
+ @SuppressWarnings("unchecked")
MapModelCriteriaBuilder b = criteria.unwrap(MapModelCriteriaBuilder.class);
if (b == null) {
throw new IllegalStateException("Incompatible class: " + criteria.getClass());
diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java
index 1666c406a8..691cafef8c 100644
--- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java
+++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java
@@ -44,8 +44,11 @@ import org.keycloak.models.map.authorization.entity.MapResourceServerEntity;
import org.keycloak.models.map.authorization.entity.MapScopeEntity;
import org.keycloak.models.map.client.MapClientEntity;
import org.keycloak.models.map.client.MapClientEntityImpl;
+import org.keycloak.models.map.client.MapProtocolMapperEntity;
+import org.keycloak.models.map.client.MapProtocolMapperEntityImpl;
import org.keycloak.models.map.clientscope.MapClientScopeEntity;
import org.keycloak.models.map.common.AbstractEntity;
+import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.Serialization;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.map.group.MapGroupEntity;
@@ -96,6 +99,12 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide
private StringKeyConvertor defaultKeyConvertor;
+ private final static DeepCloner CLONER = new DeepCloner.Builder()
+ .genericCloner(Serialization::from)
+ .constructorDC(MapClientEntityImpl.class, MapClientEntityImpl::new)
+ .constructor(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl::new)
+ .build();
+
public static final Map, String> MODEL_TO_NAME = new HashMap<>();
static {
MODEL_TO_NAME.put(AuthenticatedClientSessionModel.class, "client-sessions");
@@ -224,13 +233,13 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide
storages.forEach(this::storeMap);
}
+ @SuppressWarnings("unchecked")
private void storeMap(String mapName, ConcurrentHashMapStorage, ?, ?> store) {
if (mapName != null) {
File f = getFile(mapName);
try {
if (storageDirectory != null) {
LOG.debugf("Storing contents to %s", f.getCanonicalPath());
- @SuppressWarnings("unchecked")
final ModelCriteriaBuilder readAllCriteria = store.createCriteriaBuilder();
Serialization.MAPPER.writeValue(f, store.read(withCriteria(readAllCriteria)));
} else {
@@ -242,24 +251,24 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide
}
}
+ @SuppressWarnings("unchecked")
private ConcurrentHashMapStorage loadMap(String mapName,
Class modelType, EnumSet flags) {
final StringKeyConvertor kc = keyConvertors.getOrDefault(mapName, defaultKeyConvertor);
Class> valueType = MODEL_TO_VALUE_TYPE.get(modelType);
LOG.debugf("Initializing new map storage: %s", mapName);
- @SuppressWarnings("unchecked")
ConcurrentHashMapStorage store;
if (modelType == UserSessionModel.class) {
ConcurrentHashMapStorage clientSessionStore = getStorage(AuthenticatedClientSessionModel.class);
- store = new UserSessionConcurrentHashMapStorage(clientSessionStore, kc) {
+ store = new UserSessionConcurrentHashMapStorage(clientSessionStore, kc, CLONER) {
@Override
public String toString() {
return "ConcurrentHashMapStorage(" + mapName + suffix + ")";
}
};
} else {
- store = new ConcurrentHashMapStorage(modelType, kc) {
+ store = new ConcurrentHashMapStorage(modelType, kc, CLONER) {
@Override
public String toString() {
return "ConcurrentHashMapStorage(" + mapName + suffix + ")";
diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/CriteriaOperator.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/CriteriaOperator.java
index 9de970b572..79d7da15a6 100644
--- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/CriteriaOperator.java
+++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/CriteriaOperator.java
@@ -144,10 +144,10 @@ class CriteriaOperator {
if (value.length == 1) {
final Object value0 = value[0];
if (value0 instanceof Collection) {
- operand = (Collection) value0;
+ operand = (Collection>) value0;
} else if (value0 instanceof Stream) {
- try (Stream valueS = (Stream) value0) {
- operand = (Set) valueS.collect(Collectors.toSet());
+ try (Stream> valueS = (Stream>) value0) {
+ operand = valueS.collect(Collectors.toSet());
}
} else {
operand = Collections.singleton(value0);
diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java
index be2f323afd..34f4625196 100644
--- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java
+++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java
@@ -21,6 +21,7 @@ import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.map.common.AbstractEntity;
+import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator;
@@ -46,8 +47,8 @@ public class UserSessionConcurrentHashMapStorage extends ConcurrentHashMapSto
private final MapKeycloakTransaction clientSessionTr;
- public Transaction(MapKeycloakTransaction clientSessionTr, StringKeyConvertor keyConvertor) {
- super(UserSessionConcurrentHashMapStorage.this, keyConvertor);
+ public Transaction(MapKeycloakTransaction clientSessionTr, StringKeyConvertor keyConvertor, DeepCloner cloner) {
+ super(UserSessionConcurrentHashMapStorage.this, keyConvertor, cloner);
this.clientSessionTr = clientSessionTr;
}
@@ -70,8 +71,8 @@ public class UserSessionConcurrentHashMapStorage extends ConcurrentHashMapSto
@SuppressWarnings("unchecked")
public UserSessionConcurrentHashMapStorage(ConcurrentHashMapStorage clientSessionStore,
- StringKeyConvertor keyConvertor) {
- super(UserSessionModel.class, keyConvertor);
+ StringKeyConvertor keyConvertor, DeepCloner cloner) {
+ super(UserSessionModel.class, keyConvertor, cloner);
this.clientSessionStore = clientSessionStore;
}
@@ -79,6 +80,6 @@ public class UserSessionConcurrentHashMapStorage extends ConcurrentHashMapSto
@SuppressWarnings("unchecked")
public MapKeycloakTransaction createTransaction(KeycloakSession session) {
MapKeycloakTransaction sessionTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class);
- return sessionTransaction == null ? new Transaction(clientSessionStore.createTransaction(session), clientSessionStore.getKeyConvertor()) : sessionTransaction;
+ return sessionTransaction == null ? new Transaction(clientSessionStore.createTransaction(session), clientSessionStore.getKeyConvertor(), cloner) : sessionTransaction;
}
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java
index 63ffab1968..2eab99a6aa 100644
--- a/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java
@@ -40,7 +40,7 @@ import java.util.stream.Stream;
*
* @author mhajas
*/
-public class MapUserEntity implements AbstractEntity, UpdatableEntity {
+public class MapUserEntity extends UpdatableEntity.Impl implements AbstractEntity {
private String id;
private String realmId;
@@ -69,9 +69,8 @@ public class MapUserEntity implements AbstractEntity, UpdatableEntity {
/**
* Flag signalizing that any of the setters has been meaningfully used.
*/
- protected boolean updated;
- protected MapUserEntity() {}
+ public MapUserEntity() {}
public MapUserEntity(String id, String realmId) {
this.id = id;
diff --git a/model/map/src/main/java/org/keycloak/models/map/user/UserConsentEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/UserConsentEntity.java
index 966d38832a..7b2a3f0da1 100644
--- a/model/map/src/main/java/org/keycloak/models/map/user/UserConsentEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/user/UserConsentEntity.java
@@ -31,13 +31,12 @@ import java.util.Objects;
import java.util.Set;
-public class UserConsentEntity implements UpdatableEntity {
+public class UserConsentEntity extends UpdatableEntity.Impl {
private String clientId;
private final Set grantedClientScopesIds = new HashSet<>();
private Long createdDate;
private Long lastUpdatedDate;
- private boolean updated;
private UserConsentEntity() {}
@@ -78,11 +77,6 @@ public class UserConsentEntity implements UpdatableEntity {
return model;
}
- @Override
- public boolean isUpdated() {
- return updated;
- }
-
public String getClientId() {
return clientId;
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/user/UserCredentialEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/UserCredentialEntity.java
index 68df6f012a..c674925fa7 100644
--- a/model/map/src/main/java/org/keycloak/models/map/user/UserCredentialEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/user/UserCredentialEntity.java
@@ -23,7 +23,7 @@ import org.keycloak.models.map.common.UpdatableEntity;
import java.util.Objects;
-public class UserCredentialEntity implements UpdatableEntity {
+public class UserCredentialEntity extends UpdatableEntity.Impl {
private String id;
private String type;
@@ -31,7 +31,6 @@ public class UserCredentialEntity implements UpdatableEntity {
private Long createdDate;
private String secretData;
private String credentialData;
- private boolean updated;
UserCredentialEntity() {}
@@ -112,9 +111,4 @@ public class UserCredentialEntity implements UpdatableEntity {
this.updated |= !Objects.equals(this.credentialData, credentialData);
this.credentialData = credentialData;
}
-
- @Override
- public boolean isUpdated() {
- return updated;
- }
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/user/UserFederatedIdentityEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/UserFederatedIdentityEntity.java
index a57f96aca7..c73393e6b7 100644
--- a/model/map/src/main/java/org/keycloak/models/map/user/UserFederatedIdentityEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/user/UserFederatedIdentityEntity.java
@@ -22,12 +22,11 @@ import org.keycloak.models.map.common.UpdatableEntity;
import java.util.Objects;
-public class UserFederatedIdentityEntity implements UpdatableEntity {
+public class UserFederatedIdentityEntity extends UpdatableEntity.Impl {
private String token;
private String userId;
private String identityProvider;
private String userName;
- private boolean updated;
private UserFederatedIdentityEntity() {}
@@ -82,9 +81,4 @@ public class UserFederatedIdentityEntity implements UpdatableEntity {
this.updated |= !Objects.equals(this.userName, userName);
this.userName = userName;
}
-
- @Override
- public boolean isUpdated() {
- return updated;
- }
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/userSession/MapAuthenticatedClientSessionEntity.java b/model/map/src/main/java/org/keycloak/models/map/userSession/MapAuthenticatedClientSessionEntity.java
index 1e8dde041e..33ad54b89b 100644
--- a/model/map/src/main/java/org/keycloak/models/map/userSession/MapAuthenticatedClientSessionEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/userSession/MapAuthenticatedClientSessionEntity.java
@@ -27,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap;
/**
* @author Martin Kanis
*/
-public class MapAuthenticatedClientSessionEntity implements AbstractEntity, UpdatableEntity {
+public class MapAuthenticatedClientSessionEntity extends UpdatableEntity.Impl implements AbstractEntity {
private String id;
private String userSessionId;
@@ -37,7 +37,6 @@ public class MapAuthenticatedClientSessionEntity implements AbstractEntity, Upda
/**
* Flag signalizing that any of the setters has been meaningfully used.
*/
- protected boolean updated;
private String authMethod;
private String redirectUri;
@@ -75,11 +74,6 @@ public class MapAuthenticatedClientSessionEntity implements AbstractEntity, Upda
this.updated |= id != null;
}
- @Override
- public boolean isUpdated() {
- return this.updated;
- }
-
public String getRealmId() {
return realmId;
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionEntity.java b/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionEntity.java
index b63f1a29fc..edba96570f 100644
--- a/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionEntity.java
+++ b/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionEntity.java
@@ -30,7 +30,7 @@ import java.util.concurrent.ConcurrentHashMap;
/**
* @author Martin Kanis
*/
-public class MapUserSessionEntity implements AbstractEntity, UpdatableEntity {
+public class MapUserSessionEntity extends UpdatableEntity.Impl implements AbstractEntity {
private String id;
private String realmId;
@@ -38,7 +38,6 @@ public class MapUserSessionEntity implements AbstractEntity, UpdatableEntity {
/**
* Flag signalizing that any of the setters has been meaningfully used.
*/
- protected boolean updated;
private String userId;
@@ -105,11 +104,6 @@ public class MapUserSessionEntity implements AbstractEntity, UpdatableEntity {
this.updated |= id != null;
}
- @Override
- public boolean isUpdated() {
- return this.updated;
- }
-
public String getRealmId() {
return realmId;
}
diff --git a/model/map/src/test/java/org/keycloak/models/map/client/MapClientEntityClonerTest.java b/model/map/src/test/java/org/keycloak/models/map/client/MapClientEntityClonerTest.java
new file mode 100644
index 0000000000..aed292b0fa
--- /dev/null
+++ b/model/map/src/test/java/org/keycloak/models/map/client/MapClientEntityClonerTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models.map.client;
+
+import org.keycloak.models.map.common.DeepCloner;
+import java.util.Arrays;
+import java.util.HashMap;
+import org.junit.Test;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.sameInstance;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class MapClientEntityClonerTest {
+
+ private final static DeepCloner CLONER = new DeepCloner.Builder()
+ .constructorDC(MapClientEntityImpl.class, MapClientEntityImpl::new)
+ .constructor(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl::new)
+ .build();
+
+ @Test
+ public void testNewInstance() {
+ MapClientEntity newInstance = CLONER.newInstance(MapClientEntity.class);
+ assertThat(newInstance, instanceOf(MapClientEntityImpl.class));
+ assertThat(newInstance.getId(), nullValue());
+ }
+
+ @Test
+ public void testNewInstanceWithId() {
+ MapClientEntity newInstance = CLONER.newInstance(MapClientEntity.class);
+ newInstance.setId("my-id");
+ assertThat(newInstance, instanceOf(MapClientEntityImpl.class));
+ assertThat(newInstance.getId(), is("my-id"));
+ }
+
+ @Test
+ public void testCloneAsNewInstance() {
+ MapClientEntity newInstance = CLONER.newInstance(MapClientEntity.class);
+ newInstance.setId("my-id");
+ newInstance.setClientId("a-client-id");
+ newInstance.setAttribute("attr", Arrays.asList("aa", "bb", "cc"));
+
+ MapClientEntity clonedInstance = CLONER.from(newInstance);
+ assertThat(clonedInstance, instanceOf(MapClientEntityImpl.class));
+ assertThat(clonedInstance.getId(), is("my-id"));
+ assertThat(clonedInstance.getClientId(), is("a-client-id"));
+
+ assertThat(clonedInstance.getAttributes(), not(sameInstance(newInstance.getAttributes())));
+ assertThat(clonedInstance.getAttributes().keySet(), containsInAnyOrder("attr"));
+ assertThat(clonedInstance.getAttributes().get("attr"), contains("aa", "bb", "cc"));
+ assertThat(clonedInstance.getAttributes().get("attr"), not(sameInstance(newInstance.getAttributes().get("attr"))));
+
+ assertThat(clonedInstance.getAuthenticationFlowBindingOverrides(), nullValue());
+ assertThat(clonedInstance.getRegistrationToken(), nullValue());
+ }
+
+ @Test
+ public void testCloneToExistingInstance() {
+ MapClientEntity newInstance = CLONER.newInstance(MapClientEntity.class);
+ newInstance.setId("my-id");
+ newInstance.setClientId("a-client-id");
+ newInstance.setAttribute("attr", Arrays.asList("aa", "bb", "cc"));
+ MapProtocolMapperEntity pmm = new MapProtocolMapperEntityImpl();
+ pmm.setId("pmm-id");
+ pmm.setConfig(new HashMap<>());
+ pmm.getConfig().put("key1", "value1");
+ pmm.getConfig().put("key2", "value2");
+ newInstance.setProtocolMapper("pmm-id", pmm);
+ newInstance.setAttribute("attr", Arrays.asList("aa", "bb", "cc"));
+
+ MapClientEntity clonedInstance = CLONER.newInstance(MapClientEntity.class);
+ assertThat(CLONER.deepCloneNoId(newInstance, clonedInstance), sameInstance(clonedInstance));
+ assertThat(clonedInstance, instanceOf(MapClientEntityImpl.class));
+ clonedInstance.setId("my-id2");
+ assertThat(clonedInstance.getId(), is("my-id2"));
+ assertThat(clonedInstance.getClientId(), is("a-client-id"));
+
+ assertThat(clonedInstance.getAttributes(), not(sameInstance(newInstance.getAttributes())));
+ assertThat(clonedInstance.getAttributes().keySet(), containsInAnyOrder("attr"));
+ assertThat(clonedInstance.getAttributes().get("attr"), contains("aa", "bb", "cc"));
+ assertThat(clonedInstance.getAttributes().get("attr"), not(sameInstance(newInstance.getAttributes().get("attr"))));
+
+ assertThat(clonedInstance.getProtocolMappers(), not(sameInstance(newInstance.getProtocolMappers())));
+ assertThat(clonedInstance.getProtocolMapper("pmm-id"), not(sameInstance(newInstance.getProtocolMapper("pmm-id"))));
+ assertThat(clonedInstance.getProtocolMapper("pmm-id"), equalTo(newInstance.getProtocolMapper("pmm-id")));
+ assertThat(clonedInstance.getProtocolMapper("pmm-id").getConfig(), not(sameInstance(newInstance.getProtocolMapper("pmm-id").getConfig())));
+ assertThat(clonedInstance.getProtocolMapper("pmm-id").getConfig(), equalTo(newInstance.getProtocolMapper("pmm-id").getConfig()));
+
+ assertThat(clonedInstance.getAuthenticationFlowBindingOverrides(), nullValue());
+ assertThat(clonedInstance.getRegistrationToken(), nullValue());
+ }
+}
diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/MapStorageTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/MapStorageTest.java
index 0f00ffc20a..218def8c25 100644
--- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/MapStorageTest.java
+++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/MapStorageTest.java
@@ -117,11 +117,14 @@ public class MapStorageTest extends KeycloakModelTest {
assertClientDoesNotExist(storage2, idMain, kcMain, kc2);
assertClientDoesNotExist(storage2, id1, kc1, kc2);
- MapClientEntity clientMain = new MapClientEntityImpl(idMain);
+ MapClientEntity clientMain = new MapClientEntityImpl();
+ clientMain.setId(idMain);
clientMain.setRealmId(realmId);
- MapClientEntity client1 = new MapClientEntityImpl(id1);
+ MapClientEntity client1 = new MapClientEntityImpl();
+ client1.setId(id1);
client1.setRealmId(realmId);
- MapClientEntity client2 = new MapClientEntityImpl(id2);
+ MapClientEntity client2 = new MapClientEntityImpl();
+ client2.setId(id2);
client2.setRealmId(realmId);
clientMain = storageMain.create(clientMain);