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