diff --git a/src/main/java/cz/cvut/kbss/keycloak/provider/Configuration.java b/src/main/java/cz/cvut/kbss/keycloak/provider/Configuration.java index 28b5339..2e3895a 100644 --- a/src/main/java/cz/cvut/kbss/keycloak/provider/Configuration.java +++ b/src/main/java/cz/cvut/kbss/keycloak/provider/Configuration.java @@ -1,7 +1,10 @@ package cz.cvut.kbss.keycloak.provider; import cz.cvut.kbss.keycloak.provider.model.UserAccount; + +import java.util.Arrays; import java.util.Base64; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -14,7 +17,7 @@ public class Configuration { private final String realmId; - private final String repositoryId; + private final List repositoryIds; private final String repositoryUsername; @@ -41,14 +44,14 @@ public class Configuration { (Map) parseComponents(components).get("al-db-server"); final GraphDbUrlParser parser = new GraphDbUrlParser(dbServer.get("url").toString()); this.dbServerUrl = parser.getGraphdbUrl(); - this.repositoryId = parser.getRepositoryId(); + this.repositoryIds = parser.getRepositoryIds(); final Map authServer = (Map) parseComponents(components).get("al-auth-server"); final AuthServerParser aParser = new AuthServerParser(authServer.get("url").toString()); this.realmId = aParser.getRealmId(); } else { this.dbServerUrl = getProperty("DB_SERVER_URL"); - this.repositoryId = getProperty("DB_SERVER_REPOSITORY_ID"); + this.repositoryIds = Arrays.asList(getProperty("DB_SERVER_REPOSITORY_ID").split(",")); this.realmId = getProperty("REALM_ID"); } this.repositoryUsername = getProperty("REPOSITORY_USERNAME"); @@ -96,8 +99,8 @@ public String getRealmId() { return realmId; } - public String getRepositoryId() { - return repositoryId; + public List getRepositoryIds() { + return repositoryIds; } public String getRepositoryUsername() { diff --git a/src/main/java/cz/cvut/kbss/keycloak/provider/DataReplicationProvider.java b/src/main/java/cz/cvut/kbss/keycloak/provider/DataReplicationProvider.java index 7b5588e..f0a73e4 100644 --- a/src/main/java/cz/cvut/kbss/keycloak/provider/DataReplicationProvider.java +++ b/src/main/java/cz/cvut/kbss/keycloak/provider/DataReplicationProvider.java @@ -124,6 +124,5 @@ private UserAccount resolveUser(AdminEvent event) { @Override public void close() { - userAccountDao.close(); } } diff --git a/src/main/java/cz/cvut/kbss/keycloak/provider/DataReplicationProviderFactory.java b/src/main/java/cz/cvut/kbss/keycloak/provider/DataReplicationProviderFactory.java index d9f0cfa..65dea47 100644 --- a/src/main/java/cz/cvut/kbss/keycloak/provider/DataReplicationProviderFactory.java +++ b/src/main/java/cz/cvut/kbss/keycloak/provider/DataReplicationProviderFactory.java @@ -2,7 +2,6 @@ import cz.cvut.kbss.keycloak.provider.dao.GraphDBUserDao; import cz.cvut.kbss.keycloak.provider.dao.UserAccountDao; -import org.eclipse.rdf4j.repository.Repository; import org.keycloak.Config; import org.keycloak.events.EventListenerProvider; import org.keycloak.events.EventListenerProviderFactory; @@ -17,18 +16,18 @@ public class DataReplicationProviderFactory implements EventListenerProviderFact private Configuration configuration; - private Repository repository; + private Repositories repositories; @Override public EventListenerProvider create(KeycloakSession keycloakSession) { LOG.info("Creating EventListenerProvider."); - if (repository == null) { - // Init persistence factory lazily, because GraphDB won't start until its OIDC provider (Keycloak) is available - this.repository = PersistenceFactory.connect(configuration); + if (repositories == null) { + // Init persistence factory lazily to ensure the target db server is up and running + this.repositories = PersistenceFactory.connect(configuration); } return new DataReplicationProvider( new KeycloakAdapter(keycloakSession.users(), keycloakSession.realms(), configuration), - new UserAccountDao(repository.getConnection(), configuration.getVocabulary(), + new UserAccountDao(repositories, configuration.getVocabulary(), configuration.getRepositoryLanguage()), new GraphDBUserDao(configuration)); } @@ -45,8 +44,8 @@ public void postInit(KeycloakSessionFactory keycloakSessionFactory) { @Override public void close() { - if (repository != null) { - repository.shutDown(); + if (repositories != null) { + repositories.close(); } } diff --git a/src/main/java/cz/cvut/kbss/keycloak/provider/GraphDbUrlParser.java b/src/main/java/cz/cvut/kbss/keycloak/provider/GraphDbUrlParser.java index dd909d7..395d891 100644 --- a/src/main/java/cz/cvut/kbss/keycloak/provider/GraphDbUrlParser.java +++ b/src/main/java/cz/cvut/kbss/keycloak/provider/GraphDbUrlParser.java @@ -1,6 +1,8 @@ package cz.cvut.kbss.keycloak.provider; import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -9,24 +11,27 @@ public class GraphDbUrlParser { private String graphdbUrl; - private String repositoryId; + private final List repositoryIds = new ArrayList<>(); - public GraphDbUrlParser(String sparqlEndpoint) { - final Matcher m = regex.matcher(sparqlEndpoint); - if (!m.matches()) { - throw new RuntimeException(MessageFormat.format( - "The URL {0} is not a graphDb SPARQL endpoint URL (conforming to the pattern {1})", - sparqlEndpoint, regex.pattern())); + public GraphDbUrlParser(String sparqlEndpoints) { + final String[] endpoints = sparqlEndpoints.split(","); + for (String s : endpoints) { + final Matcher m = regex.matcher(s); + if (!m.matches()) { + throw new RuntimeException(MessageFormat.format( + "The URL {0} is not a graphDb SPARQL endpoint URL (conforming to the pattern {1})", + s, regex.pattern())); + } + this.graphdbUrl = m.group(1); + repositoryIds.add(m.group(2)); } - graphdbUrl = m.group(1); - repositoryId = m.group(2); } public String getGraphdbUrl() { return graphdbUrl; } - public String getRepositoryId() { - return repositoryId; + public List getRepositoryIds() { + return repositoryIds; } } diff --git a/src/main/java/cz/cvut/kbss/keycloak/provider/PersistenceFactory.java b/src/main/java/cz/cvut/kbss/keycloak/provider/PersistenceFactory.java index 3156e99..01ee7df 100644 --- a/src/main/java/cz/cvut/kbss/keycloak/provider/PersistenceFactory.java +++ b/src/main/java/cz/cvut/kbss/keycloak/provider/PersistenceFactory.java @@ -1,6 +1,5 @@ package cz.cvut.kbss.keycloak.provider; -import org.eclipse.rdf4j.repository.Repository; import org.eclipse.rdf4j.repository.manager.RemoteRepositoryManager; import org.eclipse.rdf4j.repository.manager.RepositoryManager; import org.eclipse.rdf4j.repository.manager.RepositoryProvider; @@ -11,20 +10,25 @@ class PersistenceFactory { private static final Logger LOG = LoggerFactory.getLogger(PersistenceFactory.class); - static Repository connect(Configuration configuration) { - LOG.info("Initializing connection to repository {}.", configuration.getRepositoryId()); - final String url = configuration.getDbServerUrl() + "/repositories/" + configuration.getRepositoryId(); - return connectToRemoteRepository(url, configuration); + static Repositories connect(Configuration configuration) { + final Repositories repositories = new Repositories(); + final RepositoryManager repositoryManager = connectToRepositoryServer(configuration); + configuration.getRepositoryIds().forEach(repoId -> { + LOG.info("Connecting to repository {}.", repoId); + repositories.add(repositoryManager.getRepository(repoId)); + }); + return repositories; } - private static Repository connectToRemoteRepository(String repoUri, Configuration configuration) { - final RepositoryManager manager = RepositoryProvider.getRepositoryManagerOfRepository(repoUri); + private static RepositoryManager connectToRepositoryServer(Configuration configuration) { + LOG.info("Initializing connection to repository server {}.", configuration.getDbServerUrl()); + final RepositoryManager manager = RepositoryProvider.getRepositoryManager(configuration.getDbServerUrl()); final RemoteRepositoryManager remoteManager = (RemoteRepositoryManager) manager; final String username = configuration.getRepositoryUsername(); if (username != null) { final String password = configuration.getRepositoryPassword(); remoteManager.setUsernameAndPassword(username, password); } - return manager.getRepository(configuration.getRepositoryId()); + return manager; } } diff --git a/src/main/java/cz/cvut/kbss/keycloak/provider/Repositories.java b/src/main/java/cz/cvut/kbss/keycloak/provider/Repositories.java new file mode 100644 index 0000000..3f62407 --- /dev/null +++ b/src/main/java/cz/cvut/kbss/keycloak/provider/Repositories.java @@ -0,0 +1,37 @@ +package cz.cvut.kbss.keycloak.provider; + +import org.eclipse.rdf4j.model.ValueFactory; +import org.eclipse.rdf4j.repository.Repository; +import org.eclipse.rdf4j.repository.RepositoryConnection; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class Repositories { + + private final List repositories = new ArrayList<>(); + + public void add(Repository repository) { + repositories.add(repository); + } + + public void execute(Consumer executor) { + repositories.forEach(r -> { + try (final RepositoryConnection conn = r.getConnection()) { + conn.begin(); + executor.accept(conn); + conn.commit(); + } + }); + } + + public void close() { + repositories.forEach(Repository::shutDown); + } + + public ValueFactory getValueFactory() { + assert !repositories.isEmpty(); + return repositories.get(0).getValueFactory(); + } +} diff --git a/src/main/java/cz/cvut/kbss/keycloak/provider/dao/GraphDBUserDao.java b/src/main/java/cz/cvut/kbss/keycloak/provider/dao/GraphDBUserDao.java index 6bfd230..219c699 100644 --- a/src/main/java/cz/cvut/kbss/keycloak/provider/dao/GraphDBUserDao.java +++ b/src/main/java/cz/cvut/kbss/keycloak/provider/dao/GraphDBUserDao.java @@ -36,7 +36,7 @@ public GraphDBUserDao(Configuration configuration) { public void addUser(UserAccount userAccount) { final GraphDBUserDto userDto = new GraphDBUserDto(); - userDto.addAccessToRepository(configuration.getRepositoryId()); + userDto.addAccessToRepository(configuration.getRepositoryIds()); assert userAccount.getUsername() != null; postUserToGraphDB(userAccount.getUsername(), userDto); } diff --git a/src/main/java/cz/cvut/kbss/keycloak/provider/dao/UserAccountDao.java b/src/main/java/cz/cvut/kbss/keycloak/provider/dao/UserAccountDao.java index 44513fc..85d78ad 100644 --- a/src/main/java/cz/cvut/kbss/keycloak/provider/dao/UserAccountDao.java +++ b/src/main/java/cz/cvut/kbss/keycloak/provider/dao/UserAccountDao.java @@ -1,5 +1,6 @@ package cz.cvut.kbss.keycloak.provider.dao; +import cz.cvut.kbss.keycloak.provider.Repositories; import cz.cvut.kbss.keycloak.provider.model.UserAccount; import cz.cvut.kbss.keycloak.provider.model.Vocabulary; import org.eclipse.rdf4j.model.IRI; @@ -20,27 +21,25 @@ public class UserAccountDao { private final ValueFactory vf; - private final RepositoryConnection connection; + private final Repositories repositories; private final Vocabulary vocabulary; private final String repoLang; - public UserAccountDao(RepositoryConnection connection, Vocabulary vocabulary, String repoLang) { - this.connection = connection; - this.vf = connection.getValueFactory(); + public UserAccountDao(Repositories repositories, Vocabulary vocabulary, String repoLang) { + this.repositories = repositories; + this.vf = repositories.getValueFactory(); this.vocabulary = vocabulary; this.repoLang = repoLang; } public void persist(UserAccount userAccount) { Objects.requireNonNull(userAccount); - connection.begin(); - persistInTransaction(userAccount); - connection.commit(); + repositories.execute(conn -> persistInTransaction(userAccount, conn)); } - private void persistInTransaction(UserAccount userAccount) { + private void persistInTransaction(UserAccount userAccount, RepositoryConnection connection) { if (Objects.isNull(UserAccount.getContext()) || UserAccount.getContext().isEmpty()) { generateUserMetadataStatements(userAccount).forEach(connection::add); } else { @@ -86,19 +85,15 @@ private Literal stringLiteral(String value) { public void update(UserAccount userAccount) { Objects.requireNonNull(userAccount); - connection.begin(); - final IRI subject = vf.createIRI(userAccount.getUri().toString()); - connection.remove(subject, vf.createIRI(vocabulary.getFirstName()), null); - connection.remove(subject, vf.createIRI(vocabulary.getLastName()), null); - connection.remove(subject, vf.createIRI(vocabulary.getUsername()), null); - if (vocabulary.getEmail() != null) { - connection.remove(subject, vf.createIRI(vocabulary.getEmail()), null); - } - persistInTransaction(userAccount); - connection.commit(); - } - - public void close() { - connection.close(); + repositories.execute(conn -> { + final IRI subject = vf.createIRI(userAccount.getUri().toString()); + conn.remove(subject, vf.createIRI(vocabulary.getFirstName()), null); + conn.remove(subject, vf.createIRI(vocabulary.getLastName()), null); + conn.remove(subject, vf.createIRI(vocabulary.getUsername()), null); + if (vocabulary.getEmail() != null) { + conn.remove(subject, vf.createIRI(vocabulary.getEmail()), null); + } + persistInTransaction(userAccount, conn); + }); } } diff --git a/src/main/java/cz/cvut/kbss/keycloak/provider/model/GraphDBUserDto.java b/src/main/java/cz/cvut/kbss/keycloak/provider/model/GraphDBUserDto.java index ce08e45..872ba67 100644 --- a/src/main/java/cz/cvut/kbss/keycloak/provider/model/GraphDBUserDto.java +++ b/src/main/java/cz/cvut/kbss/keycloak/provider/model/GraphDBUserDto.java @@ -2,6 +2,7 @@ import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Objects; public class GraphDBUserDto { @@ -28,10 +29,12 @@ public Collection getGrantedAuthorities() { return grantedAuthorities; } - public void addAccessToRepository(String repositoryId) { - Objects.requireNonNull(repositoryId); - grantedAuthorities.add(WRITE_REPO + repositoryId); - grantedAuthorities.add(READ_REPO + repositoryId); + public void addAccessToRepository(List repositoryIds) { + Objects.requireNonNull(repositoryIds); + repositoryIds.forEach(rId -> { + grantedAuthorities.add(WRITE_REPO + rId); + grantedAuthorities.add(READ_REPO + rId); + }); } static class AppSettings { diff --git a/src/test/java/cz/cvut/kbss/keycloak/provider/GraphDbUrlParserTest.java b/src/test/java/cz/cvut/kbss/keycloak/provider/GraphDbUrlParserTest.java index 48c2486..e7bb19a 100644 --- a/src/test/java/cz/cvut/kbss/keycloak/provider/GraphDbUrlParserTest.java +++ b/src/test/java/cz/cvut/kbss/keycloak/provider/GraphDbUrlParserTest.java @@ -5,14 +5,16 @@ import org.junit.jupiter.api.Test; +import java.util.Collections; + public class GraphDbUrlParserTest { @Test public void parsesGraphDbRepositoryUrlCorrectly() { final String sparqlEndpointUrl = "https://localhost/služby/graphdb/repositories/kodi"; final GraphDbUrlParser parser = new GraphDbUrlParser(sparqlEndpointUrl); - assertEquals("kodi",parser.getRepositoryId()); - assertEquals("https://localhost/služby/graphdb",parser.getGraphdbUrl()); + assertEquals(Collections.singletonList("kodi"), parser.getRepositoryIds()); + assertEquals("https://localhost/služby/graphdb", parser.getGraphdbUrl()); } @Test diff --git a/src/test/java/cz/cvut/kbss/keycloak/provider/dao/UserAccountDaoTest.java b/src/test/java/cz/cvut/kbss/keycloak/provider/dao/UserAccountDaoTest.java index f074cd9..1a69b41 100644 --- a/src/test/java/cz/cvut/kbss/keycloak/provider/dao/UserAccountDaoTest.java +++ b/src/test/java/cz/cvut/kbss/keycloak/provider/dao/UserAccountDaoTest.java @@ -1,5 +1,6 @@ package cz.cvut.kbss.keycloak.provider.dao; +import cz.cvut.kbss.keycloak.provider.Repositories; import cz.cvut.kbss.keycloak.provider.model.UserAccount; import cz.cvut.kbss.keycloak.provider.model.Vocabulary; import org.eclipse.rdf4j.model.IRI; @@ -7,6 +8,7 @@ import org.eclipse.rdf4j.model.impl.SimpleValueFactory; import org.eclipse.rdf4j.model.vocabulary.FOAF; import org.eclipse.rdf4j.model.vocabulary.RDF; +import org.eclipse.rdf4j.repository.Repository; import org.eclipse.rdf4j.repository.RepositoryConnection; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -28,15 +30,23 @@ class UserAccountDaoTest { private final Vocabulary vocabulary = new Vocabulary(); + @Mock + private Repository repository; + @Mock private RepositoryConnection connection; + private Repositories repositories; + private UserAccountDao sut; @BeforeEach void setUp() { - when(connection.getValueFactory()).thenReturn(vf); - this.sut = new UserAccountDao(connection, vocabulary, null); + this.repositories = new Repositories(); + repositories.add(repository); + when(repository.getConnection()).thenReturn(connection); + when(repository.getValueFactory()).thenReturn(vf); + this.sut = new UserAccountDao(repositories, vocabulary, null); } @AfterEach @@ -55,9 +65,12 @@ void persistGeneratesUserMetadataStatementsToDefaultContextWhenNoneIsSpecified() private void verifyBasicUserMetadataPersist(UserAccount user) { final IRI subj = vf.createIRI(user.getUri().toString()); verify(connection).add(vf.createStatement(subj, RDF.TYPE, vf.createIRI(vocabulary.getType()))); - verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getFirstName()), vf.createLiteral(user.getFirstName()))); - verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getLastName()), vf.createLiteral(user.getLastName()))); - verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getUsername()), vf.createLiteral(user.getUsername()))); + verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getFirstName()), + vf.createLiteral(user.getFirstName()))); + verify(connection).add( + vf.createStatement(subj, vf.createIRI(vocabulary.getLastName()), vf.createLiteral(user.getLastName()))); + verify(connection).add( + vf.createStatement(subj, vf.createIRI(vocabulary.getUsername()), vf.createLiteral(user.getUsername()))); } private UserAccount initUserAccount() { @@ -78,10 +91,16 @@ void persistGeneratesUserMetadataStatementsToContextWhenItIsConfigured() { sut.persist(user); final IRI subj = vf.createIRI(user.getUri().toString()); - verify(connection).add(vf.createStatement(subj, RDF.TYPE, vf.createIRI(vocabulary.getType())), vf.createIRI(context)); - verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getFirstName()), vf.createLiteral(user.getFirstName())), vf.createIRI(context)); - verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getLastName()), vf.createLiteral(user.getLastName())), vf.createIRI(context)); - verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getUsername()), vf.createLiteral(user.getUsername())), vf.createIRI(context)); + verify(connection).add(vf.createStatement(subj, RDF.TYPE, vf.createIRI(vocabulary.getType())), + vf.createIRI(context)); + verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getFirstName()), + vf.createLiteral(user.getFirstName())), vf.createIRI(context)); + verify(connection).add( + vf.createStatement(subj, vf.createIRI(vocabulary.getLastName()), vf.createLiteral(user.getLastName())), + vf.createIRI(context)); + verify(connection).add( + vf.createStatement(subj, vf.createIRI(vocabulary.getUsername()), vf.createLiteral(user.getUsername())), + vf.createIRI(context)); } @Test @@ -92,7 +111,8 @@ void persistGeneratesEmailStatementWhenEmailPropertyIsConfigured() { sut.persist(user); final IRI subj = vf.createIRI(user.getUri().toString()); verifyBasicUserMetadataPersist(user); - verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getEmail()), vf.createLiteral(user.getEmail()))); + verify(connection).add( + vf.createStatement(subj, vf.createIRI(vocabulary.getEmail()), vf.createLiteral(user.getEmail()))); } @Test @@ -115,23 +135,28 @@ void updateRemovesEmailWhenItsPropertyIsConfigured() { sut.update(user); final IRI subj = vf.createIRI(user.getUri().toString()); verify(connection).remove(subj, vf.createIRI(vocabulary.getEmail()), null); - verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getEmail()), vf.createLiteral(user.getEmail()))); + verify(connection).add( + vf.createStatement(subj, vf.createIRI(vocabulary.getEmail()), vf.createLiteral(user.getEmail()))); } @Test void persistSavesStringLiteralsWithLanguageTagWhenItIsConfigured() { final String lang = "cs"; vocabulary.setEmail(FOAF.MBOX.stringValue()); - this.sut = new UserAccountDao(connection, vocabulary, lang); + this.sut = new UserAccountDao(repositories, vocabulary, lang); final UserAccount user = initUserAccount(); sut.persist(user); final IRI subj = vf.createIRI(user.getUri().toString()); verify(connection).add(vf.createStatement(subj, RDF.TYPE, vf.createIRI(vocabulary.getType()))); - verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getFirstName()), vf.createLiteral(user.getFirstName(), lang))); - verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getLastName()), vf.createLiteral(user.getLastName(), lang))); - verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getUsername()), vf.createLiteral(user.getUsername(), lang))); - verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getEmail()), vf.createLiteral(user.getEmail(), lang))); + verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getFirstName()), + vf.createLiteral(user.getFirstName(), lang))); + verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getLastName()), + vf.createLiteral(user.getLastName(), lang))); + verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getUsername()), + vf.createLiteral(user.getUsername(), lang))); + verify(connection).add( + vf.createStatement(subj, vf.createIRI(vocabulary.getEmail()), vf.createLiteral(user.getEmail(), lang))); } @Test @@ -143,6 +168,7 @@ void persistSkipsNullUserAttributes() { final IRI subj = vf.createIRI(user.getUri().toString()); verify(connection).add(vf.createStatement(subj, RDF.TYPE, vf.createIRI(vocabulary.getType()))); - verify(connection).add(vf.createStatement(subj, vf.createIRI(vocabulary.getUsername()), vf.createLiteral(user.getUsername()))); + verify(connection).add( + vf.createStatement(subj, vf.createIRI(vocabulary.getUsername()), vf.createLiteral(user.getUsername()))); } } \ No newline at end of file