Github user GregAlbiston commented on a diff in the pull request: https://github.com/apache/jena/pull/449#discussion_r207248104 --- 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 -- This error isn't relevant anymore as the `List` is being enforced by the `setValues` method. A check is performed that the number of target vars equals the number of 'items' in a 'row' with exception thrown. The check is performed late as the 'command' is not final in the class and can be changed using `setCommandText`. Could be performed earlier but the existing approach is to wait until `toString` is called.
---