diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 052b283010..71c91a737c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,6 +31,7 @@ jobs:
mvn clean install -nsu -B -e -f testsuite/integration-arquillian/servers/auth-server -Pauth-server-quarkus
mvn clean install -nsu -B -e -f testsuite/integration-arquillian/servers/auth-server -Pauth-server-wildfly
mvn clean install -nsu -B -e -f testsuite/integration-arquillian/servers/auth-server -Pauth-server-undertow
+ mvn clean install -nsu -B -e -f testsuite/integration-arquillian/servers/cache-server -Pcache-server-infinispan
- name: Store Keycloak artifacts
id: store-keycloak
@@ -136,13 +137,19 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- server: ['quarkus', 'undertow-map', 'wildfly']
+ server: ['quarkus', 'undertow-map', 'wildfly', 'undertow-map-hot-rod']
tests: ['group1','group2','group3']
fail-fast: false
steps:
- uses: actions/checkout@v2
+ with:
+ fetch-depth: 2
+
+ - name: Check whether HEAD^ contains HotRod storage relevant changes
+ run: echo "GIT_HOTROD_RELEVANT_DIFF=$( git diff --name-only HEAD^ | egrep -ic -e '^model/hot-rod|^model/map|^model/build-processor|^testsuite/model' )" >> $GITHUB_ENV
- name: Cache Maven packages
+ if: ${{ github.event_name != 'pull_request' || matrix.server != 'undertow-map-hot-rod' || env.GIT_HOTROD_RELEVANT_DIFF != 0 }}
uses: actions/cache@v2
with:
path: ~/.m2/repository
@@ -150,6 +157,7 @@ jobs:
restore-keys: cache-1-${{ runner.os }}-m2
- name: Download built keycloak
+ if: ${{ github.event_name != 'pull_request' || matrix.server != 'undertow-map-hot-rod' || env.GIT_HOTROD_RELEVANT_DIFF != 0 }}
id: download-keycloak
uses: actions/download-artifact@v2
with:
@@ -162,16 +170,20 @@ jobs:
# ls -lR ~/.m2/repository
- uses: actions/setup-java@v1
+ if: ${{ github.event_name != 'pull_request' || matrix.server != 'undertow-map-hot-rod' || env.GIT_HOTROD_RELEVANT_DIFF != 0 }}
with:
java-version: ${{ env.DEFAULT_JDK_VERSION }}
- name: Update maven settings
+ if: ${{ github.event_name != 'pull_request' || matrix.server != 'undertow-map-hot-rod' || env.GIT_HOTROD_RELEVANT_DIFF != 0 }}
run: mkdir -p ~/.m2 ; cp .github/settings.xml ~/.m2/
- name: Run base tests
+ if: ${{ github.event_name != 'pull_request' || matrix.server != 'undertow-map-hot-rod' || env.GIT_HOTROD_RELEVANT_DIFF != 0 }}
run: |
declare -A PARAMS TESTGROUP
PARAMS["quarkus"]="-Pauth-server-quarkus"
PARAMS["undertow-map"]="-Pauth-server-undertow -Pmap-storage -Dpageload.timeout=90000"
+ PARAMS["undertow-map-hot-rod"]="-Pauth-server-undertow -Pmap-storage,map-storage-hot-rod -Dpageload.timeout=90000"
PARAMS["wildfly"]="-Pauth-server-wildfly"
TESTGROUP["group1"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(a[abc]|ad[a-l]|[^a-q]).*]" # Tests alphabetically before admin tests and those after "r"
TESTGROUP["group2"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(ad[^a-l]|a[^a-d]|b).*]" # Admin tests and those starting with "b"
diff --git a/model/hot-rod/pom.xml b/model/map-hot-rod/pom.xml
similarity index 85%
rename from model/hot-rod/pom.xml
rename to model/map-hot-rod/pom.xml
index 74e27e0a04..8f642b4fbf 100644
--- a/model/hot-rod/pom.xml
+++ b/model/map-hot-rod/pom.xml
@@ -47,6 +47,17 @@
protostream-processor
provided
+
+
+ junit
+ junit
+ test
+
+
+ org.hamcrest
+ hamcrest-all
+ test
+
diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodAttributeEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodAttributeEntity.java
new file mode 100644
index 0000000000..3b6f9e0b97
--- /dev/null
+++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodAttributeEntity.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.map.client;
+
+import org.infinispan.protostream.annotations.ProtoField;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+
+public class HotRodAttributeEntity {
+ @ProtoField(number = 1)
+ public String name;
+
+ @ProtoField(number = 2)
+ public List values = new LinkedList<>();
+
+ public HotRodAttributeEntity() {
+ }
+
+ public HotRodAttributeEntity(String name, List values) {
+ this.name = name;
+ this.values.addAll(values);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public List getValues() {
+ return values;
+ }
+
+ public void setValues(List values) {
+ this.values = values;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ HotRodAttributeEntity that = (HotRodAttributeEntity) o;
+ return Objects.equals(name, that.name) && Objects.equals(values, that.values);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, values);
+ }
+}
diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodClientEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodClientEntity.java
new file mode 100644
index 0000000000..493a2a77a0
--- /dev/null
+++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodClientEntity.java
@@ -0,0 +1,712 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.map.client;
+
+import org.infinispan.protostream.annotations.ProtoField;
+import org.keycloak.models.map.common.DeepCloner;
+import org.keycloak.models.map.common.HotRodPair;
+import org.keycloak.models.map.common.Versioned;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class HotRodClientEntity implements MapClientEntity, Versioned {
+
+ @ProtoField(number = 1, required = true)
+ public int entityVersion = 1;
+
+ @ProtoField(number = 2, required = true)
+ public String id;
+
+ @ProtoField(number = 3)
+ public String realmId;
+
+ @ProtoField(number = 4)
+ public String clientId;
+
+ @ProtoField(number = 5)
+ public String name;
+
+ @ProtoField(number = 6)
+ public String description;
+
+ @ProtoField(number = 7)
+ public Set redirectUris = new HashSet<>();
+
+ @ProtoField(number = 8)
+ public Boolean enabled;
+
+ @ProtoField(number = 9)
+ public Boolean alwaysDisplayInConsole;
+
+ @ProtoField(number = 10)
+ public String clientAuthenticatorType;
+
+ @ProtoField(number = 11)
+ public String secret;
+
+ @ProtoField(number = 12)
+ public String registrationToken;
+
+ @ProtoField(number = 13)
+ public String protocol;
+
+ @ProtoField(number = 14)
+ public Set attributes = new HashSet<>();
+
+ @ProtoField(number = 15)
+ public Set> authFlowBindings = new HashSet<>();
+
+ @ProtoField(number = 16)
+ public Boolean publicClient;
+
+ @ProtoField(number = 17)
+ public Boolean fullScopeAllowed;
+
+ @ProtoField(number = 18)
+ public Boolean frontchannelLogout;
+
+ @ProtoField(number = 19)
+ public Integer notBefore;
+
+ @ProtoField(number = 20)
+ public Set scope = new HashSet<>();
+
+ @ProtoField(number = 21)
+ public Set webOrigins = new HashSet<>();
+
+ @ProtoField(number = 22)
+ public Set protocolMappers = new HashSet<>();
+
+ @ProtoField(number = 23)
+ public Set> clientScopes = new HashSet<>();
+
+ @ProtoField(number = 24)
+ public Set scopeMappings = new HashSet<>();
+
+ @ProtoField(number = 25)
+ public Boolean surrogateAuthRequired;
+
+ @ProtoField(number = 26)
+ public String managementUrl;
+
+ @ProtoField(number = 27)
+ public String baseUrl;
+
+ @ProtoField(number = 28)
+ public Boolean bearerOnly;
+
+ @ProtoField(number = 29)
+ public Boolean consentRequired;
+
+ @ProtoField(number = 30)
+ public String rootUrl;
+
+ @ProtoField(number = 31)
+ public Boolean standardFlowEnabled;
+
+ @ProtoField(number = 32)
+ public Boolean implicitFlowEnabled;
+
+ @ProtoField(number = 33)
+ public Boolean directAccessGrantsEnabled;
+
+ @ProtoField(number = 34)
+ public Boolean serviceAccountsEnabled;
+
+ @ProtoField(number = 35)
+ public Integer nodeReRegistrationTimeout;
+
+ private boolean updated = false;
+
+ private final DeepCloner cloner;
+
+ public HotRodClientEntity() {
+ this(DeepCloner.DUMB_CLONER);
+ }
+
+ public HotRodClientEntity(DeepCloner cloner) {
+ this.cloner = cloner;
+ }
+
+ @Override
+ public int getEntityVersion() {
+ return entityVersion;
+ }
+
+ @Override
+ public List getAttribute(String name) {
+ return attributes.stream()
+ .filter(attributeEntity -> Objects.equals(attributeEntity.getName(), name))
+ .findFirst()
+ .map(HotRodAttributeEntity::getValues)
+ .orElse(Collections.emptyList());
+ }
+
+ @Override
+ public Map> getAttributes() {
+ return attributes.stream().collect(Collectors.toMap(HotRodAttributeEntity::getName, HotRodAttributeEntity::getValues));
+ }
+
+ @Override
+ public void setAttribute(String name, List values) {
+ boolean valueUndefined = values == null || values.isEmpty();
+
+ Optional first = attributes.stream()
+ .filter(attributeEntity -> Objects.equals(attributeEntity.getName(), name))
+ .findFirst();
+
+ if (first.isPresent()) {
+ HotRodAttributeEntity attributeEntity = first.get();
+ if (valueUndefined) {
+ this.updated = true;
+ removeAttribute(name);
+ } else {
+ this.updated |= !Objects.equals(attributeEntity.getValues(), values);
+ attributeEntity.setValues(values);
+ }
+
+ return;
+ }
+
+ // do not create attributes if empty or null
+ if (valueUndefined) {
+ return;
+ }
+
+ HotRodAttributeEntity newAttributeEntity = new HotRodAttributeEntity(name, values);
+ updated |= attributes.add(newAttributeEntity);
+ }
+
+ @Override
+ public void removeAttribute(String name) {
+ attributes.stream()
+ .filter(attributeEntity -> Objects.equals(attributeEntity.getName(), name))
+ .findFirst()
+ .ifPresent(attr -> {
+ this.updated |= attributes.remove(attr);
+ });
+ }
+
+ @Override
+ public String getClientId() {
+ return clientId;
+ }
+
+ @Override
+ public void setClientId(String clientId) {
+ this.updated |= ! Objects.equals(this.clientId, clientId);
+ this.clientId = clientId;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public void setName(String name) {
+ this.updated |= ! Objects.equals(this.name, name);
+ this.name = name;
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+
+ @Override
+ public void setDescription(String description) {
+ this.updated |= ! Objects.equals(this.description, description);
+ this.description = description;
+ }
+
+ @Override
+ public Set getRedirectUris() {
+ return redirectUris;
+ }
+
+ @Override
+ public void setRedirectUris(Set redirectUris) {
+ if (redirectUris == null || redirectUris.isEmpty()) {
+ this.updated |= !this.redirectUris.isEmpty();
+ this.redirectUris.clear();
+ return;
+ }
+
+ this.updated |= ! Objects.equals(this.redirectUris, redirectUris);
+ this.redirectUris.clear();
+ this.redirectUris.addAll(redirectUris);
+ }
+
+ @Override
+ public Boolean isEnabled() {
+ return enabled;
+ }
+
+ @Override
+ public void setEnabled(Boolean enabled) {
+ this.updated |= ! Objects.equals(this.enabled, enabled);
+ this.enabled = enabled;
+ }
+
+ @Override
+ public Boolean isAlwaysDisplayInConsole() {
+ return alwaysDisplayInConsole;
+ }
+
+ @Override
+ public void setAlwaysDisplayInConsole(Boolean alwaysDisplayInConsole) {
+ this.updated |= ! Objects.equals(this.alwaysDisplayInConsole, alwaysDisplayInConsole);
+ this.alwaysDisplayInConsole = alwaysDisplayInConsole;
+ }
+
+ @Override
+ public String getClientAuthenticatorType() {
+ return clientAuthenticatorType;
+ }
+
+ @Override
+ public void setClientAuthenticatorType(String clientAuthenticatorType) {
+ this.updated |= ! Objects.equals(this.clientAuthenticatorType, clientAuthenticatorType);
+ this.clientAuthenticatorType = clientAuthenticatorType;
+ }
+
+ @Override
+ public String getSecret() {
+ return secret;
+ }
+
+ @Override
+ public void setSecret(String secret) {
+ this.updated |= ! Objects.equals(this.secret, secret);
+ this.secret = secret;
+ }
+
+ @Override
+ public String getRegistrationToken() {
+ return registrationToken;
+ }
+
+ @Override
+ public void setRegistrationToken(String registrationToken) {
+ this.updated |= ! Objects.equals(this.registrationToken, registrationToken);
+ this.registrationToken = registrationToken;
+ }
+
+ @Override
+ public String getProtocol() {
+ return protocol;
+ }
+
+ @Override
+ public void setProtocol(String protocol) {
+ this.updated |= ! Objects.equals(this.protocol, protocol);
+ this.protocol = protocol;
+ }
+
+ @Override
+ public Map getAuthFlowBindings() {
+ return authFlowBindings.stream().collect(Collectors.toMap(HotRodPair::getFirst, HotRodPair::getSecond));
+ }
+
+ @Override
+ public void setAuthFlowBindings(Map authFlowBindings) {
+ if (authFlowBindings == null || authFlowBindings.isEmpty()) {
+ this.updated |= !this.authFlowBindings.isEmpty();
+ this.authFlowBindings.clear();
+ return;
+ }
+
+ this.updated = true;
+ this.authFlowBindings.clear();
+ this.authFlowBindings.addAll(authFlowBindings.entrySet().stream().map(e -> new HotRodPair<>(e.getKey(), e.getValue())).collect(Collectors.toSet()));
+ }
+
+ @Override
+ public Boolean isPublicClient() {
+ return publicClient;
+ }
+
+ @Override
+ public void setPublicClient(Boolean publicClient) {
+ this.updated |= ! Objects.equals(this.publicClient, publicClient);
+ this.publicClient = publicClient;
+ }
+
+ @Override
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+
+ @Override
+ public Boolean isFullScopeAllowed() {
+ return fullScopeAllowed;
+ }
+
+ @Override
+ public void setFullScopeAllowed(Boolean fullScopeAllowed) {
+ this.updated |= ! Objects.equals(this.fullScopeAllowed, fullScopeAllowed);
+ this.fullScopeAllowed = fullScopeAllowed;
+ }
+
+ @Override
+ public Boolean isFrontchannelLogout() {
+ return frontchannelLogout;
+ }
+
+ @Override
+ public void setFrontchannelLogout(Boolean frontchannelLogout) {
+ this.updated |= ! Objects.equals(this.frontchannelLogout, frontchannelLogout);
+ this.frontchannelLogout = frontchannelLogout;
+ }
+
+ @Override
+ public Integer getNotBefore() {
+ return notBefore;
+ }
+
+ @Override
+ public void setNotBefore(Integer notBefore) {
+ this.updated |= ! Objects.equals(this.notBefore, notBefore);
+ this.notBefore = notBefore;
+ }
+
+ @Override
+ public Set getScope() {
+ return scope;
+ }
+
+ @Override
+ public void setScope(Set scope) {
+ if (scope == null || scope.isEmpty()) {
+ this.updated |= !this.scope.isEmpty();
+ this.scope.clear();
+ return;
+ }
+
+ this.updated |= ! Objects.equals(this.scope, scope);
+ this.scope.clear();
+ this.scope.addAll(scope);
+ }
+
+ @Override
+ public Set getWebOrigins() {
+ return webOrigins;
+ }
+
+ @Override
+ public void setWebOrigins(Set webOrigins) {
+ if (webOrigins == null || webOrigins.isEmpty()) {
+ this.updated |= !this.webOrigins.isEmpty();
+ this.webOrigins.clear();
+ return;
+ }
+
+ this.updated |= ! Objects.equals(this.webOrigins, webOrigins);
+ this.webOrigins.clear();
+ this.webOrigins.addAll(webOrigins);
+ }
+
+ @Override
+ public Map getProtocolMappers() {
+ return protocolMappers.stream().collect(Collectors.toMap(HotRodProtocolMapperEntity::getId, Function.identity()));
+ }
+
+
+ @Override
+ public MapProtocolMapperEntity getProtocolMapper(String id) {
+ return protocolMappers.stream().filter(hotRodMapper -> Objects.equals(hotRodMapper.getId(), id)).findFirst().orElse(null);
+ }
+
+ @Override
+ public void setProtocolMapper(String id, MapProtocolMapperEntity mapping) {
+ removeProtocolMapper(id);
+
+ protocolMappers.add((HotRodProtocolMapperEntity) cloner.from(mapping)); // Workaround, will be replaced by cloners
+ this.updated = true;
+ }
+
+ @Override
+ public void removeProtocolMapper(String id) {
+ protocolMappers.stream().filter(entity -> Objects.equals(id, entity.id))
+ .findFirst()
+ .ifPresent(entity -> {
+ protocolMappers.remove(entity);
+ updated = true;
+ });
+ }
+
+ @Override
+ public Boolean isSurrogateAuthRequired() {
+ return surrogateAuthRequired;
+ }
+
+ @Override
+ public void setSurrogateAuthRequired(Boolean surrogateAuthRequired) {
+ this.updated |= ! Objects.equals(this.surrogateAuthRequired, surrogateAuthRequired);
+ this.surrogateAuthRequired = surrogateAuthRequired;
+ }
+
+ @Override
+ public String getManagementUrl() {
+ return managementUrl;
+ }
+
+ @Override
+ public void setManagementUrl(String managementUrl) {
+ this.updated |= ! Objects.equals(this.managementUrl, managementUrl);
+ this.managementUrl = managementUrl;
+ }
+
+ @Override
+ public String getRootUrl() {
+ return rootUrl;
+ }
+
+ @Override
+ public void setRootUrl(String rootUrl) {
+ this.updated |= ! Objects.equals(this.rootUrl, rootUrl);
+ this.rootUrl = rootUrl;
+ }
+
+ @Override
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+
+ @Override
+ public void setBaseUrl(String baseUrl) {
+ this.updated |= ! Objects.equals(this.baseUrl, baseUrl);
+ this.baseUrl = baseUrl;
+ }
+
+ @Override
+ public Boolean isBearerOnly() {
+ return bearerOnly;
+ }
+
+ @Override
+ public void setBearerOnly(Boolean bearerOnly) {
+ this.updated |= ! Objects.equals(this.bearerOnly, bearerOnly);
+ this.bearerOnly = bearerOnly;
+ }
+
+ @Override
+ public Boolean isConsentRequired() {
+ return consentRequired;
+ }
+
+ @Override
+ public void setConsentRequired(Boolean consentRequired) {
+ this.updated |= ! Objects.equals(this.consentRequired, consentRequired);
+ this.consentRequired = consentRequired;
+ }
+
+ @Override
+ public Boolean isStandardFlowEnabled() {
+ return standardFlowEnabled;
+ }
+
+ @Override
+ public void setStandardFlowEnabled(Boolean standardFlowEnabled) {
+ this.updated |= ! Objects.equals(this.standardFlowEnabled, standardFlowEnabled);
+ this.standardFlowEnabled = standardFlowEnabled;
+ }
+
+ @Override
+ public Boolean isImplicitFlowEnabled() {
+ return implicitFlowEnabled;
+ }
+
+ @Override
+ public void setImplicitFlowEnabled(Boolean implicitFlowEnabled) {
+ this.updated |= ! Objects.equals(this.implicitFlowEnabled, implicitFlowEnabled);
+ this.implicitFlowEnabled = implicitFlowEnabled;
+ }
+
+ @Override
+ public Boolean isDirectAccessGrantsEnabled() {
+ return directAccessGrantsEnabled;
+ }
+
+ @Override
+ public void setDirectAccessGrantsEnabled(Boolean directAccessGrantsEnabled) {
+ this.updated |= ! Objects.equals(this.directAccessGrantsEnabled, directAccessGrantsEnabled);
+ this.directAccessGrantsEnabled = directAccessGrantsEnabled;
+ }
+
+ @Override
+ public Boolean isServiceAccountsEnabled() {
+ return serviceAccountsEnabled;
+ }
+
+ @Override
+ public void setServiceAccountsEnabled(Boolean serviceAccountsEnabled) {
+ this.updated |= ! Objects.equals(this.serviceAccountsEnabled, serviceAccountsEnabled);
+ this.serviceAccountsEnabled = serviceAccountsEnabled;
+ }
+
+ @Override
+ public Integer getNodeReRegistrationTimeout() {
+ return nodeReRegistrationTimeout;
+ }
+
+ @Override
+ public void setNodeReRegistrationTimeout(Integer nodeReRegistrationTimeout) {
+ this.updated |= ! Objects.equals(this.nodeReRegistrationTimeout, nodeReRegistrationTimeout);
+ this.nodeReRegistrationTimeout = nodeReRegistrationTimeout;
+ }
+
+ @Override
+ public void addWebOrigin(String webOrigin) {
+ updated = true;
+ this.webOrigins.add(webOrigin);
+ }
+
+ @Override
+ public void removeWebOrigin(String webOrigin) {
+ updated |= this.webOrigins.remove(webOrigin);
+ }
+
+ @Override
+ public void addRedirectUri(String redirectUri) {
+ this.updated |= ! this.redirectUris.contains(redirectUri);
+ this.redirectUris.add(redirectUri);
+ }
+
+ @Override
+ public void removeRedirectUri(String redirectUri) {
+ updated |= this.redirectUris.remove(redirectUri);
+ }
+
+ @Override
+ public String getAuthenticationFlowBindingOverride(String binding) {
+ return authFlowBindings.stream().filter(pair -> Objects.equals(pair.getFirst(), binding)).findFirst()
+ .map(HotRodPair::getSecond)
+ .orElse(null);
+ }
+
+ @Override
+ public Map getAuthenticationFlowBindingOverrides() {
+ return this.authFlowBindings.stream().collect(Collectors.toMap(HotRodPair::getFirst, HotRodPair::getSecond));
+ }
+
+ @Override
+ public void removeAuthenticationFlowBindingOverride(String binding) {
+ this.authFlowBindings.stream().filter(pair -> Objects.equals(pair.getFirst(), binding)).findFirst()
+ .ifPresent(pair -> {
+ updated = true;
+ authFlowBindings.remove(pair);
+ });
+ }
+
+ @Override
+ public void setAuthenticationFlowBindingOverride(String binding, String flowId) {
+ this.updated = true;
+
+ removeAuthenticationFlowBindingOverride(binding);
+
+ this.authFlowBindings.add(new HotRodPair<>(binding, flowId));
+ }
+
+ @Override
+ public Collection getScopeMappings() {
+ return scopeMappings;
+ }
+
+ @Override
+ public void addScopeMapping(String id) {
+ if (id != null) {
+ updated = true;
+ scopeMappings.add(id);
+ }
+ }
+
+ @Override
+ public void removeScopeMapping(String id) {
+ updated |= scopeMappings.remove(id);
+ }
+
+ @Override
+ public Map getClientScopes() {
+ return this.clientScopes.stream().collect(Collectors.toMap(HotRodPair::getFirst, HotRodPair::getSecond));
+ }
+
+ @Override
+ public void setClientScope(String id, Boolean defaultScope) {
+ if (id != null) {
+ updated = true;
+ removeClientScope(id);
+
+ this.clientScopes.add(new HotRodPair<>(id, defaultScope));
+ }
+ }
+
+ @Override
+ public void removeClientScope(String id) {
+ this.clientScopes.stream().filter(pair -> Objects.equals(pair.getFirst(), id)).findFirst()
+ .ifPresent(pair -> {
+ updated = true;
+ clientScopes.remove(pair);
+ });
+ }
+
+ @Override
+ public Stream getClientScopes(boolean defaultScope) {
+ return this.clientScopes.stream()
+ .filter(pair -> Objects.equals(pair.getSecond(), defaultScope))
+ .map(HotRodPair::getFirst);
+ }
+
+ @Override
+ public String getRealmId() {
+ return this.realmId;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public void setId(String id) {
+ if (this.id != null) throw new IllegalStateException("Id cannot be changed");
+ this.id = id;
+ this.updated |= id != null;
+ }
+
+ @Override
+ public void clearUpdatedFlag() {
+ this.updated = false;
+ }
+
+ @Override
+ public boolean isUpdated() {
+ return updated;
+ }
+}
diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodProtocolMapperEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodProtocolMapperEntity.java
new file mode 100644
index 0000000000..96683184c0
--- /dev/null
+++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodProtocolMapperEntity.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.map.client;
+
+import org.infinispan.protostream.annotations.ProtoField;
+import org.keycloak.models.map.common.HotRodPair;
+
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class HotRodProtocolMapperEntity implements MapProtocolMapperEntity {
+ @ProtoField(number = 1)
+ public String id;
+ @ProtoField(number = 2)
+ public String name;
+ @ProtoField(number = 3)
+ public String protocol;
+ @ProtoField(number = 4)
+ public String protocolMapper;
+// @ProtoField(number = 5, defaultValue = "false")
+// public boolean consentRequired;
+// @ProtoField(number = 5)
+// public String consentText;
+ @ProtoField(number = 5)
+ public Set> config = new LinkedHashSet<>();
+
+ private boolean updated;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ HotRodProtocolMapperEntity entity = (HotRodProtocolMapperEntity) o;
+
+ return id.equals(entity.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public void setId(String id) {
+ updated |= !Objects.equals(this.id, id);
+ this.id = id;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public void setName(String name) {
+ updated |= !Objects.equals(this.name, name);
+ this.name = name;
+ }
+
+ @Override
+ public String getProtocolMapper() {
+ return protocolMapper;
+ }
+
+ @Override
+ public void setProtocolMapper(String protocolMapper) {
+ updated |= !Objects.equals(this.protocolMapper, protocolMapper);
+ this.protocolMapper = protocolMapper;
+ }
+
+ @Override
+ public Map getConfig() {
+ return config.stream().collect(Collectors.toMap(HotRodPair::getFirst, HotRodPair::getSecond));
+ }
+
+ @Override
+ public void setConfig(Map config) {
+ updated |= !Objects.equals(this.config, config);
+ this.config.clear();
+
+ config.entrySet().stream().map(entry -> new HotRodPair<>(entry.getKey(), entry.getValue())).forEach(this.config::add);
+ }
+
+ @Override
+ public boolean isUpdated() {
+ return updated;
+ }
+}
diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodEntityDescriptor.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodEntityDescriptor.java
new file mode 100644
index 0000000000..2423dd4fc1
--- /dev/null
+++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodEntityDescriptor.java
@@ -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;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+public class HotRodEntityDescriptor {
+ private final Class> modelTypeClass;
+ private final Class entityTypeClass;
+ private final List> hotRodClasses;
+ private final String cacheName;
+
+ public HotRodEntityDescriptor(Class> modelTypeClass, Class entityTypeClass, List> hotRodClasses, String cacheName) {
+ this.modelTypeClass = modelTypeClass;
+ this.entityTypeClass = entityTypeClass;
+ this.hotRodClasses = hotRodClasses;
+ this.cacheName = cacheName;
+ }
+
+ public Class> getModelTypeClass() {
+ return modelTypeClass;
+ }
+
+ public Class getEntityTypeClass() {
+ return entityTypeClass;
+ }
+
+ public Stream> getHotRodClasses() {
+ return hotRodClasses.stream();
+ }
+
+ public String getCacheName() {
+ return cacheName;
+ }
+}
diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodPair.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodPair.java
new file mode 100644
index 0000000000..906d9bffbc
--- /dev/null
+++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodPair.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.map.common;
+
+import org.infinispan.protostream.WrappedMessage;
+import org.infinispan.protostream.annotations.ProtoField;
+
+public class HotRodPair {
+
+ @ProtoField(number = 1)
+ public WrappedMessage firstWrapped;
+ @ProtoField(number = 2)
+ public WrappedMessage secondWrapped;
+
+ public HotRodPair() {}
+
+ public HotRodPair(T first, V second) {
+ this.firstWrapped = new WrappedMessage(first);
+ this.secondWrapped = new WrappedMessage(second);
+ }
+
+ public T getFirst() {
+ return firstWrapped == null ? null : (T) firstWrapped.getValue();
+ }
+
+ public V getSecond() {
+ return secondWrapped == null ? null : (V) secondWrapped.getValue();
+ }
+
+ public void setFirst(T first) {
+ this.firstWrapped = new WrappedMessage(first);
+ }
+
+ public void setSecond(V second) {
+ this.secondWrapped = new WrappedMessage(second);
+ }
+}
diff --git a/model/hot-rod/src/main/java/org/keycloak/models/map/common/HotRodUtils.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodUtils.java
similarity index 92%
rename from model/hot-rod/src/main/java/org/keycloak/models/map/common/HotRodUtils.java
rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodUtils.java
index a6e7a2ff4f..cfb9f4c699 100644
--- a/model/hot-rod/src/main/java/org/keycloak/models/map/common/HotRodUtils.java
+++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodUtils.java
@@ -17,6 +17,7 @@
package org.keycloak.models.map.common;
import org.infinispan.manager.DefaultCacheManager;
+import org.infinispan.query.dsl.Query;
import org.infinispan.rest.RestServer;
import org.infinispan.rest.configuration.RestServerConfigurationBuilder;
import org.infinispan.server.configuration.endpoint.SinglePortServerConfigurationBuilder;
@@ -84,4 +85,16 @@ public class HotRodUtils {
HotRodUtils.createHotRodMapStoreServer(new HotRodServer(), hotRodCacheManager, embeddedPort);
}
+
+ public static Query paginateQuery(Query query, Integer first, Integer max) {
+ if (first != null && first > 0) {
+ query = query.startOffset(first);
+ }
+
+ if (max != null && max >= 0) {
+ query = query.maxResults(max);
+ }
+
+ return query;
+ }
}
diff --git a/model/hot-rod/src/main/java/org/keycloak/models/map/common/ProtoSchemaInitializer.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/ProtoSchemaInitializer.java
similarity index 75%
rename from model/hot-rod/src/main/java/org/keycloak/models/map/common/ProtoSchemaInitializer.java
rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/common/ProtoSchemaInitializer.java
index ec19172ae0..762a6fceb7 100644
--- a/model/hot-rod/src/main/java/org/keycloak/models/map/common/ProtoSchemaInitializer.java
+++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/ProtoSchemaInitializer.java
@@ -19,20 +19,19 @@ package org.keycloak.models.map.common;
import org.infinispan.protostream.GeneratedSchema;
import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder;
-//import org.keycloak.models.map.client.HotRodAttributeEntity;
-//import org.keycloak.models.map.client.HotRodClientEntity;
-//import org.keycloak.models.map.client.HotRodPair;
-//import org.keycloak.models.map.client.HotRodProtocolMapperEntity;
+import org.keycloak.models.map.client.HotRodAttributeEntity;
+import org.keycloak.models.map.client.HotRodClientEntity;
+import org.keycloak.models.map.client.HotRodProtocolMapperEntity;
/**
* @author Martin Kanis
*/
@AutoProtoSchemaBuilder(
includeClasses = {
- //HotRodAttributeEntity.class,
- //HotRodClientEntity.class,
- //HotRodProtocolMapperEntity.class,
- //HotRodPair.class
+ HotRodAttributeEntity.class,
+ HotRodClientEntity.class,
+ HotRodProtocolMapperEntity.class,
+ HotRodPair.class
},
schemaFileName = "KeycloakHotRodMapStorage.proto",
schemaFilePath = "proto/",
diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/Versioned.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/Versioned.java
new file mode 100644
index 0000000000..c048381fa4
--- /dev/null
+++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/Versioned.java
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+public interface Versioned {
+ int getEntityVersion();
+}
diff --git a/model/hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProvider.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProvider.java
similarity index 100%
rename from model/hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProvider.java
rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProvider.java
diff --git a/model/hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProviderFactory.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProviderFactory.java
similarity index 92%
rename from model/hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProviderFactory.java
rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProviderFactory.java
index 4508095af2..e5e3030568 100644
--- a/model/hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProviderFactory.java
+++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProviderFactory.java
@@ -26,10 +26,10 @@ import org.infinispan.query.remote.client.ProtobufMetadataManagerConstants;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
-//import org.keycloak.models.map.common.HotRodEntityDescriptor;
+import org.keycloak.models.map.common.HotRodEntityDescriptor;
import org.keycloak.models.map.common.HotRodUtils;
import org.keycloak.models.map.common.ProtoSchemaInitializer;
-//import org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory;
+import org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory;
import java.net.URI;
import java.net.URISyntaxException;
@@ -106,9 +106,9 @@ public class DefaultHotRodConnectionProviderFactory implements HotRodConnectionP
if (configureRemoteCaches) {
// access the caches to force their creation
- /*HotRodMapStorageProviderFactory.ENTITY_DESCRIPTOR_MAP.values().stream()
+ HotRodMapStorageProviderFactory.ENTITY_DESCRIPTOR_MAP.values().stream()
.map(HotRodEntityDescriptor::getCacheName)
- .forEach(remoteCacheManager::getCache);*/
+ .forEach(remoteCacheManager::getCache);
}
registerSchemata(ProtoSchemaInitializer.INSTANCE);
@@ -133,8 +133,8 @@ public class DefaultHotRodConnectionProviderFactory implements HotRodConnectionP
throw new RuntimeException("Cannot read the cache configuration!", e);
}
- /*HotRodMapStorageProviderFactory.ENTITY_DESCRIPTOR_MAP.values().stream()
+ HotRodMapStorageProviderFactory.ENTITY_DESCRIPTOR_MAP.values().stream()
.map(HotRodEntityDescriptor::getCacheName)
- .forEach(name -> builder.remoteCache(name).configurationURI(uri));*/
+ .forEach(name -> builder.remoteCache(name).configurationURI(uri));
}
}
diff --git a/model/hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionProvider.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionProvider.java
similarity index 100%
rename from model/hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionProvider.java
rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionProvider.java
diff --git a/model/hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionProviderFactory.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionProviderFactory.java
similarity index 100%
rename from model/hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionProviderFactory.java
rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionProviderFactory.java
diff --git a/model/hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionSpi.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionSpi.java
similarity index 100%
rename from model/hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionSpi.java
rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionSpi.java
diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java
new file mode 100644
index 0000000000..775ef84f41
--- /dev/null
+++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.map.storage.hotRod;
+
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.Search;
+import org.infinispan.commons.util.CloseableIterator;
+import org.infinispan.query.dsl.Query;
+import org.infinispan.query.dsl.QueryFactory;
+import org.jboss.logging.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.map.common.AbstractEntity;
+import org.keycloak.models.map.common.DeepCloner;
+import org.keycloak.models.map.common.HotRodEntityDescriptor;
+import org.keycloak.models.map.common.StringKeyConvertor;
+import org.keycloak.models.map.common.UpdatableEntity;
+import org.keycloak.models.map.storage.MapKeycloakTransaction;
+import org.keycloak.models.map.storage.MapStorage;
+import org.keycloak.models.map.storage.QueryParameters;
+import org.keycloak.models.map.storage.chm.ConcurrentHashMapCrudOperations;
+import org.keycloak.models.map.storage.chm.ConcurrentHashMapKeycloakTransaction;
+import org.keycloak.models.map.storage.chm.MapFieldPredicates;
+import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder;
+import org.keycloak.storage.SearchableModelField;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Spliterators;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import static org.keycloak.models.map.common.HotRodUtils.paginateQuery;
+import static org.keycloak.utils.StreamsUtil.closing;
+
+public class HotRodMapStorage implements MapStorage, ConcurrentHashMapCrudOperations {
+
+ private static final Logger LOG = Logger.getLogger(HotRodMapStorage.class);
+
+ private final RemoteCache remoteCache;
+ private final StringKeyConvertor keyConvertor;
+ private final HotRodEntityDescriptor storedEntityDescriptor;
+ private final DeepCloner cloner;
+
+ public HotRodMapStorage(RemoteCache remoteCache, StringKeyConvertor keyConvertor, HotRodEntityDescriptor storedEntityDescriptor, DeepCloner cloner) {
+ this.remoteCache = remoteCache;
+ this.keyConvertor = keyConvertor;
+ this.storedEntityDescriptor = storedEntityDescriptor;
+ this.cloner = cloner;
+ }
+
+ @Override
+ public V create(V value) {
+ K key = keyConvertor.fromStringSafe(value.getId());
+ if (key == null) {
+ key = keyConvertor.yieldNewUniqueKey();
+ value = cloner.from(keyConvertor.keyToString(key), value);
+ }
+
+ remoteCache.putIfAbsent(key, value);
+
+ return value;
+ }
+
+ @Override
+ public V read(String key) {
+ Objects.requireNonNull(key, "Key must be non-null");
+ K k = keyConvertor.fromStringSafe(key);
+ return remoteCache.get(k);
+ }
+
+ @Override
+ public V update(V value) {
+ K key = keyConvertor.fromStringSafe(value.getId());
+ return remoteCache.replace(key, value);
+ }
+
+ @Override
+ public boolean delete(String key) {
+ K k = keyConvertor.fromStringSafe(key);
+ return remoteCache.remove(k) != null;
+ }
+
+ private static String toOrderString(QueryParameters.OrderBy> orderBy) {
+ SearchableModelField> field = orderBy.getModelField();
+ String modelFieldName = IckleQueryMapModelCriteriaBuilder.getFieldName(field);
+ String orderString = orderBy.getOrder().equals(QueryParameters.Order.ASCENDING) ? "ASC" : "DESC";
+
+ return modelFieldName + " " + orderString;
+ }
+
+ @Override
+ public Stream read(QueryParameters queryParameters) {
+ IckleQueryMapModelCriteriaBuilder iqmcb = queryParameters.getModelCriteriaBuilder()
+ .flashToModelCriteriaBuilder(createCriteriaBuilder());
+ String queryString = iqmcb.getIckleQuery();
+
+ if (!queryParameters.getOrderBy().isEmpty()) {
+ queryString += " ORDER BY " + queryParameters.getOrderBy().stream().map(HotRodMapStorage::toOrderString)
+ .collect(Collectors.joining(", "));
+ }
+
+ LOG.tracef("Executing read Ickle query: %s", queryString);
+
+ QueryFactory queryFactory = Search.getQueryFactory(remoteCache);
+
+ Query query = paginateQuery(queryFactory.create(queryString), queryParameters.getOffset(),
+ queryParameters.getLimit());
+
+ query.setParameters(iqmcb.getParameters());
+
+ CloseableIterator iterator = query.iterator();
+ return closing(StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false))
+ .onClose(iterator::close);
+ }
+
+ @Override
+ public long getCount(QueryParameters queryParameters) {
+ IckleQueryMapModelCriteriaBuilder iqmcb = queryParameters.getModelCriteriaBuilder()
+ .flashToModelCriteriaBuilder(createCriteriaBuilder());
+ String queryString = iqmcb.getIckleQuery();
+
+ LOG.tracef("Executing count Ickle query: %s", queryString);
+
+ QueryFactory queryFactory = Search.getQueryFactory(remoteCache);
+
+ Query query = queryFactory.create(queryString);
+ query.setParameters(iqmcb.getParameters());
+
+ return query.execute().hitCount().orElse(0);
+ }
+
+ @Override
+ public long delete(QueryParameters queryParameters) {
+ IckleQueryMapModelCriteriaBuilder iqmcb = queryParameters.getModelCriteriaBuilder()
+ .flashToModelCriteriaBuilder(createCriteriaBuilder());
+ String queryString = "SELECT id " + iqmcb.getIckleQuery();
+
+ if (!queryParameters.getOrderBy().isEmpty()) {
+ queryString += " ORDER BY " + queryParameters.getOrderBy().stream().map(HotRodMapStorage::toOrderString)
+ .collect(Collectors.joining(", "));
+ }
+
+ LOG.tracef("Executing delete Ickle query: %s", queryString);
+
+ QueryFactory queryFactory = Search.getQueryFactory(remoteCache);
+
+ Query query = paginateQuery(queryFactory.create(queryString), queryParameters.getOffset(),
+ queryParameters.getLimit());
+
+ query.setParameters(iqmcb.getParameters());
+
+ AtomicLong result = new AtomicLong();
+
+ CloseableIterator iterator = query.iterator();
+ StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false)
+ .peek(e -> result.incrementAndGet())
+ .map(AbstractEntity::getId)
+ .forEach(this::delete);
+ iterator.close();
+
+ return result.get();
+ }
+
+ public IckleQueryMapModelCriteriaBuilder createCriteriaBuilder() {
+ return new IckleQueryMapModelCriteriaBuilder<>();
+ }
+
+ @Override
+ public MapKeycloakTransaction createTransaction(KeycloakSession session) {
+ Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates = MapFieldPredicates.getPredicates((Class) storedEntityDescriptor.getModelTypeClass());
+ return new ConcurrentHashMapKeycloakTransaction<>(this, keyConvertor, cloner, fieldPredicates);
+ }
+}
diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java
new file mode 100644
index 0000000000..1f4e94c062
--- /dev/null
+++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.map.storage.hotRod;
+
+import org.keycloak.models.map.common.AbstractEntity;
+import org.keycloak.models.map.common.DeepCloner;
+import org.keycloak.models.map.common.HotRodEntityDescriptor;
+import org.keycloak.models.map.common.StringKeyConvertor;
+import org.keycloak.models.map.common.UpdatableEntity;
+import org.keycloak.models.map.connections.HotRodConnectionProvider;
+import org.keycloak.models.map.storage.MapStorage;
+import org.keycloak.models.map.storage.MapStorageProvider;
+import org.keycloak.models.map.storage.MapStorageProviderFactory;
+
+public class HotRodMapStorageProvider implements MapStorageProvider {
+
+ private final HotRodMapStorageProviderFactory factory;
+ private final HotRodConnectionProvider connectionProvider;
+ private final DeepCloner cloner;
+
+ public HotRodMapStorageProvider(HotRodMapStorageProviderFactory factory, HotRodConnectionProvider connectionProvider, DeepCloner cloner) {
+ this.factory = factory;
+ this.connectionProvider = connectionProvider;
+ this.cloner = cloner;
+ }
+
+ @Override
+ public MapStorage getStorage(Class modelType, MapStorageProviderFactory.Flag... flags) {
+ HotRodMapStorage storage = getHotRodStorage(modelType, flags);
+ return storage;
+ }
+
+ @SuppressWarnings("unchecked")
+ public HotRodMapStorage getHotRodStorage(Class modelType, MapStorageProviderFactory.Flag... flags) {
+ HotRodEntityDescriptor entityDescriptor = (HotRodEntityDescriptor) factory.getEntityDescriptor(modelType);
+ return new HotRodMapStorage<>(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConvertor.StringKey.INSTANCE, entityDescriptor, cloner);
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java
new file mode 100644
index 0000000000..94c2d4ea47
--- /dev/null
+++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.map.storage.hotRod;
+
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.common.Profile;
+import org.keycloak.component.AmphibianProviderFactory;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.map.client.HotRodAttributeEntity;
+import org.keycloak.models.map.client.HotRodClientEntity;
+import org.keycloak.models.map.client.HotRodProtocolMapperEntity;
+import org.keycloak.models.map.common.HotRodPair;
+import org.keycloak.models.map.client.MapClientEntity;
+import org.keycloak.models.map.client.MapProtocolMapperEntity;
+import org.keycloak.models.map.common.DeepCloner;
+import org.keycloak.models.map.common.HotRodEntityDescriptor;
+import org.keycloak.models.map.connections.HotRodConnectionProvider;
+import org.keycloak.models.map.storage.MapStorageProvider;
+import org.keycloak.models.map.storage.MapStorageProviderFactory;
+import org.keycloak.provider.EnvironmentDependentProviderFactory;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory, MapStorageProviderFactory, EnvironmentDependentProviderFactory {
+
+ public static final String PROVIDER_ID = "hotrod";
+ private static final Logger LOG = Logger.getLogger(HotRodMapStorageProviderFactory.class);
+
+ private final static DeepCloner CLONER = new DeepCloner.Builder()
+ .constructorDC(MapClientEntity.class, HotRodClientEntity::new)
+ .constructor(MapProtocolMapperEntity.class, HotRodProtocolMapperEntity::new)
+ .build();
+
+ public static final Map, HotRodEntityDescriptor>> ENTITY_DESCRIPTOR_MAP = new HashMap<>();
+ static {
+ // Clients descriptor
+ ENTITY_DESCRIPTOR_MAP.put(ClientModel.class,
+ new HotRodEntityDescriptor<>(ClientModel.class,
+ MapClientEntity.class,
+ Arrays.asList(HotRodClientEntity.class, HotRodAttributeEntity.class, HotRodProtocolMapperEntity.class, HotRodPair.class),
+ "clients"));
+ }
+
+ @Override
+ public MapStorageProvider create(KeycloakSession session) {
+ HotRodConnectionProvider cacheProvider = session.getProvider(HotRodConnectionProvider.class);
+
+ if (cacheProvider == null) {
+ throw new IllegalStateException("Cannot find HotRodConnectionProvider interface implementation");
+ }
+
+ return new HotRodMapStorageProvider(this, cacheProvider, CLONER);
+ }
+
+ public HotRodEntityDescriptor> getEntityDescriptor(Class> c) {
+ return ENTITY_DESCRIPTOR_MAP.get(c);
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public boolean isSupported() {
+ return Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE);
+ }
+
+ @Override
+ public String getHelpText() {
+ return "HotRod client storage";
+ }
+}
diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java
new file mode 100644
index 0000000000..07cbc4b355
--- /dev/null
+++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.map.storage.hotRod;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.map.common.AbstractEntity;
+import org.keycloak.models.map.storage.ModelCriteriaBuilder;
+import org.keycloak.storage.SearchableModelField;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.keycloak.models.map.storage.hotRod.IckleQueryOperators.C;
+import static org.keycloak.models.map.storage.hotRod.IckleQueryOperators.findAvailableNamedParam;
+
+public class IckleQueryMapModelCriteriaBuilder implements ModelCriteriaBuilder> {
+
+ private static final int INITIAL_BUILDER_CAPACITY = 250;
+ private final StringBuilder whereClauseBuilder = new StringBuilder(INITIAL_BUILDER_CAPACITY);
+ private final Map parameters;
+ public static final Map, String> INFINISPAN_NAME_OVERRIDES = new HashMap<>();
+
+ static {
+ INFINISPAN_NAME_OVERRIDES.put(ClientModel.SearchableFields.SCOPE_MAPPING_ROLE, "scopeMappings");
+ INFINISPAN_NAME_OVERRIDES.put(ClientModel.SearchableFields.ATTRIBUTE, "attributes");
+ }
+
+ public IckleQueryMapModelCriteriaBuilder(StringBuilder whereClauseBuilder, Map parameters) {
+ this.whereClauseBuilder.append(whereClauseBuilder);
+ this.parameters = parameters;
+ }
+
+ public IckleQueryMapModelCriteriaBuilder() {
+ this.parameters = new HashMap<>();
+ }
+
+ public static String getFieldName(SearchableModelField> modelField) {
+ return INFINISPAN_NAME_OVERRIDES.getOrDefault(modelField, modelField.getName());
+ }
+
+ private static boolean notEmpty(StringBuilder builder) {
+ return builder.length() != 0;
+ }
+
+ @Override
+ public IckleQueryMapModelCriteriaBuilder compare(SearchableModelField super M> modelField, Operator op, Object... value) {
+ StringBuilder newBuilder = new StringBuilder(INITIAL_BUILDER_CAPACITY);
+ newBuilder.append("(");
+
+ if (notEmpty(whereClauseBuilder)) {
+ newBuilder.append(whereClauseBuilder).append(" AND (");
+ }
+
+ Map newParameters = new HashMap<>(parameters);
+ newBuilder.append(IckleQueryWhereClauses.produceWhereClause(modelField, op, value, newParameters));
+
+ if (notEmpty(whereClauseBuilder)) {
+ newBuilder.append(")");
+ }
+
+ return new IckleQueryMapModelCriteriaBuilder<>(newBuilder.append(")"), newParameters);
+ }
+
+ private StringBuilder joinBuilders(IckleQueryMapModelCriteriaBuilder[] builders, String delimiter) {
+ return new StringBuilder(INITIAL_BUILDER_CAPACITY).append("(").append(Arrays.stream(builders)
+ .map(IckleQueryMapModelCriteriaBuilder::getWhereClauseBuilder)
+ .filter(IckleQueryMapModelCriteriaBuilder::notEmpty)
+ .collect(Collectors.joining(delimiter))).append(")");
+ }
+
+ private Map joinParameters(IckleQueryMapModelCriteriaBuilder[] builders) {
+ return Arrays.stream(builders)
+ .map(IckleQueryMapModelCriteriaBuilder::getParameters)
+ .map(Map::entrySet)
+ .flatMap(Collection::stream)
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ }
+
+ @SuppressWarnings("unchecked")
+ private IckleQueryMapModelCriteriaBuilder[] resolveNamedQueryConflicts(IckleQueryMapModelCriteriaBuilder[] builders) {
+ final Set existingKeys = new HashSet<>();
+
+ return Arrays.stream(builders).map(builder -> {
+ Map oldParameters = builder.getParameters();
+
+ if (oldParameters.keySet().stream().noneMatch(existingKeys::contains)) {
+ existingKeys.addAll(oldParameters.keySet());
+ return builder;
+ }
+
+ String newWhereClause = builder.getWhereClauseBuilder().toString();
+ Map newParameters = new HashMap<>();
+ for (String key : oldParameters.keySet()) {
+ if (existingKeys.contains(key)) {
+ // resolve conflict
+ String newNamedParameter = findAvailableNamedParam(existingKeys, key + "n");
+ newParameters.put(newNamedParameter, oldParameters.get(key));
+ newWhereClause = newWhereClause.replace(key, newNamedParameter);
+ existingKeys.add(newNamedParameter);
+ } else {
+ newParameters.put(key, oldParameters.get(key));
+ existingKeys.add(key);
+ }
+ }
+
+ return new IckleQueryMapModelCriteriaBuilder<>(new StringBuilder(newWhereClause), newParameters);
+ }).toArray(IckleQueryMapModelCriteriaBuilder[]::new);
+ }
+
+ @Override
+ public IckleQueryMapModelCriteriaBuilder and(IckleQueryMapModelCriteriaBuilder... builders) {
+ if (builders.length == 0) {
+ return new IckleQueryMapModelCriteriaBuilder<>();
+ }
+
+ builders = resolveNamedQueryConflicts(builders);
+
+ return new IckleQueryMapModelCriteriaBuilder<>(joinBuilders(builders, " AND "),
+ joinParameters(builders));
+ }
+
+ @Override
+ public IckleQueryMapModelCriteriaBuilder or(IckleQueryMapModelCriteriaBuilder... builders) {
+ if (builders.length == 0) {
+ return new IckleQueryMapModelCriteriaBuilder<>();
+ }
+
+ builders = resolveNamedQueryConflicts(builders);
+
+ return new IckleQueryMapModelCriteriaBuilder<>(joinBuilders(builders, " OR "),
+ joinParameters(builders));
+ }
+
+ @Override
+ public IckleQueryMapModelCriteriaBuilder not(IckleQueryMapModelCriteriaBuilder builder) {
+ StringBuilder newBuilder = new StringBuilder(INITIAL_BUILDER_CAPACITY);
+ StringBuilder originalBuilder = builder.getWhereClauseBuilder();
+
+ if (originalBuilder.length() != 0) {
+ newBuilder.append("not").append(originalBuilder);
+ }
+
+ return new IckleQueryMapModelCriteriaBuilder<>(newBuilder, builder.getParameters());
+ }
+
+ private StringBuilder getWhereClauseBuilder() {
+ return whereClauseBuilder;
+ }
+
+ /**
+ *
+ * @return Ickle query that represents this QueryBuilder
+ */
+ public String getIckleQuery() {
+ return "FROM org.keycloak.models.map.storage.hotrod.HotRodClientEntity " + C + ((whereClauseBuilder.length() != 0) ? " WHERE " + whereClauseBuilder : "");
+ }
+
+ /**
+ * Ickle queries are created using named parameters to avoid query injections; this method provides mapping
+ * between parameter names and corresponding values
+ *
+ * @return Mapping from name of the parameter to value
+ */
+ public Map getParameters() {
+ return parameters;
+ }
+}
diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryOperators.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryOperators.java
new file mode 100644
index 0000000000..34202dfbd4
--- /dev/null
+++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryOperators.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.map.storage.hotRod;
+
+import org.keycloak.models.map.storage.ModelCriteriaBuilder;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+/**
+ * This class provides knowledge on how to build Ickle query where clauses for specified {@link ModelCriteriaBuilder.Operator}.
+ *
+ * For example,
+ *
+ * for operator {@link ModelCriteriaBuilder.Operator.EQ} we concatenate left operand and right operand with equal sign:
+ * {@code fieldName = :parameterName}
+ *
+ * however, for operator {@link ModelCriteriaBuilder.Operator.EXISTS} we add following:
+ *
+ * {@code fieldName IS NOT NULL AND fieldName IS NOT EMPTY"}.
+ *
+ * For right side operands we use named parameters to avoid injection attacks. Mapping between named parameter and
+ * corresponding value is then saved into {@code Map} that is passed to each {@link ExpressionCombinator}.
+ */
+public class IckleQueryOperators {
+ private static final String UNWANTED_CHARACTERS_REGEX = "[^a-zA-Z\\d]";
+ public static final String C = "c";
+ private static final Map OPERATOR_TO_STRING = new HashMap<>();
+ private static final Map OPERATOR_TO_EXPRESSION_COMBINATORS = new HashMap<>();
+
+ static {
+ OPERATOR_TO_EXPRESSION_COMBINATORS.put(ModelCriteriaBuilder.Operator.IN, IckleQueryOperators::in);
+ OPERATOR_TO_EXPRESSION_COMBINATORS.put(ModelCriteriaBuilder.Operator.EXISTS, IckleQueryOperators::exists);
+ OPERATOR_TO_EXPRESSION_COMBINATORS.put(ModelCriteriaBuilder.Operator.NOT_EXISTS, IckleQueryOperators::notExists);
+
+ OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.EQ, "=");
+ OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.NE, "!=");
+ OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.LT, "<");
+ OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.LE, "<=");
+ OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.GT, ">");
+ OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.GE, ">=");
+ OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.LIKE, "LIKE");
+ OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.ILIKE, "LIKE");
+ OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.IN, "IN");
+ }
+
+ @FunctionalInterface
+ private interface ExpressionCombinator {
+
+ /**
+ * Produces an Ickle query where clause for obtained parameters
+ *
+ * @param fieldName left side operand
+ * @param values right side operands
+ * @param parameters mapping between named parameters and actual parameter values
+ * @return resulting string that will be part of resulting
+ */
+ String combine(String fieldName, Object[] values, Map parameters);
+ }
+
+ private static String exists(String modelField, Object[] values, Map parameters) {
+ String field = C + "." + modelField;
+ return field + " IS NOT NULL AND " + field + " IS NOT EMPTY";
+ }
+
+ private static String notExists(String modelField, Object[] values, Map parameters) {
+ String field = C + "." + modelField;
+ return field + " IS NULL OR " + field + " IS EMPTY";
+ }
+
+ private static String in(String modelField, Object[] values, Map parameters) {
+ if (values == null || values.length == 0) {
+ return "false";
+ }
+
+ final Collection> operands;
+ if (values.length == 1) {
+ final Object value0 = values[0];
+ if (value0 instanceof Collection) {
+ operands = (Collection) value0;
+ } else if (value0 instanceof Stream) {
+ try (Stream valueS = (Stream) value0) {
+ operands = (Set) valueS.collect(Collectors.toSet());
+ }
+ } else {
+ operands = Collections.singleton(value0);
+ }
+ } else {
+ operands = new HashSet<>(Arrays.asList(values));
+ }
+
+ return C + "." + modelField + " IN (" + operands.stream()
+ .map(operand -> {
+ String namedParam = findAvailableNamedParam(parameters.keySet(), modelField);
+ parameters.put(namedParam, operand);
+ return ":" + namedParam;
+ })
+ .collect(Collectors.joining(", ")) +
+ ")";
+ }
+
+ private static String removeForbiddenCharactersFromNamedParameter(String name) {
+ return name.replaceAll(UNWANTED_CHARACTERS_REGEX, "");
+ }
+
+ /**
+ * Maps {@code namePrefix} to next available parameter name. For example, if {@code namePrefix == "id"}
+ * and {@code existingNames} set already contains {@code id0} and {@code id1} it returns {@code id2}.
+ *
+ * This method is used for computing available names for name query parameters
+ *
+ * @param existingNames set of parameter names that are already used in this Ickle query
+ * @param namePrefix name of the parameter
+ * @return next available parameter name
+ */
+ public static String findAvailableNamedParam(Set existingNames, String namePrefix) {
+ String namePrefixCleared = removeForbiddenCharactersFromNamedParameter(namePrefix);
+ return IntStream.iterate(0, i -> i + 1)
+ .boxed()
+ .map(num -> namePrefixCleared + num)
+ .filter(name -> !existingNames.contains(name))
+ .findFirst().orElseThrow(() -> new IllegalArgumentException("Cannot create Parameter name for " + namePrefix));
+ }
+
+ private static ExpressionCombinator singleValueOperator(ModelCriteriaBuilder.Operator op) {
+ return (modelFieldName, values, parameters) -> {
+ if (values.length != 1) throw new RuntimeException("Invalid arguments, expected (" + modelFieldName + "), got: " + Arrays.toString(values));
+
+ String namedParameter = findAvailableNamedParam(parameters.keySet(), modelFieldName);
+
+ parameters.put(namedParameter, values[0]);
+ return C + "." + modelFieldName + " " + IckleQueryOperators.operatorToString(op) + " :" + namedParameter;
+ };
+ }
+
+ private static String operatorToString(ModelCriteriaBuilder.Operator op) {
+ return OPERATOR_TO_STRING.get(op);
+ }
+
+ private static ExpressionCombinator operatorToExpressionCombinator(ModelCriteriaBuilder.Operator op) {
+ return OPERATOR_TO_EXPRESSION_COMBINATORS.getOrDefault(op, singleValueOperator(op));
+ }
+
+ /**
+ * Provides a string containing where clause for given operator, field name and values
+ *
+ * @param op operator
+ * @param filedName field name
+ * @param values values
+ * @param parameters mapping between named parameters and their values
+ * @return where clause
+ */
+ public static String combineExpressions(ModelCriteriaBuilder.Operator op, String filedName, Object[] values, Map parameters) {
+ return operatorToExpressionCombinator(op).combine(filedName, values, parameters);
+ }
+
+}
\ No newline at end of file
diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryWhereClauses.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryWhereClauses.java
new file mode 100644
index 0000000000..6bcafa853d
--- /dev/null
+++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryWhereClauses.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.map.storage.hotRod;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.map.storage.CriterionNotSupportedException;
+import org.keycloak.models.map.storage.ModelCriteriaBuilder;
+import org.keycloak.storage.SearchableModelField;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This class provides knowledge on how to build Ickle query where clauses for specified {@link SearchableModelField}.
+ *
+ * For example,
+ *
+ * for {@link ClientModel.SearchableFields.CLIENT_ID} we use {@link IckleQueryOperators.ExpressionCombinator} for
+ * obtained {@link ModelCriteriaBuilder.Operator} and use it with field name corresponding to {@link ClientModel.SearchableFields.CLIENT_ID}
+ *
+ * however, for {@link ClientModel.SearchableFields.ATTRIBUTE} we need to compare attribute name and attribute value
+ * so we create where clause similar to the following:
+ * {@code (attributes.name = :attributeName) AND ( attributes.value = :attributeValue )}
+ *
+ *
+ */
+public class IckleQueryWhereClauses {
+ private static final Map, WhereClauseProducer> WHERE_CLAUSE_PRODUCER_OVERRIDES = new HashMap<>();
+
+ static {
+ WHERE_CLAUSE_PRODUCER_OVERRIDES.put(ClientModel.SearchableFields.ATTRIBUTE, IckleQueryWhereClauses::whereClauseForClientsAttributes);
+ }
+
+ @FunctionalInterface
+ private interface WhereClauseProducer {
+ String produceWhereClause(String modelFieldName, ModelCriteriaBuilder.Operator op, Object[] values, Map parameters);
+ }
+
+ private static String produceWhereClause(String modelFieldName, ModelCriteriaBuilder.Operator op, Object[] values, Map parameters) {
+ return IckleQueryOperators.combineExpressions(op, modelFieldName, values, parameters);
+ }
+
+ private static WhereClauseProducer whereClauseProducerForModelField(SearchableModelField> modelField) {
+ return WHERE_CLAUSE_PRODUCER_OVERRIDES.getOrDefault(modelField, IckleQueryWhereClauses::produceWhereClause);
+ }
+
+ /**
+ * Produces where clause for given {@link SearchableModelField}, {@link ModelCriteriaBuilder.Operator} and values
+ *
+ * @param modelField model field
+ * @param op operator
+ * @param values searched values
+ * @param parameters mapping between named parameters and corresponding values
+ * @return resulting where clause
+ */
+ public static String produceWhereClause(SearchableModelField> modelField, ModelCriteriaBuilder.Operator op,
+ Object[] values, Map parameters) {
+ return whereClauseProducerForModelField(modelField)
+ .produceWhereClause(IckleQueryMapModelCriteriaBuilder.getFieldName(modelField), op, values, parameters);
+ }
+
+ private static String whereClauseForClientsAttributes(String modelFieldName, ModelCriteriaBuilder.Operator op, Object[] values, Map parameters) {
+ if (values == null || values.length != 2) {
+ throw new CriterionNotSupportedException(ClientModel.SearchableFields.ATTRIBUTE, op, "Invalid arguments, expected attribute_name-value pair, got: " + Arrays.toString(values));
+ }
+
+ final Object attrName = values[0];
+ if (! (attrName instanceof String)) {
+ throw new CriterionNotSupportedException(ClientModel.SearchableFields.ATTRIBUTE, op, "Invalid arguments, expected (String attribute_name), got: " + Arrays.toString(values));
+ }
+
+ String attrNameS = (String) attrName;
+ Object[] realValues = new Object[values.length - 1];
+ System.arraycopy(values, 1, realValues, 0, values.length - 1);
+
+ // Clause for searching attribute name
+ String nameClause = IckleQueryOperators.combineExpressions(ModelCriteriaBuilder.Operator.EQ, modelFieldName + ".name", new Object[]{attrNameS}, parameters);
+ // Clause for searching attribute value
+ String valueClause = IckleQueryOperators.combineExpressions(op, modelFieldName + ".values", realValues, parameters);
+
+ return "(" + nameClause + ")" + " AND " + "(" + valueClause + ")";
+ }
+}
diff --git a/model/hot-rod/src/main/resources/META-INF/services/org.keycloak.models.map.connections.HotRodConnectionProviderFactory b/model/map-hot-rod/src/main/resources/META-INF/services/org.keycloak.models.map.connections.HotRodConnectionProviderFactory
similarity index 100%
rename from model/hot-rod/src/main/resources/META-INF/services/org.keycloak.models.map.connections.HotRodConnectionProviderFactory
rename to model/map-hot-rod/src/main/resources/META-INF/services/org.keycloak.models.map.connections.HotRodConnectionProviderFactory
diff --git a/model/map-hot-rod/src/main/resources/META-INF/services/org.keycloak.models.map.storage.MapStorageProviderFactory b/model/map-hot-rod/src/main/resources/META-INF/services/org.keycloak.models.map.storage.MapStorageProviderFactory
new file mode 100644
index 0000000000..132921e337
--- /dev/null
+++ b/model/map-hot-rod/src/main/resources/META-INF/services/org.keycloak.models.map.storage.MapStorageProviderFactory
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory
diff --git a/model/hot-rod/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/map-hot-rod/src/main/resources/META-INF/services/org.keycloak.provider.Spi
similarity index 100%
rename from model/hot-rod/src/main/resources/META-INF/services/org.keycloak.provider.Spi
rename to model/map-hot-rod/src/main/resources/META-INF/services/org.keycloak.provider.Spi
diff --git a/model/hot-rod/src/main/resources/config/cacheConfig.xml b/model/map-hot-rod/src/main/resources/config/cacheConfig.xml
similarity index 100%
rename from model/hot-rod/src/main/resources/config/cacheConfig.xml
rename to model/map-hot-rod/src/main/resources/config/cacheConfig.xml
diff --git a/model/hot-rod/src/main/resources/config/infinispan.xml b/model/map-hot-rod/src/main/resources/config/infinispan.xml
similarity index 100%
rename from model/hot-rod/src/main/resources/config/infinispan.xml
rename to model/map-hot-rod/src/main/resources/config/infinispan.xml
diff --git a/model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilderTest.java b/model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilderTest.java
new file mode 100644
index 0000000000..6ea10cc6cf
--- /dev/null
+++ b/model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilderTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.map.storage.hotRod;
+
+import org.junit.Test;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.map.client.HotRodClientEntity;
+import org.keycloak.models.map.storage.ModelCriteriaBuilder;
+import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.keycloak.models.ClientModel.SearchableFields.CLIENT_ID;
+import static org.keycloak.models.ClientModel.SearchableFields.ID;
+import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.criteria;
+
+public class IckleQueryMapModelCriteriaBuilderTest {
+ @Test
+ public void testSimpleIckleQuery() {
+ IckleQueryMapModelCriteriaBuilder v = new IckleQueryMapModelCriteriaBuilder<>();
+ IckleQueryMapModelCriteriaBuilder mcb = v.compare(CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, 3);
+ assertThat(mcb.getIckleQuery(), is(equalTo("FROM org.keycloak.models.map.storage.hotrod.HotRodClientEntity c WHERE (c.clientId = :clientId0)")));
+ assertThat(mcb.getParameters().entrySet(), hasSize(1));
+ assertThat(mcb.getParameters(), hasEntry("clientId0", 3));
+
+
+ mcb = v.compare(CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, 4)
+ .compare(ID, ModelCriteriaBuilder.Operator.EQ, 5);
+
+ assertThat(mcb.getIckleQuery(), is(equalTo("FROM org.keycloak.models.map.storage.hotrod.HotRodClientEntity c WHERE ((c.clientId = :clientId0) AND (c.id = :id0))")));
+ assertThat(mcb.getParameters().entrySet(), hasSize(2));
+ assertThat(mcb.getParameters(), allOf(hasEntry("clientId0", 4), hasEntry("id0", 5)));
+ }
+
+
+ @Test
+ public void testSimpleIckleQueryFlashedFromDefault() {
+ DefaultModelCriteria v = criteria();
+ IckleQueryMapModelCriteriaBuilder mcb = v.compare(CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, 3).flashToModelCriteriaBuilder(new IckleQueryMapModelCriteriaBuilder<>());
+ assertThat(mcb.getIckleQuery(), is(equalTo("FROM org.keycloak.models.map.storage.hotrod.HotRodClientEntity c WHERE (c.clientId = :clientId0)")));
+ assertThat(mcb.getParameters().entrySet(), hasSize(1));
+ assertThat(mcb.getParameters(), hasEntry("clientId0", 3));
+
+
+ mcb = v.compare(CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, 4)
+ .compare(ID, ModelCriteriaBuilder.Operator.EQ, 5).flashToModelCriteriaBuilder(new IckleQueryMapModelCriteriaBuilder<>());
+
+ assertThat(mcb.getIckleQuery(), is(equalTo("FROM org.keycloak.models.map.storage.hotrod.HotRodClientEntity c WHERE ((c.clientId = :clientId0) AND (c.id = :id0))")));
+ assertThat(mcb.getParameters().entrySet(), hasSize(2));
+ assertThat(mcb.getParameters(), allOf(hasEntry("clientId0", 4), hasEntry("id0", 5)));
+ }
+}
\ No newline at end of file
diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapCrudOperations.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapCrudOperations.java
new file mode 100644
index 0000000000..1b2a9d87f7
--- /dev/null
+++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapCrudOperations.java
@@ -0,0 +1,73 @@
+package org.keycloak.models.map.storage.chm;
+
+import org.keycloak.models.map.common.AbstractEntity;
+import org.keycloak.models.map.common.UpdatableEntity;
+import org.keycloak.models.map.storage.ModelCriteriaBuilder;
+import org.keycloak.models.map.storage.QueryParameters;
+
+
+import java.util.stream.Stream;
+
+public interface ConcurrentHashMapCrudOperations {
+ /**
+ * Creates an object in the store. ID of the {@code value} may be prescribed in id of the {@code value}.
+ * If the id is {@code null} or its format is not matching the store internal format for ID, then
+ * the {@code value}'s ID will be generated and returned in the id of the return value.
+ * @param value Entity to create in the store
+ * @throws NullPointerException if {@code value} is {@code null}
+ * @see AbstractEntity#getId()
+ * @return Entity representing the {@code value} in the store. It may or may not be the same instance as {@code value}
+ */
+ V create(V value);
+
+ /**
+ * Returns object with the given {@code key} from the storage or {@code null} if object does not exist.
+ *
+ * TODO: Consider returning {@code Optional} instead.
+ * @param key Key of the object. Must not be {@code null}.
+ * @return See description
+ * @throws NullPointerException if the {@code key} is {@code null}
+ */
+ public V read(String key);
+
+ /**
+ * Updates the object with the key of the {@code value}'s ID in the storage if it already exists.
+ *
+ * @param value Updated value
+ * @throws NullPointerException if the object or its {@code id} is {@code null}
+ * @see AbstractEntity#getId()
+ */
+ V update(V value);
+
+ /**
+ * Deletes object with the given {@code key} from the storage, if exists, no-op otherwise.
+ * @param key
+ * @return Returns {@code true} if the object has been deleted or result cannot be determined, {@code false} otherwise.
+ */
+ boolean delete(String key);
+
+ /**
+ * Deletes objects that match the given criteria.
+ * @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc.
+ * @return Number of removed objects (might return {@code -1} if not supported)
+ */
+ long delete(QueryParameters queryParameters);
+
+ /**
+ * Returns stream of objects satisfying given {@code criteria} from the storage.
+ * The criteria are specified in the given criteria builder based on model properties.
+ *
+ * @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc.
+ * @return Stream of objects. Never returns {@code null}.
+ */
+ Stream read(QueryParameters queryParameters);
+
+ /**
+ * Returns the number of objects satisfying given {@code criteria} from the storage.
+ * The criteria are specified in the given criteria builder based on model properties.
+ *
+ * @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc.
+ * @return Number of objects. Never returns {@code null}.
+ */
+ long getCount(QueryParameters queryParameters);
+}
diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java
index 7913805a1a..547c91c3de 100644
--- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java
+++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java
@@ -32,8 +32,9 @@ import java.util.stream.Stream;
import org.jboss.logging.Logger;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.QueryParameters;
+import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder.UpdatePredicatesFunc;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
-import org.keycloak.utils.StreamsUtil;
+import org.keycloak.storage.SearchableModelField;
public class ConcurrentHashMapKeycloakTransaction implements MapKeycloakTransaction {
@@ -42,18 +43,20 @@ public class ConcurrentHashMapKeycloakTransaction tasks = new LinkedHashMap<>();
- protected final ConcurrentHashMapStorage map;
+ protected final ConcurrentHashMapCrudOperations map;
protected final StringKeyConvertor keyConvertor;
protected final DeepCloner cloner;
+ protected final Map, UpdatePredicatesFunc> fieldPredicates;
enum MapOperation {
CREATE, UPDATE, DELETE,
}
- public ConcurrentHashMapKeycloakTransaction(ConcurrentHashMapStorage map, StringKeyConvertor keyConvertor, DeepCloner cloner) {
+ public ConcurrentHashMapKeycloakTransaction(ConcurrentHashMapCrudOperations map, StringKeyConvertor keyConvertor, DeepCloner cloner, Map, UpdatePredicatesFunc> fieldPredicates) {
this.map = map;
this.keyConvertor = keyConvertor;
this.cloner = cloner;
+ this.fieldPredicates = fieldPredicates;
}
@Override
@@ -95,6 +98,10 @@ public class ConcurrentHashMapKeycloakTransaction createCriteriaBuilder() {
+ return new MapModelCriteriaBuilder(keyConvertor, fieldPredicates);
+ }
+
/**
* Adds a given task if not exists for the given key
*/
@@ -168,7 +175,7 @@ public class ConcurrentHashMapKeycloakTransaction read(QueryParameters queryParameters) {
DefaultModelCriteria mcb = queryParameters.getModelCriteriaBuilder();
- MapModelCriteriaBuilder mapMcb = mcb.flashToModelCriteriaBuilder(map.createCriteriaBuilder());
+ MapModelCriteriaBuilder mapMcb = mcb.flashToModelCriteriaBuilder(createCriteriaBuilder());
Predicate super V> filterOutAllBulkDeletedObjects = tasks.values().stream()
.filter(BulkDeleteOperation.class::isInstance)
@@ -196,7 +203,7 @@ public class ConcurrentHashMapKeycloakTransaction queryParameters) {
log.tracef("Adding operation DELETE_BULK");
@@ -401,7 +407,7 @@ public class ConcurrentHashMapKeycloakTransaction getFilterForNonDeletedObjects() {
DefaultModelCriteria mcb = queryParameters.getModelCriteriaBuilder();
- MapModelCriteriaBuilder mmcb = mcb.flashToModelCriteriaBuilder(map.createCriteriaBuilder());
+ MapModelCriteriaBuilder mmcb = mcb.flashToModelCriteriaBuilder(createCriteriaBuilder());
Predicate super V> entityFilter = mmcb.getEntityFilter();
Predicate super K> keyFilter = mmcb.getKeyFilter();
diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorage.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorage.java
index 93e0097874..44935a91ec 100644
--- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorage.java
+++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorage.java
@@ -49,7 +49,7 @@ import static org.keycloak.utils.StreamsUtil.paginatedStream;
*
* @author hmlnarik
*/
-public class ConcurrentHashMapStorage implements MapStorage {
+public class ConcurrentHashMapStorage implements MapStorage, ConcurrentHashMapCrudOperations {
protected final ConcurrentMap store = new ConcurrentHashMap<>();
@@ -64,15 +64,7 @@ public class ConcurrentHashMapStorage
- * TODO: Consider returning {@code Optional} instead.
- * @param key Key of the object. Must not be {@code null}.
- * @return See description
- * @throws NullPointerException if the {@code key} is {@code null}
- */
+ @Override
public V read(String key) {
Objects.requireNonNull(key, "Key must be non-null");
K k = keyConvertor.fromStringSafe(key);
return store.get(k);
}
- /**
- * Updates the object with the key of the {@code value}'s ID in the storage if it already exists.
- *
- * @param value Updated value
- * @throws NullPointerException if the object or its {@code id} is {@code null}
- * @see AbstractEntity#getId()
- */
+ @Override
public V update(V value) {
K key = getKeyConvertor().fromStringSafe(value.getId());
return store.replace(key, value);
}
- /**
- * Deletes object with the given {@code key} from the storage, if exists, no-op otherwise.
- * @param key
- * @return Returns {@code true} if the object has been deleted or result cannot be determined, {@code false} otherwise.
- */
+ @Override
public boolean delete(String key) {
K k = getKeyConvertor().fromStringSafe(key);
return store.remove(k) != null;
}
- /**
- * Deletes objects that match the given criteria.
- * @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc.
- * @return Number of removed objects (might return {@code -1} if not supported)
- */
+ @Override
public long delete(QueryParameters queryParameters) {
DefaultModelCriteria criteria = queryParameters.getModelCriteriaBuilder();
@@ -158,7 +129,7 @@ public class ConcurrentHashMapStorage createTransaction(KeycloakSession session) {
MapKeycloakTransaction sessionTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class);
- return sessionTransaction == null ? new ConcurrentHashMapKeycloakTransaction<>(this, keyConvertor, cloner) : sessionTransaction;
+ return sessionTransaction == null ? new ConcurrentHashMapKeycloakTransaction<>(this, keyConvertor, cloner, fieldPredicates) : sessionTransaction;
}
public MapModelCriteriaBuilder createCriteriaBuilder() {
@@ -169,13 +140,7 @@ public class ConcurrentHashMapStorage read(QueryParameters queryParameters) {
DefaultModelCriteria criteria = queryParameters.getModelCriteriaBuilder();
@@ -188,18 +153,17 @@ public class ConcurrentHashMapStorage keyFilter = mcb.getKeyFilter();
Predicate super V> entityFilter = mcb.getEntityFilter();
- stream = stream.filter(me -> keyFilter.test(me.getKey()) && entityFilter.test(me.getValue()));
+ Stream valueStream = stream.filter(me -> keyFilter.test(me.getKey()) && entityFilter.test(me.getValue()))
+ .map(Map.Entry::getValue);
- return stream.map(Map.Entry::getValue);
+ if (!queryParameters.getOrderBy().isEmpty()) {
+ valueStream = valueStream.sorted(MapFieldPredicates.getComparator(queryParameters.getOrderBy().stream()));
+ }
+
+ return paginatedStream(valueStream, queryParameters.getOffset(), queryParameters.getLimit());
}
- /**
- * Returns the number of objects satisfying given {@code criteria} from the storage.
- * The criteria are specified in the given criteria builder based on model properties.
- *
- * @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc.
- * @return Number of objects. Never returns {@code null}.
- */
+ @Override
public long getCount(QueryParameters queryParameters) {
return read(queryParameters).count();
}
diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java
index 0ebf1a07b6..95293f46c8 100644
--- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java
+++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java
@@ -25,9 +25,13 @@ import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator;
import org.keycloak.models.map.storage.QueryParameters;
+import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder.UpdatePredicatesFunc;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity;
import org.keycloak.models.map.userSession.MapUserSessionEntity;
+import org.keycloak.storage.SearchableModelField;
+
+import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@@ -47,8 +51,14 @@ public class UserSessionConcurrentHashMapStorage extends ConcurrentHashMapSto
private final MapKeycloakTransaction clientSessionTr;
- public Transaction(MapKeycloakTransaction clientSessionTr, StringKeyConvertor keyConvertor, DeepCloner cloner) {
- super(UserSessionConcurrentHashMapStorage.this, keyConvertor, cloner);
+ public Transaction(MapKeycloakTransaction clientSessionTr,
+ StringKeyConvertor keyConvertor,
+ DeepCloner cloner,
+ Map,
+ UpdatePredicatesFunc> fieldPredicates) {
+ super(UserSessionConcurrentHashMapStorage.this, keyConvertor, cloner, fieldPredicates);
this.clientSessionTr = clientSessionTr;
}
@@ -82,6 +92,6 @@ public class UserSessionConcurrentHashMapStorage extends ConcurrentHashMapSto
@SuppressWarnings("unchecked")
public MapKeycloakTransaction createTransaction(KeycloakSession session) {
MapKeycloakTransaction sessionTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class);
- return sessionTransaction == null ? new Transaction(clientSessionStore.createTransaction(session), clientSessionStore.getKeyConvertor(), cloner) : sessionTransaction;
+ return sessionTransaction == null ? new Transaction(clientSessionStore.createTransaction(session), clientSessionStore.getKeyConvertor(), cloner, fieldPredicates) : sessionTransaction;
}
}
diff --git a/model/pom.xml b/model/pom.xml
index a3932630ce..e2af09a965 100755
--- a/model/pom.xml
+++ b/model/pom.xml
@@ -35,6 +35,6 @@
infinispan
map
build-processor
- hot-rod
+ map-hot-rod
diff --git a/pom.xml b/pom.xml
index d9e413a695..21e7972cdc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -909,6 +909,21 @@
infinispan-jboss-marshalling
${infinispan.version}
+
+ org.infinispan
+ infinispan-client-hotrod
+ ${infinispan.version}
+
+
+ org.infinispan
+ infinispan-query-dsl
+ ${infinispan.version}
+
+
+ org.infinispan
+ infinispan-remote-query-client
+ ${infinispan.version}
+
org.infinispan
infinispan-server-core
diff --git a/testsuite/integration-arquillian/servers/cache-server/infinispan/pom.xml b/testsuite/integration-arquillian/servers/cache-server/infinispan/pom.xml
index ac336a199d..d9ec9785c1 100644
--- a/testsuite/integration-arquillian/servers/cache-server/infinispan/pom.xml
+++ b/testsuite/integration-arquillian/servers/cache-server/infinispan/pom.xml
@@ -255,6 +255,8 @@
myuser
-p
"qwer1234!"
+ -g
+ admin
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
index f70c4aeecb..d08bea17d3 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
@@ -141,6 +141,10 @@ public class AuthServerTestEnricher {
public static final String AUTH_SERVER_CROSS_DC_PROPERTY = "auth.server.crossdc";
public static final boolean AUTH_SERVER_CROSS_DC = Boolean.parseBoolean(System.getProperty(AUTH_SERVER_CROSS_DC_PROPERTY, "false"));
+
+ public static final String HOT_ROD_STORE_ENABLED_PROPERTY = "hotrod.store.enabled";
+ public static final boolean HOT_ROD_STORE_ENABLED = Boolean.parseBoolean(System.getProperty(HOT_ROD_STORE_ENABLED_PROPERTY, "false"));
+
public static final String AUTH_SERVER_HOME_PROPERTY = "auth.server.home";
public static final String CACHE_SERVER_LIFECYCLE_SKIP_PROPERTY = "cache.server.lifecycle.skip";
@@ -345,6 +349,17 @@ public class AuthServerTestEnricher {
}
}
+ if (HOT_ROD_STORE_ENABLED) {
+ HotRodStoreTestEnricher.initializeSuiteContext(suiteContext);
+
+ for (ContainerInfo container : suiteContext.getContainers()) {
+ // migrated auth server
+ if (container.getQualifier().equals("hot-rod-store")) {
+ suiteContext.setHotRodStoreInfo(container);
+ }
+ }
+ }
+
suiteContextProducer.set(suiteContext);
CrossDCTestEnricher.initializeSuiteContext(suiteContext);
log.info("\n\n" + suiteContext);
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/HotRodStoreTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/HotRodStoreTestEnricher.java
new file mode 100644
index 0000000000..293ba5a033
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/HotRodStoreTestEnricher.java
@@ -0,0 +1,53 @@
+package org.keycloak.testsuite.arquillian;
+
+import org.jboss.arquillian.container.spi.event.StartContainer;
+import org.jboss.arquillian.container.spi.event.StopContainer;
+import org.jboss.arquillian.container.test.api.ContainerController;
+import org.jboss.arquillian.core.api.Event;
+import org.jboss.arquillian.core.api.Instance;
+import org.jboss.arquillian.core.api.annotation.Inject;
+import org.jboss.arquillian.core.api.annotation.Observes;
+import org.jboss.arquillian.core.spi.Validate;
+import org.jboss.arquillian.test.spi.event.suite.AfterSuite;
+import org.jboss.logging.Logger;
+import org.jboss.arquillian.container.spi.event.StartSuiteContainers;
+
+
+public class HotRodStoreTestEnricher {
+
+ private static SuiteContext suiteContext;
+ private static final Logger log = Logger.getLogger(HotRodStoreTestEnricher.class);
+
+ @Inject
+ private Event startContainerEvent;
+
+ @Inject
+ private Event stopContainerEvent;
+
+ static void initializeSuiteContext(SuiteContext suiteContext) {
+ Validate.notNull(suiteContext, "Suite context cannot be null.");
+ HotRodStoreTestEnricher.suiteContext = suiteContext;
+ }
+
+ public void beforeContainerStarted(@Observes(precedence = 1) StartSuiteContainers event) {
+ if (!AuthServerTestEnricher.HOT_ROD_STORE_ENABLED) return;
+
+ ContainerInfo hotRodContainer = suiteContext.getHotRodStoreInfo();
+
+ if (hotRodContainer != null && !hotRodContainer.isStarted()) {
+ log.infof("HotRod store starting: %s", hotRodContainer.getQualifier());
+ startContainerEvent.fire(new StartContainer(hotRodContainer.getArquillianContainer()));
+ }
+ }
+
+ public void afterSuite(@Observes(precedence = 4) AfterSuite event) {
+ if (!AuthServerTestEnricher.HOT_ROD_STORE_ENABLED) return;
+
+ ContainerInfo hotRodContainer = suiteContext.getHotRodStoreInfo();
+
+ if (hotRodContainer != null && hotRodContainer.isStarted()) {
+ log.infof("HotRod store stopping: %s", hotRodContainer.getQualifier());
+ stopContainerEvent.fire(new StopContainer(hotRodContainer.getArquillianContainer()));
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
index b8bfec9950..afe2ac41a0 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
@@ -67,6 +67,7 @@ public class KeycloakArquillianExtension implements LoadableExtension {
.observer(AuthServerTestEnricher.class)
.observer(AppServerTestEnricher.class)
.observer(CrossDCTestEnricher.class)
+ .observer(HotRodStoreTestEnricher.class)
.observer(H2TestEnricher.class);
builder
.service(TestExecutionDecider.class, MigrationTestExecutionDecider.class)
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SuiteContext.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SuiteContext.java
index 0641112184..71e8f497c3 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SuiteContext.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SuiteContext.java
@@ -49,6 +49,8 @@ public final class SuiteContext {
private ContainerInfo migratedAuthServerInfo;
private final MigrationContext migrationContext = new MigrationContext();
+ private ContainerInfo hotRodStoreInfo;
+
private boolean adminPasswordUpdated;
private final Map smtpServer = new HashMap<>();
@@ -174,6 +176,14 @@ public final class SuiteContext {
this.migratedAuthServerInfo = migratedAuthServerInfo;
}
+ public ContainerInfo getHotRodStoreInfo() {
+ return hotRodStoreInfo;
+ }
+
+ public void setHotRodStoreInfo(ContainerInfo hotRodStoreInfo) {
+ this.hotRodStoreInfo = hotRodStoreInfo;
+ }
+
public boolean isAuthServerCluster() {
return ! authServerBackendsInfo.isEmpty();
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerEventsController.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerEventsController.java
index 59c7318831..37cb824861 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerEventsController.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerEventsController.java
@@ -52,6 +52,8 @@ import org.wildfly.extras.creaper.core.online.OnlineOptions;
import java.io.File;
import java.io.IOException;
+import java.util.Arrays;
+
import org.jboss.shrinkwrap.api.Archive;
import org.keycloak.testsuite.util.ContainerAssume;
@@ -142,6 +144,10 @@ public class KeycloakContainerEventsController extends ContainerEventController
if (restartContainer.withoutKeycloakAddUserFile()) {
removeKeycloakAddUserFile();
}
+
+ if (restartContainer.initializeDatabase()) {
+ clearMapStorageFiles();
+ }
}
/**
@@ -201,6 +207,17 @@ public class KeycloakContainerEventsController extends ContainerEventController
}
+ private void clearMapStorageFiles() {
+ String filePath = System.getProperty("project.build.directory", "target/map");
+
+ File f = new File(filePath);
+ if (!f.exists()) return;
+
+ Arrays.stream(f.listFiles())
+ .filter(file -> file.getName().startsWith("map-") && file.getName().endsWith(".json"))
+ .forEach(File::delete);
+ }
+
/**
* Copy keycloak-add-user.json only if it is jboss container (has jbossHome property).
*/
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
index 51d7a7c78d..198c0d29ee 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
@@ -135,7 +135,6 @@
},
"mapStorage": {
- "provider": "${keycloak.mapStorage.provider:}",
"concurrenthashmap": {
"dir": "${project.build.directory:target}",
"keyType.realms": "string",
@@ -247,9 +246,12 @@
"connectionsHotRod": {
"default": {
- "embedded": "${keycloak.connectionsHotRod.embedded:true}",
- "embeddedPort": "${keycloak.connectionsHotRod.embeddedPort:11444}",
- "enableSecurity": "${keycloak.connectionsHotRod.enableSecurity:false}"
+ "embedded": "${keycloak.connectionsHotRod.embedded:false}",
+ "port": "${keycloak.connectionsHotRod.port:14232}",
+ "configureRemoteCaches": "${keycloak.connectionsHotRod.configureRemoteCaches:true}",
+ "username": "${keycloak.connectionsHotRod.username:myuser}",
+ "password": "${keycloak.connectionsHotRod.password:qwer1234!}",
+ "enableSecurity": "${keycloak.connectionsHotRod.enableSecurity:true}"
}
},
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
index 92a1311233..417a66cd7f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
@@ -641,6 +641,19 @@
+
+
+ ${hotrod.store.enabled}
+ org.keycloak.testsuite.arquillian.containers.InfinispanServerDeployableContainer
+ ${cache.server.home}-hot-rod-store
+
+ ${hotrod.store.port.offset}
+ ${hotrod.store.management.port}
+ ${cache.server.java.home}
+
+
+
+
${auth.server.remote}
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index f87a931075..db65775e93 100755
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -49,6 +49,7 @@
false
false
+ false
false
false
false
@@ -140,6 +141,9 @@
true
false
+ 3010
+ 13000
+
+ ${hotrod.store.enabled}
+ ${hotrod.store.port.offset}
+ ${hotrod.store.management.port}
+
${cache.server}
${cache.server.legacy}
${cache.server.1.port.offset}
@@ -1422,6 +1431,80 @@
+
+ map-storage-hot-rod
+
+ true
+ false
+ infinispan
+
+
+
+
+
+
+
+ maven-dependency-plugin
+
+
+ unpack-cache-server-standalone-infinispan
+ generate-resources
+
+ unpack
+
+
+
+
+ org.keycloak.testsuite
+ integration-arquillian-servers-cache-server-infinispan-infinispan
+ ${project.version}
+ zip
+ ${containers.home}
+
+
+ true
+
+
+
+
+
+
+ maven-antrun-plugin
+
+
+ copy-cache-server-to-hot-rod-directory
+ process-resources
+
+ run
+
+
+ ${skip.copy.hotrod.server}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ hotrod
+
+
+
+
+
+
+
+
auth-server-profile
diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/ClientModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/ClientModelTest.java
index 6344d57199..8ca08de0c8 100644
--- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/ClientModelTest.java
+++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/ClientModelTest.java
@@ -17,7 +17,10 @@
package org.keycloak.testsuite.model;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
import org.junit.Test;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientProvider;
@@ -51,6 +54,42 @@ public class ClientModelTest extends KeycloakModelTest {
s.realms().removeRealm(realmId);
}
+ @Test
+ public void testClientsBasics() {
+ // Create client
+ ClientModel originalModel = withRealm(realmId, (session, realm) -> session.clients().addClient(realm, "myClientId"));
+ assertThat(originalModel.getId(), notNullValue());
+
+ // Find by id
+ {
+ ClientModel model = withRealm(realmId, (session, realm) -> session.clients().getClientById(realm, originalModel.getId()));
+ assertThat(model, notNullValue());
+ assertThat(model.getId(), is(equalTo(model.getId())));
+ assertThat(model.getClientId(), is(equalTo("myClientId")));
+ }
+
+ // Find by clientId
+ {
+ ClientModel model = withRealm(realmId, (session, realm) -> session.clients().getClientByClientId(realm, "myClientId"));
+ assertThat(model, notNullValue());
+ assertThat(model.getId(), is(equalTo(originalModel.getId())));
+ assertThat(model.getClientId(), is(equalTo("myClientId")));
+ }
+
+ // Test storing flow binding override
+ {
+ // Add some override
+ withRealm(realmId, (session, realm) -> {
+ ClientModel clientById = session.clients().getClientById(realm, originalModel.getId());
+ clientById.setAuthenticationFlowBindingOverride("browser", "customFlowId");
+ return clientById;
+ });
+
+ String browser = withRealm(realmId, (session, realm) -> session.clients().getClientById(realm, originalModel.getId()).getAuthenticationFlowBindingOverride("browser"));
+ assertThat(browser, is(equalTo("customFlowId")));
+ }
+ }
+
@Test
public void testScopeMappingRoleRemoval() {
// create two clients, one realm role and one client role and assign both to one of the clients
diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/ConcurrentHashMapStorageTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/ConcurrentHashMapStorageTest.java
index 59e3d1bf6c..c3fa42d450 100644
--- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/ConcurrentHashMapStorageTest.java
+++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/ConcurrentHashMapStorageTest.java
@@ -60,7 +60,7 @@ public class ConcurrentHashMapStorageTest extends KeycloakModelTest {
@Before
public void initMapStorageProviderId() {
- MapStorageProviderFactory ms = (MapStorageProviderFactory) getFactory().getProviderFactory(MapStorageProvider.class);
+ MapStorageProviderFactory ms = (MapStorageProviderFactory) getFactory().getProviderFactory(MapStorageProvider.class, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID);
mapStorageProviderId = ms.getId();
assertThat(mapStorageProviderId, Matchers.notNullValue());
}
@@ -84,7 +84,7 @@ public class ConcurrentHashMapStorageTest extends KeycloakModelTest {
String component2Id = createMapStorageComponent("component2", "keyType", "string");
String[] ids = withRealm(realmId, (session, realm) -> {
- ConcurrentHashMapStorage storageMain = (ConcurrentHashMapStorage) (MapStorage) session.getProvider(MapStorageProvider.class).getStorage(ClientModel.class);
+ ConcurrentHashMapStorage storageMain = (ConcurrentHashMapStorage) (MapStorage) session.getProvider(MapStorageProvider.class, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID).getStorage(ClientModel.class);
ConcurrentHashMapStorage storage1 = (ConcurrentHashMapStorage) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component1Id).getStorage(ClientModel.class);
ConcurrentHashMapStorage storage2 = (ConcurrentHashMapStorage) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component2Id).getStorage(ClientModel.class);
@@ -168,7 +168,7 @@ public class ConcurrentHashMapStorageTest extends KeycloakModelTest {
// Check that in the next transaction, the objects are still there
withRealm(realmId, (session, realm) -> {
@SuppressWarnings("unchecked")
- ConcurrentHashMapStorage storageMain = (ConcurrentHashMapStorage) (MapStorage) session.getProvider(MapStorageProvider.class).getStorage(ClientModel.class);
+ ConcurrentHashMapStorage storageMain = (ConcurrentHashMapStorage) (MapStorage) session.getProvider(MapStorageProvider.class, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID).getStorage(ClientModel.class);
@SuppressWarnings("unchecked")
ConcurrentHashMapStorage storage1 = (ConcurrentHashMapStorage) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component1Id).getStorage(ClientModel.class);
@SuppressWarnings("unchecked")
diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java
index 5c989aa7ad..1b57fbf38b 100644
--- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java
+++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java
@@ -38,7 +38,7 @@ import org.keycloak.models.map.realm.MapRealmProviderFactory;
import org.keycloak.models.map.role.MapRoleProviderFactory;
import org.keycloak.models.map.storage.MapStorageSpi;
import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory;
-//import org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory;
+import org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory;
import org.keycloak.models.map.user.MapUserProviderFactory;
import org.keycloak.models.map.userSession.MapUserSessionProviderFactory;
import org.keycloak.provider.ProviderFactory;
@@ -61,9 +61,9 @@ public class HotRodMapStorage extends KeycloakModelParameters {
.build();
static final Set> ALLOWED_FACTORIES = ImmutableSet.>builder()
- //.add(HotRodMapStorageProviderFactory.class)
+ .add(HotRodMapStorageProviderFactory.class)
.add(HotRodConnectionProviderFactory.class)
- .add(ConcurrentHashMapStorageProviderFactory.class)
+ .add(ConcurrentHashMapStorageProviderFactory.class) // TODO: this should be removed when we have a HotRod implementation for each area
.build();
private static final String STORAGE_CONFIG = "storage.provider";
@@ -73,8 +73,7 @@ public class HotRodMapStorage extends KeycloakModelParameters {
@Override
public void updateConfig(Config cf) {
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
- //.spi("client").provider(MapClientProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
- .spi("client").provider(MapClientProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
+ .spi("client").provider(MapClientProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
.spi("clientScope").provider(MapClientScopeProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("group").provider(MapGroupProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("realm").provider(MapRealmProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java
index d6ce5fd07f..97eb6f1433 100644
--- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java
+++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java
@@ -67,8 +67,6 @@ public class Map extends KeycloakModelParameters {
.add(MapUserSessionProviderFactory.class)
.add(MapUserLoginFailureProviderFactory.class)
.add(NoLockingDBLockProviderFactory.class)
-
- .add(MapStorageProviderFactory.class)
.build();
public Map() {