rdftemplate

Library for generating XML documents from RDF data using templates
git clone https://code.djc.id.au/git/rdftemplate/
commit 4f25607ea77ca8eb8953b99705c85de97ad9d984
parent b68f599d66065f4a9997216495960fb69629b543
Author: Dan Callaghan <djc@djc.id.au>
Date:   Sun,  4 Oct 2009 16:22:01 +1000

added support for: type predicate, boolean combination of predicates, unions

Diffstat:
Msrc/main/antlr3/au/com/miskinhill/rdftemplate/selector/Selector.g | 53+++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/main/java/au/com/miskinhill/rdftemplate/TemplateInterpolator.java | 62+++++++++++++++++++++++++++++++++++++++++++++-----------------
Asrc/main/java/au/com/miskinhill/rdftemplate/selector/BooleanAndPredicate.java | 34++++++++++++++++++++++++++++++++++
Asrc/main/java/au/com/miskinhill/rdftemplate/selector/TypePredicate.java | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/com/miskinhill/rdftemplate/selector/UnionSelector.java | 45+++++++++++++++++++++++++++++++++++++++++++++
Msrc/test/java/au/com/miskinhill/rdftemplate/TemplateInterpolatorUnitTest.java | 16++++++++++++++++
Msrc/test/java/au/com/miskinhill/rdftemplate/selector/PredicateMatcher.java | 17+++++++++++++++++
Msrc/test/java/au/com/miskinhill/rdftemplate/selector/SelectorEvaluationUnitTest.java | 31++++++++++++++++++++++++++++---
Msrc/test/java/au/com/miskinhill/rdftemplate/selector/SelectorMatcher.java | 14++++++++++----
Msrc/test/java/au/com/miskinhill/rdftemplate/selector/SelectorParserUnitTest.java | 53++++++++++++++++++++++++++++++++++++++++-------------
Asrc/test/resources/au/com/miskinhill/rdftemplate/conditional.xml | 11+++++++++++
Msrc/test/resources/au/com/miskinhill/rdftemplate/test-data.xml | 2++
12 files changed, 347 insertions(+), 43 deletions(-)
diff --git a/src/main/antlr3/au/com/miskinhill/rdftemplate/selector/Selector.g b/src/main/antlr3/au/com/miskinhill/rdftemplate/selector/Selector.g
@@ -11,7 +11,7 @@ package au.com.miskinhill.rdftemplate.selector;
         CommonTokenStream tokens = new CommonTokenStream(lexer);
         SelectorParser parser = new SelectorParser(tokens);
         try {
-            return parser.selector();
+            return parser.unionSelector();
         } catch (RecognitionException e) {
             throw new InvalidSelectorSyntaxException(e);
         }
@@ -34,10 +34,27 @@ package au.com.miskinhill.rdftemplate.selector;
     }
 }
 
-start : selector ;
+start : unionSelector ;
+
+unionSelector returns [Selector<?> result]
+@init {
+    List<Selector<?>> selectors = new ArrayList<Selector<?>>();
+}
+    : s=selector { selectors.add(s); }
+      ( '|'
+        s=selector { selectors.add(s); }
+      )*
+      {
+        if (selectors.size() > 1)
+            result = new UnionSelector(selectors);
+        else
+            result = selectors.get(0);
+      }
+    ;
 
 selector returns [Selector<?> result]
-    : ( ts=traversingSelector { result = ts; }
+    : ' '*
+      ( ts=traversingSelector { result = ts; }
       | { result = new NoopSelector(); }
       )
       ( '#'
@@ -52,6 +69,7 @@ selector returns [Selector<?> result]
         )
       |
       )
+      ' '*
     ;
 
 traversingSelector returns [TraversingSelector result]
@@ -75,9 +93,7 @@ traversal returns [Traversal result]
       ':'
       localname=XMLTOKEN { $result.setPropertyLocalName($localname.text); }
       ( '['
-        URI_PREFIX_PREDICATE
-        '='
-        uriPrefix=SINGLE_QUOTED { $result.setPredicate(new UriPrefixPredicate($uriPrefix.text)); }
+        p=booleanPredicate { $result.setPredicate(p); }
         ']'
       | // optional
       )
@@ -96,7 +112,32 @@ traversal returns [Traversal result]
       )
     ;
 
+booleanPredicate returns [Predicate result]
+    : ( p=predicate { result = p; }
+      | left=predicate
+        ' '+
+        'and'
+        ' '+
+        right=booleanPredicate
+        { result = new BooleanAndPredicate(left, right); }
+      )
+    ;
+    
+predicate returns [Predicate result]
+    : ( URI_PREFIX_PREDICATE
+        '='
+        uriPrefix=SINGLE_QUOTED { result = new UriPrefixPredicate($uriPrefix.text); }
+      | TYPE_PREDICATE
+        '='
+        nsprefix=XMLTOKEN
+        ':'
+        localname=XMLTOKEN
+        { result = new TypePredicate($nsprefix.text, $localname.text); }
+      )
+    ;
+
 URI_PREFIX_PREDICATE : 'uri-prefix' ;
+TYPE_PREDICATE : 'type' ;
 FIRST_PREDICATE : 'first' ;
 LV_ADAPTATION : 'lv' ;
 COMPARABLE_LV_ADAPTATION : 'comparable-lv' ;
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/TemplateInterpolator.java b/src/main/java/au/com/miskinhill/rdftemplate/TemplateInterpolator.java
@@ -43,6 +43,8 @@ public class TemplateInterpolator {
     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);
+    public static final String IF_ACTION = "if";
+    private static final QName IF_ACTION_QNAME = new QName(NS, IF_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";
     
@@ -70,25 +72,51 @@ public class TemplateInterpolator {
             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);
+                    if (start.getName().equals(IF_ACTION_QNAME)) {
+                        Attribute testAttribute = start.getAttributeByName(new QName("test"));
+                        if (testAttribute == null)
+                            throw new TemplateSyntaxException("rdf:if must have a test attribute");
+                        Selector<?> selector = SelectorParser.parse(testAttribute.getValue());
+                        if (selector.result(node).isEmpty()) {
+                            consumeTree(start, reader);
+                            break;
+                        } else {
+                            List<XMLEvent> events = consumeTree(start, reader);
+                            // discard the enclosing rdf:if element
+                            events.remove(events.size() - 1);
+                            events.remove(0);
+                            interpolate(events.iterator(), node, writer);
                         }
                     } else {
-                        start = interpolateAttributes(start, node);
-                        writer.add(start);
+                        Attribute ifAttribute = start.getAttributeByName(IF_ACTION_QNAME);
+                        Attribute contentAttribute = start.getAttributeByName(CONTENT_ACTION_QNAME);
+                        Attribute forAttribute = start.getAttributeByName(FOR_ACTION_QNAME);
+                        if (ifAttribute != null) {
+                            Selector<?> selector = SelectorParser.parse(ifAttribute.getValue());
+                            if (selector.result(node).isEmpty()) {
+                                consumeTree(start, reader);
+                                break;
+                            }
+                            start = cloneStartWithAttributes(start, cloneAttributesWithout(start, IF_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;
                 }
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/BooleanAndPredicate.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/BooleanAndPredicate.java
@@ -0,0 +1,34 @@
+package au.com.miskinhill.rdftemplate.selector;
+
+import com.hp.hpl.jena.rdf.model.RDFNode;
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+public class BooleanAndPredicate implements Predicate {
+    
+    private final Predicate left;
+    private final Predicate right;
+    
+    public BooleanAndPredicate(Predicate left, Predicate right) {
+        this.left = left;
+        this.right = right;
+    }
+    
+    public Predicate getLeft() {
+        return left;
+    }
+    
+    public Predicate getRight() {
+        return right;
+    }
+    
+    @Override
+    public boolean evaluate(RDFNode node) {
+        return left.evaluate(node) && right.evaluate(node);
+    }
+    
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this).append(left).append(right).toString();
+    }
+
+}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/TypePredicate.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/TypePredicate.java
@@ -0,0 +1,52 @@
+package au.com.miskinhill.rdftemplate.selector;
+
+import java.util.Set;
+
+import com.hp.hpl.jena.rdf.model.RDFNode;
+import com.hp.hpl.jena.rdf.model.Resource;
+import com.hp.hpl.jena.rdf.model.Statement;
+import com.hp.hpl.jena.vocabulary.RDF;
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+import au.com.miskinhill.rdftemplate.NamespacePrefixMapper;
+
+public class TypePredicate implements Predicate {
+    
+    private final String namespacePrefix;
+    private final String localName;
+    
+    public TypePredicate(String namespacePrefix, String localName) {
+        this.namespacePrefix = namespacePrefix;
+        this.localName = localName;
+    }
+    
+    public String getNamespacePrefix() {
+        return namespacePrefix;
+    }
+    
+    public String getLocalName() {
+        return localName;
+    }
+    
+    @SuppressWarnings("unchecked")
+    @Override
+    public boolean evaluate(RDFNode node) {
+        if (!node.isResource()) {
+            throw new SelectorEvaluationException("Attempted to apply [type] to non-resource node " + node);
+        }
+        Resource resource = (Resource) node;
+        Resource type = resource.getModel().createResource(
+                NamespacePrefixMapper.getInstance().get(namespacePrefix) + localName);
+        for (Statement statement: (Set<Statement>) resource.listProperties(RDF.type).toSet()) {
+            if (statement.getObject().equals(type))
+                return true;
+        }
+        return false;
+    }
+    
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this).append(namespacePrefix).append(localName).toString();
+    }
+
+}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/UnionSelector.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/UnionSelector.java
@@ -0,0 +1,45 @@
+package au.com.miskinhill.rdftemplate.selector;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+import com.hp.hpl.jena.rdf.model.RDFNode;
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+public class UnionSelector<T> extends AbstractSelector<T> {
+    
+    private final List<Selector<? extends T>> selectors;
+    
+    public UnionSelector(List<Selector<? extends T>> selectors) {
+        super(null);
+        this.selectors = selectors;
+    }
+    
+    @Override
+    public List<T> result(RDFNode node) {
+        LinkedHashSet<T> results = new LinkedHashSet<T>();
+        for (Selector<? extends T> selector: selectors) {
+            results.addAll(selector.result(node));
+        }
+        return new ArrayList<T>(results);
+    }
+    
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this).append(selectors).toString();
+    }
+    
+    public List<Selector<? extends T>> getSelectors() {
+        return selectors;
+    }
+    
+    @Override
+    public <Other> Selector<Other> withResultType(Class<Other> otherType) {
+        for (Selector<? extends T> selector: selectors) {
+            selector.withResultType(otherType); // class cast exception?
+        }
+        return (Selector<Other>) this;
+    }
+    
+}
diff --git a/src/test/java/au/com/miskinhill/rdftemplate/TemplateInterpolatorUnitTest.java b/src/test/java/au/com/miskinhill/rdftemplate/TemplateInterpolatorUnitTest.java
@@ -59,6 +59,22 @@ public class TemplateInterpolatorUnitTest {
     }
     
     @Test
+    public void shouldHandleIfs() throws Exception {
+        Resource author = model.getResource("http://miskinhill.com.au/authors/test-author");
+        String result = TemplateInterpolator.interpolate(
+                new InputStreamReader(this.getClass().getResourceAsStream("conditional.xml")), author);
+        assertThat(result, containsString("attribute test"));
+        assertThat(result, containsString("element test"));
+        assertThat(result, not(containsString("rdf:if")));
+        
+        Resource authorWithoutNotes = model.getResource("http://miskinhill.com.au/authors/another-author");
+        result = TemplateInterpolator.interpolate(
+                new InputStreamReader(this.getClass().getResourceAsStream("conditional.xml")), authorWithoutNotes);
+        assertThat(result, not(containsString("attribute test")));
+        assertThat(result, not(containsString("element test")));
+    }
+    
+    @Test
     public void shouldWork() throws Exception {
         Resource journal = model.getResource("http://miskinhill.com.au/journals/test/");
         String result = TemplateInterpolator.interpolate(
diff --git a/src/test/java/au/com/miskinhill/rdftemplate/selector/PredicateMatcher.java b/src/test/java/au/com/miskinhill/rdftemplate/selector/PredicateMatcher.java
@@ -2,6 +2,8 @@ package au.com.miskinhill.rdftemplate.selector;
 
 import static org.hamcrest.CoreMatchers.equalTo;
 
+import org.hamcrest.Matcher;
+
 public class PredicateMatcher<T extends Predicate> extends BeanPropertyMatcher<T> {
     
     private PredicateMatcher(Class<T> type) {
@@ -13,5 +15,20 @@ public class PredicateMatcher<T extends Predicate> extends BeanPropertyMatcher<T
         m.addRequiredProperty("prefix", equalTo(prefix));
         return m;
     }
+    
+    public static PredicateMatcher<TypePredicate> typePredicate(String namespacePrefix, String localName) {
+        PredicateMatcher<TypePredicate> m = new PredicateMatcher<TypePredicate>(TypePredicate.class);
+        m.addRequiredProperty("namespacePrefix", equalTo(namespacePrefix));
+        m.addRequiredProperty("localName", equalTo(localName));
+        return m;
+    }
+    
+    public static PredicateMatcher<BooleanAndPredicate> booleanAndPredicate(
+            Matcher<? extends Predicate> left, Matcher<? extends Predicate> right) {
+        PredicateMatcher<BooleanAndPredicate> m = new PredicateMatcher<BooleanAndPredicate>(BooleanAndPredicate.class);
+        m.addRequiredProperty("left", left);
+        m.addRequiredProperty("right", right);
+        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
@@ -1,9 +1,8 @@
 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 static org.junit.matchers.JUnitMatchers.hasItems;
 
 import java.io.InputStream;
 import java.util.List;
@@ -22,7 +21,7 @@ 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;
+    private Resource journal, issue, article, citedArticle, author, anotherAuthor, book, review, anotherReview, obituary, en, ru;
     
     @BeforeClass
     public static void ensureDatatypesRegistered() {
@@ -37,7 +36,9 @@ public class SelectorEvaluationUnitTest {
         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");
+        citedArticle = m.createResource("http://miskinhill.com.au/cited/journals/asdf/1:1/article");
         author = m.createResource("http://miskinhill.com.au/authors/test-author");
+        anotherAuthor = m.createResource("http://miskinhill.com.au/authors/another-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");
@@ -134,4 +135,28 @@ public class SelectorEvaluationUnitTest {
         assertThat(results, hasItems((Object) "en", (Object) "ru"));
     }
     
+    @Test
+    public void shouldEvaluateTypePredicate() throws Exception {
+        List<RDFNode> results = SelectorParser.parse("!dc:creator[type=mhs:Review]")
+                .withResultType(RDFNode.class).result(author);
+        assertThat(results.size(), equalTo(1));
+        assertThat(results, hasItems((RDFNode) review));
+    }
+    
+    @Test
+    public void shouldEvaluateAndCombinationOfPredicates() throws Exception {
+        List<RDFNode> results = SelectorParser.parse("!dc:creator[type=mhs:Article and uri-prefix='http://miskinhill.com.au/journals/']")
+                .withResultType(RDFNode.class).result(author);
+        assertThat(results.size(), equalTo(1));
+        assertThat(results, hasItems((RDFNode) article));
+    }
+    
+    @Test
+    public void shouldEvaluateUnion() throws Exception {
+        List<RDFNode> results = SelectorParser.parse("!dc:creator | !mhs:translator")
+                .withResultType(RDFNode.class).result(anotherAuthor);
+        assertThat(results.size(), equalTo(3));
+        assertThat(results, hasItems((RDFNode) article, (RDFNode) citedArticle, (RDFNode) anotherReview));
+    }
+    
 }
diff --git a/src/test/java/au/com/miskinhill/rdftemplate/selector/SelectorMatcher.java b/src/test/java/au/com/miskinhill/rdftemplate/selector/SelectorMatcher.java
@@ -1,8 +1,8 @@
 package au.com.miskinhill.rdftemplate.selector;
 
-
 import static org.junit.matchers.JUnitMatchers.hasItems;
 
+import com.hp.hpl.jena.rdf.model.RDFNode;
 import org.hamcrest.Matcher;
 
 public class SelectorMatcher<T extends Selector<?>> extends BeanPropertyMatcher<T> {
@@ -11,15 +11,21 @@ public class SelectorMatcher<T extends Selector<?>> extends BeanPropertyMatcher<
         super(type);
     }
     
-    public static SelectorMatcher<Selector<?>> selector(Matcher<Traversal>... traversals) {
+    public static SelectorMatcher<Selector<RDFNode>> selector(Matcher<Traversal>... traversals) {
         if (traversals.length == 0) {
-            return new SelectorMatcher<Selector<?>>(NoopSelector.class);
+            return new SelectorMatcher<Selector<RDFNode>>(NoopSelector.class);
         }
-        SelectorMatcher<Selector<?>> m = new SelectorMatcher<Selector<?>>(TraversingSelector.class);
+        SelectorMatcher<Selector<RDFNode>> m = new SelectorMatcher<Selector<RDFNode>>(TraversingSelector.class);
         m.addRequiredProperty("traversals", hasItems(traversals));
         return m;
     }
     
+    public static <R> SelectorMatcher<UnionSelector<R>> unionSelector(Matcher<Selector<R>>... selectors) {
+        SelectorMatcher<UnionSelector<R>> m = new SelectorMatcher(UnionSelector.class);
+        m.addRequiredProperty("selectors", hasItems(selectors));
+        return m;
+    }
+    
     public <A> SelectorMatcher<Selector<?>> withAdaptation(Matcher<? extends Adaptation<A>> adaptation) {
         SelectorMatcher<Selector<?>> m = new SelectorMatcher(SelectorWithAdaptation.class);
         m.addRequiredProperty("baseSelector", this);
diff --git a/src/test/java/au/com/miskinhill/rdftemplate/selector/SelectorParserUnitTest.java b/src/test/java/au/com/miskinhill/rdftemplate/selector/SelectorParserUnitTest.java
@@ -2,24 +2,24 @@ 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.SelectorMatcher.*;
 import static au.com.miskinhill.rdftemplate.selector.TraversalMatcher.traversal;
-
 import static org.junit.Assert.assertThat;
 
+import com.hp.hpl.jena.rdf.model.RDFNode;
 import org.junit.Test;
 
 public class SelectorParserUnitTest {
     
     @Test
     public void shouldRecogniseSingleTraversal() throws Exception {
-        Selector<?> selector = SelectorParser.parse("dc:creator");
+        Selector<RDFNode> selector = SelectorParser.parse("dc:creator").withResultType(RDFNode.class);
         assertThat(selector, selector(traversal("dc", "creator")));
     }
     
     @Test
     public void shouldRecogniseMultipleTraversals() throws Exception {
-        Selector<?> selector = SelectorParser.parse("dc:creator/foaf:name");
+        Selector<RDFNode> selector = SelectorParser.parse("dc:creator/foaf:name").withResultType(RDFNode.class);
         assertThat(selector, selector(
                 traversal("dc", "creator"),
                 traversal("foaf", "name")));
@@ -27,7 +27,7 @@ public class SelectorParserUnitTest {
     
     @Test
     public void shouldRecogniseInverseTraversal() throws Exception {
-        Selector<?> selector = SelectorParser.parse("!dc:isPartOf/!dc:isPartOf");
+        Selector<RDFNode> selector = SelectorParser.parse("!dc:isPartOf/!dc:isPartOf").withResultType(RDFNode.class);
         assertThat(selector, selector(
                 traversal("dc", "isPartOf").inverse(),
                 traversal("dc", "isPartOf").inverse()));
@@ -35,7 +35,7 @@ public class SelectorParserUnitTest {
     
     @Test
     public void shouldRecogniseSortOrder() throws Exception {
-        Selector<?> selector = SelectorParser.parse("!mhs:isIssueOf(mhs:publicationDate#comparable-lv)");
+        Selector<RDFNode> selector = SelectorParser.parse("!mhs:isIssueOf(mhs:publicationDate#comparable-lv)").withResultType(RDFNode.class);
         assertThat(selector, selector(
                 traversal("mhs", "isIssueOf").inverse()
                 .withSortOrder(selector(traversal("mhs", "publicationDate"))
@@ -44,7 +44,7 @@ public class SelectorParserUnitTest {
     
     @Test
     public void shouldRecogniseReverseSortOrder() throws Exception {
-        Selector<?> selector = SelectorParser.parse("!mhs:isIssueOf(~mhs:publicationDate#comparable-lv)");
+        Selector<RDFNode> selector = SelectorParser.parse("!mhs:isIssueOf(~mhs:publicationDate#comparable-lv)").withResultType(RDFNode.class);
         assertThat(selector, selector(
                 traversal("mhs", "isIssueOf").inverse()
                 .withSortOrder(selector(traversal("mhs", "publicationDate"))
@@ -54,7 +54,7 @@ public class SelectorParserUnitTest {
     
     @Test
     public void shouldRecogniseComplexSortOrder() throws Exception {
-        Selector<?> selector = SelectorParser.parse("!mhs:reviews(dc:isPartOf/mhs:publicationDate#comparable-lv)");
+        Selector<RDFNode> selector = SelectorParser.parse("!mhs:reviews(dc:isPartOf/mhs:publicationDate#comparable-lv)").withResultType(RDFNode.class);
         assertThat(selector, selector(
                 traversal("mhs", "reviews")
                 .withSortOrder(selector(traversal("dc", "isPartOf"), traversal("mhs", "publicationDate"))
@@ -86,8 +86,9 @@ public class SelectorParserUnitTest {
     
     @Test
     public void shouldRecogniseUriPrefixPredicate() throws Exception {
-        Selector<?> selector = SelectorParser.parse(
-                "!mhs:isIssueOf[uri-prefix='http://miskinhill.com.au/journals/'](~mhs:publicationDate#comparable-lv)");
+        Selector<RDFNode> selector = SelectorParser.parse(
+                "!mhs:isIssueOf[uri-prefix='http://miskinhill.com.au/journals/'](~mhs:publicationDate#comparable-lv)")
+                .withResultType(RDFNode.class);
         assertThat(selector, selector(
                 traversal("mhs", "isIssueOf")
                     .inverse()
@@ -99,8 +100,9 @@ public class SelectorParserUnitTest {
     
     @Test
     public void shouldRecogniseSubscript() throws Exception {
-        Selector<?> selector = SelectorParser.parse(
-                "!mhs:isIssueOf(~mhs:publicationDate#comparable-lv)[0]/mhs:coverThumbnail#uri");
+        Selector<String> selector = SelectorParser.parse(
+                "!mhs:isIssueOf(~mhs:publicationDate#comparable-lv)[0]/mhs:coverThumbnail#uri")
+                .withResultType(String.class);
         assertThat(selector, selector(
                 traversal("mhs", "isIssueOf")
                     .inverse()
@@ -114,13 +116,38 @@ public class SelectorParserUnitTest {
     
     @Test
     public void shouldRecogniseLVAdaptation() throws Exception {
-        Selector<?> selector = SelectorParser.parse("dc:language/lingvoj:iso1#lv");
+        Selector<Object> selector = SelectorParser.parse("dc:language/lingvoj:iso1#lv").withResultType(Object.class);
         assertThat(selector, selector(
                 traversal("dc", "language"),
                 traversal("lingvoj", "iso1"))
                 .withAdaptation(lvAdaptation()));
     }
     
+    @Test
+    public void shouldRecogniseTypePredicate() throws Exception {
+        Selector<RDFNode> selector = SelectorParser.parse("!dc:creator[type=mhs:Review]").withResultType(RDFNode.class);
+        assertThat(selector, selector(
+                traversal("dc", "creator").inverse().withPredicate(typePredicate("mhs", "Review"))));
+    }
+    
+    @Test
+    public void shouldRecogniseAndCombinationOfPredicates() throws Exception {
+        Selector<RDFNode> selector = SelectorParser.parse("!dc:creator[type=mhs:Review and uri-prefix='http://miskinhill.com.au/journals/']").withResultType(RDFNode.class);
+        assertThat(selector, selector(
+                traversal("dc", "creator").inverse()
+                .withPredicate(booleanAndPredicate(
+                    typePredicate("mhs", "Review"),
+                    uriPrefixPredicate("http://miskinhill.com.au/journals/")))));
+    }
+    
+    @Test
+    public void shouldRecogniseUnion() throws Exception {
+        Selector<RDFNode> selector = SelectorParser.parse("!dc:creator | !mhs:translator").withResultType(RDFNode.class);
+        assertThat((UnionSelector<RDFNode>) selector, unionSelector(
+                selector(traversal("dc", "creator").inverse()),
+                selector(traversal("mhs", "translator").inverse())));
+    }
+    
     @Test(expected = InvalidSelectorSyntaxException.class)
     public void shouldThrowForInvalidSyntax() throws Exception {
         SelectorParser.parse("dc:creator]["); // this is a parser error
diff --git a/src/test/resources/au/com/miskinhill/rdftemplate/conditional.xml b/src/test/resources/au/com/miskinhill/rdftemplate/conditional.xml
@@ -0,0 +1,10 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:rdf="http://code.miskinhill.com.au/rdftemplate/">
+<body>
+
+<span rdf:if="mhs:biographicalNotes">attribute test</span>
+
+<rdf:if test="mhs:biographicalNotes">element test</rdf:if>
+
+</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
@@ -52,6 +52,7 @@
   <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"/>
+    <mhs:translator rdf:resource="http://miskinhill.com.au/authors/another-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>
@@ -60,6 +61,7 @@
     <foaf:name>Test Author</foaf:name>
     <foaf:surname>Author</foaf:surname>
     <foaf:givenNames>Test</foaf:givenNames>
+    <mhs:biographicalNotes>This person does stuff.</mhs:biographicalNotes>
   </mhs:Author>
   <mhs:Journal rdf:about="http://miskinhill.com.au/cited/journals/asdf/">
     <dc:title xml:lang="en">A Cited Journal</dc:title>