/******************************************************************************* * Copyright 2012 Pearson Education * * 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.semantictools.context.renderer; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.semantictools.context.renderer.model.BranchStyle; import org.semantictools.context.renderer.model.ContextProperties; import org.semantictools.context.renderer.model.FrameConstraints; import org.semantictools.context.renderer.model.JsonContext; import org.semantictools.context.renderer.model.ObjectPresentation; import org.semantictools.context.renderer.model.TermInfo; import org.semantictools.context.renderer.model.TreeNode; import org.semantictools.context.renderer.model.TreeNode.Kind; import org.semantictools.frame.api.FrameNotFoundException; import org.semantictools.frame.api.TypeManager; import org.semantictools.frame.model.Datatype; import org.semantictools.frame.model.Field; import org.semantictools.frame.model.Frame; import org.semantictools.frame.model.RdfType; import org.semantictools.frame.model.RestCategory; import com.hp.hpl.jena.ontology.OntProperty; import com.hp.hpl.jena.ontology.OntResource; import com.hp.hpl.jena.vocabulary.OWL; public class TreeGenerator { private static final String XMLSCHEMA_URI = "http://www.w3.org/2001/XMLSchema#"; private static final String pageOf = "http://www.w3.org/ns/ldp#pageOf"; private JsonContext context; private ContextProperties contextProperties; private int maxDepth; private Set<String> memory; private TypeManager typeManager; public TreeGenerator(TypeManager typeManager, JsonContext context, ContextProperties properties) { this.typeManager = typeManager; this.context = context; contextProperties = properties; } public TreeNode generateRoot(Frame frame, String propertyName, int depth) { if (propertyName == null) { return generateRoot(frame, depth); } TermInfo term = context.getTermInfoByShortName(propertyName); if (term == null) { throw new TermNotFoundException(propertyName); } Field field = getField(frame, propertyName); if (field == null) { throw new FieldNotFoundException(frame.getLocalName(), propertyName); } RdfType type = field.getRdfType(); if ((type.canAsDatatype() || type.canAsEnumeration()) && field.getMaxCardinality()==1) { throw new RuntimeException( "Cannot define a JSON-LD representation where the top node is a literal or an enumeration as required by " + frame.getLocalName() + "." + propertyName); } if (type.canAsFrame() && field.getMaxCardinality()==1) { return generateRoot(type.asFrame(), depth); } TreeNode root = new TreeNode(); root.setTypeName(""); addContextNode(root); TreeNode graph = new TreeNode(); root.add(graph); graph.setLocalName("@graph"); graph.setMaxCardinality(-1); setType(graph, field); if (type.canAsFrame()) { memory = new HashSet<String>(); addProperties(graph, type.asFrame(), depth); memory = null; } return root; } private Field getField(Frame frame, String propertyName) { List<Field> list = frame.listAllFields(); for (Field field : list) { if (field.getLocalName().equals(propertyName)) { return field; } } return null; } public TreeNode generateRoot(Frame frame, int depth) { memory = new HashSet<String>(); this.maxDepth = depth; TreeNode root = null; if (frame.getSubtypeList().isEmpty() || depth==1) { root = createBasicFrameNode(frame); addContextNode(root); addTypeNode(root, frame, 1); addProperties(root, frame, 0); } else { root = generateRootSubtypes(frame, depth); } memory = null; return root; } private TreeNode generateRootSubtypes(Frame frame, int depth) { TreeNode root = new TreeNode(); root.setKind(Kind.FRAME); root.setDescription(""); root.setTypeName(""); root.setLocalName(""); addSubtypes(root, frame, true, depth); return root; } public TreeNode generateGraph(List<Frame> frameList, int maxDepth) { memory = new HashSet<String>(); this.maxDepth = maxDepth; TreeNode root = new TreeNode(); root.setDescription(""); root.setKind(Kind.FRAME); root.setLocalName(""); root.setTypeName(""); addContextNode(root); TreeNode graph = new TreeNode(); root.add(graph); graph.setMaxCardinality(-1); graph.setKind(Kind.PROPERTY); graph.setLocalName("@graph"); if (frameList.size() == 1) { Frame frame = frameList.get(0); graph.setTypeName(frame.getLocalName()); addProperties(graph, frame, 0); } else { graph.setTypeName(""); graph.setBranchStyle(BranchStyle.OBLIQUE); for (Frame frame : frameList) { TreeNode node = createBasicFrameNode(frame); graph.add(node); addTypeNode(node, frame, 1); addProperties(node, frame, 0); } } memory = null; return root; } public TreeNode generateNode(Frame frame, int depth) { memory = new HashSet<String>(); maxDepth = depth; TreeNode root = createBasicFrameNode(frame); if (frame.hasFields()) { addProperties(root, frame, 0); } memory = null; return root; } private void addProperties(TreeNode parent, Frame frame, int depth) { if (maxDepth>=0 && depth >= maxDepth) return; String frameURI = frame.getUri(); if (memory.contains(frameURI)) { return; } memory.add(frameURI); depth++; addIdProperty(parent, frame); List<Field> list = frame.listAllFields(); for (Field field : list) { addField(parent, frame, field, depth); } } private void addIdProperty(TreeNode parent, Frame frame) { if (frame.getCategory() == RestCategory.ADDRESSABLE) { String parentType = parent.getTypeName(); TreeNode node = new TreeNode(); node.setKind(Kind.PROPERTY); node.setDescription("The URI that identifies this <code>" + parentType + "</code> instance."); node.setLocalName("@id"); node.setMaxCardinality(1); if (contextProperties.requiresId(frame.getUri())) { node.setMinCardinality(1); } else { node.setMinCardinality(0); } node.setTypeName("xs:anyURI"); parent.add(node); } } public TreeNode generateNode(Field field) { memory = new HashSet<String>(); TreeNode result = doGenerateNode(field); memory = null; return result; } private TreeNode doGenerateNode(Field field) { String uri = field.getURI(); TermInfo term = context.getTermInfoByURI(uri); if (term == null) return null; String frameURI = field.getDeclaringFrame().getUri(); FrameConstraints constraints = contextProperties.getFrameConstraints(frameURI); if (constraints != null && !constraints.isIncludedProperty(field.getURI())) { return null; } Frame frame = getFieldTypeAsFrame(field); TreeNode node = new TreeNode(); TreeNode setContainer = null; int max = field.getMaxCardinality(); if (max > 0) { node.setMaxCardinality(max); } boolean isList = field.getRdfType().canAsListType(); if (max < 0 && !isList && contextProperties.isSetProperty(field.getURI())) { node.setLocalName("@set"); TreeNode id = new TreeNode(); id.setLocalName("@id"); id.setMaxCardinality(1); id.setMinCardinality(1); id.setTypeName("xs:anyURI"); setContainer = new TreeNode(); setContainer.add(id); setContainer.add(node); setContainer.setLocalName(context.rewrite(uri)); setContainer.setMinCardinality(field.getMinCardinality()); setContainer.setMaxCardinality(1); setContainer.setTypeName(""); setContainer.setKind(Kind.PROPERTY); } else { String localName = context.rewrite(uri); if (contextProperties.usePrefix(uri)) { String namespace = typeManager.getNamespace(uri); String prefix = context.rewrite(namespace); node.setLocalName(prefix + ":" + localName); } else { node.setLocalName(localName); } } if (field.getValueRestriction() != null) { String valueURI = field.getValueRestriction().getUri(); TermInfo info = context.getTermInfoByURI(valueURI); if (info != null) { valueURI = info.getTermName(); } node.setValueRestriction(valueURI); } boolean readOnly = field.getRdfType().canAsFrame() && (field.getRdfType().asFrame().getContainerRestriction() != null); int maxCardinality = field.getMaxCardinality(); if (contextProperties.getOptionalProperties().contains(uri)) { maxCardinality = 0; } node.setReadOnly(readOnly); node.setKind(Kind.PROPERTY); node.setMinCardinality(field.getMinCardinality()); node.setMaxCardinality(maxCardinality); setDescription(node, field); setType(node, field); if (isList) { node.setMaxCardinality(-1); node.setSequential(true); } if (contextProperties.isMixed(uri)) { node.setObjectPresentation(ObjectPresentation.MIXED_VALUE); } else if ( term.hasObjectValue() && "@id".equals(term.getObjectValue().getType()) && frame != null && frame.getCategory() != RestCategory.ENUMERABLE ) { node.setObjectPresentation(ObjectPresentation.URI_REFERENCE); node.setTypeHref(null); } else if ( frame != null && frame.getCategory() == RestCategory.ENUMERABLE ) { node.setObjectPresentation(ObjectPresentation.SIMPLE_NAME); } if (contextProperties.isSimpleName(uri)) { node.setObjectPresentation(ObjectPresentation.SIMPLE_NAME); } checkExpandedValue(node, field, term); return setContainer == null ? node : setContainer; } private void checkExpandedValue(TreeNode node, Field field, TermInfo term) { if (term.hasObjectValue()) return; String typeURI = node.getTypeURI(); TypeManager manager = field.getDeclaringFrame().getTypeManager(); if (manager.getDatatypeByUri(typeURI)==null) return; node.setObjectPresentation(ObjectPresentation.EXPANDED_VALUE); } private void setDescription(TreeNode node, Field field) { String comment = field.getComment(); if (comment == null || comment.length()==0) { if (OWL.sameAs.getURI().equals(field.getURI())) { Frame parent = field.getDeclaringFrame(); TermInfo term = context.getTermInfoByURI(parent.getUri()); String typeName = (term==null) ? parent.getLocalName() : term.getTermName(); String text = "Specifies an alternative representation of this <code>{0}</code>."; comment = text.replace("{0}", typeName); } } node.setDescription(comment); } private Frame getFieldTypeAsFrame(Field field) { RdfType rdfType = field.getRdfType(); if (rdfType.canAsListType()) { rdfType = rdfType.asListType().getElementType(); } Frame frame = rdfType.canAsFrame() ? rdfType.asFrame() : null; return frame; } private void addField(TreeNode parent, Frame parentFrame, Field field, int depth) { TreeNode node = doGenerateNode(field); if (node == null) { return; } parent.add(node); if (contextProperties.isSetProperty(field.getURI())) { node = node.getChildren().get(1); } setKnownValues(node, field); Frame frame = getFieldTypeAsFrame(field); if (frame == null || isCyclic(node)) return; if (field.getURI().equals(pageOf)) { // Special handling for LDP pageOf property String containerType = contextProperties.getRdfTypeURI(); frame = typeManager.getFrameByUri(containerType); if (frame == null) { throw new FrameNotFoundException(containerType); } node.setTypeURI(containerType); node.setTypeName(frame.getLocalName()); } if (node.getObjectPresentation()==ObjectPresentation.NONE ) { List<Frame> subtypeList = frame.getSubtypeList(); boolean excludeSubtypes = excludeSubtypes(parentFrame, field); if (subtypeList.isEmpty() || excludeSubtypes) { addProperties(node, frame, depth); } else { node.setBranchStyle(BranchStyle.OBLIQUE); addSubtypes(node, frame, false, depth); } } } private void setKnownValues(TreeNode node, Field field) { OntResource type = field.getType(); if (type.canAs(OntProperty.class)) { List<String> knownValues = new ArrayList<String>(); node.setKnownValues(knownValues); getKnownValues(knownValues, type.asProperty()); } } private void getKnownValues(List<String> knownValues, OntProperty property) { List<? extends OntProperty> sequence = property.listSubProperties().toList(); for (OntProperty p : sequence) { String uri = p.getURI(); if (uri.equals(property.getURI())) { continue; } TermInfo info = context.getTermInfoByURI(uri); if (info != null) { knownValues.add(info.getTermName()); } else { knownValues.add(uri); } } } private boolean excludeSubtypes(Frame frame, Field field) { FrameConstraints constraints = contextProperties.getFrameConstraints(frame.getUri()); return constraints != null && constraints.isExcludesSubtypes(field.getURI()); } private void addSubtypes(TreeNode node, Frame frame, boolean hasContext, int depth) { List<Frame> list = frame.listAllSubtypes(); filterSubtypes(list); Collections.sort(list); if (frame.isAbstract() && list.size()==1) { // There is only one known subtype of an abstract base type. // So instead of offering a choice, just require that this subtype // be used. Frame subtype = list.get(0); TermInfo info = context.getTermInfoByURI(subtype.getUri()); if (info == null) { throw new TermNotFoundException(subtype.getUri()); } String typeName = info.getTermName(); String href = "#" + typeName; node.setTypeHref(href); node.setTypeName(typeName); node.setTypeURI(subtype.getUri()); node.setBranchStyle(BranchStyle.RECTILINEAR); addConcreteTypeNode(node, subtype, hasContext, depth); addProperties(node, subtype, depth); return; } if (maxDepth>=0 && depth>=maxDepth) return; if (!frame.isAbstract()) list.add(0, frame); node.setBranchStyle(BranchStyle.OBLIQUE); for (Frame sub : list) { if (contextProperties.getExcludedTypes().contains(sub.getUri())) { continue; } TreeNode child = createBasicFrameNode(sub); node.add(child); TermInfo info = context.getTermInfoByURI(sub.getUri()); if (info == null) { throw new TermNotFoundException(contextProperties.getMediaType(), sub.getUri()); } String typeName = info.getTermName(); String href = "#" + typeName; child.setTypeHref(href); addConcreteTypeNode(child, sub, hasContext, depth+1); addProperties(child, sub, depth+1); } } private void addConcreteTypeNode(TreeNode node, Frame subtype, boolean hasContext, int depth) { if (maxDepth>=0 && depth >= maxDepth) return; if (hasContext) { addContextNode(node); } TreeNode typeNode = new TreeNode(); node.add(typeNode); typeNode.setLocalName("@type"); typeNode.setTypeName("owl:Class"); typeNode.setKind(Kind.PROPERTY); typeNode.setDescription( "A simple name that identifies the type of this resource. The value should be <code>" + subtype.getLocalName() + "</code>." ); typeNode.setMaxCardinality(1); typeNode.setMinCardinality(1); typeNode.setObjectPresentation(ObjectPresentation.SIMPLE_NAME); } private void filterSubtypes(List<Frame> list) { Iterator<Frame> sequence = list.iterator(); while (sequence.hasNext()) { Frame frame = sequence.next(); if (frame.isAbstract()) { sequence.remove(); } // Don't include subtypes that are not referenced by the JSON-LD context. // if (context.getTermInfoByURI(frame.getUri())==null) { sequence.remove(); } } } private boolean isCyclic(TreeNode node) { String typeName = node.getTypeName(); if (typeName.length()==0) return false; while ((node=node.getParent()) != null) { if (typeName.equals(node.getTypeName())) { return true; } } return false; } private void setType(TreeNode node, Field field) { RdfType type = field.getRdfType(); String propertyURI = field.getURI(); if (!contextProperties.isIdRef(propertyURI) && shortCircuitType(node, field)) { return; } boolean isList = type.canAsListType(); String typeURI = isList ? type.asListType().getElementType().getUri() : field.getType().getURI(); TermInfo term = context.getTermInfoByURI(typeURI); String typeName = null; String typeHref = null; if (typeURI.startsWith(XMLSCHEMA_URI)) { typeName = "xs:" + field.getType().getLocalName(); } else { if (term == null) { TreeNode tmp = NodeUtil.createDefaultTypeNode(typeManager, context, typeURI); typeName = tmp.getTypeName(); typeHref = tmp.getTypeHref(); } else { typeName = term.getTermName(); if (!field.getRdfType().canAsDatatype()) { typeHref = "#" + typeName; } } } node.setTypeName(typeName); node.setTypeURI(typeURI); node.setTypeHref(typeHref); if (isList) { node.setSequential(true); } } private boolean shortCircuitType(TreeNode node, Field field) { RdfType rdfType = field.getRdfType(); if (!rdfType.canAsFrame()) return false; Frame frame = rdfType.asFrame(); if (!frame.isAbstract()) return false; List<Frame> subtypeList = frame.listAllSubtypes(); List<Datatype> datatypeList = frame.getSubdatatypeList(); if ((subtypeList.size()+datatypeList.size()) != 1) return false; RdfType type = subtypeList.isEmpty() ? datatypeList.get(0) : subtypeList.get(0); String typeURI = type.getUri(); TermInfo term = context.getTermInfoByURI(typeURI); String typeName = null; String typeHref = null; if (typeURI.startsWith(XMLSCHEMA_URI)) { typeName = "xs:" + type.getLocalName(); } else { if (term == null) { throw new TermNotFoundException(typeURI); } typeName = term.getTermName(); typeHref = "#" + typeName; } node.setTypeName(typeName); node.setTypeURI(typeURI); node.setTypeHref(typeHref); // if (isList) { // node.setSequential(true); // } return true; } private void addTypeNode(TreeNode parent, Frame frame, int minCardinality) { if (frame.isAbstract()) minCardinality = 1; Set<String> set = getTypesForFrame(frame); if (minCardinality == 0 && set.isEmpty()) return; TreeNode child = new TreeNode(); child.setLocalName("@type"); child.setTypeName("owl:Class"); child.setTypeURI(OWL.Class.getURI()); child.setObjectPresentation(ObjectPresentation.SIMPLE_NAME); child.setMinCardinality(minCardinality); child.setMaxCardinality(1); StringBuilder text = new StringBuilder( "A simple name identifying the object's type. "); if (!frame.isAbstract()) { set.add(parent.getTypeName()); } text.append("The standard context " + contextProperties.getContextRef() + " defines the following simple names that are applicable: <UL>\n"); for (String name : set) { text.append(" <LI><CODE>"); text.append(name); text.append("</CODE></LI>"); } text.append("</UL>"); TermInfo info = context.getTermInfoByURI(frame.getUri()); if (info == null) { throw new TermNotFoundException(context.getMediaType(), frame.getUri()); } String termName = info.getTermName(); text.append( "<P>Implementations may use a custom JSON-LD context which defines simple names for additional types " + "that are subtypes of <code>" + termName + ".</code></P>" ); if (minCardinality == 0) { text.append( "<P>The default value of the @type property is <code>" + termName + "</code>. " + "The @type property may be omitted if the object's type is <code>" + termName + "</code>, " + "but this property is required otherwise." ); } child.setDescription(text.toString()); parent.add(child); } private Set<String> getTypesForFrame(Frame frame) { Set<String> set = new HashSet<String>(); List<Frame> list = frame.listAllSubtypes(); Iterator<Frame> sequence = list.iterator(); while (sequence.hasNext()) { Frame subtype = sequence.next(); if (!subtype.isAbstract()) { String uri = subtype.getUri(); TermInfo term = context.getTermInfoByURI(uri); if (term == null) { continue; // The class must have been excluded } set.add(term.getTermName()); } } return set; } private void addContextNode(TreeNode parent) { TreeNode node = new TreeNode(); node.setKind(Kind.PROPERTY); node.setLocalName("@context"); node.setTypeName("JSON-LD Context"); node.setMinCardinality(1); node.setMaxCardinality(-1); addContextDescription(node); parent.add(node); } private void addContextDescription(TreeNode node) { if (contextProperties == null) { node.setDescription( "This value specifies one or more JSON-LD contexts, either by reference or by value." ); return; } String text = "<p>This value specifies one or more JSON-LD contexts, either by reference or by value.\n" + "When multiple contexts are specified, they must be encapsulated within an array.</p>\n" + "<p>For most implementations, the value will be the single URI for the standard context associated " + "with the <code>{0}</code> media type. In this case, the value will be</p>\n" + "<blockquote><code>\"{1}\"</code></blockquote>"; String mediaType = contextProperties.getMediaType(); String contextURI = contextProperties.getContextURI(); text = text.replace("{0}", mediaType).replace("{1}", contextURI); node.setDescription(text); } private TreeNode createBasicFrameNode(Frame frame) { String localName = context.rewrite(frame.getUri()); TreeNode node = new TreeNode(); node.setDescription(frame.getComment()); node.setKind(Kind.FRAME); node.setLocalName(""); node.setTypeName(localName); node.setTypeURI(frame.getUri()); return node; } }