jenkins-bot has submitted this change and it was merged.

Change subject: Resolve labels
......................................................................


Resolve labels

Implements a "service" that resolves label like things in a way that doesn't
change the cardinality of the result set. You can call it like this:
```
 SELECT *
 WHERE {
   SERVICE wikibase:label.en.de.fr {
     wd:Q123 rdfs:label ?q123Label .
     wd:Q123 rdfs:altLabel ?q123Alt .
     wd:Q123 schema:description ?q123Desc .
     wd:Q321 rdf:label ?q321Label .
   }
 }
```
or like this:
```
 SELECT ?sLabel ?sAltLabel ?sDescription ?oLabel
 WHERE {
   ?s wdt:P22 ?o .
   SERVICE wikibase:label.en.de.fr {
   }
 }
```

It works by resolving the labels one by one on each solution. Blazegraph
naturally pushes SERVICE calls to the last step in the query which is
perfect for this.

Change-Id: I37df29ad442692d350fdd35cd17badb05170b416
---
M blazegraph/pom.xml
A 
blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/PrefixDelegatingServiceFactory.java
M 
blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/WikibaseContextListener.java
A 
blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/WikibaseOptimizers.java
A 
blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/label/EmptyLabelServiceOptimizer.java
A 
blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/label/LabelService.java
A 
blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/AbstractRandomizedBlazegraphStorageTestCase.java
M 
blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/AbstractRandomizedBlazegraphTestBase.java
A 
blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/label/LabelServiceUnitTest.java
M common/src/main/java/org/wikidata/query/rdf/common/uri/Ontology.java
M common/src/main/java/org/wikidata/query/rdf/common/uri/RDFS.java
M common/src/main/java/org/wikidata/query/rdf/common/uri/SKOS.java
M dist/pom.xml
M dist/src/config/web.xml
M dist/src/script/runBlazegraph.sh
M pom.xml
M src/build/forbidden/all.txt
M src/build/forbidden/core.txt
A testTools/pom.xml
A testTools/src/main/java/org/wikidata/query/rdf/test/Matchers.java
R testTools/src/main/java/org/wikidata/query/rdf/test/StatementHelper.java
M tools/pom.xml
M 
tools/src/main/java/org/wikidata/query/rdf/tool/wikibase/WikibaseRepository.java
M tools/src/test/java/org/wikidata/query/rdf/tool/ExpandedStatementBuilder.java
M tools/src/test/java/org/wikidata/query/rdf/tool/IOBlastingIntegrationTest.java
D tools/src/test/java/org/wikidata/query/rdf/tool/Matchers.java
M 
tools/src/test/java/org/wikidata/query/rdf/tool/MultipleResultsQueryIntegrationTest.java
M tools/src/test/java/org/wikidata/query/rdf/tool/MungeIntegrationTest.java
M 
tools/src/test/java/org/wikidata/query/rdf/tool/WikibaseDateExtensionIntegrationTest.java
M tools/src/test/java/org/wikidata/query/rdf/tool/rdf/MungerUnitTest.java
M 
tools/src/test/java/org/wikidata/query/rdf/tool/rdf/NormalizingRdfHandlerUnitTest.java
M 
tools/src/test/java/org/wikidata/query/rdf/tool/rdf/RdfRepositoryIntegrationTest.java
32 files changed, 1,468 insertions(+), 301 deletions(-)

Approvals:
  Smalyshev: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/blazegraph/pom.xml b/blazegraph/pom.xml
index bd07ee1..db81d01 100644
--- a/blazegraph/pom.xml
+++ b/blazegraph/pom.xml
@@ -22,17 +22,29 @@
     <dependency>
       <groupId>com.bigdata</groupId>
       <artifactId>bigdata</artifactId>
-      <version>${blazegraph.version}</version>
+      <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.wikidata.query.rdf</groupId>
       <artifactId>common</artifactId>
+    </dependency>
+    <dependency>
+      <!-- Blazegraph needs http client to run services. -->
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-client</artifactId>
+      <version>9.2.10.v20150310</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.wikidata.query.rdf</groupId>
+      <artifactId>testTools</artifactId>
       <version>${project.parent.version}</version>
+      <scope>test</scope>
     </dependency>
   </dependencies>
 
   <build>
-    <finalName>wikidata-query-blazegraph-${project.version}</finalName>  
+    <finalName>wikidata-query-blazegraph-${project.version}</finalName>
     <plugins>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
diff --git 
a/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/PrefixDelegatingServiceFactory.java
 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/PrefixDelegatingServiceFactory.java
new file mode 100644
index 0000000..45daf08
--- /dev/null
+++ 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/PrefixDelegatingServiceFactory.java
@@ -0,0 +1,60 @@
+package org.wikidata.query.rdf.blazegraph;
+
+import org.openrdf.model.URI;
+
+import com.bigdata.rdf.sparql.ast.service.IServiceOptions;
+import com.bigdata.rdf.sparql.ast.service.ServiceCall;
+import com.bigdata.rdf.sparql.ast.service.ServiceCallCreateParams;
+import com.bigdata.rdf.sparql.ast.service.ServiceFactory;
+
+/**
+ * ServiceFactory that sends service calls that match a prefix to a different
+ * factory than those that don't. Useful for setting ServiceRegistry's
+ * defaultService so some prefixes can be reserved for different types of
+ * services.
+ */
+public class PrefixDelegatingServiceFactory implements ServiceFactory {
+    /**
+     * Service factory to use if the prefix doesn't match.
+     */
+    private final ServiceFactory defaultFactory;
+    /**
+     * Prefix to check.
+     */
+    private final String prefix;
+    /**
+     * Service factory to use if the prefix does match.
+     */
+    private final ServiceFactory prefixedFactory;
+
+    public PrefixDelegatingServiceFactory(ServiceFactory defaultFactory, 
String prefix, ServiceFactory prefixedFactory) {
+        this.defaultFactory = defaultFactory;
+        this.prefix = prefix;
+        this.prefixedFactory = prefixedFactory;
+    }
+
+    @Override
+    public IServiceOptions getServiceOptions() {
+        /*
+         * Sadly we can't figure out which service options to use so we just 
use
+         * the default ones. This is almost certainly wrong bug doesn't seem to
+         * cause any trouble yet.
+         */
+        return defaultFactory.getServiceOptions();
+    }
+
+    @Override
+    public ServiceCall<?> create(ServiceCallCreateParams params) {
+        return getServiceFactory(params.getServiceURI()).create(params);
+    }
+
+    /**
+     * Get the service factory to use given this URI.
+     */
+    private ServiceFactory getServiceFactory(URI uri) {
+        if (uri.stringValue().startsWith(prefix)) {
+            return prefixedFactory;
+        }
+        return defaultFactory;
+    }
+}
diff --git 
a/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/WikibaseContextListener.java
 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/WikibaseContextListener.java
index dcdb3b0..a29a5fa 100644
--- 
a/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/WikibaseContextListener.java
+++ 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/WikibaseContextListener.java
@@ -2,6 +2,8 @@
 
 import javax.servlet.ServletContextEvent;
 
+import org.wikidata.query.rdf.blazegraph.label.LabelService;
+
 import com.bigdata.rdf.sail.webapp.BigdataRDFServletContextListener;
 import com.bigdata.rdf.sparql.ast.service.IServiceOptions;
 import com.bigdata.rdf.sparql.ast.service.ServiceCall;
@@ -13,17 +15,25 @@
  * Context listener to enact configurations we need on initialization.
  */
 public class WikibaseContextListener extends BigdataRDFServletContextListener {
+    /**
+     * Replaces the default Blazegraph services with ones that do not allow
+     * remote services and a label resolution service.
+     */
+    public static void initializeServices() {
+        ServiceRegistry.getInstance().setDefaultServiceFactory(new 
DisableRemotesServiceFactory());
+        LabelService.register();
+    }
 
     @Override
     public void contextInitialized(final ServletContextEvent e) {
         super.contextInitialized(e);
-        ServiceRegistry.getInstance().setDefaultServiceFactory(new 
DisableRemotesServiceFactory());
+        initializeServices();
     }
 
     /**
      * Service factory that disables remote access.
      */
-    private final class DisableRemotesServiceFactory implements ServiceFactory 
{
+    private static final class DisableRemotesServiceFactory implements 
ServiceFactory {
 
         @Override
         public IServiceOptions getServiceOptions() {
diff --git 
a/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/WikibaseOptimizers.java
 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/WikibaseOptimizers.java
new file mode 100644
index 0000000..cc4a7e4
--- /dev/null
+++ 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/WikibaseOptimizers.java
@@ -0,0 +1,16 @@
+package org.wikidata.query.rdf.blazegraph;
+
+import org.wikidata.query.rdf.blazegraph.label.EmptyLabelServiceOptimizer;
+
+import com.bigdata.rdf.sparql.ast.optimizers.DefaultOptimizerList;
+
+/**
+ * Optimizer list for Wikibase.
+ */
+public class WikibaseOptimizers extends DefaultOptimizerList {
+    private static final long serialVersionUID = 2364845438265527328L;
+
+    public WikibaseOptimizers() {
+        add(new EmptyLabelServiceOptimizer());
+    }
+}
diff --git 
a/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/label/EmptyLabelServiceOptimizer.java
 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/label/EmptyLabelServiceOptimizer.java
new file mode 100644
index 0000000..113413b
--- /dev/null
+++ 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/label/EmptyLabelServiceOptimizer.java
@@ -0,0 +1,98 @@
+package org.wikidata.query.rdf.blazegraph.label;
+
+import org.apache.log4j.Logger;
+import org.openrdf.model.URI;
+import org.openrdf.model.impl.URIImpl;
+import org.openrdf.model.vocabulary.RDFS;
+import org.openrdf.model.vocabulary.SKOS;
+import org.wikidata.query.rdf.common.uri.Ontology;
+import org.wikidata.query.rdf.common.uri.SchemaDotOrg;
+
+import com.bigdata.bop.IBindingSet;
+import com.bigdata.bop.IConstant;
+import com.bigdata.bop.IVariable;
+import com.bigdata.rdf.internal.IV;
+import com.bigdata.rdf.model.BigdataValue;
+import com.bigdata.rdf.sparql.ast.AssignmentNode;
+import com.bigdata.rdf.sparql.ast.ConstantNode;
+import com.bigdata.rdf.sparql.ast.JoinGroupNode;
+import com.bigdata.rdf.sparql.ast.ProjectionNode;
+import com.bigdata.rdf.sparql.ast.StatementPatternNode;
+import com.bigdata.rdf.sparql.ast.StaticAnalysis;
+import com.bigdata.rdf.sparql.ast.VarNode;
+import com.bigdata.rdf.sparql.ast.eval.AST2BOpContext;
+import com.bigdata.rdf.sparql.ast.optimizers.AbstractJoinGroupOptimizer;
+import com.bigdata.rdf.sparql.ast.service.ServiceNode;
+
+/**
+ * Rewrites empty calls to the label service to attempt to resolve labels based
+ * on the query's projection.
+ */
+@SuppressWarnings("rawtypes")
+public class EmptyLabelServiceOptimizer extends AbstractJoinGroupOptimizer {
+    private static final Logger log = 
Logger.getLogger(EmptyLabelServiceOptimizer.class);
+
+    /**
+     * Schema.org's description property as a URI.
+     */
+    private static final URI DESCRIPTION = new 
URIImpl(SchemaDotOrg.DESCRIPTION);
+
+    @Override
+    protected void optimizeJoinGroup(AST2BOpContext ctx, StaticAnalysis sa, 
IBindingSet[] bSets, JoinGroupNode op) {
+        for (ServiceNode service : op.getServiceNodes()) {
+            BigdataValue serviceRef = service.getServiceRef().getValue();
+            if (serviceRef == null) {
+                continue;
+            }
+            if (!serviceRef.stringValue().startsWith(Ontology.LABEL)) {
+                continue;
+            }
+            JoinGroupNode g = (JoinGroupNode) service.getGraphPattern();
+            if (!g.args().isEmpty()) {
+                continue;
+            }
+            addResolutions(ctx, g, sa.getQueryRoot().getProjection());
+            // We can really only do this once....
+            return;
+        }
+    }
+
+    /**
+     * Infer that the user wanted to resolve some variables using the label
+     * service.
+     */
+    private void addResolutions(AST2BOpContext ctx, JoinGroupNode g, 
ProjectionNode p) {
+        for (AssignmentNode a : p) {
+            IVariable<IV> var = a.getVar();
+            if (a.getValueExpression() != var) {
+                continue;
+            }
+            /*
+             * Try and match a variable name we can resolve via labels. Note
+             * that we should match AltLabel before Label because Label is a
+             * suffix of it....
+             */
+            boolean replaced = addResolutionIfSuffix(ctx, g, "AltLabel", 
SKOS.ALT_LABEL, var)
+                    || addResolutionIfSuffix(ctx, g, "Label", RDFS.LABEL, var)
+                    || addResolutionIfSuffix(ctx, g, "Description", 
DESCRIPTION, var);
+            if (replaced && log.isDebugEnabled()) {
+                log.debug("Resolving " + var + " using a label lookup.");
+            }
+        }
+    }
+
+    /**
+     * Add the join group to resolve a variable if it matches a suffix,
+     * returning true if it matched, false otherwise.
+     */
+    private boolean addResolutionIfSuffix(AST2BOpContext ctx, JoinGroupNode g, 
String suffix, URI labelType,
+            IVariable<IV> var) {
+        if (!var.getName().endsWith(suffix)) {
+            return false;
+        }
+        String source = var.getName().substring(0, var.getName().length() - 
suffix.length());
+        IConstant<IV> labelTypeAsConstant = 
ctx.getAbstractTripleStore().getVocabulary().getConstant(labelType);
+        g.addArg(new StatementPatternNode(new VarNode(source), new 
ConstantNode(labelTypeAsConstant), new VarNode(var)));
+        return true;
+    }
+}
diff --git 
a/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/label/LabelService.java
 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/label/LabelService.java
new file mode 100644
index 0000000..3749d3b
--- /dev/null
+++ 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/label/LabelService.java
@@ -0,0 +1,530 @@
+package org.wikidata.query.rdf.blazegraph.label;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.openrdf.model.Literal;
+import org.openrdf.model.impl.LiteralImpl;
+import org.openrdf.model.vocabulary.RDFS;
+import org.wikidata.query.rdf.blazegraph.PrefixDelegatingServiceFactory;
+import org.wikidata.query.rdf.common.uri.Ontology;
+import org.wikidata.query.rdf.common.uri.WikibaseUris;
+
+import com.bigdata.bop.BOp;
+import com.bigdata.bop.Constant;
+import com.bigdata.bop.IBindingSet;
+import com.bigdata.bop.IValueExpression;
+import com.bigdata.bop.IVariable;
+import com.bigdata.rdf.internal.IV;
+import com.bigdata.rdf.internal.VTE;
+import com.bigdata.rdf.internal.impl.TermId;
+import com.bigdata.rdf.lexicon.LexiconRelation;
+import com.bigdata.rdf.model.BigdataValue;
+import com.bigdata.rdf.sparql.ast.JoinGroupNode;
+import com.bigdata.rdf.sparql.ast.StatementPatternNode;
+import com.bigdata.rdf.sparql.ast.VarNode;
+import com.bigdata.rdf.sparql.ast.service.BigdataNativeServiceOptions;
+import com.bigdata.rdf.sparql.ast.service.BigdataServiceCall;
+import com.bigdata.rdf.sparql.ast.service.IServiceOptions;
+import com.bigdata.rdf.sparql.ast.service.ServiceCall;
+import com.bigdata.rdf.sparql.ast.service.ServiceCallCreateParams;
+import com.bigdata.rdf.sparql.ast.service.ServiceFactory;
+import com.bigdata.rdf.sparql.ast.service.ServiceRegistry;
+import com.bigdata.rdf.spo.ISPO;
+import com.bigdata.rdf.store.AbstractTripleStore;
+import com.bigdata.striterator.IChunkedOrderedIterator;
+
+import cutthecrap.utils.striterators.ICloseableIterator;
+
+/**
+ * Implements a "service" that resolves label like things in a way that doesn't
+ * change the cardinality of the result set. You can call it like this: <code>
+ *  SELECT *
+ *  WHERE {
+ *    SERVICE wikibase:label.en.de.fr {
+ *      wd:Q123 rdfs:label ?q123Label .
+ *      wd:Q123 rdfs:altLabel ?q123Alt .
+ *      wd:Q123 schema:description ?q123Desc .
+ *      wd:Q321 rdf:label ?q321Label .
+ *    }
+ *  }
+ * </code> or like this:<code>
+ *  SELECT ?sLabel ?sAltLabel ?sDescription ?oLabel
+ *  WHERE {
+ *    ?s wdt:P22 ?o .
+ *    SERVICE wikibase:label.en.de.fr {
+ *    }
+ *  }
+ * </code>
+ * <p>
+ * If the label isn't available in any of the fallback languages it'll come 
back
+ * as the entityId. Alt labels and descriptions just come back unbound if they
+ * don't exist. If multiple values are defined they come back as a comma
+ * separated list.
+ *
+ * <p>
+ * This works by resolving the label-like thing per incoming binding, one at a
+ * time. It would probably be faster to do something bulkish but we don't do
+ * that yet. The code to do the comma separated lists and entityIds is pretty
+ * simple once you've resolve the data.
+ * <p>
+ * The second invocation pattern works using {@code EmptyLabelServiceOptimizer}
+ * to inspect the query and automatically build the first form out of the 
second
+ * form by inspecting the query's projection.
+ */
+public class LabelService implements ServiceFactory {
+    /**
+     * Options configuring this service as a native Blazegraph service.
+     */
+    private static final BigdataNativeServiceOptions SERVICE_OPTIONS = new 
BigdataNativeServiceOptions();
+
+    /**
+     * Register the service so it is recognized by Blazegraph.
+     */
+    public static void register() {
+        ServiceRegistry.getInstance().setDefaultServiceFactory(
+                new 
PrefixDelegatingServiceFactory(ServiceRegistry.getInstance().getDefaultServiceFactory(),
+                        Ontology.LABEL, new LabelService()));
+    }
+
+    @Override
+    public IServiceOptions getServiceOptions() {
+        return SERVICE_OPTIONS;
+    }
+
+    @Override
+    public ServiceCall<?> create(ServiceCallCreateParams params) {
+        /*
+         * Luckily service calls are always pushed to the last operation in a
+         * query. We still check it and tell users we won't resolve labels for
+         * unbound subjects.
+         */
+        // TODO this whole class just throws RuntimeException instead of ??
+        return new LabelServiceCall(new 
ResolutionContext(params.getTripleStore(), findLanguageFallbacks(params)),
+                findResolutions(params));
+    }
+
+    /**
+     * Resolve the language fallbacks from the statement pattern node in the
+     * query.
+     */
+    private Map<String, Integer> findLanguageFallbacks(ServiceCallCreateParams 
params) {
+        String uri = params.getServiceURI().stringValue();
+        if (!uri.startsWith(Ontology.LABEL + ".")) {
+            throw new IllegalArgumentException("You must provide the label 
service a list of languages.");
+        }
+        String fallbacks = uri.substring(Ontology.LABEL.length() + 1);
+        // TODO there has to be a better data structure for this.
+        /*
+         * Lucene has tons of things for this, but yeah. Maybe it doesn't
+         * matter.
+         */
+        Map<String, Integer> fallbacksMap = new HashMap<>();
+        String[] lang = fallbacks.split("\\.");
+        for (int i = 0; i < lang.length; i++) {
+            fallbacksMap.put(lang[i], i);
+        }
+        if (fallbacksMap.isEmpty()) {
+            throw new IllegalArgumentException("You must provide the label 
service a list of languages.");
+        }
+        return fallbacksMap;
+    }
+
+    /**
+     * Create the resolutions list from the service call parameters.
+     */
+    private List<Resolution> findResolutions(ServiceCallCreateParams params) {
+        JoinGroupNode g = (JoinGroupNode) 
params.getServiceNode().getGraphPattern();
+        List<Resolution> resolutions = new ArrayList<>(g.args().size());
+        for (BOp st : g.args()) {
+            resolutions.add(new Resolution((StatementPatternNode) st));
+        }
+        return resolutions;
+    }
+
+    /**
+     * Represents the call site in a particular SPARQL query.
+     */
+    @SuppressWarnings("checkstyle:visibilitymodifier")
+    private static class LabelServiceCall implements BigdataServiceCall {
+        /*
+         * Suppress VisibilityModifier check because members are package 
private
+         * so non-static inner classes can access them without the messy
+         * accessor methods. This isn't an information leak because this class
+         * is already a private inner class.
+         */
+        /**
+         * The context in which the resolutions will be done.
+         */
+        final ResolutionContext context;
+        /**
+         * Things to resolve.
+         */
+        final List<Resolution> resolutions;
+
+        /**
+         * Build with all the right stuff resolved.
+         */
+        public LabelServiceCall(ResolutionContext context, List<Resolution> 
resolutions) {
+            this.context = context;
+            this.resolutions = resolutions;
+        }
+
+        @Override
+        public IServiceOptions getServiceOptions() {
+            return SERVICE_OPTIONS;
+        }
+
+        @Override
+        public ICloseableIterator<IBindingSet> call(final IBindingSet[] 
bindingSets) throws Exception {
+            return new Chunk(bindingSets);
+        }
+
+        /**
+         * A chunk of calls to resolve labels.
+         */
+        private class Chunk implements ICloseableIterator<IBindingSet> {
+            /**
+             * Binding sets being resolved in this chunk.
+             */
+            private final IBindingSet[] bindingSets;
+            /**
+             * Has this chunk been closed?
+             */
+            private boolean closed;
+            /**
+             * Index of the next binding set to handle when next is next 
called.
+             */
+            private int i;
+
+            public Chunk(IBindingSet[] bindingSets) {
+                this.bindingSets = bindingSets;
+            }
+
+            @Override
+            public boolean hasNext() {
+                return !closed && i < bindingSets.length;
+            }
+
+            @Override
+            public IBindingSet next() {
+                IBindingSet binding = bindingSets[i++];
+                context.binding(binding);
+                for (Resolution resolution : resolutions) {
+                    context.resolve(resolution);
+                }
+                return binding;
+            }
+
+            @Override
+            public void remove() {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public void close() {
+                closed = true;
+            }
+        }
+    }
+
+    /**
+     * Description of a specific resolution request for the service. The 
service
+     * can resolve many such requests at once.
+     */
+    @SuppressWarnings("rawtypes")
+    private static final class Resolution {
+        /**
+         * Subject of the service call.
+         */
+        private final IValueExpression subject;
+        /**
+         * URI for the label to resolve.
+         */
+        private final IValueExpression label;
+        /**
+         * The target variable to which to bind the label.
+         */
+        private final IVariable target;
+
+        private Resolution(StatementPatternNode st) {
+            subject = st.s().getValueExpression();
+            label = st.p().getValueExpression();
+            target = getVariableToBind(st);
+        }
+
+        /**
+         * Subject of the service call.
+         */
+        public IValueExpression subject() {
+            return subject;
+        }
+
+        /**
+         * URI for the label to resolve.
+         */
+        public IValueExpression labelType() {
+            return label;
+        }
+
+        /**
+         * The target variable to which to bind the label.
+         */
+        public IVariable target() {
+            return target;
+        }
+
+        /**
+         * Resolve the variable that needs to be bound from the statement
+         * pattern node in the query.
+         */
+        private IVariable<IV> getVariableToBind(StatementPatternNode st) {
+            try {
+                return ((VarNode) st.o()).getValueExpression();
+            } catch (ClassCastException e) {
+                throw new RuntimeException("Expected a variable in the object 
position to which to bind the language.");
+            }
+        }
+    }
+
+    /**
+     * Context in which Resolutions are resolved. This only goes from subjects
+     * and label types to labels. It doesn't go from label types and label
+     * values to subjects. That wouldn't be an efficient process anyway even
+     * though it is technically possible.
+     */
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    private static class ResolutionContext {
+        /**
+         * The TripleStore to resolve the BindingSets against.
+         */
+        private final AbstractTripleStore tripleStore;
+        /**
+         * The LexiconRelation for the TripleStore we're working with.
+         */
+        private final LexiconRelation lexiconRelation;
+        /**
+         * The language fallbacks as a map from language code to order of
+         * precidence.
+         */
+        private final Map<String, Integer> languageFallbacks;
+        /**
+         * List of labels with the best language. Cleared and rebuilt as on
+         * every new call to resolve.
+         */
+        private final List<IV> bestLabels = new ArrayList<>();
+        /**
+         * The binding currently being resolved.
+         */
+        private IBindingSet binding;
+        /**
+         * The subject for this resolution as resolved in this BindingSet.
+         */
+        private IV resolvedSubject;
+        /**
+         * The label type for the current resolution as resolved in this
+         * BindingSet.
+         */
+        private IV resolvedLabelType;
+        /**
+         * The IV the represents rdfs:label. Its built lazily when needed and
+         * cached.
+         */
+        private IV rdfsLabelIv;
+
+        public ResolutionContext(AbstractTripleStore tripleStore, Map<String, 
Integer> languageFallbacks) {
+            this.tripleStore = tripleStore;
+            this.languageFallbacks = languageFallbacks;
+            lexiconRelation = tripleStore.getLexiconRelation();
+        }
+
+        /**
+         * Set the current BindingSet to be worked on.
+         */
+        public void binding(IBindingSet binding) {
+            this.binding = binding;
+        }
+
+        /**
+         * Resolve the target of the resolution in the current BindingSet.
+         */
+        public void resolve(Resolution resolution) {
+            resolvedSubject = resolveToIvOrError(resolution.subject(), 
"subject");
+            resolvedLabelType = resolveToIvOrError(resolution.labelType(), 
"label type");
+            // TODO this is one at a time - maybe a batch things?
+            fillBestLabels();
+            IV label = pickOrBuildBestLabel();
+            if (label != null) {
+                binding.set(resolution.target(), new Constant(label));
+            }
+        }
+
+        /**
+         * Gets the best label from a lookup. The best labels are put into the
+         * bestLabels list parameter. That parameter is cleared before the
+         * method starts and returning an empty list means there are no good
+         * labels.
+         */
+        private void fillBestLabels() {
+            IChunkedOrderedIterator<ISPO> lookup = 
tripleStore.getAccessPath(resolvedSubject, resolvedLabelType, null)
+                    .iterator();
+            try {
+                bestLabels.clear();
+                int bestLabelRank = Integer.MAX_VALUE;
+                while (lookup.hasNext()) {
+                    ISPO spo = lookup.next();
+                    IV o = spo.o();
+                    if (!o.isLiteral()) {
+                        // Not a literal, no chance its a label then
+                        continue;
+                    }
+                    /*
+                     * Hydrate all of the objects into language literals so we
+                     * can check the language. This is slow because it has to 
go
+                     * to the term dictionary but there isn't anything we can 
do
+                     * about it for now.
+                     */
+                    Literal literal = (Literal) lexiconRelation.getTerm(o);
+                    String language = literal.getLanguage();
+                    if (language == null) {
+                        // Not a language label, skip.
+                        continue;
+                    }
+                    Integer languageOrdinal = languageFallbacks.get(language);
+                    if (languageOrdinal == null) {
+                        // Not a language the user wants
+                        continue;
+                    }
+                    if (languageOrdinal == bestLabelRank) {
+                        bestLabels.add(o);
+                    }
+                    if (languageOrdinal < bestLabelRank) {
+                        bestLabelRank = languageOrdinal;
+                        bestLabels.clear();
+                        bestLabels.add(o);
+                    }
+                }
+            } finally {
+                lookup.close();
+            }
+        }
+
+        /**
+         * By hook or by crook return a single IV for this resolution. 
Processes
+         * bestLabels, so you'll have to call fillBestLabels before calling
+         * this. Options:
+         * <ul>
+         * <li>If there is a single label it returns it.
+         * <li>If there isn't a label it uses bestEffortLabel to mock up a 
label
+         * <li>If there are multiple labels it uses joinLabels to smoosh them
+         * into a comma separated list.
+         * </ul>
+         */
+        private IV pickOrBuildBestLabel() {
+            switch (bestLabels.size()) {
+            case 1:
+                // Found a single label so we can just return it.
+                // This is probably the most common case.
+                return bestLabels.get(0);
+            case 0:
+                // Didn't find a real label so lets fake one up
+                return bestEffortLabel();
+            default:
+                return joinLabels();
+            }
+        }
+
+        /**
+         * Build a mock IV from a literal.
+         */
+        private IV mock(Literal literal) {
+            TermId mock = TermId.mockIV(VTE.LITERAL);
+            mock.setValue(lexiconRelation.getValueFactory().asValue(literal));
+            return mock;
+        }
+
+        /**
+         * Returns the IV to which expression is bound in the current context 
or
+         * throws an error if it isn't bound.
+         */
+        private IV resolveToIvOrError(IValueExpression expression, String 
nameOfExpression) {
+            Object resolved = expression.get(binding);
+            if (resolved == null) {
+                throw new RuntimeException(String.format(Locale.ROOT,
+                        "Refusing to lookup labels for unknown %s (%s). That'd 
be way way way inefficient.",
+                        nameOfExpression, expression));
+            }
+            try {
+                return (IV) resolved;
+            } catch (ClassCastException e) {
+                throw new RuntimeException(String.format(Locale.ROOT,
+                        "Expected %s (%s) to be bound to an IV but it 
wasn't.", nameOfExpression, expression));
+            }
+        }
+
+        /**
+         * The IV the represents rdfs:label. Its built lazily when needed and
+         * cached.
+         */
+        public IV rdfsLabelIv() {
+            if (rdfsLabelIv == null) {
+                rdfsLabelIv = tripleStore.getVocabulary().get(RDFS.LABEL);
+            }
+            return rdfsLabelIv;
+        }
+
+        /**
+         * The WikibaseUris to use in this context.
+         */
+        private WikibaseUris uris() {
+            // TODO lookup wikibase host and default to wikidata
+            return WikibaseUris.WIKIDATA;
+        }
+
+        /**
+         * Build a label for something without a label. If the 
resolvedLabelType
+         * is actually rdfs:label you'll get a nice Q1324 style label but if it
+         * isn't you'll get an empty string.
+         */
+        private IV bestEffortLabel() {
+            // Only rdfs:label gets the entity ID as the label
+            if (!rdfsLabelIv().equals(resolvedLabelType)) {
+                // Everything else gets the empty string
+                return null;
+            }
+            BigdataValue value = lexiconRelation.getTerm(resolvedSubject);
+            String bestEffortLabel = value.stringValue();
+            if (bestEffortLabel.startsWith(uris().entity())) {
+                bestEffortLabel = 
bestEffortLabel.substring(uris().entity().length());
+            }
+            return mock(new LiteralImpl(bestEffortLabel));
+        }
+
+        /**
+         * Smoosh bestLabels into a comma separated list.
+         */
+        private IV joinLabels() {
+            // Found lots of labels so we should merge them into one.
+            // This is going to be common for alt labels
+            StringBuilder b = new StringBuilder();
+            String language = null;
+            boolean first = true;
+            for (IV label : bestLabels) {
+                Literal literal = (Literal) lexiconRelation.getTerm(label);
+                if (!first) {
+                    b.append(", ");
+                } else {
+                    first = false;
+                }
+                b.append(literal.stringValue());
+                if (language == null) {
+                    language = literal.getLanguage();
+                }
+            }
+            return mock(new LiteralImpl(b.toString(), language));
+        }
+    }
+}
diff --git 
a/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/AbstractRandomizedBlazegraphStorageTestCase.java
 
b/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/AbstractRandomizedBlazegraphStorageTestCase.java
new file mode 100644
index 0000000..567e536
--- /dev/null
+++ 
b/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/AbstractRandomizedBlazegraphStorageTestCase.java
@@ -0,0 +1,116 @@
+package org.wikidata.query.rdf.blazegraph;
+
+import static 
com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope.Scope.SUITE;
+
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.util.Properties;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.runner.RunWith;
+
+import com.bigdata.bop.engine.QueryEngine;
+import com.bigdata.bop.fed.QueryEngineFactory;
+import com.bigdata.cache.SynchronizedHardReferenceQueueWithTimeout;
+import com.bigdata.journal.TemporaryStore;
+import com.bigdata.rdf.store.AbstractTripleStore;
+import com.bigdata.rdf.store.TempTripleStore;
+import com.carrotsearch.randomizedtesting.RandomizedRunner;
+import com.carrotsearch.randomizedtesting.RandomizedTest;
+import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
+
+/**
+ * Randomized test that creates a triple store.
+ *
+ * <p>
+ * We have to take a number of actions to make RandomizedRunner compatible with
+ * Blazegraph:
+ * <ul>
+ * <li>Switch the ThreadLeakScope to SUITE because there are threads that
+ * survive across tests
+ * <li>Create a temporary store that is shared for all test methods that holds
+ * multiple triple stores
+ * <li>Create a new triple store per test method (lazily)
+ * </ul>
+ */
+@RunWith(RandomizedRunner.class)
+@ThreadLeakScope(SUITE)
+public class AbstractRandomizedBlazegraphStorageTestCase extends 
RandomizedTest {
+
+    /**
+     * Holds all the triples stores. Initialized once per test class.
+     */
+    private static TemporaryStore temporaryStore;
+    /**
+     * Triple store for the current test method. Lazily initialized.
+     */
+    private AbstractTripleStore store;
+
+    /**
+     * Get a TemporaryStore. Lazily initialized once per test class.
+     */
+    private static TemporaryStore temporaryStore() {
+        if (temporaryStore != null) {
+            return temporaryStore;
+        }
+        /*
+         * Initializing the temporary store replaces RandomizedRunner's
+         * painstakingly applied UncaughtExceptionHandler. That is bad so we
+         * replace it.
+         */
+        UncaughtExceptionHandler uncaughtExceptionHandler = 
Thread.getDefaultUncaughtExceptionHandler();
+        temporaryStore = new TemporaryStore();
+        Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
+        return temporaryStore;
+    }
+
+    /**
+     * Get a triple store. Lazily initialized once per test method.
+     */
+    protected AbstractTripleStore store() {
+        if (store != null) {
+            return store;
+        }
+        Properties properties = new Properties();
+        
properties.setProperty("com.bigdata.rdf.store.AbstractTripleStore.vocabularyClass",
+                WikibaseVocabulary.V001.class.getName());
+        
properties.setProperty("com.bigdata.rdf.store.AbstractTripleStore.inlineURIFactory",
+                WikibaseInlineUriFactory.class.getName());
+        store = new TempTripleStore(temporaryStore(), properties, null);
+        return store;
+    }
+
+    /**
+     * Close the temporary store used by this test.
+     * @throws InterruptedException if the executor service fails to await 
termination
+     */
+    @AfterClass
+    public static void closeTemporaryStore() throws InterruptedException {
+        if (temporaryStore == null) {
+            return;
+        }
+        ExecutorService executorService = temporaryStore.getExecutorService();
+        temporaryStore.close();
+        QueryEngine queryEngine = 
QueryEngineFactory.getExistingQueryController(temporaryStore);
+        if (queryEngine != null) {
+            queryEngine.shutdownNow();
+        }
+        SynchronizedHardReferenceQueueWithTimeout.stopStaleReferenceCleaner();
+        executorService.awaitTermination(20, TimeUnit.SECONDS);
+        temporaryStore = null;
+    }
+
+    /**
+     * Close the triple store used by the test that just finished.
+     */
+    @After
+    public void closeStore() {
+        if (store == null) {
+            return;
+        }
+        store.close();
+        store = null;
+    }
+}
diff --git 
a/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/AbstractRandomizedBlazegraphTestBase.java
 
b/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/AbstractRandomizedBlazegraphTestBase.java
index 86b168b..18b7a4a 100644
--- 
a/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/AbstractRandomizedBlazegraphTestBase.java
+++ 
b/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/AbstractRandomizedBlazegraphTestBase.java
@@ -1,62 +1,36 @@
 package org.wikidata.query.rdf.blazegraph;
 
-import static 
com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope.Scope.SUITE;
-
-import java.lang.Thread.UncaughtExceptionHandler;
 import java.math.BigInteger;
-import java.util.Properties;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeUnit;
 
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.runner.RunWith;
 import org.openrdf.model.Resource;
 import org.openrdf.model.URI;
 import org.openrdf.model.Value;
 import org.openrdf.model.impl.IntegerLiteralImpl;
 import org.openrdf.model.impl.URIImpl;
+import org.openrdf.query.MalformedQueryException;
+import org.openrdf.query.QueryEvaluationException;
+import org.openrdf.query.TupleQueryResult;
+import org.openrdf.query.algebra.evaluation.QueryBindingSet;
+import org.wikidata.query.rdf.common.uri.Ontology;
 import org.wikidata.query.rdf.common.uri.WikibaseUris;
 import org.wikidata.query.rdf.common.uri.WikibaseUris.PropertyType;
 
-import com.bigdata.cache.SynchronizedHardReferenceQueueWithTimeout;
-import com.bigdata.journal.TemporaryStore;
 import com.bigdata.rdf.model.BigdataStatement;
-import com.bigdata.rdf.store.ITripleStore;
-import com.bigdata.rdf.store.TempTripleStore;
-import com.carrotsearch.randomizedtesting.RandomizedRunner;
-import com.carrotsearch.randomizedtesting.RandomizedTest;
-import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
+import com.bigdata.rdf.sail.sparql.Bigdata2ASTSPARQLParser;
+import com.bigdata.rdf.sparql.ast.ASTContainer;
+import com.bigdata.rdf.sparql.ast.eval.ASTEvalHelper;
 
 /**
- * Randomized test that can create a triple store.
- *
- * <p>
- * We have to take a number of actions to make RandomizedRunner compatible with
- * Blazegraph:
- * <ul>
- * <li>Wwitch the ThreadLeakScope to SUITE because there are threads that
- * survive across tests
- * <li>Create a temporary store that is shared for all test methods that holds
- * multiple triple stores
- * <li>Create a triple store per test method
- * </ul>
+ * Base class for tests that need to interact with a temporary triple store. 
All
+ * the triple store creation logic lives in the parent class. This class just
+ * has convenient utilities to help with things like adding data and running
+ * queries.
  */
-@RunWith(RandomizedRunner.class)
-@ThreadLeakScope(SUITE)
-public class AbstractRandomizedBlazegraphTestBase extends RandomizedTest {
-    /**
-     * Holds all the triples stores. Initialized once per test class.
-     */
-    private static TemporaryStore temporaryStore;
+public class AbstractRandomizedBlazegraphTestBase extends 
AbstractRandomizedBlazegraphStorageTestCase {
     /**
      * Which uris this test uses.
      */
     private WikibaseUris uris = WikibaseUris.WIKIDATA;
-    /**
-     * Triple store for the current test method. Lazily initialized.
-     */
-    private ITripleStore store;
 
     /**
      * The uris this test uses.
@@ -66,19 +40,23 @@
     }
 
     /**
-     * Get a triple store. Lazily initialized once per test method.
+     * Run a query.
      */
-    protected ITripleStore store() {
-        if (store != null) {
-            return store;
+    protected TupleQueryResult query(String query) {
+        try {
+            ASTContainer astContainer = new 
Bigdata2ASTSPARQLParser(store()).parseQuery2(query, null);
+
+            return ASTEvalHelper.evaluateTupleQuery(store(), astContainer, new 
QueryBindingSet());
+        } catch (MalformedQueryException | QueryEvaluationException e) {
+            throw new RuntimeException(e);
         }
-        Properties properties = new Properties();
-        
properties.setProperty("com.bigdata.rdf.store.AbstractTripleStore.vocabularyClass",
-                WikibaseVocabulary.V001.class.getName());
-        
properties.setProperty("com.bigdata.rdf.store.AbstractTripleStore.inlineURIFactory",
-                WikibaseInlineUriFactory.class.getName());
-        store = new TempTripleStore(temporaryStore(), properties, null);
-        return store;
+    }
+
+    /**
+     * Add a triple to the store.
+     */
+    protected void add(Object s, Object p, Object o) {
+        store().addStatement((Resource) convert(s), (URI) convert(p), 
convert(o), null);
     }
 
     /**
@@ -105,12 +83,13 @@
         }
         if (o instanceof String) {
             String s = (String) o;
+            s = s.replaceFirst("^ontology:", Ontology.NAMESPACE);
             s = s.replaceFirst("^wdata:", uris.entityData());
             s = s.replaceFirst("^wd:", uris.entity());
             s = s.replaceFirst("^wds:", uris.statement());
             s = s.replaceFirst("^wdv:", uris.value());
             s = s.replaceFirst("^wdref:", uris.reference());
-            for (PropertyType p: PropertyType.values()) {
+            for (PropertyType p : PropertyType.values()) {
                 s = s.replaceFirst("^" + p.prefix() + ":", uris.property(p));
             }
             return new URIImpl(s);
@@ -121,49 +100,12 @@
         throw new RuntimeException("No idea how to convert " + o + " to a 
value.  Its a " + o.getClass() + ".");
     }
 
-    /**
-     * Get a TemporaryStore. Lazily initialized once per test class.
+    /*
+     * Initialize the Wikibase services including shutting off remote SERVICE
+     * calls and turning on label service calls.
      */
-    private static TemporaryStore temporaryStore() {
-        if (temporaryStore != null) {
-            return temporaryStore;
-        }
-        /*
-         * Initializing the temporary store replaces RandomizedRunner's
-         * painstakingly applied UncaughtExceptionHandler. That is bad so we
-         * replace it.
-         */
-        UncaughtExceptionHandler uncaughtExceptionHandler = 
Thread.getDefaultUncaughtExceptionHandler();
-        temporaryStore = new TemporaryStore();
-        Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
-        return temporaryStore;
-    }
-
-    /**
-     * Close the triple store used by the test that just finished.
-     */
-    @After
-    public void closeStore() {
-        if (store == null) {
-            return;
-        }
-        store.close();
-        store = null;
-    }
-
-    /**
-     * Close the temporary store used by this test.
-     * @throws InterruptedException if the executor service fails to await 
termination
-     */
-    @AfterClass
-    public static void closeTemporaryStore() throws InterruptedException {
-        if (temporaryStore == null) {
-            return;
-        }
-        ExecutorService executorService = temporaryStore.getExecutorService();
-        temporaryStore.close();
-        SynchronizedHardReferenceQueueWithTimeout.stopStaleReferenceCleaner();
-        executorService.awaitTermination(20, TimeUnit.SECONDS);
-        temporaryStore = null;
+    static {
+        WikibaseContextListener.initializeServices();
+        System.setProperty("ASTOptimizerClass", 
WikibaseOptimizers.class.getName());
     }
 }
diff --git 
a/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/label/LabelServiceUnitTest.java
 
b/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/label/LabelServiceUnitTest.java
new file mode 100644
index 0000000..fbd4441
--- /dev/null
+++ 
b/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/label/LabelServiceUnitTest.java
@@ -0,0 +1,176 @@
+package org.wikidata.query.rdf.blazegraph.label;
+
+import static org.hamcrest.Matchers.both;
+import static org.hamcrest.Matchers.containsString;
+import static org.wikidata.query.rdf.test.Matchers.assertResult;
+import static org.wikidata.query.rdf.test.Matchers.binds;
+
+import java.util.Locale;
+
+import org.apache.log4j.Logger;
+import org.junit.Test;
+import org.openrdf.model.impl.LiteralImpl;
+import org.openrdf.query.QueryEvaluationException;
+import org.openrdf.query.TupleQueryResult;
+import org.wikidata.query.rdf.blazegraph.AbstractRandomizedBlazegraphTestBase;
+import org.wikidata.query.rdf.common.uri.Ontology;
+import org.wikidata.query.rdf.common.uri.RDFS;
+import org.wikidata.query.rdf.common.uri.SKOS;
+import org.wikidata.query.rdf.common.uri.SchemaDotOrg;
+
+public class LabelServiceUnitTest extends AbstractRandomizedBlazegraphTestBase 
{
+    private static final Logger log = 
Logger.getLogger(LabelServiceUnitTest.class);
+
+    @Test
+    public void labelOverConstant() throws QueryEvaluationException {
+        simpleLabelLookupTestCase(null, "wd:Q123");
+    }
+
+    @Test
+    public void labelOverVariable() throws QueryEvaluationException {
+        add("ontology:dummy", "ontology:dummy", "wd:Q123");
+        simpleLabelLookupTestCase("ontology:dummy ontology:dummy ?o.", "?o");
+    }
+
+    @Test
+    public void chain() throws QueryEvaluationException {
+        add("ontology:dummy", "ontology:dummy", "wd:Q1");
+        add("wd:Q1", "ontology:dummy", "wd:Q2");
+        add("wd:Q2", "ontology:dummy", "wd:Q3");
+        add("wd:Q3", "ontology:dummy", "wd:Q4");
+        add("wd:Q4", "ontology:dummy", "wd:Q123");
+        simpleLabelLookupTestCase(
+                "ontology:dummy 
ontology:dummy/ontology:dummy/ontology:dummy/ontology:dummy/ontology:dummy 
?o.", "?o");
+    }
+
+    @Test
+    public void many() throws QueryEvaluationException {
+        for (int i = 1; i <= 10; i++) {
+            addSimpleLabels("Q" + i);
+            add("ontology:dummy", "ontology:dummy", "wd:Q" + i);
+        }
+        TupleQueryResult result = lookupLabel("ontology:dummy ontology:dummy 
?o", "en", "?o", "rdfs:label");
+        for (int i = 1; i <= 10; i++) {
+            assertTrue(result.hasNext());
+            assertThat(result.next(), binds("oLabel", new LiteralImpl("in en", 
"en")));
+        }
+        assertFalse(result.hasNext());
+    }
+
+    @Test
+    public void labelOverUnboundSubjectIsError() {
+        try {
+            lookupLabel(null, "en", "?s", "rdfs:label").next();
+            fail();
+        } catch (QueryEvaluationException e) {
+            assertThat(
+                    e.getMessage(),
+                    containsString("Refusing to lookup labels for unknown 
subject (s). That'd be way way way inefficient."));
+        }
+    }
+
+    @Test
+    public void noDotIsOkErrorMessage() {
+        try {
+            StringBuilder query = Ontology.prefix(new StringBuilder());
+            query.append("SELECT *\n");
+            query.append("WHERE {\n");
+            query.append("  SERVICE ontology:label {}\n");
+            query.append("}\n");
+            query(query.toString());
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("must provide the label 
service a list of languages"));
+        }
+    }
+
+    @Test
+    public void deeperServiceCall() {
+        add("ontology:dummy", "ontology:dummy", "wd:Q1");
+        add("wd:Q1", "ontology:dummy", "wd:Q123");
+        addSimpleLabels("Q123");
+        StringBuilder query = uris().prefixes(Ontology.prefix(new 
StringBuilder()));
+        query.append("SELECT ?pLabel\n");
+        query.append("WHERE {\n");
+        query.append("  ontology:dummy ontology:dummy ?s .\n");
+        query.append("  {\n");
+        query.append("    ?s ontology:dummy ?p .\n");
+        query.append("    SERVICE ontology:label.en {}\n");
+        query.append("  }\n");
+        query.append("}\n");
+        assertResult(query(query.toString()), binds("pLabel", "in en", "en"));
+    }
+
+    private void simpleLabelLookupTestCase(String extraQuery, String 
subjectInQuery) throws QueryEvaluationException {
+        addSimpleLabels("Q123");
+        slltcp(extraQuery, subjectInQuery, "en", "in en", "en", "alt label in 
en, alt label in en2", "en");
+        slltcp(extraQuery, subjectInQuery, "ru", "in ru", "ru", null, null);
+        slltcp(extraQuery, subjectInQuery, "dummy", "Q123", null, null, null);
+        slltcp(extraQuery, subjectInQuery, "dummy.en", "in en", "en", "alt 
label in en, alt label in en2", "en");
+        slltcp(extraQuery, subjectInQuery, "en.ru", "in en", "en", "alt label 
in en, alt label in en2", "en");
+        slltcp(extraQuery, subjectInQuery, "ru.de", "in ru", "ru", "alt label 
in de", "de");
+    }
+
+    private void slltcp(String extraQuery, String subjectInQuery, String 
language, String labelText,
+            String labelLanguage, String altLabelText, String 
altLabelLanguage) throws QueryEvaluationException {
+        assertResult(
+                lookupLabel(extraQuery, language, subjectInQuery, 
"rdfs:label", "skos:altLabel"),
+                both(binds(labelName(subjectInQuery, "rdfs:label"), labelText, 
labelLanguage)).and(
+                        binds(labelName(subjectInQuery, "skos:altLabel"), 
altLabelText, altLabelLanguage)));
+
+    }
+
+    private TupleQueryResult lookupLabel(String otherQuery, String 
inLanguages, String subject, String... labelTypes)
+            throws QueryEvaluationException {
+        if (inLanguages.indexOf(' ') >= 0) {
+            throw new IllegalArgumentException("Languages cannot contain a 
space or that'd make an invalid query.");
+        }
+        StringBuilder query = uris().prefixes(
+                
SchemaDotOrg.prefix(SKOS.prefix(RDFS.prefix(Ontology.prefix(new 
StringBuilder())))));
+        query.append("SELECT");
+        for (String labelType : labelTypes) {
+            query.append(" ?").append(labelName(subject, labelType));
+        }
+        query.append('\n');
+        query.append("WHERE {\n");
+        if (otherQuery != null) {
+            query.append(otherQuery).append("\n");
+        }
+        query.append("  SERVICE ontology:label.").append(inLanguages).append(" 
{\n");
+        if (subject.contains(":") || rarely()) {
+            // We rarely explicitly specify the labels to load
+            for (String labelType : labelTypes) {
+                query.append("    ").append(subject).append(" 
").append(labelType).append(" ?")
+                        .append(labelName(subject, labelType)).append(" .\n");
+            }
+        }
+        query.append("  }\n");
+        query.append("}\n");
+        if (log.isDebugEnabled()) {
+            log.debug("Query:  " + query);
+        }
+        return query(query.toString());
+    }
+
+    private void addSimpleLabels(String entity) {
+        for (String language : new String[] {"en", "de", "ru"}) {
+            add("wd:" + entity, RDFS.LABEL, new LiteralImpl("in " + language, 
language));
+        }
+        add("wd:" + entity, SKOS.ALT_LABEL, new LiteralImpl("alt label in en", 
"en"));
+        add("wd:" + entity, SKOS.ALT_LABEL, new LiteralImpl("alt label in 
en2", "en"));
+        add("wd:" + entity, SKOS.ALT_LABEL, new LiteralImpl("alt label in de", 
"de"));
+        for (String language : new String[] {"en", "de", "ru"}) {
+            add("wd:" + entity, SchemaDotOrg.DESCRIPTION, new 
LiteralImpl("description in " + language, language));
+        }
+    }
+
+    private String labelName(String subjectName, String labelType) {
+        int start = labelType.indexOf(':') + 1;
+        if (subjectName.contains(":")) {
+            return labelType.substring(start);
+        }
+        return subjectName.substring(1) + labelType.substring(start, start + 
1).toUpperCase(Locale.ROOT)
+                + labelType.substring(start + 1);
+    }
+
+}
diff --git 
a/common/src/main/java/org/wikidata/query/rdf/common/uri/Ontology.java 
b/common/src/main/java/org/wikidata/query/rdf/common/uri/Ontology.java
index 2b20ea6..a0fae60 100644
--- a/common/src/main/java/org/wikidata/query/rdf/common/uri/Ontology.java
+++ b/common/src/main/java/org/wikidata/query/rdf/common/uri/Ontology.java
@@ -72,6 +72,11 @@
      * them.
      */
     public static final String DEPRECATED_RANK = NAMESPACE + "DeprecatedRank";
+    /**
+     * Prefix for the label service. Not used in that data - just for SPARQL
+     * queries.
+     */
+    public static final String LABEL = NAMESPACE + "label";
 
     /**
      * Predicates used to describe a time.
diff --git a/common/src/main/java/org/wikidata/query/rdf/common/uri/RDFS.java 
b/common/src/main/java/org/wikidata/query/rdf/common/uri/RDFS.java
index 9302dc9..021f7c4 100644
--- a/common/src/main/java/org/wikidata/query/rdf/common/uri/RDFS.java
+++ b/common/src/main/java/org/wikidata/query/rdf/common/uri/RDFS.java
@@ -18,7 +18,7 @@
     /**
      * Add the rdfs: prefix to the query.
      */
-    public static StringBuilder prefixes(StringBuilder query) {
+    public static StringBuilder prefix(StringBuilder query) {
         return query.append("PREFIX rdfs: <").append(NAMESPACE).append(">\n");
     }
 
diff --git a/common/src/main/java/org/wikidata/query/rdf/common/uri/SKOS.java 
b/common/src/main/java/org/wikidata/query/rdf/common/uri/SKOS.java
index 0795f4c..997b8b8 100644
--- a/common/src/main/java/org/wikidata/query/rdf/common/uri/SKOS.java
+++ b/common/src/main/java/org/wikidata/query/rdf/common/uri/SKOS.java
@@ -19,6 +19,13 @@
     public static final String ALT_LABEL = NAMESPACE + "altLabel";
 
     /**
+     * Adds the skos: prefix to the query.
+     */
+    public static StringBuilder prefix(StringBuilder query) {
+        return query.append("PREFIX skos: <").append(NAMESPACE).append(">\n");
+    }
+
+    /**
      * Utility class uncallable constructor.
      */
     private SKOS() {
diff --git a/dist/pom.xml b/dist/pom.xml
index ad277e9..d046346 100644
--- a/dist/pom.xml
+++ b/dist/pom.xml
@@ -29,7 +29,6 @@
     <dependency>
       <groupId>org.wikidata.query.rdf</groupId>
       <artifactId>common</artifactId>
-      <version>${project.parent.version}</version>
     </dependency>
     <dependency>
       <groupId>org.wikidata.query.rdf</groupId>
diff --git a/dist/src/config/web.xml b/dist/src/config/web.xml
index 222c477..7d0febd 100644
--- a/dist/src/config/web.xml
+++ b/dist/src/config/web.xml
@@ -55,11 +55,14 @@
    <param-name>queryTimeout</param-name>
    <param-value>30000</param-value>
   </context-param>
-- <context-param>
+- <!-- We can't use the builtin whitelist because it breaks label relation. 
But we enable our own whitelist so its all good. -->
+  <!--
+  <context-param>
    <description>List of allowed services.</description>
    <param-name>serviceWhitelist</param-name>
    <param-value>http://www.bigdata.com/rdf#describe</param-value>
   </context-param>
+  -->
   <listener>
    
<listener-class>org.wikidata.query.rdf.blazegraph.WikibaseContextListener</listener-class>
   </listener>
diff --git a/dist/src/script/runBlazegraph.sh b/dist/src/script/runBlazegraph.sh
index cb33766..6a64b5a 100755
--- a/dist/src/script/runBlazegraph.sh
+++ b/dist/src/script/runBlazegraph.sh
@@ -27,6 +27,7 @@
      -Dorg.eclipse.jetty.server.Request.maxFormContentSize=20000000 \
      -Dcom.bigdata.rdf.sparql.ast.QueryHints.analytic=true \
      
-Dcom.bigdata.rdf.sparql.ast.QueryHints.analyticMaxMemoryPerQuery=1073741824 \
+     -DASTOptimizerClass=org.wikidata.query.rdf.blazegraph.WikibaseOptimizers \
      -jar jetty-runner*.jar \
      --port $PORT \
      --path /$CONTEXT \
diff --git a/pom.xml b/pom.xml
index 1a6a72a..49237a4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,6 +13,7 @@
   <packaging>pom</packaging>
 
   <modules>
+    <module>testTools</module>
     <module>common</module>
     <module>blazegraph</module>
     <module>tools</module>
@@ -61,9 +62,9 @@
       <url>https://oss.sonatype.org/content/repositories/snapshots</url>
     </snapshotRepository>
     <repository>
-    <id>ossrh</id>
-    <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
-  </repository>
+      <id>ossrh</id>
+      <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
+    </repository>
   </distributionManagement>
 
   <build>
@@ -190,20 +191,19 @@
                 </excludes>
                 <!-- Use as many JVMs as the test runner wants to use. -->
                 <parallelism>auto</parallelism>
-                <!-- The listeners and balancers elements do three things:
-                  1. Control how output is formatted during tests
-                  2. Dump the report to the right spot
-                  3. Log an "execution-hints" file that is used to balance the 
load when tests are forked.  -->
+                <!-- The listeners and balancers elements do three things: 1. 
Control how output is formatted during tests 
+                  2. Dump the report to the right spot 3. Log an 
"execution-hints" file that is used to balance the load when tests are forked. 
-->
                 <listeners>
                   <report-ant-xml mavenExtensions="true" 
dir="${project.build.directory}/surefire-reports" />
                   <report-text showThrowable="true" showStackTraces="true" 
showOutput="onerror"
                     showStatusOk="false" showStatusError="true" 
showStatusFailure="true" showStatusIgnored="true"
                     showSuiteSummary="true" timestamps="true" />
-                  <report-execution-times historyLength="20" 
file="${basedir}/.execution-hints-${project.version}.log" />
+                  <report-execution-times historyLength="20"
+                    file="${basedir}/.execution-hints-${project.version}.log" 
/>
                 </listeners>
                 <balancers>
                   <execution-times>
-                    <fileset dir="${basedir}" 
includes="${basedir}/.execution-hints-${project.version}.log"/>
+                    <fileset dir="${basedir}" 
includes="${basedir}/.execution-hints-${project.version}.log" />
                   </execution-times>
                 </balancers>
               </configuration>
@@ -258,7 +258,7 @@
               </goals>
               <configuration>
                 <quiet>true</quiet>
-                <!-- We don't want Java 8's strict javadoc checking so we use 
this trick borrowed from dropwizard to turn
+                <!-- We don't want Java 8's strict javadoc checking so we use 
this trick borrowed from dropwizard to turn 
                   it off -->
                 <additionalparam>${javadoc.doclint.none}</additionalparam>
               </configuration>
@@ -327,7 +327,7 @@
                 
<signaturesFile>${project.parent.basedir}/src/build/forbidden/core.txt</signaturesFile>
               </signaturesFiles>
               <excludes>
-                <!-- Some portions of the project need access to System.out 
and things so we contain them in inner classes
+                <!-- Some portions of the project need access to System.out 
and things so we contain them in inner classes 
                   of this form. -->
                 <exclude>**/*$ForbiddenOk**.class</exclude>
               </excludes>
@@ -351,36 +351,63 @@
     </plugins>
   </build>
 
+  <dependencyManagement>
+    <!-- Decalare the default parameters for some dependencies. -->
+    <dependencies>
+      <dependency>
+        <groupId>com.google.guava</groupId>
+        <artifactId>guava</artifactId>
+        <version>18.0</version>
+      </dependency>
+      <dependency>
+        <groupId>org.wikidata.query.rdf</groupId>
+        <artifactId>common</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.openrdf.sesame</groupId>
+        <artifactId>sesame-query</artifactId>
+        <version>${sesame.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>com.bigdata</groupId>
+        <artifactId>bigdata</artifactId>
+        <version>${blazegraph.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.hamcrest</groupId>
+        <artifactId>hamcrest-all</artifactId>
+        <version>1.3</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>junit</groupId>
+        <artifactId>junit</artifactId>
+        <version>4.12</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>com.carrotsearch.randomizedtesting</groupId>
+        <artifactId>randomizedtesting-runner</artifactId>
+        <version>2.1.13</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.mockito</groupId>
+        <artifactId>mockito-all</artifactId>
+        <version>1.9.5</version>
+        <scope>test</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
   <dependencies>
-    <dependency>
-      <!-- Everyone needs Guava! -->
-      <groupId>com.google.guava</groupId>
-      <artifactId>guava</artifactId>
-      <version>18.0</version>
-    </dependency>
-    <dependency>
-      <groupId>org.hamcrest</groupId>
-      <artifactId>hamcrest-all</artifactId>
-      <version>1.3</version>
-      <scope>test</scope>
-    </dependency>
     <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
-      <version>4.12</version>
-      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>com.carrotsearch.randomizedtesting</groupId>
       <artifactId>randomizedtesting-runner</artifactId>
-      <version>2.1.13</version>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.mockito</groupId>
-      <artifactId>mockito-all</artifactId>
-      <version>1.9.5</version>
-      <scope>test</scope>
     </dependency>
   </dependencies>
 
@@ -492,25 +519,25 @@
       </build>
     </profile>
     <profile>
-    <id>release</id>
-       <build>
-         <plugins>
-        <plugin>
-             <groupId>org.apache.maven.plugins</groupId>
-             <artifactId>maven-gpg-plugin</artifactId>
-             <version>1.5</version>
-             <executions>
-               <execution>
-                 <id>sign-artifacts</id>
-                 <phase>verify</phase>
-                 <goals>
-                   <goal>sign</goal>
-                 </goals>
-               </execution>
-             </executions>
-           </plugin>
-         </plugins>
-       </build>
+      <id>release</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-gpg-plugin</artifactId>
+            <version>1.5</version>
+            <executions>
+              <execution>
+                <id>sign-artifacts</id>
+                <phase>verify</phase>
+                <goals>
+                  <goal>sign</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
     </profile>
   </profiles>
 </project>
diff --git a/src/build/forbidden/all.txt b/src/build/forbidden/all.txt
index 24f4d6b..c2a4111 100644
--- a/src/build/forbidden/all.txt
+++ b/src/build/forbidden/all.txt
@@ -3,7 +3,3 @@
 @defaultMessage Convert to URI
 java.net.URL#getPath()
 java.net.URL#getFile()
-
-@defaultMessage These throw out the stack trace so are almost always wrong
-java.lang.Throwable#getMessage()
-java.lang.Throwable#toString()
diff --git a/src/build/forbidden/core.txt b/src/build/forbidden/core.txt
index 568c3c0..8f4926b 100644
--- a/src/build/forbidden/core.txt
+++ b/src/build/forbidden/core.txt
@@ -33,8 +33,9 @@
 @defaultMessage Please do not try to stop the world
 java.lang.System#gc()
 
-@defaultMessage Use Long.compare instead we are on Java7
-com.google.common.primitives.Longs#compare(long,long)
+# Disabled for a bit because it causes compilation to fail on things that 
don't depend on guava
+#@defaultMessage Use Long.compare instead we are on Java7
+#com.google.common.primitives.Longs#compare(long,long)
 
 @defaultMessage Use Channels.* methods to write to channels. Do not write 
directly.
 java.nio.channels.WritableByteChannel#write(java.nio.ByteBuffer)
@@ -45,3 +46,7 @@
 java.nio.channels.ScatteringByteChannel#read(java.nio.ByteBuffer[])
 java.nio.channels.ScatteringByteChannel#read(java.nio.ByteBuffer[], int, int)
 java.nio.channels.FileChannel#read(java.nio.ByteBuffer, long)
+
+@defaultMessage These throw out the stack trace so are almost always wrong
+java.lang.Throwable#getMessage()
+java.lang.Throwable#toString()
diff --git a/testTools/pom.xml b/testTools/pom.xml
new file mode 100644
index 0000000..870c78f
--- /dev/null
+++ b/testTools/pom.xml
@@ -0,0 +1,81 @@
+<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>
+  <parent>
+    <groupId>org.wikidata.query.rdf</groupId>
+    <artifactId>parent</artifactId>
+    <version>0.0.3-SNAPSHOT</version>
+  </parent>
+  <artifactId>testTools</artifactId>
+  <packaging>jar</packaging>
+
+  <name>Wikidata Query RDF Testing Tools</name>
+  <description>Tools for testing the rest of the project.</description>
+  <licenses>
+    <license>
+      <name>The Apache Software License, Version 2.0</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.wikidata.query.rdf</groupId>
+      <artifactId>common</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.openrdf.sesame</groupId>
+      <artifactId>sesame-query</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-all</artifactId>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.carrotsearch.randomizedtesting</groupId>
+      <artifactId>randomizedtesting-runner</artifactId>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-all</artifactId>
+      <scope>compile</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+      </plugin>
+      <plugin>
+        <groupId>com.carrotsearch.randomizedtesting</groupId>
+        <artifactId>junit4-maven-plugin</artifactId>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-javadoc-plugin</artifactId>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/testTools/src/main/java/org/wikidata/query/rdf/test/Matchers.java 
b/testTools/src/main/java/org/wikidata/query/rdf/test/Matchers.java
new file mode 100644
index 0000000..fc3f466
--- /dev/null
+++ b/testTools/src/main/java/org/wikidata/query/rdf/test/Matchers.java
@@ -0,0 +1,189 @@
+package org.wikidata.query.rdf.test;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.wikidata.query.rdf.test.StatementHelper.uri;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.openrdf.model.Literal;
+import org.openrdf.model.Statement;
+import org.openrdf.model.URI;
+import org.openrdf.model.impl.LiteralImpl;
+import org.openrdf.query.Binding;
+import org.openrdf.query.BindingSet;
+import org.openrdf.query.QueryEvaluationException;
+import org.openrdf.query.TupleQueryResult;
+import org.wikidata.query.rdf.common.uri.WikibaseUris;
+import org.wikidata.query.rdf.common.uri.WikibaseUris.PropertyType;
+
+/**
+ * Useful matchers for RDF.
+ */
+public final class Matchers {
+    /**
+     * Check a binding to a uri.
+     */
+    public static Matcher<BindingSet> binds(String name, String value) {
+        if (value.startsWith("P")) {
+            value = WikibaseUris.WIKIDATA.property(PropertyType.CLAIM) + value;
+        }
+        return new BindsMatcher<URI>(name, equalTo(uri(value)));
+    }
+
+    /**
+     * Check binding to specific class.
+     */
+    public static Matcher<BindingSet> binds(String name, Class<?> value) {
+        return new BindsMatcher<Object>(name, instanceOf(value));
+    }
+
+    /**
+     * Check a binding to a value.
+     */
+    public static <V> Matcher<BindingSet> binds(String name, V value) {
+        return new BindsMatcher<V>(name, equalTo(value));
+    }
+
+    /**
+     * Check a binding to a language string.
+     */
+    public static Matcher<BindingSet> binds(String name, String str, String 
language) {
+        if (str == null && language == null) {
+            return notBinds(name);
+        }
+        return new BindsMatcher<Literal>(name, equalTo((Literal) new 
LiteralImpl(str, language)));
+    }
+
+    /**
+     * Check that a binding isn't bound.
+     */
+    public static Matcher<BindingSet> notBinds(String name) {
+        return new NotBindsMatcher(name);
+    }
+
+    /**
+     * Construct subject, predicate, and object matchers for a bunch of
+     * statements.
+     */
+    @SuppressWarnings("unchecked")
+    public static Matcher<BindingSet>[] 
subjectPredicateObjectMatchers(Iterable<Statement> statements) {
+        List<Matcher<? super BindingSet>> matchers = new ArrayList<>();
+        for (Statement statement : statements) {
+            matchers.add(allOf(//
+                    binds("s", statement.getSubject()), //
+                    binds("p", statement.getPredicate()), //
+                    binds("o", statement.getObject()) //
+            ));
+        }
+        return (Matcher<BindingSet>[]) matchers.toArray(new 
Matcher<?>[matchers.size()]);
+    }
+
+    /**
+     * Assert that a result set contains some result bindings. Not a matcher
+     * because it modifies the result by iterating it.
+     */
+    @SafeVarargs
+    public static void assertResult(TupleQueryResult result, 
Matcher<BindingSet>... bindingMatchers) {
+        try {
+            int position = 0;
+            for (Matcher<BindingSet> bindingMatcher : bindingMatchers) {
+                assertTrue("There should be at least " + position + " 
results", result.hasNext());
+                assertThat(result.next(), bindingMatcher);
+                position++;
+            }
+            assertFalse("There should be no more than " + position + " 
result", result.hasNext());
+            result.close();
+        } catch (QueryEvaluationException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Checks bindings.
+     *
+     * @param <V> type of the binding result
+     */
+    private static class BindsMatcher<V> extends TypeSafeMatcher<BindingSet> {
+        /**
+         * Name of the binding to check.
+         */
+        private final String name;
+        /**
+         * Delegate matcher for the bound value.
+         */
+        private final Matcher<V> valueMatcher;
+
+        public BindsMatcher(String name, Matcher<V> valueMatcher) {
+            this.name = name;
+            this.valueMatcher = valueMatcher;
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText("contains a binding 
from").appendValue(name).appendText("to")
+                    .appendDescriptionOf(valueMatcher);
+        }
+
+        @Override
+        protected void describeMismatchSafely(BindingSet item, Description 
mismatchDescription) {
+            Binding binding = item.getBinding(name);
+            if (binding == null) {
+                mismatchDescription.appendText("but did not contain such a 
binding");
+                return;
+            }
+            mismatchDescription.appendText("instead it was bound 
to").appendValue(binding.getValue());
+        }
+
+        @Override
+        protected boolean matchesSafely(BindingSet item) {
+            Binding binding = item.getBinding(name);
+            if (binding == null) {
+                return false;
+            }
+            return valueMatcher.matches(binding.getValue());
+        }
+    }
+
+    /**
+     * Checks that a name isn't bound.
+     */
+    private static class NotBindsMatcher extends TypeSafeMatcher<BindingSet> {
+        /**
+         * Name of the binding to check.
+         */
+        private final String name;
+
+        public NotBindsMatcher(String name) {
+            this.name = name;
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText("does not contain a binding 
for").appendValue(name);
+        }
+
+        @Override
+        protected void describeMismatchSafely(BindingSet item, Description 
mismatchDescription) {
+            Binding binding = item.getBinding(name);
+            mismatchDescription.appendText("instead it was bound 
to").appendValue(binding.getValue());
+        }
+
+        @Override
+        protected boolean matchesSafely(BindingSet item) {
+            return item.getBinding(name) == null;
+        }
+    }
+
+    private Matchers() {
+        // Utility constructor
+    }
+}
diff --git 
a/tools/src/test/java/org/wikidata/query/rdf/tool/StatementHelper.java 
b/testTools/src/main/java/org/wikidata/query/rdf/test/StatementHelper.java
similarity index 98%
rename from tools/src/test/java/org/wikidata/query/rdf/tool/StatementHelper.java
rename to 
testTools/src/main/java/org/wikidata/query/rdf/test/StatementHelper.java
index fb44b4c..af81124 100644
--- a/tools/src/test/java/org/wikidata/query/rdf/tool/StatementHelper.java
+++ b/testTools/src/main/java/org/wikidata/query/rdf/test/StatementHelper.java
@@ -1,4 +1,4 @@
-package org.wikidata.query.rdf.tool;
+package org.wikidata.query.rdf.test;
 
 import static com.carrotsearch.randomizedtesting.RandomizedTest.randomInt;
 import static 
com.carrotsearch.randomizedtesting.RandomizedTest.randomIntBetween;
diff --git a/tools/pom.xml b/tools/pom.xml
index f71395c..1804527 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -21,9 +21,12 @@
 
   <dependencies>
     <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+    <dependency>
       <groupId>org.wikidata.query.rdf</groupId>
       <artifactId>common</artifactId>
-      <version>${project.parent.version}</version>
     </dependency>
     <dependency>
       <groupId>com.lexicalscope.jewelcli</groupId>
@@ -68,7 +71,6 @@
     <dependency>
       <groupId>org.openrdf.sesame</groupId>
       <artifactId>sesame-query</artifactId>
-      <version>${sesame.version}</version>
     </dependency>
     <dependency>
       <groupId>io.dropwizard.metrics</groupId>
@@ -82,9 +84,15 @@
       <version>2.1.1</version>
     </dependency>
     <dependency>
-       <groupId>org.apache.commons</groupId>
-       <artifactId>commons-lang3</artifactId>
-       <version>3.4</version>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+      <version>3.4</version>
+    </dependency>
+    <dependency>
+      <groupId>org.wikidata.query.rdf</groupId>
+      <artifactId>testTools</artifactId>
+      <version>${project.parent.version}</version>
+      <scope>test</scope>
     </dependency>
   </dependencies>
 
@@ -183,6 +191,10 @@
                   
<name>org.eclipse.jetty.server.Request.maxFormContentSize</name>
                   <value>20000000</value>
                 </property>
+                <property>
+                  <name>ASTOptimizerClass</name>
+                  
<value>org.wikidata.query.rdf.blazegraph.WikibaseOptimizers</value>
+                </property>
               </properties>
             </configuration>
           </execution>
diff --git 
a/tools/src/main/java/org/wikidata/query/rdf/tool/wikibase/WikibaseRepository.java
 
b/tools/src/main/java/org/wikidata/query/rdf/tool/wikibase/WikibaseRepository.java
index 9ab23f3..5e25b06 100644
--- 
a/tools/src/main/java/org/wikidata/query/rdf/tool/wikibase/WikibaseRepository.java
+++ 
b/tools/src/main/java/org/wikidata/query/rdf/tool/wikibase/WikibaseRepository.java
@@ -1,7 +1,5 @@
 package org.wikidata.query.rdf.tool.wikibase;
 
-import static 
org.wikidata.query.rdf.tool.wikibase.WikibaseRepository.outputDateFormat;
-
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.net.URI;
diff --git 
a/tools/src/test/java/org/wikidata/query/rdf/tool/ExpandedStatementBuilder.java 
b/tools/src/test/java/org/wikidata/query/rdf/tool/ExpandedStatementBuilder.java
index a62184e..21f00a8 100644
--- 
a/tools/src/test/java/org/wikidata/query/rdf/tool/ExpandedStatementBuilder.java
+++ 
b/tools/src/test/java/org/wikidata/query/rdf/tool/ExpandedStatementBuilder.java
@@ -2,7 +2,7 @@
 
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.equalTo;
-import static org.wikidata.query.rdf.tool.StatementHelper.statement;
+import static org.wikidata.query.rdf.test.StatementHelper.statement;
 
 import java.util.ArrayList;
 import java.util.Collections;
diff --git 
a/tools/src/test/java/org/wikidata/query/rdf/tool/IOBlastingIntegrationTest.java
 
b/tools/src/test/java/org/wikidata/query/rdf/tool/IOBlastingIntegrationTest.java
index c93a855..6047ccc 100644
--- 
a/tools/src/test/java/org/wikidata/query/rdf/tool/IOBlastingIntegrationTest.java
+++ 
b/tools/src/test/java/org/wikidata/query/rdf/tool/IOBlastingIntegrationTest.java
@@ -1,8 +1,8 @@
 package org.wikidata.query.rdf.tool;
 
 import static org.hamcrest.Matchers.hasItems;
-import static 
org.wikidata.query.rdf.tool.Matchers.subjectPredicateObjectMatchers;
-import static 
org.wikidata.query.rdf.tool.StatementHelper.randomStatementsAbout;
+import static 
org.wikidata.query.rdf.test.Matchers.subjectPredicateObjectMatchers;
+import static 
org.wikidata.query.rdf.test.StatementHelper.randomStatementsAbout;
 import static org.wikidata.query.rdf.tool.TupleQueryResultHelper.toIterable;
 
 import java.util.ArrayList;
@@ -67,8 +67,8 @@
         private final Matcher<BindingSet>[] matchers;
 
         IOBlasterResults(Iterable<BindingSet> first, Matcher<BindingSet>[] 
second) {
-            this.results = first;
-            this.matchers = second;
+            results = first;
+            matchers = second;
         }
 
         Iterable<BindingSet> results() {
@@ -90,7 +90,7 @@
         private final RdfRepositoryForTesting rdfRepository;
 
         IOBlaster(String namespace) {
-            this.rdfRepository = new RdfRepositoryForTesting(namespace);
+            rdfRepository = new RdfRepositoryForTesting(namespace);
         }
 
         /**
diff --git a/tools/src/test/java/org/wikidata/query/rdf/tool/Matchers.java 
b/tools/src/test/java/org/wikidata/query/rdf/tool/Matchers.java
deleted file mode 100644
index 9212622..0000000
--- a/tools/src/test/java/org/wikidata/query/rdf/tool/Matchers.java
+++ /dev/null
@@ -1,116 +0,0 @@
-package org.wikidata.query.rdf.tool;
-
-import static org.hamcrest.Matchers.allOf;
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.instanceOf;
-import static org.wikidata.query.rdf.tool.StatementHelper.uri;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.hamcrest.Description;
-import org.hamcrest.Matcher;
-import org.hamcrest.TypeSafeMatcher;
-import org.openrdf.model.Statement;
-import org.openrdf.model.URI;
-import org.openrdf.query.Binding;
-import org.openrdf.query.BindingSet;
-import org.wikidata.query.rdf.common.uri.WikibaseUris;
-import org.wikidata.query.rdf.common.uri.WikibaseUris.PropertyType;
-
-/**
- * Useful matchers for RDF.
- */
-public final class Matchers {
-    /**
-     * Check a binding to a uri.
-     */
-    public static Matcher<BindingSet> binds(String name, String value) {
-        if (value.startsWith("P")) {
-            value = WikibaseUris.WIKIDATA.property(PropertyType.CLAIM) + value;
-        }
-        return new BindsMatcher<URI>(name, equalTo(uri(value)));
-    }
-
-    /**
-     * Check binding to specific class.
-     */
-    public static Matcher<BindingSet> binds(String name, Class<?> value) {
-        return new BindsMatcher<Object>(name, instanceOf(value));
-    }
-
-    /**
-     * Check a binding to a value.
-     */
-    public static <V> Matcher<BindingSet> binds(String name, V value) {
-        return new BindsMatcher<V>(name, equalTo(value));
-    }
-
-
-    /**
-     * Checks bindings.
-     *
-     * @param <V> type of the binding result
-     */
-    private static class BindsMatcher<V> extends TypeSafeMatcher<BindingSet> {
-        /**
-         * Name of the binding to check.
-         */
-        private final String name;
-        /**
-         * Delegate matcher for the bound value.
-         */
-        private final Matcher<V> valueMatcher;
-
-        public BindsMatcher(String name, Matcher<V> valueMatcher) {
-            this.name = name;
-            this.valueMatcher = valueMatcher;
-        }
-
-        @Override
-        public void describeTo(Description description) {
-            description.appendText("contains a binding 
from").appendValue(name).appendText("to")
-                    .appendDescriptionOf(valueMatcher);
-        }
-
-        @Override
-        protected void describeMismatchSafely(BindingSet item, Description 
mismatchDescription) {
-            Binding binding = item.getBinding(name);
-            if (binding == null) {
-                mismatchDescription.appendText("but did not contain such a 
binding");
-                return;
-            }
-            mismatchDescription.appendText("instead it was bound 
to").appendValue(binding.getValue());
-        }
-
-        @Override
-        protected boolean matchesSafely(BindingSet item) {
-            Binding binding = item.getBinding(name);
-            if (binding == null) {
-                return false;
-            }
-            return valueMatcher.matches(binding.getValue());
-        }
-    }
-
-    /**
-     * Construct subject, predicate, and object matchers for a bunch of
-     * statements.
-     */
-    @SuppressWarnings("unchecked")
-    public static Matcher<BindingSet>[] 
subjectPredicateObjectMatchers(Iterable<Statement> statements) {
-        List<Matcher<? super BindingSet>> matchers = new ArrayList<>();
-        for (Statement statement : statements) {
-            matchers.add(allOf(//
-                    binds("s", statement.getSubject()), //
-                    binds("p", statement.getPredicate()), //
-                    binds("o", statement.getObject()) //
-            ));
-        }
-        return (Matcher<BindingSet>[]) matchers.toArray(new 
Matcher<?>[matchers.size()]);
-    }
-
-    private Matchers() {
-        // Utility constructor
-    }
-}
diff --git 
a/tools/src/test/java/org/wikidata/query/rdf/tool/MultipleResultsQueryIntegrationTest.java
 
b/tools/src/test/java/org/wikidata/query/rdf/tool/MultipleResultsQueryIntegrationTest.java
index d6b29af..652d024 100644
--- 
a/tools/src/test/java/org/wikidata/query/rdf/tool/MultipleResultsQueryIntegrationTest.java
+++ 
b/tools/src/test/java/org/wikidata/query/rdf/tool/MultipleResultsQueryIntegrationTest.java
@@ -3,8 +3,8 @@
 import static org.hamcrest.Matchers.hasItem;
 import static org.hamcrest.Matchers.hasItems;
 import static org.hamcrest.Matchers.not;
-import static 
org.wikidata.query.rdf.tool.Matchers.subjectPredicateObjectMatchers;
-import static 
org.wikidata.query.rdf.tool.StatementHelper.randomStatementsAbout;
+import static 
org.wikidata.query.rdf.test.Matchers.subjectPredicateObjectMatchers;
+import static 
org.wikidata.query.rdf.test.StatementHelper.randomStatementsAbout;
 import static org.wikidata.query.rdf.tool.TupleQueryResultHelper.toIterable;
 
 import java.util.ArrayList;
diff --git 
a/tools/src/test/java/org/wikidata/query/rdf/tool/MungeIntegrationTest.java 
b/tools/src/test/java/org/wikidata/query/rdf/tool/MungeIntegrationTest.java
index 7bf806f..0e96c27 100644
--- a/tools/src/test/java/org/wikidata/query/rdf/tool/MungeIntegrationTest.java
+++ b/tools/src/test/java/org/wikidata/query/rdf/tool/MungeIntegrationTest.java
@@ -75,7 +75,7 @@
             }
         }
         assertTrue(rdfRepository().ask(
-                RDFS.prefixes(uris().prefixes(new StringBuilder()))
+                RDFS.prefix(uris().prefixes(new StringBuilder()))
                         .append("ASK { wd:Q10 rdfs:label \"Wikidata\"@en 
}").toString()));
         assertTrue(rdfRepository().ask(
                 SchemaDotOrg.prefix(Ontology.prefix(new StringBuilder()))
diff --git 
a/tools/src/test/java/org/wikidata/query/rdf/tool/WikibaseDateExtensionIntegrationTest.java
 
b/tools/src/test/java/org/wikidata/query/rdf/tool/WikibaseDateExtensionIntegrationTest.java
index 64a885f..a976c2a 100644
--- 
a/tools/src/test/java/org/wikidata/query/rdf/tool/WikibaseDateExtensionIntegrationTest.java
+++ 
b/tools/src/test/java/org/wikidata/query/rdf/tool/WikibaseDateExtensionIntegrationTest.java
@@ -1,7 +1,7 @@
 package org.wikidata.query.rdf.tool;
 
-import static org.wikidata.query.rdf.tool.Matchers.binds;
-import static org.wikidata.query.rdf.tool.StatementHelper.statement;
+import static org.wikidata.query.rdf.test.Matchers.binds;
+import static org.wikidata.query.rdf.test.StatementHelper.statement;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git 
a/tools/src/test/java/org/wikidata/query/rdf/tool/rdf/MungerUnitTest.java 
b/tools/src/test/java/org/wikidata/query/rdf/tool/rdf/MungerUnitTest.java
index b639c04..bcdaeb5 100644
--- a/tools/src/test/java/org/wikidata/query/rdf/tool/rdf/MungerUnitTest.java
+++ b/tools/src/test/java/org/wikidata/query/rdf/tool/rdf/MungerUnitTest.java
@@ -3,8 +3,8 @@
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.hasItem;
 import static org.hamcrest.Matchers.not;
-import static org.wikidata.query.rdf.tool.StatementHelper.siteLink;
-import static org.wikidata.query.rdf.tool.StatementHelper.statement;
+import static org.wikidata.query.rdf.test.StatementHelper.siteLink;
+import static org.wikidata.query.rdf.test.StatementHelper.statement;
 
 import java.math.BigInteger;
 import java.util.ArrayList;
@@ -27,7 +27,7 @@
 import org.wikidata.query.rdf.common.uri.SchemaDotOrg;
 import org.wikidata.query.rdf.common.uri.WikibaseUris;
 import org.wikidata.query.rdf.common.uri.WikibaseUris.PropertyType;
-import org.wikidata.query.rdf.tool.StatementHelper;
+import org.wikidata.query.rdf.test.StatementHelper;
 import org.wikidata.query.rdf.tool.rdf.Munger.BadSubjectException;
 
 import com.carrotsearch.randomizedtesting.RandomizedRunner;
diff --git 
a/tools/src/test/java/org/wikidata/query/rdf/tool/rdf/NormalizingRdfHandlerUnitTest.java
 
b/tools/src/test/java/org/wikidata/query/rdf/tool/rdf/NormalizingRdfHandlerUnitTest.java
index 6c60b48..47d5a10 100644
--- 
a/tools/src/test/java/org/wikidata/query/rdf/tool/rdf/NormalizingRdfHandlerUnitTest.java
+++ 
b/tools/src/test/java/org/wikidata/query/rdf/tool/rdf/NormalizingRdfHandlerUnitTest.java
@@ -10,7 +10,7 @@
 import org.wikidata.query.rdf.common.uri.RDF;
 
 import static org.junit.Assert.assertEquals;
-import static org.wikidata.query.rdf.tool.StatementHelper.statement;
+import static org.wikidata.query.rdf.test.StatementHelper.statement;
 
 public class NormalizingRdfHandlerUnitTest {
 
diff --git 
a/tools/src/test/java/org/wikidata/query/rdf/tool/rdf/RdfRepositoryIntegrationTest.java
 
b/tools/src/test/java/org/wikidata/query/rdf/tool/rdf/RdfRepositoryIntegrationTest.java
index bb84936..7d132e8 100644
--- 
a/tools/src/test/java/org/wikidata/query/rdf/tool/rdf/RdfRepositoryIntegrationTest.java
+++ 
b/tools/src/test/java/org/wikidata/query/rdf/tool/rdf/RdfRepositoryIntegrationTest.java
@@ -1,9 +1,9 @@
 package org.wikidata.query.rdf.tool.rdf;
 
 import static org.hamcrest.Matchers.allOf;
-import static org.wikidata.query.rdf.tool.Matchers.binds;
-import static org.wikidata.query.rdf.tool.StatementHelper.siteLink;
-import static org.wikidata.query.rdf.tool.StatementHelper.statement;
+import static org.wikidata.query.rdf.test.Matchers.binds;
+import static org.wikidata.query.rdf.test.StatementHelper.siteLink;
+import static org.wikidata.query.rdf.test.StatementHelper.statement;
 
 import java.math.BigInteger;
 import java.util.ArrayList;

-- 
To view, visit https://gerrit.wikimedia.org/r/206396
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I37df29ad442692d350fdd35cd17badb05170b416
Gerrit-PatchSet: 8
Gerrit-Project: wikidata/query/rdf
Gerrit-Branch: master
Gerrit-Owner: Manybubbles <[email protected]>
Gerrit-Reviewer: Jdouglas <[email protected]>
Gerrit-Reviewer: Manybubbles <[email protected]>
Gerrit-Reviewer: Smalyshev <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to