Java tutorial
/****************************************************************** * File: StoreBaseImpl.java * Created by: Dave Reynolds * Created on: 27 Jan 2013 * * (c) Copyright 2013, Epimorphics Limited * * 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 com.epimorphics.registry.store; import static com.epimorphics.rdfutil.QueryUtil.createBindings; import static com.epimorphics.rdfutil.QueryUtil.selectAll; import static com.epimorphics.rdfutil.QueryUtil.selectFirstVar; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.index.Term; import org.apache.lucene.queryparser.classic.ParseException; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.epimorphics.rdfutil.QueryUtil; import com.epimorphics.rdfutil.RDFUtil; import com.epimorphics.registry.core.DelegationRecord; import com.epimorphics.registry.core.Description; import com.epimorphics.registry.core.ForwardingRecord; import com.epimorphics.registry.core.ForwardingRecord.Type; import com.epimorphics.registry.core.Register; import com.epimorphics.registry.core.RegisterItem; import com.epimorphics.registry.core.Registry; import com.epimorphics.registry.core.Status; import com.epimorphics.registry.util.Prefixes; import com.epimorphics.registry.util.VersionUtil; import com.epimorphics.registry.vocab.RegistryVocab; import com.epimorphics.registry.vocab.Version; import com.epimorphics.server.core.Indexer; import com.epimorphics.server.core.Service; import com.epimorphics.server.core.ServiceBase; import com.epimorphics.server.core.Store; import com.epimorphics.server.indexers.LuceneIndex; import com.epimorphics.server.indexers.LuceneResult; import com.epimorphics.server.webapi.WebApiException; import com.epimorphics.util.EpiException; import com.epimorphics.vocabs.Time; import com.hp.hpl.jena.query.QueryExecution; import com.hp.hpl.jena.query.QueryExecutionFactory; import com.hp.hpl.jena.query.QuerySolution; import com.hp.hpl.jena.query.ResultSet; import com.hp.hpl.jena.query.ResultSetFactory; import com.hp.hpl.jena.rdf.model.Literal; import com.hp.hpl.jena.rdf.model.Model; import com.hp.hpl.jena.rdf.model.ModelFactory; import com.hp.hpl.jena.rdf.model.Property; import com.hp.hpl.jena.rdf.model.RDFNode; import com.hp.hpl.jena.rdf.model.Resource; import com.hp.hpl.jena.rdf.model.ResourceFactory; import com.hp.hpl.jena.sparql.util.Closure; import com.hp.hpl.jena.util.FileManager; import com.hp.hpl.jena.vocabulary.DCTerms; import com.hp.hpl.jena.vocabulary.OWL; import com.hp.hpl.jena.vocabulary.RDF; /** * Implementation of the store API which uses a triple store for persistence * and indexes all registered items as they are registered. In this version * all current data is held in the default graph and named graphs are used for * each version of each defined entity. * <p> * This implementation uses use the ServiceConfig machinery to specify the * store to be used ("store" parameter) and the (optional) indexer ("indexer" parameter). * The configured store should <strong>not</strong> use union-default. * </p> * @author <a href="mailto:dave@epimorphics.com">Dave Reynolds</a> */ public class StoreBaseImpl extends ServiceBase implements StoreAPI, Service { static final Logger log = LoggerFactory.getLogger(StoreBaseImpl.class); public static final String STORE_PARAMETER = "store"; public static final String INDEXER_PARAMETER = "indexer"; protected Store store; protected Indexer indexer; // protected DescriptionCache cache; protected Map<String, Lock> locks = new HashMap<String, Lock>(); @Override public void postInit() { store = getNamedService(getRequiredParam(STORE_PARAMETER), Store.class); String indexerName = config.get(INDEXER_PARAMETER); if (indexerName != null) { indexer = getNamedService(indexerName, Indexer.class); } } public synchronized void lock(String uri) { Lock lock = locks.get(uri); if (lock == null) { lock = new ReentrantLock(); locks.put(uri, lock); } lock.lock(); if (indexer != null) { indexer.startBatch(); } } /** * Release the "forupdate" lock on the given URI (should be a Register or RegisterItem). * Throws an error if there is no such lock. */ public synchronized void unlock(String uri) { Lock lock = locks.remove(uri); if (lock == null) { throw new EpiException("Internal error: tried to unlock a resource which was not locked for update"); } if (storeWriteLocked) { store.unlock(); storeWriteLocked = false; } lock.unlock(); if (indexer != null) { indexer.endBatch(); } } // Attempt to maintain store lock through a registry block operation boolean storeWriteLocked = false; protected synchronized void unlockStore() { if (!storeWriteLocked) { store.unlock(); } } protected synchronized void lockStore() { if (!storeWriteLocked) { store.lock(); } } protected synchronized void lockStoreWrite() { if (!storeWriteLocked) { store.lockWrite(); storeWriteLocked = true; } } @Override public void storeGraph(String graphURI, Model model) { lockStoreWrite(); try { storeLockedGraph(graphURI, model); } finally { unlockStore(); } } @Override public Model getGraph(String graphURI) { lockStore(); try { Model result = ModelFactory.createDefaultModel(); result.add(store.asDataset().getNamedModel(graphURI)); return result; } finally { unlockStore(); } } @Override public Description getDescription(String uri) { lockStore(); try { Description d = asDescription(describe(uri, null)); return d; } finally { unlockStore(); } } // Assumes store is locked private Resource describe(String uri, Model dest) { Resource r = getDefaultModel().createResource(uri); if (dest == null) { dest = Closure.closure(r, false); } else { Closure.closure(r, false, dest); } return r.inModel(dest); } protected Model getDefaultModel() { return store.asDataset().getDefaultModel(); } protected Description asDescription(Resource root) { return Description.descriptionFrom(root, this); } @Override public Description getCurrentVersion(String uri) { lockStore(); try { Description d = asDescription(doGetCurrentVersion(uri, null)); return d; } finally { unlockStore(); } } protected Resource doGetCurrentVersion(String uri, Model dest) { Resource root = describe(uri, dest); Resource version = root.getPropertyResourceValue(Version.currentVersion); if (version != null) { Closure.closure(mod(version), false, root.getModel()); VersionUtil.flatten(root, version); } return root; } @Override public Description getVersion(String uri, boolean withEntity) { lockStore(); try { Description d = doGetVersion(uri, true); if (d instanceof RegisterItem && withEntity) { doGetEntity((RegisterItem) d, null, false); } return d; } finally { unlockStore(); } } protected Description doGetVersion(String uri, boolean flatten) { Resource version = describe(uri, null); Resource root = version.getPropertyResourceValue(DCTerms.isVersionOf); if (root != null) { Closure.closure(mod(root), false, version.getModel()); if (flatten) VersionUtil.flatten(root, version); } else { throw new EpiException("Version requested on resource with no isVersionOf root"); } return asDescription(root); } @Override public Description getVersionAt(String uri, long time) { lockStore(); try { RDFNode version = selectFirstVar("version", getDefaultModel(), VERSION_AT_QUERY, Prefixes.getDefault(), "root", ResourceFactory.createResource(uri), "time", RDFUtil.fromDateTime(time)); if (version != null && version.isURIResource()) { return doGetVersion(version.asResource().getURI(), true); } else { return null; } } finally { unlockStore(); } } static String VERSION_AT_QUERY = "SELECT ?version WHERE \n" + "{ \n" + " ?version dct:isVersionOf ?root; \n" + " version:interval [ time:hasBeginning [time:inXSDDateTime ?start] ]. \n" + " FILTER (?start <= ?time) \n" + " FILTER NOT EXISTS { \n" + " ?version version:interval [ time:hasEnd [time:inXSDDateTime ?end] ]. \n" + " FILTER (?end <= ?time) \n" + " } \n" + "} \n"; @Override public List<VersionInfo> listVersions(String uri) { lockStore(); try { ResultSet rs = selectAll(getDefaultModel(), VERSION_LIST_QUERY, Prefixes.getDefault(), createBindings("root", ResourceFactory.createResource(uri))); List<VersionInfo> results = new ArrayList<VersionInfo>(); while (rs.hasNext()) { QuerySolution soln = rs.next(); VersionInfo vi = new VersionInfo(soln.getResource("version"), soln.getLiteral("info"), soln.getLiteral("from"), soln.getLiteral("to")); Resource replaces = soln.getResource("replaces"); if (replaces != null) { vi.setReplaces(replaces.getURI()); } results.add(vi); } return results; } finally { unlockStore(); } } static String VERSION_LIST_QUERY = "SELECT ?version ?info ?from ?to ?replaces WHERE \n" + "{ \n" + " ?version dct:isVersionOf ?root; \n" + " owl:versionInfo ?info . \n" + " OPTIONAL {?version version:interval [time:hasBeginning [time:inXSDDateTime ?from]].} \n" + " OPTIONAL {?version version:interval [time:hasEnd [time:inXSDDateTime ?to]].} \n" + " OPTIONAL {?version dct:replaces ?replaces.} \n" + "} ORDER BY ?info \n"; @Override public long versionStartedAt(String uri) { lockStore(); try { Resource root = getDefaultModel().getResource(uri); Resource interval = root.getPropertyResourceValue(Version.interval); if (interval == null) return -1; return RDFUtil.asTimestamp(interval.getPropertyResourceValue(Time.hasBeginning) .getProperty(Time.inXSDDateTime).getObject()); } finally { unlockStore(); } } @Override public List<EntityInfo> listEntityOccurences(String uri) { Resource entity = ResourceFactory.createResource(uri); lockStore(); try { ResultSet matches = QueryUtil.selectAll(getDefaultModel(), ENTITY_FIND_QUERY, Prefixes.getDefault(), "entity", entity); List<EntityInfo> results = new ArrayList<EntityInfo>(); while (matches.hasNext()) { QuerySolution soln = matches.next(); results.add(new EntityInfo(entity, soln.getResource("item"), soln.getResource("register"), soln.getResource("status"))); } return results; } finally { unlockStore(); } } static String ENTITY_FIND_QUERY = "SELECT * WHERE { " + "?item reg:register ?register; " + " version:currentVersion ?itemVer . " + "?itemVer reg:status ?status; " + " reg:definition [reg:entity ?entity] . " + "}"; @Override public RegisterItem getItem(String uri, boolean withEntity) { lockStore(); try { Resource root = doGetCurrentVersion(uri, null); if (!root.hasProperty(RDF.type, RegistryVocab.RegisterItem)) { return null; } RegisterItem item = new RegisterItem(root); if (withEntity) { doGetEntity(item, null, true); } return item; } finally { unlockStore(); } } protected Resource doGetEntity(RegisterItem item, Model dest, boolean getCurrent) { Resource root = item.getRoot(); if (dest == null) { dest = ModelFactory.createDefaultModel(); } Resource entityRef = root.getPropertyResourceValue(RegistryVocab.definition); if (entityRef != null) { Resource entity = entityRef.getPropertyResourceValue(RegistryVocab.entity); Resource srcGraph = entityRef.getPropertyResourceValue(RegistryVocab.sourceGraph); if (srcGraph != null) { dest.add(store.asDataset().getNamedModel(srcGraph.getURI())); entity = entity.inModel(dest); } else { // Occurs for versioned things i.e. Registers if (getCurrent) { entity = doGetCurrentVersion(entity.getURI(), dest); } else { Resource regversion = entityRef.getPropertyResourceValue(RegistryVocab.entityVersion); if (regversion != null) { entity = doGetVersion(regversion.getURI(), true).getRoot(); } else { throw new EpiException( "Illegal state of item for a Resgister, could not find entityVersion: " + root); } } } item.setEntity(entity); return entity; } else { log.warn("Item requested had no entity reference: " + root); return null; } } @Override public Resource getEntity(RegisterItem item) { lockStore(); try { return doGetEntity(item, null, true); } finally { unlockStore(); } } @Override public List<RegisterItem> fetchAll(List<String> itemURIs, boolean withEntity) { lockStore(); try { Model shared = ModelFactory.createDefaultModel(); List<RegisterItem> results = new ArrayList<RegisterItem>(itemURIs.size()); for (String uri : itemURIs) { Resource itemRoot = doGetCurrentVersion(uri, shared); RegisterItem item = new RegisterItem(itemRoot); if (withEntity) { doGetEntity(item, shared, true); } results.add(item); } return results; } finally { unlockStore(); } } // public List<RegisterItem> doFetchMembers(Register register, boolean withEntity) { // lockStore(); // try { // // A shared model would save having 2N models around but makes filtering painful //// Model shared = ModelFactory.createDefaultModel(); // List<RDFNode> items = selectAllVar("ri", getDefaultModel(), REGISTER_ENUM_QUERY, Prefixes.getDefault(), // "register", register.getRoot() ); // List<RegisterItem> results = new ArrayList<RegisterItem>(items.size()); // for ( RDFNode itemR : items) { // if (itemR.isURIResource()) { // Resource itemRoot = doGetCurrentVersion(itemR.asResource().getURI(), null); // RegisterItem item = new RegisterItem(itemRoot); // doGetEntity(item, null); // results.add(item); // } else { // throw new EpiException("Found item which isn't a resource"); // } // } // return results; // } finally { // unlockStore(); // } // } // static String REGISTER_ENUM_QUERY = // "SELECT ?ri WHERE { ?ri reg:register ?register. }"; @Override public List<RegisterEntryInfo> listMembers(Register register) { lockStore(); try { ResultSet rs = selectAll(getDefaultModel(), REGISTER_LIST_QUERY, Prefixes.getDefault(), createBindings("register", register.getRoot())); List<RegisterEntryInfo> results = new ArrayList<RegisterEntryInfo>(); Resource priorItem = null; RegisterEntryInfo prior = null; while (rs.hasNext()) { QuerySolution soln = rs.next(); Resource item = soln.getResource("item"); if (item.equals(priorItem)) { prior.addLabel(soln.getLiteral("label")); prior.addType(soln.getResource("type")); } else { prior = new RegisterEntryInfo(soln.getResource("status"), item, soln.getResource("entity"), soln.getLiteral("label"), soln.getResource("type"), soln.getLiteral("notation")); priorItem = item; results.add(prior); } } return results; } finally { unlockStore(); } } static String REGISTER_LIST_QUERY = "SELECT * WHERE { " + "?item reg:register ?register; " + " version:currentVersion ?itemVer; " + " reg:notation ?notation; " + " reg:itemClass ?type ." + "?itemVer reg:status ?status; " + " reg:definition [reg:entity ?entity]; " + " rdfs:label ?label . " + "} ORDER BY ?notation"; @Override public boolean contains(Register register, String notation) { lockStore(); try { String base = RDFUtil.getNamespace(register.getRoot()); Resource ri = ResourceFactory.createResource(base + "_" + notation); return getDefaultModel().contains(ri, RDF.type, RegistryVocab.RegisterItem); } finally { unlockStore(); } } @Override public void loadBootstrap(String filename) { Model bootmodel = FileManager.get().loadModel(filename); // Load first in case of errors lock("/"); lockStoreWrite(); try { getDefaultModel().add(bootmodel); } finally { unlock("/"); } } @Override public void addToRegister(Register register, RegisterItem item) { addToRegister(register, item, Calendar.getInstance()); } @Override public void addToRegister(Register register, RegisterItem item, Calendar now) { lockStoreWrite(); try { doUpdateItem(item, true, now); doUpdate(register.getRoot(), now); mod(item).addProperty(RegistryVocab.register, register.getRoot()); Resource entity = item.getEntity(); if (entity.hasProperty(RDF.type, RegistryVocab.Register)) { modCurrent(register).addProperty(RegistryVocab.subregister, entity); } modCurrent(register).removeAll(DCTerms.modified).addProperty(DCTerms.modified, getDefaultModel().createTypedLiteral(now)); } finally { unlockStore(); } } private Resource modCurrent(Description d) { Resource r = d.getRoot().inModel(getDefaultModel()); Resource current = r.getPropertyResourceValue(Version.currentVersion); return current == null ? r : current; } private Resource mod(Description d) { return mod(d.getRoot()); } private Resource mod(Resource r) { return r.inModel(getDefaultModel()); } @Override public String update(Register register, Calendar timestamp) { lockStoreWrite(); try { return doUpdateRegister(register.getRoot(), timestamp).getURI(); } finally { unlockStore(); } } @Override public String update(Register register) { return update(register, Calendar.getInstance()); } protected Resource doUpdateRegister(Resource root, Calendar cal, Property... rigids) { root.removeAll(RegistryVocab.subregister); Resource current = getDefaultModel().getResource(root.getURI()); Resource temp = current.getPropertyResourceValue(Version.currentVersion); if (temp != null) { current = temp; } // doUpdate call will need current versionInfo to be able to allocate next version correctly RDFUtil.copyProperty(current, root, OWL.versionInfo); // Preserve subregister - could this just be added to rigids RDFUtil.copyProperty(current, root, RegistryVocab.subregister); return doUpdate(root, cal, rigids); } protected Resource doUpdate(Resource root, Calendar cal, Property... rigids) { Resource newVersion = VersionUtil.nextVersion(root, cal, rigids); Model st = getDefaultModel(); root.inModel(st).removeAll(OWL.versionInfo).removeAll(Version.currentVersion); st.add(newVersion.getModel()); return newVersion.inModel(st); } protected void doIndex(Resource root, String graph) { if (indexer != null) { // If use updateGraph then get one entry per entity, which is much more efficient // However, then can't search on older names for entities which have been updated if (Registry.TEXT_INDEX_INCLUDES_HISTORY) { indexer.addGraph(graph, root.getModel()); } else { indexer.updateGraph(graph, root.getModel()); } } } @Override public String update(RegisterItem item, boolean withEntity, Calendar timestamp) { lockStoreWrite(); try { return doUpdateItem(item, withEntity, timestamp); } finally { unlockStore(); } } @Override public String update(RegisterItem item, boolean withEntity) { return update(item, withEntity, Calendar.getInstance()); } private String doUpdateItem(RegisterItem item, boolean withEntity, Calendar now) { Model storeModel = getDefaultModel(); Resource oldVersion = mod(item).getPropertyResourceValue(Version.currentVersion); Resource newVersion = doUpdate(item.getRoot(), now, RegisterItem.RIGID_PROPS); doIndex(item.getRoot(), newVersion.getURI()); if (withEntity) { Resource entity = item.getEntity(); Resource entityRef = newVersion.getPropertyResourceValue(RegistryVocab.definition); if (entityRef == null) { entityRef = storeModel.createResource(); entityRef.addProperty(RegistryVocab.entity, entity); newVersion.addProperty(RegistryVocab.definition, entityRef); } else { Resource oldEntity = entityRef.getPropertyResourceValue(RegistryVocab.entity); if (!entity.equals(oldEntity)) { // Legality checks must be performed by API not by the store layer entityRef.removeAll(RegistryVocab.entity); entityRef.addProperty(RegistryVocab.entity, entity); } } if (entity.hasProperty(RDF.type, RegistryVocab.Register)) { doUpdateRegister(entity, now); Resource currentRegVersion = entity.inModel(storeModel) .getPropertyResourceValue(Version.currentVersion); if (currentRegVersion != null) { entityRef.removeAll(RegistryVocab.entityVersion); entityRef.addProperty(RegistryVocab.entityVersion, currentRegVersion); } else { log.warn("Possible internal inconsistency. No version information available for register: " + entity); } } else { if (oldVersion != null) { if (!removeGraphFor(oldVersion)) { log.error("Could not find graph for old version of an updated entity"); } } String graphURI = newVersion.getURI() + "#graph"; Model entityModel = null; if (entity.getModel().contains(null, RDF.type, RegistryVocab.RegisterItem)) { // register item mixed in with entity graph, separate out for storage log.debug("Mixed entity and item"); entityModel = Closure.closure(entity, false); } else { entityModel = entity.getModel(); } storeLockedGraph(graphURI, entityModel); if (!item.isGraph()) { getDefaultModel().add(entityModel); } Resource graph = ResourceFactory.createResource(graphURI); entityRef.removeAll(RegistryVocab.sourceGraph); entityRef.addProperty(RegistryVocab.sourceGraph, graph); } } return newVersion.getURI(); } private void storeLockedGraph(String graphURI, Model entityModel) { // Avoids Store.updateGraph because we are already in a transaction Model graphModel = store.asDataset().getNamedModel(graphURI); if (graphModel == null) { // Probably an in-memory test store graphModel = ModelFactory.createDefaultModel(); store.asDataset().addNamedModel(graphURI, graphModel); } else { graphModel.removeAll(); } graphModel.add(entityModel); } private boolean removeGraphFor(Resource oldVersion) { Model st = getDefaultModel(); Resource definition = mod(oldVersion).getPropertyResourceValue(RegistryVocab.definition); if (definition != null) { Resource graph = definition.getPropertyResourceValue(RegistryVocab.sourceGraph); if (graph != null) { st.remove(store.asDataset().getNamedModel(graph.getURI())); return true; } } return false; } @Override public LuceneResult[] search(String query, int offset, int maxresults, String... fields) { if (indexer != null) { Analyzer analyzer = new StandardAnalyzer(org.apache.lucene.util.Version.LUCENE_40); QueryParser parser = new QueryParser(org.apache.lucene.util.Version.LUCENE_40, LuceneIndex.FIELD_LABEL, analyzer); Query search; try { search = parser.parse(query); } catch (ParseException e) { throw new WebApiException(BAD_REQUEST, "Could not parse query: " + e.getMessage()); } if (fields.length != 0) { BooleanQuery bq = new BooleanQuery(); for (int i = 0; i < fields.length;) { String key = SearchTagname.expandKey(fields[i++]); if (i >= fields.length) { throw new EpiException("Odd number of fields in search call"); } String value = fields[i++]; Term t = new Term(key, value); bq.add(new TermQuery(t), BooleanClause.Occur.MUST); } bq.add(search, BooleanClause.Occur.MUST); search = bq; } LuceneIndex index = (LuceneIndex) indexer; if (Registry.TEXT_INDEX_INCLUDES_HISTORY) { return SearchFilter.search(index, search, offset, maxresults); } else { return index.search(search, offset, maxresults); } } else { return new LuceneResult[0]; } } // Debug support only public void dump() { lockStore(); try { getDefaultModel().write(System.out, "Turtle"); } finally { unlockStore(); } } @Override public List<ForwardingRecord> listDelegations() { List<ForwardingRecord> results = new ArrayList<>(); lockStore(); try { Model m = getDefaultModel(); ResultSet rs = QueryUtil.selectAll(m, DELEGATION_LIST_QUERY, Prefixes.getDefault()); while (rs.hasNext()) { QuerySolution soln = rs.nextSolution(); Status status = Status.forResource(soln.getResource("status")); if (status.isAccepted()) { Resource record = soln.getResource("record").inModel(m); ForwardingRecord.Type type = Type.FORWARD; if (record.hasProperty(RDF.type, RegistryVocab.FederatedRegister)) { type = Type.FEDERATE; } else if (record.hasProperty(RDF.type, RegistryVocab.DelegatedRegister)) { type = Type.DELEGATE; } String target = soln.getResource("target").getURI(); ForwardingRecord fr = null; if (type == Type.DELEGATE) { DelegationRecord dr = new DelegationRecord(record.getURI(), target, type); Resource s = soln.getResource("subject"); if (s != null) dr.setSubject(s); Resource p = soln.getResource("predicate"); if (p != null) dr.setPredicate(p); Resource o = soln.getResource("object"); if (o != null) dr.setObject(o); fr = dr; } else { fr = new ForwardingRecord(record.getURI(), target, type); Literal code = soln.getLiteral("code"); if (code != null) { fr.setForwardingCode(code.getInt()); } } results.add(fr); } } return results; } finally { unlockStore(); } } static String DELEGATION_LIST_QUERY = "SELECT * WHERE {" + " { " + // DelegatedRegister case, registers are managed, hence additional versioning " ?record a reg:DelegatedRegister ; version:currentVersion ?ver ." + " ?ver reg:delegationTarget ?target. " + " ?item reg:status ?status; reg:definition [reg:entity ?record] . " + " [] version:currentVersion ?item. " + " OPTIONAL {?ver reg:forwardingCode ?code. } " + " OPTIONAL {?ver reg:enumerationSubject ?subject. } " + " OPTIONAL {?ver reg:enumerationPredicate ?predicate. } " + " OPTIONAL {?ver reg:enumerationObject ?object. } " + " } UNION {" + // Typical entity case " ?record a reg:Delegated ; reg:delegationTarget ?target. " + " ?item reg:status ?status; reg:definition [reg:entity ?record] . " + " [] version:currentVersion ?item. " + " OPTIONAL {?record reg:forwardingCode ?code. } " + " OPTIONAL {?record reg:enumerationSubject ?subject. } " + " OPTIONAL {?record reg:enumerationPredicate ?predicate. } " + " OPTIONAL {?record reg:enumerationObject ?object. } " + " } " + "}"; @Override public ResultSet query(String query) { store.lock(); try { QueryExecution exec = QueryExecutionFactory.create(query, store.asDataset()); try { return ResultSetFactory.copyResults(exec.execSelect()); } finally { exec.close(); } } finally { store.unlock(); } } }