KEYCLOAK-1105 Refactor InMemoryModel to use a factory instead of a

singleton.
This commit is contained in:
Stan Silvert 2015-03-17 16:14:29 -04:00
parent ecadc92f40
commit 68b88b4baf
26 changed files with 589 additions and 292 deletions

47
connections/file/pom.xml Normal file
View file

@ -0,0 +1,47 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.2.0.Beta1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-connections-file</artifactId>
<name>Keycloak Connections File</name>
<description/>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-export-import-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-export-import-single-file</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,86 @@
/*
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.connections.file;
import org.keycloak.models.KeycloakSession;
/**
* Provides the InMemoryModel and notifies the factory to save it when
* the session is done.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
*/
public class DefaultFileConnectionProvider implements FileConnectionProvider {
private final DefaultFileConnectionProviderFactory factory;
private final KeycloakSession session;
private final InMemoryModel inMemoryModel;
private boolean isRollbackOnly = false;
public DefaultFileConnectionProvider(DefaultFileConnectionProviderFactory factory,
KeycloakSession session,
InMemoryModel inMemoryModel) {
this.factory = factory;
this.session = session;
this.inMemoryModel = inMemoryModel;
}
@Override
public InMemoryModel getModel() {
return inMemoryModel;
}
@Override
public void sessionClosed(KeycloakSession session) {
factory.sessionClosed(session);
}
@Override
public void close() {
}
@Override
public void begin() {
}
@Override
public void commit() {
factory.commit(session);
}
@Override
public void rollback() {
factory.rollback(session);
}
@Override
public void setRollbackOnly() {
isRollbackOnly = true;
}
@Override
public boolean getRollbackOnly() {
return isRollbackOnly;
}
@Override
public boolean isActive() {
return factory.isActive(session);
}
}

View file

@ -0,0 +1,202 @@
/*
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.connections.file;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.exportimport.Strategy;
import org.keycloak.exportimport.util.ExportUtils;
import org.keycloak.exportimport.util.ImportUtils;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.util.JsonSerialization;
/**
* This class dispenses a FileConnectionProvider to Keycloak sessions. It
* makes sure that only one InMemoryModel is provided for each session and it
* handles thread contention for the file where the model is read or saved.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
*/
public class DefaultFileConnectionProviderFactory implements FileConnectionProviderFactory {
protected static final Logger logger = Logger.getLogger(DefaultFileConnectionProviderFactory.class);
private File kcdata;
private final Map<KeycloakSession, FileConnectionProvider> allProviders = new HashMap<KeycloakSession, FileConnectionProvider>();
@Override
public void init(Config.Scope config) {
String fileName = config.get("fileName");
if (fileName == null) {
fileName = "keycloak-model.json";
}
String directory = config.get("directory");
if (directory == null) {
directory = System.getProperty("jboss.server.data.dir");
}
if (directory == null) {
directory = ".";
}
kcdata = new File(directory, fileName);
}
public void sessionClosed(KeycloakSession session) {
synchronized(allProviders) {
allProviders.remove(session);
//logger.info("Removed session " + session.hashCode());
//logger.info("sessionClosed: Session count=" + allModels.size());
}
}
void readModelFile(KeycloakSession session) {
synchronized(allProviders) {
if (!kcdata.exists()) {
return;
}
FileInputStream fis = null;
try {
fis = new FileInputStream(kcdata);
ImportUtils.importFromStream(session, JsonSerialization.mapper, fis, Strategy.IGNORE_EXISTING);
} catch (IOException ioe) {
logger.error("Unable to read model file " + kcdata.getAbsolutePath(), ioe);
} finally {
//logger.info("Read model file for session=" + session.hashCode());
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
logger.error("Failed to close output stream.", e);
}
}
}
}
void writeModelFile(KeycloakSession session) {
synchronized(allProviders) {
FileOutputStream outStream = null;
try {
outStream = new FileOutputStream(kcdata);
exportModel(session, outStream);
} catch (IOException e) {
logger.error("Unable to write model file " + kcdata.getAbsolutePath(), e);
} finally {
//logger.info("Wrote model file for session=" + session.hashCode());
try {
if (outStream != null) {
outStream.close();
}
} catch (IOException e) {
logger.error("Failed to close output stream.", e);
}
}
}
}
private void exportModel(KeycloakSession session, FileOutputStream outStream) throws IOException {
List<RealmModel> realms = session.realms().getRealms();
List<RealmRepresentation> reps = new ArrayList<RealmRepresentation>();
for (RealmModel realm : realms) {
reps.add(ExportUtils.exportRealm(session, realm, true));
}
JsonSerialization.prettyMapper.writeValue(outStream, reps);
}
@Override
public FileConnectionProvider create(KeycloakSession session) {
synchronized (allProviders) {
FileConnectionProvider fcProvider = allProviders.get(session);
if (fcProvider == null) {
InMemoryModel model = new InMemoryModel();
fcProvider = new DefaultFileConnectionProvider(this, session, model);
allProviders.put(session, fcProvider);
session.getTransaction().enlist(fcProvider);
readModelFile(session);
//logger.info("Added session " + session.hashCode() + " total sessions=" + allModels.size());
}
return fcProvider;
}
}
// commitCount is used for debugging. This allows you to easily run a test
// to a particular point and then examine the JSON file.
//private static int commitCount = 0;
void commit(KeycloakSession session) {
//commitCount++;
synchronized (allProviders) {
// in case commit was somehow called twice on the same session
if (!allProviders.containsKey(session)) return;
try {
writeModelFile(session);
} finally {
allProviders.remove(session);
//logger.info("Removed session " + session.hashCode());
//logger.info("*** commitCount=" + commitCount);
//logger.info("commit(): Session count=" + allModels.size());
}
// if (commitCount == 16) {Thread.dumpStack();System.exit(0);}
}
}
void rollback(KeycloakSession session) {
synchronized (allProviders) {
allProviders.remove(session);
//logger.info("rollback(): Session count=" + allModels.size());
}
}
boolean isActive(KeycloakSession session) {
synchronized (allProviders) {
return allProviders.containsKey(session);
}
}
@Override
public void close() {
}
@Override
public String getId() {
return "default";
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.connections.file;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.provider.Provider;
/**
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
*/
public interface FileConnectionProvider extends Provider, KeycloakTransaction {
InMemoryModel getModel();
void sessionClosed(KeycloakSession session);
}

View file

@ -0,0 +1,25 @@
/*
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.connections.file;
import org.keycloak.provider.ProviderFactory;
/**
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
*/
public interface FileConnectionProviderFactory extends ProviderFactory<FileConnectionProvider> {
}

View file

@ -0,0 +1,27 @@
package org.keycloak.connections.file;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
*/
public class FileConnectionSpi implements Spi {
@Override
public String getName() {
return "connectionsFile";
}
@Override
public Class<? extends Provider> getProviderClass() {
return FileConnectionProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return FileConnectionProviderFactory.class;
}
}

View file

@ -0,0 +1,99 @@
/*
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.connections.file;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
/**
* This class provides an in-memory copy of the entire model for each
* Keycloak session. At the start of the session, the model is read
* from JSON. When the session's transaction ends, the model is written back
* out.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
*/
public class InMemoryModel {
private final Map<String, RealmModel> allRealms = new HashMap<String, RealmModel>();
// realmId, userId, userModel
private final Map<String, Map<String,UserModel>> allUsers = new HashMap<String, Map<String,UserModel>>();
public InMemoryModel() {
}
public void putRealm(String id, RealmModel realm) {
allRealms.put(id, realm);
allUsers.put(id, new HashMap<String, UserModel>());
}
public RealmModel getRealm(String id) {
return allRealms.get(id);
}
public Collection<RealmModel> getRealms() {
return allRealms.values();
}
public RealmModel getRealmByName(String name) {
for (RealmModel realm : getRealms()) {
if (realm.getName().equals(name)) return realm;
}
return null;
}
public boolean removeRealm(String id) {
allUsers.remove(id);
return (allRealms.remove(id) != null);
}
protected Map<String, UserModel> realmUsers(String realmId) {
Map<String, UserModel> realmUsers = allUsers.get(realmId);
if (realmUsers == null) throw new NullPointerException("Realm users not found for id=" + realmId);
return realmUsers;
}
public void putUser(String realmId, String userId, UserModel user) {
realmUsers(realmId).put(userId, user);
}
public UserModel getUser(String realmId, String userId) {
return realmUsers(realmId).get(userId);
}
public boolean hasUserWithUsername(String realmId, String username) {
for (UserModel user : getUsers(realmId)) {
if (user.getUsername().equals(username)) return true;
}
return false;
}
public Collection<UserModel> getUsers(String realmId) {
return realmUsers(realmId).values();
}
public boolean removeUser(String realmId, String userId) {
return (realmUsers(realmId).remove(userId) != null);
}
}

View file

@ -0,0 +1 @@
org.keycloak.connections.file.DefaultFileConnectionProviderFactory

View file

@ -0,0 +1 @@
org.keycloak.connections.file.FileConnectionSpi

View file

@ -17,6 +17,7 @@
<module>jpa-liquibase</module>
<module>infinispan</module>
<module>mongo</module>
<module>file</module>
</modules>
<build>

View file

@ -172,6 +172,10 @@
<maven-resource group="org.keycloak" artifact="keycloak-connections-jpa-liquibase"/>
</module-def>
<module-def name="org.keycloak.keycloak-connections-file">
<maven-resource group="org.keycloak" artifact="keycloak-connections-file"/>
</module-def>
<module-def name="org.keycloak.keycloak-connections-infinispan">
<maven-resource group="org.keycloak" artifact="keycloak-connections-infinispan"/>
</module-def>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.1" name="org.keycloak.keycloak-connections-file">
<resources>
<!-- Insert resources here -->
</resources>
<dependencies>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/>
<module name="org.keycloak.keycloak-export-import-api"/>
<module name="org.keycloak.keycloak-export-import-single-file"/>
<module name="org.codehaus.jackson.jackson-mapper-asl"/>
<module name="org.jboss.logging"/>
<module name="javax.api"/>
</dependencies>
</module>

View file

@ -10,9 +10,7 @@
<dependencies>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/>
<module name="org.keycloak.keycloak-export-import-api"/>
<module name="org.keycloak.keycloak-export-import-single-file"/>
<module name="org.codehaus.jackson.jackson-mapper-asl"/>
<module name="org.keycloak.keycloak-connections-file"/>
<module name="org.jboss.logging"/>
<module name="javax.api"/>
</dependencies>

View file

@ -16,6 +16,7 @@
<module name="org.keycloak.keycloak-connections-jpa" services="import"/>
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo" services="import"/>
<module name="org.keycloak.keycloak-connections-file" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-core-jaxrs" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/>

View file

@ -16,6 +16,7 @@
<module name="org.keycloak.keycloak-connections-jpa" services="import"/>
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo" services="import"/>
<module name="org.keycloak.keycloak-connections-file" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-core-jaxrs" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/>

View file

@ -7,6 +7,7 @@
<module name="org.keycloak.keycloak-connections-jpa" services="import" meta-inf="import"/>
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo" services="import"/>
<module name="org.keycloak.keycloak-connections-file" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-core-jaxrs" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/>

View file

@ -35,6 +35,11 @@
<artifactId>keycloak-model-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-file</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>

View file

@ -27,6 +27,8 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.List;
import org.keycloak.connections.file.FileConnectionProvider;
import org.keycloak.connections.file.InMemoryModel;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.entities.RealmEntity;
@ -38,12 +40,19 @@ import org.keycloak.models.entities.RealmEntity;
public class FileRealmProvider implements RealmProvider {
private final KeycloakSession session;
private FileConnectionProvider fcProvider;
private final InMemoryModel inMemoryModel;
public FileRealmProvider(KeycloakSession session, InMemoryModel inMemoryModel) {
public FileRealmProvider(KeycloakSession session, FileConnectionProvider fcProvider) {
this.session = session;
this.fcProvider = fcProvider;
session.enlistForClose(this);
this.inMemoryModel = inMemoryModel;
this.inMemoryModel = fcProvider.getModel();
}
@Override
public void close() {
fcProvider.sessionClosed(session);
}
@Override
@ -85,11 +94,6 @@ public class FileRealmProvider implements RealmProvider {
return inMemoryModel.removeRealm(id);
}
@Override
public void close() {
inMemoryModel.sessionClosed(session);
}
@Override
public RoleModel getRoleById(String id, RealmModel realm) {
return realm.getRoleById(id);

View file

@ -17,6 +17,7 @@
package org.keycloak.models.file;
import org.keycloak.Config;
import org.keycloak.connections.file.FileConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmProvider;
@ -30,19 +31,9 @@ import org.keycloak.models.RealmProviderFactory;
*/
public class FileRealmProviderFactory implements RealmProviderFactory {
private String directory;
private String fileName;
@Override
public void init(Config.Scope config) {
this.fileName = config.get("fileName");
if (fileName == null) fileName = "keycloak-model.json";
InMemoryModel.setFileName(fileName);
this.directory = config.get("directory");
if (directory == null) directory = System.getProperty("jboss.server.data.dir");
if (directory == null) directory = ".";
InMemoryModel.setDirectory(directory);
}
@Override
@ -52,7 +43,8 @@ public class FileRealmProviderFactory implements RealmProviderFactory {
@Override
public RealmProvider create(KeycloakSession session) {
return new FileRealmProvider(session, InMemoryModel.getModelForSession(session));
FileConnectionProvider fcProvider = session.getProvider(FileConnectionProvider.class);
return new FileRealmProvider(session, fcProvider);
}
@Override

View file

@ -34,6 +34,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.keycloak.connections.file.FileConnectionProvider;
import org.keycloak.connections.file.InMemoryModel;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.ModelDuplicateException;
@ -49,17 +51,19 @@ import org.keycloak.models.utils.CredentialValidation;
public class FileUserProvider implements UserProvider {
private final KeycloakSession session;
private FileConnectionProvider fcProvider;
private final InMemoryModel inMemoryModel;
public FileUserProvider(KeycloakSession session, InMemoryModel inMemoryModel) {
public FileUserProvider(KeycloakSession session, FileConnectionProvider fcProvider) {
this.session = session;
this.fcProvider = fcProvider;
session.enlistForClose(this);
this.inMemoryModel = inMemoryModel;
this.inMemoryModel = fcProvider.getModel();
}
@Override
public void close() {
inMemoryModel.sessionClosed(session);
fcProvider.sessionClosed(session);
}
@Override

View file

@ -17,6 +17,7 @@
package org.keycloak.models.file;
import org.keycloak.Config;
import org.keycloak.connections.file.FileConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserProvider;
@ -40,7 +41,8 @@ public class FileUserProviderFactory implements UserProviderFactory {
@Override
public UserProvider create(KeycloakSession session) {
return new FileUserProvider(session, InMemoryModel.getModelForSession(session));
FileConnectionProvider fcProvider = session.getProvider(FileConnectionProvider.class);
return new FileUserProvider(session, fcProvider);
}
@Override

View file

@ -1,263 +0,0 @@
/*
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* 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.file;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import org.keycloak.models.file.adapter.RealmAdapter;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jboss.logging.Logger;
import org.keycloak.exportimport.Strategy;
import org.keycloak.exportimport.util.ExportUtils;
import org.keycloak.exportimport.util.ImportUtils;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.util.JsonSerialization;
/**
* This class provides an in-memory copy of the entire model for each
* Keycloak session. At the start of the session, the model is read
* from JSON. When the session's transaction ends, the model is written back
* out.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
*/
public class InMemoryModel implements KeycloakTransaction {
private static final Logger logger = Logger.getLogger(InMemoryModel.class);
private static String directory;
private static String fileName;
private final static Map<KeycloakSession, InMemoryModel> allModels = new HashMap<KeycloakSession, InMemoryModel>();
private final KeycloakSession session;
private final Map<String, RealmModel> allRealms = new HashMap<String, RealmModel>();
// realmId, userId, userModel
private final Map<String, Map<String,UserModel>> allUsers = new HashMap<String, Map<String,UserModel>>();
private boolean isRollbackOnly = false;
static void setFileName(String dataFileName) {
fileName = dataFileName;
}
static void setDirectory(String dataDirectory) {
directory = dataDirectory;
}
/**
* Static factory to retrieve the model assigned to the session.
*
* @param session The Keycloak session.
* @return The in-memory model that will be flushed when the session is over.
*/
static InMemoryModel getModelForSession(KeycloakSession session) {
synchronized (allModels) {
InMemoryModel model = allModels.get(session);
if (model == null) {
model = new InMemoryModel(session);
allModels.put(session, model);
session.getTransaction().enlist(model);
model.readModelFile();
//logger.info("Added session " + session.hashCode() + " total sessions=" + allModels.size());
}
return model;
}
}
private InMemoryModel(KeycloakSession session) {
this.session = session;
}
private void readModelFile() {
File kcdata = new File(directory, fileName);
if (!kcdata.exists()) return;
FileInputStream fis = null;
try {
fis = new FileInputStream(kcdata);
ImportUtils.importFromStream(session, JsonSerialization.mapper, fis, Strategy.IGNORE_EXISTING);
} catch (IOException ioe) {
logger.error("Unable to read model file " + kcdata.getAbsolutePath(), ioe);
} finally {
//logger.info("Read model file for session=" + session.hashCode());
try {
if (fis != null) fis.close();
} catch (IOException e) {
logger.error("Failed to close output stream.", e);
}
}
}
void writeModelFile() {
FileOutputStream outStream = null;
File keycloakModelFile = new File(directory, fileName);
try {
outStream = new FileOutputStream(keycloakModelFile);
exportModel(outStream);
} catch (IOException e) {
logger.error("Unable to write model file " + keycloakModelFile.getAbsolutePath(), e);
} finally {
//logger.info("Wrote model file for session=" + session.hashCode());
try {
if (outStream != null) outStream.close();
} catch (IOException e) {
logger.error("Failed to close output stream.", e);
}
}
}
protected void exportModel(FileOutputStream outStream) throws IOException {
List<RealmModel> realms = session.realms().getRealms();
List<RealmRepresentation> reps = new ArrayList<RealmRepresentation>();
for (RealmModel realm : realms) {
reps.add(ExportUtils.exportRealm(session, realm, true));
}
JsonSerialization.prettyMapper.writeValue(outStream, reps);
}
public void putRealm(String id, RealmAdapter realm) {
allRealms.put(id, realm);
allUsers.put(id, new HashMap<String, UserModel>());
}
public RealmModel getRealm(String id) {
return allRealms.get(id);
}
public Collection<RealmModel> getRealms() {
return allRealms.values();
}
public RealmModel getRealmByName(String name) {
for (RealmModel realm : getRealms()) {
if (realm.getName().equals(name)) return realm;
}
return null;
}
public boolean removeRealm(String id) {
allUsers.remove(id);
return (allRealms.remove(id) != null);
}
protected Map<String, UserModel> realmUsers(String realmId) {
Map<String, UserModel> realmUsers = allUsers.get(realmId);
if (realmUsers == null) throw new NullPointerException("Realm users not found for id=" + realmId);
return realmUsers;
}
public void putUser(String realmId, String userId, UserModel user) {
realmUsers(realmId).put(userId, user);
}
public UserModel getUser(String realmId, String userId) {
return realmUsers(realmId).get(userId);
}
public boolean hasUserWithUsername(String realmId, String username) {
for (UserModel user : getUsers(realmId)) {
if (user.getUsername().equals(username)) return true;
}
return false;
}
public Collection<UserModel> getUsers(String realmId) {
return realmUsers(realmId).values();
}
public boolean removeUser(String realmId, String userId) {
return (realmUsers(realmId).remove(userId) != null);
}
void sessionClosed(KeycloakSession session) {
synchronized (allModels) {
allModels.remove(session);
//logger.info("Removed session " + session.hashCode());
//logger.info("sessionClosed: Session count=" + allModels.size());
}
}
@Override
public void begin() {
}
// commitCount is used for debugging. This allows you to easily run a test
// to a particular point and then examine the JSON file.
//private static int commitCount = 0;
@Override
public void commit() {
//commitCount++;
synchronized (allModels) {
// in case commit was somehow called twice on the same session
if (!allModels.containsKey(session)) return;
try {
writeModelFile();
} finally {
allModels.remove(session);
//logger.info("Removed session " + session.hashCode());
//logger.info("*** commitCount=" + commitCount);
//logger.info("commit(): Session count=" + allModels.size());
}
// if (commitCount == 16) {Thread.dumpStack();System.exit(0);}
}
}
@Override
public void rollback() {
synchronized (allModels) {
allModels.remove(session);
//logger.info("rollback(): Session count=" + allModels.size());
}
}
@Override
public void setRollbackOnly() {
isRollbackOnly = true;
}
@Override
public boolean getRollbackOnly() {
return isRollbackOnly;
}
@Override
public boolean isActive() {
synchronized (allModels) {
return allModels.containsKey(session);
}
}
}

View file

@ -29,13 +29,13 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.keycloak.connections.file.InMemoryModel;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.OAuthClientModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.entities.ApplicationEntity;
import org.keycloak.models.entities.ClientEntity;
import org.keycloak.models.entities.RoleEntity;
import org.keycloak.models.file.InMemoryModel;
import org.keycloak.models.utils.KeycloakModelUtils;
/**

View file

@ -46,6 +46,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.keycloak.connections.file.InMemoryModel;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.UserModel;
import org.keycloak.models.entities.ApplicationEntity;
@ -53,7 +54,6 @@ import org.keycloak.models.entities.ClientEntity;
import org.keycloak.models.entities.OAuthClientEntity;
import org.keycloak.models.entities.RealmEntity;
import org.keycloak.models.entities.RoleEntity;
import org.keycloak.models.file.InMemoryModel;
/**
* RealmModel for JSON persistence.
@ -769,8 +769,16 @@ public class RealmAdapter implements RealmModel {
@Override
public void addRequiredCredential(String type) {
if (type == null) throw new NullPointerException("Credential type can not be null");
RequiredCredentialModel credentialModel = initRequiredCredentialModel(type);
addRequiredCredential(credentialModel, realm.getRequiredCredentials());
List<RequiredCredentialEntity> requiredCredList = realm.getRequiredCredentials();
for (RequiredCredentialEntity cred : requiredCredList) {
if (type.equals(cred.getType())) return;
}
addRequiredCredential(credentialModel, requiredCredList);
}
protected void addRequiredCredential(RequiredCredentialModel credentialModel, List<RequiredCredentialEntity> persistentCollection) {

View file

@ -33,11 +33,11 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.keycloak.connections.file.InMemoryModel;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.entities.FederatedIdentityEntity;
import org.keycloak.models.entities.RoleEntity;
import org.keycloak.models.entities.UserEntity;
import org.keycloak.models.file.InMemoryModel;
/**
* UserModel for JSON persistence.

View file

@ -421,6 +421,7 @@
<configuration>
<excludes>
<exclude>**/broker/***</exclude>
<exclude>**/CacheTest.java</exclude>
</excludes>
<systemPropertyVariables>
<keycloak.realm.provider>file</keycloak.realm.provider>