Introduce per-field delegation of entities
This commit is contained in:
parent
80873ea4b9
commit
d39eb95705
17 changed files with 1818 additions and 42 deletions
|
@ -17,17 +17,20 @@
|
||||||
package org.keycloak.models.map.storage.jpa.client.delegate;
|
package org.keycloak.models.map.storage.jpa.client.delegate;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import javax.persistence.criteria.CriteriaBuilder;
|
import javax.persistence.criteria.CriteriaBuilder;
|
||||||
import javax.persistence.criteria.CriteriaQuery;
|
import javax.persistence.criteria.CriteriaQuery;
|
||||||
import javax.persistence.criteria.JoinType;
|
import javax.persistence.criteria.JoinType;
|
||||||
import javax.persistence.criteria.Root;
|
import javax.persistence.criteria.Root;
|
||||||
|
|
||||||
import org.keycloak.models.map.client.MapClientEntity;
|
import org.keycloak.models.map.client.MapClientEntity;
|
||||||
import org.keycloak.models.map.client.MapClientEntityFields;
|
import org.keycloak.models.map.client.MapClientEntityFields;
|
||||||
|
import org.keycloak.models.map.common.EntityField;
|
||||||
import org.keycloak.models.map.common.delegate.DelegateProvider;
|
import org.keycloak.models.map.common.delegate.DelegateProvider;
|
||||||
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity;
|
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity;
|
||||||
|
|
||||||
public class JpaClientDelegateProvider<T extends MapClientEntity> implements DelegateProvider {
|
public class JpaClientDelegateProvider implements DelegateProvider<MapClientEntity> {
|
||||||
|
|
||||||
private JpaClientEntity delegate;
|
private JpaClientEntity delegate;
|
||||||
private final EntityManager em;
|
private final EntityManager em;
|
||||||
|
@ -38,7 +41,7 @@ public class JpaClientDelegateProvider<T extends MapClientEntity> implements Del
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JpaClientEntity getDelegate(boolean isRead, Object field, Object... parameters) {
|
public MapClientEntity getDelegate(boolean isRead, Enum<? extends EntityField<MapClientEntity>> field, Object... parameters) {
|
||||||
if (delegate.isMetadataInitialized()) return delegate;
|
if (delegate.isMetadataInitialized()) return delegate;
|
||||||
if (isRead) {
|
if (isRead) {
|
||||||
if (field instanceof MapClientEntityFields) {
|
if (field instanceof MapClientEntityFields) {
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.models.map.common.delegate;
|
package org.keycloak.models.map.common.delegate;
|
||||||
|
|
||||||
|
import org.keycloak.models.map.common.EntityField;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for a provider of a delegate of type {@code T}, optionally
|
* Interface for a provider of a delegate of type {@code T}, optionally
|
||||||
* providing the flag on the object been updated.
|
* providing the flag on the object been updated.
|
||||||
|
@ -25,10 +27,12 @@ public interface DelegateProvider<T> {
|
||||||
/**
|
/**
|
||||||
* Returns a delegate for and entity for an operation on a field.
|
* Returns a delegate for and entity for an operation on a field.
|
||||||
* @param isRead {@code true} when the delegate requested for a read operation, false otherwise
|
* @param isRead {@code true} when the delegate requested for a read operation, false otherwise
|
||||||
* @param field Identification of the field this delegates operates on.
|
* @param field Identification of the field this delegates operates on. While this parameter
|
||||||
|
* can be any object including {@code null}, if it is a known field, then it is guaranteed to be
|
||||||
|
* one of the {@code EntityField}s enumerated in one of the {@code Map*EntityFields} enum.
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
T getDelegate(boolean isRead, Object field, Object... parameters);
|
T getDelegate(boolean isRead, Enum<? extends EntityField<T>> field, Object... parameters);
|
||||||
|
|
||||||
default boolean isUpdated() { return false; }
|
default boolean isUpdated() { return false; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,17 +8,17 @@ import org.keycloak.models.map.common.UpdatableEntity;
|
||||||
|
|
||||||
public interface EntityFieldDelegate<E> extends UpdatableEntity {
|
public interface EntityFieldDelegate<E> extends UpdatableEntity {
|
||||||
// Non-collection values
|
// Non-collection values
|
||||||
Object get(EntityField<E> field);
|
<EF extends Enum<? extends EntityField<E>> & EntityField<E>> Object get(EF field);
|
||||||
default <T> void set(EntityField<E> field, T value) {}
|
default <T, EF extends Enum<? extends EntityField<E>> & EntityField<E>> void set(EF field, T value) {}
|
||||||
|
|
||||||
default <T> void collectionAdd(EntityField<E> field, T value) {
|
default <T, EF extends Enum<? extends EntityField<E>> & EntityField<E>> void collectionAdd(EF field, T value) {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Collection<T> c = (Collection<T>) get(field);
|
Collection<T> c = (Collection<T>) get(field);
|
||||||
if (c != null) {
|
if (c != null) {
|
||||||
c.add(value);
|
c.add(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default <T> Object collectionRemove(EntityField<E> field, T value) {
|
default <T, EF extends Enum<? extends EntityField<E>> & EntityField<E>> Object collectionRemove(EF field, T value) {
|
||||||
Collection<?> c = (Collection<?>) get(field);
|
Collection<?> c = (Collection<?>) get(field);
|
||||||
return c == null ? null : c.remove(value);
|
return c == null ? null : c.remove(value);
|
||||||
}
|
}
|
||||||
|
@ -32,19 +32,19 @@ public interface EntityFieldDelegate<E> extends UpdatableEntity {
|
||||||
* @param valueClass class of the value
|
* @param valueClass class of the value
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
default <K> Object mapGet(EntityField<E> field, K key) {
|
default <K, EF extends Enum<? extends EntityField<E>> & EntityField<E>> Object mapGet(EF field, K key) {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<K, ?> m = (Map<K, ?>) get(field);
|
Map<K, ?> m = (Map<K, ?>) get(field);
|
||||||
return m == null ? null : m.get(key);
|
return m == null ? null : m.get(key);
|
||||||
}
|
}
|
||||||
default <K, T> void mapPut(EntityField<E> field, K key, T value) {
|
default <K, T, EF extends Enum<? extends EntityField<E>> & EntityField<E>> void mapPut(EF field, K key, T value) {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<K, T> m = (Map<K, T>) get(field);
|
Map<K, T> m = (Map<K, T>) get(field);
|
||||||
if (m != null) {
|
if (m != null) {
|
||||||
m.put(key, value);
|
m.put(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default <K> Object mapRemove(EntityField<E> field, K key) {
|
default <K, EF extends Enum<? extends EntityField<E>> & EntityField<E>> Object mapRemove(EF field, K key) {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<K, ?> m = (Map<K, ?>) get(field);
|
Map<K, ?> m = (Map<K, ?>) get(field);
|
||||||
if (m != null) {
|
if (m != null) {
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.models.map.common.delegate;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicMarkableReference;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class LazilyInitialized<T> {
|
||||||
|
|
||||||
|
private final Supplier<T> supplier;
|
||||||
|
|
||||||
|
private final AtomicMarkableReference<T> supplierRef = new AtomicMarkableReference<>(null, false);
|
||||||
|
|
||||||
|
public LazilyInitialized(Supplier<T> supplier) {
|
||||||
|
this.supplier = supplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T get() {
|
||||||
|
if (! isInitialized()) {
|
||||||
|
supplierRef.compareAndSet(null, supplier == null ? null : supplier.get(), false, true);
|
||||||
|
}
|
||||||
|
return supplierRef.getReference();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the reference to the object has been initialized
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean isInitialized() {
|
||||||
|
return supplierRef.isMarked();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -16,6 +16,8 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.models.map.common.delegate;
|
package org.keycloak.models.map.common.delegate;
|
||||||
|
|
||||||
|
import org.keycloak.models.map.common.AbstractEntity;
|
||||||
|
import org.keycloak.models.map.common.EntityField;
|
||||||
import org.keycloak.models.map.common.UpdatableEntity;
|
import org.keycloak.models.map.common.UpdatableEntity;
|
||||||
import java.util.concurrent.atomic.AtomicMarkableReference;
|
import java.util.concurrent.atomic.AtomicMarkableReference;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
@ -24,37 +26,28 @@ import java.util.function.Supplier;
|
||||||
*
|
*
|
||||||
* @author hmlnarik
|
* @author hmlnarik
|
||||||
*/
|
*/
|
||||||
public class LazyDelegateProvider<T extends UpdatableEntity> implements DelegateProvider {
|
public class LazyDelegateProvider<T extends AbstractEntity> implements DelegateProvider<T> {
|
||||||
|
|
||||||
private final Supplier<T> delegateSupplier;
|
protected final LazilyInitialized<T> delegateSupplier;
|
||||||
|
|
||||||
private final AtomicMarkableReference<T> delegate = new AtomicMarkableReference<>(null, false);
|
|
||||||
|
|
||||||
public LazyDelegateProvider(Supplier<T> delegateSupplier) {
|
public LazyDelegateProvider(Supplier<T> delegateSupplier) {
|
||||||
this.delegateSupplier = delegateSupplier;
|
this.delegateSupplier = new LazilyInitialized<>(delegateSupplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public T getDelegate(boolean isRead, Object field, Object... parameters) {
|
public T getDelegate(boolean isRead, Enum<? extends EntityField<T>> field, Object... parameters) {
|
||||||
if (! isDelegateInitialized()) {
|
T ref = delegateSupplier.get();
|
||||||
delegate.compareAndSet(null, delegateSupplier == null ? null : delegateSupplier.get(), false, true);
|
|
||||||
}
|
|
||||||
T ref = delegate.getReference();
|
|
||||||
if (ref == null) {
|
if (ref == null) {
|
||||||
throw new IllegalStateException("Invalid delegate obtained");
|
throw new IllegalStateException("Invalid delegate obtained");
|
||||||
}
|
}
|
||||||
return ref;
|
return ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean isDelegateInitialized() {
|
|
||||||
return delegate.isMarked();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isUpdated() {
|
public boolean isUpdated() {
|
||||||
if (isDelegateInitialized()) {
|
if (delegateSupplier.isInitialized()) {
|
||||||
T d = getDelegate(true, this);
|
T d = delegateSupplier.get();
|
||||||
return d.isUpdated();
|
return d instanceof UpdatableEntity ? ((UpdatableEntity) d).isUpdated() : false;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,297 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.models.map.common.delegate;
|
||||||
|
|
||||||
|
import org.keycloak.models.map.common.AbstractEntity;
|
||||||
|
import org.keycloak.models.map.common.EntityField;
|
||||||
|
import org.keycloak.models.map.common.UpdatableEntity;
|
||||||
|
import org.keycloak.models.map.storage.tree.TreeStorageNodeInstance;
|
||||||
|
import org.keycloak.models.map.storage.tree.TreeStorageNodePrescription.FieldContainedStatus;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collector;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class PerFieldDelegateProvider<V extends AbstractEntity> implements EntityFieldDelegate<V> {
|
||||||
|
|
||||||
|
private final TreeStorageNodeInstance<V> node;
|
||||||
|
|
||||||
|
private final V entity;
|
||||||
|
|
||||||
|
private final LazilyInitialized<V> lowerEntity;
|
||||||
|
|
||||||
|
public PerFieldDelegateProvider(TreeStorageNodeInstance<V> node, V entity, Supplier<V> fallbackProvider) {
|
||||||
|
this.node = node;
|
||||||
|
this.entity = entity;
|
||||||
|
this.lowerEntity = new LazilyInitialized<>(fallbackProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PerFieldDelegateProvider(TreeStorageNodeInstance<V>.WithEntity nodeWithEntity, Supplier<V> fallbackProvider) {
|
||||||
|
this(nodeWithEntity.getNode(), nodeWithEntity.getEntity(), fallbackProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private V getEntityFromDescendantNode() {
|
||||||
|
final V res = lowerEntity.get();
|
||||||
|
Objects.requireNonNull(res, () -> "Descendant entity not found for node " + node);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <K, EF extends Enum<? extends EntityField<V>> & EntityField<V>> Object mapRemove(EF field, K key) {
|
||||||
|
Objects.requireNonNull(key, "Key must not be null");
|
||||||
|
boolean needsSetEntity = false;
|
||||||
|
boolean needsSetLowerEntity = false;
|
||||||
|
|
||||||
|
switch (node.isCacheFor(field, key)) {
|
||||||
|
case FULLY:
|
||||||
|
needsSetEntity = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NOT_CONTAINED:
|
||||||
|
needsSetLowerEntity = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (node.isPrimarySourceFor(field, key)) {
|
||||||
|
case FULLY:
|
||||||
|
needsSetEntity = true;
|
||||||
|
needsSetLowerEntity = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NOT_CONTAINED:
|
||||||
|
needsSetLowerEntity = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object res = null;
|
||||||
|
if (needsSetEntity) {
|
||||||
|
res = field.mapRemove(entity, key);
|
||||||
|
}
|
||||||
|
if (needsSetLowerEntity) {
|
||||||
|
res = field.mapRemove(getEntityFromDescendantNode(), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <K, T, EF extends Enum<? extends EntityField<V>> & EntityField<V>> void mapPut(EF field, K key, T value) {
|
||||||
|
Objects.requireNonNull(key, "Key must not be null");
|
||||||
|
boolean needsSetEntity = false;
|
||||||
|
boolean needsSetLowerEntity = false;
|
||||||
|
|
||||||
|
switch (node.isCacheFor(field, key)) {
|
||||||
|
case FULLY:
|
||||||
|
needsSetEntity = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NOT_CONTAINED:
|
||||||
|
needsSetLowerEntity = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (node.isPrimarySourceFor(field, key)) {
|
||||||
|
case FULLY:
|
||||||
|
needsSetEntity = true;
|
||||||
|
needsSetLowerEntity = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NOT_CONTAINED:
|
||||||
|
needsSetLowerEntity = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsSetEntity) {
|
||||||
|
field.mapPut(entity, key, value);
|
||||||
|
}
|
||||||
|
if (needsSetLowerEntity) {
|
||||||
|
field.mapPut(getEntityFromDescendantNode(), key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <K, EF extends Enum<? extends EntityField<V>> & EntityField<V>> Object mapGet(EF field, K key) {
|
||||||
|
Objects.requireNonNull(key, "Key must not be null");
|
||||||
|
switch (node.isCacheFor(field, key).max(() -> node.isPrimarySourceFor(field, key))) {
|
||||||
|
case FULLY:
|
||||||
|
return field.mapGet(entity, key);
|
||||||
|
case NOT_CONTAINED:
|
||||||
|
return field.mapGet(getEntityFromDescendantNode(), key);
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Field is not determined: " + field);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T, EF extends Enum<? extends EntityField<V>> & EntityField<V>> Object collectionRemove(EF field, T value) {
|
||||||
|
boolean needsSetEntity = false;
|
||||||
|
boolean needsSetLowerEntity = false;
|
||||||
|
|
||||||
|
switch (node.isCacheFor(field, value)) {
|
||||||
|
case FULLY:
|
||||||
|
needsSetEntity = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NOT_CONTAINED:
|
||||||
|
needsSetLowerEntity = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (node.isPrimarySourceFor(field, value)) {
|
||||||
|
case FULLY:
|
||||||
|
needsSetEntity = true;
|
||||||
|
needsSetLowerEntity = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NOT_CONTAINED:
|
||||||
|
needsSetLowerEntity = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object res = null;
|
||||||
|
if (needsSetEntity) {
|
||||||
|
res = field.collectionRemove(entity, value);
|
||||||
|
}
|
||||||
|
if (needsSetLowerEntity) {
|
||||||
|
res = field.collectionRemove(getEntityFromDescendantNode(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T, EF extends Enum<? extends EntityField<V>> & EntityField<V>> void collectionAdd(EF field, T value) {
|
||||||
|
boolean needsSetEntity = false;
|
||||||
|
boolean needsSetLowerEntity = false;
|
||||||
|
|
||||||
|
switch (node.isCacheFor(field, null)) {
|
||||||
|
case FULLY:
|
||||||
|
needsSetEntity = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NOT_CONTAINED:
|
||||||
|
needsSetLowerEntity = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (node.isPrimarySourceFor(field, null)) {
|
||||||
|
case FULLY:
|
||||||
|
needsSetEntity = true;
|
||||||
|
needsSetLowerEntity = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NOT_CONTAINED:
|
||||||
|
needsSetLowerEntity = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsSetEntity) {
|
||||||
|
field.collectionAdd(entity, value);
|
||||||
|
}
|
||||||
|
if (needsSetLowerEntity) {
|
||||||
|
field.collectionAdd(getEntityFromDescendantNode(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Collector<Map.Entry, ?, Map<Object, Object>> ENTRY_TO_HASH_MAP_OVERRIDING_KEYS_COLLECTOR = Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (Object a, Object b) -> b, HashMap::new);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <EF extends Enum<? extends EntityField<V>> & EntityField<V>> Object get(EF field) {
|
||||||
|
switch (node.isCacheFor(field, null).max(() -> node.isPrimarySourceFor(field, null))) {
|
||||||
|
case FULLY:
|
||||||
|
return field.get(entity);
|
||||||
|
case NOT_CONTAINED:
|
||||||
|
return field.get(getEntityFromDescendantNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
// It has to be partial field. The only supported partial field is a Map
|
||||||
|
if (field.getMapKeyClass() == Void.class) {
|
||||||
|
throw new IllegalStateException("Field " + field + " expected to be a map but is " + field.getFieldClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Object, Object> m1 = (Map<Object, Object>) field.get(entity);
|
||||||
|
Map m2 = (Map) field.get(getEntityFromDescendantNode());
|
||||||
|
if (m1 == null) {
|
||||||
|
return m2 == null ? null : new HashMap<>(m2);
|
||||||
|
}
|
||||||
|
Predicate<Map.Entry<Object, Object>> isInNode = me -> node.isCacheFor(field, me.getKey())
|
||||||
|
.max(() -> node.isPrimarySourceFor(field, me.getKey())) == FieldContainedStatus.FULLY;
|
||||||
|
Stream<Map.Entry<Object, Object>> s = m1.entrySet().stream()
|
||||||
|
.filter(isInNode);
|
||||||
|
if (m2 == null) {
|
||||||
|
return s.collect(ENTRY_TO_HASH_MAP_OVERRIDING_KEYS_COLLECTOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Stream.concat(s, m2.entrySet().stream().filter(isInNode.negate()))
|
||||||
|
.collect(ENTRY_TO_HASH_MAP_OVERRIDING_KEYS_COLLECTOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T, EF extends Enum<? extends EntityField<V>> & EntityField<V>> void set(EF field, T value) {
|
||||||
|
boolean needsSetEntity = false;
|
||||||
|
boolean needsSetLowerEntity = false;
|
||||||
|
|
||||||
|
switch (node.isCacheFor(field, null)) {
|
||||||
|
case FULLY:
|
||||||
|
needsSetEntity = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PARTIALLY:
|
||||||
|
needsSetEntity = true;
|
||||||
|
needsSetLowerEntity = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (node.isPrimarySourceFor(field, null)) {
|
||||||
|
case FULLY:
|
||||||
|
needsSetEntity = true;
|
||||||
|
needsSetLowerEntity = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PARTIALLY:
|
||||||
|
needsSetEntity = true;
|
||||||
|
needsSetLowerEntity = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NOT_CONTAINED:
|
||||||
|
needsSetLowerEntity = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsSetEntity) {
|
||||||
|
field.set(entity, value);
|
||||||
|
}
|
||||||
|
if (needsSetLowerEntity) {
|
||||||
|
field.set(getEntityFromDescendantNode(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUpdated() {
|
||||||
|
return entity instanceof UpdatableEntity ? ((UpdatableEntity) entity).isUpdated() : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -16,13 +16,14 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.models.map.common.delegate;
|
package org.keycloak.models.map.common.delegate;
|
||||||
|
|
||||||
|
import org.keycloak.models.map.common.EntityField;
|
||||||
import org.keycloak.models.map.common.UpdatableEntity;
|
import org.keycloak.models.map.common.UpdatableEntity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author hmlnarik
|
* @author hmlnarik
|
||||||
*/
|
*/
|
||||||
public class SimpleDelegateProvider<T extends UpdatableEntity> implements DelegateProvider {
|
public class SimpleDelegateProvider<T extends UpdatableEntity> implements DelegateProvider<T> {
|
||||||
|
|
||||||
private final T delegate;
|
private final T delegate;
|
||||||
|
|
||||||
|
@ -31,7 +32,7 @@ public class SimpleDelegateProvider<T extends UpdatableEntity> implements Delega
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public T getDelegate(boolean isRead, Object field, Object... parameters) {
|
public T getDelegate(boolean isRead, Enum<? extends EntityField<T>> field, Object... parameters) {
|
||||||
return this.delegate;
|
return this.delegate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,9 +45,9 @@ public class DefaultTreeNode<Self extends DefaultTreeNode<Self>> implements Tree
|
||||||
|
|
||||||
private static final AtomicInteger COUNTER = new AtomicInteger();
|
private static final AtomicInteger COUNTER = new AtomicInteger();
|
||||||
|
|
||||||
private final Map<String, Object> nodeProperties;
|
protected final Map<String, Object> nodeProperties;
|
||||||
private final Map<String, Object> edgeProperties;
|
protected final Map<String, Object> edgeProperties;
|
||||||
private final Map<String, Object> treeProperties;
|
protected final Map<String, Object> treeProperties;
|
||||||
private final LinkedList<Self> children = new LinkedList<>();
|
private final LinkedList<Self> children = new LinkedList<>();
|
||||||
private String id;
|
private String id;
|
||||||
private Self parent;
|
private Self parent;
|
||||||
|
@ -224,6 +224,14 @@ public class DefaultTreeNode<Self extends DefaultTreeNode<Self>> implements Tree
|
||||||
return Collections.unmodifiableList(this.children);
|
return Collections.unmodifiableList(this.children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasChildren() {
|
||||||
|
return ! this.children.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasNoChildren() {
|
||||||
|
return this.children.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addChild(Self node) {
|
public void addChild(Self node) {
|
||||||
if (node == null) {
|
if (node == null) {
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* 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.tree;
|
||||||
|
|
||||||
|
import org.keycloak.models.map.common.EntityField;
|
||||||
|
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public final class NodeProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the filter that must be satisfied for every entity within this store.
|
||||||
|
* Type: {@link DefaultModelCriteria}
|
||||||
|
*/
|
||||||
|
public static final String ENTITY_RESTRICTION = "entity-restriction";
|
||||||
|
public static final String AUTHORITATIVE_DECIDER = "authoritative-decider";
|
||||||
|
public static final String STRONGLY_AUTHORITATIVE = "strongly-authoritative";
|
||||||
|
public static final String READ_ONLY = "read-only";
|
||||||
|
public static final String REVALIDATE = "revalidate";
|
||||||
|
|
||||||
|
public static final String AUTHORITATIVE_NODES = "___authoritative-nodes___";
|
||||||
|
public static final String STORAGE_PROVIDER = "___storage-provider___";
|
||||||
|
public static final String STORAGE_SUPPLIER = "___storage-supplier___";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of pairs ({@code k}: {@link EntityField}, {@code v}: {@link Collection}) of fields that the node is primary source for.
|
||||||
|
* <p>
|
||||||
|
* For example, the following statements are expressed:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code (name -> null)}: This node is primary source for the value of the field {@code name}.
|
||||||
|
* <li>{@code (attributes -> null)}: This node is primary source for the values of all attributes.
|
||||||
|
* <li>{@code (attributes -> {"address", "logo"})}: This node is primary source only for the values of attributes "address" and "logo".
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public static final String PRIMARY_SOURCE_FOR = "___primary-source-for___";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of pairs ({@code k}: {@link EntityField}, {@code v}: {@link Collection}) of fields that the node is not primary source for.
|
||||||
|
* <p>
|
||||||
|
* For example, the following statements are expressed:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code (name -> null)}: This node is not primary source for the value of the field {@code name}.
|
||||||
|
* <li>{@code (attributes -> null)}: This node is not primary source for the values of any attributes.
|
||||||
|
* <li>{@code (attributes -> {"address", "logo"})}: This node is primary source only for attributes apart from "address" and "logo" attributes.
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public static final String PRIMARY_SOURCE_FOR_EXCLUDED = "___primary-source-for-excluded___";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of pairs ({@code k}: {@link EntityField}, {@code v}: {@link Collection}) of fields that the node is primary source for.
|
||||||
|
* <p>
|
||||||
|
* For example, the following statements are expressed:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code (name -> null)}: This node is primary source for the value of the field {@code name}.
|
||||||
|
* <li>{@code (attributes -> null)}: This node is primary source for the values of all attributes.
|
||||||
|
* <li>{@code (attributes -> {"address", "logo"})}: This node is primary source only for the values of attributes "address" and "logo".
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public static final String CACHE_FOR = "___cache-for___";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of pairs ({@code k}: {@link EntityField}, {@code v}: {@link Collection}) of fields that the node is not primary source for.
|
||||||
|
* <p>
|
||||||
|
* For example, the following statements are expressed:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code (name -> null)}: This node is not primary source for the value of the field {@code name}.
|
||||||
|
* <li>{@code (attributes -> null)}: This node is not primary source for the values of any attributes.
|
||||||
|
* <li>{@code (attributes -> {"address", "logo"})}: This node is primary source only for attributes apart from "address" and "logo" attributes.
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public static final String CACHE_FOR_EXCLUDED = "___cache-for-excluded___";
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* 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.tree;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public final class TreeProperties {
|
||||||
|
|
||||||
|
public static final String MODEL_CLASS = "model-class";
|
||||||
|
public static final String DEFAULT_STORE_CREATE = "default-create";
|
||||||
|
public static final String DEFAULT_STORE_READ = "default-read";
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
* 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.tree;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.map.common.AbstractEntity;
|
||||||
|
import org.keycloak.models.map.common.EntityField;
|
||||||
|
import org.keycloak.models.map.storage.tree.TreeStorageNodePrescription.FieldContainedStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instance of the tree storage that is based on a prescription ({@link TreeStorageNodePrescription}),
|
||||||
|
* i.e. it provides a map storage instance that can be used for accessing data.
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class TreeStorageNodeInstance<V extends AbstractEntity>
|
||||||
|
extends DefaultTreeNode<TreeStorageNodeInstance<V>> {
|
||||||
|
|
||||||
|
private final KeycloakSession session;
|
||||||
|
private final TreeStorageNodePrescription prescription;
|
||||||
|
private final TreeStorageNodeInstance<V> original; // If this node is a subtree, keep reference to the node in the original tree here
|
||||||
|
|
||||||
|
public class WithEntity {
|
||||||
|
private final V entity;
|
||||||
|
|
||||||
|
public WithEntity(V entity) {
|
||||||
|
this.entity = entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public V getEntity() {
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TreeStorageNodeInstance<V> getNode() {
|
||||||
|
return TreeStorageNodeInstance.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public TreeStorageNodeInstance(KeycloakSession session, TreeStorageNodeInstance<V> original) {
|
||||||
|
super(original.getNodeProperties(), original.getEdgeProperties(), original.getTreeProperties());
|
||||||
|
this.original = original;
|
||||||
|
this.prescription = original.prescription;
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TreeStorageNodeInstance(KeycloakSession session, TreeStorageNodePrescription prescription) {
|
||||||
|
super(
|
||||||
|
prescription.getNodeProperties() == null ? null : new HashMap<>(prescription.getNodeProperties()),
|
||||||
|
prescription.getEdgeProperties() == null ? null : new HashMap<>(prescription.getEdgeProperties()),
|
||||||
|
prescription.getTreeProperties()
|
||||||
|
);
|
||||||
|
this.prescription = prescription;
|
||||||
|
this.session = session;
|
||||||
|
this.original = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TreeStorageNodeInstance<V> cloneNodeOnly() {
|
||||||
|
return new TreeStorageNodeInstance<>(session, this.original == null ? this : this.original);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return prescription.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReadOnly() {
|
||||||
|
return prescription.isReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
public FieldContainedStatus isCacheFor(EntityField<V> field, Object parameter) {
|
||||||
|
return prescription.isCacheFor(field, parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FieldContainedStatus isPrimarySourceFor(Enum<? extends EntityField<V>> field, Object parameter) {
|
||||||
|
return prescription.isPrimarySourceFor((EntityField<?>) field, parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,280 @@
|
||||||
|
/*
|
||||||
|
* 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.tree;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.map.common.AbstractEntity;
|
||||||
|
import org.keycloak.models.map.common.EntityField;
|
||||||
|
import org.keycloak.models.map.storage.ModelEntityUtil;
|
||||||
|
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prescription of the tree storage. This prescription can
|
||||||
|
* be externalized and contains e.g. details on the particular storage type
|
||||||
|
* represented by this node, or properties of the node and edge between this
|
||||||
|
* and the parent storage.
|
||||||
|
* <p>
|
||||||
|
* Realization of this prescription is in {@link TreeStorageNodeInstance}, namely it does not
|
||||||
|
* contain a map storage instance that can be directly used
|
||||||
|
* for accessing data.
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class TreeStorageNodePrescription extends DefaultTreeNode<TreeStorageNodePrescription> {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(TreeStorageNodePrescription.class);
|
||||||
|
private final boolean isPrimarySourceForAnything;
|
||||||
|
private final boolean isPrimarySourceForEverything;
|
||||||
|
private final boolean isCacheForAnything;
|
||||||
|
|
||||||
|
public static enum FieldContainedStatus {
|
||||||
|
/**
|
||||||
|
* Field is fully contained in the storage in this node.
|
||||||
|
* For example, attribute {@code foo} or field {@code NAME} stored in this node would be {@code FULLY} contained field.
|
||||||
|
* @see #PARTIALLY
|
||||||
|
*/
|
||||||
|
FULLY {
|
||||||
|
@Override
|
||||||
|
public FieldContainedStatus minus(FieldContainedStatus s) {
|
||||||
|
switch (s) {
|
||||||
|
case FULLY:
|
||||||
|
return FieldContainedStatus.NOT_CONTAINED;
|
||||||
|
case PARTIALLY:
|
||||||
|
return FieldContainedStatus.PARTIALLY;
|
||||||
|
default:
|
||||||
|
return FieldContainedStatus.FULLY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Field is contained in the storage in this node but parts of it might be contained in some child node as well.
|
||||||
|
* For example, a few attributes can be partially supplied from an LDAP, and the rest could be supplied from the
|
||||||
|
* supplementing JPA node.
|
||||||
|
* <p>
|
||||||
|
* This status is never used in the case of a fully specified field access but can be used for map-like attributes
|
||||||
|
* where the key is not specified.
|
||||||
|
*/
|
||||||
|
PARTIALLY {
|
||||||
|
@Override
|
||||||
|
public FieldContainedStatus minus(FieldContainedStatus s) {
|
||||||
|
switch (s) {
|
||||||
|
case FULLY:
|
||||||
|
return FieldContainedStatus.NOT_CONTAINED;
|
||||||
|
default:
|
||||||
|
return FieldContainedStatus.PARTIALLY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** Field is not contained in the storage in this node but parts of it might be contained in some child node as well */
|
||||||
|
NOT_CONTAINED {
|
||||||
|
@Override
|
||||||
|
public FieldContainedStatus minus(FieldContainedStatus s) {
|
||||||
|
return FieldContainedStatus.NOT_CONTAINED;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the status of the field if this field status in this node was stripped off the field status {@code s}.
|
||||||
|
* Specifically, for two nodes in parent/child relationship, {@code parent.minus(child)} answers the question:
|
||||||
|
* "How much of the field does parent contain that is not contained in the child?"
|
||||||
|
* <ul>
|
||||||
|
* <li>If the field in this node is contained {@link #FULLY} or {@link #PARTIALLY}, and the
|
||||||
|
* field in the other node is contained {@link #FULLY}, then
|
||||||
|
* there is no need to store any part of the field in this node,
|
||||||
|
* i.e. the result is {@link #NOT_CONTAINED}</li>
|
||||||
|
* <li>If the field in this node is contained {@link #PARTIALLY} and the field in the other node is also only contained {@link #PARTIALLY}, then
|
||||||
|
* the portions might be disjunct, so it is still necessary to store a part of the field in this node, i.e. the result is {@link #PARTIALLY}</li>
|
||||||
|
* <li>If the field in this node is {@link #NOT_CONTAINED} at all, then the result remains {@link #NOT_CONTAINED} regardless of the other node status</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public abstract FieldContainedStatus minus(FieldContainedStatus s);
|
||||||
|
/**
|
||||||
|
* Returns higher of this and the {@code other} field status: {@link #FULLY} > {@link #PARTIALLY} > {@link #NOT_CONTAINED}
|
||||||
|
*/
|
||||||
|
public FieldContainedStatus max(FieldContainedStatus other) {
|
||||||
|
return (other == null || ordinal() < other.ordinal()) ? this : other;
|
||||||
|
}
|
||||||
|
public FieldContainedStatus max(Supplier<FieldContainedStatus> otherFunc) {
|
||||||
|
if (ordinal() == 0) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
FieldContainedStatus other = otherFunc.get();
|
||||||
|
return other == null || ordinal() < other.ordinal() ? this : other;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of prescriptions restricted per entity class derived from this prescription.
|
||||||
|
*/
|
||||||
|
private final Map<Class<? extends AbstractEntity>, TreeStorageNodePrescription> restricted = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public TreeStorageNodePrescription(Map<String, Object> treeProperties) {
|
||||||
|
this(treeProperties, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TreeStorageNodePrescription(Map<String, Object> nodeProperties, Map<String, Object> edgeProperties, Map<String, Object> treeProperties) {
|
||||||
|
super(nodeProperties, edgeProperties, treeProperties);
|
||||||
|
Map<?, ?> psf = (Map<?, ?>) this.nodeProperties.get(NodeProperties.PRIMARY_SOURCE_FOR);
|
||||||
|
Map<?, ?> psfe = (Map<?, ?>) this.nodeProperties.get(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED);
|
||||||
|
isPrimarySourceForAnything = (psf != null && ! psf.isEmpty()) || (psfe != null && ! psfe.isEmpty());
|
||||||
|
isPrimarySourceForEverything = (psf == null) && (psfe == null || psfe.isEmpty()); // This could be restricted further
|
||||||
|
Map<?, ?> cf = (Map<?, ?>) this.nodeProperties.get(NodeProperties.CACHE_FOR);
|
||||||
|
Map<?, ?> cfe = (Map<?, ?>) this.nodeProperties.get(NodeProperties.CACHE_FOR_EXCLUDED);
|
||||||
|
isCacheForAnything = (cf != null && ! cf.isEmpty()) || (cfe != null && ! cfe.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
public <V extends AbstractEntity> TreeStorageNodePrescription forEntityClass(Class<V> targetEntityClass) {
|
||||||
|
return restricted.computeIfAbsent(targetEntityClass, c -> {
|
||||||
|
HashMap<String, Object> treeProperties = new HashMap<>(restrictConfigMap(targetEntityClass, getTreeProperties()));
|
||||||
|
treeProperties.put(TreeProperties.MODEL_CLASS, ModelEntityUtil.getModelType(targetEntityClass));
|
||||||
|
return cloneTree(n -> n.forEntityClass(targetEntityClass, treeProperties));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public <V extends AbstractEntity> TreeStorageNodeInstance<V> instantiate(KeycloakSession session) {
|
||||||
|
return cloneTree(n -> new TreeStorageNodeInstance<>(session, n));
|
||||||
|
}
|
||||||
|
|
||||||
|
private <V extends AbstractEntity> TreeStorageNodePrescription forEntityClass(Class<V> targetEntityClass,
|
||||||
|
Map<String, Object> treeProperties) {
|
||||||
|
Map<String, Object> nodeProperties = restrictConfigMap(targetEntityClass, getNodeProperties());
|
||||||
|
Map<String, Object> edgeProperties = restrictConfigMap(targetEntityClass, getEdgeProperties());
|
||||||
|
|
||||||
|
if (nodeProperties == null || edgeProperties == null) {
|
||||||
|
LOG.debugf("Cannot restrict storage for %s from node: %s", targetEntityClass, this);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TreeStorageNodePrescription(nodeProperties, edgeProperties, treeProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restricts configuration map to options that are either applicable everywhere (have no '[' in their name)
|
||||||
|
* or apply to a selected entity class (e.g. ends in "[clients]" for {@code MapClientEntity}).
|
||||||
|
* @return A new configuration map crafted for this particular entity class
|
||||||
|
*/
|
||||||
|
private static <V extends AbstractEntity> Map<String, Object> restrictConfigMap(Class<V> targetEntityClass, Map<String, Object> np) {
|
||||||
|
final Class<Object> modelType = ModelEntityUtil.getModelType(targetEntityClass, null);
|
||||||
|
String name = Optional.ofNullable(modelType)
|
||||||
|
.map(ModelEntityUtil::getModelName)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (name == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with all properties not specific for any domain
|
||||||
|
Map<String, Object> res = np.entrySet().stream()
|
||||||
|
.filter(me -> me.getKey().indexOf('[') == -1)
|
||||||
|
.filter(me -> me.getValue() != null)
|
||||||
|
.collect(Collectors.toMap(Entry::getKey, Entry::getValue, (s, a) -> s, HashMap::new));
|
||||||
|
|
||||||
|
// Now add and/or replace properties specific for the target domain
|
||||||
|
Pattern specificPropertyPattern = Pattern.compile("(.*?)\\[.*\\b" + Pattern.quote(name) + "\\b.*\\]");
|
||||||
|
np.keySet().stream()
|
||||||
|
.map(specificPropertyPattern::matcher)
|
||||||
|
.filter(Matcher::matches)
|
||||||
|
.forEach(m -> res.put(m.group(1), np.get(m.group(0))));
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReadOnly() {
|
||||||
|
return getNodeProperty(NodeProperties.READ_ONLY, Boolean.class).orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns if the given field is primary source for the field, potentially specified further by a parameter.
|
||||||
|
*
|
||||||
|
* @param field Field
|
||||||
|
* @param parameter First parameter, which in case of maps is the key to that map, e.g. attribute name.
|
||||||
|
* @return For a fully specified field and a parameter (e.g. "attribute" and "attr1"), or a parameterless field (e.g. "client_id"),
|
||||||
|
* returns either {@code FULLY} or {@code NOT_CONTAINED}. May also return {@code PARTIAL} for a field that requires
|
||||||
|
* a parameter but the parameter is not specified.
|
||||||
|
*/
|
||||||
|
public FieldContainedStatus isPrimarySourceFor(EntityField<?> field, Object parameter) {
|
||||||
|
if (isPrimarySourceForEverything) {
|
||||||
|
return FieldContainedStatus.FULLY;
|
||||||
|
}
|
||||||
|
if (! isPrimarySourceForAnything) {
|
||||||
|
return FieldContainedStatus.NOT_CONTAINED;
|
||||||
|
}
|
||||||
|
|
||||||
|
FieldContainedStatus isPrimarySourceFor = getNodeProperty(NodeProperties.PRIMARY_SOURCE_FOR, Map.class)
|
||||||
|
.map(m -> isFieldWithParameterIncludedInMap(m, field, parameter))
|
||||||
|
.orElse(FieldContainedStatus.FULLY);
|
||||||
|
|
||||||
|
FieldContainedStatus isExcludedPrimarySourceFor = getNodeProperty(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED, Map.class)
|
||||||
|
.map(m -> isFieldWithParameterIncludedInMap(m, field, parameter))
|
||||||
|
.orElse(FieldContainedStatus.NOT_CONTAINED);
|
||||||
|
|
||||||
|
return isPrimarySourceFor.minus(isExcludedPrimarySourceFor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FieldContainedStatus isCacheFor(EntityField<?> field, Object parameter) {
|
||||||
|
if (! isCacheForAnything) {
|
||||||
|
return FieldContainedStatus.NOT_CONTAINED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is CACHE_FOR_EXCLUDED then this node is a cache for all fields, and should be treated as such even if there is no CACHE_FOR
|
||||||
|
// This is analogous to PRIMARY_SOURCE_FOR(_EXCLUDED).
|
||||||
|
// However if there is neither CACHE_FOR_EXCLUDED nor CACHE_FOR, then this node is not a cache
|
||||||
|
final Optional<Map> cacheForExcluded = getNodeProperty(NodeProperties.CACHE_FOR_EXCLUDED, Map.class);
|
||||||
|
|
||||||
|
FieldContainedStatus isCacheFor = getNodeProperty(NodeProperties.CACHE_FOR, Map.class)
|
||||||
|
.map(m -> isFieldWithParameterIncludedInMap(m, field, parameter))
|
||||||
|
.orElse(cacheForExcluded.isPresent() ? FieldContainedStatus.FULLY : FieldContainedStatus.NOT_CONTAINED);
|
||||||
|
|
||||||
|
FieldContainedStatus isExcludedCacheFor = cacheForExcluded
|
||||||
|
.map(m -> isFieldWithParameterIncludedInMap(m, field, parameter))
|
||||||
|
.orElse(FieldContainedStatus.NOT_CONTAINED);
|
||||||
|
|
||||||
|
return isCacheFor.minus(isExcludedCacheFor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FieldContainedStatus isFieldWithParameterIncludedInMap(Map<?, ?> field2possibleParameters, EntityField<?> field, Object parameter) {
|
||||||
|
Collection<?> specificCases = (Collection<?>) field2possibleParameters.get(field);
|
||||||
|
if (specificCases == null) {
|
||||||
|
return field2possibleParameters.containsKey(field)
|
||||||
|
? FieldContainedStatus.FULLY
|
||||||
|
: FieldContainedStatus.NOT_CONTAINED;
|
||||||
|
} else {
|
||||||
|
return parameter == null
|
||||||
|
? FieldContainedStatus.PARTIALLY
|
||||||
|
: specificCases.contains(parameter)
|
||||||
|
? FieldContainedStatus.FULLY
|
||||||
|
: FieldContainedStatus.NOT_CONTAINED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getLabel() {
|
||||||
|
return getId() + getNodeProperty(NodeProperties.STORAGE_PROVIDER, String.class).map(s -> " [" + s + "]").orElse("");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,325 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.models.map.common.delegate;
|
||||||
|
|
||||||
|
import org.keycloak.models.map.client.MapClientEntity;
|
||||||
|
import org.keycloak.models.map.client.MapClientEntityFields;
|
||||||
|
import org.keycloak.models.map.client.MapClientEntityImpl;
|
||||||
|
import org.keycloak.models.map.common.DeepCloner;
|
||||||
|
import org.keycloak.models.map.storage.tree.NodeProperties;
|
||||||
|
import org.keycloak.models.map.storage.tree.TreeStorageNodeInstance;
|
||||||
|
import org.keycloak.models.map.storage.tree.TreeStorageNodePrescription;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import org.junit.Before;
|
||||||
|
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.hasEntry;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class PerFieldDelegateProviderCacheTest {
|
||||||
|
|
||||||
|
private MapClientEntity upperEnt;
|
||||||
|
private MapClientEntity lowerEnt;
|
||||||
|
|
||||||
|
private HashMap<String, Object> upperNodeProperties;
|
||||||
|
private EnumMap<MapClientEntityFields, Object> upperCacheFor;
|
||||||
|
private EnumMap<MapClientEntityFields, Object> upperCacheForExcluded;
|
||||||
|
|
||||||
|
private HashMap<String, Object> lowerNodeProperties;
|
||||||
|
private EnumMap<MapClientEntityFields, Object> lowerCacheFor;
|
||||||
|
private EnumMap<MapClientEntityFields, Object> lowerCacheForExcluded;
|
||||||
|
|
||||||
|
private TreeStorageNodeInstance<MapClientEntity> upperTsni;
|
||||||
|
private TreeStorageNodeInstance<MapClientEntity> lowerTsni;
|
||||||
|
|
||||||
|
AtomicInteger lowerEntSupplierCallCount = new AtomicInteger();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void initEntities() {
|
||||||
|
upperEnt = new MapClientEntityImpl();
|
||||||
|
lowerEnt = new MapClientEntityImpl();
|
||||||
|
|
||||||
|
upperEnt.setProtocol("upper-protocol");
|
||||||
|
upperEnt.addRedirectUri("upper-redirectUri-1");
|
||||||
|
upperEnt.addRedirectUri("upper-redirectUri-2");
|
||||||
|
upperEnt.setClientId("upper-clientId-1");
|
||||||
|
upperEnt.setAttribute("attr1", Arrays.asList("upper-value-1"));
|
||||||
|
upperEnt.setAttribute("attr2", Arrays.asList("upper-value-2"));
|
||||||
|
upperEnt.setAttribute("attr3", Arrays.asList("upper-value-3"));
|
||||||
|
|
||||||
|
lowerEnt.setProtocol("lower-protocol");
|
||||||
|
lowerEnt.addRedirectUri("lower-redirectUri-1");
|
||||||
|
lowerEnt.addRedirectUri("lower-redirectUri-2");
|
||||||
|
lowerEnt.setClientId("lower-clientId-1");
|
||||||
|
lowerEnt.setAttribute("attr1", Arrays.asList("lower-value-1"));
|
||||||
|
lowerEnt.setAttribute("attr3", Arrays.asList("lower-value-3"));
|
||||||
|
lowerEnt.setAttribute("attr4", Arrays.asList("lower-value-4"));
|
||||||
|
|
||||||
|
upperNodeProperties = new HashMap<>();
|
||||||
|
upperCacheFor = new EnumMap<>(MapClientEntityFields.class);
|
||||||
|
upperCacheForExcluded = new EnumMap<>(MapClientEntityFields.class);
|
||||||
|
|
||||||
|
lowerNodeProperties = new HashMap<>();
|
||||||
|
lowerCacheFor = new EnumMap<>(MapClientEntityFields.class);
|
||||||
|
lowerCacheForExcluded = new EnumMap<>(MapClientEntityFields.class);
|
||||||
|
|
||||||
|
lowerEntSupplierCallCount.set(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MapClientEntity prepareEntityAndTreeNodeInstances() {
|
||||||
|
TreeStorageNodePrescription upperTsnp = new TreeStorageNodePrescription(upperNodeProperties, null, null);
|
||||||
|
TreeStorageNodePrescription lowerTsnp = new TreeStorageNodePrescription(lowerNodeProperties, null, null);
|
||||||
|
|
||||||
|
upperTsni = new TreeStorageNodeInstance<>(null, upperTsnp);
|
||||||
|
lowerTsni = new TreeStorageNodeInstance<>(null, lowerTsnp);
|
||||||
|
|
||||||
|
PerFieldDelegateProvider<MapClientEntity> fieldProvider = new PerFieldDelegateProvider<>(upperTsni.new WithEntity(upperEnt), () -> {
|
||||||
|
lowerEntSupplierCallCount.incrementAndGet();
|
||||||
|
return lowerEnt;
|
||||||
|
});
|
||||||
|
return DeepCloner.DUMB_CLONER.entityFieldDelegate(MapClientEntity.class, fieldProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGet_CacheFor() {
|
||||||
|
//
|
||||||
|
// High-level perspective: cache for listed fields, the primary source for values is in the lower entity.
|
||||||
|
//
|
||||||
|
// Upper node is not a primary source for any fields, and caches only the enumerated fields
|
||||||
|
// Lower node is a primary source for all fields
|
||||||
|
// There is an (intentional) discrepancy between the field values in lowerEnt and upperEnt to be able to distinguish
|
||||||
|
// which entity the return value was obtained from.
|
||||||
|
upperCacheFor.put(MapClientEntityFields.CLIENT_ID, null);
|
||||||
|
upperCacheFor.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr2", "attr3", "attr4"));
|
||||||
|
upperNodeProperties.put(NodeProperties.CACHE_FOR, upperCacheFor);
|
||||||
|
upperNodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR, Collections.emptyMap()); // set the upper object to be exclusively a cache - none of the fields is primary source
|
||||||
|
|
||||||
|
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
|
||||||
|
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(0));
|
||||||
|
|
||||||
|
assertThat(ent.getClientId(), is("upper-clientId-1"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr2"), contains("upper-value-2"));
|
||||||
|
assertThat(ent.getAttribute("attr3"), contains("upper-value-3"));
|
||||||
|
assertThat(ent.getAttribute("attr4"), nullValue());
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(0));
|
||||||
|
|
||||||
|
assertThat(ent.getProtocol(), is("lower-protocol"));
|
||||||
|
assertThat(ent.getAttribute("attr1"), contains("lower-value-1"));
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(1));
|
||||||
|
|
||||||
|
assertThat(ent.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3"));
|
||||||
|
assertThat(ent.getAttributes(), hasEntry("attr1", Arrays.asList("lower-value-1")));
|
||||||
|
assertThat(ent.getAttributes(), hasEntry("attr2", Arrays.asList("upper-value-2")));
|
||||||
|
assertThat(ent.getAttributes(), hasEntry("attr3", Arrays.asList("upper-value-3")));
|
||||||
|
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSet_CacheFor() {
|
||||||
|
//
|
||||||
|
// High-level perspective: fields available in the lower layer (e.g. LDAP) cached in a faster layer (e.g. database)
|
||||||
|
//
|
||||||
|
// Upper node is a primary source for all fields apart from cached ones. It only caches only the enumerated fields
|
||||||
|
// Lower node is a primary source for all fields
|
||||||
|
// There is an (intentional) discrepancy between the field values in lowerEnt and upperEnt to be able to distinguish
|
||||||
|
// which entity the return value was obtained from.
|
||||||
|
upperCacheFor.put(MapClientEntityFields.CLIENT_ID, null);
|
||||||
|
upperCacheFor.put(MapClientEntityFields.PROTOCOL, null);
|
||||||
|
upperCacheFor.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr2", "attr3", "attr4"));
|
||||||
|
upperNodeProperties.put(NodeProperties.CACHE_FOR, upperCacheFor);
|
||||||
|
upperNodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED, upperCacheFor);
|
||||||
|
|
||||||
|
// When there is primary source in the node properties, named properties are considered as owned by this entity, and
|
||||||
|
// all other are considered as owned by child entity.
|
||||||
|
// Thus there is no call to the child entity creator.
|
||||||
|
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
|
||||||
|
|
||||||
|
// When
|
||||||
|
ent.setProtocol("modified-protocol"); // modification in: upper and lower
|
||||||
|
ent.setAttribute("attr1", Arrays.asList("modified-value-1")); // modification in: upper
|
||||||
|
ent.setAttribute("attrX", Arrays.asList("modified-value-X")); // modification in: upper
|
||||||
|
ent.addRedirectUri("added-redirectUri"); // modification in: upper
|
||||||
|
ent.removeRedirectUri("upper-redirectUri-2"); // modification in: upper
|
||||||
|
ent.removeRedirectUri("lower-redirectUri-2"); // modification in: upper
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(1));
|
||||||
|
|
||||||
|
assertThat(ent.getClientId(), is("upper-clientId-1"));
|
||||||
|
assertThat(upperEnt.getClientId(), is("upper-clientId-1"));
|
||||||
|
assertThat(lowerEnt.getClientId(), is("lower-clientId-1"));
|
||||||
|
|
||||||
|
assertThat(ent.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "added-redirectUri"));
|
||||||
|
assertThat(upperEnt.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "added-redirectUri"));
|
||||||
|
assertThat(lowerEnt.getRedirectUris(), containsInAnyOrder("lower-redirectUri-1", "lower-redirectUri-2"));
|
||||||
|
|
||||||
|
assertThat(ent.getProtocol(), is("modified-protocol"));
|
||||||
|
assertThat(upperEnt.getProtocol(), is("modified-protocol"));
|
||||||
|
assertThat(lowerEnt.getProtocol(), is("modified-protocol"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr1"), contains("modified-value-1"));
|
||||||
|
assertThat(upperEnt.getAttribute("attr1"), contains("modified-value-1"));
|
||||||
|
assertThat(lowerEnt.getAttribute("attr1"), contains("lower-value-1"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr3"), contains("upper-value-3"));
|
||||||
|
assertThat(upperEnt.getAttribute("attr3"), contains("upper-value-3"));
|
||||||
|
assertThat(lowerEnt.getAttribute("attr3"), contains("lower-value-3"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attrX"), contains("modified-value-X"));
|
||||||
|
assertThat(upperEnt.getAttribute("attrX"), contains("modified-value-X"));
|
||||||
|
assertThat(lowerEnt.getAttribute("attrX"), nullValue());
|
||||||
|
|
||||||
|
assertThat(ent.getAttributes().keySet(), containsInAnyOrder(
|
||||||
|
"attr1", // From upper
|
||||||
|
"attr2", // From upper, since it is a "cached" value (deliberately a stale cache here, it is not in lower)
|
||||||
|
"attr3", // From upper
|
||||||
|
// "attr4", // From upper where it has no value (deliberately a stale cache here, it has a value in lower)
|
||||||
|
"attrX" // From upper
|
||||||
|
));
|
||||||
|
assertThat(upperEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3", "attrX"));
|
||||||
|
assertThat(lowerEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr3", "attr4"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGet_CacheForExcluded() {
|
||||||
|
//
|
||||||
|
// High-level perspective: listed fields exclusively available in the lower layer (e.g. LDAP),
|
||||||
|
// never stored nor cached in the top layer (e.g. database).
|
||||||
|
// All other fields are stored in the top layer.
|
||||||
|
//
|
||||||
|
// Upper node is a primary source for all fields apart from cached ones. It only caches only the enumerated fields
|
||||||
|
// Lower node is a primary source for all fields
|
||||||
|
// There is an (intentional) discrepancy between the field values in lowerEnt and upperEnt to be able to distinguish
|
||||||
|
// which entity the return value was obtained from.
|
||||||
|
upperCacheForExcluded.put(MapClientEntityFields.CLIENT_ID, null);
|
||||||
|
upperCacheForExcluded.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr2", "attr3", "attr4"));
|
||||||
|
upperNodeProperties.put(NodeProperties.CACHE_FOR_EXCLUDED, upperCacheForExcluded);
|
||||||
|
upperNodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED, upperCacheForExcluded);
|
||||||
|
|
||||||
|
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
|
||||||
|
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(0));
|
||||||
|
assertThat(ent.getProtocol(), is("upper-protocol"));
|
||||||
|
assertThat(ent.getAttribute("attr1"), contains("upper-value-1"));
|
||||||
|
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(0));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr2"), nullValue());
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(1));
|
||||||
|
assertThat(ent.getAttribute("attr3"), contains("lower-value-3"));
|
||||||
|
assertThat(ent.getAttribute("attr4"), contains("lower-value-4"));
|
||||||
|
|
||||||
|
assertThat(ent.getClientId(), is("lower-clientId-1"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttributes().keySet(), containsInAnyOrder("attr1", "attr3", "attr4"));
|
||||||
|
assertThat(ent.getAttributes(), hasEntry("attr1", Arrays.asList("upper-value-1")));
|
||||||
|
assertThat(ent.getAttributes(), hasEntry("attr3", Arrays.asList("lower-value-3")));
|
||||||
|
assertThat(ent.getAttributes(), hasEntry("attr4", Arrays.asList("lower-value-4")));
|
||||||
|
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSet_CacheForExcluded() {
|
||||||
|
//
|
||||||
|
// High-level perspective: listed fields exclusively available in the lower layer (e.g. LDAP),
|
||||||
|
// never stored nor cached in the top layer (e.g. database).
|
||||||
|
// All other fields are stored in the top layer.
|
||||||
|
//
|
||||||
|
// When there is are primary source exclusion in the node properties, all properties apart from those enumerated
|
||||||
|
// are considered as owned by this entity. Those enumerated are obtained from the child entity.
|
||||||
|
// Thus there must be a single call to the child entity creator.
|
||||||
|
upperCacheForExcluded.put(MapClientEntityFields.CLIENT_ID, null);
|
||||||
|
upperCacheForExcluded.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr2", "attr3", "attr4"));
|
||||||
|
upperNodeProperties.put(NodeProperties.CACHE_FOR_EXCLUDED, upperCacheForExcluded);
|
||||||
|
upperNodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED, upperCacheForExcluded);
|
||||||
|
|
||||||
|
// When there is primary source exclusion in the node properties, listed properties are considered as owned by the child
|
||||||
|
// entity, and all other are considered as owned by this entity.
|
||||||
|
// Thus there is no call to the child entity creator.
|
||||||
|
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
|
||||||
|
|
||||||
|
// When
|
||||||
|
ent.setClientId("modified-client-id-1"); // modification in: lower
|
||||||
|
ent.setProtocol("modified-protocol"); // modification in: upper
|
||||||
|
ent.setAttribute("attr4", Arrays.asList("modified-value-4")); // modification in: lower
|
||||||
|
ent.setAttribute("attrX", Arrays.asList("modified-value-X")); // modification in: upper
|
||||||
|
ent.addRedirectUri("added-redirectUri"); // modification in: upper
|
||||||
|
ent.removeRedirectUri("upper-redirectUri-2"); // modification in: upper
|
||||||
|
ent.removeRedirectUri("lower-redirectUri-2"); // modification in: upper
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(1));
|
||||||
|
|
||||||
|
assertThat(ent.getClientId(), is("modified-client-id-1"));
|
||||||
|
assertThat(upperEnt.getClientId(), is("upper-clientId-1"));
|
||||||
|
assertThat(lowerEnt.getClientId(), is("modified-client-id-1"));
|
||||||
|
|
||||||
|
assertThat(ent.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "added-redirectUri"));
|
||||||
|
assertThat(upperEnt.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "added-redirectUri"));
|
||||||
|
assertThat(lowerEnt.getRedirectUris(), containsInAnyOrder("lower-redirectUri-1", "lower-redirectUri-2"));
|
||||||
|
|
||||||
|
assertThat(ent.getProtocol(), is("modified-protocol"));
|
||||||
|
assertThat(upperEnt.getProtocol(), is("modified-protocol"));
|
||||||
|
assertThat(lowerEnt.getProtocol(), is("lower-protocol"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr1"), contains("upper-value-1"));
|
||||||
|
assertThat(upperEnt.getAttribute("attr1"), contains("upper-value-1"));
|
||||||
|
assertThat(lowerEnt.getAttribute("attr1"), contains("lower-value-1"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr2"), nullValue());
|
||||||
|
assertThat(upperEnt.getAttribute("attr2"), contains("upper-value-2"));
|
||||||
|
assertThat(lowerEnt.getAttribute("attr2"), nullValue());
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr3"), contains("lower-value-3"));
|
||||||
|
assertThat(upperEnt.getAttribute("attr3"), contains("upper-value-3"));
|
||||||
|
assertThat(lowerEnt.getAttribute("attr3"), contains("lower-value-3"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr4"), contains("modified-value-4"));
|
||||||
|
assertThat(upperEnt.getAttribute("attr4"), nullValue());
|
||||||
|
assertThat(lowerEnt.getAttribute("attr4"), contains("modified-value-4"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attrX"), contains("modified-value-X"));
|
||||||
|
assertThat(upperEnt.getAttribute("attrX"), contains("modified-value-X"));
|
||||||
|
assertThat(lowerEnt.getAttribute("attrX"), nullValue());
|
||||||
|
|
||||||
|
assertThat(ent.getAttributes().keySet(), containsInAnyOrder(
|
||||||
|
"attr1", // From upper
|
||||||
|
// "attr2", // From lower where it has no value
|
||||||
|
"attr3", // From lower
|
||||||
|
"attr4", // From lower
|
||||||
|
"attrX" // From upper
|
||||||
|
));
|
||||||
|
assertThat(upperEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3", "attrX"));
|
||||||
|
assertThat(lowerEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr3", "attr4"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,388 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.models.map.common.delegate;
|
||||||
|
|
||||||
|
import org.keycloak.models.map.client.MapClientEntity;
|
||||||
|
import org.keycloak.models.map.client.MapClientEntityFields;
|
||||||
|
import org.keycloak.models.map.client.MapClientEntityImpl;
|
||||||
|
import org.keycloak.models.map.common.DeepCloner;
|
||||||
|
import org.keycloak.models.map.storage.tree.NodeProperties;
|
||||||
|
import org.keycloak.models.map.storage.tree.TreeStorageNodeInstance;
|
||||||
|
import org.keycloak.models.map.storage.tree.TreeStorageNodePrescription;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import org.junit.Before;
|
||||||
|
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.hasEntry;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class PerFieldDelegateProviderPrimarySourceTest {
|
||||||
|
|
||||||
|
private MapClientEntity upperEnt;
|
||||||
|
private MapClientEntity lowerEnt;
|
||||||
|
|
||||||
|
private HashMap<String, Object> upperNodeProperties;
|
||||||
|
private EnumMap<MapClientEntityFields, Object> upperPrimarySourceFor;
|
||||||
|
private EnumMap<MapClientEntityFields, Object> upperPrimarySourceForExcluded;
|
||||||
|
|
||||||
|
private HashMap<String, Object> lowerNodeProperties;
|
||||||
|
private EnumMap<MapClientEntityFields, Object> lowerPrimarySourceFor;
|
||||||
|
private EnumMap<MapClientEntityFields, Object> lowerPrimarySourceForExcluded;
|
||||||
|
|
||||||
|
private TreeStorageNodeInstance<MapClientEntity> upperTsni;
|
||||||
|
private TreeStorageNodeInstance<MapClientEntity> lowerTsni;
|
||||||
|
|
||||||
|
AtomicInteger lowerEntSupplierCallCount = new AtomicInteger();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void initEntities() {
|
||||||
|
upperEnt = new MapClientEntityImpl();
|
||||||
|
lowerEnt = new MapClientEntityImpl();
|
||||||
|
|
||||||
|
upperEnt.setProtocol("upper-protocol");
|
||||||
|
upperEnt.addRedirectUri("upper-redirectUri-1");
|
||||||
|
upperEnt.addRedirectUri("upper-redirectUri-2");
|
||||||
|
upperEnt.setClientId("upper-clientId-1");
|
||||||
|
upperEnt.setAttribute("attr1", Arrays.asList("upper-value-1"));
|
||||||
|
upperEnt.setAttribute("attr2", Arrays.asList("upper-value-2"));
|
||||||
|
upperEnt.setAttribute("attr3", Arrays.asList("upper-value-3"));
|
||||||
|
|
||||||
|
lowerEnt.setProtocol("lower-protocol");
|
||||||
|
lowerEnt.addRedirectUri("lower-redirectUri-1");
|
||||||
|
lowerEnt.addRedirectUri("lower-redirectUri-2");
|
||||||
|
lowerEnt.setClientId("lower-clientId-1");
|
||||||
|
lowerEnt.setAttribute("attr1", Arrays.asList("lower-value-1"));
|
||||||
|
lowerEnt.setAttribute("attr3", Arrays.asList("lower-value-3"));
|
||||||
|
lowerEnt.setAttribute("attr4", Arrays.asList("lower-value-4"));
|
||||||
|
|
||||||
|
upperNodeProperties = new HashMap<>();
|
||||||
|
upperPrimarySourceFor = new EnumMap<>(MapClientEntityFields.class);
|
||||||
|
upperPrimarySourceForExcluded = new EnumMap<>(MapClientEntityFields.class);
|
||||||
|
|
||||||
|
lowerNodeProperties = new HashMap<>();
|
||||||
|
lowerPrimarySourceFor = new EnumMap<>(MapClientEntityFields.class);
|
||||||
|
lowerPrimarySourceForExcluded = new EnumMap<>(MapClientEntityFields.class);
|
||||||
|
|
||||||
|
lowerEntSupplierCallCount.set(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MapClientEntity prepareEntityAndTreeNodeInstances() {
|
||||||
|
TreeStorageNodePrescription upperTsnp = new TreeStorageNodePrescription(upperNodeProperties, null, null);
|
||||||
|
TreeStorageNodePrescription lowerTsnp = new TreeStorageNodePrescription(lowerNodeProperties, null, null);
|
||||||
|
|
||||||
|
upperTsni = new TreeStorageNodeInstance<>(null, upperTsnp);
|
||||||
|
lowerTsni = new TreeStorageNodeInstance<>(null, lowerTsnp);
|
||||||
|
|
||||||
|
PerFieldDelegateProvider<MapClientEntity> fieldProvider = new PerFieldDelegateProvider<>(upperTsni.new WithEntity(upperEnt), () -> {
|
||||||
|
lowerEntSupplierCallCount.incrementAndGet();
|
||||||
|
return lowerEnt;
|
||||||
|
});
|
||||||
|
return DeepCloner.DUMB_CLONER.entityFieldDelegate(MapClientEntity.class, fieldProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGet_NoPrimarySource() {
|
||||||
|
// When there is no primary source (exclusion) in the node properties, all properties are considered as owned by this entity.
|
||||||
|
// Thus there is no call to the child entity creator.
|
||||||
|
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
|
||||||
|
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(0));
|
||||||
|
assertThat(ent.getProtocol(), is("upper-protocol"));
|
||||||
|
assertThat(ent.getAttribute("attr1"), contains("upper-value-1"));
|
||||||
|
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(0));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr2"), contains("upper-value-2"));
|
||||||
|
assertThat(ent.getAttribute("attr3"), contains("upper-value-3"));
|
||||||
|
assertThat(ent.getAttribute("attr4"), nullValue());
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(0));
|
||||||
|
|
||||||
|
assertThat(ent.getClientId(), is("upper-clientId-1"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3"));
|
||||||
|
assertThat(ent.getAttributes(), hasEntry("attr1", Arrays.asList("upper-value-1")));
|
||||||
|
assertThat(ent.getAttributes(), hasEntry("attr2", Arrays.asList("upper-value-2")));
|
||||||
|
assertThat(ent.getAttributes(), hasEntry("attr3", Arrays.asList("upper-value-3")));
|
||||||
|
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSet_NoPrimarySource() {
|
||||||
|
// When there is no primary source (exclusion) in the node properties, all properties are considered as owned by this entity.
|
||||||
|
// Thus there is no call to the child entity creator.
|
||||||
|
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
|
||||||
|
|
||||||
|
// When
|
||||||
|
ent.setProtocol("modified-protocol"); // modification in: upper
|
||||||
|
ent.setAttribute("attr1", Arrays.asList("modified-value-1")); // modification in: upper
|
||||||
|
ent.setAttribute("attrX", Arrays.asList("modified-value-X")); // modification in: upper
|
||||||
|
ent.addRedirectUri("added-redirectUri"); // modification in: upper
|
||||||
|
ent.removeRedirectUri("upper-redirectUri-2"); // modification in: upper
|
||||||
|
ent.removeRedirectUri("lower-redirectUri-2"); // modification in: upper
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(0));
|
||||||
|
|
||||||
|
assertThat(ent.getClientId(), is("upper-clientId-1"));
|
||||||
|
assertThat(upperEnt.getClientId(), is("upper-clientId-1"));
|
||||||
|
assertThat(lowerEnt.getClientId(), is("lower-clientId-1"));
|
||||||
|
|
||||||
|
assertThat(ent.getProtocol(), is("modified-protocol"));
|
||||||
|
assertThat(upperEnt.getProtocol(), is("modified-protocol"));
|
||||||
|
assertThat(lowerEnt.getProtocol(), is("lower-protocol"));
|
||||||
|
|
||||||
|
assertThat(ent.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "added-redirectUri"));
|
||||||
|
assertThat(upperEnt.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "added-redirectUri"));
|
||||||
|
assertThat(lowerEnt.getRedirectUris(), containsInAnyOrder("lower-redirectUri-1", "lower-redirectUri-2"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr1"), contains("modified-value-1"));
|
||||||
|
assertThat(upperEnt.getAttribute("attr1"), contains("modified-value-1"));
|
||||||
|
assertThat(lowerEnt.getAttribute("attr1"), contains("lower-value-1"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr3"), contains("upper-value-3"));
|
||||||
|
assertThat(upperEnt.getAttribute("attr3"), contains("upper-value-3"));
|
||||||
|
assertThat(lowerEnt.getAttribute("attr3"), contains("lower-value-3"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attrX"), contains("modified-value-X"));
|
||||||
|
assertThat(upperEnt.getAttribute("attrX"), contains("modified-value-X"));
|
||||||
|
assertThat(lowerEnt.getAttribute("attrX"), nullValue());
|
||||||
|
|
||||||
|
assertThat(ent.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3", "attrX"));
|
||||||
|
assertThat(upperEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3", "attrX"));
|
||||||
|
assertThat(lowerEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr3", "attr4"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGet_PrimarySourceFor() {
|
||||||
|
//
|
||||||
|
// High-level perspective: only listed fields are available in upper entity, the rest is in lower one
|
||||||
|
//
|
||||||
|
// When there is are primary source in the node properties, only properties from those listed
|
||||||
|
// are considered as owned by this entity. Those not listed are obtained from the child entity.
|
||||||
|
// Thus there must be a single call to the child entity creator.
|
||||||
|
upperPrimarySourceFor.put(MapClientEntityFields.CLIENT_ID, null);
|
||||||
|
upperPrimarySourceFor.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr2", "attr3", "attr4"));
|
||||||
|
upperNodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR, upperPrimarySourceFor);
|
||||||
|
|
||||||
|
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
|
||||||
|
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(0));
|
||||||
|
|
||||||
|
assertThat(ent.getClientId(), is("upper-clientId-1"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr2"), contains("upper-value-2"));
|
||||||
|
assertThat(ent.getAttribute("attr3"), contains("upper-value-3"));
|
||||||
|
assertThat(ent.getAttribute("attr4"), nullValue());
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(0));
|
||||||
|
|
||||||
|
assertThat(ent.getProtocol(), is("lower-protocol"));
|
||||||
|
assertThat(ent.getAttribute("attr1"), contains("lower-value-1"));
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(1));
|
||||||
|
|
||||||
|
assertThat(ent.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3"));
|
||||||
|
assertThat(ent.getAttributes(), hasEntry("attr1", Arrays.asList("lower-value-1")));
|
||||||
|
assertThat(ent.getAttributes(), hasEntry("attr2", Arrays.asList("upper-value-2")));
|
||||||
|
assertThat(ent.getAttributes(), hasEntry("attr3", Arrays.asList("upper-value-3")));
|
||||||
|
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSet_PrimarySourceFor() {
|
||||||
|
//
|
||||||
|
// High-level perspective: only listed fields are available in upper entity, the rest is in lower one
|
||||||
|
//
|
||||||
|
// When there is are primary source in the node properties, only properties from those enumerated
|
||||||
|
// are considered as owned by this entity. Those not listed are obtained from the child entity.
|
||||||
|
// Thus there must be a single call to the child entity creator.
|
||||||
|
upperPrimarySourceFor.put(MapClientEntityFields.CLIENT_ID, null);
|
||||||
|
upperPrimarySourceFor.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr2", "attr3", "attr4"));
|
||||||
|
upperNodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR, upperPrimarySourceFor);
|
||||||
|
|
||||||
|
// When there is primary source in the node properties, named properties are considered as owned by this entity, and
|
||||||
|
// all other are considered as owned by child entity.
|
||||||
|
// Thus there is no call to the child entity creator.
|
||||||
|
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
|
||||||
|
|
||||||
|
// When
|
||||||
|
ent.setClientId("modified-client-id"); // modification in: upper
|
||||||
|
ent.setProtocol("modified-protocol"); // modification in: lower
|
||||||
|
ent.setAttribute("attr1", Arrays.asList("modified-value-1")); // modification in: lower
|
||||||
|
ent.setAttribute("attrX", Arrays.asList("modified-value-X")); // modification in: lower
|
||||||
|
ent.addRedirectUri("added-redirectUri"); // modification in: lower
|
||||||
|
ent.removeRedirectUri("upper-redirectUri-2"); // modification in: lower
|
||||||
|
ent.removeRedirectUri("lower-redirectUri-2"); // modification in: lower
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(1));
|
||||||
|
|
||||||
|
assertThat(ent.getClientId(), is("modified-client-id"));
|
||||||
|
assertThat(upperEnt.getClientId(), is("modified-client-id"));
|
||||||
|
assertThat(lowerEnt.getClientId(), is("lower-clientId-1"));
|
||||||
|
|
||||||
|
assertThat(ent.getRedirectUris(), containsInAnyOrder("lower-redirectUri-1", "added-redirectUri"));
|
||||||
|
assertThat(upperEnt.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "upper-redirectUri-2"));
|
||||||
|
assertThat(lowerEnt.getRedirectUris(), containsInAnyOrder("lower-redirectUri-1", "added-redirectUri"));
|
||||||
|
|
||||||
|
assertThat(ent.getProtocol(), is("modified-protocol"));
|
||||||
|
assertThat(upperEnt.getProtocol(), is("upper-protocol"));
|
||||||
|
assertThat(lowerEnt.getProtocol(), is("modified-protocol"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr1"), contains("modified-value-1"));
|
||||||
|
assertThat(upperEnt.getAttribute("attr1"), contains("upper-value-1"));
|
||||||
|
assertThat(lowerEnt.getAttribute("attr1"), contains("modified-value-1"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr3"), contains("upper-value-3"));
|
||||||
|
assertThat(upperEnt.getAttribute("attr3"), contains("upper-value-3"));
|
||||||
|
assertThat(lowerEnt.getAttribute("attr3"), contains("lower-value-3"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attrX"), contains("modified-value-X"));
|
||||||
|
assertThat(upperEnt.getAttribute("attrX"), nullValue());
|
||||||
|
assertThat(lowerEnt.getAttribute("attrX"), contains("modified-value-X"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttributes().keySet(), containsInAnyOrder(
|
||||||
|
"attr1", // From lower
|
||||||
|
"attr2", // From upper
|
||||||
|
"attr3", // From upper
|
||||||
|
// "attr4", // From upper where it has no value
|
||||||
|
"attrX" // From lower
|
||||||
|
));
|
||||||
|
assertThat(upperEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3"));
|
||||||
|
assertThat(lowerEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr3", "attr4", "attrX"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGet_PrimarySourceForExcluded() {
|
||||||
|
//
|
||||||
|
// High-level perspective: all but the listed fields are available in upper entity (e.g. JPA),
|
||||||
|
// the listed ones are stored in the lower entity (e.g. LDAP)
|
||||||
|
//
|
||||||
|
// When there is are primary source exclusion in the node properties, all properties apart from those enumerated
|
||||||
|
// are considered as owned by this entity. Those enumerated are obtained from the child entity.
|
||||||
|
// Thus there must be a single call to the child entity creator.
|
||||||
|
upperPrimarySourceForExcluded.put(MapClientEntityFields.CLIENT_ID, null);
|
||||||
|
upperPrimarySourceForExcluded.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr2", "attr3", "attr4"));
|
||||||
|
upperNodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED, upperPrimarySourceForExcluded);
|
||||||
|
|
||||||
|
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
|
||||||
|
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(0));
|
||||||
|
assertThat(ent.getProtocol(), is("upper-protocol"));
|
||||||
|
assertThat(ent.getAttribute("attr1"), contains("upper-value-1"));
|
||||||
|
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(0));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr2"), nullValue());
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(1));
|
||||||
|
assertThat(ent.getAttribute("attr3"), contains("lower-value-3"));
|
||||||
|
assertThat(ent.getAttribute("attr4"), contains("lower-value-4"));
|
||||||
|
|
||||||
|
assertThat(ent.getClientId(), is("lower-clientId-1"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttributes().keySet(), containsInAnyOrder("attr1", "attr3", "attr4"));
|
||||||
|
assertThat(ent.getAttributes(), hasEntry("attr1", Arrays.asList("upper-value-1")));
|
||||||
|
assertThat(ent.getAttributes(), hasEntry("attr3", Arrays.asList("lower-value-3")));
|
||||||
|
assertThat(ent.getAttributes(), hasEntry("attr4", Arrays.asList("lower-value-4")));
|
||||||
|
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSet_PrimarySourceForExcluded() {
|
||||||
|
//
|
||||||
|
// High-level perspective: all but the listed fields are available in upper entity (e.g. JPA),
|
||||||
|
// the listed ones are stored in the lower entity (e.g. LDAP)
|
||||||
|
//
|
||||||
|
// When there is are primary source exclusion in the node properties, all properties apart from those enumerated
|
||||||
|
// are considered as owned by this entity. Those enumerated are obtained from the child entity.
|
||||||
|
// Thus there must be a single call to the child entity creator.
|
||||||
|
upperPrimarySourceForExcluded.put(MapClientEntityFields.CLIENT_ID, null);
|
||||||
|
upperPrimarySourceForExcluded.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr2", "attr3", "attr4"));
|
||||||
|
upperNodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED, upperPrimarySourceForExcluded);
|
||||||
|
|
||||||
|
// When there is primary source exclusion in the node properties, listed properties are considered as owned by the child
|
||||||
|
// entity, and all other are considered as owned by this entity.
|
||||||
|
// Thus there is no call to the child entity creator.
|
||||||
|
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
|
||||||
|
|
||||||
|
// When
|
||||||
|
ent.setClientId("modified-client-id-1"); // modification in: lower
|
||||||
|
ent.setProtocol("modified-protocol"); // modification in: upper
|
||||||
|
ent.setAttribute("attr4", Arrays.asList("modified-value-4")); // modification in: lower
|
||||||
|
ent.setAttribute("attrX", Arrays.asList("modified-value-X")); // modification in: upper
|
||||||
|
ent.addRedirectUri("added-redirectUri"); // modification in: upper
|
||||||
|
ent.removeRedirectUri("upper-redirectUri-2"); // modification in: upper
|
||||||
|
ent.removeRedirectUri("lower-redirectUri-2"); // modification in: upper
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(lowerEntSupplierCallCount.get(), is(1));
|
||||||
|
|
||||||
|
assertThat(ent.getClientId(), is("modified-client-id-1"));
|
||||||
|
assertThat(upperEnt.getClientId(), is("upper-clientId-1"));
|
||||||
|
assertThat(lowerEnt.getClientId(), is("modified-client-id-1"));
|
||||||
|
|
||||||
|
assertThat(ent.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "added-redirectUri"));
|
||||||
|
assertThat(upperEnt.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "added-redirectUri"));
|
||||||
|
assertThat(lowerEnt.getRedirectUris(), containsInAnyOrder("lower-redirectUri-1", "lower-redirectUri-2"));
|
||||||
|
|
||||||
|
assertThat(ent.getProtocol(), is("modified-protocol"));
|
||||||
|
assertThat(upperEnt.getProtocol(), is("modified-protocol"));
|
||||||
|
assertThat(lowerEnt.getProtocol(), is("lower-protocol"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr1"), contains("upper-value-1"));
|
||||||
|
assertThat(upperEnt.getAttribute("attr1"), contains("upper-value-1"));
|
||||||
|
assertThat(lowerEnt.getAttribute("attr1"), contains("lower-value-1"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr2"), nullValue());
|
||||||
|
assertThat(upperEnt.getAttribute("attr2"), contains("upper-value-2"));
|
||||||
|
assertThat(lowerEnt.getAttribute("attr2"), nullValue());
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr3"), contains("lower-value-3"));
|
||||||
|
assertThat(upperEnt.getAttribute("attr3"), contains("upper-value-3"));
|
||||||
|
assertThat(lowerEnt.getAttribute("attr3"), contains("lower-value-3"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attr4"), contains("modified-value-4"));
|
||||||
|
assertThat(upperEnt.getAttribute("attr4"), nullValue());
|
||||||
|
assertThat(lowerEnt.getAttribute("attr4"), contains("modified-value-4"));
|
||||||
|
|
||||||
|
assertThat(ent.getAttribute("attrX"), contains("modified-value-X"));
|
||||||
|
assertThat(upperEnt.getAttribute("attrX"), contains("modified-value-X"));
|
||||||
|
assertThat(lowerEnt.getAttribute("attrX"), nullValue());
|
||||||
|
|
||||||
|
assertThat(ent.getAttributes().keySet(), containsInAnyOrder(
|
||||||
|
"attr1", // From upper
|
||||||
|
// "attr2", // From lower where it has no value
|
||||||
|
"attr3", // From lower
|
||||||
|
"attr4", // From lower
|
||||||
|
"attrX" // From lower
|
||||||
|
));
|
||||||
|
assertThat(upperEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3", "attrX"));
|
||||||
|
assertThat(lowerEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr3", "attr4"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -44,16 +44,16 @@ public class DefaultTreeNodeTest {
|
||||||
private class Node extends DefaultTreeNode<Node> {
|
private class Node extends DefaultTreeNode<Node> {
|
||||||
|
|
||||||
public Node() {
|
public Node() {
|
||||||
super(treeProperties);
|
super(DefaultTreeNodeTest.this.treeProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Node(String id) {
|
public Node(String id) {
|
||||||
super(treeProperties);
|
super(DefaultTreeNodeTest.this.treeProperties);
|
||||||
setId(id);
|
setId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Node(Node parent, String id) {
|
public Node(Node parent, String id) {
|
||||||
super(treeProperties);
|
super(DefaultTreeNodeTest.this.treeProperties);
|
||||||
setId(id);
|
setId(id);
|
||||||
setParent(parent);
|
setParent(parent);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
/*
|
||||||
|
* 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.tree;
|
||||||
|
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.map.client.MapClientEntity;
|
||||||
|
import org.keycloak.models.map.client.MapClientEntityFields;
|
||||||
|
import org.keycloak.models.map.client.MapClientEntityFields;
|
||||||
|
import org.keycloak.models.map.realm.MapRealmEntity;
|
||||||
|
import org.keycloak.models.map.storage.ModelEntityUtil;
|
||||||
|
import org.keycloak.models.map.storage.tree.TreeStorageNodePrescription.FieldContainedStatus;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.junit.Test;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.empty;
|
||||||
|
import static org.hamcrest.Matchers.hasEntry;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
|
import static org.hamcrest.Matchers.sameInstance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class TreeStorageNodePrescriptionTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmpty() {
|
||||||
|
TreeStorageNodePrescription n = new TreeStorageNodePrescription(null);
|
||||||
|
TreeStorageNodePrescription c1 = n.forEntityClass(MapClientEntity.class);
|
||||||
|
TreeStorageNodePrescription c2 = n.forEntityClass(MapClientEntity.class);
|
||||||
|
TreeStorageNodePrescription r1 = n.forEntityClass(MapRealmEntity.class);
|
||||||
|
|
||||||
|
assertThat(c1, sameInstance(c2));
|
||||||
|
assertThat(c1, not(sameInstance(r1)));
|
||||||
|
assertThat(c1.getNodeProperties().entrySet(), empty());
|
||||||
|
assertThat(c1.getEdgeProperties().entrySet(), empty());
|
||||||
|
assertThat(c1.getTreeProperties().size(), is(1));
|
||||||
|
|
||||||
|
assertThat(c1.getTreeProperties(), hasEntry(TreeProperties.MODEL_CLASS, ClientModel.class));
|
||||||
|
assertThat(r1.getTreeProperties(), hasEntry(TreeProperties.MODEL_CLASS, RealmModel.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTreePropertyProjection() {
|
||||||
|
Map<String, Object> treeProperties = new HashMap<>();
|
||||||
|
treeProperties.put("prop[" + ModelEntityUtil.getModelName(ClientModel.class) + "]", "propClientValue");
|
||||||
|
treeProperties.put("prop[" + ModelEntityUtil.getModelName(RealmModel.class) + "]", "propRealmValue");
|
||||||
|
treeProperties.put("propRealmOnly[" + ModelEntityUtil.getModelName(RealmModel.class) + "]", "propRealmOnlyValue");
|
||||||
|
treeProperties.put("propBoth", "propBothValue");
|
||||||
|
|
||||||
|
Map<String, Object> nodeProperties = new HashMap<>();
|
||||||
|
nodeProperties.put("nprop[" + ModelEntityUtil.getModelName(ClientModel.class) + "]", "propClientValue");
|
||||||
|
nodeProperties.put("nprop[" + ModelEntityUtil.getModelName(RealmModel.class) + "]", "propRealmValue");
|
||||||
|
nodeProperties.put("npropRealmOnly[" + ModelEntityUtil.getModelName(RealmModel.class) + "]", "propRealmOnlyValue");
|
||||||
|
nodeProperties.put("npropBoth", "propBothValue");
|
||||||
|
|
||||||
|
Map<String, Object> edgeProperties = new HashMap<>();
|
||||||
|
edgeProperties.put("eprop[" + ModelEntityUtil.getModelName(ClientModel.class) + "]", "propClientValue");
|
||||||
|
edgeProperties.put("eprop[" + ModelEntityUtil.getModelName(RealmModel.class) + "]", "propRealmValue");
|
||||||
|
edgeProperties.put("epropRealmOnly[" + ModelEntityUtil.getModelName(RealmModel.class) + "]", "propRealmOnlyValue");
|
||||||
|
edgeProperties.put("epropBoth", "propBothValue");
|
||||||
|
|
||||||
|
TreeStorageNodePrescription n = new TreeStorageNodePrescription(nodeProperties, edgeProperties, treeProperties);
|
||||||
|
TreeStorageNodePrescription c1 = n.forEntityClass(MapClientEntity.class);
|
||||||
|
TreeStorageNodePrescription r1 = n.forEntityClass(MapRealmEntity.class);
|
||||||
|
|
||||||
|
assertThat(c1.getTreeProperties(), hasEntry(TreeProperties.MODEL_CLASS, ClientModel.class));
|
||||||
|
assertThat(c1.getTreeProperties(), hasEntry("prop", "propClientValue"));
|
||||||
|
assertThat(c1.getTreeProperties(), hasEntry("propBoth", "propBothValue"));
|
||||||
|
assertThat(c1.getTreeProperties().size(), is(3));
|
||||||
|
assertThat(c1.getNodeProperties(), hasEntry("nprop", "propClientValue"));
|
||||||
|
assertThat(c1.getNodeProperties(), hasEntry("npropBoth", "propBothValue"));
|
||||||
|
assertThat(c1.getNodeProperties().size(), is(2));
|
||||||
|
assertThat(c1.getEdgeProperties(), hasEntry("eprop", "propClientValue"));
|
||||||
|
assertThat(c1.getEdgeProperties(), hasEntry("epropBoth", "propBothValue"));
|
||||||
|
assertThat(c1.getEdgeProperties().size(), is(2));
|
||||||
|
|
||||||
|
assertThat(r1.getTreeProperties(), hasEntry(TreeProperties.MODEL_CLASS, RealmModel.class));
|
||||||
|
assertThat(r1.getTreeProperties(), hasEntry("prop", "propRealmValue"));
|
||||||
|
assertThat(r1.getTreeProperties(), hasEntry("propRealmOnly", "propRealmOnlyValue"));
|
||||||
|
assertThat(r1.getTreeProperties(), hasEntry("propBoth", "propBothValue"));
|
||||||
|
assertThat(r1.getTreeProperties().size(), is(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test a node that has neither PRIMARY_SOURCE_FOR nor PRIMARY_SOURCE_FOR_EXCLUDED set.
|
||||||
|
* <p>
|
||||||
|
* Represents e.g. a node in the tree that stores all fields.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testPrimarySourceForBasicUnset() {
|
||||||
|
Map<String, Object> nodeProperties = new HashMap<>();
|
||||||
|
TreeStorageNodePrescription n = new TreeStorageNodePrescription(nodeProperties, null, null);
|
||||||
|
|
||||||
|
for (MapClientEntityFields field : MapClientEntityFields.values()) {
|
||||||
|
assertThat("Field " + field + " has primary source in this node", n.isPrimarySourceFor(field, null), is(FieldContainedStatus.FULLY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test a node that has PRIMARY_SOURCE_FOR set to all fields with no specialization (i.e. {@code null}),
|
||||||
|
* and no PRIMARY_SOURCE_FOR_EXCLUDED is set.
|
||||||
|
* <p>
|
||||||
|
* Represents e.g. a node in the tree that stores all fields.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testPrimarySourceForBasicSet() {
|
||||||
|
Map<String, Object> nodeProperties = new HashMap<>();
|
||||||
|
EnumMap<MapClientEntityFields, Collection<String>> primarySourceFor = new EnumMap<>(MapClientEntityFields.class);
|
||||||
|
for (MapClientEntityFields field : MapClientEntityFields.values()) {
|
||||||
|
primarySourceFor.put(field, null);
|
||||||
|
}
|
||||||
|
nodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR, primarySourceFor);
|
||||||
|
TreeStorageNodePrescription n = new TreeStorageNodePrescription(nodeProperties, null, null);
|
||||||
|
|
||||||
|
for (MapClientEntityFields field : MapClientEntityFields.values()) {
|
||||||
|
assertThat("Field " + field + " has primary source in this node", n.isPrimarySourceFor(field, null), is(FieldContainedStatus.FULLY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test a node that has PRIMARY_SOURCE_FOR set to only ID field with no specialization (i.e. {@code null}),
|
||||||
|
* and no PRIMARY_SOURCE_FOR_EXCLUDED is set.
|
||||||
|
* <p>
|
||||||
|
* Represents e.g. a node in the tree that stores only ID (maintains existence check of an object)
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testPrimarySourceForBasicSetId() {
|
||||||
|
Map<String, Object> nodeProperties = new HashMap<>();
|
||||||
|
EnumMap<MapClientEntityFields, Collection<String>> primarySourceFor = new EnumMap<>(MapClientEntityFields.class);
|
||||||
|
nodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR, primarySourceFor);
|
||||||
|
primarySourceFor.put(MapClientEntityFields.ID, null);
|
||||||
|
TreeStorageNodePrescription n = new TreeStorageNodePrescription(nodeProperties, null, null);
|
||||||
|
|
||||||
|
for (MapClientEntityFields field : MapClientEntityFields.values()) {
|
||||||
|
assertThat(n.isPrimarySourceFor(field, null),
|
||||||
|
is(field == MapClientEntityFields.ID ? FieldContainedStatus.FULLY : FieldContainedStatus.NOT_CONTAINED));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test a node that has no PRIMARY_SOURCE_FOR set,
|
||||||
|
* and PRIMARY_SOURCE_FOR_EXCLUDED is set to ATTRIBUTES field with no specialization (i.e. {@code null}).
|
||||||
|
* <p>
|
||||||
|
* Represents e.g. a node in the tree that stores all attributes apart from attributes
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testPrimarySourceForWithExcluded() {
|
||||||
|
Map<String, Object> nodeProperties = new HashMap<>();
|
||||||
|
EnumMap<MapClientEntityFields, Collection<String>> primarySourceForExcluded = new EnumMap<>(MapClientEntityFields.class);
|
||||||
|
nodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED, primarySourceForExcluded);
|
||||||
|
primarySourceForExcluded.put(MapClientEntityFields.ATTRIBUTES, null);
|
||||||
|
|
||||||
|
// node is primary for all fields apart from all attributes
|
||||||
|
TreeStorageNodePrescription n = new TreeStorageNodePrescription(nodeProperties, null, null);
|
||||||
|
|
||||||
|
for (MapClientEntityFields field : MapClientEntityFields.values()) {
|
||||||
|
assertThat(n.isPrimarySourceFor(field, null),
|
||||||
|
is(field == MapClientEntityFields.ATTRIBUTES ? FieldContainedStatus.NOT_CONTAINED: FieldContainedStatus.FULLY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test a node that has PRIMARY_SOURCE_FOR set to ATTRIBUTES field with no specialization (i.e. {@code null}),
|
||||||
|
* and PRIMARY_SOURCE_FOR_EXCLUDED is set to ATTRIBUTES field specialization to "attr1" and "attr2".
|
||||||
|
* <p>
|
||||||
|
* Represents e.g. a node in the tree that acts as a supplementary store for all attributes apart from "attr1" and "attr2"
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testPrimarySourceForWithExcludedTwoAttributes() {
|
||||||
|
Map<String, Object> nodeProperties = new HashMap<>();
|
||||||
|
EnumMap<MapClientEntityFields, Collection<String>> primarySourceFor = new EnumMap<>(MapClientEntityFields.class);
|
||||||
|
nodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR, primarySourceFor);
|
||||||
|
primarySourceFor.put(MapClientEntityFields.ATTRIBUTES, null);
|
||||||
|
EnumMap<MapClientEntityFields, Collection<String>> primarySourceForExcluded = new EnumMap<>(MapClientEntityFields.class);
|
||||||
|
nodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED, primarySourceForExcluded);
|
||||||
|
primarySourceForExcluded.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr1", "attr2"));
|
||||||
|
|
||||||
|
// node is primary for all attributes apart from "attr1" and "attr2"
|
||||||
|
TreeStorageNodePrescription n = new TreeStorageNodePrescription(nodeProperties, null, null);
|
||||||
|
|
||||||
|
assertThat("Field ID has NOT primary source in this node", n.isPrimarySourceFor(MapClientEntityFields.ID, null), is(FieldContainedStatus.NOT_CONTAINED));
|
||||||
|
assertThat("Attribute attr1 has NOT primary source in this node", n.isPrimarySourceFor(MapClientEntityFields.ATTRIBUTES, "attr1"), is(FieldContainedStatus.NOT_CONTAINED));
|
||||||
|
assertThat("Attribute attr2 has NOT primary source in this node", n.isPrimarySourceFor(MapClientEntityFields.ATTRIBUTES, "attr2"), is(FieldContainedStatus.NOT_CONTAINED));
|
||||||
|
assertThat("Attribute attr3 has primary source in this node", n.isPrimarySourceFor(MapClientEntityFields.ATTRIBUTES, "attr3"), is(FieldContainedStatus.FULLY));
|
||||||
|
assertThat("Attributes have primary source in this node and other source in some other nodes", n.isPrimarySourceFor(MapClientEntityFields.ATTRIBUTES, null), is(FieldContainedStatus.PARTIALLY));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -27,7 +27,6 @@ import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.keycloak.models.map.client.MapClientEntity;
|
import org.keycloak.models.map.client.MapClientEntity;
|
||||||
import org.keycloak.models.map.client.MapClientEntityFieldDelegate;
|
|
||||||
import org.keycloak.models.map.client.MapClientEntityFields;
|
import org.keycloak.models.map.client.MapClientEntityFields;
|
||||||
import org.keycloak.models.map.common.DeepCloner;
|
import org.keycloak.models.map.common.DeepCloner;
|
||||||
import org.keycloak.models.map.common.EntityField;
|
import org.keycloak.models.map.common.EntityField;
|
||||||
|
@ -80,7 +79,7 @@ public class Dict<E> extends UpdatableEntity.Impl implements EntityFieldDelegate
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object get(EntityField<E> field) {
|
public <EF extends Enum<? extends EntityField<E>> & EntityField<E>> Object get(EF field) {
|
||||||
if ("Attributes".equals(field.getName())) {
|
if ("Attributes".equals(field.getName())) {
|
||||||
return attributeName2key.entrySet().stream()
|
return attributeName2key.entrySet().stream()
|
||||||
.filter(me -> get(me.getValue()) != null)
|
.filter(me -> get(me.getValue()) != null)
|
||||||
|
@ -94,7 +93,7 @@ public class Dict<E> extends UpdatableEntity.Impl implements EntityFieldDelegate
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> void set(EntityField<E> field, T value) {
|
public <T, EF extends Enum<? extends EntityField<E>> & EntityField<E>> void set(EF field, T value) {
|
||||||
String key = fieldName2key.get(field.getName());
|
String key = fieldName2key.get(field.getName());
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
put(key, value);
|
put(key, value);
|
||||||
|
@ -102,7 +101,7 @@ public class Dict<E> extends UpdatableEntity.Impl implements EntityFieldDelegate
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <K> Object mapGet(EntityField<E> field, K key) {
|
public <K, EF extends Enum<? extends EntityField<E>> & EntityField<E>> Object mapGet(EF field, K key) {
|
||||||
if ("Attributes".equals(field.getName()) && attributeName2key.containsKey(key)) {
|
if ("Attributes".equals(field.getName()) && attributeName2key.containsKey(key)) {
|
||||||
Object v = get(attributeName2key.get(key));
|
Object v = get(attributeName2key.get(key));
|
||||||
return v == null ? null : Collections.singletonList(get(attributeName2key.get(key)));
|
return v == null ? null : Collections.singletonList(get(attributeName2key.get(key)));
|
||||||
|
@ -111,7 +110,7 @@ public class Dict<E> extends UpdatableEntity.Impl implements EntityFieldDelegate
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <K, T> void mapPut(EntityField<E> field, K key, T value) {
|
public <K, T, EF extends Enum<? extends EntityField<E>> & EntityField<E>> void mapPut(EF field, K key, T value) {
|
||||||
if ("Attributes".equals(field.getName()) && attributeName2key.containsKey(key) && (value instanceof List)) {
|
if ("Attributes".equals(field.getName()) && attributeName2key.containsKey(key) && (value instanceof List)) {
|
||||||
List<?> l = (List<?>) value;
|
List<?> l = (List<?>) value;
|
||||||
if (l.isEmpty()) {
|
if (l.isEmpty()) {
|
||||||
|
@ -123,7 +122,7 @@ public class Dict<E> extends UpdatableEntity.Impl implements EntityFieldDelegate
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <K> Object mapRemove(EntityField<E> field, K key) {
|
public <K, EF extends Enum<? extends EntityField<E>> & EntityField<E>> Object mapRemove(EF field, K key) {
|
||||||
if ("Attributes".equals(field.getName()) && attributeName2key.containsKey(key)) {
|
if ("Attributes".equals(field.getName()) && attributeName2key.containsKey(key)) {
|
||||||
Object o = remove(attributeName2key.get(key));
|
Object o = remove(attributeName2key.get(key));
|
||||||
return o == null ? null : Collections.singletonList(o);
|
return o == null ? null : Collections.singletonList(o);
|
||||||
|
|
Loading…
Reference in a new issue