package won.protocol.util; import org.apache.commons.lang3.RandomStringUtils; import org.apache.jena.graph.Node; import org.apache.jena.query.*; import org.apache.jena.rdf.model.*; import org.apache.jena.rdf.model.impl.ResourceImpl; import org.apache.jena.rdf.model.impl.StatementImpl; import org.apache.jena.sparql.path.Path; import org.apache.jena.sparql.path.PathParser; import org.apache.jena.vocabulary.DCTerms; import org.apache.jena.vocabulary.RDF; import won.protocol.exception.DataIntegrityException; import won.protocol.exception.IncorrectPropertyCountException; import won.protocol.message.WonMessageBuilder; import won.protocol.model.NeedContentPropertyType; import won.protocol.model.NeedGraphType; import won.protocol.model.NeedState; import won.protocol.vocabulary.WON; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; /** * This class wraps the need models (need and sysinfo graphs in a need dataset). * It provides abstraction for the need structure of is/seeks content nodes that are part of the need model. * It can be used to load and query an existing need dataset (or models). * Furthermore it can be used to create a need model by adding triples. * <p> * Created by hfriedrich on 16.03.2017. */ public class NeedModelWrapper { protected Model needModel; protected Model sysInfoModel; /** * Create a new need model (incluing sysinfo) * * @param needUri need uri to create the need models for */ public NeedModelWrapper(String needUri) { needModel = ModelFactory.createDefaultModel(); DefaultPrefixUtils.setDefaultPrefixes(needModel); needModel.createResource(needUri, WON.NEED); sysInfoModel = ModelFactory.createDefaultModel(); DefaultPrefixUtils.setDefaultPrefixes(sysInfoModel); sysInfoModel.createResource(needUri, WON.NEED); } /** * Load a need dataset and extract the need and sysinfo models from it * * @param ds need dataset to load */ public NeedModelWrapper(Dataset ds) { Iterator<String> iter = ds.listNames(); while (iter.hasNext()) { String m = iter.next(); if (m.endsWith("#need") || m.contains(WonMessageBuilder.CONTENT_URI_SUFFIX)) { needModel = ds.getNamedModel(m); needModel.setNsPrefixes(ds.getDefaultModel().getNsPrefixMap()); } else if (m.endsWith("#sysinfo")) { sysInfoModel = ds.getNamedModel(m); sysInfoModel.setNsPrefixes(ds.getDefaultModel().getNsPrefixMap()); } } if ((sysInfoModel == null) && (needModel != null)) { this.sysInfoModel = ModelFactory.createDefaultModel(); DefaultPrefixUtils.setDefaultPrefixes(this.sysInfoModel); this.sysInfoModel.createResource(getNeedUri(), WON.NEED); } if ((needModel == null) && (sysInfoModel != null)) { this.needModel = ModelFactory.createDefaultModel(); DefaultPrefixUtils.setDefaultPrefixes(this.needModel); this.needModel.createResource(getNeedNode(NeedGraphType.SYSINFO).getURI(), WON.NEED); } checkModels(); } /** * Load the need and sysinfo models, if one of these models is null then initialize the other one as default model * * @param needModel * @param sysInfoModel */ public NeedModelWrapper(Model needModel, Model sysInfoModel) { this.needModel = needModel; this.sysInfoModel = sysInfoModel; if ((sysInfoModel == null) && (needModel != null)) { this.sysInfoModel = ModelFactory.createDefaultModel(); DefaultPrefixUtils.setDefaultPrefixes(this.sysInfoModel); this.sysInfoModel.createResource(getNeedUri(), WON.NEED); } if ((needModel == null) && (sysInfoModel != null)) { this.needModel = ModelFactory.createDefaultModel(); DefaultPrefixUtils.setDefaultPrefixes(this.needModel); this.needModel.createResource(getNeedNode(NeedGraphType.SYSINFO).getURI(), WON.NEED); } checkModels(); } private void checkModels() { try { getNeedNode(NeedGraphType.NEED); getNeedNode(NeedGraphType.SYSINFO); } catch (NullPointerException e1) { throw new DataIntegrityException("at least one graph of need or sysinfo must exist in dataset", e1); } catch (IncorrectPropertyCountException e2) { throw new DataIntegrityException("need and sysinfo models must be a won:Need"); } } /** * get the need or sysinfo model * * @param graph type specifies the need or sysinfo model to return * @return need or sysinfo model */ public Model getNeedModel(NeedGraphType graph) { if (graph.equals(NeedGraphType.NEED)) { return needModel; } else { return sysInfoModel; } } /** * get the node of the need of either the need model or the sysinfo model * * @param graph type specifies the need or sysinfo need node to return * @return need or sysinfo need node */ public Resource getNeedNode(NeedGraphType graph) { if (graph.equals(NeedGraphType.NEED)) { return RdfUtils.findOneSubjectResource(needModel, RDF.type, WON.NEED); } else { return RdfUtils.findOneSubjectResource(sysInfoModel, RDF.type, WON.NEED); } } public String getNeedUri() { return getNeedNode(NeedGraphType.NEED).getURI(); } public void addFlag(Resource flag) { getNeedNode(NeedGraphType.NEED).addProperty(WON.HAS_FLAG, flag); } public boolean hasFlag(Resource flag) { return getNeedNode(NeedGraphType.NEED).hasProperty(WON.HAS_FLAG, flag); } public void addFacetUri(String facetUri) { Resource facet = needModel.createResource(facetUri); getNeedNode(NeedGraphType.NEED).addProperty(WON.HAS_FACET, facet); } public Collection<String> getFacetUris() { Collection<String> facetUris = new LinkedList<>(); NodeIterator iter = needModel.listObjectsOfProperty(getNeedNode(NeedGraphType.NEED), WON.HAS_FACET); while (iter.hasNext()) { facetUris.add(iter.next().asResource().getURI()); } return facetUris; } public void setNeedState(NeedState state) { Resource stateRes = NeedState.ACTIVE.equals(state) ? WON.NEED_STATE_ACTIVE : WON.NEED_STATE_INACTIVE; Resource need = getNeedNode(NeedGraphType.SYSINFO); need.removeAll(WON.IS_IN_STATE); need.addProperty(WON.IS_IN_STATE, stateRes); } public NeedState getNeedState() { RDFNode state = RdfUtils.findOnePropertyFromResource(sysInfoModel, getNeedNode(NeedGraphType.SYSINFO), WON.IS_IN_STATE); if (state.equals(WON.NEED_STATE_ACTIVE)) { return NeedState.ACTIVE; } else { return NeedState.INACTIVE; } } public ZonedDateTime getCreationDate() { String dateString = RdfUtils.findOnePropertyFromResource( sysInfoModel, getNeedNode(NeedGraphType.SYSINFO), DCTerms.created).asLiteral().getString(); return ZonedDateTime.parse(dateString, DateTimeFormatter.ISO_DATE_TIME); } public void setConnectionContainerUri(String containerUri) { Resource container = sysInfoModel.createResource(containerUri); Resource need = getNeedNode(NeedGraphType.SYSINFO); need.removeAll(WON.HAS_CONNECTIONS); need.addProperty(WON.HAS_CONNECTIONS, container); } public String getConnectionContainerUri() { return RdfUtils.findOnePropertyFromResource( sysInfoModel, getNeedNode(NeedGraphType.SYSINFO), WON.HAS_CONNECTIONS).asResource().getURI(); } public void setWonNodeUri(String nodeUri) { Resource node = sysInfoModel.createResource(nodeUri); Resource need = getNeedNode(NeedGraphType.SYSINFO); need.removeAll(WON.HAS_WON_NODE); need.addProperty(WON.HAS_WON_NODE, node); } public String getWonNodeUri() { return RdfUtils.findOnePropertyFromResource( sysInfoModel, getNeedNode(NeedGraphType.SYSINFO), WON.HAS_WON_NODE).asResource().getURI(); } /** * create a content node below the need node of the need model. * * @param type specifies which property (e.g. IS, SEEKS, ...) is used to connect the need node with the content node * @param uri uri of the content node, if null then create blank node * @return content node created */ public Resource createContentNode(NeedContentPropertyType type, String uri) { if (NeedContentPropertyType.ALL.equals(type)) { throw new IllegalArgumentException("NeedContentPropertyType.ALL not defined for this method"); } Resource contentNode = (uri != null) ? needModel.createResource(uri) : needModel.createResource(); addContentPropertyToNeedNode(type, contentNode); return contentNode; } private void addContentPropertyToNeedNode(NeedContentPropertyType type, RDFNode contentNode) { Resource needNode = getNeedNode(NeedGraphType.NEED); if (NeedContentPropertyType.IS.equals(type)) { needNode.addProperty(WON.IS, contentNode); } else if (NeedContentPropertyType.SEEKS.equals(type)) { needNode.addProperty(WON.SEEKS, contentNode); } else if (NeedContentPropertyType.SEEKS_SEEKS.equals(type)) { Resource intermediate = needModel.createResource(); needNode.addProperty(WON.SEEKS, intermediate); intermediate.addProperty(WON.SEEKS, contentNode); } else if (NeedContentPropertyType.IS_AND_SEEKS.equals(type)) { needNode.addProperty(WON.IS, contentNode); needNode.addProperty(WON.SEEKS, contentNode); } } public NeedContentPropertyType getContentPropertyType(Resource contentNode) { boolean is = getContentNodes(NeedContentPropertyType.IS).size() > 0; boolean seeks = getContentNodes(NeedContentPropertyType.SEEKS).size() > 0; boolean seeksSeeks = getContentNodes(NeedContentPropertyType.SEEKS_SEEKS).size() > 0; if (is && seeks && seeksSeeks) { return NeedContentPropertyType.ALL; } else if (is && seeks) { return NeedContentPropertyType.IS_AND_SEEKS; } else if (is) { return NeedContentPropertyType.IS; } else if (seeks) { return NeedContentPropertyType.SEEKS; } else if (seeksSeeks) { return NeedContentPropertyType.SEEKS_SEEKS; } return null; } /** * get all content nodes of a specified type * * @param type specifies which content nodes to return (IS, SEEKS, ALL, ...) * @return content nodes */ public Collection<Resource> getContentNodes(NeedContentPropertyType type) { Collection<Resource> contentNodes = new LinkedList<>(); String queryClause = null; String isClause = "{ ?needNode a won:Need. ?needNode won:is ?contentNode. }"; String isAndSeeksClause = "{ ?needNode a won:Need. ?needNode won:is ?contentNode. ?needNode won:seeks ?contentNode. }"; String seeksClause = "{ ?needNode a won:Need. ?needNode won:seeks ?contentNode. FILTER NOT EXISTS { ?needNode won:seeks/won:seeks ?contentNode. } }"; String seeksSeeksClause = "{ ?needNode a won:Need. ?needNode won:seeks/won:seeks ?contentNode. }"; switch (type) { case IS: queryClause = isClause; break; case SEEKS: queryClause = seeksClause; break; case IS_AND_SEEKS: queryClause = isAndSeeksClause; break; case SEEKS_SEEKS: queryClause = seeksSeeksClause; break; case ALL: queryClause = isClause + "UNION \n" + seeksClause + "UNION \n" + seeksSeeksClause; } String queryString = "prefix won: <http://purl.org/webofneeds/model#> \n" + "SELECT DISTINCT ?contentNode WHERE { \n" + queryClause + "\n }"; Query query = QueryFactory.create(queryString); QueryExecution qexec = QueryExecutionFactory.create(query, needModel); ResultSet rs = qexec.execSelect(); while (rs.hasNext()) { QuerySolution qs = rs.next(); if (qs.contains("contentNode")) { contentNodes.add(qs.get("contentNode").asResource()); } } return contentNodes; } public void setContentPropertyStringValue(NeedContentPropertyType type, Property p, String value) { Collection<Resource> nodes = getContentNodes(type); for (Resource node : nodes) { node.removeAll(p); node.addLiteral(p, value); } } public void addContentPropertyStringValue(NeedContentPropertyType type, Property p, String value) { Collection<Resource> nodes = getContentNodes(type); for (Resource node : nodes) { node.addLiteral(p, value); } } public String getContentPropertyStringValue(Resource contentNode, Property p) { RDFNode node = RdfUtils.findOnePropertyFromResource(needModel, contentNode, p); if (node != null && node.isLiteral()) { return node.asLiteral().getString(); } return null; } public String getContentPropertyStringValue(NeedContentPropertyType type, Property p) { return getContentPropertyObject(type, p).asLiteral().getString(); } public String getContentPropertyStringValue(NeedContentPropertyType type, String propertyPath) { Node node = getContentPropertyObject(type, propertyPath); return node != null ? node.getLiteralLexicalForm() : null; } public Collection<String> getContentPropertyStringValues(Resource contentNode, Property p, String language) { Collection<String> values = new LinkedList<>(); NodeIterator nodeIterator = needModel.listObjectsOfProperty(contentNode, p); while (nodeIterator.hasNext()) { Literal literalValue = nodeIterator.next().asLiteral(); if (language == null || language.equals(literalValue.getLanguage())) { values.add(literalValue.getString()); } } return values; } public Collection<String> getContentPropertyStringValues(NeedContentPropertyType type, Property p, String language) { Collection<String> values = new LinkedList<>(); Collection<Resource> nodes = getContentNodes(type); for (Resource node : nodes) { Collection valuesOfContentNode = getContentPropertyStringValues(node, p, language); values.addAll(valuesOfContentNode); } return values; } /** * Returns one of the possibly many specified values. The specified preferred languages will be tried first in the specified order. * @param contentNode * @return the string value or null if nothing is found */ public String getSomeContentPropertyStringValue(Resource contentNode, Property p){ return getSomeContentPropertyStringValue(contentNode, p, null); } /** * Returns one of the possibly many specified values. The specified preferred languages will be tried first in the specified order. * @param contentNode * @param preferredLanguages String array of a non-empty language tag as defined by https://tools.ietf.org/html/bcp47. The language tag must be well-formed according to section 2.2.9 of https://tools.ietf.org/html/bcp47. * @return the string value or null if nothing is found */ public String getSomeContentPropertyStringValue(Resource contentNode, Property p, String... preferredLanguages){ Collection<String> values = null; for (int i = 0; i < preferredLanguages.length; i++){ values = getContentPropertyStringValues(contentNode, p, preferredLanguages[i]); if (values != null && values.size() > 0) return values.iterator().next(); } values = getContentPropertyStringValues(contentNode, p, null); if (values != null && values.size() > 0) return values.iterator().next(); return null; } /** * Returns one of the possibly many specified values. The specified preferred languages will be tried first in the specified order. * @param preferredLanguages String array of a non-empty language tag as defined by https://tools.ietf.org/html/bcp47. The language tag must be well-formed according to section 2.2.9 of https://tools.ietf.org/html/bcp47. * @return the string value or null if nothing is found */ public String getSomeContentPropertyStringValue(NeedContentPropertyType type, Property p, String... preferredLanguages){ Collection<Resource> nodes = getContentNodes(type); for (int i = 0; i < preferredLanguages.length; i++) { for (Resource node : nodes) { String valueOfContentNode = getSomeContentPropertyStringValue(node, p, preferredLanguages[i]); if (valueOfContentNode != null) return valueOfContentNode; } } for (Resource node : nodes) { String valueOfContentNode = getSomeContentPropertyStringValue(node, p); if (valueOfContentNode != null) return valueOfContentNode; } return null; } private RDFNode getContentPropertyObject(NeedContentPropertyType type, Property p) { Collection<Resource> nodes = getContentNodes(type); RDFNode object = null; for (Resource node : nodes) { NodeIterator nodeIterator = needModel.listObjectsOfProperty(node, p); if (nodeIterator.hasNext()) { if (object != null) { throw new IncorrectPropertyCountException("expected exactly one occurrence of property " + p.getURI(), 1, 2); } object = nodeIterator.next(); } } if (object == null) { throw new IncorrectPropertyCountException("expected exactly one occurrence of property " + p.getURI(), 1, 0); } return object; } private Node getContentPropertyObject(NeedContentPropertyType type, String propertyPath) { Path path = PathParser.parse(propertyPath, DefaultPrefixUtils.getDefaultPrefixes()); Collection<Resource> nodes = getContentNodes(type); if (nodes.size() != 1) { throw new IncorrectPropertyCountException("expected exactly one occurrence of object for property path " + propertyPath, 1, nodes.size()); } Node node = nodes.iterator().next().asNode(); return RdfUtils.getNodeForPropertyPath(needModel, node, path); } private boolean isSplittableNode(RDFNode node) { return node.isResource() && (node.isAnon() || ( node.asResource().getURI().startsWith(getNeedUri()) && (! node.asResource().getURI().equals(getNeedUri())) )); } private Resource copyNode(Resource node) { if (node.isAnon()) return node.getModel().createResource(); int i = 0; String uri = node.getURI() + RandomStringUtils.randomAlphanumeric(4); String newUri = uri+"_"+ i; while (node.getModel().containsResource(new ResourceImpl(newUri))){ i++; newUri = uri+"_"+i; } return node.getModel().getResource(newUri); } /** * Returns a copy of the model in which no node reachable from the need node has multiple incoming edges * (unless the graph contains a circle, see below). This is achieved by making copies of all nodes that have multiple * incoming edges, such that each copy and the original get one of the incoming edges. The outgoing * edges of the original are replicated in the copies. * * Nodes that were newly introduced by this algorithm are never split. * * In that special case that the graph contains a circle, the resulting graph still contains a circle, and * possibly one or more nodes with more than one incoming edge. * * @return */ public Model normalizeNeedModel() { Model copy = RdfUtils.cloneModel(needModel); Set<RDFNode> blacklist = new HashSet<>(); RDFNode needNode = copy.getResource(getNeedUri().toString()); //System.out.println("model before modification:"); //RDFDataMgr.write(System.out, copy, Lang.TRIG); recursiveCopyWhereMultipleInEdges(needNode); //System.out.println("model after modifcation:"); //RDFDataMgr.write(System.out, copy, Lang.TRIG); return copy; } private void recursiveCopyWhereMultipleInEdges(RDFNode node) { ModelModification modelModification = new ModelModification(); recursiveCopyWhereMultipleInEdges(node, modelModification, new HashSet<>()); modelModification.modify(node.getModel()); } /** * If the specified node that has multiple incoming edges that have already been visited (in depth-first order, i.e. * on the way from the root to this node, if this node is not the root), the node is 'split', i.e. one copy is made * per such incoming edge. No copies are made for incoming edges from nodes that are discovered further down the tree. * * When a copy of the node is made, the subgraph reachable from the node is copied as well. * * This process is done when coming back from a depth-first recursion, i.e. smaller subgraphs are copied * before larger subgraphs. * * @param node * @param modelModification * @param visited */ private void recursiveCopyWhereMultipleInEdges(RDFNode node, ModelModification modelModification, Collection<RDFNode> visited) { //a non-resource is trivially ok if (!node.isResource()) return; if (visited.contains(node)) return; visited.add(node); List<Statement> outgoingEdges = node.getModel().listStatements(node.asResource(), null, (RDFNode) null).toList(); for(Statement stmt: outgoingEdges ){ recursiveCopyWhereMultipleInEdges(stmt.getObject(), modelModification, visited); } if (outgoingEdges.size() > 0) { Set<Resource> reachableFromNode = findReachableResources(node); List<Statement> incomingEdges = node.getModel().listStatements(null, null, node).toList(); incomingEdges = incomingEdges.stream().filter(stmt -> ! reachableFromNode.contains(stmt.getSubject())).collect(Collectors.toList()); if (incomingEdges.size() > 1 && isSplittableNode(node)) { for (Statement stmt : incomingEdges) { RDFNode copy = recursiveCopy(node, modelModification); Statement newEdge = new StatementImpl(stmt.getSubject(), stmt.getPredicate(), copy); modelModification.add(newEdge); modelModification.remove(stmt); //RDFDataMgr.write(System.out, modelModification.copyAndModify(node.getModel()), Lang.TRIG); } modelModification.remove(outgoingEdges); } } } private boolean isReachableFrom(RDFNode src, RDFNode target){ return isReachableFrom(src, target, new HashSet<>()); } private boolean isReachableFrom(RDFNode src, RDFNode target, Collection<RDFNode> visited){ if (src.equals(target)) return true; if (!src.isResource()) return false; if (visited.contains(src)) return false; visited.add(src); StmtIterator it = src.getModel().listStatements(src.asResource(), null, (RDFNode) null); while(it.hasNext()){ Statement stmt = it.nextStatement(); if (isReachableFrom(src, stmt.getObject(), visited)){ return true; } } return false; } private Set<Resource> findReachableResources(RDFNode src){ Set<Resource> reachable = new HashSet<>(); findReachableResources(src, reachable); return reachable; } private void findReachableResources(RDFNode src, Set<Resource> found){ if (!src.isResource()) return; if (found.contains(src)) return; found.add(src.asResource()); StmtIterator it = src.getModel().listStatements(src.asResource(), null, (RDFNode) null); while(it.hasNext()){ Statement stmt = it.nextStatement(); findReachableResources(src, found); } } private RDFNode recursiveCopy(RDFNode node, ModelModification modelModification){ return recursiveCopy(node, modelModification, null,null, new HashSet<>()); } private RDFNode recursiveCopy(RDFNode node, ModelModification modelModification, RDFNode toReplace, RDFNode replacement, Collection<RDFNode> visited){ if (node.equals(toReplace)) return replacement; if (!node.isResource()) return node; if (visited.contains(node)) return copyNode(node.asResource()); visited.add(node); RDFNode nodeInCopy; if (isSplittableNode(node)) { nodeInCopy = copyNode(node.asResource()); visited.add(nodeInCopy); } else { return node; } if (toReplace == null && replacement == null){ toReplace = node; replacement = nodeInCopy; } List<Statement> outgoingEdges = node.getModel().listStatements(node.asResource(), null, (RDFNode) null).toList(); for(Statement stmt: outgoingEdges ){ RDFNode newObject = recursiveCopy(stmt.getObject(), modelModification, toReplace, replacement, visited); modelModification.add(new StatementImpl(nodeInCopy.asResource(), stmt.getPredicate(), newObject)); modelModification.remove(stmt); //RDFDataMgr.write(System.out, modelModification.copyAndModify(node.getModel()), Lang.TRIG); } return nodeInCopy; } private class ModelModification{ private List<Statement> statementsToAdd; private List<Statement> statementsToRemove; public ModelModification() { this.statementsToAdd = new LinkedList<>(); this.statementsToRemove = new LinkedList<>(); } public Collection<Statement> getStatementsToAdd() { return Collections.unmodifiableCollection(statementsToAdd); } public Collection<Statement> getStatementsToRemove() { return Collections.unmodifiableCollection(statementsToRemove); } public void add (Statement stmt){ this.statementsToAdd.add(stmt); } public void add(Collection<Statement> statements) { this.statementsToAdd.addAll(statements); } public void remove(Statement stmt){ this.statementsToRemove.add(stmt); } public void remove(Collection<Statement> statements){ this.statementsToRemove.addAll(statements); } public void mergeModificationsFrom(ModelModification other){ this.statementsToRemove.addAll(other.statementsToRemove); this.statementsToAdd.addAll(other.statementsToAdd); } public Model copyAndModify(Model model) { Model ret = RdfUtils.cloneModel(model); modify(ret); return ret; } public void modify(Model model){ model.add(this.statementsToAdd); model.remove(this.statementsToRemove); } } }