Skip to content

Commit

Permalink
apacheGH-2799: Return substitutions -- QueryTransformOps.syntaxSubsti…
Browse files Browse the repository at this point in the history
…tute
  • Loading branch information
afs committed Jan 15, 2025
1 parent 8018231 commit d5f72d2
Show file tree
Hide file tree
Showing 17 changed files with 538 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ public X build() {
throw new QueryException("Substitution only supported for Query objects. Failed to parse the given string as a Query object.", e);
}
}
queryActual = QueryTransformOps.transform(queryActual, substitutionMap);
queryActual = QueryTransformOps.replaceVars(queryActual, substitutionMap);
queryStringActual = queryActual.toString();
}
Context cxt = contextAcc.context();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,17 @@ public void remove(Var var) {
exprs.remove(var);
}

/**
* If the variable is already in the VarExprList, replace the expression.
* This retains the list order.
* Otherwise, add the variable and expression.
*/
public void update(Var var, Expr newExpr) {
exprs.put(var, newExpr);
if ( ! vars.contains(var) )
vars.add(var);
}

public void clear() {
vars.clear();
exprs.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ public QueryExec build() {
String queryStringActual = queryString;

if ( substitutionMap != null && ! substitutionMap.isEmpty() ) {
queryActual = QueryTransformOps.transform(query, substitutionMap);
queryActual = QueryTransformOps.replaceVars(query, substitutionMap);
queryStringActual = null;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.jena.sparql.syntax.syntaxtransform;

import org.apache.jena.query.QueryException;

public class QueryScopeException extends QueryException {
public QueryScopeException(String msg) { super(msg) ; }
public QueryScopeException(String msg, Throwable cause) { super(msg, cause) ; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.jena.sparql.syntax.syntaxtransform;

import java.util.Collection;

import org.apache.jena.query.Query;
import org.apache.jena.sparql.core.Var;
import org.apache.jena.sparql.core.VarExprList;
import org.apache.jena.sparql.syntax.*;

/**
* Variable usage check for {@link QueryTransformOps#transform}.
*/
public class QuerySyntaxSubstituteScope {

/**
* Check that the query can be transformed by replacing the variables with values.
* For example, an assigned variables ({@code AS ?var}) can not be replace by a value.
*/
public static void scopeCheck(Query query, Collection<Var> vars) {
checkLevel(query, vars);
checkPattern(query.getQueryPattern(), vars);
}

public static void checkPattern(Element element, Collection<Var> vars) {
ElementVisitor visitor = new SubstituteScopeVisitor(vars);
ElementWalker.walk(element, visitor);
}

private static void checkLevel(Query query, Collection<Var> vars) {
checkAssignments("Query project expression", vars, query.getProject());
checkAssignments("GROUP BY ", vars, query.getGroupBy());
query.getAggregators().forEach(agg->{
checkAssignment("Aggregator", vars, agg.getVar());
});
}

private static void checkAssignments(String context, Collection<Var> vars, VarExprList varExprList) {
varExprList.forEachVarExpr((v,e)->{
if ( e != null )
checkAssignment(context, vars, v);
});
}

private static void checkAssignment(String context, Collection<Var> vars, Var assignedVar) {
if ( vars.contains(assignedVar) )
reject("BIND", assignedVar);
}

private static void reject(String elementName, Var badVar) {
throw new QueryScopeException("Can not use "+badVar+" in this query");
}

private static class SubstituteScopeVisitor extends ElementVisitorBase {

private Collection<Var> vars;

SubstituteScopeVisitor(Collection<Var> vars) {
this.vars = vars;
}

// BOUND(?x) with no ?x in scope.
// @Override
// public void visit(ElementFilter el) {
// Set<Var> mentioned = el.getExpr().getVarsMentioned();
// // EXISTS
// }

@Override
public void visit(ElementAssign el) {
Var assignedVar = el.getVar();
checkAssignment("LET", vars, assignedVar);
}

@Override
public void visit(ElementBind el) {
Var assignedVar = el.getVar();
checkAssignment("BIND", vars, assignedVar);
}

@Override
public void visit(ElementData el) {
var assignedVars = el.getVars();
assignedVars.forEach(v->checkAssignment("VALUES", vars, v));
}

// @Override
// public void visit(ElementExists el) { }
//
// @Override
// public void visit(ElementNotExists el) { }

@Override
public void visit(ElementSubQuery el) {
// Check project
el.getQuery().getQueryPattern().visit(this);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

package org.apache.jena.sparql.syntax.syntaxtransform;

import static org.apache.jena.sparql.syntax.syntaxtransform.QuerySyntaxSubstituteScope.scopeCheck;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
Expand All @@ -43,32 +45,80 @@

/** Support for transformation of query abstract syntax. */
public class QueryTransformOps {
/** Transform a query based on a mapping from {@link Var} variable to replacement {@link Node}. */

/**
* Replace variables in a query by RDF terms.
* The replacements are added to the return queries SELECT clause (if a SELECT query).
* <p>
* @throws QueryScopeException if the query contains variables used in a
* way that does not allow substitution (.e.g {@code AS ?var} or used in
* {@code VALUES}).
*/
public static Query syntaxSubstitute(Query input, Map<Var, Node> substitutions) {
scopeCheck(input, substitutions.keySet());
Query output = transformTopLevel(input, substitutions);
return output;
}

// Call transform, add in the substitutions as top-level SELECT expressions/
private static Query transformTopLevel(Query query, Map<Var, Node> substitutions) {
Query query2 = transformSubstitute(query, substitutions);
// Include substitutions
if ( query.isSelectType() ) {
query2.setQueryResultStar(false);
substitutions.forEach((v, n) -> {
var nv = NodeValue.makeNode(n);
query2.getProject().update(v, NodeValue.makeNode(n));
});
}
return query2;
}

/** @deprecated Use {@link #queryReplaceVars} */
@Deprecated
public static Query transform(Query query, Map<Var, ? extends Node> substitutions) {
ElementTransform eltrans = new ElementTransformSubst(substitutions);
NodeTransform nodeTransform = new NodeTransformSubst(substitutions);
ExprTransform exprTrans = new ExprTransformNodeElement(nodeTransform, eltrans);
return transform(query, eltrans, exprTrans);
return replaceVars(query, substitutions);
}

/** Transform a query based on a mapping from {@link Var} variable to replacement {@link Node}. */
public static Query replaceVars(Query query, Map<Var, ? extends Node> substitutions) {
return transformSubstitute(query, substitutions);
}


/** @deprecated Use {@link #queryReplaceVars} */
@Deprecated
public static Query transformQuery(Query query, Map<String, ? extends RDFNode> substitutions) {
return queryReplaceVars(query, substitutions);
}

/**
* Transform a query based on a mapping from variable name to replacement
* {@link RDFNode} (a {@link Resource} (or blank node) or a {@link Literal}).
*/
public static Query transformQuery(Query query, Map<String, ? extends RDFNode> substitutions) {
public static Query queryReplaceVars(Query query, Map<String, ? extends RDFNode> substitutions) {
// Must have a different name because of Java's erasure of parameterised types.
Map<Var, Node> map = TransformElementLib.convert(substitutions);
return transform(query, map);
return replaceVars(query, map);
}

private static Query transformSubstitute(Query query, Map<Var, ? extends Node> substitutions) {
scopeCheck(query, substitutions.keySet());
ElementTransform eltrans = new ElementTransformSubst(substitutions);
NodeTransform nodeTransform = new NodeTransformSubst(substitutions);
ExprTransform exprTrans = new ExprTransformNodeElement(nodeTransform, eltrans);
return transform(query, eltrans, exprTrans);
}

// ----------------

/**
* Transform a query using {@link ElementTransform} and {@link ExprTransform}.
* It is the responsibility of these transforms to transform to a legal SPARQL query.
*/
public static Query transform(Query query, ElementTransform transform, ExprTransform exprTransform) {
Query q2 = QueryTransformOps.shallowCopy(query);
// Mutate the q2 structures which are already allocated and no other code can access yet.

mutateByQueryType(q2, exprTransform);
mutateVarExprList(q2.getGroupBy(), exprTransform);
mutateExprList(q2.getHavingExprs(), exprTransform);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@

package org.apache.jena.riot.system;



// 2024-10 Using Junit5 can confuse Eclipse testing.
// Test classes get missed
import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.Suite;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,25 @@

package org.apache.jena.sparql.syntax;

import org.apache.jena.sparql.syntax.syntaxtransform.TestFlattenSyntax ;
import org.apache.jena.sparql.syntax.syntaxtransform.TestQueryOps ;
import org.apache.jena.sparql.syntax.syntaxtransform.TestQuerySyntaxTransform ;
import org.junit.runner.RunWith ;
import org.junit.runners.Suite ;
import org.junit.runners.Suite.SuiteClasses ;
import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.Suite;

@RunWith(Suite.class)
@SuiteClasses( {
import org.apache.jena.sparql.syntax.syntaxtransform.*;

@Suite
@SelectClasses({

//import org.junit.runner.RunWith;
//import org.junit.runners.Suite;
//import org.junit.runners.Suite.SuiteClasses;
//@RunWith(Suite.class)
//@SuiteClasses({
TestQueryParser.class
, TestSerialization.class
, TestQueryOps.class
, TestQueryShallowCopy.class
, TestQuerySubstituteScope.class
, TestQuerySyntaxTransform.class
, TestQuerySyntaxSubstitute.class
, TestFlattenSyntax.class
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@

package org.apache.jena.sparql.syntax;

import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

import org.apache.jena.atlas.logging.LogCtl;
import org.apache.jena.query.QueryFactory;
import org.apache.jena.query.QueryParseException;
import org.apache.jena.sparql.lang.QueryParserBase;
import org.junit.Test;
import org.slf4j.Logger;

/**
Expand All @@ -49,34 +52,33 @@ private static void silent(Runnable action) {
testParseIRIs("<http://[::1]/abc>");
}

@Test(expected = QueryParseException.class)
@Test
public void syntax_uri_brace_1() {
testParseIRIs("<http://example/{}>");
assertThrows(QueryParseException.class, ()->testParseIRIs("<http://example/{}>"));
}

@Test(expected = QueryParseException.class)
public void syntax_uri_brace_2() {
testParseIRIs("<http://example/#{}>");
@Test public void syntax_uri_brace_2() {
assertThrows(QueryParseException.class, ()->testParseIRIs("<http://example/#{}>"));
}

@Test(expected = QueryParseException.class)
@Test
public void syntax_uri_space_1() {
testParseIRIs("<http://example/abc def>");
assertThrows(QueryParseException.class, ()->testParseIRIs("<http://example/abc def>"));
}

@Test(expected = QueryParseException.class)
@Test
public void syntax_uri_space_2() {
testParseIRIs("<http://example/abc?q= def>");
assertThrows(QueryParseException.class, ()->testParseIRIs("<http://example/abc?q= def>"));
}

@Test(expected = QueryParseException.class)
@Test
public void syntax_uri_space_3() {
testParseIRIs("< http://example/abc>");
assertThrows(QueryParseException.class, ()->testParseIRIs("< http://example/abc>"));
}

@Test(expected = QueryParseException.class)
@Test
public void syntax_uri_space_4() {
testParseIRIs("<http://example/abc >");
assertThrows(QueryParseException.class, ()->testParseIRIs("<http://example/abc >"));
}

// Test that a URI string can be used in Turtle data
Expand Down
Loading

0 comments on commit d5f72d2

Please sign in to comment.