From 2c29c58af145e251d85c5c379aab700e789c4524 Mon Sep 17 00:00:00 2001 From: Hynek Mlnarik Date: Mon, 22 Jun 2020 22:17:49 +0200 Subject: [PATCH] KEYCLOAK-14551 Map transaction --- dependencies/server-all/pom.xml | 4 + model/map/pom.xml | 42 +++ .../map/storage/MapKeycloakTransaction.java | 241 ++++++++++++++++++ .../models/map/storage/MapStorage.java | 45 ++++ model/pom.xml | 1 + pom.xml | 5 + 6 files changed, 338 insertions(+) create mode 100644 model/map/pom.xml create mode 100644 model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransaction.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/storage/MapStorage.java diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml index 16df7422ed..f216f51ce0 100755 --- a/dependencies/server-all/pom.xml +++ b/dependencies/server-all/pom.xml @@ -40,6 +40,10 @@ org.keycloak keycloak-model-jpa + + org.keycloak + keycloak-model-map + org.keycloak keycloak-model-infinispan diff --git a/model/map/pom.xml b/model/map/pom.xml new file mode 100644 index 0000000000..d5b8ed8902 --- /dev/null +++ b/model/map/pom.xml @@ -0,0 +1,42 @@ + + + + keycloak-model-pom + org.keycloak + 11.0.0-SNAPSHOT + + 4.0.0 + + keycloak-model-map + Keycloak Model Naive Map + + + + + org.bouncycastle + bcprov-jdk15on + + + org.keycloak + keycloak-core + + + org.keycloak + keycloak-server-spi + + + org.keycloak + keycloak-server-spi-private + + + org.keycloak + keycloak-services + + + junit + junit + test + + + + \ No newline at end of file diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransaction.java b/model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransaction.java new file mode 100644 index 0000000000..7b24d3dd06 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransaction.java @@ -0,0 +1,241 @@ +/* + * Copyright 2020 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; + +import org.keycloak.models.KeycloakTransaction; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; +import org.jboss.logging.Logger; + +public class MapKeycloakTransaction implements KeycloakTransaction { + + private final static Logger log = Logger.getLogger(MapKeycloakTransaction.class); + + private enum MapOperation { + PUT { + @Override + protected MapTaskWithValue taskFor(K key, V value) { + return new MapTaskWithValue(value) { + @Override + public void execute(MapStorage map) { + map.put(key, getValue()); + } + }; + } + }, + PUT_IF_ABSENT { + @Override + protected MapTaskWithValue taskFor(K key, V value) { + return new MapTaskWithValue(value) { + @Override + public void execute(MapStorage map) { + map.putIfAbsent(key, getValue()); + } + }; + } + }, + REMOVE { + @Override + protected MapTaskWithValue taskFor(K key, V value) { + return new MapTaskWithValue(null) { + @Override + public void execute(MapStorage map) { + map.remove(key); + } + }; + } + }, + REPLACE { + @Override + protected MapTaskWithValue taskFor(K key, V value) { + return new MapTaskWithValue(value) { + @Override + public void execute(MapStorage map) { + map.replace(key, getValue()); + } + }; + } + }, + ; + + protected abstract MapTaskWithValue taskFor(K key, V value); + + } + + private boolean active; + private boolean rollback; + private final Map> tasks = new LinkedHashMap<>(); + private final MapStorage map; + + public MapKeycloakTransaction(MapStorage map) { + this.map = map; + } + + @Override + public void begin() { + active = true; + } + + @Override + public void commit() { + if (rollback) { + throw new RuntimeException("Rollback only!"); + } + + for (MapTaskWithValue value : tasks.values()) { + value.execute(map); + } + } + + @Override + public void rollback() { + tasks.clear(); + } + + @Override + public void setRollbackOnly() { + rollback = true; + } + + @Override + public boolean getRollbackOnly() { + return rollback; + } + + @Override + public boolean isActive() { + return active; + } + + /** + * Adds a given task if not exists for the given key + */ + private void addTask(MapOperation op, K key, V value) { + log.tracev("Adding operation {0} for {1}", op, key); + + K taskKey = key; + tasks.merge(taskKey, op.taskFor(key, value), MapTaskCompose::new); + } + + // This is for possibility to lookup for session by id, which was created in this transaction + public V get(K key, Function defaultValueFunc) { + MapTaskWithValue current = tasks.get(key); + if (current != null) { + return current.getValue(); + } + + return defaultValueFunc.apply(key); + } + + public V getUpdated(Map.Entry keyDefaultValue) { + MapTaskWithValue current = tasks.get(keyDefaultValue.getKey()); + if (current != null) { + return current.getValue(); + } + + return keyDefaultValue.getValue(); + } + + public void put(K key, V value) { + addTask(MapOperation.PUT, key, value); + } + + public void putIfAbsent(K key, V value) { + addTask(MapOperation.PUT_IF_ABSENT, key, value); + } + + public void putIfChanged(K key, V value, Predicate shouldPut) { + log.tracev("Adding operation PUT_IF_CHANGED for {0}", key); + + K taskKey = key; + MapTaskWithValue op = new MapTaskWithValue(value) { + @Override + public void execute(MapStorage map) { + if (shouldPut.test(getValue())) { + map.put(key, getValue()); + } + } + }; + tasks.merge(taskKey, op, MapTaskCompose::new); + } + + public void replace(K key, V value) { + addTask(MapOperation.REPLACE, key, value); + } + + public void remove(K key) { + addTask(MapOperation.REMOVE, key, null); + } + + public Stream valuesStream() { + return this.tasks.values().stream() + .map(MapTaskWithValue::getValue) + .filter(Objects::nonNull); + } + + public Stream createdValuesStream(Collection existingKeys) { + return this.tasks.entrySet().stream() + .filter(me -> ! existingKeys.contains(me.getKey())) + .map(Map.Entry::getValue) + .map(MapTaskWithValue::getValue) + .filter(Objects::nonNull); + } + + private static abstract class MapTaskWithValue { + protected final V value; + + public MapTaskWithValue(V value) { + this.value = value; + } + + public V getValue() { + return value; + } + + public abstract void execute(MapStorage map); + } + + private static class MapTaskCompose extends MapTaskWithValue { + + private final MapTaskWithValue oldValue; + private final MapTaskWithValue newValue; + + public MapTaskCompose(MapTaskWithValue oldValue, MapTaskWithValue newValue) { + super(null); + this.oldValue = oldValue; + this.newValue = newValue; + } + + @Override + public void execute(MapStorage map) { + oldValue.execute(map); + newValue.execute(map); + } + + @Override + public V getValue() { + return newValue.getValue(); + } + + } +} \ No newline at end of file diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/MapStorage.java b/model/map/src/main/java/org/keycloak/models/map/storage/MapStorage.java new file mode 100644 index 0000000000..f39adc1143 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/storage/MapStorage.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020 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; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * + * @author hmlnarik + */ +public interface MapStorage { + + V get(K key); + + V put(K key, V value); + + V putIfAbsent(K key, V value); + + V remove(K key); + + V replace(K key, V value); + + Set keySet(); + + Set> entrySet(); + + Collection values(); + +} diff --git a/model/pom.xml b/model/pom.xml index 131cbcd942..0721a5a4f0 100755 --- a/model/pom.xml +++ b/model/pom.xml @@ -33,5 +33,6 @@ jpa infinispan + map diff --git a/pom.xml b/pom.xml index abb888702c..48647ea2c4 100755 --- a/pom.xml +++ b/pom.xml @@ -1134,6 +1134,11 @@ keycloak-model-jpa ${project.version} + + org.keycloak + keycloak-model-map + ${project.version} + org.keycloak keycloak-model-infinispan