Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-4997: Enable SparqlBuilder to create VALUES clauses #5002

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
fkleedorfer marked this conversation as resolved.
Show resolved Hide resolved
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
Loading