Skip to content

Commit 665864b

Browse files
authored
GH-4997: Enable SparqlBuilder to create VALUES clauses (#5002)
2 parents 3d80cdc + a900175 commit 665864b

File tree

6 files changed

+392
-6
lines changed

6 files changed

+392
-6
lines changed
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Eclipse RDF4J contributors.
3+
*
4+
* All rights reserved. This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Distribution License v1.0
6+
* which accompanies this distribution, and is available at
7+
* http://www.eclipse.org/org/documents/edl-v10.php.
8+
*
9+
* SPDX-License-Identifier: BSD-3-Clause
10+
*******************************************************************************/
11+
package org.eclipse.rdf4j.sparqlbuilder.constraint;
12+
13+
import java.util.*;
14+
import java.util.stream.Collectors;
15+
import java.util.stream.Stream;
16+
17+
import org.eclipse.rdf4j.model.IRI;
18+
import org.eclipse.rdf4j.sparqlbuilder.core.Variable;
19+
import org.eclipse.rdf4j.sparqlbuilder.graphpattern.GraphPattern;
20+
import org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf;
21+
import org.eclipse.rdf4j.sparqlbuilder.rdf.RdfValue;
22+
23+
public class Values implements GraphPattern {
24+
Variable[] variables;
25+
RdfValue[][] solutionSequence;
26+
27+
private static final RdfValue UNDEF = new RdfValue() {
28+
@Override
29+
public String getQueryString() {
30+
return "UNDEF";
31+
}
32+
};
33+
34+
public Values(Variable[] variables, RdfValue[][] solutionSequence) {
35+
Objects.requireNonNull(solutionSequence);
36+
Objects.requireNonNull(solutionSequence);
37+
if (variables.length == 0) {
38+
throw new IllegalArgumentException("no variables provided for VALUES clause");
39+
}
40+
if (solutionSequence.length == 0
41+
|| solutionSequence[0] == null
42+
|| solutionSequence[0].length == 0) {
43+
throw new IllegalArgumentException("no values provided for VALUES clause");
44+
}
45+
if (solutionSequence[0].length != variables.length) {
46+
throw new IllegalArgumentException(
47+
solutionSequence[0].length
48+
+ " values provided for "
49+
+ variables.length
50+
+ variables);
51+
}
52+
this.solutionSequence = solutionSequence;
53+
this.variables = variables;
54+
}
55+
56+
@Override
57+
public String getQueryString() {
58+
StringBuilder sb = new StringBuilder();
59+
String parOpen = this.variables.length > 1 ? "( " : "";
60+
String parClose = this.variables.length > 1 ? ") " : "";
61+
sb.append("VALUES ").append(parOpen);
62+
for (int i = 0; i < variables.length; i++) {
63+
sb.append(variables[i].getQueryString()).append(" ");
64+
}
65+
sb.append(parClose).append("{").append(System.lineSeparator());
66+
for (int i = 0; i < solutionSequence.length; i++) {
67+
sb.append(" ").append(parOpen);
68+
for (int j = 0; j < solutionSequence[i].length; j++) {
69+
sb.append(solutionSequence[i][j].getQueryString()).append(" ");
70+
}
71+
sb.append(parClose).append(System.lineSeparator());
72+
}
73+
sb.append("}").append(System.lineSeparator());
74+
return sb.toString();
75+
}
76+
77+
public static VariablesBuilder builder() {
78+
return new Builder();
79+
}
80+
81+
public static class Builder implements VariablesBuilder, ValuesBuilder {
82+
public Builder() {
83+
}
84+
85+
private List<Variable> variables = new ArrayList<>();
86+
87+
private List<List<RdfValue>> values = new ArrayList<>();
88+
89+
private List<RdfValue> currentValues = new ArrayList<>();
90+
91+
@Override
92+
public VariablesBuilder variables(Variable... variable) {
93+
Arrays.stream(variable).forEach(this.variables::add);
94+
return this;
95+
}
96+
97+
/**
98+
* Provide another value. This will fill up the current solution sequence. If this value is the last one (i.e.
99+
* the solution sequence now is of the same length as the list of variables), the current solution sequence is
100+
* recorded and a new solution sequence begins.
101+
*
102+
* @param value
103+
* @return
104+
*/
105+
@Override
106+
public ValuesBuilder value(RdfValue value) {
107+
this.currentValues.add(valueOrUndef(value));
108+
if (currentValues.size() >= variables.size()) {
109+
this.values.add(currentValues);
110+
currentValues = new ArrayList<>();
111+
}
112+
return this;
113+
}
114+
115+
@Override
116+
public ValuesBuilder values(RdfValue... values) {
117+
if (this.variables.size() == 1) {
118+
for (int i = 0; i < values.length; i++) {
119+
this.values.add(List.of(valueOrUndef(values[i])));
120+
}
121+
} else if (this.variables.size() == values.length) {
122+
this.values.add(Stream.of(values).map(Values::valueOrUndef).collect(Collectors.toList()));
123+
} else {
124+
throw new IllegalArgumentException(
125+
"Provided list of values must match length of variables, or there must be only one variable.");
126+
}
127+
return this;
128+
}
129+
130+
@Override
131+
public ValuesBuilder values(Collection<RdfValue> values) {
132+
return values(values.toArray(i -> new RdfValue[i]));
133+
}
134+
135+
@Override
136+
public ValuesBuilder iriValue(IRI value) {
137+
return value(Rdf.iri(value));
138+
}
139+
140+
@Override
141+
public ValuesBuilder iriValues(IRI... values) {
142+
return values(Stream.of(values).map(Rdf::iri).toArray(i -> new RdfValue[i]));
143+
}
144+
145+
@Override
146+
public ValuesBuilder iriValues(Collection<IRI> values) {
147+
return iriValues(values.toArray(i -> new IRI[i]));
148+
}
149+
150+
@Override
151+
public Values build() {
152+
if (this.values.isEmpty()) {
153+
throw new IllegalArgumentException("No values provided");
154+
}
155+
if (!this.currentValues.isEmpty()) {
156+
throw new IllegalArgumentException(
157+
"Current solution sequence is not finished - you added too few or too many values.");
158+
}
159+
RdfValue[][] values = new RdfValue[this.values.size()][this.variables.size()];
160+
for (int i = 0; i < this.values.size(); i++) {
161+
List<RdfValue> current = this.values.get(i);
162+
if (current.size() != this.variables.size()) {
163+
throw new IllegalArgumentException(
164+
String.format(
165+
"You provided $d values for $d variables",
166+
current.size(),
167+
this.variables.size()));
168+
}
169+
for (int j = 0; j < current.size(); j++) {
170+
values[i][j] = current.get(j);
171+
}
172+
}
173+
return new Values(this.variables.toArray(size -> new Variable[size]), values);
174+
}
175+
}
176+
177+
public interface VariablesBuilder {
178+
179+
public VariablesBuilder variables(Variable... variable);
180+
181+
public ValuesBuilder value(RdfValue value);
182+
183+
public ValuesBuilder values(RdfValue... values);
184+
185+
public ValuesBuilder values(Collection<RdfValue> values);
186+
187+
public ValuesBuilder iriValue(IRI value);
188+
189+
public ValuesBuilder iriValues(IRI... values);
190+
191+
public ValuesBuilder iriValues(Collection<IRI> values);
192+
}
193+
194+
public interface ValuesBuilder {
195+
public ValuesBuilder value(RdfValue value);
196+
197+
public ValuesBuilder values(RdfValue... values);
198+
199+
public ValuesBuilder values(Collection<RdfValue> values);
200+
201+
public ValuesBuilder iriValue(IRI value);
202+
203+
public ValuesBuilder iriValues(IRI... values);
204+
205+
public ValuesBuilder iriValues(Collection<IRI> values);
206+
207+
public Values build();
208+
}
209+
210+
private static RdfValue valueOrUndef(RdfValue value) {
211+
if (value == null) {
212+
return UNDEF;
213+
}
214+
return value;
215+
}
216+
217+
}

core/sparqlbuilder/src/main/java/org/eclipse/rdf4j/sparqlbuilder/core/query/Query.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
package org.eclipse.rdf4j.sparqlbuilder.core.query;
1313

1414
import java.util.Optional;
15+
import java.util.function.Consumer;
1516

1617
import org.eclipse.rdf4j.sparqlbuilder.constraint.Expression;
18+
import org.eclipse.rdf4j.sparqlbuilder.constraint.Values;
1719
import org.eclipse.rdf4j.sparqlbuilder.core.Dataset;
1820
import org.eclipse.rdf4j.sparqlbuilder.core.From;
1921
import org.eclipse.rdf4j.sparqlbuilder.core.GroupBy;
@@ -45,6 +47,7 @@ public abstract class Query<T extends Query<T>> implements QueryElement {
4547
protected Optional<GroupBy> groupBy = Optional.empty();
4648
protected Optional<OrderBy> orderBy = Optional.empty();
4749
protected Optional<Having> having = Optional.empty();
50+
protected Optional<Values> values = Optional.empty();
4851
protected int limit = -1, offset = -1, varCount = -1, bnodeCount = -1;
4952

5053
/**
@@ -201,6 +204,13 @@ public T offset(int offset) {
201204
return (T) this;
202205
}
203206

207+
public T values(Consumer<Values.VariablesBuilder> valuesConfigurer) {
208+
Values.Builder builder = (Values.Builder) Values.builder();
209+
valuesConfigurer.accept(builder);
210+
this.values = Optional.of(builder.build());
211+
return (T) this;
212+
}
213+
204214
/**
205215
* A shortcut. Each call to this method returns a new {@link Variable} that is unique (i.e., has a unique alias) to
206216
* this query instance.
@@ -246,7 +256,7 @@ public String getQueryString() {
246256
if (offset >= 0) {
247257
query.append(OFFSET + " ").append(offset).append("\n");
248258
}
249-
259+
SparqlBuilderUtils.appendAndNewlineIfPresent(values, query);
250260
return query.toString();
251261
}
252262
}

core/sparqlbuilder/src/main/java/org/eclipse/rdf4j/sparqlbuilder/graphpattern/GraphPattern.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111

1212
package org.eclipse.rdf4j.sparqlbuilder.graphpattern;
1313

14+
import java.util.function.Consumer;
15+
1416
import org.eclipse.rdf4j.sparqlbuilder.constraint.Expression;
17+
import org.eclipse.rdf4j.sparqlbuilder.constraint.Values;
1518
import org.eclipse.rdf4j.sparqlbuilder.core.QueryElement;
1619

1720
/**
@@ -42,6 +45,12 @@ default GraphPattern and(GraphPattern... patterns) {
4245
return GraphPatterns.and(this).and(patterns);
4346
}
4447

48+
default GraphPattern values(Consumer<Values.VariablesBuilder> valuesConfigurer) {
49+
Values.Builder valuesBuilder = (Values.Builder) Values.builder();
50+
valuesConfigurer.accept(valuesBuilder);
51+
return GraphPatterns.and(this).and(valuesBuilder.build());
52+
}
53+
4554
/**
4655
* Convert this graph pattern into an alternative graph pattern, combining this graph pattern with the given
4756
* patterns: <br>

core/sparqlbuilder/src/test/java/org/eclipse/rdf4j/sparqlbuilder/examples/BaseExamples.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,7 @@ public boolean matches(Object item) {
9090
@Override
9191
public void describeTo(Description description) {
9292
description.appendText(
93-
"To match the following String after lowercasing, removal of newlines and whitespaces.\n");
94-
description.appendText("\nHint: first difference: " + aroundString + "\n");
95-
description.appendText(
96-
"Expected: was \"" + expected.replaceAll("\n", "\\\\n").replaceAll("\\s+", " ") + "\"");
93+
"\"" + expected + "\" (ignoring case, whitespace and newlines)");
9794
}
9895
});
9996
}

0 commit comments

Comments
 (0)