/** * PODD is an OWL ontology database used for scientific project management * * Copyright (C) 2009-2013 The University Of Queensland * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see <http://www.gnu.org/licenses/>. */ package com.github.podd.impl.data; import info.aduna.iteration.Iterations; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import org.openrdf.OpenRDFException; import org.openrdf.model.Literal; import org.openrdf.model.Model; import org.openrdf.model.Resource; import org.openrdf.model.Statement; import org.openrdf.model.URI; import org.openrdf.model.Value; import org.openrdf.model.impl.LinkedHashModel; import org.openrdf.model.impl.ValueFactoryImpl; import org.openrdf.model.vocabulary.RDF; import org.openrdf.query.BindingSet; import org.openrdf.query.QueryLanguage; import org.openrdf.query.TupleQuery; import org.openrdf.query.resultio.helpers.QueryResultCollector; import org.openrdf.repository.RepositoryConnection; import org.openrdf.repository.RepositoryException; import org.openrdf.rio.RDFFormat; import org.openrdf.rio.RDFParseException; import org.openrdf.rio.Rio; import org.openrdf.rio.UnsupportedRDFormatException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.github.podd.api.PoddOWLManager; import com.github.podd.api.PoddRepositoryManager; import com.github.podd.api.data.DataReference; import com.github.podd.api.data.PoddDataRepository; import com.github.podd.api.data.PoddDataRepositoryManager; import com.github.podd.api.data.PoddDataRepositoryRegistry; import com.github.podd.exception.DataReferenceInvalidException; import com.github.podd.exception.DataReferenceVerificationException; import com.github.podd.exception.DataRepositoryException; import com.github.podd.exception.DataRepositoryIncompleteException; import com.github.podd.exception.DataRepositoryMappingExistsException; import com.github.podd.exception.DataRepositoryMappingNotFoundException; import com.github.podd.exception.PoddException; import com.github.podd.exception.PoddRuntimeException; import com.github.podd.utils.PODD; import com.github.podd.utils.RdfUtility; /** * An implementation of FileRepositoryManager which uses an RDF repository graph as the back-end * storage for maintaining information about Repository Configurations. * * @author kutila */ public class PoddDataRepositoryManagerImpl implements PoddDataRepositoryManager { private final Logger log = LoggerFactory.getLogger(this.getClass()); private PoddRepositoryManager repositoryManager; private PoddOWLManager owlManager; private final Model dataRepositorySchema; /** * */ public PoddDataRepositoryManagerImpl() { try (final InputStream inputA = this.getClass().getResourceAsStream(PODD.PATH_PODD_DATA_REPOSITORY_V3);) { Objects.requireNonNull(inputA, "could not find data repository ontology"); // load poddDataRepository.owl into a Model this.dataRepositorySchema = Rio.parse(inputA, "", RDFFormat.RDFXML); } catch(IOException | RDFParseException | UnsupportedRDFormatException e) { throw new PoddRuntimeException( "Could not initialise data repository manager due to an RDF or IO exception", e); } } @Override public void addRepositoryMapping(final String alias, final PoddDataRepository<?> repositoryConfiguration) throws OpenRDFException, DataRepositoryException { this.addRepositoryMapping(alias, repositoryConfiguration, false); } @Override public void addRepositoryMapping(final String alias, final PoddDataRepository<?> repositoryConfiguration, final boolean overwrite) throws OpenRDFException, DataRepositoryException { if(repositoryConfiguration == null || alias == null) { throw new NullPointerException("Cannot add NULL as a File Repository mapping"); } final String aliasInLowerCase = alias.toLowerCase(); // - check if a mapping with this alias already exists if(this.getRepository(aliasInLowerCase) != null) { if(overwrite) { this.removeRepositoryMapping(aliasInLowerCase); } else { throw new DataRepositoryMappingExistsException(aliasInLowerCase, "File Repository mapping with this alias already exists"); } } boolean repositoryConfigurationExistsInGraph = true; if(this.getRepositoryAliases(repositoryConfiguration).isEmpty()) { // adding a new repository configuration repositoryConfigurationExistsInGraph = false; } final URI context = this.repositoryManager.getFileRepositoryManagementGraph(); RepositoryConnection conn = null; try { conn = this.repositoryManager.getManagementRepositoryConnection(); conn.begin(); if(repositoryConfigurationExistsInGraph) { final Set<Resource> subjectUris = repositoryConfiguration.getAsModel().filter(null, PODD.PODD_DATA_REPOSITORY_ALIAS, null) .subjects(); this.log.debug("Found {} subject URIs", subjectUris.size()); // should // be // only // 1 // here for(final Resource subjectUri : subjectUris) { conn.add(subjectUri, PODD.PODD_DATA_REPOSITORY_ALIAS, ValueFactoryImpl.getInstance().createLiteral(aliasInLowerCase), context); this.log.debug("Added alias '{}' triple with subject <{}>", aliasInLowerCase, subjectUri); } } else { final Model model = repositoryConfiguration.getAsModel(); if(model == null || model.isEmpty()) { throw new DataRepositoryIncompleteException("Incomplete File Repository since Model is empty"); } // check that the subject URIs used in the repository // configuration are not already // used in the file repository management graph final Set<Resource> subjectUris = model.filter(null, PODD.PODD_DATA_REPOSITORY_ALIAS, null).subjects(); for(final Resource subjectUri : subjectUris) { if(conn.hasStatement(subjectUri, null, null, false, context)) { throw new DataRepositoryIncompleteException( "Subject URIs used in Model already exist in Management Graph: uri=" + subjectUri); } } conn.add(model, context); } conn.commit(); } catch(final Throwable e) { if(conn != null) { conn.rollback(); } throw e; } finally { try { if(conn != null && conn.isOpen()) { conn.close(); } } catch(final RepositoryException e) { this.log.warn("Failed to close RepositoryConnection", e); } } } @Override public void downloadFileReference(final DataReference nextFileReference, final OutputStream outputStream) throws PoddException, IOException { // TODO throw new RuntimeException("TODO: Implement me"); } @Override public List<String> getAllAliases() throws DataRepositoryException, OpenRDFException { final List<String> results = new ArrayList<String>(); RepositoryConnection conn = null; try { conn = this.repositoryManager.getManagementRepositoryConnection(); conn.begin(); final URI context = this.repositoryManager.getFileRepositoryManagementGraph(); final StringBuilder sb = new StringBuilder(); sb.append("SELECT ?alias WHERE { "); sb.append(" ?aliasUri <" + PODD.PODD_DATA_REPOSITORY_ALIAS.stringValue() + "> ?alias ."); sb.append(" } "); this.log.debug("Created SPARQL {} ", sb); final TupleQuery query = conn.prepareTupleQuery(QueryLanguage.SPARQL, sb.toString()); final QueryResultCollector queryResults = RdfUtility.executeTupleQuery(query, context); for(final BindingSet binding : queryResults.getBindingSets()) { final Value member = binding.getValue("alias"); results.add(member.stringValue()); } } finally { if(conn != null && conn.isActive()) { conn.rollback(); } if(conn != null && conn.isOpen()) { conn.close(); } } return results; } @Override public List<String> getEquivalentAliases(final String alias) throws DataRepositoryException, OpenRDFException { final List<String> results = new ArrayList<String>(); final String aliasInLowerCase = alias.toLowerCase(); RepositoryConnection conn = null; try { conn = this.repositoryManager.getManagementRepositoryConnection(); conn.begin(); final URI context = this.repositoryManager.getFileRepositoryManagementGraph(); final StringBuilder sb = new StringBuilder(); sb.append("SELECT ?otherAlias WHERE { "); sb.append(" ?aliasUri <" + PODD.PODD_DATA_REPOSITORY_ALIAS.stringValue() + "> ?otherAlias ."); sb.append(" ?aliasUri <" + PODD.PODD_DATA_REPOSITORY_ALIAS.stringValue() + "> ?alias ."); sb.append(" } "); this.log.debug("Created SPARQL {} with alias bound to '{}'", sb, aliasInLowerCase); final TupleQuery query = conn.prepareTupleQuery(QueryLanguage.SPARQL, sb.toString()); query.setBinding("alias", ValueFactoryImpl.getInstance().createLiteral(aliasInLowerCase)); final QueryResultCollector queryResults = RdfUtility.executeTupleQuery(query, context); for(final BindingSet binding : queryResults.getBindingSets()) { final Value member = binding.getValue("otherAlias"); results.add(member.stringValue()); } } finally { if(conn != null && conn.isActive()) { conn.rollback(); } if(conn != null && conn.isOpen()) { conn.close(); } } return results; } @Override public PoddOWLManager getOWLManager() { return this.owlManager; } @Override public PoddDataRepository<? extends DataReference> getRepository(final String alias) throws DataRepositoryException, OpenRDFException { if(alias == null) { this.log.warn("Could not find a repository with a null alias"); throw new IllegalArgumentException("Could not find a repository with a null alias"); } RepositoryConnection conn = null; try { conn = this.repositoryManager.getManagementRepositoryConnection(); final URI context = this.repositoryManager.getFileRepositoryManagementGraph(); final Model repositories = new LinkedHashModel(); // Fetch the entire configuration into memory, as it should never be // more than a trivial // size. If this hampers efficiency could switch back to on demand // querying Iterations.addAll(conn.getStatements(null, null, null, true, context), repositories); final Set<Resource> matchingRepositories = new HashSet<Resource>(); for(final Resource nextRepository : repositories.filter(null, RDF.TYPE, PODD.PODD_DATA_REPOSITORY) .subjects()) { for(final Value nextAlias : repositories.filter(nextRepository, PODD.PODD_DATA_REPOSITORY_ALIAS, null) .objects()) { if(nextAlias instanceof Literal && ((Literal)nextAlias).getLabel().equalsIgnoreCase(alias)) { matchingRepositories.add(nextRepository); break; } } } for(final Resource nextMatchingRepository : matchingRepositories) { final PoddDataRepository<? extends DataReference> repository = PoddDataRepositoryRegistry.getInstance().createDataRepository(nextMatchingRepository, repositories); if(repository != null) { return repository; } } } finally { if(conn != null && conn.isOpen()) { conn.close(); } } // log.warn("Could not find a repository with alias: {}", alias); // throw new DataRepositoryMappingNotFoundException(alias, // "Could not find a repository with this alias"); return null; } @Override public List<String> getRepositoryAliases(final PoddDataRepository<?> repositoryConfiguration) throws DataRepositoryException, OpenRDFException { return this.getEquivalentAliases(repositoryConfiguration.getAlias()); } @Override public PoddRepositoryManager getRepositoryManager() { return this.repositoryManager; } @Override public void initialise(final Model defaultAliasConfiguration) throws OpenRDFException, PoddException, IOException { if(this.repositoryManager == null) { throw new NullPointerException("A RepositoryManager should be set before calling initialise()"); } if(this.getAllAliases().size() == 0) { this.log.info("File Repository Graph is empty. Loading default configurations..."); // validate the default alias file against the File Repository // configuration schema this.getOWLManager().verifyAgainstSchema(defaultAliasConfiguration, this.dataRepositorySchema); final Model allAliases = defaultAliasConfiguration.filter(null, PODD.PODD_DATA_REPOSITORY_ALIAS, null); this.log.info("Found {} default aliases to add", allAliases.size()); for(final Statement stmt : allAliases) { final String alias = stmt.getObject().stringValue(); try { final PoddDataRepository<?> dataRepository = PoddDataRepositoryRegistry.getInstance().createDataRepository(stmt.getSubject(), defaultAliasConfiguration); if(dataRepository != null) { this.addRepositoryMapping(alias, dataRepository, false); } } catch(final DataRepositoryException dre) { this.log.error("Found error attempting to create repository for alias", dre); } } } } @Override public PoddDataRepository<?> removeRepositoryMapping(final String alias) throws DataRepositoryException, OpenRDFException { final String aliasInLowerCase = alias.toLowerCase(); final PoddDataRepository<?> repositoryToRemove = this.getRepository(aliasInLowerCase); if(repositoryToRemove == null) { throw new DataRepositoryMappingNotFoundException(aliasInLowerCase, "No File Repository mapped to this alias"); } // retrieved early simply to avoid having multiple RepositoryConnections // open simultaneously final int aliasCount = this.getRepositoryAliases(repositoryToRemove).size(); RepositoryConnection conn = null; try { conn = this.repositoryManager.getManagementRepositoryConnection(); conn.begin(); final URI context = this.repositoryManager.getFileRepositoryManagementGraph(); if(aliasCount > 1) { // several aliases map to this repository. only remove the // statement which maps this // alias conn.remove(null, PODD.PODD_DATA_REPOSITORY_ALIAS, ValueFactoryImpl.getInstance().createLiteral(aliasInLowerCase), context); this.log.debug("Removed ONLY the mapping for alias '{}'", aliasInLowerCase); } else { // only one mapping exists. delete the repository configuration final Set<Resource> subjectUris = repositoryToRemove .getAsModel() .filter(null, PODD.PODD_DATA_REPOSITORY_ALIAS, ValueFactoryImpl.getInstance().createLiteral(aliasInLowerCase)).subjects(); this.log.debug("Need to remove {} triples", subjectUris.size()); // DEBUG // output for(final Resource subjectUri : subjectUris) { conn.remove(subjectUri, null, null, context); this.log.debug("Removed ALL triples for alias '{}' with URI <{}>", aliasInLowerCase, subjectUri); } } conn.commit(); return repositoryToRemove; } finally { if(conn != null && conn.isActive()) { conn.rollback(); } if(conn != null && conn.isOpen()) { conn.close(); } } } @Override public void setOWLManager(final PoddOWLManager owlManager) { this.owlManager = owlManager; } @Override public void setRepositoryManager(final PoddRepositoryManager repositoryManager) { this.repositoryManager = repositoryManager; } @Override public void verifyDataReferences(final Set<DataReference> fileReferenceResults) throws OpenRDFException, DataRepositoryException, DataReferenceVerificationException { final Map<DataReference, Throwable> errors = new HashMap<DataReference, Throwable>(); for(final DataReference dataReference : fileReferenceResults) { final String alias = dataReference.getRepositoryAlias(); final PoddDataRepository<DataReference> repository = (PoddDataRepository<DataReference>)this.getRepository(alias); if(repository == null) { errors.put(dataReference, new DataRepositoryMappingNotFoundException(alias, "Could not find a File Repository configuration mapped to this alias")); } else { try { if(!repository.validate(dataReference)) { errors.put(dataReference, new DataReferenceInvalidException(dataReference, "Remote File Repository says this File Reference is invalid")); } } catch(final Exception e) { errors.put(dataReference, e); } } } if(!errors.isEmpty()) { throw new DataReferenceVerificationException(errors, "File Reference validation resulted in failures"); } } }