Github user rvesse commented on a diff in the pull request:
https://github.com/apache/jena/pull/449#discussion_r207151396
--- Diff:
jena-arq/src/main/java/org/apache/jena/query/ParameterizedSparqlString.java ---
@@ -1734,4 +1739,250 @@ public String toString() {
}
}
+
+ /**
+ * Assign a VALUES varName with a multiple items.<br>
+ * Can be used to assign multiple values to a single variable or single
+ * value to multiple variables (if using a List) in the SPARQL
query.<br>
+ * See setGroupedValues to assign multiple values to multiple
variables.<br>
+ * Using "var" with list(prop_A, obj_A) on query "VALUES (?p ?o)
{?var}"
+ * would produce "VALUES (?p ?o) {(prop_A obj_A)}".
+ *
+ *
+ * @param varName
+ * @param items
+ */
+ public void setValues(String varName, Collection<? extends RDFNode>
items) {
+ items.forEach(item -> validateParameterValue(item.asNode()));
+ this.valuesReplacements.put(varName, new ValueReplacement(varName,
items));
+ }
+
+ /**
+ * Assign a VALUES varName with a single item.<br>
+ * Using "var" with Literal obj_A on query "VALUES ?o {?var}" would
produce
+ * "VALUES ?o {obj_A}".
+ *
+ * @param varName
+ * @param item
+ */
+ public void setValues(String varName, RDFNode item) {
+ setValues(varName, Arrays.asList(item));
+ }
+
+ /**
+ * **
+ * Sets a map of VALUES varNames and their items.<br>
+ * Can be used to assign multiple values to a single variable or single
+ * value to multiple variables (if using a List) in the SPARQL
query.<br>
+ * See setGroupedValues to assign multiple values to multiple
variables.
+ *
+ * @param itemsMap
+ */
+ public void setValues(Map<String, Collection<? extends RDFNode>>
itemsMap) {
+ itemsMap.forEach(this::setValues);
+ }
+
+ /**
+ * Allocate multiple lists of variables to a single VALUES varName.<br>
+ * Using "vars" with list(list(prop_A, obj_A), list(prop_B, obj_B)) on
query
+ * "VALUES (?p ?o) {?vars}" would produce "VALUES (?p ?o) {(prop_A
obj_A)
+ * (prop_B obj_B)}".
+ *
+ * @param varName
+ * @param groupedItems
+ */
+ public void setGroupedValues(String varName, Collection<List<? extends
RDFNode>> groupedItems) {
+ groupedItems.forEach(collection -> collection.forEach(item ->
validateParameterValue(item.asNode())));
+ this.valuesReplacements.put(varName, new ValueReplacement(varName,
groupedItems, true));
+ }
+
+ private String applyValues(String command) {
+
+ for (ValueReplacement valueReplacement :
valuesReplacements.values()) {
+ command = valueReplacement.apply(command);
+ }
+ return command;
+ }
+
+ private static final String VALUES_KEYWORD = "values";
+
+ protected static String[] extractTargetVars(String command, String
varName) {
+ String[] targetVars;
+
+ int varIndex = command.indexOf(varName);
+ if (varIndex > -1) {
+ String subCmd = command.substring(0, varIndex).toLowerCase();
//Truncate the command at the varName. Lowercase to search both types of values.
+ int valuesIndex = subCmd.lastIndexOf(VALUES_KEYWORD);
+ int bracesIndex = subCmd.lastIndexOf("{");
+ String vars = command.substring(valuesIndex +
VALUES_KEYWORD.length(), bracesIndex);
+ targetVars = vars.replaceAll("[(?)]", "").trim().split(" ");
+ } else {
+ targetVars = new String[]{};
+ }
+ return targetVars;
+ }
+
+ protected static boolean checkParenthesis(String command, String
varName) {
+ boolean isNeeded;
+
+ int varIndex = command.indexOf(varName);
+ if (varIndex > -1) {
+ String subCmd = command.substring(0, varIndex).toLowerCase();
//Truncate the command at the varName. Lowercase to search both types of values.
+ int valuesIndex = subCmd.lastIndexOf(VALUES_KEYWORD);
+ int parenthesisIndex = subCmd.indexOf("(", valuesIndex +
VALUES_KEYWORD.length());
+ isNeeded = parenthesisIndex > -1;
+ } else {
+ isNeeded = false;
+ }
+ return isNeeded;
+ }
+
+ /**
+ * Performs replacement of VALUES in query string.
+ *
+ */
+ private class ValueReplacement {
+
+ private final String varName;
+ private final Collection<? extends RDFNode> items;
+ private final Collection<List<? extends RDFNode>> groupedItems;
+ private final Boolean isGrouped;
+
+ public ValueReplacement(String varName, Collection<? extends
RDFNode> items) {
+ this.varName = varName;
+ this.items = items;
+ this.groupedItems = new ArrayList<>();
+ this.isGrouped = false;
+ }
+
+ public ValueReplacement(String varName, Collection<List<? extends
RDFNode>> groupedItems, Boolean isGrouped) {
+ this.varName = varName;
+ this.items = new ArrayList<>();
+ this.groupedItems = groupedItems;
+ this.isGrouped = isGrouped;
+ }
+
+ public String apply(String command) {
+
+ if (items.isEmpty() && groupedItems.isEmpty()) {
+ return command;
+ }
+
+ String[] targetVars = extractTargetVars(command, varName);
+ validateValuesSafeToInject(command, targetVars);
+
+ String target = createTarget();
+
+ StringBuilder replacement;
+ if (isGrouped) {
+ replacement = groupedApply();
+ } else {
+
+ replacement = ungroupedApply(command, targetVars.length);
+ }
+
+ return command.replace(target, replacement);
+ }
+
+ private StringBuilder groupedApply() {
+ StringBuilder replacement = new StringBuilder("");
+
+ for (List<? extends RDFNode> group : groupedItems) {
+ replacement.append("(");
+
+ for (RDFNode item : group) {
+ String insert = FmtUtils.stringForNode(item.asNode(),
(PrefixMapping) null);
+ replacement.append(insert);
+ replacement.append(" ");
+ }
+
+ replacement.deleteCharAt(replacement.length() - 1);
+ replacement.append(") ");
+ }
+
+ replacement.deleteCharAt(replacement.length() - 1);
+ return replacement;
+ }
+
+ private StringBuilder ungroupedApply(String command, int
targetVarCount) {
+
+ StringBuilder replacement = new StringBuilder("");
+
+ if (targetVarCount == 1) {
+ boolean isParenthesisNeeded = checkParenthesis(command,
varName);
+ for (RDFNode item : items) {
+ if (isParenthesisNeeded) {
+ replacement.append("(");
+ }
+ String insert = FmtUtils.stringForNode(item.asNode(),
(PrefixMapping) null);
+ replacement.append(insert);
+ if (isParenthesisNeeded) {
+ replacement.append(")");
+ }
+ replacement.append(" ");
+ }
+ replacement.deleteCharAt(replacement.length() - 1);
+ } else {
+ replacement.append("(");
+ for (RDFNode item : items) {
+ String insert = FmtUtils.stringForNode(item.asNode(),
(PrefixMapping) null);
+ replacement.append(insert);
+ replacement.append(" ");
+ }
+ replacement.deleteCharAt(replacement.length() - 1);
+ replacement.append(")");
+ }
+
+ return replacement;
+ }
+
+ /**
+ * Tidy up varName if doesn't start with a ? or $.
+ *
+ * @param varName
+ * @return
+ */
+ private String createTarget() {
+ String target;
+
+ if (varName.startsWith("?") || varName.startsWith("$")) {
+ target = varName;
+ } else {
+ target = "?" + varName;
+ }
+ return target;
+ }
+
+ protected void validateValuesSafeToInject(String command, String[]
targetVars) {
+
+ for (int i = 0; i < targetVars.length; i++) {
+ String targetVar = targetVars[i];
+ if (isGrouped) {
+ //Iterate through each group according to the position
of var and item.
+ for (List<? extends RDFNode> group : groupedItems) {
+ RDFNode item = group.get(i);
+ validateSafeToInject(command, targetVar,
item.asNode());
+ }
+ } else {
+ if (targetVars.length > 1) {
+ if (items instanceof List) {
+ //Multiple vars with items in an ordered list.
Each var is checked against the item.
+ List<? extends RDFNode> listItems = (List<?
extends RDFNode>) items;
+ RDFNode item = listItems.get(i);
+ validateSafeToInject(command, targetVar,
item.asNode());
+ } else {
+ //Multiple vars with items not in an ordered
list. This is parsing error.
+ throw new ARQException("Multiple VALUES
variables (" + String.join(", ", targetVars) + ") being used without an ordered
list of items: " + items.toString());
--- End diff --
Can this error be caught sooner at the time the user makes the original
`setValues()` call?
---