rdftemplate

Library for generating XML documents from RDF data using templates
git clone https://code.djc.id.au/git/rdftemplate/
commit c888c5f681d4543cbe2bf969c5476a5c3b94f973
parent 6443a512e21466244f7dc9fdefe40812cf87fc77
Author: Dan Callaghan <djc@djc.id.au>
Date:   Sun, 16 May 2010 16:30:22 +1000

changed group/package to au.id.djc

--HG--
rename : src/main/antlr3/au/com/miskinhill/rdftemplate/selector/Selector.g => src/main/antlr3/au/id/djc/rdftemplate/selector/Selector.g
rename : src/main/java/au/com/miskinhill/rdftemplate/ContentAction.java => src/main/java/au/id/djc/rdftemplate/ContentAction.java
rename : src/main/java/au/com/miskinhill/rdftemplate/ForAction.java => src/main/java/au/id/djc/rdftemplate/ForAction.java
rename : src/main/java/au/com/miskinhill/rdftemplate/IfAction.java => src/main/java/au/id/djc/rdftemplate/IfAction.java
rename : src/main/java/au/com/miskinhill/rdftemplate/JoinAction.java => src/main/java/au/id/djc/rdftemplate/JoinAction.java
rename : src/main/java/au/com/miskinhill/rdftemplate/TemplateAction.java => src/main/java/au/id/djc/rdftemplate/TemplateAction.java
rename : src/main/java/au/com/miskinhill/rdftemplate/TemplateInterpolationException.java => src/main/java/au/id/djc/rdftemplate/TemplateInterpolationException.java
rename : src/main/java/au/com/miskinhill/rdftemplate/TemplateInterpolator.java => src/main/java/au/id/djc/rdftemplate/TemplateInterpolator.java
rename : src/main/java/au/com/miskinhill/rdftemplate/TemplateSyntaxException.java => src/main/java/au/id/djc/rdftemplate/TemplateSyntaxException.java
rename : src/main/java/au/com/miskinhill/rdftemplate/XMLStream.java => src/main/java/au/id/djc/rdftemplate/XMLStream.java
rename : src/main/java/au/com/miskinhill/rdftemplate/datatype/DateDataType.java => src/main/java/au/id/djc/rdftemplate/datatype/DateDataType.java
rename : src/main/java/au/com/miskinhill/rdftemplate/datatype/DateTimeDataType.java => src/main/java/au/id/djc/rdftemplate/datatype/DateTimeDataType.java
rename : src/main/java/au/com/miskinhill/rdftemplate/datatype/Year.java => src/main/java/au/id/djc/rdftemplate/datatype/Year.java
rename : src/main/java/au/com/miskinhill/rdftemplate/datatype/YearMonth.java => src/main/java/au/id/djc/rdftemplate/datatype/YearMonth.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/AbstractAdaptation.java => src/main/java/au/id/djc/rdftemplate/selector/AbstractAdaptation.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/AbstractSelector.java => src/main/java/au/id/djc/rdftemplate/selector/AbstractSelector.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/Adaptation.java => src/main/java/au/id/djc/rdftemplate/selector/Adaptation.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/AdaptationFactory.java => src/main/java/au/id/djc/rdftemplate/selector/AdaptationFactory.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/AntlrSelectorFactory.java => src/main/java/au/id/djc/rdftemplate/selector/AntlrSelectorFactory.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/BooleanAndPredicate.java => src/main/java/au/id/djc/rdftemplate/selector/BooleanAndPredicate.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/ComparableLiteralValueAdaptation.java => src/main/java/au/id/djc/rdftemplate/selector/ComparableLiteralValueAdaptation.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/DefaultAdaptationFactory.java => src/main/java/au/id/djc/rdftemplate/selector/DefaultAdaptationFactory.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/DefaultPredicateResolver.java => src/main/java/au/id/djc/rdftemplate/selector/DefaultPredicateResolver.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/EternallyCachingSelectorFactory.java => src/main/java/au/id/djc/rdftemplate/selector/EternallyCachingSelectorFactory.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/FormattedDateTimeAdaptation.java => src/main/java/au/id/djc/rdftemplate/selector/FormattedDateTimeAdaptation.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/InvalidSelectorSyntaxException.java => src/main/java/au/id/djc/rdftemplate/selector/InvalidSelectorSyntaxException.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/LiteralValueAdaptation.java => src/main/java/au/id/djc/rdftemplate/selector/LiteralValueAdaptation.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/NoopSelector.java => src/main/java/au/id/djc/rdftemplate/selector/NoopSelector.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/Predicate.java => src/main/java/au/id/djc/rdftemplate/selector/Predicate.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/PredicateResolver.java => src/main/java/au/id/djc/rdftemplate/selector/PredicateResolver.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/Selector.java => src/main/java/au/id/djc/rdftemplate/selector/Selector.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/SelectorComparator.java => src/main/java/au/id/djc/rdftemplate/selector/SelectorComparator.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/SelectorEvaluationException.java => src/main/java/au/id/djc/rdftemplate/selector/SelectorEvaluationException.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/SelectorFactory.java => src/main/java/au/id/djc/rdftemplate/selector/SelectorFactory.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/SelectorWithAdaptation.java => src/main/java/au/id/djc/rdftemplate/selector/SelectorWithAdaptation.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/StringLiteralValueAdaptation.java => src/main/java/au/id/djc/rdftemplate/selector/StringLiteralValueAdaptation.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/Traversal.java => src/main/java/au/id/djc/rdftemplate/selector/Traversal.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/TraversingSelector.java => src/main/java/au/id/djc/rdftemplate/selector/TraversingSelector.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/TypePredicate.java => src/main/java/au/id/djc/rdftemplate/selector/TypePredicate.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/UnionSelector.java => src/main/java/au/id/djc/rdftemplate/selector/UnionSelector.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/UriAdaptation.java => src/main/java/au/id/djc/rdftemplate/selector/UriAdaptation.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/UriAnchorAdaptation.java => src/main/java/au/id/djc/rdftemplate/selector/UriAnchorAdaptation.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/UriPrefixPredicate.java => src/main/java/au/id/djc/rdftemplate/selector/UriPrefixPredicate.java
rename : src/main/java/au/com/miskinhill/rdftemplate/selector/UriSliceAdaptation.java => src/main/java/au/id/djc/rdftemplate/selector/UriSliceAdaptation.java
rename : src/main/java/au/com/miskinhill/rdftemplate/view/RDFTemplateView.java => src/main/java/au/id/djc/rdftemplate/view/RDFTemplateView.java
rename : src/main/java/au/com/miskinhill/rdftemplate/view/RDFTemplateViewResolver.java => src/main/java/au/id/djc/rdftemplate/view/RDFTemplateViewResolver.java
rename : src/test/java/au/com/miskinhill/rdftemplate/TemplateInterpolatorUnitTest.java => src/test/java/au/id/djc/rdftemplate/TemplateInterpolatorUnitTest.java
rename : src/test/java/au/com/miskinhill/rdftemplate/TestNamespacePrefixMap.java => src/test/java/au/id/djc/rdftemplate/TestNamespacePrefixMap.java
rename : src/test/java/au/com/miskinhill/rdftemplate/datatype/DateDataTypeUnitTest.java => src/test/java/au/id/djc/rdftemplate/datatype/DateDataTypeUnitTest.java
rename : src/test/java/au/com/miskinhill/rdftemplate/datatype/DateTimeDataTypeUnitTest.java => src/test/java/au/id/djc/rdftemplate/datatype/DateTimeDataTypeUnitTest.java
rename : src/test/java/au/com/miskinhill/rdftemplate/datatype/YearMonthUnitTest.java => src/test/java/au/id/djc/rdftemplate/datatype/YearMonthUnitTest.java
rename : src/test/java/au/com/miskinhill/rdftemplate/datatype/YearUnitTest.java => src/test/java/au/id/djc/rdftemplate/datatype/YearUnitTest.java
rename : src/test/java/au/com/miskinhill/rdftemplate/selector/AdaptationMatcher.java => src/test/java/au/id/djc/rdftemplate/selector/AdaptationMatcher.java
rename : src/test/java/au/com/miskinhill/rdftemplate/selector/BeanPropertyMatcher.java => src/test/java/au/id/djc/rdftemplate/selector/BeanPropertyMatcher.java
rename : src/test/java/au/com/miskinhill/rdftemplate/selector/EternallyCachingSelectorFactoryUnitTest.java => src/test/java/au/id/djc/rdftemplate/selector/EternallyCachingSelectorFactoryUnitTest.java
rename : src/test/java/au/com/miskinhill/rdftemplate/selector/PredicateMatcher.java => src/test/java/au/id/djc/rdftemplate/selector/PredicateMatcher.java
rename : src/test/java/au/com/miskinhill/rdftemplate/selector/SelectorComparatorMatcher.java => src/test/java/au/id/djc/rdftemplate/selector/SelectorComparatorMatcher.java
rename : src/test/java/au/com/miskinhill/rdftemplate/selector/SelectorEvaluationUnitTest.java => src/test/java/au/id/djc/rdftemplate/selector/SelectorEvaluationUnitTest.java
rename : src/test/java/au/com/miskinhill/rdftemplate/selector/SelectorMatcher.java => src/test/java/au/id/djc/rdftemplate/selector/SelectorMatcher.java
rename : src/test/java/au/com/miskinhill/rdftemplate/selector/SelectorParserUnitTest.java => src/test/java/au/id/djc/rdftemplate/selector/SelectorParserUnitTest.java
rename : src/test/java/au/com/miskinhill/rdftemplate/selector/TraversalMatcher.java => src/test/java/au/id/djc/rdftemplate/selector/TraversalMatcher.java
rename : src/test/java/au/com/miskinhill/rdftemplate/selector/UriAnchorAdaptationUnitTest.java => src/test/java/au/id/djc/rdftemplate/selector/UriAnchorAdaptationUnitTest.java
rename : src/test/resources/au/com/miskinhill/rdftemplate/conditional.xml => src/test/resources/au/id/djc/rdftemplate/conditional.xml
rename : src/test/resources/au/com/miskinhill/rdftemplate/for-seq.xml => src/test/resources/au/id/djc/rdftemplate/for-seq.xml
rename : src/test/resources/au/com/miskinhill/rdftemplate/for.xml => src/test/resources/au/id/djc/rdftemplate/for.xml
rename : src/test/resources/au/com/miskinhill/rdftemplate/join-seq.xml => src/test/resources/au/id/djc/rdftemplate/join-seq.xml
rename : src/test/resources/au/com/miskinhill/rdftemplate/join.xml => src/test/resources/au/id/djc/rdftemplate/join.xml
rename : src/test/resources/au/com/miskinhill/rdftemplate/namespaces.xml => src/test/resources/au/id/djc/rdftemplate/namespaces.xml
rename : src/test/resources/au/com/miskinhill/rdftemplate/replace-subtree.xml => src/test/resources/au/id/djc/rdftemplate/replace-subtree.xml
rename : src/test/resources/au/com/miskinhill/rdftemplate/replace-xml.xml => src/test/resources/au/id/djc/rdftemplate/replace-xml.xml
rename : src/test/resources/au/com/miskinhill/rdftemplate/test-data.xml => src/test/resources/au/id/djc/rdftemplate/test-data.xml

Diffstat:
Mpom.xml | 14+++++++-------
Rsrc/main/antlr3/au/com/miskinhill/rdftemplate/selector/Selector.g -> src/main/antlr3/au/id/djc/rdftemplate/selector/Selector.g | 0
Dsrc/main/java/au/com/miskinhill/rdftemplate/ContentAction.java | 68--------------------------------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/ForAction.java | 55-------------------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/IfAction.java | 46----------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/JoinAction.java | 64----------------------------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/TemplateAction.java | 5-----
Dsrc/main/java/au/com/miskinhill/rdftemplate/TemplateInterpolationException.java | 23-----------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/TemplateInterpolator.java | 431-------------------------------------------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/TemplateSyntaxException.java | 22----------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/XMLStream.java | 43-------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/datatype/DateDataType.java | 113-------------------------------------------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/datatype/DateTimeDataType.java | 99-------------------------------------------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/datatype/Year.java | 50--------------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/datatype/YearMonth.java | 58----------------------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/AbstractAdaptation.java | 58----------------------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/AbstractSelector.java | 41-----------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/Adaptation.java | 15---------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/AdaptationFactory.java | 9---------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/AntlrSelectorFactory.java | 48------------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/BooleanAndPredicate.java | 34----------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/ComparableLiteralValueAdaptation.java | 22----------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/DefaultAdaptationFactory.java | 35-----------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/DefaultPredicateResolver.java | 19-------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/EternallyCachingSelectorFactory.java | 35-----------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/FormattedDateTimeAdaptation.java | 49-------------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/InvalidSelectorSyntaxException.java | 15---------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/LiteralValueAdaptation.java | 16----------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/NoopSelector.java | 19-------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/Predicate.java | 7-------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/PredicateResolver.java | 7-------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/Selector.java | 17-----------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/SelectorComparator.java | 59-----------------------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/SelectorEvaluationException.java | 15---------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/SelectorFactory.java | 7-------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/SelectorWithAdaptation.java | 49-------------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/StringLiteralValueAdaptation.java | 45---------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/Traversal.java | 126-------------------------------------------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/TraversingSelector.java | 46----------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/TypePredicate.java | 46----------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/UnionSelector.java | 45---------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/UriAdaptation.java | 16----------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/UriAnchorAdaptation.java | 24------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/UriPrefixPredicate.java | 26--------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/selector/UriSliceAdaptation.java | 28----------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/view/RDFTemplateView.java | 76----------------------------------------------------------------------------
Dsrc/main/java/au/com/miskinhill/rdftemplate/view/RDFTemplateViewResolver.java | 54------------------------------------------------------
Asrc/main/java/au/id/djc/rdftemplate/ContentAction.java | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/ForAction.java | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/IfAction.java | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/JoinAction.java | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/TemplateAction.java | 5+++++
Asrc/main/java/au/id/djc/rdftemplate/TemplateInterpolationException.java | 23+++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/TemplateInterpolator.java | 431+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/TemplateSyntaxException.java | 22++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/XMLStream.java | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/datatype/DateDataType.java | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/datatype/DateTimeDataType.java | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/datatype/Year.java | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/datatype/YearMonth.java | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/AbstractAdaptation.java | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/AbstractSelector.java | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/Adaptation.java | 15+++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/AdaptationFactory.java | 9+++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/AntlrSelectorFactory.java | 48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/BooleanAndPredicate.java | 34++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/ComparableLiteralValueAdaptation.java | 22++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/DefaultAdaptationFactory.java | 35+++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/DefaultPredicateResolver.java | 19+++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/EternallyCachingSelectorFactory.java | 35+++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/FormattedDateTimeAdaptation.java | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/InvalidSelectorSyntaxException.java | 15+++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/LiteralValueAdaptation.java | 16++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/NoopSelector.java | 19+++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/Predicate.java | 7+++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/PredicateResolver.java | 7+++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/Selector.java | 17+++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/SelectorComparator.java | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/SelectorEvaluationException.java | 15+++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/SelectorFactory.java | 7+++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/SelectorWithAdaptation.java | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/StringLiteralValueAdaptation.java | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/Traversal.java | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/TraversingSelector.java | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/TypePredicate.java | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/UnionSelector.java | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/UriAdaptation.java | 16++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/UriAnchorAdaptation.java | 24++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/UriPrefixPredicate.java | 26++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/selector/UriSliceAdaptation.java | 28++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/view/RDFTemplateView.java | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/au/id/djc/rdftemplate/view/RDFTemplateViewResolver.java | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/test/java/au/com/miskinhill/rdftemplate/TemplateInterpolatorUnitTest.java | 140-------------------------------------------------------------------------------
Dsrc/test/java/au/com/miskinhill/rdftemplate/TestNamespacePrefixMap.java | 32--------------------------------
Dsrc/test/java/au/com/miskinhill/rdftemplate/datatype/DateDataTypeUnitTest.java | 35-----------------------------------
Dsrc/test/java/au/com/miskinhill/rdftemplate/datatype/DateTimeDataTypeUnitTest.java | 34----------------------------------
Dsrc/test/java/au/com/miskinhill/rdftemplate/datatype/YearMonthUnitTest.java | 24------------------------
Dsrc/test/java/au/com/miskinhill/rdftemplate/datatype/YearUnitTest.java | 24------------------------
Dsrc/test/java/au/com/miskinhill/rdftemplate/selector/AdaptationMatcher.java | 36------------------------------------
Dsrc/test/java/au/com/miskinhill/rdftemplate/selector/BeanPropertyMatcher.java | 56--------------------------------------------------------
Dsrc/test/java/au/com/miskinhill/rdftemplate/selector/EternallyCachingSelectorFactoryUnitTest.java | 22----------------------
Dsrc/test/java/au/com/miskinhill/rdftemplate/selector/PredicateMatcher.java | 34----------------------------------
Dsrc/test/java/au/com/miskinhill/rdftemplate/selector/SelectorComparatorMatcher.java | 25-------------------------
Dsrc/test/java/au/com/miskinhill/rdftemplate/selector/SelectorEvaluationUnitTest.java | 224-------------------------------------------------------------------------------
Dsrc/test/java/au/com/miskinhill/rdftemplate/selector/SelectorMatcher.java | 36------------------------------------
Dsrc/test/java/au/com/miskinhill/rdftemplate/selector/SelectorParserUnitTest.java | 206-------------------------------------------------------------------------------
Dsrc/test/java/au/com/miskinhill/rdftemplate/selector/TraversalMatcher.java | 44--------------------------------------------
Dsrc/test/java/au/com/miskinhill/rdftemplate/selector/UriAnchorAdaptationUnitTest.java | 23-----------------------
Asrc/test/java/au/id/djc/rdftemplate/TemplateInterpolatorUnitTest.java | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/test/java/au/id/djc/rdftemplate/TestNamespacePrefixMap.java | 32++++++++++++++++++++++++++++++++
Asrc/test/java/au/id/djc/rdftemplate/datatype/DateDataTypeUnitTest.java | 35+++++++++++++++++++++++++++++++++++
Asrc/test/java/au/id/djc/rdftemplate/datatype/DateTimeDataTypeUnitTest.java | 34++++++++++++++++++++++++++++++++++
Asrc/test/java/au/id/djc/rdftemplate/datatype/YearMonthUnitTest.java | 24++++++++++++++++++++++++
Asrc/test/java/au/id/djc/rdftemplate/datatype/YearUnitTest.java | 24++++++++++++++++++++++++
Asrc/test/java/au/id/djc/rdftemplate/selector/AdaptationMatcher.java | 36++++++++++++++++++++++++++++++++++++
Asrc/test/java/au/id/djc/rdftemplate/selector/BeanPropertyMatcher.java | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/test/java/au/id/djc/rdftemplate/selector/EternallyCachingSelectorFactoryUnitTest.java | 22++++++++++++++++++++++
Asrc/test/java/au/id/djc/rdftemplate/selector/PredicateMatcher.java | 34++++++++++++++++++++++++++++++++++
Asrc/test/java/au/id/djc/rdftemplate/selector/SelectorComparatorMatcher.java | 25+++++++++++++++++++++++++
Asrc/test/java/au/id/djc/rdftemplate/selector/SelectorEvaluationUnitTest.java | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/test/java/au/id/djc/rdftemplate/selector/SelectorMatcher.java | 36++++++++++++++++++++++++++++++++++++
Asrc/test/java/au/id/djc/rdftemplate/selector/SelectorParserUnitTest.java | 206+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/test/java/au/id/djc/rdftemplate/selector/TraversalMatcher.java | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/test/java/au/id/djc/rdftemplate/selector/UriAnchorAdaptationUnitTest.java | 23+++++++++++++++++++++++
Rsrc/test/resources/au/com/miskinhill/rdftemplate/conditional.xml -> src/test/resources/au/id/djc/rdftemplate/conditional.xml | 0
Rsrc/test/resources/au/com/miskinhill/rdftemplate/for-seq.xml -> src/test/resources/au/id/djc/rdftemplate/for-seq.xml | 0
Rsrc/test/resources/au/com/miskinhill/rdftemplate/for.xml -> src/test/resources/au/id/djc/rdftemplate/for.xml | 0
Rsrc/test/resources/au/com/miskinhill/rdftemplate/join-seq.xml -> src/test/resources/au/id/djc/rdftemplate/join-seq.xml | 0
Rsrc/test/resources/au/com/miskinhill/rdftemplate/join.xml -> src/test/resources/au/id/djc/rdftemplate/join.xml | 0
Rsrc/test/resources/au/com/miskinhill/rdftemplate/namespaces.xml -> src/test/resources/au/id/djc/rdftemplate/namespaces.xml | 0
Rsrc/test/resources/au/com/miskinhill/rdftemplate/replace-subtree.xml -> src/test/resources/au/id/djc/rdftemplate/replace-subtree.xml | 0
Rsrc/test/resources/au/com/miskinhill/rdftemplate/replace-xml.xml -> src/test/resources/au/id/djc/rdftemplate/replace-xml.xml | 0
Rsrc/test/resources/au/com/miskinhill/rdftemplate/test-data.xml -> src/test/resources/au/id/djc/rdftemplate/test-data.xml | 0
133 files changed, 3187 insertions(+), 3187 deletions(-)
diff --git a/pom.xml b/pom.xml
@@ -1,24 +1,24 @@
 <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>
+    <groupId>au.id.djc</groupId>
     <artifactId>rdftemplate</artifactId>
     <packaging>jar</packaging>
     <version>1.1-SNAPSHOT</version>
     <name>rdftemplate</name>
-    <url>http://code.miskinhill.com.au/</url>
+    <url>http://code.djc.id.au/</url>
     
     <distributionManagement>
         <repository>
-            <id>code.miskinhill.com.au</id>
-            <name>Miskin Hill Maven repository</name>
-            <url>dav:http://code.miskinhill.com.au/maven2/</url>
+            <id>code.djc.id.au</id>
+            <name>code.djc.id.au Maven repository</name>
+            <url>dav:http://code.djc.id.au/maven2/</url>
         </repository>
     </distributionManagement>
     
     <scm>
-        <url>http://code.miskinhill.com.au/hg/</url>
-        <connection>scm:hg:http://code.miskinhill.com.au/hg/</connection>
+        <url>http://code.djc.id.au/hg/rdftemplate-master/</url>
+        <connection>scm:hg:http://code.djc.id.au/hg/rdftemplate-master/</connection>
         <developerConnection>scm:hg:file://${project.basedir}</developerConnection>
     </scm>
     
diff --git a/src/main/antlr3/au/com/miskinhill/rdftemplate/selector/Selector.g b/src/main/antlr3/au/id/djc/rdftemplate/selector/Selector.g
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/ContentAction.java b/src/main/java/au/com/miskinhill/rdftemplate/ContentAction.java
@@ -1,68 +0,0 @@
-package au.com.miskinhill.rdftemplate;
-
-import java.util.Set;
-
-import javax.xml.XMLConstants;
-import javax.xml.namespace.QName;
-import javax.xml.stream.XMLEventFactory;
-import javax.xml.stream.XMLStreamException;
-import javax.xml.stream.events.Attribute;
-import javax.xml.stream.events.StartElement;
-import javax.xml.stream.util.XMLEventConsumer;
-
-import com.hp.hpl.jena.rdf.model.Literal;
-import com.hp.hpl.jena.rdf.model.RDFNode;
-import org.apache.commons.lang.StringUtils;
-import org.apache.commons.lang.builder.ToStringBuilder;
-
-import au.com.miskinhill.rdftemplate.selector.Selector;
-
-public class ContentAction extends TemplateAction {
-    
-    public static final String ACTION_NAME = "content";
-    public static final QName ACTION_QNAME = new QName(TemplateInterpolator.NS, ACTION_NAME);
-    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 final StartElement start;
-    private final Selector<?> selector;
-    
-    public ContentAction(StartElement start, Selector<?> selector) {
-        this.start = start;
-        this.selector = selector;
-    }
-    
-    public void evaluate(TemplateInterpolator interpolator, RDFNode node, XMLEventConsumer writer, XMLEventFactory eventFactory)
-            throws XMLStreamException {
-        Object replacement = selector.singleResult(node);
-        StartElement start = interpolator.interpolateAttributes(this.start, node);
-        Set<Attribute> attributes = interpolator.cloneAttributesWithout(start, ACTION_QNAME);
-        if (replacement instanceof Literal) {
-            Literal literal = (Literal) replacement;
-            if (!StringUtils.isEmpty(literal.getLanguage())) {
-                attributes.add(eventFactory.createAttribute(XML_LANG_QNAME, ((Literal) replacement).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()));
-        interpolator.writeTreeForContent(writer, replacement);
-        writer.add(eventFactory.createEndElement(start.getName(), start.getNamespaces()));
-    }
-    
-    @Override
-    public String toString() {
-        return new ToStringBuilder(this)
-                .append("start", start)
-                .append("selector", selector)
-                .toString();
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/ForAction.java b/src/main/java/au/com/miskinhill/rdftemplate/ForAction.java
@@ -1,55 +0,0 @@
-package au.com.miskinhill.rdftemplate;
-
-import java.util.List;
-import java.util.logging.Logger;
-
-import javax.xml.namespace.QName;
-import javax.xml.stream.XMLStreamException;
-import javax.xml.stream.events.XMLEvent;
-import javax.xml.stream.util.XMLEventConsumer;
-
-import com.hp.hpl.jena.rdf.model.RDFNode;
-import com.hp.hpl.jena.rdf.model.Resource;
-import com.hp.hpl.jena.rdf.model.Seq;
-import com.hp.hpl.jena.vocabulary.RDF;
-import org.apache.commons.lang.builder.ToStringBuilder;
-
-import au.com.miskinhill.rdftemplate.selector.Selector;
-
-public class ForAction extends TemplateAction {
-    
-    public static final String ACTION_NAME = "for";
-    public static final QName ACTION_QNAME = new QName(TemplateInterpolator.NS, ACTION_NAME);
-    private static final Logger LOG = Logger.getLogger(ForAction.class.getName());
-    
-    private final List<XMLEvent> tree;
-    private final Selector<RDFNode> selector;
-    
-    public ForAction(List<XMLEvent> tree, Selector<RDFNode> selector) {
-        this.tree = tree;
-        this.selector = selector;
-    }
-    
-    public void evaluate(TemplateInterpolator interpolator, RDFNode node, XMLEventConsumer writer)
-            throws XMLStreamException {
-        List<RDFNode> result = selector.result(node);
-        if (result.size() == 1 && result.get(0).canAs(Resource.class)) {
-            if (result.get(0).as(Resource.class).hasProperty(RDF.type, RDF.Seq)) {
-                LOG.fine("Apply rdf:Seq special case for " + result.get(0));
-                result = result.get(0).as(Seq.class).iterator().toList();
-                LOG.fine("Resulting sequence is " + result);
-            }
-        }
-        for (RDFNode eachNode: result) {
-            interpolator.interpolate(tree.iterator(), eachNode, writer);
-        }
-    }
-    
-    @Override
-    public String toString() {
-        return new ToStringBuilder(this)
-                .append("selector", selector)
-                .toString();
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/IfAction.java b/src/main/java/au/com/miskinhill/rdftemplate/IfAction.java
@@ -1,46 +0,0 @@
-package au.com.miskinhill.rdftemplate;
-
-import java.util.List;
-
-import javax.xml.namespace.QName;
-import javax.xml.stream.XMLStreamException;
-import javax.xml.stream.events.XMLEvent;
-import javax.xml.stream.util.XMLEventConsumer;
-
-import com.hp.hpl.jena.rdf.model.RDFNode;
-import org.apache.commons.lang.builder.ToStringBuilder;
-
-import au.com.miskinhill.rdftemplate.selector.Selector;
-
-public class IfAction extends TemplateAction {
-    
-    public static final String ACTION_NAME = "if";
-    public static final QName ACTION_QNAME = new QName(TemplateInterpolator.NS, ACTION_NAME);
-    
-    private final List<XMLEvent> tree;
-    private final Selector<?> condition;
-    private final boolean negate;
-    
-    public IfAction(List<XMLEvent> tree, Selector<?> condition, boolean negate) {
-        this.tree = tree;
-        this.condition = condition;
-        this.negate = negate;
-    }
-    
-    public void evaluate(TemplateInterpolator interpolator, RDFNode node, XMLEventConsumer writer)
-            throws XMLStreamException {
-        List<?> selectorResult = condition.result(node);
-        if (negate ? selectorResult.isEmpty() : !selectorResult.isEmpty()) {
-            interpolator.interpolate(tree.iterator(), node, writer);
-        }
-    }
-    
-    @Override
-    public String toString() {
-        return new ToStringBuilder(this)
-                .append("condition", condition)
-                .append("negate", negate)
-                .toString();
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/JoinAction.java b/src/main/java/au/com/miskinhill/rdftemplate/JoinAction.java
@@ -1,64 +0,0 @@
-package au.com.miskinhill.rdftemplate;
-
-import java.util.List;
-import java.util.logging.Logger;
-
-import javax.xml.namespace.QName;
-import javax.xml.stream.XMLEventFactory;
-import javax.xml.stream.XMLStreamException;
-import javax.xml.stream.events.XMLEvent;
-import javax.xml.stream.util.XMLEventConsumer;
-
-import com.hp.hpl.jena.rdf.model.RDFNode;
-import com.hp.hpl.jena.rdf.model.Resource;
-import com.hp.hpl.jena.rdf.model.Seq;
-import com.hp.hpl.jena.vocabulary.RDF;
-import org.apache.commons.lang.builder.ToStringBuilder;
-
-import au.com.miskinhill.rdftemplate.selector.Selector;
-
-public class JoinAction extends TemplateAction {
-    
-    public static final String ACTION_NAME = "join";
-    public static final QName ACTION_QNAME = new QName(TemplateInterpolator.NS, ACTION_NAME);
-    private static final Logger LOG = Logger.getLogger(JoinAction.class.getName());
-    
-    private final List<XMLEvent> tree;
-    private final Selector<RDFNode> selector;
-    private final String separator;
-    
-    public JoinAction(List<XMLEvent> tree, Selector<RDFNode> selector, String separator) {
-        this.tree = tree;
-        this.selector = selector;
-        this.separator = separator;
-    }
-    
-    public void evaluate(TemplateInterpolator interpolator, RDFNode node, XMLEventConsumer writer, XMLEventFactory eventFactory)
-            throws XMLStreamException {
-        List<RDFNode> result = selector.result(node);
-        if (result.size() == 1 && result.get(0).canAs(Resource.class)) {
-            if (result.get(0).as(Resource.class).hasProperty(RDF.type, RDF.Seq)) {
-                LOG.fine("Apply rdf:Seq special case for " + result.get(0));
-                result = result.get(0).as(Seq.class).iterator().toList();
-                LOG.fine("Resulting sequence is " + result);
-            }
-        }
-        boolean first = true;
-        for (RDFNode eachNode: result) {
-            if (!first) {
-                writer.add(eventFactory.createCharacters(separator));
-            }
-            interpolator.interpolate(tree.iterator(), eachNode, writer);
-            first = false;
-        }
-    }
-    
-    @Override
-    public String toString() {
-        return new ToStringBuilder(this)
-                .append("selector", selector)
-                .append("separator", separator)
-                .toString();
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/TemplateAction.java b/src/main/java/au/com/miskinhill/rdftemplate/TemplateAction.java
@@ -1,5 +0,0 @@
-package au.com.miskinhill.rdftemplate;
-
-public abstract class TemplateAction {
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/TemplateInterpolationException.java b/src/main/java/au/com/miskinhill/rdftemplate/TemplateInterpolationException.java
@@ -1,23 +0,0 @@
-package au.com.miskinhill.rdftemplate;
-
-import javax.xml.stream.Location;
-
-import com.hp.hpl.jena.rdf.model.RDFNode;
-
-public class TemplateInterpolationException extends RuntimeException {
-    
-    private static final long serialVersionUID = -1472104970210074672L;
-
-    public TemplateInterpolationException(Location location, TemplateAction action, RDFNode node, Throwable cause) {
-        super("Exception evaluating action [" + action + "] " +
-                "at location [" + location.getLineNumber() + "," + location.getColumnNumber() + "] " +
-                "with context node " + node, cause);
-    }
-    
-    public TemplateInterpolationException(Location location, String selectorExpression, RDFNode node, Throwable cause) {
-        super("Exception evaluating selector expression [" + selectorExpression + "] " +
-                "at location [" + location.getLineNumber() + "," + location.getColumnNumber() + "] " +
-                "with context node " + node, cause);
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/TemplateInterpolator.java b/src/main/java/au/com/miskinhill/rdftemplate/TemplateInterpolator.java
@@ -1,431 +0,0 @@
-package au.com.miskinhill.rdftemplate;
-
-import java.io.InputStream;
-import java.io.Reader;
-import java.io.StringReader;
-import java.io.StringWriter;
-import java.io.Writer;
-import java.util.ArrayList;
-import java.util.Collection;
-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.namespace.QName;
-import javax.xml.stream.XMLEventFactory;
-import javax.xml.stream.XMLEventReader;
-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.Namespace;
-import javax.xml.stream.events.StartElement;
-import javax.xml.stream.events.XMLEvent;
-import javax.xml.stream.util.XMLEventConsumer;
-
-import com.hp.hpl.jena.rdf.model.Literal;
-import com.hp.hpl.jena.rdf.model.RDFNode;
-
-import au.com.miskinhill.rdftemplate.selector.InvalidSelectorSyntaxException;
-import au.com.miskinhill.rdftemplate.selector.Selector;
-import au.com.miskinhill.rdftemplate.selector.SelectorFactory;
-
-public class TemplateInterpolator {
-    
-    public static final String NS = "http://code.miskinhill.com.au/rdftemplate/";
-    
-    private final XMLInputFactory inputFactory = XMLInputFactory.newInstance();
-    private final XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
-    private final XMLEventFactory eventFactory = XMLEventFactory.newInstance();
-    
-    private final SelectorFactory selectorFactory;
-    
-    public TemplateInterpolator(SelectorFactory selectorFactory) {
-        this.selectorFactory = selectorFactory;
-        inputFactory.setProperty(XMLInputFactory.IS_COALESCING, true);
-        outputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
-    }
-    
-    public String interpolate(Reader reader, RDFNode node) {
-        try {
-            StringWriter writer = new StringWriter();
-            final XMLEventWriter eventWriter = outputFactory.createXMLEventWriter(writer);
-            XMLEventConsumer destination = new XMLEventConsumer() {
-                @Override
-                public void add(XMLEvent event) throws XMLStreamException {
-                    eventWriter.add(event);
-                }
-            };
-            interpolate(reader, node, destination);
-            return writer.toString();
-        } catch (XMLStreamException e) {
-            throw new TemplateSyntaxException(e);            
-        }
-    }
-    
-    @SuppressWarnings("unchecked")
-    public void interpolate(Reader reader, RDFNode node, XMLEventConsumer writer) {
-        try {
-            interpolate(inputFactory.createXMLEventReader(reader), node, writer);
-        } catch (XMLStreamException e) {
-            throw new RuntimeException(e);
-        }
-    }
-    
-    @SuppressWarnings("unchecked")
-    public void interpolate(InputStream inputStream, RDFNode node, Writer writer) {
-        try {
-            final XMLEventWriter eventWriter = outputFactory.createXMLEventWriter(writer);
-            XMLEventConsumer destination = new XMLEventConsumer() {
-                @Override
-                public void add(XMLEvent event) throws XMLStreamException {
-                    eventWriter.add(event);
-                }
-            };
-            interpolate(inputFactory.createXMLEventReader(inputStream), node, destination);
-        } catch (XMLStreamException e) {
-            throw new RuntimeException(e);
-        }
-    }
-    
-    public void interpolate(Reader reader, RDFNode node, final Collection<XMLEvent> destination) {
-        interpolate(reader, node, new XMLEventConsumer() {
-            @Override
-            public void add(XMLEvent event) {
-                destination.add(event);
-            }
-        });
-    }
-    
-    public void interpolate(Iterator<XMLEvent> reader, RDFNode node, XMLEventConsumer writer)
-            throws XMLStreamException {
-        while (reader.hasNext()) {
-            XMLEvent event = reader.next();
-            switch (event.getEventType()) {
-                case XMLStreamConstants.START_ELEMENT: {
-                    StartElement start = (StartElement) event;
-                    if (start.getName().equals(IfAction.ACTION_QNAME)) {
-                        Attribute testAttribute = start.getAttributeByName(new QName("test"));
-                        Attribute notAttribute = start.getAttributeByName(new QName("not"));
-                        String condition;
-                        boolean negate = false;
-                        if (testAttribute != null && notAttribute != null)
-                            throw new TemplateSyntaxException(start.getLocation(), "test and not attribute on rdf:if are mutually exclusive");
-                        else if (testAttribute != null)
-                            condition = testAttribute.getValue();
-                        else if (notAttribute != null) {
-                            condition = notAttribute.getValue();
-                            negate = true;
-                        } else
-                            throw new TemplateSyntaxException(start.getLocation(), "rdf:if must have a test attribute or a not attribute");
-                        Selector<?> conditionSelector;
-                        try {
-                            conditionSelector = selectorFactory.get(condition);
-                        } catch (InvalidSelectorSyntaxException e) {
-                            throw new TemplateSyntaxException(start.getLocation(), e);
-                        }
-                        List<XMLEvent> tree = consumeTree(start, reader);
-                        // discard enclosing rdf:if
-                        tree.remove(tree.size() - 1);
-                        tree.remove(0);
-                        IfAction action = new IfAction(tree, conditionSelector, negate);
-                        try {
-                            action.evaluate(this, node, writer);
-                        } catch (Exception e) {
-                            throw new TemplateInterpolationException(start.getLocation(), action, node, e);
-                        }
-                    } else if (start.getName().equals(JoinAction.ACTION_QNAME)) {
-                        Attribute eachAttribute = start.getAttributeByName(new QName("each"));
-                        if (eachAttribute == null)
-                            throw new TemplateSyntaxException(start.getLocation(), "rdf:join must have an each attribute");
-                        String separator = "";
-                        Attribute separatorAttribute = start.getAttributeByName(new QName("separator"));
-                        if (separatorAttribute != null)
-                            separator = separatorAttribute.getValue();
-                        Selector<RDFNode> selector;
-                        try {
-                            selector = selectorFactory.get(eachAttribute.getValue()).withResultType(RDFNode.class);
-                        } catch (InvalidSelectorSyntaxException e) {
-                            throw new TemplateSyntaxException(start.getLocation(), e);
-                        }
-                        List<XMLEvent> events = consumeTree(start, reader);
-                        // discard enclosing rdf:join
-                        events.remove(events.size() - 1);
-                        events.remove(0);
-                        JoinAction action = new JoinAction(events, selector, separator);
-                        try {
-                            action.evaluate(this, node, writer, eventFactory);
-                        } catch (Exception e) {
-                            throw new TemplateInterpolationException(start.getLocation(), action, node, e);
-                        }
-                    } else if (start.getName().equals(ForAction.ACTION_QNAME)) {
-                        Attribute eachAttribute = start.getAttributeByName(new QName("each"));
-                        if (eachAttribute == null)
-                            throw new TemplateSyntaxException(start.getLocation(), "rdf:for must have an each attribute");
-                        Selector<RDFNode> selector;
-                        try {
-                            selector = selectorFactory.get(eachAttribute.getValue()).withResultType(RDFNode.class);
-                        } catch (InvalidSelectorSyntaxException e) {
-                            throw new TemplateSyntaxException(start.getLocation(), e);
-                        }
-                        List<XMLEvent> events = consumeTree(start, reader);
-                        // discard enclosing rdf:for
-                        events.remove(events.size() - 1);
-                        events.remove(0);
-                        ForAction action = new ForAction(events, selector);
-                        try {
-                            action.evaluate(this, node, writer);
-                        } catch (Exception e) {
-                            throw new TemplateInterpolationException(start.getLocation(), action, node, e);
-                        }
-                    } else {
-                        Attribute ifAttribute = start.getAttributeByName(IfAction.ACTION_QNAME);
-                        Attribute contentAttribute = start.getAttributeByName(ContentAction.ACTION_QNAME);
-                        Attribute forAttribute = start.getAttributeByName(ForAction.ACTION_QNAME);
-                        if (ifAttribute != null) {
-                            Selector<?> selector;
-                            try {
-                                selector = selectorFactory.get(ifAttribute.getValue());
-                            } catch (InvalidSelectorSyntaxException e) {
-                                throw new TemplateSyntaxException(ifAttribute.getLocation(), e);
-                            }
-                            start = cloneStart(start, cloneAttributesWithout(start, IfAction.ACTION_QNAME), cloneNamespacesWithoutRdf(start));
-                            IfAction action = new IfAction(consumeTree(start, reader), selector, false);
-                            action.evaluate(this, node, writer);
-                        } else if (contentAttribute != null && forAttribute != null) {
-                            throw new TemplateSyntaxException(start.getLocation(), "rdf:for and rdf:content cannot both be present on an element");
-                        } else if (contentAttribute != null) {
-                            consumeTree(start, reader); // discard
-                            Selector<?> selector;
-                            try {
-                                selector = selectorFactory.get(contentAttribute.getValue());
-                            } catch (InvalidSelectorSyntaxException e) {
-                                throw new TemplateSyntaxException(contentAttribute.getLocation(), e);
-                            }
-                            ContentAction action = new ContentAction(start, selector);
-                            try {
-                                action.evaluate(this, node, writer, eventFactory);
-                            } catch (Exception e) {
-                                throw new TemplateInterpolationException(contentAttribute.getLocation(), action, node, e);
-                            }
-                        } else if (forAttribute != null) {
-                            Selector<RDFNode> selector;
-                            try {
-                                selector = selectorFactory.get(forAttribute.getValue()).withResultType(RDFNode.class);
-                            } catch (InvalidSelectorSyntaxException e) {
-                                throw new TemplateSyntaxException(forAttribute.getLocation(), e);
-                            }
-                            start = cloneStart(start, cloneAttributesWithout(start, ForAction.ACTION_QNAME), cloneNamespacesWithoutRdf(start));
-                            List<XMLEvent> tree = consumeTree(start, reader);
-                            ForAction action = new ForAction(tree, selector);
-                            try {
-                                action.evaluate(this, node, writer);
-                            } catch (Exception e) {
-                                throw new TemplateInterpolationException(forAttribute.getLocation(), action, node, e);
-                            }
-                        } else {
-                            start = interpolateAttributes(start, node);
-                            writer.add(start);
-                        }
-                    }
-                    break;
-                }
-                case XMLStreamConstants.CHARACTERS: {
-                    Characters characters = (Characters) event;
-                    interpolateCharacters(writer, characters, node);
-                    break;
-                }
-                case XMLStreamConstants.CDATA: {
-                    Characters characters = (Characters) event;
-                    interpolateCharacters(writer, characters, node);
-                    break;
-                }
-                default:
-                    writer.add(event);
-            }
-        }
-    }
-    
-    private 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")
-    protected 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
-                try {
-                    replacementValue = interpolateString(attribute.getValue(), node);
-                } catch (Exception e) {
-                    throw new TemplateInterpolationException(attribute.getLocation(), attribute.getValue(), node, e);
-                }
-            }
-            replacementAttributes.add(eventFactory.createAttribute(attribute.getName(),
-                    replacementValue));
-        }
-        return cloneStart(start, replacementAttributes, cloneNamespacesWithoutRdf(start));
-    }
-    
-    private StartElement cloneStart(StartElement start, Iterable<Attribute> attributes, Iterable<Namespace> namespaces) {
-        return eventFactory.createStartElement(
-                start.getName().getPrefix(),
-                start.getName().getNamespaceURI(),
-                start.getName().getLocalPart(),
-                attributes.iterator(),
-                namespaces.iterator(),
-                start.getNamespaceContext());
-    }
-    
-    @SuppressWarnings("unchecked")
-    private Set<Namespace> cloneNamespacesWithoutRdf(StartElement start) {
-        Set<Namespace> clonedNamespaces = new LinkedHashSet<Namespace>();
-        for (Iterator<Namespace> it = start.getNamespaces(); it.hasNext(); ) {
-            Namespace namespace = it.next();
-            if (!namespace.getNamespaceURI().equals(NS))
-                clonedNamespaces.add(namespace);
-        }
-        return clonedNamespaces;
-    }
-    
-    private static final Pattern SUBSTITUTION_PATTERN = Pattern.compile("\\$\\{([^}]*)\\}");
-    public 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 = selectorFactory.get(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 {
-                replacementValue = replacement.toString();
-            }
-            
-            matcher.appendReplacement(substituted, replacementValue.replace("$", "\\$"));
-        }
-        matcher.appendTail(substituted);
-        return substituted.toString();
-    }
-    
-    private void interpolateCharacters(XMLEventConsumer writer, Characters characters, RDFNode node) throws XMLStreamException {
-        String template = characters.getData();
-        if (!SUBSTITUTION_PATTERN.matcher(template).find()) {
-            writer.add(characters); // fast path
-            return;
-        }
-        Matcher matcher = SUBSTITUTION_PATTERN.matcher(template);
-        int lastAppendedPos = 0;
-        while (matcher.find()) {
-            writer.add(eventFactory.createCharacters(template.substring(lastAppendedPos, matcher.start())));
-            lastAppendedPos = matcher.end();
-            String expression = matcher.group(1);
-            Selector<?> selector;
-            try {
-                selector = selectorFactory.get(expression);
-            } catch (InvalidSelectorSyntaxException e) {
-                throw new TemplateSyntaxException(characters.getLocation(), e);
-            }
-            try {
-                Object replacement = selector.singleResult(node);
-                writeTreeForContent(writer, replacement);
-            } catch (Exception e) {
-                throw new TemplateInterpolationException(characters.getLocation(), expression, node, e);
-            }
-        }
-        writer.add(eventFactory.createCharacters(template.substring(lastAppendedPos)));
-    }
-    
-    protected void writeTreeForContent(XMLEventConsumer writer, Object replacement)
-            throws XMLStreamException {
-        if (replacement instanceof RDFNode) {
-            RDFNode replacementNode = (RDFNode) replacement;
-            if (replacementNode.isLiteral()) {
-                Literal literal = (Literal) replacementNode;
-                if (literal.isWellFormedXML()) {
-                    writeXMLLiteral(literal.getLexicalForm(), writer);
-                } else {
-                    writer.add(eventFactory.createCharacters(literal.getValue().toString()));
-                }
-            } else {
-                throw new UnsupportedOperationException("Not a literal: " + replacementNode);
-            }
-        } else if (replacement instanceof XMLStream) {
-            for (XMLEvent event: (XMLStream) replacement) {
-                writer.add(event);
-            }
-        } else {
-            writer.add(eventFactory.createCharacters(replacement.toString()));
-        }
-    }
-
-    @SuppressWarnings("unchecked")
-    protected 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;
-    }
-    
-    private void writeXMLLiteral(String literal, XMLEventConsumer writer)
-            throws XMLStreamException {
-        XMLEventReader reader = inputFactory.createXMLEventReader(new StringReader(literal));
-        while (reader.hasNext()) {
-            XMLEvent event = reader.nextEvent();
-            switch (event.getEventType()) {
-                case XMLStreamConstants.START_DOCUMENT:
-                case XMLStreamConstants.END_DOCUMENT:
-                    break; // discard
-                default:
-                    writer.add(event);
-            }
-        }
-    }
-    
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/TemplateSyntaxException.java b/src/main/java/au/com/miskinhill/rdftemplate/TemplateSyntaxException.java
@@ -1,22 +0,0 @@
-package au.com.miskinhill.rdftemplate;
-
-import javax.xml.stream.Location;
-import javax.xml.stream.XMLStreamException;
-
-public class TemplateSyntaxException extends RuntimeException {
-
-    private static final long serialVersionUID = 6518982504570154030L;
-    
-    public TemplateSyntaxException(Location location, String message) {
-        super("[location " + location.getLineNumber() + "," + location.getColumnNumber() + "] " + message);
-    }
-    
-    public TemplateSyntaxException(Location location, Throwable cause) {
-        super("[location " + location.getLineNumber() + "," + location.getColumnNumber() + "]", cause);
-    }
-    
-    public TemplateSyntaxException(XMLStreamException e) {
-        super(e);
-    }
-    
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/XMLStream.java b/src/main/java/au/com/miskinhill/rdftemplate/XMLStream.java
@@ -1,43 +0,0 @@
-package au.com.miskinhill.rdftemplate;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.List;
-
-import javax.xml.stream.XMLStreamConstants;
-import javax.xml.stream.events.XMLEvent;
-
-public class XMLStream implements Iterable<XMLEvent> {
-    
-    public static XMLStream collect(Iterator<XMLEvent> it) {
-        List<XMLEvent> events = new ArrayList<XMLEvent>();
-        while (it.hasNext()) {
-            XMLEvent event = it.next();
-            switch (event.getEventType()) {
-                case XMLStreamConstants.START_DOCUMENT:
-                case XMLStreamConstants.END_DOCUMENT:
-                    break; // discard
-                default:
-                    events.add(event);
-            }
-        }
-        return new XMLStream(events);
-    }
-    
-    private final List<XMLEvent> events;
-    
-    public XMLStream(XMLEvent... events) {
-        this.events = Arrays.asList(events);
-    }
-    
-    public XMLStream(List<XMLEvent> events) {
-        this.events = events;
-    }
-    
-    @Override
-    public Iterator<XMLEvent> iterator() {
-        return events.iterator();
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/datatype/DateDataType.java b/src/main/java/au/com/miskinhill/rdftemplate/datatype/DateDataType.java
@@ -1,112 +0,0 @@
-package au.com.miskinhill.rdftemplate.datatype;
-
-import org.springframework.stereotype.Component;
-
-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;
-
-@Component
-public class DateDataType implements RDFDatatype {
-    
-    public static final String URI = "http://www.w3.org/TR/xmlschema-2/#date";
-    
-    @SuppressWarnings("unused")
-    private static DateDataType instance;
-    public static void registerStaticInstance() {
-        instance = new DateDataType();
-    }
-    
-    private final DateTimeFormatter yearParser = DateTimeFormat.forPattern("yyyy");
-    private final DateTimeFormatter yearMonthParser = DateTimeFormat.forPattern("yyyy-MM");
-    private final DateTimeFormatter dateParser = DateTimeFormat.forPattern("yyyy-MM-dd");
-
-    public DateDataType() {
-        TypeMapper.getInstance().registerDatatype(this);
-    }
-
-    @Override
-    public String getURI() {
-        return URI;
-    }
-    
-    @Override
-    public Class<LocalDate> getJavaClass() {
-        return null;
-    }
-
-    @Override
-    public String unparse(Object value) {
-        throw new UnsupportedOperationException();
-    }
-
-    @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 Object parse(String lexicalForm) throws DatatypeFormatException {
-        try {
-            return dateParser.parseDateTime(lexicalForm).toLocalDate();
-        } catch (IllegalArgumentException e) {
-            // pass
-        }
-        try {
-            return new YearMonth(yearMonthParser.parseDateTime(lexicalForm).toLocalDate());
-        } catch (IllegalArgumentException e) {
-            // pass
-        }
-        try {
-            return new Year(yearParser.parseDateTime(lexicalForm).toLocalDate());
-        } catch (IllegalArgumentException e) {
-            // pass
-        }
-        throw new DatatypeFormatException(lexicalForm, this, "No matching parsers found");
-    }
-
-    @Override
-    public boolean isValid(String lexicalForm) {
-        try {
-            parse(lexicalForm);
-            return true;
-        } catch (DatatypeFormatException e) {
-            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/datatype/DateTimeDataType.java b/src/main/java/au/com/miskinhill/rdftemplate/datatype/DateTimeDataType.java
@@ -1,98 +0,0 @@
-package au.com.miskinhill.rdftemplate.datatype;
-
-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.DateTime;
-import org.joda.time.format.DateTimeFormatter;
-import org.joda.time.format.ISODateTimeFormat;
-import org.springframework.stereotype.Component;
-
-@Component
-public class DateTimeDataType implements RDFDatatype {
-    
-    public static final String URI = "http://www.w3.org/TR/xmlschema-2/#datetime";
-    
-    @SuppressWarnings("unused")
-    private static DateTimeDataType instance;
-    public static void registerStaticInstance() {
-        instance = new DateTimeDataType();
-    }
-    
-    private final DateTimeFormatter format = ISODateTimeFormat.dateTimeNoMillis().withOffsetParsed();
-
-    public DateTimeDataType() {
-        TypeMapper.getInstance().registerDatatype(this);
-    }
-
-    @Override
-    public String getURI() {
-        return URI;
-    }
-    
-    @Override
-    public Class<DateTime> getJavaClass() {
-        return DateTime.class;
-    }
-
-    @Override
-    public String unparse(Object value) {
-        return ((DateTime) value).toString(format);
-    }
-
-    @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 DateTime parse(String lexicalForm) throws DatatypeFormatException {
-        try {
-            return format.parseDateTime(lexicalForm);
-        } catch (IllegalArgumentException e) {
-            throw new DatatypeFormatException(lexicalForm, this, "Parser barfed");
-        }
-    }
-
-    @Override
-    public boolean isValid(String lexicalForm) {
-        try {
-            parse(lexicalForm);
-            return true;
-        } catch (DatatypeFormatException e) {
-            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 DateTime);
-    }
-
-    @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/datatype/Year.java b/src/main/java/au/com/miskinhill/rdftemplate/datatype/Year.java
@@ -1,50 +0,0 @@
-package au.com.miskinhill.rdftemplate.datatype;
-
-import org.joda.time.LocalDate;
-
-public class Year {
-    
-    private final int year;
-    
-    public Year(int value) {
-        this.year = value;
-    }
-    
-    public Year(LocalDate date) {
-        this.year = date.getYear();
-    }
-    
-    public Year(YearMonth yearMonth) {
-        this.year = yearMonth.getYear();
-    }
-    
-    public int getYear() {
-        return year;
-    }
-    
-    @Override
-    public String toString() {
-        return Integer.toString(year);
-    }
-
-    @Override
-    public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + year;
-        return result;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj)
-            return true;
-        if (obj == null)
-            return false;
-        if (getClass() != obj.getClass())
-            return false;
-        Year other = (Year) obj;
-        return (year == other.year);
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/datatype/YearMonth.java b/src/main/java/au/com/miskinhill/rdftemplate/datatype/YearMonth.java
@@ -1,58 +0,0 @@
-package au.com.miskinhill.rdftemplate.datatype;
-
-import org.joda.time.LocalDate;
-
-public class YearMonth {
-    
-    private final int year;
-    private final int month;
-    
-    public YearMonth(int year, int month) {
-        this.year = year;
-        this.month = month;
-    }
-    
-    public YearMonth(LocalDate date) {
-        this.year = date.getYear();
-        this.month = date.getMonthOfYear();
-    }
-    
-    public int getYear() {
-        return year;
-    }
-    
-    public int getMonth() {
-        return month;
-    }
-    
-    @Override
-    public String toString() {
-        return String.format("%04d-%02d", year, month);
-    }
-
-    @Override
-    public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + month;
-        result = prime * result + year;
-        return result;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj)
-            return true;
-        if (obj == null)
-            return false;
-        if (getClass() != obj.getClass())
-            return false;
-        YearMonth other = (YearMonth) obj;
-        if (month != other.month)
-            return false;
-        if (year != other.year)
-            return false;
-        return true;
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/AbstractAdaptation.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/AbstractAdaptation.java
@@ -1,58 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import java.util.Arrays;
-
-import com.hp.hpl.jena.rdf.model.RDFNode;
-
-public abstract class AbstractAdaptation<DestType, NodeType extends RDFNode> implements Adaptation<DestType> {
-    
-    private final Class<DestType> destinationType;
-    private final Class<?>[] argTypes;
-    private final Class<NodeType> nodeType;
-    
-    protected AbstractAdaptation(Class<DestType> destinationType, Class<?>[] argTypes, Class<NodeType> nodeType) {
-        this.destinationType = destinationType;
-        this.argTypes = argTypes;
-        this.nodeType = nodeType;
-    }
-    
-    @Override
-    public Class<DestType> getDestinationType() {
-        return destinationType;
-    }
-    
-    @Override
-    public Class<?>[] getArgTypes() {
-        return argTypes;
-    }
-    
-    @Override
-    public void setArgs(Object[] args) {
-        if (args.length != argTypes.length)
-            throw new SelectorEvaluationException("Expected args of types " + Arrays.toString(argTypes) +
-                    " but invoked with " + Arrays.toString(args));
-        for (int i = 0; i < args.length; i ++) {
-            if (!argTypes[i].isAssignableFrom(args[i].getClass()))
-                throw new SelectorEvaluationException("Arg " + i + ": expected type " + argTypes[i] +
-                        " but was " + args[i].getClass());
-        }
-        setCheckedArgs(args);
-    }
-    
-    protected void setCheckedArgs(Object[] args) {
-        throw new UnsupportedOperationException();
-    }
-    
-    @Override
-    public DestType adapt(RDFNode node) {
-        if (!nodeType.equals(RDFNode.class)) {
-            if (!node.canAs(nodeType))
-                throw new SelectorEvaluationException("Adaptation can only be applied to " + nodeType +
-                        " but was applied to " + node);
-        }
-        return doAdapt(node.as(nodeType));
-    }
-    
-    protected abstract DestType doAdapt(NodeType node);
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/AbstractSelector.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/AbstractSelector.java
@@ -1,40 +0,0 @@
-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
@@ -1,15 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import com.hp.hpl.jena.rdf.model.RDFNode;
-
-public interface Adaptation<T> {
-
-    Class<T> getDestinationType();
-    
-    Class<?>[] getArgTypes();
-    
-    void setArgs(Object[] args);
-    
-    T adapt(RDFNode node);
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/AdaptationFactory.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/AdaptationFactory.java
@@ -1,9 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-public interface AdaptationFactory {
-    
-    boolean hasName(String name);
-    
-    Adaptation<?> getByName(String name);
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/AntlrSelectorFactory.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/AntlrSelectorFactory.java
@@ -1,48 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import java.util.Collections;
-import java.util.Map;
-
-import org.antlr.runtime.ANTLRStringStream;
-import org.antlr.runtime.CharStream;
-import org.antlr.runtime.CommonTokenStream;
-import org.antlr.runtime.RecognitionException;
-
-public class AntlrSelectorFactory implements SelectorFactory {
-    
-    private AdaptationFactory adaptationFactory = new DefaultAdaptationFactory();
-    private PredicateResolver predicateResolver = new DefaultPredicateResolver();
-    private Map<String, String> namespacePrefixMap = Collections.emptyMap();
-    
-    public AntlrSelectorFactory() {
-    }
-    
-    public void setAdaptationFactory(AdaptationFactory adaptationFactory) {
-        this.adaptationFactory = adaptationFactory;
-    }
-    
-    public void setPredicateResolver(PredicateResolver predicateResolver) {
-        this.predicateResolver = predicateResolver;
-    }
-    
-    public void setNamespacePrefixMap(Map<String, String> namespacePrefixMap) {
-        this.namespacePrefixMap = namespacePrefixMap;
-    }
-    
-    @Override
-    public Selector<?> get(String expression) {
-        CharStream stream = new ANTLRStringStream(expression);
-        SelectorLexer lexer = new SelectorLexer(stream);
-        CommonTokenStream tokens = new CommonTokenStream(lexer);
-        SelectorParser parser = new SelectorParser(tokens);
-        parser.setAdaptationFactory(adaptationFactory);
-        parser.setPredicateResolver(predicateResolver);
-        parser.setNamespacePrefixMap(namespacePrefixMap);
-        try {
-            return parser.unionSelector();
-        } catch (RecognitionException e) {
-            throw new InvalidSelectorSyntaxException(e);
-        }
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/BooleanAndPredicate.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/BooleanAndPredicate.java
@@ -1,34 +0,0 @@
-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/ComparableLiteralValueAdaptation.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/ComparableLiteralValueAdaptation.java
@@ -1,22 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import com.hp.hpl.jena.rdf.model.Literal;
-
-@SuppressWarnings("unchecked")
-public class ComparableLiteralValueAdaptation extends AbstractAdaptation<Comparable, Literal> {
-    
-    public ComparableLiteralValueAdaptation() {
-        super(Comparable.class, new Class<?>[] { }, Literal.class);
-    }
-    
-    @Override
-    protected Comparable<?> doAdapt(Literal node) {
-        Object literalValue = 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/DefaultAdaptationFactory.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/DefaultAdaptationFactory.java
@@ -1,35 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import org.springframework.beans.BeanUtils;
-
-public class DefaultAdaptationFactory implements AdaptationFactory {
-    
-    private static final Map<String, Class<? extends Adaptation<?>>> ADAPTATIONS = new HashMap<String, Class<? extends Adaptation<?>>>();
-    static {
-        ADAPTATIONS.put("uri", UriAdaptation.class);
-        ADAPTATIONS.put("uri-slice", UriSliceAdaptation.class);
-        ADAPTATIONS.put("uri-anchor", UriAnchorAdaptation.class);
-        ADAPTATIONS.put("lv", LiteralValueAdaptation.class);
-        ADAPTATIONS.put("comparable-lv", ComparableLiteralValueAdaptation.class);
-        ADAPTATIONS.put("string-lv", StringLiteralValueAdaptation.class);
-        ADAPTATIONS.put("formatted-dt", FormattedDateTimeAdaptation.class);
-    }
-    
-    @Override
-    public boolean hasName(String name) {
-        return ADAPTATIONS.containsKey(name);
-    }
-    
-    @Override
-    public Adaptation<?> getByName(String name) {
-        Class<? extends Adaptation<?>> adaptationClass = ADAPTATIONS.get(name);
-        if (adaptationClass == null) {
-            throw new InvalidSelectorSyntaxException("No adaptation named " + name);
-        }
-        return BeanUtils.instantiate(adaptationClass);
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/DefaultPredicateResolver.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/DefaultPredicateResolver.java
@@ -1,19 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public class DefaultPredicateResolver implements PredicateResolver {
-    
-    private static final Map<String, Class<? extends Predicate>> PREDICATES = new HashMap<String, Class<? extends Predicate>>();
-    static {
-        PREDICATES.put("type", TypePredicate.class);
-        PREDICATES.put("uri-prefix", UriPrefixPredicate.class);
-    }
-
-    @Override
-    public Class<? extends Predicate> getByName(String name) {
-        return PREDICATES.get(name);
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/EternallyCachingSelectorFactory.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/EternallyCachingSelectorFactory.java
@@ -1,35 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * {@link SelectorFactory} implementation which indirects to a real
- * implementation and caches its return values eternally. Do not use in
- * situations where the set of input expressions can be unbounded (e.g.
- * user-provided) as this will lead to unbounded cache growth.
- * <p>
- * A better implementation would use a LRU cache or similar, but I cbf.
- */
-public class EternallyCachingSelectorFactory implements SelectorFactory {
-    
-    private final SelectorFactory real;
-    private final Map<String, Selector<?>> cache = new HashMap<String, Selector<?>>();
-    
-    public EternallyCachingSelectorFactory(SelectorFactory real) {
-        this.real = real;
-    }
-    
-    @Override
-    public Selector<?> get(String expression) {
-        Selector<?> cached = cache.get(expression);
-        if (cached == null) {
-            Selector<?> fresh = real.get(expression);
-            cache.put(expression, fresh);
-            return fresh;
-        } else {
-            return cached;
-        }
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/FormattedDateTimeAdaptation.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/FormattedDateTimeAdaptation.java
@@ -1,49 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import org.apache.commons.lang.builder.ToStringBuilder;
-import org.joda.time.ReadableInstant;
-import org.joda.time.ReadablePartial;
-import org.joda.time.format.DateTimeFormat;
-import org.joda.time.format.DateTimeFormatter;
-
-import com.hp.hpl.jena.rdf.model.Literal;
-
-public class FormattedDateTimeAdaptation extends AbstractAdaptation<String, Literal> {
-    
-    private String pattern;
-    private DateTimeFormatter formatter;
-    
-    public FormattedDateTimeAdaptation() {
-        super(String.class, new Class<?>[] { String.class }, Literal.class);
-    }
-    
-    @Override
-    protected void setCheckedArgs(Object[] args) {
-        this.pattern = (String) args[0];
-        this.formatter = DateTimeFormat.forPattern(pattern.replace("\"", "'")); // for convenience in XML
-    }
-
-    public String getPattern() {
-        return pattern;
-    }
-    
-    @Override
-    protected String doAdapt(Literal node) {
-        Object lv = node.getValue();
-        if (lv instanceof ReadableInstant) {
-            ReadableInstant instant = (ReadableInstant) lv;
-            return formatter.print(instant);
-        } else if (lv instanceof ReadablePartial) {
-            ReadablePartial instant = (ReadablePartial) lv;
-            return formatter.print(instant);
-        } else {
-            throw new SelectorEvaluationException("Attempted to apply #formatted-dt to non-datetime literal " + lv);
-        }
-    }
-    
-    @Override
-    public String toString() {
-        return new ToStringBuilder(this).append("pattern", pattern).toString();
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/InvalidSelectorSyntaxException.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/InvalidSelectorSyntaxException.java
@@ -1,15 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-public class InvalidSelectorSyntaxException extends RuntimeException {
-    
-    private static final long serialVersionUID = 5805546105865617336L;
-
-    public InvalidSelectorSyntaxException(Throwable cause) {
-        super(cause);
-    }
-    
-    public InvalidSelectorSyntaxException(String message) {
-        super(message);
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/LiteralValueAdaptation.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/LiteralValueAdaptation.java
@@ -1,16 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import com.hp.hpl.jena.rdf.model.Literal;
-
-public class LiteralValueAdaptation extends AbstractAdaptation<Object, Literal> {
-    
-    public LiteralValueAdaptation() {
-        super(Object.class, new Class<?>[] { }, Literal.class);
-    }
-    
-    @Override
-    protected Object doAdapt(Literal node) {
-        return 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
@@ -1,19 +0,0 @@
-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
@@ -1,7 +0,0 @@
-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/PredicateResolver.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/PredicateResolver.java
@@ -1,7 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-public interface PredicateResolver {
-    
-    Class<? extends Predicate> getByName(String name);
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/Selector.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/Selector.java
@@ -1,17 +0,0 @@
-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/SelectorComparator.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/SelectorComparator.java
@@ -1,58 +0,0 @@
-/**
- * 
- */
-package au.com.miskinhill.rdftemplate.selector;
-
-import java.util.Comparator;
-
-import org.apache.commons.lang.builder.ToStringBuilder;
-
-import com.hp.hpl.jena.rdf.model.RDFNode;
-
-public class SelectorComparator<T extends Comparable<T>> implements Comparator<RDFNode> {
-    
-    private Selector<T> selector;
-    private boolean reversed = false;
-    
-    public Selector<T> getSelector() {
-        return selector;
-    }
-    
-    public void setSelector(Selector<T> selector) {
-        this.selector = selector;
-    }
-    
-    public boolean isReversed() {
-        return reversed;
-    }
-    
-    public void setReversed(boolean reversed) {
-        this.reversed = reversed;
-    }
-    
-    @Override
-    public String toString() {
-        return new ToStringBuilder(this).append(selector).append("reversed", reversed).toString();
-    }
-    
-    @Override
-    public int compare(RDFNode left, RDFNode right) {
-        T leftKey;
-        try {
-            leftKey = selector.singleResult(left);
-        } catch (SelectorEvaluationException e) {
-            throw new SelectorEvaluationException("Exception evaluating selector [" + selector + "] " +
-            		"with context node [" + left + "] for comparison", e);
-        }
-        T rightKey;
-        try {
-            rightKey = selector.singleResult(right);
-        } catch (SelectorEvaluationException e) {
-            throw new SelectorEvaluationException("Exception evaluating selector [" + selector + "] " +
-            		"with context node [" + right + "] for comparison", e);
-        }
-        int result = leftKey.compareTo(rightKey);
-        return reversed ? -result : result;
-    }
-    
-}
-\ No newline at end of file
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/SelectorEvaluationException.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/SelectorEvaluationException.java
@@ -1,15 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-public class SelectorEvaluationException extends RuntimeException {
-
-    private static final long serialVersionUID = -398277800899471326L;
-    
-    public SelectorEvaluationException(String message) {
-        super(message);
-    }
-    
-    public SelectorEvaluationException(String message, Throwable cause) {
-        super(message, cause);
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/SelectorFactory.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/SelectorFactory.java
@@ -1,7 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-public interface SelectorFactory {
-    
-    Selector<?> get(String expression);
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/SelectorWithAdaptation.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/SelectorWithAdaptation.java
@@ -1,48 +0,0 @@
-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/StringLiteralValueAdaptation.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/StringLiteralValueAdaptation.java
@@ -1,45 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import java.io.StringReader;
-
-import javax.xml.stream.XMLEventReader;
-import javax.xml.stream.XMLInputFactory;
-import javax.xml.stream.XMLStreamException;
-import javax.xml.stream.events.XMLEvent;
-
-import com.hp.hpl.jena.rdf.model.Literal;
-
-public class StringLiteralValueAdaptation extends AbstractAdaptation<String, Literal> {
-    
-    private static final XMLInputFactory inputFactory = XMLInputFactory.newInstance();
-    
-    public StringLiteralValueAdaptation() {
-        super(String.class, new Class<?>[] { }, Literal.class);
-    }
-    
-    @Override
-    protected String doAdapt(Literal literal) {
-        if (literal.isWellFormedXML()) {
-            try {
-                return stripTags(literal.getLexicalForm());
-            } catch (XMLStreamException e) {
-                throw new RuntimeException(e);
-            }
-        } else {
-            return literal.getValue().toString();
-        }
-    }
-    
-    private String stripTags(String literal) throws XMLStreamException {
-        StringBuilder sb = new StringBuilder();
-        XMLEventReader reader = inputFactory.createXMLEventReader(new StringReader(literal));
-        while (reader.hasNext()) {
-            XMLEvent event = reader.nextEvent();
-            if (event.isCharacters()) {
-                sb.append(event.asCharacters().getData());
-            }
-        }
-        return sb.toString();
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/Traversal.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/Traversal.java
@@ -1,126 +0,0 @@
-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;
-
-public class Traversal {
-    
-    private String propertyNamespace;
-    private String propertyLocalName;
-    private boolean inverse = false;
-    private Predicate predicate;
-    private List<Comparator<RDFNode>> sortOrder = new ArrayList<Comparator<RDFNode>>();
-    private Integer subscript;
-    
-    private class SortComparator implements Comparator<RDFNode> {
-        @Override
-        public int compare(RDFNode left, RDFNode right) {
-            for (Comparator<RDFNode> comparator: sortOrder) {
-                int result = comparator.compare(left, right);
-                if (result != 0)
-                    return result;
-            }
-            return 0;
-        }
-    }
-    
-    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(propertyNamespace, 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());
-            }
-        }
-        CollectionUtils.filter(destinations, predicate);
-        if (!sortOrder.isEmpty())
-            Collections.sort(destinations, new SortComparator());
-        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("propertyNamespace", propertyNamespace)
-                .append("propertyLocalName", propertyLocalName)
-                .append("inverse", inverse)
-                .append("predicate", predicate)
-                .append("sortOrder", sortOrder)
-                .append("subscript", subscript)
-                .toString();
-    }
-    
-    public String getPropertyLocalName() {
-        return propertyLocalName;
-    }
-    
-    public void setPropertyLocalName(String propertyLocalName) {
-        this.propertyLocalName = propertyLocalName;
-    }
-    
-    public String getPropertyNamespace() {
-        return propertyNamespace;
-    }
-    
-    public void setPropertyNamespace(String propertyNamespace) {
-        this.propertyNamespace = propertyNamespace;
-    }
-    
-    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 List<Comparator<RDFNode>> getSortOrder() {
-        return sortOrder;
-    }
-    
-    public void addSortOrderComparator(Comparator<RDFNode> selector) {
-        this.sortOrder.add(selector);
-    }
-    
-    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
@@ -1,46 +0,0 @@
-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/TypePredicate.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/TypePredicate.java
@@ -1,46 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-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;
-
-public class TypePredicate implements Predicate {
-    
-    private final String namespace;
-    private final String localName;
-    
-    public TypePredicate(String namespace, String localName) {
-        this.namespace = namespace;
-        this.localName = localName;
-    }
-    
-    public String getNamespace() {
-        return namespace;
-    }
-    
-    public String getLocalName() {
-        return localName;
-    }
-    
-    @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(namespace + localName);
-        for (Statement statement: resource.listProperties(RDF.type).toSet()) {
-            if (statement.getObject().equals(type))
-                return true;
-        }
-        return false;
-    }
-    
-    @Override
-    public String toString() {
-        return new ToStringBuilder(this).append(namespace).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
@@ -1,45 +0,0 @@
-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/main/java/au/com/miskinhill/rdftemplate/selector/UriAdaptation.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/UriAdaptation.java
@@ -1,16 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import com.hp.hpl.jena.rdf.model.Resource;
-
-public class UriAdaptation extends AbstractAdaptation<String, Resource> {
-    
-    public UriAdaptation() {
-        super(String.class, new Class<?>[] { }, Resource.class);
-    }
-    
-    @Override
-    protected String doAdapt(Resource node) {
-        return node.getURI();
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/UriAnchorAdaptation.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/UriAnchorAdaptation.java
@@ -1,24 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import com.hp.hpl.jena.rdf.model.Resource;
-
-/**
- * Returns the anchor component of the node's URI (excluding initial #), or the
- * empty string if it has no anchor component.
- */
-public class UriAnchorAdaptation extends AbstractAdaptation<String, Resource> {
-    
-    public UriAnchorAdaptation() {
-        super(String.class, new Class<?>[] { }, Resource.class);
-    }
-    
-    @Override
-    protected String doAdapt(Resource node) {
-        String uri = node.getURI();
-        int hashIndex = uri.lastIndexOf('#');
-        if (hashIndex < 0)
-            return "";
-        return uri.substring(hashIndex + 1);
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/selector/UriPrefixPredicate.java b/src/main/java/au/com/miskinhill/rdftemplate/selector/UriPrefixPredicate.java
@@ -1,26 +0,0 @@
-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
@@ -1,28 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import com.hp.hpl.jena.rdf.model.Resource;
-
-public class UriSliceAdaptation extends AbstractAdaptation<String, Resource> {
-    
-    private Integer startIndex;
-    
-    public UriSliceAdaptation() {
-        super(String.class, new Class<?>[] { Integer.class }, Resource.class);
-    }
-    
-    public Integer getStartIndex() {
-        return startIndex;
-    }
-    
-    @Override
-    protected void setCheckedArgs(Object[] args) {
-        this.startIndex = (Integer) args[0];
-    }
-    
-    @Override
-    protected String doAdapt(Resource node) {
-        String uri = node.getURI();
-        return uri.substring(startIndex);
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/view/RDFTemplateView.java b/src/main/java/au/com/miskinhill/rdftemplate/view/RDFTemplateView.java
@@ -1,76 +0,0 @@
-package au.com.miskinhill.rdftemplate.view;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Locale;
-import java.util.Map;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.springframework.web.servlet.view.AbstractTemplateView;
-
-import au.com.miskinhill.rdftemplate.TemplateInterpolator;
-import au.com.miskinhill.rdftemplate.selector.SelectorFactory;
-import au.id.djc.jena.util.ModelOperations;
-import au.id.djc.jena.util.ModelOperations.ModelExecutionCallbackWithoutResult;
-
-import com.hp.hpl.jena.rdf.model.Model;
-import com.hp.hpl.jena.rdf.model.Resource;
-
-public class RDFTemplateView extends AbstractTemplateView {
-    
-    public static final String NODE_URI_KEY = "nodeUri";
-    
-    private TemplateInterpolator templateInterpolator;
-    private SelectorFactory selectorFactory;
-    private ModelOperations modelOperations;
-    
-    public void setSelectorFactory(SelectorFactory selectorFactory) {
-        this.selectorFactory = selectorFactory;
-    }
-    
-    public void setModelOperations(ModelOperations modelOperations) {
-        this.modelOperations = modelOperations;
-    }
-    
-    @Override
-    public void afterPropertiesSet() throws Exception {
-        super.afterPropertiesSet();
-        if (selectorFactory == null) {
-            throw new IllegalArgumentException("Property 'selectorFactory' is required");
-        }
-        if (modelOperations == null) {
-            throw new IllegalArgumentException("Property 'sdbTemplate' is required");
-        }
-        this.templateInterpolator = new TemplateInterpolator(selectorFactory);
-    }
-
-    @Override
-    protected void renderMergedTemplateModel(final Map<String, Object> model,
-            final HttpServletRequest request, final HttpServletResponse response)
-            throws Exception {
-        final InputStream inputStream = getApplicationContext().getResource(getUrl()).getInputStream();
-        try {
-            modelOperations.withModel(new ModelExecutionCallbackWithoutResult() {
-                @Override
-                protected void executeWithoutResult(Model rdfModel) {
-                    Resource node = rdfModel.getResource((String) model.get(NODE_URI_KEY));
-                    try {
-                        templateInterpolator.interpolate(inputStream, node, response.getWriter());
-                    } catch (IOException e) {
-                        throw new RuntimeException(e);
-                    }
-                }
-            });
-        } finally {
-            inputStream.close();
-        }
-    }
-    
-    @Override
-    public boolean checkResource(Locale locale) throws Exception {
-        return getApplicationContext().getResource(getUrl()).exists();
-    }
-
-}
diff --git a/src/main/java/au/com/miskinhill/rdftemplate/view/RDFTemplateViewResolver.java b/src/main/java/au/com/miskinhill/rdftemplate/view/RDFTemplateViewResolver.java
@@ -1,54 +0,0 @@
-package au.com.miskinhill.rdftemplate.view;
-
-import org.springframework.beans.factory.InitializingBean;
-import org.springframework.web.servlet.view.AbstractTemplateView;
-import org.springframework.web.servlet.view.AbstractTemplateViewResolver;
-
-import au.com.miskinhill.rdftemplate.selector.SelectorFactory;
-import au.id.djc.jena.util.ModelOperations;
-
-public class RDFTemplateViewResolver extends AbstractTemplateViewResolver implements InitializingBean {
-    
-    private SelectorFactory selectorFactory;
-    private ModelOperations modelOperations;
-    
-    public RDFTemplateViewResolver() {
-        super();
-        setViewClass(requiredViewClass());
-        setExposeRequestAttributes(false);
-        setExposeSessionAttributes(false);
-        setExposeSpringMacroHelpers(false);
-    }
-    
-    public void setSelectorFactory(SelectorFactory selectorFactory) {
-        this.selectorFactory = selectorFactory;
-    }
-
-    public void setModelOperations(ModelOperations modelOperations) {
-        this.modelOperations = modelOperations;
-    }
-    
-    @Override
-    public void afterPropertiesSet() throws Exception {
-        if (selectorFactory == null) {
-            throw new IllegalArgumentException("Property 'selectorFactory' is required");
-        }
-        if (modelOperations == null) {
-            throw new IllegalArgumentException("Property 'modelOperations' is required");
-        }
-    }
-    
-    @Override
-    protected Class<? extends AbstractTemplateView> requiredViewClass() {
-        return RDFTemplateView.class;
-    }
-    
-    @Override
-    protected RDFTemplateView buildView(String viewName) throws Exception {
-        RDFTemplateView view = (RDFTemplateView) super.buildView(viewName);
-        view.setSelectorFactory(selectorFactory);
-        view.setModelOperations(modelOperations);
-        return view;
-    }
-
-}
diff --git a/src/main/java/au/id/djc/rdftemplate/ContentAction.java b/src/main/java/au/id/djc/rdftemplate/ContentAction.java
@@ -0,0 +1,68 @@
+package au.id.djc.rdftemplate;
+
+import java.util.Set;
+
+import javax.xml.XMLConstants;
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLEventFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.Attribute;
+import javax.xml.stream.events.StartElement;
+import javax.xml.stream.util.XMLEventConsumer;
+
+import com.hp.hpl.jena.rdf.model.Literal;
+import com.hp.hpl.jena.rdf.model.RDFNode;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+import au.id.djc.rdftemplate.selector.Selector;
+
+public class ContentAction extends TemplateAction {
+    
+    public static final String ACTION_NAME = "content";
+    public static final QName ACTION_QNAME = new QName(TemplateInterpolator.NS, ACTION_NAME);
+    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 final StartElement start;
+    private final Selector<?> selector;
+    
+    public ContentAction(StartElement start, Selector<?> selector) {
+        this.start = start;
+        this.selector = selector;
+    }
+    
+    public void evaluate(TemplateInterpolator interpolator, RDFNode node, XMLEventConsumer writer, XMLEventFactory eventFactory)
+            throws XMLStreamException {
+        Object replacement = selector.singleResult(node);
+        StartElement start = interpolator.interpolateAttributes(this.start, node);
+        Set<Attribute> attributes = interpolator.cloneAttributesWithout(start, ACTION_QNAME);
+        if (replacement instanceof Literal) {
+            Literal literal = (Literal) replacement;
+            if (!StringUtils.isEmpty(literal.getLanguage())) {
+                attributes.add(eventFactory.createAttribute(XML_LANG_QNAME, ((Literal) replacement).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()));
+        interpolator.writeTreeForContent(writer, replacement);
+        writer.add(eventFactory.createEndElement(start.getName(), start.getNamespaces()));
+    }
+    
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this)
+                .append("start", start)
+                .append("selector", selector)
+                .toString();
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/ForAction.java b/src/main/java/au/id/djc/rdftemplate/ForAction.java
@@ -0,0 +1,55 @@
+package au.id.djc.rdftemplate;
+
+import java.util.List;
+import java.util.logging.Logger;
+
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.XMLEvent;
+import javax.xml.stream.util.XMLEventConsumer;
+
+import com.hp.hpl.jena.rdf.model.RDFNode;
+import com.hp.hpl.jena.rdf.model.Resource;
+import com.hp.hpl.jena.rdf.model.Seq;
+import com.hp.hpl.jena.vocabulary.RDF;
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+import au.id.djc.rdftemplate.selector.Selector;
+
+public class ForAction extends TemplateAction {
+    
+    public static final String ACTION_NAME = "for";
+    public static final QName ACTION_QNAME = new QName(TemplateInterpolator.NS, ACTION_NAME);
+    private static final Logger LOG = Logger.getLogger(ForAction.class.getName());
+    
+    private final List<XMLEvent> tree;
+    private final Selector<RDFNode> selector;
+    
+    public ForAction(List<XMLEvent> tree, Selector<RDFNode> selector) {
+        this.tree = tree;
+        this.selector = selector;
+    }
+    
+    public void evaluate(TemplateInterpolator interpolator, RDFNode node, XMLEventConsumer writer)
+            throws XMLStreamException {
+        List<RDFNode> result = selector.result(node);
+        if (result.size() == 1 && result.get(0).canAs(Resource.class)) {
+            if (result.get(0).as(Resource.class).hasProperty(RDF.type, RDF.Seq)) {
+                LOG.fine("Apply rdf:Seq special case for " + result.get(0));
+                result = result.get(0).as(Seq.class).iterator().toList();
+                LOG.fine("Resulting sequence is " + result);
+            }
+        }
+        for (RDFNode eachNode: result) {
+            interpolator.interpolate(tree.iterator(), eachNode, writer);
+        }
+    }
+    
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this)
+                .append("selector", selector)
+                .toString();
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/IfAction.java b/src/main/java/au/id/djc/rdftemplate/IfAction.java
@@ -0,0 +1,46 @@
+package au.id.djc.rdftemplate;
+
+import java.util.List;
+
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.XMLEvent;
+import javax.xml.stream.util.XMLEventConsumer;
+
+import com.hp.hpl.jena.rdf.model.RDFNode;
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+import au.id.djc.rdftemplate.selector.Selector;
+
+public class IfAction extends TemplateAction {
+    
+    public static final String ACTION_NAME = "if";
+    public static final QName ACTION_QNAME = new QName(TemplateInterpolator.NS, ACTION_NAME);
+    
+    private final List<XMLEvent> tree;
+    private final Selector<?> condition;
+    private final boolean negate;
+    
+    public IfAction(List<XMLEvent> tree, Selector<?> condition, boolean negate) {
+        this.tree = tree;
+        this.condition = condition;
+        this.negate = negate;
+    }
+    
+    public void evaluate(TemplateInterpolator interpolator, RDFNode node, XMLEventConsumer writer)
+            throws XMLStreamException {
+        List<?> selectorResult = condition.result(node);
+        if (negate ? selectorResult.isEmpty() : !selectorResult.isEmpty()) {
+            interpolator.interpolate(tree.iterator(), node, writer);
+        }
+    }
+    
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this)
+                .append("condition", condition)
+                .append("negate", negate)
+                .toString();
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/JoinAction.java b/src/main/java/au/id/djc/rdftemplate/JoinAction.java
@@ -0,0 +1,64 @@
+package au.id.djc.rdftemplate;
+
+import java.util.List;
+import java.util.logging.Logger;
+
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLEventFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.XMLEvent;
+import javax.xml.stream.util.XMLEventConsumer;
+
+import com.hp.hpl.jena.rdf.model.RDFNode;
+import com.hp.hpl.jena.rdf.model.Resource;
+import com.hp.hpl.jena.rdf.model.Seq;
+import com.hp.hpl.jena.vocabulary.RDF;
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+import au.id.djc.rdftemplate.selector.Selector;
+
+public class JoinAction extends TemplateAction {
+    
+    public static final String ACTION_NAME = "join";
+    public static final QName ACTION_QNAME = new QName(TemplateInterpolator.NS, ACTION_NAME);
+    private static final Logger LOG = Logger.getLogger(JoinAction.class.getName());
+    
+    private final List<XMLEvent> tree;
+    private final Selector<RDFNode> selector;
+    private final String separator;
+    
+    public JoinAction(List<XMLEvent> tree, Selector<RDFNode> selector, String separator) {
+        this.tree = tree;
+        this.selector = selector;
+        this.separator = separator;
+    }
+    
+    public void evaluate(TemplateInterpolator interpolator, RDFNode node, XMLEventConsumer writer, XMLEventFactory eventFactory)
+            throws XMLStreamException {
+        List<RDFNode> result = selector.result(node);
+        if (result.size() == 1 && result.get(0).canAs(Resource.class)) {
+            if (result.get(0).as(Resource.class).hasProperty(RDF.type, RDF.Seq)) {
+                LOG.fine("Apply rdf:Seq special case for " + result.get(0));
+                result = result.get(0).as(Seq.class).iterator().toList();
+                LOG.fine("Resulting sequence is " + result);
+            }
+        }
+        boolean first = true;
+        for (RDFNode eachNode: result) {
+            if (!first) {
+                writer.add(eventFactory.createCharacters(separator));
+            }
+            interpolator.interpolate(tree.iterator(), eachNode, writer);
+            first = false;
+        }
+    }
+    
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this)
+                .append("selector", selector)
+                .append("separator", separator)
+                .toString();
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/TemplateAction.java b/src/main/java/au/id/djc/rdftemplate/TemplateAction.java
@@ -0,0 +1,5 @@
+package au.id.djc.rdftemplate;
+
+public abstract class TemplateAction {
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/TemplateInterpolationException.java b/src/main/java/au/id/djc/rdftemplate/TemplateInterpolationException.java
@@ -0,0 +1,23 @@
+package au.id.djc.rdftemplate;
+
+import javax.xml.stream.Location;
+
+import com.hp.hpl.jena.rdf.model.RDFNode;
+
+public class TemplateInterpolationException extends RuntimeException {
+    
+    private static final long serialVersionUID = -1472104970210074672L;
+
+    public TemplateInterpolationException(Location location, TemplateAction action, RDFNode node, Throwable cause) {
+        super("Exception evaluating action [" + action + "] " +
+                "at location [" + location.getLineNumber() + "," + location.getColumnNumber() + "] " +
+                "with context node " + node, cause);
+    }
+    
+    public TemplateInterpolationException(Location location, String selectorExpression, RDFNode node, Throwable cause) {
+        super("Exception evaluating selector expression [" + selectorExpression + "] " +
+                "at location [" + location.getLineNumber() + "," + location.getColumnNumber() + "] " +
+                "with context node " + node, cause);
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/TemplateInterpolator.java b/src/main/java/au/id/djc/rdftemplate/TemplateInterpolator.java
@@ -0,0 +1,431 @@
+package au.id.djc.rdftemplate;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Collection;
+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.namespace.QName;
+import javax.xml.stream.XMLEventFactory;
+import javax.xml.stream.XMLEventReader;
+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.Namespace;
+import javax.xml.stream.events.StartElement;
+import javax.xml.stream.events.XMLEvent;
+import javax.xml.stream.util.XMLEventConsumer;
+
+import com.hp.hpl.jena.rdf.model.Literal;
+import com.hp.hpl.jena.rdf.model.RDFNode;
+
+import au.id.djc.rdftemplate.selector.InvalidSelectorSyntaxException;
+import au.id.djc.rdftemplate.selector.Selector;
+import au.id.djc.rdftemplate.selector.SelectorFactory;
+
+public class TemplateInterpolator {
+    
+    public static final String NS = "http://code.miskinhill.com.au/rdftemplate/";
+    
+    private final XMLInputFactory inputFactory = XMLInputFactory.newInstance();
+    private final XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
+    private final XMLEventFactory eventFactory = XMLEventFactory.newInstance();
+    
+    private final SelectorFactory selectorFactory;
+    
+    public TemplateInterpolator(SelectorFactory selectorFactory) {
+        this.selectorFactory = selectorFactory;
+        inputFactory.setProperty(XMLInputFactory.IS_COALESCING, true);
+        outputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
+    }
+    
+    public String interpolate(Reader reader, RDFNode node) {
+        try {
+            StringWriter writer = new StringWriter();
+            final XMLEventWriter eventWriter = outputFactory.createXMLEventWriter(writer);
+            XMLEventConsumer destination = new XMLEventConsumer() {
+                @Override
+                public void add(XMLEvent event) throws XMLStreamException {
+                    eventWriter.add(event);
+                }
+            };
+            interpolate(reader, node, destination);
+            return writer.toString();
+        } catch (XMLStreamException e) {
+            throw new TemplateSyntaxException(e);            
+        }
+    }
+    
+    @SuppressWarnings("unchecked")
+    public void interpolate(Reader reader, RDFNode node, XMLEventConsumer writer) {
+        try {
+            interpolate(inputFactory.createXMLEventReader(reader), node, writer);
+        } catch (XMLStreamException e) {
+            throw new RuntimeException(e);
+        }
+    }
+    
+    @SuppressWarnings("unchecked")
+    public void interpolate(InputStream inputStream, RDFNode node, Writer writer) {
+        try {
+            final XMLEventWriter eventWriter = outputFactory.createXMLEventWriter(writer);
+            XMLEventConsumer destination = new XMLEventConsumer() {
+                @Override
+                public void add(XMLEvent event) throws XMLStreamException {
+                    eventWriter.add(event);
+                }
+            };
+            interpolate(inputFactory.createXMLEventReader(inputStream), node, destination);
+        } catch (XMLStreamException e) {
+            throw new RuntimeException(e);
+        }
+    }
+    
+    public void interpolate(Reader reader, RDFNode node, final Collection<XMLEvent> destination) {
+        interpolate(reader, node, new XMLEventConsumer() {
+            @Override
+            public void add(XMLEvent event) {
+                destination.add(event);
+            }
+        });
+    }
+    
+    public void interpolate(Iterator<XMLEvent> reader, RDFNode node, XMLEventConsumer writer)
+            throws XMLStreamException {
+        while (reader.hasNext()) {
+            XMLEvent event = reader.next();
+            switch (event.getEventType()) {
+                case XMLStreamConstants.START_ELEMENT: {
+                    StartElement start = (StartElement) event;
+                    if (start.getName().equals(IfAction.ACTION_QNAME)) {
+                        Attribute testAttribute = start.getAttributeByName(new QName("test"));
+                        Attribute notAttribute = start.getAttributeByName(new QName("not"));
+                        String condition;
+                        boolean negate = false;
+                        if (testAttribute != null && notAttribute != null)
+                            throw new TemplateSyntaxException(start.getLocation(), "test and not attribute on rdf:if are mutually exclusive");
+                        else if (testAttribute != null)
+                            condition = testAttribute.getValue();
+                        else if (notAttribute != null) {
+                            condition = notAttribute.getValue();
+                            negate = true;
+                        } else
+                            throw new TemplateSyntaxException(start.getLocation(), "rdf:if must have a test attribute or a not attribute");
+                        Selector<?> conditionSelector;
+                        try {
+                            conditionSelector = selectorFactory.get(condition);
+                        } catch (InvalidSelectorSyntaxException e) {
+                            throw new TemplateSyntaxException(start.getLocation(), e);
+                        }
+                        List<XMLEvent> tree = consumeTree(start, reader);
+                        // discard enclosing rdf:if
+                        tree.remove(tree.size() - 1);
+                        tree.remove(0);
+                        IfAction action = new IfAction(tree, conditionSelector, negate);
+                        try {
+                            action.evaluate(this, node, writer);
+                        } catch (Exception e) {
+                            throw new TemplateInterpolationException(start.getLocation(), action, node, e);
+                        }
+                    } else if (start.getName().equals(JoinAction.ACTION_QNAME)) {
+                        Attribute eachAttribute = start.getAttributeByName(new QName("each"));
+                        if (eachAttribute == null)
+                            throw new TemplateSyntaxException(start.getLocation(), "rdf:join must have an each attribute");
+                        String separator = "";
+                        Attribute separatorAttribute = start.getAttributeByName(new QName("separator"));
+                        if (separatorAttribute != null)
+                            separator = separatorAttribute.getValue();
+                        Selector<RDFNode> selector;
+                        try {
+                            selector = selectorFactory.get(eachAttribute.getValue()).withResultType(RDFNode.class);
+                        } catch (InvalidSelectorSyntaxException e) {
+                            throw new TemplateSyntaxException(start.getLocation(), e);
+                        }
+                        List<XMLEvent> events = consumeTree(start, reader);
+                        // discard enclosing rdf:join
+                        events.remove(events.size() - 1);
+                        events.remove(0);
+                        JoinAction action = new JoinAction(events, selector, separator);
+                        try {
+                            action.evaluate(this, node, writer, eventFactory);
+                        } catch (Exception e) {
+                            throw new TemplateInterpolationException(start.getLocation(), action, node, e);
+                        }
+                    } else if (start.getName().equals(ForAction.ACTION_QNAME)) {
+                        Attribute eachAttribute = start.getAttributeByName(new QName("each"));
+                        if (eachAttribute == null)
+                            throw new TemplateSyntaxException(start.getLocation(), "rdf:for must have an each attribute");
+                        Selector<RDFNode> selector;
+                        try {
+                            selector = selectorFactory.get(eachAttribute.getValue()).withResultType(RDFNode.class);
+                        } catch (InvalidSelectorSyntaxException e) {
+                            throw new TemplateSyntaxException(start.getLocation(), e);
+                        }
+                        List<XMLEvent> events = consumeTree(start, reader);
+                        // discard enclosing rdf:for
+                        events.remove(events.size() - 1);
+                        events.remove(0);
+                        ForAction action = new ForAction(events, selector);
+                        try {
+                            action.evaluate(this, node, writer);
+                        } catch (Exception e) {
+                            throw new TemplateInterpolationException(start.getLocation(), action, node, e);
+                        }
+                    } else {
+                        Attribute ifAttribute = start.getAttributeByName(IfAction.ACTION_QNAME);
+                        Attribute contentAttribute = start.getAttributeByName(ContentAction.ACTION_QNAME);
+                        Attribute forAttribute = start.getAttributeByName(ForAction.ACTION_QNAME);
+                        if (ifAttribute != null) {
+                            Selector<?> selector;
+                            try {
+                                selector = selectorFactory.get(ifAttribute.getValue());
+                            } catch (InvalidSelectorSyntaxException e) {
+                                throw new TemplateSyntaxException(ifAttribute.getLocation(), e);
+                            }
+                            start = cloneStart(start, cloneAttributesWithout(start, IfAction.ACTION_QNAME), cloneNamespacesWithoutRdf(start));
+                            IfAction action = new IfAction(consumeTree(start, reader), selector, false);
+                            action.evaluate(this, node, writer);
+                        } else if (contentAttribute != null && forAttribute != null) {
+                            throw new TemplateSyntaxException(start.getLocation(), "rdf:for and rdf:content cannot both be present on an element");
+                        } else if (contentAttribute != null) {
+                            consumeTree(start, reader); // discard
+                            Selector<?> selector;
+                            try {
+                                selector = selectorFactory.get(contentAttribute.getValue());
+                            } catch (InvalidSelectorSyntaxException e) {
+                                throw new TemplateSyntaxException(contentAttribute.getLocation(), e);
+                            }
+                            ContentAction action = new ContentAction(start, selector);
+                            try {
+                                action.evaluate(this, node, writer, eventFactory);
+                            } catch (Exception e) {
+                                throw new TemplateInterpolationException(contentAttribute.getLocation(), action, node, e);
+                            }
+                        } else if (forAttribute != null) {
+                            Selector<RDFNode> selector;
+                            try {
+                                selector = selectorFactory.get(forAttribute.getValue()).withResultType(RDFNode.class);
+                            } catch (InvalidSelectorSyntaxException e) {
+                                throw new TemplateSyntaxException(forAttribute.getLocation(), e);
+                            }
+                            start = cloneStart(start, cloneAttributesWithout(start, ForAction.ACTION_QNAME), cloneNamespacesWithoutRdf(start));
+                            List<XMLEvent> tree = consumeTree(start, reader);
+                            ForAction action = new ForAction(tree, selector);
+                            try {
+                                action.evaluate(this, node, writer);
+                            } catch (Exception e) {
+                                throw new TemplateInterpolationException(forAttribute.getLocation(), action, node, e);
+                            }
+                        } else {
+                            start = interpolateAttributes(start, node);
+                            writer.add(start);
+                        }
+                    }
+                    break;
+                }
+                case XMLStreamConstants.CHARACTERS: {
+                    Characters characters = (Characters) event;
+                    interpolateCharacters(writer, characters, node);
+                    break;
+                }
+                case XMLStreamConstants.CDATA: {
+                    Characters characters = (Characters) event;
+                    interpolateCharacters(writer, characters, node);
+                    break;
+                }
+                default:
+                    writer.add(event);
+            }
+        }
+    }
+    
+    private 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")
+    protected 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
+                try {
+                    replacementValue = interpolateString(attribute.getValue(), node);
+                } catch (Exception e) {
+                    throw new TemplateInterpolationException(attribute.getLocation(), attribute.getValue(), node, e);
+                }
+            }
+            replacementAttributes.add(eventFactory.createAttribute(attribute.getName(),
+                    replacementValue));
+        }
+        return cloneStart(start, replacementAttributes, cloneNamespacesWithoutRdf(start));
+    }
+    
+    private StartElement cloneStart(StartElement start, Iterable<Attribute> attributes, Iterable<Namespace> namespaces) {
+        return eventFactory.createStartElement(
+                start.getName().getPrefix(),
+                start.getName().getNamespaceURI(),
+                start.getName().getLocalPart(),
+                attributes.iterator(),
+                namespaces.iterator(),
+                start.getNamespaceContext());
+    }
+    
+    @SuppressWarnings("unchecked")
+    private Set<Namespace> cloneNamespacesWithoutRdf(StartElement start) {
+        Set<Namespace> clonedNamespaces = new LinkedHashSet<Namespace>();
+        for (Iterator<Namespace> it = start.getNamespaces(); it.hasNext(); ) {
+            Namespace namespace = it.next();
+            if (!namespace.getNamespaceURI().equals(NS))
+                clonedNamespaces.add(namespace);
+        }
+        return clonedNamespaces;
+    }
+    
+    private static final Pattern SUBSTITUTION_PATTERN = Pattern.compile("\\$\\{([^}]*)\\}");
+    public 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 = selectorFactory.get(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 {
+                replacementValue = replacement.toString();
+            }
+            
+            matcher.appendReplacement(substituted, replacementValue.replace("$", "\\$"));
+        }
+        matcher.appendTail(substituted);
+        return substituted.toString();
+    }
+    
+    private void interpolateCharacters(XMLEventConsumer writer, Characters characters, RDFNode node) throws XMLStreamException {
+        String template = characters.getData();
+        if (!SUBSTITUTION_PATTERN.matcher(template).find()) {
+            writer.add(characters); // fast path
+            return;
+        }
+        Matcher matcher = SUBSTITUTION_PATTERN.matcher(template);
+        int lastAppendedPos = 0;
+        while (matcher.find()) {
+            writer.add(eventFactory.createCharacters(template.substring(lastAppendedPos, matcher.start())));
+            lastAppendedPos = matcher.end();
+            String expression = matcher.group(1);
+            Selector<?> selector;
+            try {
+                selector = selectorFactory.get(expression);
+            } catch (InvalidSelectorSyntaxException e) {
+                throw new TemplateSyntaxException(characters.getLocation(), e);
+            }
+            try {
+                Object replacement = selector.singleResult(node);
+                writeTreeForContent(writer, replacement);
+            } catch (Exception e) {
+                throw new TemplateInterpolationException(characters.getLocation(), expression, node, e);
+            }
+        }
+        writer.add(eventFactory.createCharacters(template.substring(lastAppendedPos)));
+    }
+    
+    protected void writeTreeForContent(XMLEventConsumer writer, Object replacement)
+            throws XMLStreamException {
+        if (replacement instanceof RDFNode) {
+            RDFNode replacementNode = (RDFNode) replacement;
+            if (replacementNode.isLiteral()) {
+                Literal literal = (Literal) replacementNode;
+                if (literal.isWellFormedXML()) {
+                    writeXMLLiteral(literal.getLexicalForm(), writer);
+                } else {
+                    writer.add(eventFactory.createCharacters(literal.getValue().toString()));
+                }
+            } else {
+                throw new UnsupportedOperationException("Not a literal: " + replacementNode);
+            }
+        } else if (replacement instanceof XMLStream) {
+            for (XMLEvent event: (XMLStream) replacement) {
+                writer.add(event);
+            }
+        } else {
+            writer.add(eventFactory.createCharacters(replacement.toString()));
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    protected 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;
+    }
+    
+    private void writeXMLLiteral(String literal, XMLEventConsumer writer)
+            throws XMLStreamException {
+        XMLEventReader reader = inputFactory.createXMLEventReader(new StringReader(literal));
+        while (reader.hasNext()) {
+            XMLEvent event = reader.nextEvent();
+            switch (event.getEventType()) {
+                case XMLStreamConstants.START_DOCUMENT:
+                case XMLStreamConstants.END_DOCUMENT:
+                    break; // discard
+                default:
+                    writer.add(event);
+            }
+        }
+    }
+    
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/TemplateSyntaxException.java b/src/main/java/au/id/djc/rdftemplate/TemplateSyntaxException.java
@@ -0,0 +1,22 @@
+package au.id.djc.rdftemplate;
+
+import javax.xml.stream.Location;
+import javax.xml.stream.XMLStreamException;
+
+public class TemplateSyntaxException extends RuntimeException {
+
+    private static final long serialVersionUID = 6518982504570154030L;
+    
+    public TemplateSyntaxException(Location location, String message) {
+        super("[location " + location.getLineNumber() + "," + location.getColumnNumber() + "] " + message);
+    }
+    
+    public TemplateSyntaxException(Location location, Throwable cause) {
+        super("[location " + location.getLineNumber() + "," + location.getColumnNumber() + "]", cause);
+    }
+    
+    public TemplateSyntaxException(XMLStreamException e) {
+        super(e);
+    }
+    
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/XMLStream.java b/src/main/java/au/id/djc/rdftemplate/XMLStream.java
@@ -0,0 +1,43 @@
+package au.id.djc.rdftemplate;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.events.XMLEvent;
+
+public class XMLStream implements Iterable<XMLEvent> {
+    
+    public static XMLStream collect(Iterator<XMLEvent> it) {
+        List<XMLEvent> events = new ArrayList<XMLEvent>();
+        while (it.hasNext()) {
+            XMLEvent event = it.next();
+            switch (event.getEventType()) {
+                case XMLStreamConstants.START_DOCUMENT:
+                case XMLStreamConstants.END_DOCUMENT:
+                    break; // discard
+                default:
+                    events.add(event);
+            }
+        }
+        return new XMLStream(events);
+    }
+    
+    private final List<XMLEvent> events;
+    
+    public XMLStream(XMLEvent... events) {
+        this.events = Arrays.asList(events);
+    }
+    
+    public XMLStream(List<XMLEvent> events) {
+        this.events = events;
+    }
+    
+    @Override
+    public Iterator<XMLEvent> iterator() {
+        return events.iterator();
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/datatype/DateDataType.java b/src/main/java/au/id/djc/rdftemplate/datatype/DateDataType.java
@@ -0,0 +1,112 @@
+package au.id.djc.rdftemplate.datatype;
+
+import org.springframework.stereotype.Component;
+
+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;
+
+@Component
+public class DateDataType implements RDFDatatype {
+    
+    public static final String URI = "http://www.w3.org/TR/xmlschema-2/#date";
+    
+    @SuppressWarnings("unused")
+    private static DateDataType instance;
+    public static void registerStaticInstance() {
+        instance = new DateDataType();
+    }
+    
+    private final DateTimeFormatter yearParser = DateTimeFormat.forPattern("yyyy");
+    private final DateTimeFormatter yearMonthParser = DateTimeFormat.forPattern("yyyy-MM");
+    private final DateTimeFormatter dateParser = DateTimeFormat.forPattern("yyyy-MM-dd");
+
+    public DateDataType() {
+        TypeMapper.getInstance().registerDatatype(this);
+    }
+
+    @Override
+    public String getURI() {
+        return URI;
+    }
+    
+    @Override
+    public Class<LocalDate> getJavaClass() {
+        return null;
+    }
+
+    @Override
+    public String unparse(Object value) {
+        throw new UnsupportedOperationException();
+    }
+
+    @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 Object parse(String lexicalForm) throws DatatypeFormatException {
+        try {
+            return dateParser.parseDateTime(lexicalForm).toLocalDate();
+        } catch (IllegalArgumentException e) {
+            // pass
+        }
+        try {
+            return new YearMonth(yearMonthParser.parseDateTime(lexicalForm).toLocalDate());
+        } catch (IllegalArgumentException e) {
+            // pass
+        }
+        try {
+            return new Year(yearParser.parseDateTime(lexicalForm).toLocalDate());
+        } catch (IllegalArgumentException e) {
+            // pass
+        }
+        throw new DatatypeFormatException(lexicalForm, this, "No matching parsers found");
+    }
+
+    @Override
+    public boolean isValid(String lexicalForm) {
+        try {
+            parse(lexicalForm);
+            return true;
+        } catch (DatatypeFormatException e) {
+            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/id/djc/rdftemplate/datatype/DateTimeDataType.java b/src/main/java/au/id/djc/rdftemplate/datatype/DateTimeDataType.java
@@ -0,0 +1,98 @@
+package au.id.djc.rdftemplate.datatype;
+
+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.DateTime;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+import org.springframework.stereotype.Component;
+
+@Component
+public class DateTimeDataType implements RDFDatatype {
+    
+    public static final String URI = "http://www.w3.org/TR/xmlschema-2/#datetime";
+    
+    @SuppressWarnings("unused")
+    private static DateTimeDataType instance;
+    public static void registerStaticInstance() {
+        instance = new DateTimeDataType();
+    }
+    
+    private final DateTimeFormatter format = ISODateTimeFormat.dateTimeNoMillis().withOffsetParsed();
+
+    public DateTimeDataType() {
+        TypeMapper.getInstance().registerDatatype(this);
+    }
+
+    @Override
+    public String getURI() {
+        return URI;
+    }
+    
+    @Override
+    public Class<DateTime> getJavaClass() {
+        return DateTime.class;
+    }
+
+    @Override
+    public String unparse(Object value) {
+        return ((DateTime) value).toString(format);
+    }
+
+    @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 DateTime parse(String lexicalForm) throws DatatypeFormatException {
+        try {
+            return format.parseDateTime(lexicalForm);
+        } catch (IllegalArgumentException e) {
+            throw new DatatypeFormatException(lexicalForm, this, "Parser barfed");
+        }
+    }
+
+    @Override
+    public boolean isValid(String lexicalForm) {
+        try {
+            parse(lexicalForm);
+            return true;
+        } catch (DatatypeFormatException e) {
+            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 DateTime);
+    }
+
+    @Override
+    public RDFDatatype normalizeSubType(Object value, RDFDatatype dt) {
+        return dt;
+    }
+
+}
+\ No newline at end of file
diff --git a/src/main/java/au/id/djc/rdftemplate/datatype/Year.java b/src/main/java/au/id/djc/rdftemplate/datatype/Year.java
@@ -0,0 +1,50 @@
+package au.id.djc.rdftemplate.datatype;
+
+import org.joda.time.LocalDate;
+
+public class Year {
+    
+    private final int year;
+    
+    public Year(int value) {
+        this.year = value;
+    }
+    
+    public Year(LocalDate date) {
+        this.year = date.getYear();
+    }
+    
+    public Year(YearMonth yearMonth) {
+        this.year = yearMonth.getYear();
+    }
+    
+    public int getYear() {
+        return year;
+    }
+    
+    @Override
+    public String toString() {
+        return Integer.toString(year);
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + year;
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        Year other = (Year) obj;
+        return (year == other.year);
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/datatype/YearMonth.java b/src/main/java/au/id/djc/rdftemplate/datatype/YearMonth.java
@@ -0,0 +1,58 @@
+package au.id.djc.rdftemplate.datatype;
+
+import org.joda.time.LocalDate;
+
+public class YearMonth {
+    
+    private final int year;
+    private final int month;
+    
+    public YearMonth(int year, int month) {
+        this.year = year;
+        this.month = month;
+    }
+    
+    public YearMonth(LocalDate date) {
+        this.year = date.getYear();
+        this.month = date.getMonthOfYear();
+    }
+    
+    public int getYear() {
+        return year;
+    }
+    
+    public int getMonth() {
+        return month;
+    }
+    
+    @Override
+    public String toString() {
+        return String.format("%04d-%02d", year, month);
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + month;
+        result = prime * result + year;
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        YearMonth other = (YearMonth) obj;
+        if (month != other.month)
+            return false;
+        if (year != other.year)
+            return false;
+        return true;
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/AbstractAdaptation.java b/src/main/java/au/id/djc/rdftemplate/selector/AbstractAdaptation.java
@@ -0,0 +1,58 @@
+package au.id.djc.rdftemplate.selector;
+
+import java.util.Arrays;
+
+import com.hp.hpl.jena.rdf.model.RDFNode;
+
+public abstract class AbstractAdaptation<DestType, NodeType extends RDFNode> implements Adaptation<DestType> {
+    
+    private final Class<DestType> destinationType;
+    private final Class<?>[] argTypes;
+    private final Class<NodeType> nodeType;
+    
+    protected AbstractAdaptation(Class<DestType> destinationType, Class<?>[] argTypes, Class<NodeType> nodeType) {
+        this.destinationType = destinationType;
+        this.argTypes = argTypes;
+        this.nodeType = nodeType;
+    }
+    
+    @Override
+    public Class<DestType> getDestinationType() {
+        return destinationType;
+    }
+    
+    @Override
+    public Class<?>[] getArgTypes() {
+        return argTypes;
+    }
+    
+    @Override
+    public void setArgs(Object[] args) {
+        if (args.length != argTypes.length)
+            throw new SelectorEvaluationException("Expected args of types " + Arrays.toString(argTypes) +
+                    " but invoked with " + Arrays.toString(args));
+        for (int i = 0; i < args.length; i ++) {
+            if (!argTypes[i].isAssignableFrom(args[i].getClass()))
+                throw new SelectorEvaluationException("Arg " + i + ": expected type " + argTypes[i] +
+                        " but was " + args[i].getClass());
+        }
+        setCheckedArgs(args);
+    }
+    
+    protected void setCheckedArgs(Object[] args) {
+        throw new UnsupportedOperationException();
+    }
+    
+    @Override
+    public DestType adapt(RDFNode node) {
+        if (!nodeType.equals(RDFNode.class)) {
+            if (!node.canAs(nodeType))
+                throw new SelectorEvaluationException("Adaptation can only be applied to " + nodeType +
+                        " but was applied to " + node);
+        }
+        return doAdapt(node.as(nodeType));
+    }
+    
+    protected abstract DestType doAdapt(NodeType node);
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/AbstractSelector.java b/src/main/java/au/id/djc/rdftemplate/selector/AbstractSelector.java
@@ -0,0 +1,40 @@
+package au.id.djc.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/id/djc/rdftemplate/selector/Adaptation.java b/src/main/java/au/id/djc/rdftemplate/selector/Adaptation.java
@@ -0,0 +1,15 @@
+package au.id.djc.rdftemplate.selector;
+
+import com.hp.hpl.jena.rdf.model.RDFNode;
+
+public interface Adaptation<T> {
+
+    Class<T> getDestinationType();
+    
+    Class<?>[] getArgTypes();
+    
+    void setArgs(Object[] args);
+    
+    T adapt(RDFNode node);
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/AdaptationFactory.java b/src/main/java/au/id/djc/rdftemplate/selector/AdaptationFactory.java
@@ -0,0 +1,9 @@
+package au.id.djc.rdftemplate.selector;
+
+public interface AdaptationFactory {
+    
+    boolean hasName(String name);
+    
+    Adaptation<?> getByName(String name);
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/AntlrSelectorFactory.java b/src/main/java/au/id/djc/rdftemplate/selector/AntlrSelectorFactory.java
@@ -0,0 +1,48 @@
+package au.id.djc.rdftemplate.selector;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.antlr.runtime.ANTLRStringStream;
+import org.antlr.runtime.CharStream;
+import org.antlr.runtime.CommonTokenStream;
+import org.antlr.runtime.RecognitionException;
+
+public class AntlrSelectorFactory implements SelectorFactory {
+    
+    private AdaptationFactory adaptationFactory = new DefaultAdaptationFactory();
+    private PredicateResolver predicateResolver = new DefaultPredicateResolver();
+    private Map<String, String> namespacePrefixMap = Collections.emptyMap();
+    
+    public AntlrSelectorFactory() {
+    }
+    
+    public void setAdaptationFactory(AdaptationFactory adaptationFactory) {
+        this.adaptationFactory = adaptationFactory;
+    }
+    
+    public void setPredicateResolver(PredicateResolver predicateResolver) {
+        this.predicateResolver = predicateResolver;
+    }
+    
+    public void setNamespacePrefixMap(Map<String, String> namespacePrefixMap) {
+        this.namespacePrefixMap = namespacePrefixMap;
+    }
+    
+    @Override
+    public Selector<?> get(String expression) {
+        CharStream stream = new ANTLRStringStream(expression);
+        SelectorLexer lexer = new SelectorLexer(stream);
+        CommonTokenStream tokens = new CommonTokenStream(lexer);
+        SelectorParser parser = new SelectorParser(tokens);
+        parser.setAdaptationFactory(adaptationFactory);
+        parser.setPredicateResolver(predicateResolver);
+        parser.setNamespacePrefixMap(namespacePrefixMap);
+        try {
+            return parser.unionSelector();
+        } catch (RecognitionException e) {
+            throw new InvalidSelectorSyntaxException(e);
+        }
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/BooleanAndPredicate.java b/src/main/java/au/id/djc/rdftemplate/selector/BooleanAndPredicate.java
@@ -0,0 +1,34 @@
+package au.id.djc.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/id/djc/rdftemplate/selector/ComparableLiteralValueAdaptation.java b/src/main/java/au/id/djc/rdftemplate/selector/ComparableLiteralValueAdaptation.java
@@ -0,0 +1,22 @@
+package au.id.djc.rdftemplate.selector;
+
+import com.hp.hpl.jena.rdf.model.Literal;
+
+@SuppressWarnings("unchecked")
+public class ComparableLiteralValueAdaptation extends AbstractAdaptation<Comparable, Literal> {
+    
+    public ComparableLiteralValueAdaptation() {
+        super(Comparable.class, new Class<?>[] { }, Literal.class);
+    }
+    
+    @Override
+    protected Comparable<?> doAdapt(Literal node) {
+        Object literalValue = 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/id/djc/rdftemplate/selector/DefaultAdaptationFactory.java b/src/main/java/au/id/djc/rdftemplate/selector/DefaultAdaptationFactory.java
@@ -0,0 +1,35 @@
+package au.id.djc.rdftemplate.selector;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.beans.BeanUtils;
+
+public class DefaultAdaptationFactory implements AdaptationFactory {
+    
+    private static final Map<String, Class<? extends Adaptation<?>>> ADAPTATIONS = new HashMap<String, Class<? extends Adaptation<?>>>();
+    static {
+        ADAPTATIONS.put("uri", UriAdaptation.class);
+        ADAPTATIONS.put("uri-slice", UriSliceAdaptation.class);
+        ADAPTATIONS.put("uri-anchor", UriAnchorAdaptation.class);
+        ADAPTATIONS.put("lv", LiteralValueAdaptation.class);
+        ADAPTATIONS.put("comparable-lv", ComparableLiteralValueAdaptation.class);
+        ADAPTATIONS.put("string-lv", StringLiteralValueAdaptation.class);
+        ADAPTATIONS.put("formatted-dt", FormattedDateTimeAdaptation.class);
+    }
+    
+    @Override
+    public boolean hasName(String name) {
+        return ADAPTATIONS.containsKey(name);
+    }
+    
+    @Override
+    public Adaptation<?> getByName(String name) {
+        Class<? extends Adaptation<?>> adaptationClass = ADAPTATIONS.get(name);
+        if (adaptationClass == null) {
+            throw new InvalidSelectorSyntaxException("No adaptation named " + name);
+        }
+        return BeanUtils.instantiate(adaptationClass);
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/DefaultPredicateResolver.java b/src/main/java/au/id/djc/rdftemplate/selector/DefaultPredicateResolver.java
@@ -0,0 +1,19 @@
+package au.id.djc.rdftemplate.selector;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class DefaultPredicateResolver implements PredicateResolver {
+    
+    private static final Map<String, Class<? extends Predicate>> PREDICATES = new HashMap<String, Class<? extends Predicate>>();
+    static {
+        PREDICATES.put("type", TypePredicate.class);
+        PREDICATES.put("uri-prefix", UriPrefixPredicate.class);
+    }
+
+    @Override
+    public Class<? extends Predicate> getByName(String name) {
+        return PREDICATES.get(name);
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/EternallyCachingSelectorFactory.java b/src/main/java/au/id/djc/rdftemplate/selector/EternallyCachingSelectorFactory.java
@@ -0,0 +1,35 @@
+package au.id.djc.rdftemplate.selector;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * {@link SelectorFactory} implementation which indirects to a real
+ * implementation and caches its return values eternally. Do not use in
+ * situations where the set of input expressions can be unbounded (e.g.
+ * user-provided) as this will lead to unbounded cache growth.
+ * <p>
+ * A better implementation would use a LRU cache or similar, but I cbf.
+ */
+public class EternallyCachingSelectorFactory implements SelectorFactory {
+    
+    private final SelectorFactory real;
+    private final Map<String, Selector<?>> cache = new HashMap<String, Selector<?>>();
+    
+    public EternallyCachingSelectorFactory(SelectorFactory real) {
+        this.real = real;
+    }
+    
+    @Override
+    public Selector<?> get(String expression) {
+        Selector<?> cached = cache.get(expression);
+        if (cached == null) {
+            Selector<?> fresh = real.get(expression);
+            cache.put(expression, fresh);
+            return fresh;
+        } else {
+            return cached;
+        }
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/FormattedDateTimeAdaptation.java b/src/main/java/au/id/djc/rdftemplate/selector/FormattedDateTimeAdaptation.java
@@ -0,0 +1,49 @@
+package au.id.djc.rdftemplate.selector;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.joda.time.ReadableInstant;
+import org.joda.time.ReadablePartial;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+
+import com.hp.hpl.jena.rdf.model.Literal;
+
+public class FormattedDateTimeAdaptation extends AbstractAdaptation<String, Literal> {
+    
+    private String pattern;
+    private DateTimeFormatter formatter;
+    
+    public FormattedDateTimeAdaptation() {
+        super(String.class, new Class<?>[] { String.class }, Literal.class);
+    }
+    
+    @Override
+    protected void setCheckedArgs(Object[] args) {
+        this.pattern = (String) args[0];
+        this.formatter = DateTimeFormat.forPattern(pattern.replace("\"", "'")); // for convenience in XML
+    }
+
+    public String getPattern() {
+        return pattern;
+    }
+    
+    @Override
+    protected String doAdapt(Literal node) {
+        Object lv = node.getValue();
+        if (lv instanceof ReadableInstant) {
+            ReadableInstant instant = (ReadableInstant) lv;
+            return formatter.print(instant);
+        } else if (lv instanceof ReadablePartial) {
+            ReadablePartial instant = (ReadablePartial) lv;
+            return formatter.print(instant);
+        } else {
+            throw new SelectorEvaluationException("Attempted to apply #formatted-dt to non-datetime literal " + lv);
+        }
+    }
+    
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this).append("pattern", pattern).toString();
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/InvalidSelectorSyntaxException.java b/src/main/java/au/id/djc/rdftemplate/selector/InvalidSelectorSyntaxException.java
@@ -0,0 +1,15 @@
+package au.id.djc.rdftemplate.selector;
+
+public class InvalidSelectorSyntaxException extends RuntimeException {
+    
+    private static final long serialVersionUID = 5805546105865617336L;
+
+    public InvalidSelectorSyntaxException(Throwable cause) {
+        super(cause);
+    }
+    
+    public InvalidSelectorSyntaxException(String message) {
+        super(message);
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/LiteralValueAdaptation.java b/src/main/java/au/id/djc/rdftemplate/selector/LiteralValueAdaptation.java
@@ -0,0 +1,16 @@
+package au.id.djc.rdftemplate.selector;
+
+import com.hp.hpl.jena.rdf.model.Literal;
+
+public class LiteralValueAdaptation extends AbstractAdaptation<Object, Literal> {
+    
+    public LiteralValueAdaptation() {
+        super(Object.class, new Class<?>[] { }, Literal.class);
+    }
+    
+    @Override
+    protected Object doAdapt(Literal node) {
+        return node.getValue();
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/NoopSelector.java b/src/main/java/au/id/djc/rdftemplate/selector/NoopSelector.java
@@ -0,0 +1,19 @@
+package au.id.djc.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/id/djc/rdftemplate/selector/Predicate.java b/src/main/java/au/id/djc/rdftemplate/selector/Predicate.java
@@ -0,0 +1,7 @@
+package au.id.djc.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/id/djc/rdftemplate/selector/PredicateResolver.java b/src/main/java/au/id/djc/rdftemplate/selector/PredicateResolver.java
@@ -0,0 +1,7 @@
+package au.id.djc.rdftemplate.selector;
+
+public interface PredicateResolver {
+    
+    Class<? extends Predicate> getByName(String name);
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/Selector.java b/src/main/java/au/id/djc/rdftemplate/selector/Selector.java
@@ -0,0 +1,17 @@
+package au.id.djc.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/id/djc/rdftemplate/selector/SelectorComparator.java b/src/main/java/au/id/djc/rdftemplate/selector/SelectorComparator.java
@@ -0,0 +1,58 @@
+/**
+ * 
+ */
+package au.id.djc.rdftemplate.selector;
+
+import java.util.Comparator;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+import com.hp.hpl.jena.rdf.model.RDFNode;
+
+public class SelectorComparator<T extends Comparable<T>> implements Comparator<RDFNode> {
+    
+    private Selector<T> selector;
+    private boolean reversed = false;
+    
+    public Selector<T> getSelector() {
+        return selector;
+    }
+    
+    public void setSelector(Selector<T> selector) {
+        this.selector = selector;
+    }
+    
+    public boolean isReversed() {
+        return reversed;
+    }
+    
+    public void setReversed(boolean reversed) {
+        this.reversed = reversed;
+    }
+    
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this).append(selector).append("reversed", reversed).toString();
+    }
+    
+    @Override
+    public int compare(RDFNode left, RDFNode right) {
+        T leftKey;
+        try {
+            leftKey = selector.singleResult(left);
+        } catch (SelectorEvaluationException e) {
+            throw new SelectorEvaluationException("Exception evaluating selector [" + selector + "] " +
+            		"with context node [" + left + "] for comparison", e);
+        }
+        T rightKey;
+        try {
+            rightKey = selector.singleResult(right);
+        } catch (SelectorEvaluationException e) {
+            throw new SelectorEvaluationException("Exception evaluating selector [" + selector + "] " +
+            		"with context node [" + right + "] for comparison", e);
+        }
+        int result = leftKey.compareTo(rightKey);
+        return reversed ? -result : result;
+    }
+    
+}
+\ No newline at end of file
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/SelectorEvaluationException.java b/src/main/java/au/id/djc/rdftemplate/selector/SelectorEvaluationException.java
@@ -0,0 +1,15 @@
+package au.id.djc.rdftemplate.selector;
+
+public class SelectorEvaluationException extends RuntimeException {
+
+    private static final long serialVersionUID = -398277800899471326L;
+    
+    public SelectorEvaluationException(String message) {
+        super(message);
+    }
+    
+    public SelectorEvaluationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/SelectorFactory.java b/src/main/java/au/id/djc/rdftemplate/selector/SelectorFactory.java
@@ -0,0 +1,7 @@
+package au.id.djc.rdftemplate.selector;
+
+public interface SelectorFactory {
+    
+    Selector<?> get(String expression);
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/SelectorWithAdaptation.java b/src/main/java/au/id/djc/rdftemplate/selector/SelectorWithAdaptation.java
@@ -0,0 +1,48 @@
+package au.id.djc.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/id/djc/rdftemplate/selector/StringLiteralValueAdaptation.java b/src/main/java/au/id/djc/rdftemplate/selector/StringLiteralValueAdaptation.java
@@ -0,0 +1,45 @@
+package au.id.djc.rdftemplate.selector;
+
+import java.io.StringReader;
+
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.XMLEvent;
+
+import com.hp.hpl.jena.rdf.model.Literal;
+
+public class StringLiteralValueAdaptation extends AbstractAdaptation<String, Literal> {
+    
+    private static final XMLInputFactory inputFactory = XMLInputFactory.newInstance();
+    
+    public StringLiteralValueAdaptation() {
+        super(String.class, new Class<?>[] { }, Literal.class);
+    }
+    
+    @Override
+    protected String doAdapt(Literal literal) {
+        if (literal.isWellFormedXML()) {
+            try {
+                return stripTags(literal.getLexicalForm());
+            } catch (XMLStreamException e) {
+                throw new RuntimeException(e);
+            }
+        } else {
+            return literal.getValue().toString();
+        }
+    }
+    
+    private String stripTags(String literal) throws XMLStreamException {
+        StringBuilder sb = new StringBuilder();
+        XMLEventReader reader = inputFactory.createXMLEventReader(new StringReader(literal));
+        while (reader.hasNext()) {
+            XMLEvent event = reader.nextEvent();
+            if (event.isCharacters()) {
+                sb.append(event.asCharacters().getData());
+            }
+        }
+        return sb.toString();
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/Traversal.java b/src/main/java/au/id/djc/rdftemplate/selector/Traversal.java
@@ -0,0 +1,126 @@
+package au.id.djc.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;
+
+public class Traversal {
+    
+    private String propertyNamespace;
+    private String propertyLocalName;
+    private boolean inverse = false;
+    private Predicate predicate;
+    private List<Comparator<RDFNode>> sortOrder = new ArrayList<Comparator<RDFNode>>();
+    private Integer subscript;
+    
+    private class SortComparator implements Comparator<RDFNode> {
+        @Override
+        public int compare(RDFNode left, RDFNode right) {
+            for (Comparator<RDFNode> comparator: sortOrder) {
+                int result = comparator.compare(left, right);
+                if (result != 0)
+                    return result;
+            }
+            return 0;
+        }
+    }
+    
+    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(propertyNamespace, 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());
+            }
+        }
+        CollectionUtils.filter(destinations, predicate);
+        if (!sortOrder.isEmpty())
+            Collections.sort(destinations, new SortComparator());
+        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("propertyNamespace", propertyNamespace)
+                .append("propertyLocalName", propertyLocalName)
+                .append("inverse", inverse)
+                .append("predicate", predicate)
+                .append("sortOrder", sortOrder)
+                .append("subscript", subscript)
+                .toString();
+    }
+    
+    public String getPropertyLocalName() {
+        return propertyLocalName;
+    }
+    
+    public void setPropertyLocalName(String propertyLocalName) {
+        this.propertyLocalName = propertyLocalName;
+    }
+    
+    public String getPropertyNamespace() {
+        return propertyNamespace;
+    }
+    
+    public void setPropertyNamespace(String propertyNamespace) {
+        this.propertyNamespace = propertyNamespace;
+    }
+    
+    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 List<Comparator<RDFNode>> getSortOrder() {
+        return sortOrder;
+    }
+    
+    public void addSortOrderComparator(Comparator<RDFNode> selector) {
+        this.sortOrder.add(selector);
+    }
+    
+    public Integer getSubscript() {
+        return subscript;
+    }
+    
+    public void setSubscript(Integer subscript) {
+        this.subscript = subscript;
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/TraversingSelector.java b/src/main/java/au/id/djc/rdftemplate/selector/TraversingSelector.java
@@ -0,0 +1,46 @@
+package au.id.djc.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/id/djc/rdftemplate/selector/TypePredicate.java b/src/main/java/au/id/djc/rdftemplate/selector/TypePredicate.java
@@ -0,0 +1,46 @@
+package au.id.djc.rdftemplate.selector;
+
+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;
+
+public class TypePredicate implements Predicate {
+    
+    private final String namespace;
+    private final String localName;
+    
+    public TypePredicate(String namespace, String localName) {
+        this.namespace = namespace;
+        this.localName = localName;
+    }
+    
+    public String getNamespace() {
+        return namespace;
+    }
+    
+    public String getLocalName() {
+        return localName;
+    }
+    
+    @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(namespace + localName);
+        for (Statement statement: resource.listProperties(RDF.type).toSet()) {
+            if (statement.getObject().equals(type))
+                return true;
+        }
+        return false;
+    }
+    
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this).append(namespace).append(localName).toString();
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/UnionSelector.java b/src/main/java/au/id/djc/rdftemplate/selector/UnionSelector.java
@@ -0,0 +1,45 @@
+package au.id.djc.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/main/java/au/id/djc/rdftemplate/selector/UriAdaptation.java b/src/main/java/au/id/djc/rdftemplate/selector/UriAdaptation.java
@@ -0,0 +1,16 @@
+package au.id.djc.rdftemplate.selector;
+
+import com.hp.hpl.jena.rdf.model.Resource;
+
+public class UriAdaptation extends AbstractAdaptation<String, Resource> {
+    
+    public UriAdaptation() {
+        super(String.class, new Class<?>[] { }, Resource.class);
+    }
+    
+    @Override
+    protected String doAdapt(Resource node) {
+        return node.getURI();
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/UriAnchorAdaptation.java b/src/main/java/au/id/djc/rdftemplate/selector/UriAnchorAdaptation.java
@@ -0,0 +1,24 @@
+package au.id.djc.rdftemplate.selector;
+
+import com.hp.hpl.jena.rdf.model.Resource;
+
+/**
+ * Returns the anchor component of the node's URI (excluding initial #), or the
+ * empty string if it has no anchor component.
+ */
+public class UriAnchorAdaptation extends AbstractAdaptation<String, Resource> {
+    
+    public UriAnchorAdaptation() {
+        super(String.class, new Class<?>[] { }, Resource.class);
+    }
+    
+    @Override
+    protected String doAdapt(Resource node) {
+        String uri = node.getURI();
+        int hashIndex = uri.lastIndexOf('#');
+        if (hashIndex < 0)
+            return "";
+        return uri.substring(hashIndex + 1);
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/selector/UriPrefixPredicate.java b/src/main/java/au/id/djc/rdftemplate/selector/UriPrefixPredicate.java
@@ -0,0 +1,26 @@
+package au.id.djc.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/id/djc/rdftemplate/selector/UriSliceAdaptation.java b/src/main/java/au/id/djc/rdftemplate/selector/UriSliceAdaptation.java
@@ -0,0 +1,28 @@
+package au.id.djc.rdftemplate.selector;
+
+import com.hp.hpl.jena.rdf.model.Resource;
+
+public class UriSliceAdaptation extends AbstractAdaptation<String, Resource> {
+    
+    private Integer startIndex;
+    
+    public UriSliceAdaptation() {
+        super(String.class, new Class<?>[] { Integer.class }, Resource.class);
+    }
+    
+    public Integer getStartIndex() {
+        return startIndex;
+    }
+    
+    @Override
+    protected void setCheckedArgs(Object[] args) {
+        this.startIndex = (Integer) args[0];
+    }
+    
+    @Override
+    protected String doAdapt(Resource node) {
+        String uri = node.getURI();
+        return uri.substring(startIndex);
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/view/RDFTemplateView.java b/src/main/java/au/id/djc/rdftemplate/view/RDFTemplateView.java
@@ -0,0 +1,76 @@
+package au.id.djc.rdftemplate.view;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.web.servlet.view.AbstractTemplateView;
+
+import au.id.djc.rdftemplate.TemplateInterpolator;
+import au.id.djc.rdftemplate.selector.SelectorFactory;
+import au.id.djc.jena.util.ModelOperations;
+import au.id.djc.jena.util.ModelOperations.ModelExecutionCallbackWithoutResult;
+
+import com.hp.hpl.jena.rdf.model.Model;
+import com.hp.hpl.jena.rdf.model.Resource;
+
+public class RDFTemplateView extends AbstractTemplateView {
+    
+    public static final String NODE_URI_KEY = "nodeUri";
+    
+    private TemplateInterpolator templateInterpolator;
+    private SelectorFactory selectorFactory;
+    private ModelOperations modelOperations;
+    
+    public void setSelectorFactory(SelectorFactory selectorFactory) {
+        this.selectorFactory = selectorFactory;
+    }
+    
+    public void setModelOperations(ModelOperations modelOperations) {
+        this.modelOperations = modelOperations;
+    }
+    
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        super.afterPropertiesSet();
+        if (selectorFactory == null) {
+            throw new IllegalArgumentException("Property 'selectorFactory' is required");
+        }
+        if (modelOperations == null) {
+            throw new IllegalArgumentException("Property 'sdbTemplate' is required");
+        }
+        this.templateInterpolator = new TemplateInterpolator(selectorFactory);
+    }
+
+    @Override
+    protected void renderMergedTemplateModel(final Map<String, Object> model,
+            final HttpServletRequest request, final HttpServletResponse response)
+            throws Exception {
+        final InputStream inputStream = getApplicationContext().getResource(getUrl()).getInputStream();
+        try {
+            modelOperations.withModel(new ModelExecutionCallbackWithoutResult() {
+                @Override
+                protected void executeWithoutResult(Model rdfModel) {
+                    Resource node = rdfModel.getResource((String) model.get(NODE_URI_KEY));
+                    try {
+                        templateInterpolator.interpolate(inputStream, node, response.getWriter());
+                    } catch (IOException e) {
+                        throw new RuntimeException(e);
+                    }
+                }
+            });
+        } finally {
+            inputStream.close();
+        }
+    }
+    
+    @Override
+    public boolean checkResource(Locale locale) throws Exception {
+        return getApplicationContext().getResource(getUrl()).exists();
+    }
+
+}
diff --git a/src/main/java/au/id/djc/rdftemplate/view/RDFTemplateViewResolver.java b/src/main/java/au/id/djc/rdftemplate/view/RDFTemplateViewResolver.java
@@ -0,0 +1,54 @@
+package au.id.djc.rdftemplate.view;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.web.servlet.view.AbstractTemplateView;
+import org.springframework.web.servlet.view.AbstractTemplateViewResolver;
+
+import au.id.djc.rdftemplate.selector.SelectorFactory;
+import au.id.djc.jena.util.ModelOperations;
+
+public class RDFTemplateViewResolver extends AbstractTemplateViewResolver implements InitializingBean {
+    
+    private SelectorFactory selectorFactory;
+    private ModelOperations modelOperations;
+    
+    public RDFTemplateViewResolver() {
+        super();
+        setViewClass(requiredViewClass());
+        setExposeRequestAttributes(false);
+        setExposeSessionAttributes(false);
+        setExposeSpringMacroHelpers(false);
+    }
+    
+    public void setSelectorFactory(SelectorFactory selectorFactory) {
+        this.selectorFactory = selectorFactory;
+    }
+
+    public void setModelOperations(ModelOperations modelOperations) {
+        this.modelOperations = modelOperations;
+    }
+    
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        if (selectorFactory == null) {
+            throw new IllegalArgumentException("Property 'selectorFactory' is required");
+        }
+        if (modelOperations == null) {
+            throw new IllegalArgumentException("Property 'modelOperations' is required");
+        }
+    }
+    
+    @Override
+    protected Class<? extends AbstractTemplateView> requiredViewClass() {
+        return RDFTemplateView.class;
+    }
+    
+    @Override
+    protected RDFTemplateView buildView(String viewName) throws Exception {
+        RDFTemplateView view = (RDFTemplateView) super.buildView(viewName);
+        view.setSelectorFactory(selectorFactory);
+        view.setModelOperations(modelOperations);
+        return view;
+    }
+
+}
diff --git a/src/test/java/au/com/miskinhill/rdftemplate/TemplateInterpolatorUnitTest.java b/src/test/java/au/com/miskinhill/rdftemplate/TemplateInterpolatorUnitTest.java
@@ -1,140 +0,0 @@
-package au.com.miskinhill.rdftemplate;
-
-import static org.hamcrest.CoreMatchers.*;
-import static org.junit.Assert.*;
-import static org.junit.matchers.JUnitMatchers.*;
-
-import java.io.InputStream;
-import java.io.InputStreamReader;
-
-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;
-import au.com.miskinhill.rdftemplate.selector.AntlrSelectorFactory;
-
-public class TemplateInterpolatorUnitTest {
-    
-    @BeforeClass
-    public static void ensureDatatypesRegistered() {
-        DateDataType.registerStaticInstance();
-    }
-    
-    private Model model;
-    private TemplateInterpolator templateInterpolator;
-    
-    @Before
-    public void setUp() {
-        model = ModelFactory.createDefaultModel();
-        InputStream stream = this.getClass().getResourceAsStream(
-                "/au/com/miskinhill/rdftemplate/test-data.xml");
-        model.read(stream, "");
-        AntlrSelectorFactory selectorFactory = new AntlrSelectorFactory();
-        selectorFactory.setNamespacePrefixMap(TestNamespacePrefixMap.getInstance());
-        templateInterpolator = new TemplateInterpolator(selectorFactory);
-    }
-    
-    @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\">Test Journal of Good Stuff</div>"));
-        assertThat(result, not(containsString("<p>This should all go <em>away</em>!</p>")));
-    }
-    
-    @Test
-    public void shouldHandleXMLLiterals() throws Exception {
-        Resource journal = model.getResource("http://miskinhill.com.au/journals/test/");
-        String result = templateInterpolator.interpolate(
-                new InputStreamReader(this.getClass().getResourceAsStream("replace-xml.xml")), journal);
-        assertThat(result, containsString(
-                "<div lang=\"en\"><p><em>Test Journal</em> is a journal.</p></div>"));
-    }
-    
-    @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")));
-        assertThat(result, not(containsString("negated test")));
-        
-        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")));
-        assertThat(result, containsString("negated test"));
-    }
-    
-    @Test
-    public void shouldHandleJoins() throws Exception {
-        Resource citedArticle = model.getResource("http://miskinhill.com.au/cited/journals/asdf/1:1/article");
-        String result = templateInterpolator.interpolate(
-                new InputStreamReader(this.getClass().getResourceAsStream("join.xml")), citedArticle);
-        assertThat(result, containsString("<p><a href=\"http://miskinhill.com.au/authors/another-author\">Another Author</a>, " +
-                "<a href=\"http://miskinhill.com.au/authors/test-author\">Test Author</a></p>"));
-    }
-    
-    @Test
-    public void shouldHandleFor() throws Exception {
-        Resource journal = model.getResource("http://miskinhill.com.au/journals/test/");
-        String result = templateInterpolator.interpolate(
-                new InputStreamReader(this.getClass().getResourceAsStream("for.xml")), journal);
-        assertThat(result, containsString("<span>http://miskinhill.com.au/journals/test/1:1/</span>"));
-        assertThat(result, containsString("<span>http://miskinhill.com.au/journals/test/2:1/</span>"));
-        assertThat(result, containsString("<p>http://miskinhill.com.au/journals/test/1:1/</p>"));
-        assertThat(result, containsString("<p>http://miskinhill.com.au/journals/test/2:1/</p>"));
-    }
-    
-    @Test
-    public void shouldStripRdfNamespaceDeclarations() throws Exception {
-        Resource author = model.getResource("http://miskinhill.com.au/authors/test-author");
-        String result = templateInterpolator.interpolate(
-                new InputStreamReader(this.getClass().getResourceAsStream("namespaces.xml")), author);
-        assertThat(result, not(containsString("xmlns:rdf=\"http://code.miskinhill.com.au/rdftemplate/\"")));
-        assertThat(result, not(containsString("rdf:")));
-    }
-    
-    @Test
-    public void forShouldIterateRdfSeqsInOrder() throws Exception {
-        Resource article = model.getResource("http://miskinhill.com.au/journals/test/1:1/multi-author-article");
-        String result = templateInterpolator.interpolate(
-                new InputStreamReader(this.getClass().getResourceAsStream("for-seq.xml")), article);
-        assertThat(result, containsString("Another Author\n\nTest Author"));
-    }
-    
-    @Test
-    public void joinShouldIterateRdfSeqsInOrder() throws Exception {
-        Resource article = model.getResource("http://miskinhill.com.au/journals/test/1:1/multi-author-article");
-        String result = templateInterpolator.interpolate(
-                new InputStreamReader(this.getClass().getResourceAsStream("join-seq.xml")), article);
-        assertThat(result, containsString("<p><a href=\"http://miskinhill.com.au/authors/another-author\">Another Author</a>, " +
-        		"<a href=\"http://miskinhill.com.au/authors/test-author\">Test Author</a></p>"));
-    }
-    
-    @Test
-    public void forShouldWorkForSingleResult() throws Exception {
-        Resource journal = model.getResource("http://miskinhill.com.au/cited/journals/asdf/");
-        String result = templateInterpolator.interpolate(
-                new InputStreamReader(this.getClass().getResourceAsStream("for.xml")), journal);
-        assertThat(result, containsString("<span>http://miskinhill.com.au/cited/journals/asdf/1:1/</span>"));
-        assertThat(result, containsString("<p>http://miskinhill.com.au/cited/journals/asdf/1:1/</p>"));
-    }
-    
-    @Test
-    public void joinShouldWorkForSingleResult() throws Exception {
-        Resource review = model.getResource("http://miskinhill.com.au/journals/test/1:1/reviews/review");
-        String result = templateInterpolator.interpolate(
-                new InputStreamReader(this.getClass().getResourceAsStream("join.xml")), review);
-        assertThat(result, containsString("<p><a href=\"http://miskinhill.com.au/authors/test-author\">Test Author</a></p>"));
-    }
-    
-}
diff --git a/src/test/java/au/com/miskinhill/rdftemplate/TestNamespacePrefixMap.java b/src/test/java/au/com/miskinhill/rdftemplate/TestNamespacePrefixMap.java
@@ -1,32 +0,0 @@
-package au.com.miskinhill.rdftemplate;
-
-import java.util.HashMap;
-
-import com.hp.hpl.jena.vocabulary.DCTerms;
-import com.hp.hpl.jena.vocabulary.RDF;
-import org.junit.Ignore;
-
-@Ignore // why does JUnit think this is a test?
-public final class TestNamespacePrefixMap extends HashMap<String, String> {
-    
-    public static final String MHS_NS = "http://miskinhill.com.au/rdfschema/1.0/";
-    public static final String FOAF_NS = "http://xmlns.com/foaf/0.1/";
-    
-    private static final long serialVersionUID = 2119318190108418683L;
-    
-    private static final TestNamespacePrefixMap instance = new TestNamespacePrefixMap();
-    public static TestNamespacePrefixMap getInstance() {
-        return instance;
-    }
-    
-    private TestNamespacePrefixMap() {
-        put("mhs", MHS_NS);
-        put("dc", DCTerms.NS);
-        put("foaf", FOAF_NS);
-        put("rdf", RDF.getURI());
-        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#");
-    }
-    
-}
diff --git a/src/test/java/au/com/miskinhill/rdftemplate/datatype/DateDataTypeUnitTest.java b/src/test/java/au/com/miskinhill/rdftemplate/datatype/DateDataTypeUnitTest.java
@@ -1,35 +0,0 @@
-package au.com.miskinhill.rdftemplate.datatype;
-
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.junit.Assert.assertThat;
-
-import com.hp.hpl.jena.datatypes.RDFDatatype;
-import org.joda.time.LocalDate;
-import org.junit.Before;
-import org.junit.Test;
-
-public class DateDataTypeUnitTest {
-    
-    private RDFDatatype type;
-    
-    @Before
-    public void setUp() {
-        type = new DateDataType();
-    }
-    
-    @Test
-    public void shouldParseYear() {
-        assertThat((Year) type.parse("2003"), equalTo(new Year(2003)));
-    }
-    
-    @Test
-    public void shouldParseYearMonth() {
-        assertThat((YearMonth) type.parse("2003-05"), equalTo(new YearMonth(2003, 5)));
-    }
-    
-    @Test
-    public void shouldParseDate() {
-        assertThat((LocalDate) type.parse("2003-05-25"), equalTo(new LocalDate(2003, 5, 25)));
-    }
-
-}
diff --git a/src/test/java/au/com/miskinhill/rdftemplate/datatype/DateTimeDataTypeUnitTest.java b/src/test/java/au/com/miskinhill/rdftemplate/datatype/DateTimeDataTypeUnitTest.java
@@ -1,34 +0,0 @@
-package au.com.miskinhill.rdftemplate.datatype;
-
-import static org.hamcrest.CoreMatchers.*;
-import static org.junit.Assert.*;
-
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
-import org.junit.Before;
-import org.junit.Test;
-
-import com.hp.hpl.jena.datatypes.RDFDatatype;
-
-public class DateTimeDataTypeUnitTest {
-    
-    private RDFDatatype type;
-    
-    @Before
-    public void setUp() {
-        type = new DateTimeDataType();
-    }
-    
-    @Test
-    public void shouldParseDate() {
-        assertThat((DateTime) type.parse("2003-05-25T10:11:12+05:00"),
-                equalTo(new DateTime(2003, 5, 25, 10, 11, 12, 0, DateTimeZone.forOffsetHours(5))));
-    }
-    
-    @Test
-    public void shouldUnparseDate() {
-        assertThat(type.unparse(new DateTime(2003, 5, 25, 10, 11, 12, 0, DateTimeZone.forOffsetHours(5))),
-                equalTo("2003-05-25T10:11:12+05:00"));
-    }
-
-}
diff --git a/src/test/java/au/com/miskinhill/rdftemplate/datatype/YearMonthUnitTest.java b/src/test/java/au/com/miskinhill/rdftemplate/datatype/YearMonthUnitTest.java
@@ -1,24 +0,0 @@
-package au.com.miskinhill.rdftemplate.datatype;
-
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.junit.Assert.assertThat;
-
-import org.joda.time.LocalDate;
-import org.junit.Test;
-
-public class YearMonthUnitTest {
-    
-    @Test
-    public void testToString() {
-        assertThat(new YearMonth(new LocalDate(2001, 5, 1)).toString(), equalTo("2001-05"));
-    }
-    
-    @Test
-    public void testEqualsHashCode() {
-        YearMonth yearMonth1 = new YearMonth(new LocalDate(2001, 5, 1));
-        YearMonth yearMonth2 = new YearMonth(new LocalDate(2001, 5, 1));
-        assertThat(yearMonth1, equalTo(yearMonth2));
-        assertThat(yearMonth1.hashCode(), equalTo(yearMonth2.hashCode()));
-    }
-
-}
diff --git a/src/test/java/au/com/miskinhill/rdftemplate/datatype/YearUnitTest.java b/src/test/java/au/com/miskinhill/rdftemplate/datatype/YearUnitTest.java
@@ -1,24 +0,0 @@
-package au.com.miskinhill.rdftemplate.datatype;
-
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.junit.Assert.assertThat;
-
-import org.joda.time.LocalDate;
-import org.junit.Test;
-
-public class YearUnitTest {
-    
-    @Test
-    public void testToString() {
-        assertThat(new Year(new LocalDate(2001, 1, 1)).toString(), equalTo("2001"));
-    }
-    
-    @Test
-    public void testEqualsHashCode() {
-        Year year1 = new Year(new LocalDate(2001, 1, 1));
-        Year year2 = new Year(new LocalDate(2001, 1, 1));
-        assertThat(year1, equalTo(year2));
-        assertThat(year1.hashCode(), equalTo(year2.hashCode()));
-    }
-
-}
diff --git a/src/test/java/au/com/miskinhill/rdftemplate/selector/AdaptationMatcher.java b/src/test/java/au/com/miskinhill/rdftemplate/selector/AdaptationMatcher.java
@@ -1,36 +0,0 @@
-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);
-    }
-    
-    public static AdaptationMatcher<FormattedDateTimeAdaptation> formattedDTAdaptation(String pattern) {
-        AdaptationMatcher<FormattedDateTimeAdaptation> m = new AdaptationMatcher<FormattedDateTimeAdaptation>(FormattedDateTimeAdaptation.class);
-        m.addRequiredProperty("pattern", equalTo(pattern));
-        return m;
-    }
-
-}
diff --git a/src/test/java/au/com/miskinhill/rdftemplate/selector/BeanPropertyMatcher.java b/src/test/java/au/com/miskinhill/rdftemplate/selector/BeanPropertyMatcher.java
@@ -1,56 +0,0 @@
-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/EternallyCachingSelectorFactoryUnitTest.java b/src/test/java/au/com/miskinhill/rdftemplate/selector/EternallyCachingSelectorFactoryUnitTest.java
@@ -1,22 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import static org.hamcrest.CoreMatchers.sameInstance;
-import static org.junit.Assert.assertThat;
-
-import org.junit.Test;
-
-import au.com.miskinhill.rdftemplate.TestNamespacePrefixMap;
-
-public class EternallyCachingSelectorFactoryUnitTest {
-    
-    @Test
-    public void shouldCacheSelectors() {
-        AntlrSelectorFactory wrappedFactory = new AntlrSelectorFactory();
-        wrappedFactory.setNamespacePrefixMap(TestNamespacePrefixMap.getInstance());
-        EternallyCachingSelectorFactory factory = new EternallyCachingSelectorFactory(wrappedFactory);
-        Selector<?> first = factory.get("dc:creator/foaf:name");
-        Selector<?> second = factory.get("dc:creator/foaf:name");
-        assertThat((Selector) first, sameInstance((Selector) second));
-    }
-
-}
diff --git a/src/test/java/au/com/miskinhill/rdftemplate/selector/PredicateMatcher.java b/src/test/java/au/com/miskinhill/rdftemplate/selector/PredicateMatcher.java
@@ -1,34 +0,0 @@
-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) {
-        super(type);
-    }
-    
-    public static PredicateMatcher<UriPrefixPredicate> uriPrefixPredicate(String prefix) {
-        PredicateMatcher<UriPrefixPredicate> m = new PredicateMatcher<UriPrefixPredicate>(UriPrefixPredicate.class);
-        m.addRequiredProperty("prefix", equalTo(prefix));
-        return m;
-    }
-    
-    public static PredicateMatcher<TypePredicate> typePredicate(String namespace, String localName) {
-        PredicateMatcher<TypePredicate> m = new PredicateMatcher<TypePredicate>(TypePredicate.class);
-        m.addRequiredProperty("namespace", equalTo(namespace));
-        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/SelectorComparatorMatcher.java b/src/test/java/au/com/miskinhill/rdftemplate/selector/SelectorComparatorMatcher.java
@@ -1,25 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import static org.hamcrest.CoreMatchers.equalTo;
-
-import org.hamcrest.Matcher;
-
-public class SelectorComparatorMatcher<T extends Comparable<T>> extends BeanPropertyMatcher<SelectorComparator<T>> {
-
-    @SuppressWarnings("unchecked")
-    public SelectorComparatorMatcher() {
-        super((Class) SelectorComparator.class);
-    }
-    
-    public static SelectorComparatorMatcher<?> selectorComparator(Matcher<? extends Selector<?>> selector) {
-        SelectorComparatorMatcher<?> m = new SelectorComparatorMatcher();
-        m.addRequiredProperty("selector", selector);
-        return m;
-    }
-    
-    public SelectorComparatorMatcher<T> reversed() {
-        addRequiredProperty("reversed", equalTo(true));
-        return this;
-    }
-
-}
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,224 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import static org.hamcrest.CoreMatchers.*;
-import static org.junit.Assert.*;
-import static org.junit.matchers.JUnitMatchers.*;
-
-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.TestNamespacePrefixMap;
-import au.com.miskinhill.rdftemplate.datatype.DateDataType;
-import au.com.miskinhill.rdftemplate.datatype.DateTimeDataType;
-
-public class SelectorEvaluationUnitTest {
-    
-    private Model m;
-    private Resource journal, issue, article, multiAuthorArticle, citedArticle, author, anotherAuthor, book, review, anotherReview, obituary, en, ru, forum;
-    private AntlrSelectorFactory selectorFactory;
-    
-    @BeforeClass
-    public static void ensureDatatypesRegistered() {
-        DateDataType.registerStaticInstance();
-        DateTimeDataType.registerStaticInstance();
-    }
-    
-    @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");
-        multiAuthorArticle = m.createResource("http://miskinhill.com.au/journals/test/1:1/multi-author-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");
-        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");
-        forum = m.createResource("http://miskinhill.com.au/");
-        selectorFactory = new AntlrSelectorFactory();
-        selectorFactory.setNamespacePrefixMap(TestNamespacePrefixMap.getInstance());
-    }
-    
-    @Test
-    public void shouldEvaluateTraversal() {
-        RDFNode result = selectorFactory.get("dc:creator").withResultType(RDFNode.class).singleResult(article);
-        assertThat(result, equalTo((RDFNode) author));
-    }
-    
-    @Test
-    public void shouldEvaluateMultipleTraversals() throws Exception {
-        RDFNode result = selectorFactory.get("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 = selectorFactory.get("!dc:isPartOf")
-                .withResultType(RDFNode.class).result(issue);
-        assertThat(results.size(), equalTo(4));
-        assertThat(results, hasItems((RDFNode) article, (RDFNode) multiAuthorArticle, (RDFNode) review, (RDFNode) obituary));
-    }
-    
-    @Test
-    public void shouldEvaluateSortOrder() throws Exception {
-        List<RDFNode> results = selectorFactory.get("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 = selectorFactory.get("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 = selectorFactory.get("!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 = selectorFactory.get("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 = selectorFactory.get("#uri").withResultType(String.class).singleResult(journal);
-        assertThat(result, equalTo("http://miskinhill.com.au/journals/test/"));
-    }
-    
-    @Test
-    public void shouldEvaluateUriSliceAdaptation() throws Exception {
-        String result = selectorFactory.get("dc:identifier[uri-prefix='urn:issn:']#uri-slice(9)")
-                .withResultType(String.class).singleResult(journal);
-        assertThat(result, equalTo("12345678"));
-    }
-    
-    @Test
-    public void shouldEvaluateUriAnchorAdaptation() throws Exception {
-        String result = selectorFactory.get("mhs:inSection#uri-anchor")
-                .withResultType(String.class).singleResult(anotherReview);
-        assertThat(result, equalTo("stuff"));
-    }
-    
-    @Test
-    public void shouldEvaluateSubscript() throws Exception {
-        String result = selectorFactory.get(
-                "!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 = selectorFactory.get(
-                "!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 = selectorFactory.get("dc:language/lingvoj:iso1#lv")
-                .withResultType(Object.class).result(journal);
-        assertThat(results.size(), equalTo(2));
-        assertThat(results, hasItems((Object) "en", (Object) "ru"));
-    }
-    
-    @Test
-    public void shouldEvaluateTypePredicate() throws Exception {
-        List<RDFNode> results = selectorFactory.get("!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 = selectorFactory.get("!dc:creator[type=mhs:Article and uri-prefix='http://miskinhill.com.au/journals/']")
-                .withResultType(RDFNode.class).result(author);
-        assertThat(results.size(), equalTo(2));
-        assertThat(results, hasItems((RDFNode) article, (RDFNode) multiAuthorArticle));
-    }
-    
-    @Test
-    public void shouldEvaluateUnion() throws Exception {
-        List<RDFNode> results = selectorFactory.get("!dc:creator | !mhs:translator")
-                .withResultType(RDFNode.class).result(anotherAuthor);
-        assertThat(results.size(), equalTo(4));
-        assertThat(results, hasItems((RDFNode) article, (RDFNode) multiAuthorArticle, (RDFNode) citedArticle, (RDFNode) anotherReview));
-    }
-    
-    @Test
-    public void shouldEvaluateMultipleSortSelectors() throws Exception {
-        List<RDFNode> results = selectorFactory.get("!dc:creator[uri-prefix='http://miskinhill.com.au/journals/']" +
-        		"(~dc:isPartOf/mhs:publicationDate#comparable-lv,mhs:startPage#comparable-lv)")
-                .withResultType(RDFNode.class).result(author);
-        assertThat(results.size(), equalTo(4));
-        assertThat(results.get(0), equalTo((RDFNode) obituary));
-        assertThat(results.get(1), equalTo((RDFNode) article));
-        assertThat(results.get(2), equalTo((RDFNode) multiAuthorArticle));
-        assertThat(results.get(3), equalTo((RDFNode) review));
-    }
-    
-    @Test
-    public void shouldEvaluateFormattedDTAdaptation() throws Exception {
-        String result = selectorFactory.get("!sioc:has_container/dc:created#formatted-dt('d MMMM yyyy')")
-                .withResultType(String.class).singleResult(forum);
-        assertThat(result, equalTo("15 June 2009"));
-    }
-    
-    @Test
-    public void shouldEvaluateFormattedDTAdaptationWithDoubleQuotes() throws Exception {
-        String result = selectorFactory.get("!sioc:has_container/dc:created#formatted-dt('yyyy-MM-dd\"T\"HH:mm:ssZZ')")
-                .withResultType(String.class).singleResult(forum);
-        assertThat(result, equalTo("2009-06-15T18:21:32+10:00"));
-    }
-    
-    @Test
-    public void shouldEvaluateStringLVAdaptation() throws Exception {
-        List<String> results = selectorFactory.get("dc:language/lingvoj:iso1#string-lv")
-                .withResultType(String.class).result(journal);
-        assertThat(results.size(), equalTo(2));
-        assertThat(results, hasItems("en", "ru"));
-    }
-    
-    @Test
-    public void stringLVAdaptationShouldStripTagsFromXMLLiteral() throws Exception {
-        String result = selectorFactory.get("!sioc:has_container/sioc:content/awol:body#string-lv")
-                .withResultType(String.class).singleResult(forum);
-        assertEquals("To coincide with the publication of our second issue, " +
-        		"the 2008 volume of Australian Slavonic and East European Studies, " +
-        		"we are making available two new data feeds: an Atom feed of all " +
-        		"journal issues published on this site, and the complete RDF dataset " +
-        		"underlying the site. We hope this helps our users and aggregators " +
-        		"to discover new content as it is published.",
-        		result.trim().replaceAll("\\s+", " "));
-    }
-    
-}
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,36 +0,0 @@
-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> {
-
-    private SelectorMatcher(Class<? extends T> type) {
-        super(type);
-    }
-    
-    public static SelectorMatcher<Selector<RDFNode>> selector(Matcher<Traversal>... traversals) {
-        if (traversals.length == 0) {
-            return new SelectorMatcher<Selector<RDFNode>>(NoopSelector.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);
-        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
@@ -1,206 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import static au.com.miskinhill.rdftemplate.TestNamespacePrefixMap.*;
-import static au.com.miskinhill.rdftemplate.selector.AdaptationMatcher.*;
-import static au.com.miskinhill.rdftemplate.selector.PredicateMatcher.*;
-import static au.com.miskinhill.rdftemplate.selector.SelectorComparatorMatcher.*;
-import static au.com.miskinhill.rdftemplate.selector.SelectorMatcher.*;
-import static au.com.miskinhill.rdftemplate.selector.TraversalMatcher.*;
-import static org.junit.Assert.*;
-
-import com.hp.hpl.jena.rdf.model.RDFNode;
-import com.hp.hpl.jena.vocabulary.DCTerms;
-import com.hp.hpl.jena.vocabulary.RDF;
-import org.junit.Before;
-import org.junit.Test;
-
-import au.com.miskinhill.rdftemplate.TestNamespacePrefixMap;
-
-public class SelectorParserUnitTest {
-    
-    private AntlrSelectorFactory factory;
-    
-    @Before
-    public void setUp() {
-        factory = new AntlrSelectorFactory();
-        factory.setNamespacePrefixMap(TestNamespacePrefixMap.getInstance());
-    }
-    
-    @Test
-    public void shouldRecogniseSingleTraversal() throws Exception {
-        Selector<RDFNode> selector = factory.get("dc:creator").withResultType(RDFNode.class);
-        assertThat(selector, selector(traversal(DCTerms.NS, "creator")));
-    }
-    
-    @Test
-    public void shouldRecogniseMultipleTraversals() throws Exception {
-        Selector<RDFNode> selector = factory.get("dc:creator/foaf:name").withResultType(RDFNode.class);
-        assertThat(selector, selector(
-                traversal(DCTerms.NS, "creator"),
-                traversal(FOAF_NS, "name")));
-    }
-    
-    @Test
-    public void shouldRecogniseMultipleTraversalsWithWhitespace() throws Exception {
-        Selector<RDFNode> selector = factory.get("dc:creator / foaf:name").withResultType(RDFNode.class);
-        assertThat(selector, selector(
-                traversal(DCTerms.NS, "creator"),
-                traversal(FOAF_NS, "name")));
-    }
-    
-    @Test
-    public void shouldRecogniseInverseTraversal() throws Exception {
-        Selector<RDFNode> selector = factory.get("!dc:isPartOf/!dc:isPartOf").withResultType(RDFNode.class);
-        assertThat(selector, selector(
-                traversal(DCTerms.NS, "isPartOf").inverse(),
-                traversal(DCTerms.NS, "isPartOf").inverse()));
-    }
-    
-    @Test
-    public void shouldRecogniseSortOrder() throws Exception {
-        Selector<RDFNode> selector = factory.get("!mhs:isIssueOf(mhs:publicationDate#comparable-lv)").withResultType(RDFNode.class);
-        assertThat(selector, selector(
-                traversal(MHS_NS, "isIssueOf").inverse()
-                .withSortOrder(selectorComparator(selector(traversal(MHS_NS, "publicationDate"))
-                    .withAdaptation(comparableLVAdaptation())))));
-    }
-    
-    @Test
-    public void shouldRecogniseReverseSortOrder() throws Exception {
-        Selector<RDFNode> selector = factory.get("!mhs:isIssueOf(~mhs:publicationDate#comparable-lv)").withResultType(RDFNode.class);
-        assertThat(selector, selector(
-                traversal(MHS_NS, "isIssueOf").inverse()
-                .withSortOrder(selectorComparator(selector(traversal(MHS_NS, "publicationDate"))
-                    .withAdaptation(comparableLVAdaptation())).reversed())));
-    }
-    
-    @Test
-    public void shouldRecogniseComplexSortOrder() throws Exception {
-        Selector<RDFNode> selector = factory.get("!mhs:reviews(dc:isPartOf/mhs:publicationDate#comparable-lv)").withResultType(RDFNode.class);
-        assertThat(selector, selector(
-                traversal(MHS_NS, "reviews")
-                .withSortOrder(selectorComparator(selector(traversal(DCTerms.NS, "isPartOf"), traversal(MHS_NS, "publicationDate"))
-                        .withAdaptation(comparableLVAdaptation())))));
-    }
-    
-    @Test
-    public void shouldRecogniseUriAdaptation() throws Exception {
-        Selector<?> selector = factory.get("mhs:coverThumbnail#uri");
-        assertThat(selector, selector(
-                traversal(MHS_NS, "coverThumbnail"))
-                .withAdaptation(uriAdaptation()));
-    }
-    
-    @Test
-    public void shouldRecogniseBareUriAdaptation() throws Exception {
-        Selector<?> selector = factory.get("#uri");
-        assertThat(selector, selector().withAdaptation(uriAdaptation()));
-    }
-    
-    @Test
-    public void shouldRecogniseUriSliceAdaptation() throws Exception {
-        Selector<?> selector = factory.get("dc:identifier[uri-prefix='urn:issn:']#uri-slice(9)");
-        assertThat(selector, selector(
-                traversal(DCTerms.NS, "identifier")
-                    .withPredicate(uriPrefixPredicate("urn:issn:")))
-                .withAdaptation(uriSliceAdaptation(9)));
-    }
-    
-    @Test
-    public void shouldRecogniseUriPrefixPredicate() throws Exception {
-        Selector<RDFNode> selector = factory.get(
-                "!mhs:isIssueOf[uri-prefix='http://miskinhill.com.au/journals/'](~mhs:publicationDate#comparable-lv)")
-                .withResultType(RDFNode.class);
-        assertThat(selector, selector(
-                traversal(MHS_NS, "isIssueOf")
-                    .inverse()
-                    .withPredicate(uriPrefixPredicate("http://miskinhill.com.au/journals/"))
-                    .withSortOrder(selectorComparator(selector(traversal(MHS_NS, "publicationDate"))
-                            .withAdaptation(comparableLVAdaptation())).reversed())));
-    }
-    
-    @Test
-    public void shouldRecogniseSubscript() throws Exception {
-        Selector<String> selector = factory.get(
-                "!mhs:isIssueOf(~mhs:publicationDate#comparable-lv)[0]/mhs:coverThumbnail#uri")
-                .withResultType(String.class);
-        assertThat(selector, selector(
-                traversal(MHS_NS, "isIssueOf")
-                    .inverse()
-                    .withSortOrder(selectorComparator(selector(traversal(MHS_NS, "publicationDate"))
-                            .withAdaptation(comparableLVAdaptation())).reversed())
-                    .withSubscript(0),
-                traversal(MHS_NS, "coverThumbnail"))
-                .withAdaptation(uriAdaptation()));
-    }
-    
-    @Test
-    public void shouldRecogniseLVAdaptation() throws Exception {
-        Selector<Object> selector = factory.get("dc:language/lingvoj:iso1#lv").withResultType(Object.class);
-        assertThat(selector, selector(
-                traversal(DCTerms.NS, "language"),
-                traversal("http://www.lingvoj.org/ontology#", "iso1"))
-                .withAdaptation(lvAdaptation()));
-    }
-    
-    @Test
-    public void shouldRecogniseTypePredicate() throws Exception {
-        Selector<RDFNode> selector = factory.get("!dc:creator[type=mhs:Review]").withResultType(RDFNode.class);
-        assertThat(selector, selector(
-                traversal(DCTerms.NS, "creator").inverse().withPredicate(typePredicate(MHS_NS, "Review"))));
-    }
-    
-    @Test
-    public void shouldRecogniseAndCombinationOfPredicates() throws Exception {
-        Selector<RDFNode> selector = factory.get("!dc:creator[type=mhs:Review and uri-prefix='http://miskinhill.com.au/journals/']").withResultType(RDFNode.class);
-        assertThat(selector, selector(
-                traversal(DCTerms.NS, "creator").inverse()
-                .withPredicate(booleanAndPredicate(
-                    typePredicate(MHS_NS, "Review"),
-                    uriPrefixPredicate("http://miskinhill.com.au/journals/")))));
-    }
-    
-    @Test
-    public void shouldRecogniseUnion() throws Exception {
-        Selector<RDFNode> selector = factory.get("!dc:creator | !mhs:translator").withResultType(RDFNode.class);
-        assertThat((UnionSelector<RDFNode>) selector, unionSelector(
-                selector(traversal(DCTerms.NS, "creator").inverse()),
-                selector(traversal(MHS_NS, "translator").inverse())));
-    }
-    
-    @Test
-    public void shouldRecogniseMultipleSortSelectors() throws Exception {
-        Selector<RDFNode> selector = factory.get("!dc:creator(~dc:isPartOf/mhs:publicationDate#comparable-lv,mhs:startPage#comparable-lv)").withResultType(RDFNode.class);
-        assertThat(selector, selector(
-                traversal(DCTerms.NS, "creator").inverse()
-                .withSortOrder(
-                    selectorComparator(selector(traversal(DCTerms.NS, "isPartOf"), traversal(MHS_NS, "publicationDate"))
-                        .withAdaptation(comparableLVAdaptation())).reversed(),
-                    selectorComparator(selector(traversal(MHS_NS, "startPage")).withAdaptation(comparableLVAdaptation())))));
-    }
-    
-    @Test
-    public void shouldRecogniseFormattedDTAdaptation() throws Exception {
-        Selector<?> selector = factory.get("dc:created#formatted-dt('d MMMM yyyy')");
-        assertThat(selector, selector(traversal(DCTerms.NS, "created"))
-                .withAdaptation(formattedDTAdaptation("d MMMM yyyy")));
-    }
-    
-    @Test
-    public void shouldRecogniseRdfType() throws Exception {
-        // was broken due to ANTLR being confused about the literal string "type" which was hardcoded to be a predicate
-        Selector<RDFNode> selector = factory.get("rdf:type").withResultType(RDFNode.class);
-        assertThat(selector, selector(traversal(RDF.getURI(), "type")));
-    }
-    
-    @Test(expected = InvalidSelectorSyntaxException.class)
-    public void shouldThrowForInvalidSyntax() throws Exception {
-        factory.get("dc:creator]["); // this is a parser error
-    }
-    
-    @Test(expected = InvalidSelectorSyntaxException.class)
-    public void shouldThrowForUnrecognisedCharacter() throws Exception {
-        factory.get("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
@@ -1,44 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.junit.matchers.JUnitMatchers.hasItems;
-
-import java.util.Comparator;
-
-import com.hp.hpl.jena.rdf.model.RDFNode;
-import org.hamcrest.Matcher;
-
-public class TraversalMatcher extends BeanPropertyMatcher<Traversal> {
-    
-    private TraversalMatcher() {
-        super(Traversal.class);
-    }
-    
-    public static TraversalMatcher traversal(String propertyNamespace, String propertyLocalName) {
-        TraversalMatcher m = new TraversalMatcher();
-        m.addRequiredProperty("propertyNamespace", equalTo(propertyNamespace));
-        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<? extends Comparator<RDFNode>>... sortOrder) {
-        addRequiredProperty("sortOrder", hasItems(sortOrder));
-        return this;
-    }
-    
-    public TraversalMatcher withSubscript(int subscript) {
-        addRequiredProperty("subscript", equalTo(subscript));
-        return this;
-    }
-
-}
diff --git a/src/test/java/au/com/miskinhill/rdftemplate/selector/UriAnchorAdaptationUnitTest.java b/src/test/java/au/com/miskinhill/rdftemplate/selector/UriAnchorAdaptationUnitTest.java
@@ -1,23 +0,0 @@
-package au.com.miskinhill.rdftemplate.selector;
-
-import static org.hamcrest.CoreMatchers.*;
-import static org.junit.Assert.*;
-
-import com.hp.hpl.jena.rdf.model.ResourceFactory;
-import org.junit.Test;
-
-public class UriAnchorAdaptationUnitTest {
-
-    @Test
-    public void shouldReturnAnchor() {
-        String result = new UriAnchorAdaptation().adapt(ResourceFactory.createResource("http://example.com/#asdf"));
-        assertThat(result, equalTo("asdf"));
-    }
-    
-    @Test
-    public void shouldReturnEmptyStringForUriWithoutAnchor() {
-        String result = new UriAnchorAdaptation().adapt(ResourceFactory.createResource("http://example.com/"));
-        assertThat(result, equalTo(""));
-    }
-
-}
diff --git a/src/test/java/au/id/djc/rdftemplate/TemplateInterpolatorUnitTest.java b/src/test/java/au/id/djc/rdftemplate/TemplateInterpolatorUnitTest.java
@@ -0,0 +1,140 @@
+package au.id.djc.rdftemplate;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+import static org.junit.matchers.JUnitMatchers.*;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+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.id.djc.rdftemplate.datatype.DateDataType;
+import au.id.djc.rdftemplate.selector.AntlrSelectorFactory;
+
+public class TemplateInterpolatorUnitTest {
+    
+    @BeforeClass
+    public static void ensureDatatypesRegistered() {
+        DateDataType.registerStaticInstance();
+    }
+    
+    private Model model;
+    private TemplateInterpolator templateInterpolator;
+    
+    @Before
+    public void setUp() {
+        model = ModelFactory.createDefaultModel();
+        InputStream stream = this.getClass().getResourceAsStream(
+                "/au/id/djc/rdftemplate/test-data.xml");
+        model.read(stream, "");
+        AntlrSelectorFactory selectorFactory = new AntlrSelectorFactory();
+        selectorFactory.setNamespacePrefixMap(TestNamespacePrefixMap.getInstance());
+        templateInterpolator = new TemplateInterpolator(selectorFactory);
+    }
+    
+    @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\">Test Journal of Good Stuff</div>"));
+        assertThat(result, not(containsString("<p>This should all go <em>away</em>!</p>")));
+    }
+    
+    @Test
+    public void shouldHandleXMLLiterals() throws Exception {
+        Resource journal = model.getResource("http://miskinhill.com.au/journals/test/");
+        String result = templateInterpolator.interpolate(
+                new InputStreamReader(this.getClass().getResourceAsStream("replace-xml.xml")), journal);
+        assertThat(result, containsString(
+                "<div lang=\"en\"><p><em>Test Journal</em> is a journal.</p></div>"));
+    }
+    
+    @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")));
+        assertThat(result, not(containsString("negated test")));
+        
+        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")));
+        assertThat(result, containsString("negated test"));
+    }
+    
+    @Test
+    public void shouldHandleJoins() throws Exception {
+        Resource citedArticle = model.getResource("http://miskinhill.com.au/cited/journals/asdf/1:1/article");
+        String result = templateInterpolator.interpolate(
+                new InputStreamReader(this.getClass().getResourceAsStream("join.xml")), citedArticle);
+        assertThat(result, containsString("<p><a href=\"http://miskinhill.com.au/authors/another-author\">Another Author</a>, " +
+                "<a href=\"http://miskinhill.com.au/authors/test-author\">Test Author</a></p>"));
+    }
+    
+    @Test
+    public void shouldHandleFor() throws Exception {
+        Resource journal = model.getResource("http://miskinhill.com.au/journals/test/");
+        String result = templateInterpolator.interpolate(
+                new InputStreamReader(this.getClass().getResourceAsStream("for.xml")), journal);
+        assertThat(result, containsString("<span>http://miskinhill.com.au/journals/test/1:1/</span>"));
+        assertThat(result, containsString("<span>http://miskinhill.com.au/journals/test/2:1/</span>"));
+        assertThat(result, containsString("<p>http://miskinhill.com.au/journals/test/1:1/</p>"));
+        assertThat(result, containsString("<p>http://miskinhill.com.au/journals/test/2:1/</p>"));
+    }
+    
+    @Test
+    public void shouldStripRdfNamespaceDeclarations() throws Exception {
+        Resource author = model.getResource("http://miskinhill.com.au/authors/test-author");
+        String result = templateInterpolator.interpolate(
+                new InputStreamReader(this.getClass().getResourceAsStream("namespaces.xml")), author);
+        assertThat(result, not(containsString("xmlns:rdf=\"http://code.miskinhill.com.au/rdftemplate/\"")));
+        assertThat(result, not(containsString("rdf:")));
+    }
+    
+    @Test
+    public void forShouldIterateRdfSeqsInOrder() throws Exception {
+        Resource article = model.getResource("http://miskinhill.com.au/journals/test/1:1/multi-author-article");
+        String result = templateInterpolator.interpolate(
+                new InputStreamReader(this.getClass().getResourceAsStream("for-seq.xml")), article);
+        assertThat(result, containsString("Another Author\n\nTest Author"));
+    }
+    
+    @Test
+    public void joinShouldIterateRdfSeqsInOrder() throws Exception {
+        Resource article = model.getResource("http://miskinhill.com.au/journals/test/1:1/multi-author-article");
+        String result = templateInterpolator.interpolate(
+                new InputStreamReader(this.getClass().getResourceAsStream("join-seq.xml")), article);
+        assertThat(result, containsString("<p><a href=\"http://miskinhill.com.au/authors/another-author\">Another Author</a>, " +
+        		"<a href=\"http://miskinhill.com.au/authors/test-author\">Test Author</a></p>"));
+    }
+    
+    @Test
+    public void forShouldWorkForSingleResult() throws Exception {
+        Resource journal = model.getResource("http://miskinhill.com.au/cited/journals/asdf/");
+        String result = templateInterpolator.interpolate(
+                new InputStreamReader(this.getClass().getResourceAsStream("for.xml")), journal);
+        assertThat(result, containsString("<span>http://miskinhill.com.au/cited/journals/asdf/1:1/</span>"));
+        assertThat(result, containsString("<p>http://miskinhill.com.au/cited/journals/asdf/1:1/</p>"));
+    }
+    
+    @Test
+    public void joinShouldWorkForSingleResult() throws Exception {
+        Resource review = model.getResource("http://miskinhill.com.au/journals/test/1:1/reviews/review");
+        String result = templateInterpolator.interpolate(
+                new InputStreamReader(this.getClass().getResourceAsStream("join.xml")), review);
+        assertThat(result, containsString("<p><a href=\"http://miskinhill.com.au/authors/test-author\">Test Author</a></p>"));
+    }
+    
+}
diff --git a/src/test/java/au/id/djc/rdftemplate/TestNamespacePrefixMap.java b/src/test/java/au/id/djc/rdftemplate/TestNamespacePrefixMap.java
@@ -0,0 +1,32 @@
+package au.id.djc.rdftemplate;
+
+import java.util.HashMap;
+
+import com.hp.hpl.jena.vocabulary.DCTerms;
+import com.hp.hpl.jena.vocabulary.RDF;
+import org.junit.Ignore;
+
+@Ignore // why does JUnit think this is a test?
+public final class TestNamespacePrefixMap extends HashMap<String, String> {
+    
+    public static final String MHS_NS = "http://miskinhill.com.au/rdfschema/1.0/";
+    public static final String FOAF_NS = "http://xmlns.com/foaf/0.1/";
+    
+    private static final long serialVersionUID = 2119318190108418683L;
+    
+    private static final TestNamespacePrefixMap instance = new TestNamespacePrefixMap();
+    public static TestNamespacePrefixMap getInstance() {
+        return instance;
+    }
+    
+    private TestNamespacePrefixMap() {
+        put("mhs", MHS_NS);
+        put("dc", DCTerms.NS);
+        put("foaf", FOAF_NS);
+        put("rdf", RDF.getURI());
+        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#");
+    }
+    
+}
diff --git a/src/test/java/au/id/djc/rdftemplate/datatype/DateDataTypeUnitTest.java b/src/test/java/au/id/djc/rdftemplate/datatype/DateDataTypeUnitTest.java
@@ -0,0 +1,35 @@
+package au.id.djc.rdftemplate.datatype;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+import com.hp.hpl.jena.datatypes.RDFDatatype;
+import org.joda.time.LocalDate;
+import org.junit.Before;
+import org.junit.Test;
+
+public class DateDataTypeUnitTest {
+    
+    private RDFDatatype type;
+    
+    @Before
+    public void setUp() {
+        type = new DateDataType();
+    }
+    
+    @Test
+    public void shouldParseYear() {
+        assertThat((Year) type.parse("2003"), equalTo(new Year(2003)));
+    }
+    
+    @Test
+    public void shouldParseYearMonth() {
+        assertThat((YearMonth) type.parse("2003-05"), equalTo(new YearMonth(2003, 5)));
+    }
+    
+    @Test
+    public void shouldParseDate() {
+        assertThat((LocalDate) type.parse("2003-05-25"), equalTo(new LocalDate(2003, 5, 25)));
+    }
+
+}
diff --git a/src/test/java/au/id/djc/rdftemplate/datatype/DateTimeDataTypeUnitTest.java b/src/test/java/au/id/djc/rdftemplate/datatype/DateTimeDataTypeUnitTest.java
@@ -0,0 +1,34 @@
+package au.id.djc.rdftemplate.datatype;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.hp.hpl.jena.datatypes.RDFDatatype;
+
+public class DateTimeDataTypeUnitTest {
+    
+    private RDFDatatype type;
+    
+    @Before
+    public void setUp() {
+        type = new DateTimeDataType();
+    }
+    
+    @Test
+    public void shouldParseDate() {
+        assertThat((DateTime) type.parse("2003-05-25T10:11:12+05:00"),
+                equalTo(new DateTime(2003, 5, 25, 10, 11, 12, 0, DateTimeZone.forOffsetHours(5))));
+    }
+    
+    @Test
+    public void shouldUnparseDate() {
+        assertThat(type.unparse(new DateTime(2003, 5, 25, 10, 11, 12, 0, DateTimeZone.forOffsetHours(5))),
+                equalTo("2003-05-25T10:11:12+05:00"));
+    }
+
+}
diff --git a/src/test/java/au/id/djc/rdftemplate/datatype/YearMonthUnitTest.java b/src/test/java/au/id/djc/rdftemplate/datatype/YearMonthUnitTest.java
@@ -0,0 +1,24 @@
+package au.id.djc.rdftemplate.datatype;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+import org.joda.time.LocalDate;
+import org.junit.Test;
+
+public class YearMonthUnitTest {
+    
+    @Test
+    public void testToString() {
+        assertThat(new YearMonth(new LocalDate(2001, 5, 1)).toString(), equalTo("2001-05"));
+    }
+    
+    @Test
+    public void testEqualsHashCode() {
+        YearMonth yearMonth1 = new YearMonth(new LocalDate(2001, 5, 1));
+        YearMonth yearMonth2 = new YearMonth(new LocalDate(2001, 5, 1));
+        assertThat(yearMonth1, equalTo(yearMonth2));
+        assertThat(yearMonth1.hashCode(), equalTo(yearMonth2.hashCode()));
+    }
+
+}
diff --git a/src/test/java/au/id/djc/rdftemplate/datatype/YearUnitTest.java b/src/test/java/au/id/djc/rdftemplate/datatype/YearUnitTest.java
@@ -0,0 +1,24 @@
+package au.id.djc.rdftemplate.datatype;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+import org.joda.time.LocalDate;
+import org.junit.Test;
+
+public class YearUnitTest {
+    
+    @Test
+    public void testToString() {
+        assertThat(new Year(new LocalDate(2001, 1, 1)).toString(), equalTo("2001"));
+    }
+    
+    @Test
+    public void testEqualsHashCode() {
+        Year year1 = new Year(new LocalDate(2001, 1, 1));
+        Year year2 = new Year(new LocalDate(2001, 1, 1));
+        assertThat(year1, equalTo(year2));
+        assertThat(year1.hashCode(), equalTo(year2.hashCode()));
+    }
+
+}
diff --git a/src/test/java/au/id/djc/rdftemplate/selector/AdaptationMatcher.java b/src/test/java/au/id/djc/rdftemplate/selector/AdaptationMatcher.java
@@ -0,0 +1,36 @@
+package au.id.djc.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);
+    }
+    
+    public static AdaptationMatcher<FormattedDateTimeAdaptation> formattedDTAdaptation(String pattern) {
+        AdaptationMatcher<FormattedDateTimeAdaptation> m = new AdaptationMatcher<FormattedDateTimeAdaptation>(FormattedDateTimeAdaptation.class);
+        m.addRequiredProperty("pattern", equalTo(pattern));
+        return m;
+    }
+
+}
diff --git a/src/test/java/au/id/djc/rdftemplate/selector/BeanPropertyMatcher.java b/src/test/java/au/id/djc/rdftemplate/selector/BeanPropertyMatcher.java
@@ -0,0 +1,56 @@
+package au.id.djc.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/id/djc/rdftemplate/selector/EternallyCachingSelectorFactoryUnitTest.java b/src/test/java/au/id/djc/rdftemplate/selector/EternallyCachingSelectorFactoryUnitTest.java
@@ -0,0 +1,22 @@
+package au.id.djc.rdftemplate.selector;
+
+import static org.hamcrest.CoreMatchers.sameInstance;
+import static org.junit.Assert.assertThat;
+
+import org.junit.Test;
+
+import au.id.djc.rdftemplate.TestNamespacePrefixMap;
+
+public class EternallyCachingSelectorFactoryUnitTest {
+    
+    @Test
+    public void shouldCacheSelectors() {
+        AntlrSelectorFactory wrappedFactory = new AntlrSelectorFactory();
+        wrappedFactory.setNamespacePrefixMap(TestNamespacePrefixMap.getInstance());
+        EternallyCachingSelectorFactory factory = new EternallyCachingSelectorFactory(wrappedFactory);
+        Selector<?> first = factory.get("dc:creator/foaf:name");
+        Selector<?> second = factory.get("dc:creator/foaf:name");
+        assertThat((Selector) first, sameInstance((Selector) second));
+    }
+
+}
diff --git a/src/test/java/au/id/djc/rdftemplate/selector/PredicateMatcher.java b/src/test/java/au/id/djc/rdftemplate/selector/PredicateMatcher.java
@@ -0,0 +1,34 @@
+package au.id.djc.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) {
+        super(type);
+    }
+    
+    public static PredicateMatcher<UriPrefixPredicate> uriPrefixPredicate(String prefix) {
+        PredicateMatcher<UriPrefixPredicate> m = new PredicateMatcher<UriPrefixPredicate>(UriPrefixPredicate.class);
+        m.addRequiredProperty("prefix", equalTo(prefix));
+        return m;
+    }
+    
+    public static PredicateMatcher<TypePredicate> typePredicate(String namespace, String localName) {
+        PredicateMatcher<TypePredicate> m = new PredicateMatcher<TypePredicate>(TypePredicate.class);
+        m.addRequiredProperty("namespace", equalTo(namespace));
+        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/id/djc/rdftemplate/selector/SelectorComparatorMatcher.java b/src/test/java/au/id/djc/rdftemplate/selector/SelectorComparatorMatcher.java
@@ -0,0 +1,25 @@
+package au.id.djc.rdftemplate.selector;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import org.hamcrest.Matcher;
+
+public class SelectorComparatorMatcher<T extends Comparable<T>> extends BeanPropertyMatcher<SelectorComparator<T>> {
+
+    @SuppressWarnings("unchecked")
+    public SelectorComparatorMatcher() {
+        super((Class) SelectorComparator.class);
+    }
+    
+    public static SelectorComparatorMatcher<?> selectorComparator(Matcher<? extends Selector<?>> selector) {
+        SelectorComparatorMatcher<?> m = new SelectorComparatorMatcher();
+        m.addRequiredProperty("selector", selector);
+        return m;
+    }
+    
+    public SelectorComparatorMatcher<T> reversed() {
+        addRequiredProperty("reversed", equalTo(true));
+        return this;
+    }
+
+}
diff --git a/src/test/java/au/id/djc/rdftemplate/selector/SelectorEvaluationUnitTest.java b/src/test/java/au/id/djc/rdftemplate/selector/SelectorEvaluationUnitTest.java
@@ -0,0 +1,224 @@
+package au.id.djc.rdftemplate.selector;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+import static org.junit.matchers.JUnitMatchers.*;
+
+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.id.djc.rdftemplate.TestNamespacePrefixMap;
+import au.id.djc.rdftemplate.datatype.DateDataType;
+import au.id.djc.rdftemplate.datatype.DateTimeDataType;
+
+public class SelectorEvaluationUnitTest {
+    
+    private Model m;
+    private Resource journal, issue, article, multiAuthorArticle, citedArticle, author, anotherAuthor, book, review, anotherReview, obituary, en, ru, forum;
+    private AntlrSelectorFactory selectorFactory;
+    
+    @BeforeClass
+    public static void ensureDatatypesRegistered() {
+        DateDataType.registerStaticInstance();
+        DateTimeDataType.registerStaticInstance();
+    }
+    
+    @Before
+    public void setUp() {
+        m = ModelFactory.createDefaultModel();
+        InputStream stream = this.getClass().getResourceAsStream("/au/id/djc/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");
+        multiAuthorArticle = m.createResource("http://miskinhill.com.au/journals/test/1:1/multi-author-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");
+        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");
+        forum = m.createResource("http://miskinhill.com.au/");
+        selectorFactory = new AntlrSelectorFactory();
+        selectorFactory.setNamespacePrefixMap(TestNamespacePrefixMap.getInstance());
+    }
+    
+    @Test
+    public void shouldEvaluateTraversal() {
+        RDFNode result = selectorFactory.get("dc:creator").withResultType(RDFNode.class).singleResult(article);
+        assertThat(result, equalTo((RDFNode) author));
+    }
+    
+    @Test
+    public void shouldEvaluateMultipleTraversals() throws Exception {
+        RDFNode result = selectorFactory.get("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 = selectorFactory.get("!dc:isPartOf")
+                .withResultType(RDFNode.class).result(issue);
+        assertThat(results.size(), equalTo(4));
+        assertThat(results, hasItems((RDFNode) article, (RDFNode) multiAuthorArticle, (RDFNode) review, (RDFNode) obituary));
+    }
+    
+    @Test
+    public void shouldEvaluateSortOrder() throws Exception {
+        List<RDFNode> results = selectorFactory.get("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 = selectorFactory.get("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 = selectorFactory.get("!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 = selectorFactory.get("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 = selectorFactory.get("#uri").withResultType(String.class).singleResult(journal);
+        assertThat(result, equalTo("http://miskinhill.com.au/journals/test/"));
+    }
+    
+    @Test
+    public void shouldEvaluateUriSliceAdaptation() throws Exception {
+        String result = selectorFactory.get("dc:identifier[uri-prefix='urn:issn:']#uri-slice(9)")
+                .withResultType(String.class).singleResult(journal);
+        assertThat(result, equalTo("12345678"));
+    }
+    
+    @Test
+    public void shouldEvaluateUriAnchorAdaptation() throws Exception {
+        String result = selectorFactory.get("mhs:inSection#uri-anchor")
+                .withResultType(String.class).singleResult(anotherReview);
+        assertThat(result, equalTo("stuff"));
+    }
+    
+    @Test
+    public void shouldEvaluateSubscript() throws Exception {
+        String result = selectorFactory.get(
+                "!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 = selectorFactory.get(
+                "!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 = selectorFactory.get("dc:language/lingvoj:iso1#lv")
+                .withResultType(Object.class).result(journal);
+        assertThat(results.size(), equalTo(2));
+        assertThat(results, hasItems((Object) "en", (Object) "ru"));
+    }
+    
+    @Test
+    public void shouldEvaluateTypePredicate() throws Exception {
+        List<RDFNode> results = selectorFactory.get("!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 = selectorFactory.get("!dc:creator[type=mhs:Article and uri-prefix='http://miskinhill.com.au/journals/']")
+                .withResultType(RDFNode.class).result(author);
+        assertThat(results.size(), equalTo(2));
+        assertThat(results, hasItems((RDFNode) article, (RDFNode) multiAuthorArticle));
+    }
+    
+    @Test
+    public void shouldEvaluateUnion() throws Exception {
+        List<RDFNode> results = selectorFactory.get("!dc:creator | !mhs:translator")
+                .withResultType(RDFNode.class).result(anotherAuthor);
+        assertThat(results.size(), equalTo(4));
+        assertThat(results, hasItems((RDFNode) article, (RDFNode) multiAuthorArticle, (RDFNode) citedArticle, (RDFNode) anotherReview));
+    }
+    
+    @Test
+    public void shouldEvaluateMultipleSortSelectors() throws Exception {
+        List<RDFNode> results = selectorFactory.get("!dc:creator[uri-prefix='http://miskinhill.com.au/journals/']" +
+        		"(~dc:isPartOf/mhs:publicationDate#comparable-lv,mhs:startPage#comparable-lv)")
+                .withResultType(RDFNode.class).result(author);
+        assertThat(results.size(), equalTo(4));
+        assertThat(results.get(0), equalTo((RDFNode) obituary));
+        assertThat(results.get(1), equalTo((RDFNode) article));
+        assertThat(results.get(2), equalTo((RDFNode) multiAuthorArticle));
+        assertThat(results.get(3), equalTo((RDFNode) review));
+    }
+    
+    @Test
+    public void shouldEvaluateFormattedDTAdaptation() throws Exception {
+        String result = selectorFactory.get("!sioc:has_container/dc:created#formatted-dt('d MMMM yyyy')")
+                .withResultType(String.class).singleResult(forum);
+        assertThat(result, equalTo("15 June 2009"));
+    }
+    
+    @Test
+    public void shouldEvaluateFormattedDTAdaptationWithDoubleQuotes() throws Exception {
+        String result = selectorFactory.get("!sioc:has_container/dc:created#formatted-dt('yyyy-MM-dd\"T\"HH:mm:ssZZ')")
+                .withResultType(String.class).singleResult(forum);
+        assertThat(result, equalTo("2009-06-15T18:21:32+10:00"));
+    }
+    
+    @Test
+    public void shouldEvaluateStringLVAdaptation() throws Exception {
+        List<String> results = selectorFactory.get("dc:language/lingvoj:iso1#string-lv")
+                .withResultType(String.class).result(journal);
+        assertThat(results.size(), equalTo(2));
+        assertThat(results, hasItems("en", "ru"));
+    }
+    
+    @Test
+    public void stringLVAdaptationShouldStripTagsFromXMLLiteral() throws Exception {
+        String result = selectorFactory.get("!sioc:has_container/sioc:content/awol:body#string-lv")
+                .withResultType(String.class).singleResult(forum);
+        assertEquals("To coincide with the publication of our second issue, " +
+        		"the 2008 volume of Australian Slavonic and East European Studies, " +
+        		"we are making available two new data feeds: an Atom feed of all " +
+        		"journal issues published on this site, and the complete RDF dataset " +
+        		"underlying the site. We hope this helps our users and aggregators " +
+        		"to discover new content as it is published.",
+        		result.trim().replaceAll("\\s+", " "));
+    }
+    
+}
diff --git a/src/test/java/au/id/djc/rdftemplate/selector/SelectorMatcher.java b/src/test/java/au/id/djc/rdftemplate/selector/SelectorMatcher.java
@@ -0,0 +1,36 @@
+package au.id.djc.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> {
+
+    private SelectorMatcher(Class<? extends T> type) {
+        super(type);
+    }
+    
+    public static SelectorMatcher<Selector<RDFNode>> selector(Matcher<Traversal>... traversals) {
+        if (traversals.length == 0) {
+            return new SelectorMatcher<Selector<RDFNode>>(NoopSelector.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);
+        m.addRequiredProperty("adaptation", adaptation);
+        return m;
+    }
+    
+}
diff --git a/src/test/java/au/id/djc/rdftemplate/selector/SelectorParserUnitTest.java b/src/test/java/au/id/djc/rdftemplate/selector/SelectorParserUnitTest.java
@@ -0,0 +1,206 @@
+package au.id.djc.rdftemplate.selector;
+
+import static au.id.djc.rdftemplate.TestNamespacePrefixMap.*;
+import static au.id.djc.rdftemplate.selector.AdaptationMatcher.*;
+import static au.id.djc.rdftemplate.selector.PredicateMatcher.*;
+import static au.id.djc.rdftemplate.selector.SelectorComparatorMatcher.*;
+import static au.id.djc.rdftemplate.selector.SelectorMatcher.*;
+import static au.id.djc.rdftemplate.selector.TraversalMatcher.*;
+import static org.junit.Assert.*;
+
+import com.hp.hpl.jena.rdf.model.RDFNode;
+import com.hp.hpl.jena.vocabulary.DCTerms;
+import com.hp.hpl.jena.vocabulary.RDF;
+import org.junit.Before;
+import org.junit.Test;
+
+import au.id.djc.rdftemplate.TestNamespacePrefixMap;
+
+public class SelectorParserUnitTest {
+    
+    private AntlrSelectorFactory factory;
+    
+    @Before
+    public void setUp() {
+        factory = new AntlrSelectorFactory();
+        factory.setNamespacePrefixMap(TestNamespacePrefixMap.getInstance());
+    }
+    
+    @Test
+    public void shouldRecogniseSingleTraversal() throws Exception {
+        Selector<RDFNode> selector = factory.get("dc:creator").withResultType(RDFNode.class);
+        assertThat(selector, selector(traversal(DCTerms.NS, "creator")));
+    }
+    
+    @Test
+    public void shouldRecogniseMultipleTraversals() throws Exception {
+        Selector<RDFNode> selector = factory.get("dc:creator/foaf:name").withResultType(RDFNode.class);
+        assertThat(selector, selector(
+                traversal(DCTerms.NS, "creator"),
+                traversal(FOAF_NS, "name")));
+    }
+    
+    @Test
+    public void shouldRecogniseMultipleTraversalsWithWhitespace() throws Exception {
+        Selector<RDFNode> selector = factory.get("dc:creator / foaf:name").withResultType(RDFNode.class);
+        assertThat(selector, selector(
+                traversal(DCTerms.NS, "creator"),
+                traversal(FOAF_NS, "name")));
+    }
+    
+    @Test
+    public void shouldRecogniseInverseTraversal() throws Exception {
+        Selector<RDFNode> selector = factory.get("!dc:isPartOf/!dc:isPartOf").withResultType(RDFNode.class);
+        assertThat(selector, selector(
+                traversal(DCTerms.NS, "isPartOf").inverse(),
+                traversal(DCTerms.NS, "isPartOf").inverse()));
+    }
+    
+    @Test
+    public void shouldRecogniseSortOrder() throws Exception {
+        Selector<RDFNode> selector = factory.get("!mhs:isIssueOf(mhs:publicationDate#comparable-lv)").withResultType(RDFNode.class);
+        assertThat(selector, selector(
+                traversal(MHS_NS, "isIssueOf").inverse()
+                .withSortOrder(selectorComparator(selector(traversal(MHS_NS, "publicationDate"))
+                    .withAdaptation(comparableLVAdaptation())))));
+    }
+    
+    @Test
+    public void shouldRecogniseReverseSortOrder() throws Exception {
+        Selector<RDFNode> selector = factory.get("!mhs:isIssueOf(~mhs:publicationDate#comparable-lv)").withResultType(RDFNode.class);
+        assertThat(selector, selector(
+                traversal(MHS_NS, "isIssueOf").inverse()
+                .withSortOrder(selectorComparator(selector(traversal(MHS_NS, "publicationDate"))
+                    .withAdaptation(comparableLVAdaptation())).reversed())));
+    }
+    
+    @Test
+    public void shouldRecogniseComplexSortOrder() throws Exception {
+        Selector<RDFNode> selector = factory.get("!mhs:reviews(dc:isPartOf/mhs:publicationDate#comparable-lv)").withResultType(RDFNode.class);
+        assertThat(selector, selector(
+                traversal(MHS_NS, "reviews")
+                .withSortOrder(selectorComparator(selector(traversal(DCTerms.NS, "isPartOf"), traversal(MHS_NS, "publicationDate"))
+                        .withAdaptation(comparableLVAdaptation())))));
+    }
+    
+    @Test
+    public void shouldRecogniseUriAdaptation() throws Exception {
+        Selector<?> selector = factory.get("mhs:coverThumbnail#uri");
+        assertThat(selector, selector(
+                traversal(MHS_NS, "coverThumbnail"))
+                .withAdaptation(uriAdaptation()));
+    }
+    
+    @Test
+    public void shouldRecogniseBareUriAdaptation() throws Exception {
+        Selector<?> selector = factory.get("#uri");
+        assertThat(selector, selector().withAdaptation(uriAdaptation()));
+    }
+    
+    @Test
+    public void shouldRecogniseUriSliceAdaptation() throws Exception {
+        Selector<?> selector = factory.get("dc:identifier[uri-prefix='urn:issn:']#uri-slice(9)");
+        assertThat(selector, selector(
+                traversal(DCTerms.NS, "identifier")
+                    .withPredicate(uriPrefixPredicate("urn:issn:")))
+                .withAdaptation(uriSliceAdaptation(9)));
+    }
+    
+    @Test
+    public void shouldRecogniseUriPrefixPredicate() throws Exception {
+        Selector<RDFNode> selector = factory.get(
+                "!mhs:isIssueOf[uri-prefix='http://miskinhill.com.au/journals/'](~mhs:publicationDate#comparable-lv)")
+                .withResultType(RDFNode.class);
+        assertThat(selector, selector(
+                traversal(MHS_NS, "isIssueOf")
+                    .inverse()
+                    .withPredicate(uriPrefixPredicate("http://miskinhill.com.au/journals/"))
+                    .withSortOrder(selectorComparator(selector(traversal(MHS_NS, "publicationDate"))
+                            .withAdaptation(comparableLVAdaptation())).reversed())));
+    }
+    
+    @Test
+    public void shouldRecogniseSubscript() throws Exception {
+        Selector<String> selector = factory.get(
+                "!mhs:isIssueOf(~mhs:publicationDate#comparable-lv)[0]/mhs:coverThumbnail#uri")
+                .withResultType(String.class);
+        assertThat(selector, selector(
+                traversal(MHS_NS, "isIssueOf")
+                    .inverse()
+                    .withSortOrder(selectorComparator(selector(traversal(MHS_NS, "publicationDate"))
+                            .withAdaptation(comparableLVAdaptation())).reversed())
+                    .withSubscript(0),
+                traversal(MHS_NS, "coverThumbnail"))
+                .withAdaptation(uriAdaptation()));
+    }
+    
+    @Test
+    public void shouldRecogniseLVAdaptation() throws Exception {
+        Selector<Object> selector = factory.get("dc:language/lingvoj:iso1#lv").withResultType(Object.class);
+        assertThat(selector, selector(
+                traversal(DCTerms.NS, "language"),
+                traversal("http://www.lingvoj.org/ontology#", "iso1"))
+                .withAdaptation(lvAdaptation()));
+    }
+    
+    @Test
+    public void shouldRecogniseTypePredicate() throws Exception {
+        Selector<RDFNode> selector = factory.get("!dc:creator[type=mhs:Review]").withResultType(RDFNode.class);
+        assertThat(selector, selector(
+                traversal(DCTerms.NS, "creator").inverse().withPredicate(typePredicate(MHS_NS, "Review"))));
+    }
+    
+    @Test
+    public void shouldRecogniseAndCombinationOfPredicates() throws Exception {
+        Selector<RDFNode> selector = factory.get("!dc:creator[type=mhs:Review and uri-prefix='http://miskinhill.com.au/journals/']").withResultType(RDFNode.class);
+        assertThat(selector, selector(
+                traversal(DCTerms.NS, "creator").inverse()
+                .withPredicate(booleanAndPredicate(
+                    typePredicate(MHS_NS, "Review"),
+                    uriPrefixPredicate("http://miskinhill.com.au/journals/")))));
+    }
+    
+    @Test
+    public void shouldRecogniseUnion() throws Exception {
+        Selector<RDFNode> selector = factory.get("!dc:creator | !mhs:translator").withResultType(RDFNode.class);
+        assertThat((UnionSelector<RDFNode>) selector, unionSelector(
+                selector(traversal(DCTerms.NS, "creator").inverse()),
+                selector(traversal(MHS_NS, "translator").inverse())));
+    }
+    
+    @Test
+    public void shouldRecogniseMultipleSortSelectors() throws Exception {
+        Selector<RDFNode> selector = factory.get("!dc:creator(~dc:isPartOf/mhs:publicationDate#comparable-lv,mhs:startPage#comparable-lv)").withResultType(RDFNode.class);
+        assertThat(selector, selector(
+                traversal(DCTerms.NS, "creator").inverse()
+                .withSortOrder(
+                    selectorComparator(selector(traversal(DCTerms.NS, "isPartOf"), traversal(MHS_NS, "publicationDate"))
+                        .withAdaptation(comparableLVAdaptation())).reversed(),
+                    selectorComparator(selector(traversal(MHS_NS, "startPage")).withAdaptation(comparableLVAdaptation())))));
+    }
+    
+    @Test
+    public void shouldRecogniseFormattedDTAdaptation() throws Exception {
+        Selector<?> selector = factory.get("dc:created#formatted-dt('d MMMM yyyy')");
+        assertThat(selector, selector(traversal(DCTerms.NS, "created"))
+                .withAdaptation(formattedDTAdaptation("d MMMM yyyy")));
+    }
+    
+    @Test
+    public void shouldRecogniseRdfType() throws Exception {
+        // was broken due to ANTLR being confused about the literal string "type" which was hardcoded to be a predicate
+        Selector<RDFNode> selector = factory.get("rdf:type").withResultType(RDFNode.class);
+        assertThat(selector, selector(traversal(RDF.getURI(), "type")));
+    }
+    
+    @Test(expected = InvalidSelectorSyntaxException.class)
+    public void shouldThrowForInvalidSyntax() throws Exception {
+        factory.get("dc:creator]["); // this is a parser error
+    }
+    
+    @Test(expected = InvalidSelectorSyntaxException.class)
+    public void shouldThrowForUnrecognisedCharacter() throws Exception {
+        factory.get("dc:cre&ator"); // ... and this is a lexer error
+    }
+    
+}
diff --git a/src/test/java/au/id/djc/rdftemplate/selector/TraversalMatcher.java b/src/test/java/au/id/djc/rdftemplate/selector/TraversalMatcher.java
@@ -0,0 +1,44 @@
+package au.id.djc.rdftemplate.selector;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.matchers.JUnitMatchers.hasItems;
+
+import java.util.Comparator;
+
+import com.hp.hpl.jena.rdf.model.RDFNode;
+import org.hamcrest.Matcher;
+
+public class TraversalMatcher extends BeanPropertyMatcher<Traversal> {
+    
+    private TraversalMatcher() {
+        super(Traversal.class);
+    }
+    
+    public static TraversalMatcher traversal(String propertyNamespace, String propertyLocalName) {
+        TraversalMatcher m = new TraversalMatcher();
+        m.addRequiredProperty("propertyNamespace", equalTo(propertyNamespace));
+        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<? extends Comparator<RDFNode>>... sortOrder) {
+        addRequiredProperty("sortOrder", hasItems(sortOrder));
+        return this;
+    }
+    
+    public TraversalMatcher withSubscript(int subscript) {
+        addRequiredProperty("subscript", equalTo(subscript));
+        return this;
+    }
+
+}
diff --git a/src/test/java/au/id/djc/rdftemplate/selector/UriAnchorAdaptationUnitTest.java b/src/test/java/au/id/djc/rdftemplate/selector/UriAnchorAdaptationUnitTest.java
@@ -0,0 +1,23 @@
+package au.id.djc.rdftemplate.selector;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import com.hp.hpl.jena.rdf.model.ResourceFactory;
+import org.junit.Test;
+
+public class UriAnchorAdaptationUnitTest {
+
+    @Test
+    public void shouldReturnAnchor() {
+        String result = new UriAnchorAdaptation().adapt(ResourceFactory.createResource("http://example.com/#asdf"));
+        assertThat(result, equalTo("asdf"));
+    }
+    
+    @Test
+    public void shouldReturnEmptyStringForUriWithoutAnchor() {
+        String result = new UriAnchorAdaptation().adapt(ResourceFactory.createResource("http://example.com/"));
+        assertThat(result, equalTo(""));
+    }
+
+}
diff --git a/src/test/resources/au/com/miskinhill/rdftemplate/conditional.xml b/src/test/resources/au/id/djc/rdftemplate/conditional.xml
diff --git a/src/test/resources/au/com/miskinhill/rdftemplate/for-seq.xml b/src/test/resources/au/id/djc/rdftemplate/for-seq.xml
diff --git a/src/test/resources/au/com/miskinhill/rdftemplate/for.xml b/src/test/resources/au/id/djc/rdftemplate/for.xml
diff --git a/src/test/resources/au/com/miskinhill/rdftemplate/join-seq.xml b/src/test/resources/au/id/djc/rdftemplate/join-seq.xml
diff --git a/src/test/resources/au/com/miskinhill/rdftemplate/join.xml b/src/test/resources/au/id/djc/rdftemplate/join.xml
diff --git a/src/test/resources/au/com/miskinhill/rdftemplate/namespaces.xml b/src/test/resources/au/id/djc/rdftemplate/namespaces.xml
diff --git a/src/test/resources/au/com/miskinhill/rdftemplate/replace-subtree.xml b/src/test/resources/au/id/djc/rdftemplate/replace-subtree.xml
diff --git a/src/test/resources/au/com/miskinhill/rdftemplate/replace-xml.xml b/src/test/resources/au/id/djc/rdftemplate/replace-xml.xml
diff --git a/src/test/resources/au/com/miskinhill/rdftemplate/test-data.xml b/src/test/resources/au/id/djc/rdftemplate/test-data.xml