diff --git a/core/sparqlbuilder/src/main/java/org/eclipse/rdf4j/sparqlbuilder/constraint/Values.java b/core/sparqlbuilder/src/main/java/org/eclipse/rdf4j/sparqlbuilder/constraint/Values.java new file mode 100644 index 00000000000..e994c0ca8b8 --- /dev/null +++ b/core/sparqlbuilder/src/main/java/org/eclipse/rdf4j/sparqlbuilder/constraint/Values.java @@ -0,0 +1,217 @@ +/******************************************************************************* + * Copyright (c) 2024 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +package org.eclipse.rdf4j.sparqlbuilder.constraint; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.sparqlbuilder.core.Variable; +import org.eclipse.rdf4j.sparqlbuilder.graphpattern.GraphPattern; +import org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf; +import org.eclipse.rdf4j.sparqlbuilder.rdf.RdfValue; + +public class Values implements GraphPattern { + Variable[] variables; + RdfValue[][] solutionSequence; + + private static final RdfValue UNDEF = new RdfValue() { + @Override + public String getQueryString() { + return "UNDEF"; + } + }; + + public Values(Variable[] variables, RdfValue[][] solutionSequence) { + Objects.requireNonNull(solutionSequence); + Objects.requireNonNull(solutionSequence); + if (variables.length == 0) { + throw new IllegalArgumentException("no variables provided for VALUES clause"); + } + if (solutionSequence.length == 0 + || solutionSequence[0] == null + || solutionSequence[0].length == 0) { + throw new IllegalArgumentException("no values provided for VALUES clause"); + } + if (solutionSequence[0].length != variables.length) { + throw new IllegalArgumentException( + solutionSequence[0].length + + " values provided for " + + variables.length + + variables); + } + this.solutionSequence = solutionSequence; + this.variables = variables; + } + + @Override + public String getQueryString() { + StringBuilder sb = new StringBuilder(); + String parOpen = this.variables.length > 1 ? "( " : ""; + String parClose = this.variables.length > 1 ? ") " : ""; + sb.append("VALUES ").append(parOpen); + for (int i = 0; i < variables.length; i++) { + sb.append(variables[i].getQueryString()).append(" "); + } + sb.append(parClose).append("{").append(System.lineSeparator()); + for (int i = 0; i < solutionSequence.length; i++) { + sb.append(" ").append(parOpen); + for (int j = 0; j < solutionSequence[i].length; j++) { + sb.append(solutionSequence[i][j].getQueryString()).append(" "); + } + sb.append(parClose).append(System.lineSeparator()); + } + sb.append("}").append(System.lineSeparator()); + return sb.toString(); + } + + public static VariablesBuilder builder() { + return new Builder(); + } + + public static class Builder implements VariablesBuilder, ValuesBuilder { + public Builder() { + } + + private List variables = new ArrayList<>(); + + private List> values = new ArrayList<>(); + + private List currentValues = new ArrayList<>(); + + @Override + public VariablesBuilder variables(Variable... variable) { + Arrays.stream(variable).forEach(this.variables::add); + return this; + } + + /** + * Provide another value. This will fill up the current solution sequence. If this value is the last one (i.e. + * the solution sequence now is of the same length as the list of variables), the current solution sequence is + * recorded and a new solution sequence begins. + * + * @param value + * @return + */ + @Override + public ValuesBuilder value(RdfValue value) { + this.currentValues.add(valueOrUndef(value)); + if (currentValues.size() >= variables.size()) { + this.values.add(currentValues); + currentValues = new ArrayList<>(); + } + return this; + } + + @Override + public ValuesBuilder values(RdfValue... values) { + if (this.variables.size() == 1) { + for (int i = 0; i < values.length; i++) { + this.values.add(List.of(valueOrUndef(values[i]))); + } + } else if (this.variables.size() == values.length) { + this.values.add(Stream.of(values).map(Values::valueOrUndef).collect(Collectors.toList())); + } else { + throw new IllegalArgumentException( + "Provided list of values must match length of variables, or there must be only one variable."); + } + return this; + } + + @Override + public ValuesBuilder values(Collection values) { + return values(values.toArray(i -> new RdfValue[i])); + } + + @Override + public ValuesBuilder iriValue(IRI value) { + return value(Rdf.iri(value)); + } + + @Override + public ValuesBuilder iriValues(IRI... values) { + return values(Stream.of(values).map(Rdf::iri).toArray(i -> new RdfValue[i])); + } + + @Override + public ValuesBuilder iriValues(Collection values) { + return iriValues(values.toArray(i -> new IRI[i])); + } + + @Override + public Values build() { + if (this.values.isEmpty()) { + throw new IllegalArgumentException("No values provided"); + } + if (!this.currentValues.isEmpty()) { + throw new IllegalArgumentException( + "Current solution sequence is not finished - you added too few or too many values."); + } + RdfValue[][] values = new RdfValue[this.values.size()][this.variables.size()]; + for (int i = 0; i < this.values.size(); i++) { + List current = this.values.get(i); + if (current.size() != this.variables.size()) { + throw new IllegalArgumentException( + String.format( + "You provided $d values for $d variables", + current.size(), + this.variables.size())); + } + for (int j = 0; j < current.size(); j++) { + values[i][j] = current.get(j); + } + } + return new Values(this.variables.toArray(size -> new Variable[size]), values); + } + } + + public interface VariablesBuilder { + + public VariablesBuilder variables(Variable... variable); + + public ValuesBuilder value(RdfValue value); + + public ValuesBuilder values(RdfValue... values); + + public ValuesBuilder values(Collection values); + + public ValuesBuilder iriValue(IRI value); + + public ValuesBuilder iriValues(IRI... values); + + public ValuesBuilder iriValues(Collection values); + } + + public interface ValuesBuilder { + public ValuesBuilder value(RdfValue value); + + public ValuesBuilder values(RdfValue... values); + + public ValuesBuilder values(Collection values); + + public ValuesBuilder iriValue(IRI value); + + public ValuesBuilder iriValues(IRI... values); + + public ValuesBuilder iriValues(Collection values); + + public Values build(); + } + + private static RdfValue valueOrUndef(RdfValue value) { + if (value == null) { + return UNDEF; + } + return value; + } + +} diff --git a/core/sparqlbuilder/src/main/java/org/eclipse/rdf4j/sparqlbuilder/core/query/Query.java b/core/sparqlbuilder/src/main/java/org/eclipse/rdf4j/sparqlbuilder/core/query/Query.java index 88992a7e152..ff56465530f 100644 --- a/core/sparqlbuilder/src/main/java/org/eclipse/rdf4j/sparqlbuilder/core/query/Query.java +++ b/core/sparqlbuilder/src/main/java/org/eclipse/rdf4j/sparqlbuilder/core/query/Query.java @@ -12,8 +12,10 @@ package org.eclipse.rdf4j.sparqlbuilder.core.query; import java.util.Optional; +import java.util.function.Consumer; import org.eclipse.rdf4j.sparqlbuilder.constraint.Expression; +import org.eclipse.rdf4j.sparqlbuilder.constraint.Values; import org.eclipse.rdf4j.sparqlbuilder.core.Dataset; import org.eclipse.rdf4j.sparqlbuilder.core.From; import org.eclipse.rdf4j.sparqlbuilder.core.GroupBy; @@ -45,6 +47,7 @@ public abstract class Query> implements QueryElement { protected Optional groupBy = Optional.empty(); protected Optional orderBy = Optional.empty(); protected Optional having = Optional.empty(); + protected Optional values = Optional.empty(); protected int limit = -1, offset = -1, varCount = -1, bnodeCount = -1; /** @@ -201,6 +204,13 @@ public T offset(int offset) { return (T) this; } + public T values(Consumer valuesConfigurer) { + Values.Builder builder = (Values.Builder) Values.builder(); + valuesConfigurer.accept(builder); + this.values = Optional.of(builder.build()); + return (T) this; + } + /** * A shortcut. Each call to this method returns a new {@link Variable} that is unique (i.e., has a unique alias) to * this query instance. @@ -246,7 +256,7 @@ public String getQueryString() { if (offset >= 0) { query.append(OFFSET + " ").append(offset).append("\n"); } - + SparqlBuilderUtils.appendAndNewlineIfPresent(values, query); return query.toString(); } } diff --git a/core/sparqlbuilder/src/main/java/org/eclipse/rdf4j/sparqlbuilder/graphpattern/GraphPattern.java b/core/sparqlbuilder/src/main/java/org/eclipse/rdf4j/sparqlbuilder/graphpattern/GraphPattern.java index 2dc8c7d680c..8be610acf3e 100644 --- a/core/sparqlbuilder/src/main/java/org/eclipse/rdf4j/sparqlbuilder/graphpattern/GraphPattern.java +++ b/core/sparqlbuilder/src/main/java/org/eclipse/rdf4j/sparqlbuilder/graphpattern/GraphPattern.java @@ -11,7 +11,10 @@ package org.eclipse.rdf4j.sparqlbuilder.graphpattern; +import java.util.function.Consumer; + import org.eclipse.rdf4j.sparqlbuilder.constraint.Expression; +import org.eclipse.rdf4j.sparqlbuilder.constraint.Values; import org.eclipse.rdf4j.sparqlbuilder.core.QueryElement; /** @@ -42,6 +45,12 @@ default GraphPattern and(GraphPattern... patterns) { return GraphPatterns.and(this).and(patterns); } + default GraphPattern values(Consumer valuesConfigurer) { + Values.Builder valuesBuilder = (Values.Builder) Values.builder(); + valuesConfigurer.accept(valuesBuilder); + return GraphPatterns.and(this).and(valuesBuilder.build()); + } + /** * Convert this graph pattern into an alternative graph pattern, combining this graph pattern with the given * patterns:
diff --git a/core/sparqlbuilder/src/test/java/org/eclipse/rdf4j/sparqlbuilder/examples/BaseExamples.java b/core/sparqlbuilder/src/test/java/org/eclipse/rdf4j/sparqlbuilder/examples/BaseExamples.java index 69c22345e30..6b7ad53aeaf 100644 --- a/core/sparqlbuilder/src/test/java/org/eclipse/rdf4j/sparqlbuilder/examples/BaseExamples.java +++ b/core/sparqlbuilder/src/test/java/org/eclipse/rdf4j/sparqlbuilder/examples/BaseExamples.java @@ -90,10 +90,7 @@ public boolean matches(Object item) { @Override public void describeTo(Description description) { description.appendText( - "To match the following String after lowercasing, removal of newlines and whitespaces.\n"); - description.appendText("\nHint: first difference: " + aroundString + "\n"); - description.appendText( - "Expected: was \"" + expected.replaceAll("\n", "\\\\n").replaceAll("\\s+", " ") + "\""); + "\"" + expected + "\" (ignoring case, whitespace and newlines)"); } }); } diff --git a/core/sparqlbuilder/src/test/java/org/eclipse/rdf4j/sparqlbuilder/examples/sparql11spec/Section10Test.java b/core/sparqlbuilder/src/test/java/org/eclipse/rdf4j/sparqlbuilder/examples/sparql11spec/Section10Test.java new file mode 100644 index 00000000000..ed429c169d5 --- /dev/null +++ b/core/sparqlbuilder/src/test/java/org/eclipse/rdf4j/sparqlbuilder/examples/sparql11spec/Section10Test.java @@ -0,0 +1,153 @@ +/******************************************************************************* + * Copyright (c) 2024 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +package org.eclipse.rdf4j.sparqlbuilder.examples.sparql11spec; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.eclipse.rdf4j.sparqlbuilder.constraint.Expressions.notEquals; +import static org.eclipse.rdf4j.sparqlbuilder.core.SparqlBuilder.prefix; +import static org.eclipse.rdf4j.sparqlbuilder.core.SparqlBuilder.var; +import static org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf.iri; + +import org.eclipse.rdf4j.model.vocabulary.DC; +import org.eclipse.rdf4j.model.vocabulary.FOAF; +import org.eclipse.rdf4j.model.vocabulary.RDF; +import org.eclipse.rdf4j.model.vocabulary.RDFS; +import org.eclipse.rdf4j.sparqlbuilder.constraint.Expressions; +import org.eclipse.rdf4j.sparqlbuilder.constraint.Values; +import org.eclipse.rdf4j.sparqlbuilder.core.Prefix; +import org.eclipse.rdf4j.sparqlbuilder.core.SparqlBuilder; +import org.eclipse.rdf4j.sparqlbuilder.core.Variable; +import org.eclipse.rdf4j.sparqlbuilder.core.query.Queries; +import org.eclipse.rdf4j.sparqlbuilder.examples.BaseExamples; +import org.eclipse.rdf4j.sparqlbuilder.graphpattern.GraphPattern; +import org.eclipse.rdf4j.sparqlbuilder.graphpattern.TriplePattern; +import org.eclipse.rdf4j.sparqlbuilder.rdf.Iri; +import org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf; +import org.eclipse.rdf4j.sparqlbuilder.rdf.RdfObject; +import org.junit.jupiter.api.Test; + +public class Section10Test extends BaseExamples { + private final Prefix rdfs = SparqlBuilder.prefix("rdfs", iri(RDFS.NS.getName())); + private final Prefix dc = SparqlBuilder.prefix("dc", iri(DC_NS)); + private final Prefix foaf = SparqlBuilder.prefix("foaf", iri(FOAF_NS)); + private final Prefix base = SparqlBuilder.prefix("", iri("http://example/")); + private final Prefix ex = prefix("ex", iri("http://example/")); + private final Prefix rdf = prefix("rdf", iri(RDF.NAMESPACE)); + + private final Prefix prefixBook = prefix("", iri("http://example.org/book/")); + private final Prefix ns = prefix("ns", iri("http://example.org/ns#")); + private final Variable displayString = var("displayString"); + private final Variable x = var("x"); + + private final Variable book = var("book"); + private final Variable title = var("title"); + private final Variable price = var("price"); + + private final Variable name = var("name"); + private final Variable y = var("y"); + private final Variable z = var("z"); + private final Variable ancestor = var("ancestor"); + private final Variable type = var("type"); + private final Variable p = var("p"); + private final Variable v = var("v"); + private final Variable element = var("element"); + private final Variable s = var("s"); + private final Variable total = var("total"); + private final Variable person = var("person"); + private final Iri property = base.iri("property"); + private final Iri me = iri("#me"); + private final Iri thing = iri("http://example/thing"); + private final Iri book1 = base.iri("book1"); + private final Iri book3 = base.iri("book3"); + private final Iri order = base.iri("order"); + private final Iri mailto = iri("mailto:alice@example"); + private final Iri list = base.iri("list"); + + @Test + public void example_10_2_1__two_vars_two_solutions_one_undef() { + Values values = Values.builder() + .variables(x, y) + .values(base.iri("uri1"), Rdf.literalOf(1)) + .values(base.iri("uri2"), null) + .build(); + String str = values.getQueryString(); + assertThat(str).is(stringEqualsIgnoreCaseAndWhitespace( + "VALUES (?x ?y) {\n" + + " (:uri1 1)\n" + + " (:uri2 UNDEF)\n" + + "}" + )); + } + + @Test + public void example_10_2_1__one_var_two_solutions() { + Values values = Values.builder().variables(z).value(Rdf.literalOf("abc")).value(Rdf.literalOf("def")).build(); + String str = values.getQueryString(); + assertThat(str).is(stringEqualsIgnoreCaseAndWhitespace( + "VALUES ?z { \"abc\" \"def\" }" + )); + } + + @Test + public void example_10_2_2__values__in__graphpattern() { + String str = Queries.SELECT(book, title, price) + .prefix(dc, prefixBook, ns) + .where(Values.builder() + .variables(book) + .values(book1, book3) + .build() + .and(book.has(dc.iri("title"), title) + .andHas(ns.iri("price"), price))) + .getQueryString(); + assertThat(str).is(stringEqualsIgnoreCaseAndWhitespace( + "PREFIX dc: \n" + + "\tPREFIX : \n" + + "\tPREFIX ns: \n" + + "\n" + + "\tSELECT ?book ?title ?price\n" + + "WHERE {\n" + + "\t\tVALUES ?book { :book1 :book3 }\n" + + " ?book dc:title ?title ;\n" + + "\t\tns:price ?price .\n" + + "\t}" + )); + } + + @Test + public void example_10_2_2__values__at__end() { + String str = Queries.SELECT(book, title, price) + .prefix(dc, prefixBook, ns) + .where(book.has(dc.iri("title"), title) + .andHas(ns.iri("price"), price)) + .values(v -> v + .variables(book, title) + .values(null, Rdf.literalOf("SPARQL Tutorial")) + .values(prefixBook.iri("book2"), null)) + .getQueryString(); + assertThat(str).is(stringEqualsIgnoreCaseAndWhitespace( + "PREFIX dc: \n" + + "PREFIX : \n" + + "PREFIX ns: \n" + + "\n" + + "SELECT ?book ?title ?price\n" + + "WHERE {\n" + + " ?book dc:title ?title ;\n" + + " ns:price ?price .\n" + + "}\n" + + "VALUES (?book ?title)\n" + + "{ (UNDEF \"SPARQL Tutorial\")\n" + + " (:book2 UNDEF)\n" + + "}" + )); + + } + +} diff --git a/site/themes/hugo-solstice-theme b/site/themes/hugo-solstice-theme index 4685672c230..2b762a0ed31 160000 --- a/site/themes/hugo-solstice-theme +++ b/site/themes/hugo-solstice-theme @@ -1 +1 @@ -Subproject commit 4685672c23094cdaffcbf05ca7b0b1d00c6028c8 +Subproject commit 2b762a0ed3195eb59cf56dd8160a8d8b00e24f36