Skip to content

Commit

Permalink
GH-4997: Enable SparqlBuilder to create VALUES clauses (#5002)
Browse files Browse the repository at this point in the history
  • Loading branch information
hmottestad authored Nov 21, 2024
2 parents 3d80cdc + a900175 commit 665864b
Show file tree
Hide file tree
Showing 6 changed files with 392 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -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<Variable> variables = new ArrayList<>();

private List<List<RdfValue>> values = new ArrayList<>();

private List<RdfValue> 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<RdfValue> 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<IRI> 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<RdfValue> 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<RdfValue> values);

public ValuesBuilder iriValue(IRI value);

public ValuesBuilder iriValues(IRI... values);

public ValuesBuilder iriValues(Collection<IRI> values);
}

public interface ValuesBuilder {
public ValuesBuilder value(RdfValue value);

public ValuesBuilder values(RdfValue... values);

public ValuesBuilder values(Collection<RdfValue> values);

public ValuesBuilder iriValue(IRI value);

public ValuesBuilder iriValues(IRI... values);

public ValuesBuilder iriValues(Collection<IRI> values);

public Values build();
}

private static RdfValue valueOrUndef(RdfValue value) {
if (value == null) {
return UNDEF;
}
return value;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -45,6 +47,7 @@ public abstract class Query<T extends Query<T>> implements QueryElement {
protected Optional<GroupBy> groupBy = Optional.empty();
protected Optional<OrderBy> orderBy = Optional.empty();
protected Optional<Having> having = Optional.empty();
protected Optional<Values> values = Optional.empty();
protected int limit = -1, offset = -1, varCount = -1, bnodeCount = -1;

/**
Expand Down Expand Up @@ -201,6 +204,13 @@ public T offset(int offset) {
return (T) this;
}

public T values(Consumer<Values.VariablesBuilder> 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.
Expand Down Expand Up @@ -246,7 +256,7 @@ public String getQueryString() {
if (offset >= 0) {
query.append(OFFSET + " ").append(offset).append("\n");
}

SparqlBuilderUtils.appendAndNewlineIfPresent(values, query);
return query.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -42,6 +45,12 @@ default GraphPattern and(GraphPattern... patterns) {
return GraphPatterns.and(this).and(patterns);
}

default GraphPattern values(Consumer<Values.VariablesBuilder> 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: <br>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)");
}
});
}
Expand Down
Loading

0 comments on commit 665864b

Please sign in to comment.