/** Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved. Contact: SYSTAP, LLC DBA Blazegraph 2501 Calvert ST NW #106 Washington, DC 20008 licenses@blazegraph.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* * Created on Mar 5, 2012 */ package com.bigdata.rdf.sparql.ast.service; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import org.apache.log4j.Logger; import org.openrdf.model.BNode; import org.openrdf.model.Value; import org.openrdf.query.Binding; import org.openrdf.query.BindingSet; import org.openrdf.query.impl.EmptyBindingSet; import com.bigdata.bop.IVariable; import com.bigdata.rdf.sail.webapp.client.AST2SPARQLUtil; /** * Utility class constructs a valid SPARQL query for a remote * <code>SPARQL 1.0</code> end point (no <code>BINDINGS</code> clause and does * not support SELECT expressions or BIND()). * <p> * Note: This class functions by adding * <code>FILTER (sameTerm(?var,bound-value))</code> instances for each variable * binding inside of the SERVICE's graph pattern. * <p> * Note: Unlike the {@link RemoteSparql11QueryBuilder}, this class does not need * to impose a correlation on variables which are bound to the same blank node * since we can communicate blank nodes within a <code>FILTER</code>. * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> * @version $Id$ */ public class RemoteSparql10QueryBuilder implements IRemoteSparqlQueryBuilder { private static final Logger log = Logger .getLogger(RemoteSparql10QueryBuilder.class); // private final ServiceNode serviceNode; /** The text "image" of the SERVICE clause. */ private final String exprImage; /** * The prefix declarations used within the SERVICE clause (from the original * query). */ private final Map<String, String> prefixDecls; private final AST2SPARQLUtil util; /** * The distinct variables "projected" by the SERVICE group graph pattern. * The order of this set is not important, but the variables must be * distinct. * */ private final Set<IVariable<?>> projectedVars; // /** // * This is a vectored implementation. // */ // @Override // public boolean isVectored() { // // return true; // // } /** * * @param serviceNode * The SERVICE clause. */ public RemoteSparql10QueryBuilder(final ServiceNode serviceNode) { if (serviceNode == null) throw new IllegalArgumentException(); // this.serviceNode = serviceNode; this.exprImage = serviceNode.getExprImage(); this.prefixDecls = serviceNode.getPrefixDecls(); this.projectedVars = serviceNode.getProjectedVars(); if (exprImage == null) throw new IllegalArgumentException(); if (projectedVars == null) throw new IllegalArgumentException(); this.util = new AST2SPARQLUtil(prefixDecls); } public String getSparqlQuery(BindingSet[] bindingSets) { // /* // * When true, there is only one binding set to be vectored and it is // * empty. // */ // // final boolean singleEmptyBindingSet; // /* * Normalize NO source solutions into a single empty binding set. */ if (bindingSets.length == 0) { bindingSets = new BindingSet[] { new EmptyBindingSet() }; // singleEmptyBindingSet = true; } else if (bindingSets.length == 1 && bindingSets[0].size() == 0) { // singleEmptyBindingSet = true; } else { // singleEmptyBindingSet = false; } final StringBuilder sb = new StringBuilder(); /* * Prefix declarations. * * Note: The prefix declarations need to be harvested and passed along * since we are using the text image of the SERVICE group graph pattern * in the WHERE clause. */ if (prefixDecls != null) { for (Map.Entry<String, String> e : prefixDecls.entrySet()) { sb.append("\n"); sb.append("prefix "); sb.append(e.getKey()); sb.append(":"); sb.append(" <"); sb.append(e.getValue()); sb.append(">"); sb.append("\n"); } } /* * SELECT clause. */ { sb.append("SELECT "); if (projectedVars.isEmpty()) { /* * Note: This is a dubious hack for openrdf federated query * testEmptyServiceBlock. Since there are no variables in the * service clause, it was sending an invalid SELECT expression. * It is now hacked to send a "*" instead. */ sb.append("*"); } else { for (IVariable<?> v : projectedVars) { sb.append(" ?"); sb.append(v.getName()); } } sb.append("\n"); } /* * WHERE clause. * * Note: This uses the actual SPARQL text image for the SERVICE's graph * pattern. * * Note: A FILTER is used to bind each variable. * * Note: If there are variables which are correlated through shared * variables (and there is only one solution to be vectored) then we * will impose a same-term constraint on those variables as part of the * generated query. */ { /* * Clip out the graph pattern from the SERVICE clause. * * Note: This leaves off the outer curly brackets in case we need to * add some FILTERS, BINDS, etc. into the WHERE clause. */ final int beginIndex = exprImage.indexOf("{") + 1; if (beginIndex < 0) throw new RuntimeException(); final int endIndex = exprImage.lastIndexOf("}"); if (endIndex < beginIndex) throw new RuntimeException(); final String tmp = exprImage.substring(beginIndex, endIndex); sb.append("WHERE {\n"); for (int k = 0; k < bindingSets.length; k++) { final BindingSet bset = bindingSets[k]; if (bindingSets.length > 1) { /* * UNION of SERVICE patterns. */ if (k == 0) { // Open the first UNION. sb.append("{\n"); } else { // Close / Open UNION. sb.append("\n} UNION {\n"); } } /* * Set of variables which are correlated through shared blank * nodes -or- null if there are no such correlated variables. */ final Map<BNode, Set<String/* vars */>> bnodes = getCorrelatedVarsMap(bset); if (bnodes != null) { /* * Impose a same-term constraint for all variables which are * bound to the same blank node. */ for (Set<String> sameTermVars : bnodes.values()) { final int nSameTerm = sameTermVars.size(); if (nSameTerm < 2) continue; final String[] names = sameTermVars .toArray(new String[nSameTerm]); sb.append("FILTER ("); for (int i = 1; i < names.length; i++) { if (i > 1) { sb.append(" &&"); } sb.append(" sameTerm( ?" + names[0] + ", ?" + names[i] + ")"); } sb.append(" ).\n"); } } for (Binding b : bset) { /* * Add FILTER to bind each (non-blank node) value. */ final String name = b.getName(); final Value v = b.getValue(); // Set<String> sameTermVars; // if (v instanceof BNode && bnodes != null // && ((sameTermVars = bnodes.get((BNode) v)) != null)) { if(!(v instanceof BNode)) { /* * Blank nodes are not permitted a function arguments. * Therefore we need to handle them through sameTerm() * for the correlated variables. * * Note: If there are no variables which are correlated * because of this blank node, then we just ignore this * blank node. An unbound variable and a blank node have * the same semantics. */ // final int nSameTerm = sameTermVars.size(); // if (nSameTerm < 2) // continue; // final String[] names = sameTermVars // .toArray(new String[nSameTerm]); // sb.append("FILTER ("); // for (int i = 1; i < names.length; i++) { // if (i > 1) { // sb.append(" &&"); // } // sb.append(" sameTerm( ?" + names[0] + ", ?" // + names[i] + ")"); // } // sb.append(" ).\n"); // /* // * Replace the entry with an empty set so we do not // * regenerate the FILTER when we see the other variables // * which are correlated with this binding. // */ // bnodes.put((BNode) v, (Set) Collections.emptySet()); // } else { /* * Blank nodes are not permitted a function arguments. * Therefore we need to handle them through sameTerm() * for the correlated variables (the code block above * handles this). */ sb.append("FILTER ("); sb.append(" sameTerm( ?" + name + ", " + util.toExternal(v) + ")"); sb.append(" ).\n"); } } sb.append(tmp); // append SERVICE's graph pattern. } // next solution if (bindingSets.length > 1) { sb.append("\n}\n"); // close the last UNION. } sb.append("\n}\n"); } // /* // * BINDINGS clause. // * // * Note: The BINDINGS clause is used to vector the SERVICE request. // * // * BINDINGS ?book ?title { (:book1 :title1) (:book2 UNDEF) } // */ // if (!singleEmptyBindingSet) { // // // Variables in a known stable order. // final LinkedHashSet<String> vars = getDistinctVars(bindingSets); // // sb.append("BINDINGS"); // // // Variable declarations. // { // // for (String v : vars) { // sb.append(" ?"); // sb.append(v); // } // } // // // Bindings. // { // sb.append(" {\n"); // // for (BindingSet bindingSet : bindingSets) { // sb.append("("); // for (String v : vars) { // sb.append(" "); // final Binding b = bindingSet.getBinding(v); // if (b == null) { // sb.append("UNDEF"); // } else { // final Value val = b.getValue(); // final String ext = util.toExternal(val); // sb.append(ext); // } // } // sb.append(" )"); // sb.append("\n"); // } // sb.append("}\n"); // } // // } final String q = sb.toString(); if (log.isInfoEnabled()) log.info("\n" + q); return q; } // /** // * {@inheritDoc} // * <p> // * This implementation returns it's argument. // */ // @Override // public BindingSet[] getSolutions(final BindingSet[] serviceSolutions) { // // return serviceSolutions; // // } /** * Return a correlated blank node / variables map. * <p> * Note: This is necessary because we can not have a blank node in a * FunctionCall in SPARQL. However, unlike with the BINDINGS clause, we have * to do it for each solution because we can vector more than one solution * involving correlated blank nodes. * * @return The correlated variable bindings map -or- <code>null</code> iff * there are no variables which are correlated through shared blank * nodes. */ static private Map<BNode, Set<String/* vars */>> getCorrelatedVarsMap( final BindingSet bindingSet) { Map<BNode, Set<String/* vars */>> bnodes = null; for (Binding b : bindingSet) { final Value v = b.getValue(); if (!(v instanceof BNode)) continue; if (bnodes == null) bnodes = new LinkedHashMap<BNode, Set<String>>(); final BNode bnd = (BNode) v; // Set of correlated variables. Set<String> cvars = bnodes.get(bnd); if (cvars == null) { bnodes.put(bnd, cvars = new LinkedHashSet<String>()); } else { /* * Correlated. This blank node is already the binding for some * other variable in this solution. * * Note: A FILTER can be used to enforce a same-term constraint * for variables correlated via blank nodes, but only for a * single solution. */ } if (!cvars.add(b.getName())) { /* * This would imply the same variable was bound more * than once in the solution. */ throw new AssertionError(); } } return bnodes; } }