GeoService.java

package org.wikidata.query.rdf.blazegraph.geo;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.jetty.client.HttpClient;
import org.openrdf.model.URI;
import org.openrdf.model.impl.URIImpl;
import org.wikidata.query.rdf.blazegraph.inline.literal.WKTSerializer;
import org.wikidata.query.rdf.common.uri.Ontology;

import com.bigdata.bop.IVariable;
import com.bigdata.bop.IVariableOrConstant;
import com.bigdata.rdf.model.BigdataURI;
import com.bigdata.rdf.model.BigdataValue;
import com.bigdata.rdf.model.BigdataValueFactory;
import com.bigdata.rdf.sparql.ast.DummyConstantNode;
import com.bigdata.rdf.sparql.ast.IGroupMemberNode;
import com.bigdata.rdf.sparql.ast.JoinGroupNode;
import com.bigdata.rdf.sparql.ast.StatementPatternNode;
import com.bigdata.rdf.sparql.ast.TermNode;
import com.bigdata.rdf.sparql.ast.eval.AbstractServiceFactory;
import com.bigdata.rdf.sparql.ast.eval.GeoSpatialServiceFactory;
import com.bigdata.rdf.sparql.ast.eval.ServiceParams;
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.bigdata.rdf.store.BD;
import com.bigdata.service.geospatial.GeoSpatial;
import com.bigdata.service.geospatial.GeoSpatialSearchException;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * Implements a service to do geospatial search.
 * Base class for geospatial search wrappers.
 */
@SuppressWarnings("checkstyle:classfanoutcomplexity")
@SuppressFBWarnings(value = "FCCD_FIND_CLASS_CIRCULAR_DEPENDENCY", justification = "This should probably be fixed at some point")
public abstract class GeoService extends AbstractServiceFactory {

    /**
     * Delegate Blazegraph geosearch service.
     */
    private final GeoSpatialServiceFactory blazegraphService;

    /**
     * wikibase:globe parameter name.
     */
    public static final URIImpl GLOBE_PARAM = new URIImpl(
            Ontology.NAMESPACE + "globe");

    public GeoService() {
        super();
        blazegraphService = new GeoSpatialServiceFactory();
    }

    /**
     * Register the service so it is recognized by Blazegraph.
     */
    public static void register() {
        final ServiceRegistry reg = ServiceRegistry.getInstance();

        reg.add(GeoAroundService.SERVICE_KEY, new GeoAroundService());
        reg.addWhitelistURL(GeoAroundService.SERVICE_KEY.toString());
        reg.add(GeoBoxService.SERVICE_KEY, new GeoBoxService());
        reg.addWhitelistURL(GeoBoxService.SERVICE_KEY.toString());
        reg.addWhitelistURL(GeoSpatial.SEARCH.toString());
    }

    @Override
    public IServiceOptions getServiceOptions() {
        return blazegraphService.getServiceOptions();
    }

    /**
     * Get service parameter by name.
     */
    protected TermNode getParam(ServiceParams serviceParams, URI paramName) {
        TermNode node = serviceParams.get(paramName, null);
        if (node == null) {
            throw new IllegalArgumentException("Parameter " + paramName
                    + " is required.");
        }
        return node;
    }

    /**
     * Create service parameters for delegate service call.
     */
    protected abstract JoinGroupNode buildServiceNode(ServiceCallCreateParams params,
            ServiceParams serviceParams);

    /**
     * Create globe node with appropriate value for coordSystem.
     */
    protected TermNode getGlobeNode(BigdataValueFactory vf, ServiceParams serviceParams) {
        final TermNode globeNode = serviceParams.get(GLOBE_PARAM, null);
        if (globeNode == null) {
            return new DummyConstantNode(vf.createLiteral(WKTSerializer.NO_GLOBE));
        }
        if (!globeNode.isConstant()) {
            // FIXME: add support for this
            throw new IllegalArgumentException("Non-constant globe value is not supported yet.");
        }
        BigdataValue v = globeNode.getValue();
        if (v instanceof BigdataURI) {
            WKTSerializer ser = new WKTSerializer();
            try {
                return new DummyConstantNode(vf.createLiteral(ser.trimCoordURI(v.stringValue())));
            } catch (GeoSpatialSearchException e) {
                // Unexpectedly wrong URI - still pass it along
                return globeNode;
            }
        }
        return globeNode;
    }

    /**
     * Extract pattern node from parameters.
     *
     * Pattern node looks like:
     *  ?place wdt:P625 ?location .
     * Both variables would be bound by the service.
     */
    protected StatementPatternNode getPatternNode(ServiceCallCreateParams params) {
        ServiceNode serviceNode = params.getServiceNode();
        if (serviceNode == null)
            throw new IllegalArgumentException();

        List<StatementPatternNode> patterns = getStatementPatterns(serviceNode);

        if (patterns.isEmpty()) {
            throw new IllegalArgumentException("This service requires arguments");
        }

        StatementPatternNode pattern = patterns.get(0);

        if (pattern == null) {
            throw new IllegalArgumentException();
        }

        if (!pattern.s().isVariable()) {
            throw new IllegalArgumentException(
                    "Search pattern subject must be a variable");
        }

        if (!pattern.p().isConstant()) {
            // FIXME: may be not necessary?
            throw new IllegalArgumentException(
                    "Search pattern predicate must be a constant");
        }

        if (!pattern.o().isVariable()) {
            throw new IllegalArgumentException(
                    "Search pattern object must be a variable");
        }

        return pattern;
    }

    @Override
    public BigdataServiceCall create(ServiceCallCreateParams params,
            ServiceParams serviceParams) {
        if (params == null)
            throw new IllegalArgumentException();

        final JoinGroupNode newGroup = buildServiceNode(params, serviceParams);
        final BigdataValueFactory vf = params.getTripleStore().getValueFactory();

        ServiceNode newServiceNode = new ServiceNode(
                new DummyConstantNode(vf.asValue(GeoSpatial.SEARCH)), newGroup);

        // Transfer hints
        newServiceNode.setQueryHints(params.getServiceNode().getQueryHints());

        // Call delegate service
        HttpClient client = params.getClientConnectionManager();
        return (BigdataServiceCall) ServiceRegistry.getInstance().toServiceCall(
                params.getTripleStore(), client,
                GeoSpatial.SEARCH, newServiceNode, params.getStats());
    }

    /**
     * Returns the statement patterns contained in the service node.
     */
    @SuppressFBWarnings(value = "OCP_OVERLY_CONCRETE_PARAMETER", justification = "ServiceNode is a good representation of the intent")
    protected List<StatementPatternNode> getStatementPatterns(final ServiceNode serviceNode) {

        final List<StatementPatternNode> statementPatterns = new ArrayList<>();

        for (IGroupMemberNode child : serviceNode.getGraphPattern()) {

            if (child instanceof StatementPatternNode) {
                statementPatterns.add((StatementPatternNode)child);
            } else {
                throw new GeoSpatialSearchException("Nested groups are not allowed.");
            }
        }

        return statementPatterns;
    }

    @Override
    public Set<IVariable<?>> getRequiredBound(final ServiceNode serviceNode) {
        /**
         * This method extracts exactly those variables that are incoming,
         * i.e. must be bound before executing the execution of the service.
         *
         * Those can be only in service parameters.
         */
        final Set<IVariable<?>> requiredBound = new HashSet<>();
        for (StatementPatternNode sp : getStatementPatterns(serviceNode)) {

            final TermNode subj = sp.s();
            final IVariableOrConstant<?> object = sp.o().getValueExpression();

            if (subj.isConstant()
                    && BD.SERVICE_PARAM.equals(subj.getValue())
                    && object instanceof IVariable<?>) {
                requiredBound.add((IVariable<?>) object); // the subject var is what we return
            }
        }

        return requiredBound;
    }
}