Smalyshev has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/349119 )

Change subject: [WIP] [DNM] Add Mediawiki API service
......................................................................

[WIP] [DNM] Add Mediawiki API service

Mediaiwki API is described by a template, which lists
inputs and outputs, and is invoked via a service:

  SERVICE wikibase:mwapi {
      bd:serviceParam wikibase:api "Categories" .
      bd:serviceParam :titles "Albert Einstein" .
      bd:serviceParam :category ?category .
      bd:serviceParam :title ?title .
  }

Change-Id: If0aa5c213e197f6b20b55092680930eb82d1a0a2
Bug: T148245
---
M blazegraph/pom.xml
M 
blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/WikibaseContextListener.java
M 
blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/geo/GeoBoxService.java
A 
blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/mwapi/ApiTemplate.java
A 
blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/mwapi/MWApiServiceCall.java
A 
blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/mwapi/MWApiServiceFactory.java
M dist/src/script/runBlazegraph.sh
M pom.xml
M tools/pom.xml
M tools/runBlazegraph.sh
A tools/src/test/resources/blazegraph/services.json
11 files changed, 713 insertions(+), 11 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/wikidata/query/rdf 
refs/changes/19/349119/1

diff --git a/blazegraph/pom.xml b/blazegraph/pom.xml
index 0ea96cc..4470840 100644
--- a/blazegraph/pom.xml
+++ b/blazegraph/pom.xml
@@ -20,6 +20,14 @@
 
   <dependencies>
     <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+    </dependency>
+    <dependency>
       <!-- Blazegraph needs http client to run services. -->
       <groupId>org.eclipse.jetty</groupId>
       <artifactId>jetty-client</artifactId>
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 a028935..e0012fc 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
@@ -20,6 +20,7 @@
 import org.wikidata.query.rdf.blazegraph.constraints.WikibaseNowBOp;
 import org.wikidata.query.rdf.blazegraph.geo.GeoService;
 import org.wikidata.query.rdf.blazegraph.label.LabelService;
+import org.wikidata.query.rdf.blazegraph.mwapi.MWApiServiceFactory;
 import org.wikidata.query.rdf.common.uri.GeoSparql;
 import org.wikidata.query.rdf.common.uri.OWL;
 import org.wikidata.query.rdf.common.uri.Ontology;
@@ -83,6 +84,7 @@
         reg.setWhitelistEnabled(true);
         LabelService.register();
         GeoService.register();
+        MWApiServiceFactory.register();
 
         // Whitelist services we like by default
         reg.addWhitelistURL(GASService.Options.SERVICE_KEY.toString());
diff --git 
a/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/geo/GeoBoxService.java
 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/geo/GeoBoxService.java
index a0d4e8f..5d6198b 100644
--- 
a/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/geo/GeoBoxService.java
+++ 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/geo/GeoBoxService.java
@@ -272,7 +272,7 @@
          */
         private final BigdataValueFactory vf;
 
-        public GeoBoxServiceCall(BigdataServiceCall wrappedCall, TermNode east,
+        GeoBoxServiceCall(BigdataServiceCall wrappedCall, TermNode east,
                 TermNode west, AbstractTripleStore kb) {
             this.wrappedCall = wrappedCall;
             this.east = east;
diff --git 
a/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/mwapi/ApiTemplate.java
 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/mwapi/ApiTemplate.java
new file mode 100644
index 0000000..9ac05eb
--- /dev/null
+++ 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/mwapi/ApiTemplate.java
@@ -0,0 +1,203 @@
+package org.wikidata.query.rdf.blazegraph.mwapi;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.bigdata.bop.IVariable;
+import com.bigdata.bop.IVariableOrConstant;
+import com.bigdata.rdf.sparql.ast.TermNode;
+import com.bigdata.rdf.sparql.ast.eval.ServiceParams;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import static 
org.wikidata.query.rdf.blazegraph.mwapi.MWApiServiceFactory.paramNameToURI;
+/**
+ * This class represents API template.
+ */
+public final class ApiTemplate {
+    /**
+     * Set of fixed API parameters.
+     */
+    private final Map<String, String> fixedParams = new HashMap<>();
+    /**
+     * Set of API parameters that should come from input vars.
+     * The value is the default.
+     */
+    private final Map<String, String> inputVars = new HashMap<>();
+    /**
+     * Set of API parameters that should be sent to output.
+     * The value is the XPath to find the value.
+     */
+    private final Map<String, String> outputVars = new HashMap<>();
+    /**
+     * XPath to result items.
+     */
+    private String items;
+    /**
+     * Hidden ctor.
+     * Use fromJSON() to create the object.
+     */
+    private ApiTemplate() {}
+
+    /**
+     * Create API template from JSON configuration.
+     * @param json
+     * @return
+     */
+    public static ApiTemplate fromJSON(JsonNode json) {
+        ApiTemplate template = new ApiTemplate();
+
+        // Parse input params
+        final JsonNode params = json.get("params");
+        for (String paramName: (Iterable<String>)() -> params.fieldNames()) {
+            if (template.fixedParams.containsKey(paramName)
+                    || template.inputVars.containsKey(paramName)) {
+                throw new IllegalArgumentException(
+                        "Repeated input parameter " + paramName);
+            }
+
+            JsonNode value = params.get(paramName);
+            // scalar value means fixed parameter
+            if (value.isValueNode()) {
+                template.fixedParams.put(paramName, value.asText());
+            }
+            // otherwise it's a parameter
+            // TODO: ignoring type for now
+            if (value.has("default")) {
+                template.inputVars.put(paramName, 
value.get("default").asText());
+            } else {
+                template.inputVars.put(paramName, null);
+            }
+        }
+
+        // Parse output params
+        final JsonNode output = json.get("output");
+        template.items = output.get("items").asText();
+        final JsonNode vars = output.get("vars");
+        for (String paramName: (Iterable<String>)() -> vars.fieldNames()) {
+            if (template.inputVars.containsKey(paramName)) {
+                throw new IllegalArgumentException("Parameter " + paramName + 
" declared as both input and output");
+            }
+            template.outputVars.put(paramName, vars.get(paramName).asText());
+        }
+
+        return template;
+    }
+
+    /**
+     * Get items XPath.
+     * @return
+     */
+    public String getItemsPath() {
+        return items;
+    }
+
+    /**
+     * Get call fixed parameters.
+     * @return
+     */
+    public Map<String, String> getFixedParams() {
+        return fixedParams;
+    }
+
+    /**
+     * Find default for this parameter.
+     * @param name
+     * @return Default value or null.
+     */
+    public String getInputDefault(String name) {
+        return inputVars.get(name);
+    }
+
+    /**
+     * Create list of bindings from input params to specific variables or 
constants.
+     * @param serviceParams Specific invocation params.
+     * @return
+     */
+    public Map<String, IVariableOrConstant> getInputVars(final ServiceParams 
serviceParams) {
+        Map<String, IVariableOrConstant> vars = new 
HashMap<>(inputVars.size());
+
+        for (Map.Entry<String, String> entry : inputVars.entrySet()) {
+            TermNode var = serviceParams.get(paramNameToURI(entry.getKey()), 
null);
+            if (var == null) {
+                if (entry.getValue() == null) {
+                    // Param should have either binding or default
+                    throw new IllegalArgumentException("Parameter " + 
entry.getKey() + " must be bound");
+                }
+                // If var is null but we have a default, put null there, 
service call will know
+                // how to handle it.
+                vars.put(entry.getKey(), null);
+            } else {
+                if (!var.isConstant() && !var.isVariable()) {
+                    // Binding should be constant or var
+                    throw new IllegalArgumentException("Parameter " + 
entry.getKey() + " must be constant or variable");
+                }
+                vars.put(entry.getKey(), var.getValueExpression());
+            }
+        }
+        return vars;
+    }
+
+    /**
+     * Create map of output variables from template and service params.
+     * @param serviceParams Specific invocation params.
+     * @return
+     */
+    public List<OutputVariable> getOutputVars(final ServiceParams 
serviceParams) {
+        List<OutputVariable> vars = new ArrayList<>(outputVars.size());
+        for (Map.Entry<String, String> entry : outputVars.entrySet()) {
+            TermNode varNode = 
serviceParams.get(paramNameToURI(entry.getKey()), null);
+            if (varNode == null) {
+                // It's always OK to ignore output vars
+                continue;
+            }
+            if (!varNode.isVariable()) {
+                throw new IllegalArgumentException("Output parameter " + 
entry.getKey() + " must be bound to a variable");
+            }
+            IVariable var = (IVariable)varNode.getValueExpression();
+            if (var.isAnonymous()) {
+                // Using anonymous is useless, but we'll let it pass.
+                continue;
+            }
+            vars.add(new OutputVariable(var, entry.getValue()));
+        }
+        return vars;
+    }
+
+    /**
+     * Variable in the output of the API.
+     */
+    public static class OutputVariable {
+        /**
+         * Original Blazegraph var.
+         */
+        private final IVariable var;
+        /**
+         * XPath expression to extract value from result.
+         */
+        private final String xpath;
+
+        public OutputVariable(IVariable var, String xpath) {
+            this.var = var;
+            this.xpath = xpath;
+        }
+
+        /**
+         * Get associated variable.
+         * @return
+         */
+        public IVariable getVar() {
+            return var;
+        }
+
+        /**
+         * Get associated variable name.
+         * @return
+         */
+        public String getName() {
+            return var.getName();
+        }
+    }
+
+}
diff --git 
a/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/mwapi/MWApiServiceCall.java
 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/mwapi/MWApiServiceCall.java
new file mode 100644
index 0000000..7b0a453
--- /dev/null
+++ 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/mwapi/MWApiServiceCall.java
@@ -0,0 +1,292 @@
+package org.wikidata.query.rdf.blazegraph.mwapi;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openrdf.model.Literal;
+import org.openrdf.model.impl.LiteralImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.wikidata.query.rdf.blazegraph.mwapi.ApiTemplate.OutputVariable;
+
+import com.bigdata.bop.Constant;
+import com.bigdata.bop.IBindingSet;
+import com.bigdata.bop.IVariable;
+import com.bigdata.bop.IVariableOrConstant;
+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.sparql.ast.service.BigdataServiceCall;
+import com.bigdata.rdf.sparql.ast.service.IServiceOptions;
+import com.bigdata.rdf.sparql.ast.service.MockIVReturningServiceCall;
+
+import cutthecrap.utils.striterators.ICloseableIterator;
+
+/**
+ * Instance of API service call.
+ */
+@SuppressWarnings({"rawtypes", "unchecked"})
+public class MWApiServiceCall implements MockIVReturningServiceCall, 
BigdataServiceCall {
+    private static final Logger log = 
LoggerFactory.getLogger(MWApiServiceCall.class);
+    /**
+     * Service call template.
+     */
+    private final ApiTemplate template;
+    /**
+     * List of input variable bindings.
+     */
+    private final Map<String, IVariableOrConstant> inputVars;
+    /**
+     * List of output variable bindings.
+     */
+    private final List<OutputVariable> outputVars;
+    /**
+     * HTTP connection.
+     */
+    private final HttpClient client;
+    /**
+     * The LexiconRelation for the TripleStore we're working with.
+     */
+    private final LexiconRelation lexiconRelation;
+
+    MWApiServiceCall(ApiTemplate template,
+            Map<String, IVariableOrConstant> inputVars,
+            List<OutputVariable> outputVars,
+            HttpClient client,
+            LexiconRelation lexiconRelation
+    ) {
+        this.template = template;
+        this.inputVars = inputVars;
+        this.outputVars = outputVars;
+        this.client = client;
+        this.lexiconRelation = lexiconRelation;
+    }
+
+    @Override
+    public IServiceOptions getServiceOptions() {
+        return MWApiServiceFactory.SERVICE_OPTIONS;
+    }
+
+    @Override
+    public ICloseableIterator<IBindingSet> call(IBindingSet[] bindingSets)
+            throws Exception {
+        return new MultiSearchIterator(bindingSets);
+    }
+
+    /**
+     * Get HTTP request for this particular query & binding.
+     * @param binding
+     * @return
+     */
+    private Request getHttpRequest(IBindingSet binding) {
+        // FIXME: real endpoint URL
+        Request request = 
client.newRequest("https://en.wikipedia.org/w/api.php";);
+        request.method(HttpMethod.GET);
+        // Using XML for now to use XPath on responses
+        request.param("format", "xml");
+        // Add fixed params
+        for (Map.Entry<String, String> param : 
template.getFixedParams().entrySet()) {
+            request.param(param.getKey(), param.getValue());
+        }
+        // Resolve variable params
+        for (Map.Entry<String, IVariableOrConstant> term : 
inputVars.entrySet()) {
+            String value = null;
+            IV boundValue = null;
+            if (term != null) {
+                boundValue = (IV)term.getValue().get(binding);
+            }
+            if (boundValue == null) {
+                // try default
+                value = template.getInputDefault(term.getKey());
+            } else {
+                value = boundValue.stringValue();
+            }
+            if (value == null) {
+                throw new IllegalArgumentException("Could not find binding for 
parameter " + term.getKey());
+            }
+            request.param(term.getKey(), value);
+        }
+
+        return request;
+    }
+
+    @Override
+    public List<IVariable<IV>> getMockVariables() {
+        List<IVariable<IV>> externalVars = new LinkedList<IVariable<IV>>();
+        for (OutputVariable v: outputVars) {
+            externalVars.add(v.getVar());
+        }
+        return externalVars;
+    }
+
+    /**
+     * A chunk of calls to resolve labels.
+     */
+    private class MultiSearchIterator 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;
+        /**
+         * Current search result.
+         */
+        private SearchResultsIterator searchResult;
+
+        MultiSearchIterator(IBindingSet[] bindingSets) {
+            this.bindingSets = bindingSets;
+            searchResult = doNextSearch();
+        }
+
+        @Override
+        public boolean hasNext() {
+            if (closed || i >= bindingSets.length) {
+                return false;
+            }
+
+            if (searchResult == null) {
+                return false;
+            }
+
+            if (searchResult.hasNext()) {
+                return true;
+            }
+
+            searchResult = doNextSearch();
+            if (searchResult == null) {
+                return false;
+            } else {
+                return searchResult.hasNext();
+            }
+        }
+
+        /**
+         * Produce next search results iterator. Skips over empty results.
+         * @return Result iterator or null if no more results.
+         */
+        private SearchResultsIterator doNextSearch() {
+            // Just in case, double check
+            if (closed || bindingSets == null || i >= bindingSets.length) {
+                searchResult = null;
+                return null;
+            }
+            SearchResultsIterator result;
+            do {
+                IBindingSet binding = bindingSets[i++];
+                result = doSearchFromBinding(binding);
+            } while (!result.hasNext() && i < bindingSets.length);
+            if (result.hasNext()) {
+                return result;
+            } else {
+                return null;
+            }
+        }
+
+        /**
+         * Execute search for one specific binding set.
+         * @param binding
+         * @return Search results iterator.
+         */
+        private SearchResultsIterator doSearchFromBinding(IBindingSet binding) 
{
+            final Request req = getHttpRequest(binding);
+            log.info("REQUEST: " + req.getQuery());
+            for (OutputVariable var: outputVars) {
+                binding.set(var.getVar(), new Constant(mock("TEST")));
+            }
+            return new SearchResultsIterator(new IBindingSet[] {binding});
+        }
+
+        /**
+         * Build a mock IV from a literal string.
+         */
+        private IV mock(String literalString) {
+            Literal literal = new LiteralImpl(literalString);
+            TermId mock = TermId.mockIV(VTE.LITERAL);
+            mock.setValue(lexiconRelation.getValueFactory().asValue(literal));
+            return mock;
+        }
+
+        @Override
+        public IBindingSet next() {
+            if (closed || i >= bindingSets.length || searchResult == null) {
+                return null;
+            }
+
+            if (searchResult.hasNext()) {
+                return searchResult.next();
+            }
+
+            searchResult = doNextSearch();
+            if (searchResult == null || !searchResult.hasNext()) {
+                return null;
+            } else {
+                return searchResult.next();
+            }
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void close() {
+            closed = true;
+        }
+    }
+
+    /**
+     * Simple iterator for search results.
+     */
+    private class SearchResultsIterator implements Iterator<IBindingSet> {
+        /**
+         * The index into the array of hits wrapped by this iterator.
+         */
+        private int i;
+        /**
+         * The array of hits wrapped by this iterator.
+         */
+        private final IBindingSet[] results;
+
+        SearchResultsIterator(final IBindingSet[] results) {
+            if (results == null) {
+                throw new IllegalArgumentException("Null result?");
+            }
+
+            this.results = results;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return i < results.length;
+        }
+
+        @Override
+        public IBindingSet next() {
+            return results[i++];
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public String toString() {
+            return "SearchResultsIterator{nhits=" + results.length + "} : " + 
results;
+        }
+    }
+}
diff --git 
a/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/mwapi/MWApiServiceFactory.java
 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/mwapi/MWApiServiceFactory.java
new file mode 100644
index 0000000..f22b3a4
--- /dev/null
+++ 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/mwapi/MWApiServiceFactory.java
@@ -0,0 +1,153 @@
+package org.wikidata.query.rdf.blazegraph.mwapi;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.io.IOUtils;
+import org.openrdf.model.URI;
+import org.openrdf.model.impl.URIImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.wikidata.query.rdf.common.uri.Ontology;
+
+import com.bigdata.rdf.sparql.ast.eval.AbstractServiceFactory;
+import com.bigdata.rdf.sparql.ast.eval.ServiceParams;
+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.ServiceCallCreateParams;
+import com.bigdata.rdf.sparql.ast.service.ServiceNode;
+import com.bigdata.rdf.sparql.ast.service.ServiceRegistry;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Service factory for calling out to Mediawiki API Services.
+ * Service call looks like:
+ *
+ *  SERVICE wikibase:mwapi {
+ *      bd:serviceParam wikibase:api "Categories" .
+ *      bd:serviceParam :titles "Albert Einstein" .
+ *      bd:serviceParam :category ?category .
+ *      bd:serviceParam :title ?title .
+ *  }
+ */
+public class MWApiServiceFactory extends AbstractServiceFactory {
+    private static final Logger log = 
LoggerFactory.getLogger(MWApiServiceFactory.class);
+
+    /**
+     * Options configuring this service as a native Blazegraph service.
+     */
+    public static final BigdataNativeServiceOptions SERVICE_OPTIONS = new 
BigdataNativeServiceOptions();
+    /**
+     * Service config.
+     */
+    private final Map<String, ApiTemplate> config = new HashMap<>();
+    /**
+     * The URI service key.
+     */
+    public static final URI SERVICE_KEY = new URIImpl(Ontology.NAMESPACE + 
"mwapi");
+    /**
+     * API type parameter name.
+     */
+    public static final URI API_KEY = new URIImpl(Ontology.NAMESPACE + "api");
+    /**
+     * Default service config filename.
+     */
+    public static final String CONFIG_DEFAULT = "mwservices.json";
+    /**
+     * Config file parameter.
+     */
+    public static final String CONFIG_NAME = 
MWApiServiceFactory.class.getName() + ".config";
+    /**
+     * Filename of the config.
+     */
+    public static final String CONFIG_FILE = System.getProperty(CONFIG_NAME, 
CONFIG_DEFAULT);
+
+    public MWApiServiceFactory() throws IOException {
+        log.info("Loading MWAPI service configuration from " + CONFIG_FILE);
+        loadJSONConfig(new InputStreamReader(new FileInputStream(CONFIG_FILE), 
StandardCharsets.UTF_8));
+        log.info("Registered " + config.size() + " services.");
+    }
+
+    /**
+     * @throws IOException
+     * Load config from JSON object.
+     * @param configReader
+     */
+    public void loadJSONConfig(Reader configReader) throws IOException {
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode node = mapper.readTree(IOUtils.toString(configReader));
+        for (String serviceName: (Iterable<String>)() -> node.fieldNames()) {
+            config.put(serviceName, 
ApiTemplate.fromJSON(node.get(serviceName)));
+        }
+    }
+
+    @Override
+    public IServiceOptions getServiceOptions() {
+        return SERVICE_OPTIONS;
+    }
+
+    /**
+     * Register the service so it is recognized by Blazegraph.
+     */
+    public static void register() {
+        final ServiceRegistry reg = ServiceRegistry.getInstance();
+        try {
+            reg.add(SERVICE_KEY, new MWApiServiceFactory());
+        } catch (IOException e) {
+            // Do not add to whitelist if init failed.
+            log.warn("MW Service registration failed: " + e);
+            return;
+        }
+        reg.addWhitelistURL(SERVICE_KEY.toString());
+    }
+
+    @Override
+    public BigdataServiceCall create(ServiceCallCreateParams params, final 
ServiceParams serviceParams) {
+        final ServiceNode serviceNode = params.getServiceNode();
+
+        if (serviceNode == null) {
+            throw new IllegalArgumentException();
+        }
+
+        final ApiTemplate template = getServiceTemplate(serviceParams);
+
+        return new MWApiServiceCall(template,
+                template.getInputVars(serviceParams),
+                template.getOutputVars(serviceParams),
+                params.getClientConnectionManager(),
+                params.getTripleStore().getLexiconRelation()
+                );
+    }
+
+    /**
+     * Extract service template name from params.
+     * @param serviceParams
+     * @return Service template
+     */
+    private ApiTemplate getServiceTemplate(final ServiceParams serviceParams) {
+        final String templateName = serviceParams.getAsString(API_KEY);
+        serviceParams.clear(API_KEY);
+        if (!config.containsKey(templateName)) {
+            throw new IllegalArgumentException(
+                    "Service name " + templateName + " not found in 
configuration");
+        }
+        return config.get(templateName);
+    }
+
+    /**
+     * Create predicate parameter URI from name.
+     * TODO: for now, identical to name, check if that works.
+     * @param name
+     * @return
+     */
+    public static URI paramNameToURI(String name) {
+        return new URIImpl(name);
+    }
+}
diff --git a/dist/src/script/runBlazegraph.sh b/dist/src/script/runBlazegraph.sh
index 3027837..6cd7419 100755
--- a/dist/src/script/runBlazegraph.sh
+++ b/dist/src/script/runBlazegraph.sh
@@ -62,6 +62,7 @@
      
-Dorg.wikidata.query.rdf.blazegraph.inline.literal.WKTSerializer.noGlobe=$DEFAULT_GLOBE
 \
      
-Dcom.bigdata.rdf.sail.webapp.client.RemoteRepository.maxRequestURLLength=7168 \
      
-Dcom.bigdata.rdf.sail.sparql.PrefixDeclProcessor.additionalDeclsFile=$DIR/prefixes.conf
 \
+     
-Dorg.wikidata.query.rdf.blazegraph.mwapi.MWApiServiceFactory.config=$DIR/mwservices.json
 \
      
-Dcom.bigdata.rdf.sail.webapp.client.HttpClientConfigurator=org.wikidata.query.rdf.blazegraph.ProxiedHttpConnectionFactory
 \
      -Dhttp.userAgent="${USER_AGENT}" \
      ${BLAZEGRAPH_OPTS} \
diff --git a/pom.xml b/pom.xml
index ad20dae..657f964 100644
--- a/pom.xml
+++ b/pom.xml
@@ -67,6 +67,7 @@
 
   <properties>
     <blazegraph.version>2.1.5-SNAPSHOT</blazegraph.version>
+    <jackson.version>2.2.3</jackson.version>
     <jetty.version>9.2.3.v20140905</jetty.version>
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <sesame.version>2.7.12</sesame.version>
@@ -99,6 +100,21 @@
         <type>pom</type>
       </dependency>
       <dependency>
+        <groupId>com.fasterxml.jackson.core</groupId>
+        <artifactId>jackson-annotations</artifactId>
+        <version>${jackson.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>com.fasterxml.jackson.core</groupId>
+        <artifactId>jackson-core</artifactId>
+        <version>${jackson.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>com.fasterxml.jackson.core</groupId>
+        <artifactId>jackson-databind</artifactId>
+        <version>${jackson.version}</version>
+      </dependency>
+      <dependency>
         <groupId>com.google.guava</groupId>
         <artifactId>guava</artifactId>
         <version>18.0</version>
diff --git a/tools/pom.xml b/tools/pom.xml
index a55ea6d..b13d3aa 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -263,24 +263,28 @@
                   
<value>org.wikidata.query.rdf.blazegraph.WikibaseOptimizers</value>
                 </property>
                 <property>
-                       
<name>org.wikidata.query.rdf.blazegraph.inline.literal.WKTSerializer.noGlobe</name>
-                       <value>2</value>
+                  
<name>org.wikidata.query.rdf.blazegraph.inline.literal.WKTSerializer.noGlobe</name>
+                  <value>2</value>
                 </property>
                 <property>
-                       
<name>com.bigdata.rdf.sail.sparql.PrefixDeclProcessor.additionalDeclsFile</name>
-                       
<value>${project.parent.basedir}/dist/src/script/prefixes.conf</value>
+                  
<name>com.bigdata.rdf.sail.sparql.PrefixDeclProcessor.additionalDeclsFile</name>
+                  
<value>${project.parent.basedir}/dist/src/script/prefixes.conf</value>
                 </property>
                 <property>
-                       
<name>com.bigdata.rdf.sail.webapp.client.HttpClientConfigurator</name>
-                       
<value>org.wikidata.query.rdf.blazegraph.ProxiedHttpConnectionFactory</value>
+                  
<name>com.bigdata.rdf.sail.webapp.client.HttpClientConfigurator</name>
+                  
<value>org.wikidata.query.rdf.blazegraph.ProxiedHttpConnectionFactory</value>
                 </property>
                 <property>
-                       <name>http.userAgent</name>
-                       <value>Wikidata Query Service (testing); 
https://query.wikidata.org/</value>
+                  <name>http.userAgent</name>
+                  <value>Wikidata Query Service (testing); 
https://query.wikidata.org/</value>
                 </property>
                 <property>
-                       <name>wikibaseServiceWhitelist</name>
-                       
<value>${project.basedir}/src/test/resources/blazegraph/whitelist.txt</value>
+                  <name>wikibaseServiceWhitelist</name>
+                  
<value>${project.basedir}/src/test/resources/blazegraph/whitelist.txt</value>
+                </property>
+                <property>
+                  
<name>org.wikidata.query.rdf.blazegraph.mwapi.MWApiServiceFactory.config</name>
+                  
<value>${project.basedir}/src/test/resources/blazegraph/services.json</value>
                 </property>
               </properties>
             </configuration>
diff --git a/tools/runBlazegraph.sh b/tools/runBlazegraph.sh
index d02e41d..b9e9346 100755
--- a/tools/runBlazegraph.sh
+++ b/tools/runBlazegraph.sh
@@ -1,3 +1,4 @@
 #!/bin/bash
 
+export MAVEN_OPTS="-Xdebug -Xnoagent -Djava.compiler=NONE 
-Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n"
 mvn -PrunBlazegraph generate-sources
diff --git a/tools/src/test/resources/blazegraph/services.json 
b/tools/src/test/resources/blazegraph/services.json
new file mode 100644
index 0000000..b4a93e5
--- /dev/null
+++ b/tools/src/test/resources/blazegraph/services.json
@@ -0,0 +1,22 @@
+{
+       "Categories": {
+               "params": {
+                       "action": "query",
+                       "prop": "categories",
+                       "titles": {
+                               "type": "list"
+                       },
+                       "cllimit": {
+                               "type": "int",
+                               "default": 500
+                       }
+               },
+               "output": {
+                       "items": "//api/query/pages/page/categories",
+                       "vars": {
+                               "category": "@title",
+                               "title": "//api/query/pages/page/@title"
+                       }
+               }
+       }
+}
\ No newline at end of file

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: If0aa5c213e197f6b20b55092680930eb82d1a0a2
Gerrit-PatchSet: 1
Gerrit-Project: wikidata/query/rdf
Gerrit-Branch: master
Gerrit-Owner: Smalyshev <smalys...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to