commit 27c21cec0e8271e4c56b5746d8a04ca644ab8f78 Author: Dan Callaghan <djc@djc.id.au> Date: Wed, 30 Sep 2009 21:01:10 +1000 library for RDF templates Diffstat:
33 files changed, 1806 insertions(+), 0 deletions(-) diff --git a/pom.xml b/pom.xml @@ -0,0 +1,76 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>au.com.miskinhill</groupId> + <artifactId>rdftemplate</artifactId> + <packaging>jar</packaging> + <version>1.0-SNAPSHOT</version> + <name>rdftemplate</name> + <url>http://code.miskinhill.com.au/</url> + + <build> + <plugins> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <source>1.6</source> + <target>1.6</target> + </configuration> + </plugin> + <plugin> + <groupId>org.antlr</groupId> + <artifactId>antlr3-maven-plugin</artifactId> + <version>3.1.3-1</version> + <executions> + <execution> + <goals> + <goal>antlr</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.5</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>commons-lang</groupId> + <artifactId>commons-lang</artifactId> + <version>2.4</version> + </dependency> + <dependency> + <groupId>net.sourceforge.collections</groupId> + <artifactId>collections-generic</artifactId> + <version>4.01</version> + </dependency> + <dependency> + <groupId>commons-beanutils</groupId> + <artifactId>commons-beanutils</artifactId> + <version>1.8.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.antlr</groupId> + <artifactId>antlr-runtime</artifactId> + <version>3.1.3</version> + </dependency> + <dependency> + <groupId>com.hp.hpl.jena</groupId> + <artifactId>jena</artifactId> + <version>2.5.7</version> + </dependency> + <dependency> + <groupId>joda-time</groupId> + <artifactId>joda-time</artifactId> + <version>1.6</version> + </dependency> + </dependencies> + +</project> diff --git a/src/main/antlr3/au/com/miskinhill/rdftemplate/selector/Selector.g b/src/main/antlr3/au/com/miskinhill/rdftemplate/selector/Selector.g @@ -0,0 +1,113 @@ +grammar Selector; + +@parser::header { +package au.com.miskinhill.rdftemplate.selector; +} + +@parser::members { + public static Selector<?> parse(String expression) { + CharStream stream = new ANTLRStringStream(expression); + SelectorLexer lexer = new SelectorLexer(stream); + CommonTokenStream tokens = new CommonTokenStream(lexer); + SelectorParser parser = new SelectorParser(tokens); + try { + return parser.selector(); + } catch (RecognitionException e) { + throw new InvalidSelectorSyntaxException(e); + } + } + + @Override + public void reportError(RecognitionException e) { + throw new InvalidSelectorSyntaxException(e); + } +} + +@lexer::header { +package au.com.miskinhill.rdftemplate.selector; +} + +@lexer::members { + @Override + public void reportError(RecognitionException e) { + throw new InvalidSelectorSyntaxException(e); + } +} + +start : selector ; + +selector returns [Selector<?> result] + : ( ts=traversingSelector { result = ts; } + | { result = new NoopSelector(); } + ) + ( '#' + ( URI_ADAPTATION { result = new SelectorWithAdaptation(result, new UriAdaptation()); } + | URI_SLICE_ADAPTATION + '(' + startIndex=INTEGER { result = new SelectorWithAdaptation(result, + new UriSliceAdaptation(Integer.parseInt($startIndex.text))); } + ')' + | COMPARABLE_LV_ADAPTATION { result = new SelectorWithAdaptation(result, new ComparableLiteralValueAdaptation()); } + | LV_ADAPTATION { result = new SelectorWithAdaptation(result, new LiteralValueAdaptation()); } + ) + | + ) + ; + +traversingSelector returns [TraversingSelector result] +@init { + result = new TraversingSelector(); +} + : t=traversal { $result.addTraversal(t); } + ( '/' + t=traversal { $result.addTraversal(t); } + ) * + ; + +traversal returns [Traversal result] +@init { + result = new Traversal(); +} + : ( '!' { $result.setInverse(true); } + | // optional + ) + nsprefix=XMLTOKEN { $result.setPropertyNamespacePrefix($nsprefix.text); } + ':' + localname=XMLTOKEN { $result.setPropertyLocalName($localname.text); } + ( '[' + URI_PREFIX_PREDICATE + '=' + uriPrefix=SINGLE_QUOTED { $result.setPredicate(new UriPrefixPredicate($uriPrefix.text)); } + ']' + | // optional + ) + ( '(' + ( '~' { $result.setReverseSorted(true); } + | // optional + ) + s=selector { $result.setSortOrder(s.withResultType(Comparable.class)); } + ')' + | // optional + ) + ( '[' + subscript=INTEGER { $result.setSubscript(Integer.parseInt($subscript.text)); } + ']' + | // optional + ) + ; + +URI_PREFIX_PREDICATE : 'uri-prefix' ; +FIRST_PREDICATE : 'first' ; +LV_ADAPTATION : 'lv' ; +COMPARABLE_LV_ADAPTATION : 'comparable-lv' ; +URI_SLICE_ADAPTATION : 'uri-slice' ; +URI_ADAPTATION : 'uri' ; + +XMLTOKEN : ('a'..'z'|'A'..'Z') ('a'..'z'|'A'..'Z'|'0'..'9'|'_'|'-')* ; +INTEGER : ('0'..'9')+ ; +SINGLE_QUOTED : '\'' ( options {greedy=false;} : . )* '\'' + { + // strip quotes + String txt = getText(); + setText(txt.substring(1, txt.length() -1)); + }; +\ No newline at end of file diff --git a/src/main/java/au/com/miskinhill/rdftemplate/NamespacePrefixMapper.java b/src/main/java/au/com/miskinhill/rdftemplate/NamespacePrefixMapper.java @@ -0,0 +1,41 @@ +package au.com.miskinhill.rdftemplate; + +import java.util.HashMap; + +import com.hp.hpl.jena.sparql.vocabulary.FOAF; +import com.hp.hpl.jena.vocabulary.DCTerms; +import com.hp.hpl.jena.vocabulary.DC_11; +import com.hp.hpl.jena.vocabulary.OWL; +import com.hp.hpl.jena.vocabulary.RDF; +import com.hp.hpl.jena.vocabulary.RDFS; +import com.hp.hpl.jena.vocabulary.XSD; + +public final class NamespacePrefixMapper extends HashMap<String, String> { + + private static final long serialVersionUID = 2119318190108418682L; + + private static final NamespacePrefixMapper instance = new NamespacePrefixMapper(); + public static NamespacePrefixMapper getInstance() { + return instance; + } + + private NamespacePrefixMapper() { + put("mhs", "http://miskinhill.com.au/rdfschema/1.0/"); + put("dc", DCTerms.NS); + put("old-dc", DC_11.NS); + put("foaf", FOAF.NS); + put("rdf", RDF.getURI()); + put("rdfs", RDFS.getURI()); + put("xs", XSD.getURI()); + put("xsd", "http://www.w3.org/TR/xmlschema-2/#"); + put("contact", "http://www.w3.org/2000/10/swap/pim/contact#"); + put("geonames", "http://www.geonames.org/ontology#"); + put("sioc", "http://rdfs.org/sioc/ns#"); + put("awol", "http://bblfish.net/work/atom-owl/2006-06-06/#"); + put("lingvoj", "http://www.lingvoj.org/ontology#"); + put("prism", "http://prismstandard.org/namespaces/1.2/basic/"); + put("owl", OWL.NS); + put("rev", "http://purl.org/stuff/rev#"); + } + +} diff --git a/src/main/java/au/com/miskinhill/rdftemplate/TemplateInterpolator.java b/src/main/java/au/com/miskinhill/rdftemplate/TemplateInterpolator.java @@ -0,0 +1,233 @@ +package au.com.miskinhill.rdftemplate; + +import java.io.Reader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.XMLConstants; +import javax.xml.namespace.QName; +import javax.xml.stream.XMLEventFactory; +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.Characters; +import javax.xml.stream.events.EndElement; +import javax.xml.stream.events.StartElement; +import javax.xml.stream.events.XMLEvent; + +import com.hp.hpl.jena.rdf.model.Literal; +import com.hp.hpl.jena.rdf.model.RDFNode; +import org.apache.commons.lang.StringUtils; + +import au.com.miskinhill.rdftemplate.selector.Selector; +import au.com.miskinhill.rdftemplate.selector.SelectorParser; + +public class TemplateInterpolator { + + public static final String NS = "http://code.miskinhill.com.au/rdftemplate/"; + public static final String CONTENT_ACTION = "content"; + private static final QName CONTENT_ACTION_QNAME = new QName(NS, CONTENT_ACTION); + public static final String FOR_ACTION = "for"; + private static final QName FOR_ACTION_QNAME = new QName(NS, FOR_ACTION); + private static final QName XML_LANG_QNAME = new QName(XMLConstants.XML_NS_URI, "lang", XMLConstants.XML_NS_PREFIX); + private static final String XHTML_NS_URI = "http://www.w3.org/1999/xhtml"; + + private static final XMLInputFactory inputFactory = XMLInputFactory.newInstance(); + private static final XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); + private static final XMLEventFactory eventFactory = XMLEventFactory.newInstance(); + static { + inputFactory.setProperty("javax.xml.stream.isCoalescing", true); + } + + private TemplateInterpolator() { + } + + @SuppressWarnings("unchecked") + public static String interpolate(Reader reader, RDFNode node) throws XMLStreamException { + StringWriter writer = new StringWriter(); + interpolate(inputFactory.createXMLEventReader(reader), node, outputFactory.createXMLEventWriter(writer)); + return writer.toString(); + } + + public static void interpolate(Iterator<XMLEvent> reader, RDFNode node, XMLEventWriter writer) + throws XMLStreamException { + while (reader.hasNext()) { + XMLEvent event = reader.next(); + switch (event.getEventType()) { + case XMLStreamConstants.START_ELEMENT: { + StartElement start = (StartElement) event; + Attribute contentAttribute = start.getAttributeByName(CONTENT_ACTION_QNAME); + Attribute forAttribute = start.getAttributeByName(FOR_ACTION_QNAME); + if (contentAttribute != null && forAttribute != null) { + throw new TemplateSyntaxException("rdf:for and rdf:content cannot both be present on an element"); + } else if (contentAttribute != null) { + consumeTree(start, reader); + start = interpolateAttributes(start, node); + Selector<?> selector = SelectorParser.parse(contentAttribute.getValue()); + writeTreeForContent(writer, start, selector.singleResult(node)); + } else if (forAttribute != null) { + start = cloneStartWithAttributes(start, cloneAttributesWithout(start, FOR_ACTION_QNAME)); + List<XMLEvent> tree = consumeTree(start, reader); + Selector<RDFNode> selector = SelectorParser.parse(forAttribute.getValue()).withResultType(RDFNode.class); + for (RDFNode subNode : selector.result(node)) { + interpolate(tree.iterator(), subNode, writer); + } + } else { + start = interpolateAttributes(start, node); + writer.add(start); + } + break; + } + case XMLStreamConstants.CHARACTERS: { + Characters characters = (Characters) event; + writer.add(eventFactory.createCharacters(interpolateString(characters.getData(), node))); + break; + } + case XMLStreamConstants.CDATA: { + Characters characters = (Characters) event; + writer.add(eventFactory.createCData(interpolateString(characters.getData(), node))); + break; + } + default: + writer.add(event); + } + } + } + + private static List<XMLEvent> consumeTree(StartElement start, Iterator<XMLEvent> reader) throws XMLStreamException { + List<XMLEvent> events = new ArrayList<XMLEvent>(); + events.add(start); + Deque<QName> elementStack = new LinkedList<QName>(); + while (reader.hasNext()) { + XMLEvent event = reader.next(); + events.add(event); + switch (event.getEventType()) { + case XMLStreamConstants.START_ELEMENT: + elementStack.addLast(((StartElement) event).getName()); + break; + case XMLStreamConstants.END_ELEMENT: + if (elementStack.isEmpty()) { + return events; + } else { + if (!elementStack.removeLast().equals(((EndElement) event).getName())) + throw new IllegalStateException("End element mismatch"); + } + break; + default: + } + } + throw new IllegalStateException("Reader exhausted before end element found"); + } + + @SuppressWarnings("unchecked") + private static StartElement interpolateAttributes(StartElement start, RDFNode node) { + Set<Attribute> replacementAttributes = new LinkedHashSet<Attribute>(); + for (Iterator<Attribute> it = start.getAttributes(); it.hasNext(); ) { + Attribute attribute = it.next(); + String replacementValue = attribute.getValue(); + if (!attribute.getName().getNamespaceURI().equals(NS)) // skip rdf: attributes + replacementValue = interpolateString(attribute.getValue(), node); + replacementAttributes.add(eventFactory.createAttribute(attribute.getName(), + replacementValue)); + } + return cloneStartWithAttributes(start, replacementAttributes); + } + + private static StartElement cloneStartWithAttributes(StartElement start, Iterable<Attribute> attributes) { + return eventFactory.createStartElement( + start.getName().getPrefix(), + start.getName().getNamespaceURI(), + start.getName().getLocalPart(), + attributes.iterator(), + start.getNamespaces(), + start.getNamespaceContext()); + } + + private static final Pattern SUBSTITUTION_PATTERN = Pattern.compile("\\$\\{([^}]*)\\}"); + public static String interpolateString(String template, RDFNode node) { + if (!SUBSTITUTION_PATTERN.matcher(template).find()) { + return template; // fast path + } + StringBuffer substituted = new StringBuffer(); + Matcher matcher = SUBSTITUTION_PATTERN.matcher(template); + while (matcher.find()) { + String expression = matcher.group(1); + Object replacement = SelectorParser.parse(expression).singleResult(node); + + String replacementValue; + if (replacement instanceof RDFNode) { + RDFNode replacementNode = (RDFNode) replacement; + if (replacementNode.isLiteral()) { + Literal replacementLiteral = (Literal) replacementNode; + replacementValue = replacementLiteral.getValue().toString(); + } else { + throw new UnsupportedOperationException("Not a literal: " + replacementNode); + } + } else if (replacement instanceof String) { + replacementValue = (String) replacement; + } else { + throw new UnsupportedOperationException("Not an RDFNode: " + replacement); + } + + matcher.appendReplacement(substituted, replacementValue.replace("$", "\\$"));; + } + matcher.appendTail(substituted); + return substituted.toString(); + } + + private static void writeTreeForContent(XMLEventWriter writer, StartElement start, Object replacement) + throws XMLStreamException { + if (replacement instanceof RDFNode) { + RDFNode replacementNode = (RDFNode) replacement; + if (replacementNode.isLiteral()) { + Literal literal = (Literal) replacementNode; + Set<Attribute> attributes = cloneAttributesWithout(start, CONTENT_ACTION_QNAME); + + if (!StringUtils.isEmpty(literal.getLanguage())) { + attributes.add(eventFactory.createAttribute(XML_LANG_QNAME, literal.getLanguage())); + if (start.getName().getNamespaceURI().equals(XHTML_NS_URI)) { + String xhtmlPrefixInContext = start.getNamespaceContext().getPrefix(XHTML_NS_URI); + QName xhtmlLangQNameForContext; // ugh + if (xhtmlPrefixInContext.isEmpty()) + xhtmlLangQNameForContext = new QName("lang"); + else + xhtmlLangQNameForContext = new QName(XHTML_NS_URI, "lang", xhtmlPrefixInContext); + attributes.add(eventFactory.createAttribute(xhtmlLangQNameForContext, literal.getLanguage())); + } + } + + writer.add(eventFactory.createStartElement(start.getName(), attributes.iterator(), start.getNamespaces())); + writer.add(eventFactory.createCharacters(literal.getValue().toString())); + writer.add(eventFactory.createEndElement(start.getName(), start.getNamespaces())); + } else { + throw new UnsupportedOperationException("Not a literal: " + replacementNode); + } + } else { + throw new UnsupportedOperationException("Not an RDFNode: " + replacement); + } + } + + @SuppressWarnings("unchecked") + private static Set<Attribute> cloneAttributesWithout(StartElement start, QName omit) { + // clone attributes, but without rdf:content + Set<Attribute> attributes = new LinkedHashSet<Attribute>(); + for (Iterator<Attribute> it = start.getAttributes(); it.hasNext(); ) { + Attribute attribute = it.next(); + if (!attribute.getName().equals(omit)) + attributes.add(attribute); + } + return attributes; + } + +} diff --git a/src/main/java/au/com/miskinhill/rdftemplate/TemplateSyntaxException.java b/src/main/java/au/com/miskinhill/rdftemplate/TemplateSyntaxException.java @@ -0,0 +1,11 @@ +package au.com.miskinhill.rdftemplate; + +public class TemplateSyntaxException extends RuntimeException { + + private static final long serialVersionUID = 6518982504570154030L; + + public TemplateSyntaxException(String message) { + super(message); + } + +} diff --git a/src/main/java/au/com/miskinhill/rdftemplate/datatype/DateDataType.java b/src/main/java/au/com/miskinhill/rdftemplate/datatype/DateDataType.java @@ -0,0 +1,107 @@ +package au.com.miskinhill.rdftemplate.datatype; + +import java.util.Arrays; +import java.util.List; + +import com.hp.hpl.jena.datatypes.DatatypeFormatException; +import com.hp.hpl.jena.datatypes.RDFDatatype; +import com.hp.hpl.jena.datatypes.TypeMapper; +import com.hp.hpl.jena.graph.impl.LiteralLabel; +import org.joda.time.LocalDate; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; + +public class DateDataType implements RDFDatatype { + + public static final String URI = "http://www.w3.org/TR/xmlschema-2/#date"; + + public static final DateDataType instance = new DateDataType(); + public static void register() { + TypeMapper.getInstance().registerDatatype(instance); + } + + private final List<DateTimeFormatter> parsers = Arrays.asList( + DateTimeFormat.forPattern("yyyy"), + DateTimeFormat.forPattern("yyyy-mm"), + DateTimeFormat.forPattern("yyyy-mm-dd")); + + private DateDataType() { + } + + @Override + public String getURI() { + return URI; + } + + @Override + public Class<LocalDate> getJavaClass() { + return LocalDate.class; + } + + @Override + public String unparse(Object value) { + LocalDate date = (LocalDate) value; + return date.toString(); + } + + @Override + public Object cannonicalise(Object value) { + return value; + } + + @Override + public Object extendedTypeDefinition() { + return null; + } + + @Override + public int getHashCode(LiteralLabel lit) { + return lit.getValue().hashCode(); + } + + @Override + public boolean isEqual(LiteralLabel left, LiteralLabel right) { + return left.getValue().equals(right.getValue()); + } + + @Override + public LocalDate parse(String lexicalForm) throws DatatypeFormatException { + for (DateTimeFormatter parser: parsers) { + try { + return parser.parseDateTime(lexicalForm).toLocalDate(); + } catch (IllegalArgumentException e) { + // pass + } + } + throw new DatatypeFormatException(lexicalForm, this, "No matching parsers found"); + } + + @Override + public boolean isValid(String lexicalForm) { + for (DateTimeFormatter parser: parsers) { + try { + parser.parseDateTime(lexicalForm); + return true; + } catch (IllegalArgumentException e) { + // pass + } + } + return false; + } + + @Override + public boolean isValidLiteral(LiteralLabel lit) { + return lit.getDatatypeURI().equals(URI) && isValid(lit.getLexicalForm()); + } + + @Override + public boolean isValidValue(Object valueForm) { + return (valueForm instanceof LocalDate); + } + + @Override + public RDFDatatype normalizeSubType(Object value, RDFDatatype dt) { + return dt; + } + +} +\ No newline at end of file diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/AbstractSelector.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/AbstractSelector.java @@ -0,0 +1,40 @@ +package au.com.miskinhill.rdftemplate.selector; + +import java.util.List; + +import com.hp.hpl.jena.rdf.model.RDFNode; + +public abstract class AbstractSelector<T> implements Selector<T> { + + private final Class<T> resultType; + + protected AbstractSelector(Class<T> resultType) { + this.resultType = resultType; + } + + @Override + public abstract List<T> result(RDFNode node); + + @Override + public T singleResult(RDFNode node) { + List<T> results = result(node); + if (results.size() != 1) { + throw new SelectorEvaluationException("Expected exactly one result but got " + results); + } + return results.get(0); + } + + @Override + public Class<T> getResultType() { + return resultType; + } + + @Override + public <Other> Selector<Other> withResultType(Class<Other> otherType) { + if (!otherType.isAssignableFrom(resultType)) { + throw new ClassCastException("Result type " + resultType + " incompatible with requested type " + otherType); + } + return (Selector<Other>) this; + } + +} +\ No newline at end of file diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/Adaptation.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/Adaptation.java @@ -0,0 +1,11 @@ +package au.com.miskinhill.rdftemplate.selector; + +import com.hp.hpl.jena.rdf.model.RDFNode; + +public interface Adaptation<T> { + + Class<T> getDestinationType(); + + T adapt(RDFNode node); + +} diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/ComparableLiteralValueAdaptation.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/ComparableLiteralValueAdaptation.java @@ -0,0 +1,26 @@ +package au.com.miskinhill.rdftemplate.selector; + +import com.hp.hpl.jena.rdf.model.Literal; +import com.hp.hpl.jena.rdf.model.RDFNode; + +public class ComparableLiteralValueAdaptation implements Adaptation<Comparable> { + + @Override + public Class<Comparable> getDestinationType() { + return Comparable.class; + } + + @Override + public Comparable<?> adapt(RDFNode node) { + if (!node.isLiteral()) { + throw new SelectorEvaluationException("Attempted to apply #comparable-lv to non-literal node " + node); + } + Object literalValue = ((Literal) node).getValue(); + if (!(literalValue instanceof Comparable<?>)) { + throw new SelectorEvaluationException("Attempted to apply #comparable-lv to non-Comparable node " + node + + " with literal value of type " + literalValue.getClass()); + } + return (Comparable<?>) literalValue; + } + +} diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/InvalidSelectorSyntaxException.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/InvalidSelectorSyntaxException.java @@ -0,0 +1,13 @@ +package au.com.miskinhill.rdftemplate.selector; + +import org.antlr.runtime.RecognitionException; + +public class InvalidSelectorSyntaxException extends RuntimeException { + + private static final long serialVersionUID = 5805546105865617336L; + + public InvalidSelectorSyntaxException(RecognitionException e) { + super(e); + } + +} diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/LiteralValueAdaptation.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/LiteralValueAdaptation.java @@ -0,0 +1,21 @@ +package au.com.miskinhill.rdftemplate.selector; + +import com.hp.hpl.jena.rdf.model.Literal; +import com.hp.hpl.jena.rdf.model.RDFNode; + +public class LiteralValueAdaptation implements Adaptation<Object> { + + @Override + public Class<Object> getDestinationType() { + return Object.class; + } + + @Override + public Object adapt(RDFNode node) { + if (!node.isLiteral()) { + throw new SelectorEvaluationException("Attempted to apply #comparable-lv to non-literal node " + node); + } + return ((Literal) node).getValue(); + } + +} diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/NoopSelector.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/NoopSelector.java @@ -0,0 +1,19 @@ +package au.com.miskinhill.rdftemplate.selector; + +import java.util.Collections; +import java.util.List; + +import com.hp.hpl.jena.rdf.model.RDFNode; + +public class NoopSelector extends AbstractSelector<RDFNode> { + + public NoopSelector() { + super(RDFNode.class); + } + + @Override + public List<RDFNode> result(RDFNode node) { + return Collections.singletonList(node); + } + +} diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/Predicate.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/Predicate.java @@ -0,0 +1,7 @@ +package au.com.miskinhill.rdftemplate.selector; + +import com.hp.hpl.jena.rdf.model.RDFNode; + +public interface Predicate extends org.apache.commons.collections15.Predicate<RDFNode> { + +} diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/Selector.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/Selector.java @@ -0,0 +1,17 @@ +package au.com.miskinhill.rdftemplate.selector; + +import java.util.List; + +import com.hp.hpl.jena.rdf.model.RDFNode; + +public interface Selector<T> { + + List<T> result(RDFNode node); + + T singleResult(RDFNode node); + + Class<T> getResultType(); + + <Other> Selector<Other> withResultType(Class<Other> otherType); + +} diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/SelectorEvaluationException.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/SelectorEvaluationException.java @@ -0,0 +1,11 @@ +package au.com.miskinhill.rdftemplate.selector; + +public class SelectorEvaluationException extends RuntimeException { + + private static final long serialVersionUID = -398277800899471325L; + + public SelectorEvaluationException(String message) { + super(message); + } + +} diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/SelectorWithAdaptation.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/SelectorWithAdaptation.java @@ -0,0 +1,48 @@ +package au.com.miskinhill.rdftemplate.selector; + +import java.util.ArrayList; +import java.util.List; + +import com.hp.hpl.jena.rdf.model.RDFNode; +import org.apache.commons.lang.builder.ToStringBuilder; + +public class SelectorWithAdaptation<T> extends AbstractSelector<T> { + + private final Selector<RDFNode> baseSelector; + private final Adaptation<T> adaptation; + + public SelectorWithAdaptation(Selector<RDFNode> baseSelector, Adaptation<T> adaptation) { + super(adaptation.getDestinationType()); + this.baseSelector = baseSelector; + this.adaptation = adaptation; + } + + @Override + public String toString() { + return new ToStringBuilder(this).append(baseSelector).append(adaptation).toString(); + } + + @Override + public List<T> result(RDFNode node) { + List<RDFNode> baseResults = baseSelector.result(node); + List<T> results = new ArrayList<T>(); + for (RDFNode resultNode: baseResults) { + results.add(adaptation.adapt(resultNode)); + } + return results; + } + + @Override + public T singleResult(RDFNode node) { + return adaptation.adapt(baseSelector.singleResult(node)); + } + + public Selector<RDFNode> getBaseSelector() { + return baseSelector; + } + + public Adaptation<T> getAdaptation() { + return adaptation; + } + +} +\ No newline at end of file diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/Traversal.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/Traversal.java @@ -0,0 +1,143 @@ +package au.com.miskinhill.rdftemplate.selector; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.apache.commons.collections15.CollectionUtils; + +import com.hp.hpl.jena.rdf.model.Property; +import com.hp.hpl.jena.rdf.model.RDFNode; +import com.hp.hpl.jena.rdf.model.ResIterator; +import com.hp.hpl.jena.rdf.model.Resource; +import com.hp.hpl.jena.rdf.model.StmtIterator; +import org.apache.commons.lang.builder.ToStringBuilder; + +import au.com.miskinhill.rdftemplate.NamespacePrefixMapper; + +public class Traversal { + + private String propertyNamespacePrefix; + private String propertyLocalName; + private boolean inverse = false; + private Predicate predicate; + private Selector<? extends Comparable<?>> sortOrder; + private Comparator<RDFNode> _sortComparator; + private boolean reverseSorted = false; + private Integer subscript; + + public List<RDFNode> traverse(RDFNode node) { + if (!node.isResource()) { + throw new SelectorEvaluationException("Attempted to traverse non-resource node " + node); + } + Resource resource = (Resource) node; + Property property = resource.getModel().createProperty( + NamespacePrefixMapper.getInstance().get(propertyNamespacePrefix), + propertyLocalName); + List<RDFNode> destinations = new ArrayList<RDFNode>(); + if (!inverse) { + for (StmtIterator it = resource.listProperties(property); it.hasNext(); ) { + destinations.add(it.nextStatement().getObject()); + } + } else { + for (ResIterator it = resource.getModel().listResourcesWithProperty(property, node); it.hasNext(); ) { + destinations.add(it.nextResource()); + } + } + if (_sortComparator != null) + Collections.sort(destinations, reverseSorted ? Collections.reverseOrder(_sortComparator) : _sortComparator); + CollectionUtils.filter(destinations, predicate); + if (subscript != null) { + if (destinations.size() <= subscript) { + throw new SelectorEvaluationException("Cannot apply subscript " + subscript + " to nodes " + destinations); + } + destinations = Collections.singletonList(destinations.get(subscript)); + } + return destinations; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .append("propertyNamespacePrefix", propertyNamespacePrefix) + .append("propertyLocalName", propertyLocalName) + .append("inverse", inverse) + .append("predicate", predicate) + .append("sortOrder", sortOrder) + .append("reverseSorted", reverseSorted) + .append("subscript", subscript) + .toString(); + } + + public String getPropertyLocalName() { + return propertyLocalName; + } + + public void setPropertyLocalName(String propertyLocalName) { + this.propertyLocalName = propertyLocalName; + } + + public String getPropertyNamespacePrefix() { + return propertyNamespacePrefix; + } + + public void setPropertyNamespacePrefix(String propertyNamespacePrefix) { + this.propertyNamespacePrefix = propertyNamespacePrefix; + } + + public boolean isInverse() { + return inverse; + } + + public void setInverse(boolean inverse) { + this.inverse = inverse; + } + + public Predicate getPredicate() { + return predicate; + } + + public void setPredicate(Predicate predicate) { + this.predicate = predicate; + } + + public Selector<?> getSortOrder() { + return sortOrder; + } + + private static final class SelectorComparator<T extends Comparable<T>> implements Comparator<RDFNode> { + private final Selector<T> selector; + public SelectorComparator(Selector<T> selector) { + this.selector = selector; + } + @Override + public int compare(RDFNode left, RDFNode right) { + T leftKey = selector.singleResult(left); + T rightKey = selector.singleResult(right); + return leftKey.compareTo(rightKey); + } + } + + public <T extends Comparable<T>> void setSortOrder(Selector<T> sortOrder) { + this.sortOrder = sortOrder; + this._sortComparator = new SelectorComparator<T>(sortOrder); + } + + public boolean isReverseSorted() { + return reverseSorted; + } + + public void setReverseSorted(boolean reverseSorted) { + this.reverseSorted = reverseSorted; + } + + public Integer getSubscript() { + return subscript; + } + + public void setSubscript(Integer subscript) { + this.subscript = subscript; + } + +} diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/TraversingSelector.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/TraversingSelector.java @@ -0,0 +1,46 @@ +package au.com.miskinhill.rdftemplate.selector; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import com.hp.hpl.jena.rdf.model.RDFNode; +import org.apache.commons.lang.builder.ToStringBuilder; + +public class TraversingSelector extends AbstractSelector<RDFNode> { + + private final List<Traversal> traversals = new ArrayList<Traversal>(); + + public TraversingSelector() { + super(RDFNode.class); + } + + @Override + public List<RDFNode> result(RDFNode node) { + Set<RDFNode> current = Collections.singleton(node); + for (Traversal traversal: traversals) { + LinkedHashSet<RDFNode> destinationsUnion = new LinkedHashSet<RDFNode>(); + for (RDFNode start: current) { + destinationsUnion.addAll(traversal.traverse(start)); + } + current = destinationsUnion; + } + return new ArrayList<RDFNode>(current); + } + + @Override + public String toString() { + return new ToStringBuilder(this).append(traversals).toString(); + } + + public List<Traversal> getTraversals() { + return traversals; + } + + public void addTraversal(Traversal traversal) { + traversals.add(traversal); + } + +} diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/UriAdaptation.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/UriAdaptation.java @@ -0,0 +1,22 @@ +package au.com.miskinhill.rdftemplate.selector; + +import com.hp.hpl.jena.rdf.model.Resource; + +import com.hp.hpl.jena.rdf.model.RDFNode; + +public class UriAdaptation implements Adaptation<String> { + + @Override + public Class<String> getDestinationType() { + return String.class; + } + + @Override + public String adapt(RDFNode node) { + if (!node.isResource()) { + throw new SelectorEvaluationException("Attempted to apply #uri to non-resource node " + node); + } + return ((Resource) node).getURI(); + } + +} diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/UriPrefixPredicate.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/UriPrefixPredicate.java @@ -0,0 +1,26 @@ +package au.com.miskinhill.rdftemplate.selector; + +import com.hp.hpl.jena.rdf.model.RDFNode; +import com.hp.hpl.jena.rdf.model.Resource; + +public class UriPrefixPredicate implements Predicate { + + private final String prefix; + + public UriPrefixPredicate(String prefix) { + this.prefix = prefix; + } + + public String getPrefix() { + return prefix; + } + + @Override + public boolean evaluate(RDFNode node) { + if (!node.isResource()) { + throw new SelectorEvaluationException("Attempted to apply [uri-prefix] to non-resource node " + node); + } + return ((Resource) node).getURI().startsWith(prefix); + } + +} diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/UriSliceAdaptation.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/UriSliceAdaptation.java @@ -0,0 +1,32 @@ +package au.com.miskinhill.rdftemplate.selector; + +import com.hp.hpl.jena.rdf.model.RDFNode; +import com.hp.hpl.jena.rdf.model.Resource; + +public class UriSliceAdaptation implements Adaptation<String> { + + private final Integer startIndex; + + public UriSliceAdaptation(Integer startIndex) { + this.startIndex = startIndex; + } + + public Integer getStartIndex() { + return startIndex; + } + + @Override + public Class<String> getDestinationType() { + return String.class; + } + + @Override + public String adapt(RDFNode node) { + if (!node.isResource()) { + throw new SelectorEvaluationException("Attempted to apply #uri-slice to non-resource node " + node); + } + String uri = ((Resource) node).getURI(); + return uri.substring(startIndex); + } + +} diff --git a/src/test/java/au/com/miskinhill/rdftemplate/TemplateInterpolatorUnitTest.java b/src/test/java/au/com/miskinhill/rdftemplate/TemplateInterpolatorUnitTest.java @@ -0,0 +1,74 @@ +package au.com.miskinhill.rdftemplate; + +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.*; +import static org.junit.matchers.JUnitMatchers.containsString; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; + +import com.hp.hpl.jena.rdf.model.Model; +import com.hp.hpl.jena.rdf.model.ModelFactory; +import com.hp.hpl.jena.rdf.model.Resource; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import au.com.miskinhill.rdftemplate.datatype.DateDataType; + +public class TemplateInterpolatorUnitTest { + + @BeforeClass + public static void ensureDatatypesRegistered() { + DateDataType.register(); + } + + private Model model; + + @Before + public void setUp() { + model = ModelFactory.createDefaultModel(); + InputStream stream = this.getClass().getResourceAsStream( + "/au/com/miskinhill/rdftemplate/test-data.xml"); + model.read(stream, ""); + } + + @Test + public void shouldReplaceSubtreesWithContent() throws Exception { + Resource journal = model.getResource("http://miskinhill.com.au/journals/test/"); + String result = TemplateInterpolator.interpolate( + new InputStreamReader(this.getClass().getResourceAsStream("replace-subtree.xml")), journal); + assertThat(result, containsString("<div xml:lang=\"en\" lang=\"en\">A journal, you know, with some stuff in it</div>")); + assertThat(result, not(containsString("<p>This should all go <em>away</em>!</p>"))); + } + + @Test + public void shouldWork() throws Exception { + Resource journal = model.getResource("http://miskinhill.com.au/journals/test/"); + String result = TemplateInterpolator.interpolate( + new InputStreamReader(this.getClass().getResourceAsStream("test-template.xml")), journal); + String expected = exhaust(this.getClass().getResource("test-template.out.xml").toURI()); + assertEquals(expected.trim(), result.trim()); + } + + private String exhaust(URI file) throws IOException { // sigh + FileChannel channel = new FileInputStream(new File(file)).getChannel(); + Charset charset = Charset.defaultCharset(); + StringBuffer sb = new StringBuffer(); + ByteBuffer b = ByteBuffer.allocate(8192); + while (channel.read(b) > 0) { + b.rewind(); + sb.append(charset.decode(b)); + b.flip(); + } + return sb.toString(); + } + +} diff --git a/src/test/java/au/com/miskinhill/rdftemplate/selector/AdaptationMatcher.java b/src/test/java/au/com/miskinhill/rdftemplate/selector/AdaptationMatcher.java @@ -0,0 +1,30 @@ +package au.com.miskinhill.rdftemplate.selector; + + +import static org.hamcrest.CoreMatchers.equalTo; + +public class AdaptationMatcher<T extends Adaptation<?>> extends BeanPropertyMatcher<T> { + + private AdaptationMatcher(Class<T> type) { + super(type); + } + + public static AdaptationMatcher<UriAdaptation> uriAdaptation() { + return new AdaptationMatcher<UriAdaptation>(UriAdaptation.class); + } + + public static AdaptationMatcher<UriSliceAdaptation> uriSliceAdaptation(Integer startIndex) { + AdaptationMatcher<UriSliceAdaptation> m = new AdaptationMatcher<UriSliceAdaptation>(UriSliceAdaptation.class); + m.addRequiredProperty("startIndex", equalTo(startIndex)); + return m; + } + + public static AdaptationMatcher<LiteralValueAdaptation> lvAdaptation() { + return new AdaptationMatcher<LiteralValueAdaptation>(LiteralValueAdaptation.class); + } + + public static AdaptationMatcher<ComparableLiteralValueAdaptation> comparableLVAdaptation() { + return new AdaptationMatcher<ComparableLiteralValueAdaptation>(ComparableLiteralValueAdaptation.class); + } + +} diff --git a/src/test/java/au/com/miskinhill/rdftemplate/selector/BeanPropertyMatcher.java b/src/test/java/au/com/miskinhill/rdftemplate/selector/BeanPropertyMatcher.java @@ -0,0 +1,56 @@ +package au.com.miskinhill.rdftemplate.selector; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.beanutils.PropertyUtils; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; + +public class BeanPropertyMatcher<T> extends BaseMatcher<T> { + + private final Class<? extends T> matchedType; + private final Map<String, Matcher<?>> requiredProperties = new LinkedHashMap<String, Matcher<?>>(); + + public BeanPropertyMatcher(Class<? extends T> type) { + this.matchedType = type; + } + + public void addRequiredProperty(String name, Matcher<?> matcher) { + requiredProperties.put(name, matcher); + } + + @Override + public boolean matches(Object arg0) { + if (!matchedType.isInstance(arg0)) + return false; + for (Map.Entry<String, Matcher<?>> property: requiredProperties.entrySet()) { + try { + Object beanProperty = PropertyUtils.getProperty(arg0, property.getKey()); + if (!property.getValue().matches(beanProperty)) + return false; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return true; + } + + @Override + public void describeTo(Description desc) { + desc.appendText(matchedType.getName()); + desc.appendText("["); + boolean first = true; + for (Map.Entry<String, Matcher<?>> property: requiredProperties.entrySet()) { + if (!first) + desc.appendText(","); + desc.appendText(property.getKey()); + desc.appendText("="); + property.getValue().describeTo(desc); + first = false; + } + desc.appendText("]"); + } + +} diff --git a/src/test/java/au/com/miskinhill/rdftemplate/selector/PredicateMatcher.java b/src/test/java/au/com/miskinhill/rdftemplate/selector/PredicateMatcher.java @@ -0,0 +1,17 @@ +package au.com.miskinhill.rdftemplate.selector; + +import static org.hamcrest.CoreMatchers.equalTo; + +public class PredicateMatcher<T extends Predicate> extends BeanPropertyMatcher<T> { + + private PredicateMatcher(Class<T> type) { + super(type); + } + + public static PredicateMatcher<UriPrefixPredicate> uriPrefixPredicate(String prefix) { + PredicateMatcher<UriPrefixPredicate> m = new PredicateMatcher<UriPrefixPredicate>(UriPrefixPredicate.class); + m.addRequiredProperty("prefix", equalTo(prefix)); + return m; + } + +} diff --git a/src/test/java/au/com/miskinhill/rdftemplate/selector/SelectorEvaluationUnitTest.java b/src/test/java/au/com/miskinhill/rdftemplate/selector/SelectorEvaluationUnitTest.java @@ -0,0 +1,137 @@ +package au.com.miskinhill.rdftemplate.selector; + +import static org.junit.matchers.JUnitMatchers.hasItems; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +import java.io.InputStream; +import java.util.List; + +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.RDFNode; +import com.hp.hpl.jena.rdf.model.Resource; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import au.com.miskinhill.rdftemplate.datatype.DateDataType; + +public class SelectorEvaluationUnitTest { + + private Model m; + private Resource journal, issue, article, author, book, review, anotherReview, obituary, en, ru; + + @BeforeClass + public static void ensureDatatypesRegistered() { + DateDataType.register(); + } + + @Before + public void setUp() { + m = ModelFactory.createDefaultModel(); + InputStream stream = this.getClass().getResourceAsStream("/au/com/miskinhill/rdftemplate/test-data.xml"); + m.read(stream, ""); + journal = m.createResource("http://miskinhill.com.au/journals/test/"); + issue = m.createResource("http://miskinhill.com.au/journals/test/1:1/"); + article = m.createResource("http://miskinhill.com.au/journals/test/1:1/article"); + author = m.createResource("http://miskinhill.com.au/authors/test-author"); + book = m.createResource("http://miskinhill.com.au/cited/books/test"); + review = m.createResource("http://miskinhill.com.au/journals/test/1:1/reviews/review"); + anotherReview = m.createResource("http://miskinhill.com.au/journals/test/2:1/reviews/another-review"); + obituary = m.createResource("http://miskinhill.com.au/journals/test/1:1/in-memoriam-john-doe"); + en = m.createResource("http://www.lingvoj.org/lang/en"); + ru = m.createResource("http://www.lingvoj.org/lang/ru"); + } + + @Test + public void shouldEvaluateTraversal() { + RDFNode result = SelectorParser.parse("dc:creator").withResultType(RDFNode.class).singleResult(article); + assertThat(result, equalTo((RDFNode) author)); + } + + @Test + public void shouldEvaluateMultipleTraversals() throws Exception { + RDFNode result = SelectorParser.parse("dc:creator/foaf:name") + .withResultType(RDFNode.class).singleResult(article); + assertThat(((Literal) result).getString(), equalTo("Test Author")); + } + + @Test + public void shouldEvaluateInverseTraversal() throws Exception { + List<RDFNode> results = SelectorParser.parse("!mhs:isIssueOf/!dc:isPartOf") + .withResultType(RDFNode.class).result(journal); + assertThat(results.size(), equalTo(4)); + assertThat(results, hasItems((RDFNode) article, (RDFNode) review, (RDFNode) anotherReview, (RDFNode) obituary)); + } + + @Test + public void shouldEvaluateSortOrder() throws Exception { + List<RDFNode> results = SelectorParser.parse("dc:language(lingvoj:iso1#comparable-lv)") + .withResultType(RDFNode.class).result(journal); + assertThat(results.size(), equalTo(2)); + assertThat(results.get(0), equalTo((RDFNode) en)); + assertThat(results.get(1), equalTo((RDFNode) ru)); + } + + @Test + public void shouldEvaluateReverseSortOrder() throws Exception { + List<RDFNode> results = SelectorParser.parse("dc:language(~lingvoj:iso1#comparable-lv)") + .withResultType(RDFNode.class).result(journal); + assertThat(results.size(), equalTo(2)); + assertThat(results.get(0), equalTo((RDFNode) ru)); + assertThat(results.get(1), equalTo((RDFNode) en)); + } + + @Test + public void shouldEvaluateComplexSortOrder() throws Exception { + List<RDFNode> results = SelectorParser.parse("!mhs:reviews(dc:isPartOf/mhs:publicationDate#comparable-lv)") + .withResultType(RDFNode.class).result(book); + assertThat(results.size(), equalTo(2)); + assertThat(results.get(0), equalTo((RDFNode) review)); + assertThat(results.get(1), equalTo((RDFNode) anotherReview)); + } + + @Test + public void shouldEvaluateUriAdaptation() throws Exception { + String result = SelectorParser.parse("mhs:coverThumbnail#uri") + .withResultType(String.class).singleResult(issue); + assertThat(result, equalTo("http://miskinhill.com.au/journals/test/1:1/cover.thumb.jpg")); + } + + @Test + public void shouldEvaluateBareUriAdaptation() throws Exception { + String result = SelectorParser.parse("#uri").withResultType(String.class).singleResult(journal); + assertThat(result, equalTo("http://miskinhill.com.au/journals/test/")); + } + + @Test + public void shouldEvaluateUriSliceAdaptation() throws Exception { + String result = SelectorParser.parse("dc:identifier[uri-prefix='urn:issn:']#uri-slice(9)") + .withResultType(String.class).singleResult(journal); + assertThat(result, equalTo("12345678")); + } + + @Test + public void shouldEvaluateSubscript() throws Exception { + String result = SelectorParser.parse( + "!mhs:isIssueOf(~mhs:publicationDate#comparable-lv)[0]/mhs:coverThumbnail#uri") + .withResultType(String.class).singleResult(journal); + assertThat(result, equalTo("http://miskinhill.com.au/journals/test/2:1/cover.thumb.jpg")); + result = SelectorParser.parse( + "!mhs:isIssueOf(mhs:publicationDate#comparable-lv)[0]/mhs:coverThumbnail#uri") + .withResultType(String.class).singleResult(journal); + assertThat(result, equalTo("http://miskinhill.com.au/journals/test/1:1/cover.thumb.jpg")); + } + + @Test + public void shouldEvaluateLVAdaptation() throws Exception { + List<Object> results = SelectorParser.parse("dc:language/lingvoj:iso1#lv") + .withResultType(Object.class).result(journal); + assertThat(results.size(), equalTo(2)); + assertThat(results, hasItems((Object) "en", (Object) "ru")); + } + +} diff --git a/src/test/java/au/com/miskinhill/rdftemplate/selector/SelectorMatcher.java b/src/test/java/au/com/miskinhill/rdftemplate/selector/SelectorMatcher.java @@ -0,0 +1,30 @@ +package au.com.miskinhill.rdftemplate.selector; + + +import static org.junit.matchers.JUnitMatchers.hasItems; + +import org.hamcrest.Matcher; + +public class SelectorMatcher<T extends Selector<?>> extends BeanPropertyMatcher<T> { + + private SelectorMatcher(Class<? extends T> type) { + super(type); + } + + public static SelectorMatcher<Selector<?>> selector(Matcher<Traversal>... traversals) { + if (traversals.length == 0) { + return new SelectorMatcher<Selector<?>>(NoopSelector.class); + } + SelectorMatcher<Selector<?>> m = new SelectorMatcher<Selector<?>>(TraversingSelector.class); + m.addRequiredProperty("traversals", hasItems(traversals)); + return m; + } + + public <A> SelectorMatcher<Selector<?>> withAdaptation(Matcher<? extends Adaptation<A>> adaptation) { + SelectorMatcher<Selector<?>> m = new SelectorMatcher(SelectorWithAdaptation.class); + m.addRequiredProperty("baseSelector", this); + m.addRequiredProperty("adaptation", adaptation); + return m; + } + +} diff --git a/src/test/java/au/com/miskinhill/rdftemplate/selector/SelectorParserUnitTest.java b/src/test/java/au/com/miskinhill/rdftemplate/selector/SelectorParserUnitTest.java @@ -0,0 +1,134 @@ +package au.com.miskinhill.rdftemplate.selector; + +import static au.com.miskinhill.rdftemplate.selector.AdaptationMatcher.*; +import static au.com.miskinhill.rdftemplate.selector.PredicateMatcher.*; +import static au.com.miskinhill.rdftemplate.selector.SelectorMatcher.selector; +import static au.com.miskinhill.rdftemplate.selector.TraversalMatcher.traversal; + +import static org.junit.Assert.assertThat; + +import org.junit.Test; + +public class SelectorParserUnitTest { + + @Test + public void shouldRecogniseSingleTraversal() throws Exception { + Selector<?> selector = SelectorParser.parse("dc:creator"); + assertThat(selector, selector(traversal("dc", "creator"))); + } + + @Test + public void shouldRecogniseMultipleTraversals() throws Exception { + Selector<?> selector = SelectorParser.parse("dc:creator/foaf:name"); + assertThat(selector, selector( + traversal("dc", "creator"), + traversal("foaf", "name"))); + } + + @Test + public void shouldRecogniseInverseTraversal() throws Exception { + Selector<?> selector = SelectorParser.parse("!dc:isPartOf/!dc:isPartOf"); + assertThat(selector, selector( + traversal("dc", "isPartOf").inverse(), + traversal("dc", "isPartOf").inverse())); + } + + @Test + public void shouldRecogniseSortOrder() throws Exception { + Selector<?> selector = SelectorParser.parse("!mhs:isIssueOf(mhs:publicationDate#comparable-lv)"); + assertThat(selector, selector( + traversal("mhs", "isIssueOf").inverse() + .withSortOrder(selector(traversal("mhs", "publicationDate")) + .withAdaptation(comparableLVAdaptation())))); + } + + @Test + public void shouldRecogniseReverseSortOrder() throws Exception { + Selector<?> selector = SelectorParser.parse("!mhs:isIssueOf(~mhs:publicationDate#comparable-lv)"); + assertThat(selector, selector( + traversal("mhs", "isIssueOf").inverse() + .withSortOrder(selector(traversal("mhs", "publicationDate")) + .withAdaptation(comparableLVAdaptation())) + .reverseSorted())); + } + + @Test + public void shouldRecogniseComplexSortOrder() throws Exception { + Selector<?> selector = SelectorParser.parse("!mhs:reviews(dc:isPartOf/mhs:publicationDate#comparable-lv)"); + assertThat(selector, selector( + traversal("mhs", "reviews") + .withSortOrder(selector(traversal("dc", "isPartOf"), traversal("mhs", "publicationDate")) + .withAdaptation(comparableLVAdaptation())))); + } + + @Test + public void shouldRecogniseUriAdaptation() throws Exception { + Selector<?> selector = SelectorParser.parse("mhs:coverThumbnail#uri"); + assertThat(selector, selector( + traversal("mhs", "coverThumbnail")) + .withAdaptation(uriAdaptation())); + } + + @Test + public void shouldRecogniseBareUriAdaptation() throws Exception { + Selector<?> selector = SelectorParser.parse("#uri"); + assertThat(selector, selector().withAdaptation(uriAdaptation())); + } + + @Test + public void shouldRecogniseUriSliceAdaptation() throws Exception { + Selector<?> selector = SelectorParser.parse("dc:identifier[uri-prefix='urn:issn:']#uri-slice(9)"); + assertThat(selector, selector( + traversal("dc", "identifier") + .withPredicate(uriPrefixPredicate("urn:issn:"))) + .withAdaptation(uriSliceAdaptation(9))); + } + + @Test + public void shouldRecogniseUriPrefixPredicate() throws Exception { + Selector<?> selector = SelectorParser.parse( + "!mhs:isIssueOf[uri-prefix='http://miskinhill.com.au/journals/'](~mhs:publicationDate#comparable-lv)"); + assertThat(selector, selector( + traversal("mhs", "isIssueOf") + .inverse() + .withPredicate(uriPrefixPredicate("http://miskinhill.com.au/journals/")) + .withSortOrder(selector(traversal("mhs", "publicationDate")) + .withAdaptation(comparableLVAdaptation())) + .reverseSorted())); + } + + @Test + public void shouldRecogniseSubscript() throws Exception { + Selector<?> selector = SelectorParser.parse( + "!mhs:isIssueOf(~mhs:publicationDate#comparable-lv)[0]/mhs:coverThumbnail#uri"); + assertThat(selector, selector( + traversal("mhs", "isIssueOf") + .inverse() + .withSortOrder(selector(traversal("mhs", "publicationDate")) + .withAdaptation(comparableLVAdaptation())) + .reverseSorted() + .withSubscript(0), + traversal("mhs", "coverThumbnail")) + .withAdaptation(uriAdaptation())); + } + + @Test + public void shouldRecogniseLVAdaptation() throws Exception { + Selector<?> selector = SelectorParser.parse("dc:language/lingvoj:iso1#lv"); + assertThat(selector, selector( + traversal("dc", "language"), + traversal("lingvoj", "iso1")) + .withAdaptation(lvAdaptation())); + } + + @Test(expected = InvalidSelectorSyntaxException.class) + public void shouldThrowForInvalidSyntax() throws Exception { + SelectorParser.parse("dc:creator]["); // this is a parser error + } + + @Test(expected = InvalidSelectorSyntaxException.class) + public void shouldThrowForUnrecognisedCharacter() throws Exception { + SelectorParser.parse("dc:cre&ator"); // ... and this is a lexer error + } + +} diff --git a/src/test/java/au/com/miskinhill/rdftemplate/selector/TraversalMatcher.java b/src/test/java/au/com/miskinhill/rdftemplate/selector/TraversalMatcher.java @@ -0,0 +1,46 @@ +package au.com.miskinhill.rdftemplate.selector; + + +import static org.hamcrest.CoreMatchers.equalTo; + +import org.hamcrest.Matcher; + +public class TraversalMatcher extends BeanPropertyMatcher<Traversal> { + + private TraversalMatcher() { + super(Traversal.class); + } + + public static TraversalMatcher traversal(String propertyNamespacePrefix, String propertyLocalName) { + TraversalMatcher m = new TraversalMatcher(); + m.addRequiredProperty("propertyNamespacePrefix", equalTo(propertyNamespacePrefix)); + m.addRequiredProperty("propertyLocalName", equalTo(propertyLocalName)); + return m; + } + + public TraversalMatcher inverse() { + addRequiredProperty("inverse", equalTo(true)); + return this; + } + + public TraversalMatcher withPredicate(Matcher<? extends Predicate> predicate) { + addRequiredProperty("predicate", predicate); + return this; + } + + public TraversalMatcher withSortOrder(Matcher<Selector<?>> sortOrder) { + addRequiredProperty("sortOrder", sortOrder); + return this; + } + + public TraversalMatcher reverseSorted() { + addRequiredProperty("reverseSorted", equalTo(true)); + return this; + } + + public TraversalMatcher withSubscript(int subscript) { + addRequiredProperty("subscript", equalTo(subscript)); + return this; + } + +} diff --git a/src/test/resources/au/com/miskinhill/rdftemplate/replace-subtree.xml b/src/test/resources/au/com/miskinhill/rdftemplate/replace-subtree.xml @@ -0,0 +1,10 @@ +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:rdf="http://code.miskinhill.com.au/rdftemplate/"> +<body> + +<div rdf:content="dc:description"> + <p>This should all go <em>away</em>!</p> +</div> + +</body> +</html> +\ No newline at end of file diff --git a/src/test/resources/au/com/miskinhill/rdftemplate/test-data.xml b/src/test/resources/au/com/miskinhill/rdftemplate/test-data.xml @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="utf-8"?> +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:dc="http://purl.org/dc/terms/" + xmlns:foaf="http://xmlns.com/foaf/0.1/" + xmlns:contact="http://www.w3.org/2000/10/swap/pim/contact#" + xmlns:geonames="http://www.geonames.org/ontology#" + xmlns:lingvoj="http://www.lingvoj.org/ontology#" + xmlns:mhs="http://miskinhill.com.au/rdfschema/1.0/"> + <mhs:Journal rdf:about="http://miskinhill.com.au/journals/test/"> + <dc:title xml:lang="en">Test Journal of Good Stuff</dc:title> + <dc:publisher rdf:nodeID="pub"/> + <dc:identifier rdf:resource="urn:issn:12345678"/> + <dc:description xml:lang="en">A journal, you know, with some stuff in it</dc:description> + <dc:language rdf:resource="http://www.lingvoj.org/lang/en" /> + <dc:language rdf:resource="http://www.lingvoj.org/lang/ru" /> + <mhs:beginningDate>1987</mhs:beginningDate> + </mhs:Journal> + <lingvoj:Lingvo rdf:about="http://www.lingvoj.org/lang/en"> + <lingvoj:iso1>en</lingvoj:iso1> + <lingvoj:iso2b>eng</lingvoj:iso2b> + </lingvoj:Lingvo> + <lingvoj:Lingvo rdf:about="http://www.lingvoj.org/lang/ru"> + <lingvoj:iso1>ru</lingvoj:iso1> + <lingvoj:iso2b>rus</lingvoj:iso2b> + </lingvoj:Lingvo> + <mhs:Publisher rdf:nodeID="pub"> + <foaf:name xml:lang="en">Awesome Publishing House</foaf:name> + <foaf:homepage rdf:resource="http://awesome.com"/> + <contact:address rdf:resource="http://sws.geonames.org/2150650/"/> + </mhs:Publisher> + <geonames:Feature rdf:about="http://sws.geonames.org/2150650/"> + <geonames:name xml:lang="en">St Lucia, Qld.</geonames:name> + </geonames:Feature> + <mhs:Issue rdf:about="http://miskinhill.com.au/journals/test/1:1/"> + <mhs:isIssueOf rdf:resource="http://miskinhill.com.au/journals/test/"/> + <mhs:volume rdf:datatype="http://www.w3.org/2001/XMLSchema#integer">1</mhs:volume> + <mhs:issueNumber>1</mhs:issueNumber> + <dc:coverage rdf:datatype="http://www.w3.org/TR/xmlschema-2/#date">2007</dc:coverage> + <mhs:publicationDate rdf:datatype="http://www.w3.org/TR/xmlschema-2/#date">2008-02-01</mhs:publicationDate> + <mhs:onlinePublicationDate rdf:datatype="http://www.w3.org/TR/xmlschema-2/#date">2008-03-01</mhs:onlinePublicationDate> + <mhs:coverThumbnail rdf:resource="http://miskinhill.com.au/journals/test/1:1/cover.thumb.jpg"/> + </mhs:Issue> + <mhs:Issue rdf:about="http://miskinhill.com.au/journals/test/2:1/"> + <mhs:isIssueOf rdf:resource="http://miskinhill.com.au/journals/test/"/> + <mhs:volume rdf:datatype="http://www.w3.org/2001/XMLSchema#integer">2</mhs:volume> + <mhs:issueNumber>1</mhs:issueNumber> + <dc:coverage rdf:datatype="http://www.w3.org/TR/xmlschema-2/#date">2008</dc:coverage> + <mhs:publicationDate rdf:datatype="http://www.w3.org/TR/xmlschema-2/#date">2009-02-01</mhs:publicationDate> + <mhs:onlinePublicationDate rdf:datatype="http://www.w3.org/TR/xmlschema-2/#date">2009-03-01</mhs:onlinePublicationDate> + <mhs:coverThumbnail rdf:resource="http://miskinhill.com.au/journals/test/2:1/cover.thumb.jpg"/> + </mhs:Issue> + <mhs:Article rdf:about="http://miskinhill.com.au/journals/test/1:1/article"> + <dc:isPartOf rdf:resource="http://miskinhill.com.au/journals/test/1:1/"/> + <dc:creator rdf:resource="http://miskinhill.com.au/authors/test-author"/> + <dc:title rdf:parseType="Literal"><span xmlns="http://www.w3.org/1999/xhtml" lang="en"><em>Moscow 1937</em>: the interpreter’s story</span></dc:title> + <mhs:startPage rdf:datatype="http://www.w3.org/2001/XMLSchema#integer">5</mhs:startPage> + <mhs:endPage rdf:datatype="http://www.w3.org/2001/XMLSchema#integer">35</mhs:endPage> + </mhs:Article> + <mhs:Author rdf:about="http://miskinhill.com.au/authors/test-author"> + <foaf:name>Test Author</foaf:name> + <foaf:surname>Author</foaf:surname> + <foaf:givenNames>Test</foaf:givenNames> + </mhs:Author> + <mhs:Journal rdf:about="http://miskinhill.com.au/cited/journals/asdf/"> + <dc:title xml:lang="en">A Cited Journal</dc:title> + <dc:identifier rdf:resource="urn:issn:87654321"/> + </mhs:Journal> + <mhs:Issue rdf:about="http://miskinhill.com.au/cited/journals/asdf/1:1/"> + <mhs:isIssueOf rdf:resource="http://miskinhill.com.au/cited/journals/asdf/"/> + <mhs:volume rdf:datatype="http://www.w3.org/2001/XMLSchema#integer">1</mhs:volume> + <mhs:issueNumber>1</mhs:issueNumber> + </mhs:Issue> + <mhs:Article rdf:about="http://miskinhill.com.au/cited/journals/asdf/1:1/article"> + <dc:isPartOf rdf:resource="http://miskinhill.com.au/cited/journals/asdf/1:1/"/> + <dc:creator rdf:resource="http://miskinhill.com.au/authors/test-author"/> + <dc:creator rdf:resource="http://miskinhill.com.au/authors/another-author"/> + <dc:title>Boris Pasternak in August 1936: an NKVD memorandum</dc:title> + <mhs:availableFrom rdf:resource="http://example.com/teh-cited-article"/> + </mhs:Article> + <mhs:Author rdf:about="http://miskinhill.com.au/authors/another-author"> + <foaf:name>Another Author</foaf:name> + <foaf:surname>Author</foaf:surname> + <foaf:givenNames>Another</foaf:givenNames> + </mhs:Author> + <mhs:Book rdf:about="http://miskinhill.com.au/cited/books/test"> + <dc:identifier rdf:resource="urn:isbn:9780415274319"/> + <dc:identifier rdf:resource="urn:asin:0415274311"/> + <dc:title xml:lang="en">Slovenia: evolving loyalties</dc:title> + <dc:creator>John K. Cox</dc:creator> + <dc:publisher>Routledge</dc:publisher> + <dc:date rdf:datatype="http://www.w3.org/TR/xmlschema-2/#date">2005</dc:date> + </mhs:Book> + <mhs:Review rdf:about="http://miskinhill.com.au/journals/test/1:1/reviews/review"> + <dc:isPartOf rdf:resource="http://miskinhill.com.au/journals/test/1:1/"/> + <mhs:reviews rdf:resource="http://miskinhill.com.au/cited/books/test"/> + <dc:creator rdf:resource="http://miskinhill.com.au/authors/test-author"/> + </mhs:Review> + <mhs:Review rdf:about="http://miskinhill.com.au/journals/test/2:1/reviews/another-review"> + <dc:isPartOf rdf:resource="http://miskinhill.com.au/journals/test/2:1/"/> + <mhs:reviews rdf:resource="http://miskinhill.com.au/cited/books/test"/> + <dc:creator rdf:resource="http://miskinhill.com.au/authors/another-author"/> + </mhs:Review> + <mhs:Obituary rdf:about="http://miskinhill.com.au/journals/test/1:1/in-memoriam-john-doe"> + <dc:title xml:lang="en">In memoriam John Doe</dc:title> + <dc:isPartOf rdf:resource="http://miskinhill.com.au/journals/test/1:1/"/> + <dc:creator rdf:resource="http://miskinhill.com.au/authors/test-author"/> + <mhs:obituaryOf rdf:nodeID="person"/> + </mhs:Obituary> + <foaf:Person rdf:nodeID="person"> + <foaf:name>John Doe</foaf:name> + <mhs:dateOfBirth rdf:datatype="http://www.w3.org/TR/xmlschema-2/#date">1990-01-01</mhs:dateOfBirth> + <mhs:dateOfDeath rdf:datatype="http://www.w3.org/TR/xmlschema-2/#date">1991-12-31</mhs:dateOfDeath> + </foaf:Person> +</rdf:RDF> diff --git a/src/test/resources/au/com/miskinhill/rdftemplate/test-template.out.xml b/src/test/resources/au/com/miskinhill/rdftemplate/test-template.out.xml @@ -0,0 +1,46 @@ +<?xml version="1.0"?><html xmlns="http://www.w3.org/1999/xhtml" xmlns:rdf="http://code.miskinhill.com.au/rdftemplate/" lang="en"> + +<head> + <title xml:lang="en" lang="en">Test Journal of Good Stuff</title> +</head> + +<body> + +<h2><em xml:lang="en" lang="en">Test Journal of Good Stuff</em><abbr title="http://miskinhill.com.au/journals/test/" class="unapi-id"></abbr></h2> + +<div class="metabox-container"> + <div class="issue-meta metabox"> + <h4>Journal details</h4> + <p class="cover"> + <img alt="" src="http://miskinhill.com.au/journals/test/2:1/cover.thumb.jpg"></img> + </p> + <p class="publisher">Published by <a xml:lang="en" lang="en" href="http://awesome.com">Awesome Publishing House</a></p> + <p class="issn">ISSN 12345678</p> + <!-- <p>Metadata: + ${Markup(u', ').join(r(req, node).anchor() for r in representations.for_node(node) if r.format != 'html')} + </p> --> + </div> +</div> + +<h3>Issues available online</h3> +<ul> + <li> + <a href="http://miskinhill.com.au/journals/test/2:1/"> + Vol. 2, + Issue 1 + (2008-01-01) + </a> + </li><li> + <a href="http://miskinhill.com.au/journals/test/1:1/"> + Vol. 1, + Issue 1 + (2007-01-01) + </a> + </li> +</ul> + +<h3>About this journal</h3> +<div xml:lang="en" lang="en">A journal, you know, with some stuff in it</div> + +</body> +</html> +\ No newline at end of file diff --git a/src/test/resources/au/com/miskinhill/rdftemplate/test-template.xml b/src/test/resources/au/com/miskinhill/rdftemplate/test-template.xml @@ -0,0 +1,42 @@ +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:rdf="http://code.miskinhill.com.au/rdftemplate/" + lang="en"> + +<head> + <title rdf:content="dc:title" /> +</head> + +<body> + +<h2><em rdf:content="dc:title" /><abbr class="unapi-id" title="${#uri}" /></h2> + +<div class="metabox-container"> + <div class="issue-meta metabox"> + <h4>Journal details</h4> + <p class="cover"> + <img src="${!mhs:isIssueOf(~mhs:publicationDate#comparable-lv)[0]/mhs:coverThumbnail#uri}" alt="" /> + </p> + <p class="publisher">Published by <a href="${dc:publisher/foaf:homepage#uri}" rdf:content="dc:publisher/foaf:name" /></p> + <p class="issn">ISSN ${dc:identifier[uri-prefix='urn:issn:']#uri-slice(9)}</p> + <!-- <p>Metadata: + ${Markup(u', ').join(r(req, node).anchor() for r in representations.for_node(node) if r.format != 'html')} + </p> --> + </div> +</div> + +<h3>Issues available online</h3> +<ul> + <li rdf:for="!mhs:isIssueOf[uri-prefix='http://miskinhill.com.au/journals/'](~mhs:publicationDate#comparable-lv)"> + <a href="${#uri}"> + Vol. ${mhs:volume}, + Issue ${mhs:issueNumber} + (${dc:coverage}) + </a> + </li> +</ul> + +<h3>About this journal</h3> +<div rdf:content="dc:description" /> + +</body> +</html> +\ No newline at end of file