ApiTemplate.java
package org.wikidata.query.rdf.blazegraph.mwapi;
import static java.util.Objects.requireNonNull;
import static org.wikidata.query.rdf.blazegraph.mwapi.ApiTemplate.OutputVariable.Type.ORDINAL;
import static org.wikidata.query.rdf.blazegraph.mwapi.MWApiServiceFactory.paramNameToURI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.openrdf.model.URI;
import org.openrdf.model.impl.URIImpl;
import org.wikidata.query.rdf.common.uri.Ontology;
import org.wikidata.query.rdf.common.uri.UrisSchemeFactory;
import com.bigdata.bop.IVariable;
import com.bigdata.bop.IVariableOrConstant;
import com.bigdata.rdf.internal.IV;
import com.bigdata.rdf.sparql.ast.GraphPatternGroup;
import com.bigdata.rdf.sparql.ast.IGroupMemberNode;
import com.bigdata.rdf.sparql.ast.StatementPatternNode;
import com.bigdata.rdf.sparql.ast.TermNode;
import com.bigdata.rdf.sparql.ast.eval.ServiceParams;
import com.bigdata.rdf.sparql.ast.service.ServiceNode;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
* This class represents API template.
*/
@SuppressFBWarnings(value = "FCCD_FIND_CLASS_CIRCULAR_DEPENDENCY", justification = "low priority to fix")
public class ApiTemplate {
/**
* Set of fixed API parameters.
*/
private final Map<String, String> fixedParams;
/**
* Set of API parameters that should come from input vars.
*/
private final Set<String> inputVars;
/**
* Set of defaults for API parameters that were not bound.
*/
private final Map<String, String> defaults;
/**
* 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;
/**
* XPath to result items.
*/
private final String items;
/**
* Hidden ctor.
* Use fromJSON() to create the object.
*/
protected ApiTemplate(Map<String, String> fixedParams,
Set<String> inputVars, Map<String, String> defaults,
Map<String, String> outputVars, String items) {
this.fixedParams = fixedParams;
this.inputVars = inputVars;
this.defaults = defaults;
this.outputVars = outputVars;
this.items = items;
}
/**
* Create API template from JSON configuration.
*/
public static ApiTemplate fromJSON(JsonNode json) {
Map<String, String> fixedParams = new HashMap<>();
Set<String> inputVars = new HashSet<>();
Map<String, String> defaults = new HashMap<>();
Map<String, String> outputVars = new HashMap<>();
// Parse input params
final JsonNode params = json.get("params");
requireNonNull(params, "Missing params node");
params.fieldNames().forEachRemaining(paramName -> {
if (fixedParams.containsKey(paramName)
|| inputVars.contains(paramName)) {
throw new IllegalArgumentException(
"Repeated input parameter " + paramName);
}
JsonNode value = params.get(paramName);
// scalar value means fixed parameter
if (value.isValueNode()) {
fixedParams.put(paramName, value.asText());
return;
}
// otherwise it's a parameter
// FIXME: ignoring type for now
inputVars.add(paramName);
if (value.has("default")) {
defaults.put(paramName, value.get("default").asText());
}
});
// Parse output params
final JsonNode output = json.get("output");
requireNonNull(params, "Missing output node");
String items = output.get("items").asText();
final JsonNode vars = output.get("vars");
requireNonNull(vars, "Missing vars node");
vars.fieldNames().forEachRemaining(paramName -> {
if (inputVars.contains(paramName)
|| fixedParams.containsKey(paramName)) {
throw new IllegalArgumentException("Parameter " + paramName
+ " declared as both input and output");
}
outputVars.put(paramName, vars.get(paramName).asText());
});
return new ApiTemplate(ImmutableMap.copyOf(fixedParams),
ImmutableSet.copyOf(inputVars), ImmutableMap.copyOf(defaults),
ImmutableMap.copyOf(outputVars), items);
}
/**
* Get items XPath.
*/
public String getItemsPath() {
return items;
}
/**
* Check if parameter is required.
*/
public boolean isRequiredParameter(String name) {
return inputVars.contains(name);
}
/**
* Get call fixed parameters.
*/
public Map<String, String> getFixedParams() {
return fixedParams;
}
/**
* Find default for this parameter.
*
* @return Default value or null.
*/
public String getInputDefault(String name) {
return defaults.get(name);
}
/**
* Add input var from the service params to the map.
* @param vars Target Map
* @param varName Parameter name
* @param iVar Parameter node (can be null if it's pre-defined but not specified)
*/
private void addInputVar(Map<String, IVariableOrConstant> vars, String varName, TermNode iVar) {
if (iVar == null) {
if (!defaults.containsKey(varName)) {
// Param should have either binding or default
throw new IllegalArgumentException("Parameter " + varName + " 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(varName, null);
} else {
if (!iVar.isConstant() && !iVar.isVariable()) {
// Binding should be constant or var
throw new IllegalArgumentException("Parameter " + varName + " must be constant or variable");
}
vars.put(varName, iVar.getValueExpression());
}
}
/**
* Create list of bindings from input params to specific variables or constants.
* @param serviceParams Specific invocation params.
* @return Map of bindings, which has constant or variable from service params if bound, or null if not bound.
*/
public Map<String, IVariableOrConstant> getInputVars(final ServiceParams serviceParams) {
Map<String, IVariableOrConstant> vars = Maps.newHashMapWithExpectedSize(inputVars.size());
String prefix = paramNameToURI("").stringValue();
// Collect pre-defined vars
for (String entry : inputVars) {
addInputVar(vars, entry, serviceParams.get(paramNameToURI(entry), null));
}
// Now collect new vars
// TODO: think about how to better unite these two loops
serviceParams.iterator().forEachRemaining(param -> {
String paramNameFull = param.getKey().stringValue();
if (!paramNameFull.startsWith(prefix)) {
return;
}
String paramName = paramNameFull.substring(prefix.length());
if (param.getValue().size() > 1) {
throw new IllegalArgumentException("Parameter " + paramName + " is duplicated");
}
if (vars.containsKey(paramName)) {
// already taken care of
return;
}
addInputVar(vars, paramName, param.getValue().get(0));
});
return vars;
}
/**
* Create map of output variables from template and service params.
*/
public List<OutputVariable> getOutputVars(final ServiceNode serviceNode) {
List<OutputVariable> vars = new ArrayList<>(outputVars.size());
final GraphPatternGroup<IGroupMemberNode> group = serviceNode.getGraphPattern();
requireNonNull(serviceNode, "Group node is null?");
String prefix = paramNameToURI("").stringValue();
group.iterator().forEachRemaining(node -> {
// Ouptut nodes are:
// ?variable wikibase:output mwapi:title
// or:
// ?variable wikibase:output "x/path"
if (node instanceof StatementPatternNode) {
final StatementPatternNode sp = (StatementPatternNode) node;
if (sp.s().isVariable() && sp.o().isConstant() && sp.p().isConstant()) {
for (OutputVariable.Type varType : OutputVariable.Type.values()) {
if (varType.predicate.equals(sp.p().getValue())) {
IVariable v = (IVariable)sp.s().getValueExpression();
if (varType == ORDINAL) {
// Ordinal values ignore the object
vars.add(new OutputVariable(varType, v, "."));
break;
}
IV value = sp.o().getValueExpression().get();
if (value.isURI()) {
String paramName = value.stringValue().substring(prefix.length());
vars.add(new OutputVariable(varType, v, outputVars.get(paramName)));
} else {
vars.add(new OutputVariable(varType, v, value.stringValue()));
}
break;
}
}
}
}
});
return vars;
}
/**
* Variable in the output of the API.
*/
public static class OutputVariable {
/**
* Type of variable result.
*/
public enum Type {
/**
* Plain string var.
*/
STRING("apiOutput"),
/**
* Var transformed to URI.
*/
URI("apiOutputURI"),
/**
* Item ID.
*/
ITEM("apiOutputItem"),
/**
* Ordinal - i.e. place of the result in the list.
*/
ORDINAL("apiOrdinal");
/**
* Predicate used for this type.
*/
private final URI predicate;
Type(String predicate) {
this.predicate = new URIImpl(Ontology.NAMESPACE + predicate);
}
/**
* Get predicate.
* @return Predicate URI
*/
URI predicate() {
return predicate;
}
@Override
public String toString() {
return predicate.stringValue();
}
}
/**
* Original Blazegraph var.
*/
private final IVariable iVar;
/**
* Path expression to extract value from result.
* The path is relative to items in template.
* Currently XPath syntax is being used.
*/
private final String path;
/**
* Variable type.
* Can be just string, URI or item ID.
*/
private final Type type;
public OutputVariable(Type type, IVariable iVar, String xpath) {
this.iVar = iVar;
this.path = xpath;
this.type = type;
}
public OutputVariable(IVariable iVar, String xpath) {
this(Type.STRING, iVar, xpath);
}
/**
* Get associated variable.
*/
public IVariable getVar() {
return iVar;
}
/**
* Get path to this variable.
*/
public String getPath() {
return path;
}
/**
* Get associated variable name.
*/
public String getName() {
return iVar.getName();
}
@Override
public String toString() {
return getName() + "(" + getPath() + ")";
}
/**
* Is it the ordinal value?
*/
public boolean isOrdinal() {
return type == ORDINAL;
}
/**
* Would this variable produce an URI?
*/
public boolean isURI() {
return type != Type.STRING && type != ORDINAL;
}
/**
* Get URI value matching variable type.
*/
public URI getURI(String value) {
switch (type) {
case URI:
return new URIImpl(value);
case ITEM:
return new URIImpl(UrisSchemeFactory.getURISystem().entityIdToURI(value.toUpperCase(Locale.ROOT)));
default:
throw new IllegalArgumentException("Can not produce URI for non-URI type " + type);
}
}
}
}