QueryResults.java
/*******************************************************************************
* Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
*
* 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.query;
import java.io.InputStream;
import java.lang.ref.Cleaner;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.datatype.XMLGregorianCalendar;
import org.eclipse.rdf4j.common.iteration.AbstractCloseableIteration;
import org.eclipse.rdf4j.common.iteration.CloseableIteration;
import org.eclipse.rdf4j.common.iteration.DistinctIteration;
import org.eclipse.rdf4j.common.iteration.Iterations;
import org.eclipse.rdf4j.common.iteration.LimitIteration;
import org.eclipse.rdf4j.common.iteration.OffsetIteration;
import org.eclipse.rdf4j.model.BNode;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.ModelFactory;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.datatypes.XMLDatatypeUtil;
import org.eclipse.rdf4j.model.impl.DynamicModelFactory;
import org.eclipse.rdf4j.model.util.Models;
import org.eclipse.rdf4j.model.vocabulary.XSD;
import org.eclipse.rdf4j.query.impl.BackgroundGraphResult;
import org.eclipse.rdf4j.query.impl.IteratingGraphQueryResult;
import org.eclipse.rdf4j.query.impl.IteratingTupleQueryResult;
import org.eclipse.rdf4j.query.impl.QueueCursor;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.rio.RDFHandler;
import org.eclipse.rdf4j.rio.RDFHandlerException;
import org.eclipse.rdf4j.rio.RDFParser;
import org.eclipse.rdf4j.rio.Rio;
import org.eclipse.rdf4j.rio.UnsupportedRDFormatException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility methods related to query results.
*
* @author Jeen Broekstra
*/
public class QueryResults extends Iterations {
private final static Cleaner cleaner = Cleaner.create();
/**
* Get a {@link Model} containing all elements obtained from the specified query result.
*
* @param iteration the source iteration to get the statements from.
* @return a {@link Model} containing all statements obtained from the specified source iteration.
*/
public static Model asModel(CloseableIteration<? extends Statement> iteration) throws QueryEvaluationException {
try (iteration) {
return asModel(iteration, new DynamicModelFactory());
}
}
/**
* Get a {@link Model} containing all elements obtained from the specified query result.
*
* @param iteration the source iteration to get the statements from.
* @param modelFactory the ModelFactory used to instantiate the model that gets returned.
* @return a {@link Model} containing all statements obtained from the specified source iteration.
*/
public static Model asModel(CloseableIteration<? extends Statement> iteration, ModelFactory modelFactory)
throws QueryEvaluationException {
try (iteration) {
Model model = modelFactory.createEmptyModel();
addAll(iteration, model);
return model;
}
}
/**
* Get a List containing all elements obtained from the specified {@link QueryResult}.
*
* @param queryResult the {@link QueryResult} to get the elements from
* @return a List containing all elements obtained from the specified query result.
*/
public static <T> List<T> asList(QueryResult<T> queryResult) throws QueryEvaluationException {
try (queryResult) {
// stream.collect is slightly slower than addAll for lists
List<T> list = new ArrayList<>();
// addAll closes the iteration
return addAll(queryResult, list);
}
}
/**
* Get a Set containing all elements obtained from the specified {@link QueryResult}.
*
* @param queryResult the {@link QueryResult} to get the elements from
* @return a Set containing all elements obtained from the specified query result.
*/
public static <T> Set<T> asSet(QueryResult<T> queryResult) throws QueryEvaluationException {
try (Stream<T> stream = queryResult.stream()) {
return stream.collect(Collectors.toSet());
}
}
/**
* Returns a list of values of a particular variable out of the QueryResult.
*
* @param result
* @param var variable for which list of values needs to be returned
* @return a list of Values of var
* @throws QueryEvaluationException
*/
public static List<Value> getAllValues(TupleQueryResult result, String var) throws QueryEvaluationException {
try (Stream<BindingSet> stream = result.stream()) {
return result.getBindingNames().contains(var)
? stream.map(bs -> bs.getValue(var)).collect(Collectors.toList())
: Collections.emptyList();
}
}
/**
* Returns a single element from the query result.The QueryResult is automatically closed by this method.
*
* @param result
* @return a single query result element or null
* @throws QueryEvaluationException
*/
public static Statement singleResult(GraphQueryResult result) throws QueryEvaluationException {
try (Stream<Statement> stream = result.stream()) {
return stream.findFirst().orElse(null);
}
}
/**
* Returns a single element from the query result.The QueryResult is automatically closed by this method.
*
* @param result
* @return a single query result element or null
* @throws QueryEvaluationException
*/
public static BindingSet singleResult(TupleQueryResult result) throws QueryEvaluationException {
try (Stream<BindingSet> stream = result.stream()) {
return stream.findFirst().orElse(null);
}
}
/**
* Returns a {@link GraphQueryResult} that filters out any duplicate solutions from the supplied queryResult.
*
* @param queryResult a queryResult containing possible duplicate statements.
* @return a {@link GraphQueryResult} with any duplicates filtered out.
*/
public static GraphQueryResult distinctResults(GraphQueryResult queryResult) {
return new GraphQueryResultFilter(queryResult);
}
/**
* Returns a {@link TupleQueryResult} that filters out any duplicate solutions from the supplied queryResult.
*
* @param queryResult a queryResult containing possible duplicate solutions.
* @return a {@link TupleQueryResult} with any duplicates filtered out.
*/
public static TupleQueryResult distinctResults(TupleQueryResult queryResult) {
return new TupleQueryResultFilter(queryResult);
}
/**
* Returns a {@link TupleQueryResult} that returns at most the specified maximum number of solutions, starting at
* the supplied offset.
*
* @param queryResult a query result possibly containing more solutions than the specified maximum.
* @param limit the maximum number of solutions to return. If set to 0 or lower, no limit will be applied.
* @param offset the number of solutions to skip at the beginning. If set to 0 or lower, no offset will be
* applied.
* @return A {@link TupleQueryResult} that will at return at most the specified maximum number of solutions. If
* neither {@code limit} nor {@code offset} are applied, this returns the original {@code queryResult}.
*/
public static TupleQueryResult limitResults(TupleQueryResult queryResult, long limit, long offset) {
CloseableIteration<BindingSet> iter = queryResult;
if (offset > 0) {
iter = new OffsetIteration<>(iter, offset);
}
if (limit > 0) {
iter = new LimitIteration<>(iter, limit);
}
if (!(iter instanceof TupleQueryResult)) {
return new IteratingTupleQueryResult(queryResult.getBindingNames(), iter);
}
return (TupleQueryResult) iter;
}
/**
* Returns a {@link GraphQueryResult} that returns at most the specified maximum number of solutions, starting at
* the supplied offset.
*
* @param queryResult a query result possibly containing more solutions than the specified maximum.
* @param limit the maximum number of solutions to return. If set to 0 or lower, no limit will be applied.
* @param offset the number of solutions to skip at the beginning. If set to 0 or lower, no offset will be
* applied.
* @return A {@link GraphQueryResult} that will at return at most the specified maximum number of solutions. If
* neither {@code limit} nor {@code offset} are applied, this returns the original {@code queryResult}.
*/
public static GraphQueryResult limitResults(GraphQueryResult queryResult, long limit, long offset) {
CloseableIteration<Statement> iter = queryResult;
if (offset > 0) {
iter = new OffsetIteration<>(iter, offset);
}
if (limit > 0) {
iter = new LimitIteration<>(iter, limit);
}
if (!(iter instanceof GraphQueryResult)) {
return new IteratingGraphQueryResult(queryResult.getNamespaces(), iter);
}
return (GraphQueryResult) iter;
}
/**
* Parses an RDF document and returns it as a GraphQueryResult object, with parsing done on a separate thread in the
* background.<br>
* IMPORTANT: As this method will spawn a new thread in the background, it is vitally important that the resulting
* GraphQueryResult be closed consistently when it is no longer required, to prevent resource leaks.
*
* @param in The {@link InputStream} containing the RDF document.
* @param baseURI The base URI for the RDF document.
* @param format The {@link RDFFormat} of the RDF document.
* @return A {@link GraphQueryResult} that parses in the background, and must be closed to prevent resource leaks.
*/
public static GraphQueryResult parseGraphBackground(InputStream in, String baseURI, RDFFormat format)
throws UnsupportedRDFormatException {
return parseGraphBackground(in, baseURI, Rio.createParser(format));
}
/**
* Parses an RDF document and returns it as a GraphQueryResult object, with parsing done on a separate thread in the
* background.<br>
* IMPORTANT: As this method will spawn a new thread in the background, it is vitally important that the resulting
* GraphQueryResult be closed consistently when it is no longer required, to prevent resource leaks.
*
* @param in The {@link InputStream} containing the RDF document.
* @param baseURI The base URI for the RDF document.
* @param parser The {@link RDFParser}.
* @return A {@link GraphQueryResult} that parses in the background, and must be closed to prevent resource leaks.
*/
public static GraphQueryResult parseGraphBackground(InputStream in, String baseURI, RDFParser parser) {
RDFFormat format = parser.getRDFFormat();
BackgroundGraphResult result = new BackgroundGraphResult(
new QueueCursor<>(new LinkedBlockingQueue<>(1)),
parser, in, format.getCharset(), baseURI);
try {
ForkJoinPool.commonPool().submit(result);
} catch (Throwable t) {
result.close();
throw t;
}
return new CleanerGraphQueryResult(result, cleaner);
}
/**
* Reports a tuple query result to a {@link TupleQueryResultHandler}. <br>
* The {@link TupleQueryResult#close()} method will always be called before this method returns. <br>
* If there is an exception generated by the TupleQueryResult, {@link QueryResultHandler#endQueryResult()} will not
* be called.
*
* @param tqr The query result to report.
* @param handler The handler to report the query result to.
* @throws TupleQueryResultHandlerException If such an exception is thrown by the used query result writer.
*/
public static void report(TupleQueryResult tqr, QueryResultHandler handler)
throws TupleQueryResultHandlerException, QueryEvaluationException {
try (tqr) {
handler.startQueryResult(tqr.getBindingNames());
while (tqr.hasNext()) {
BindingSet bindingSet = tqr.next();
handler.handleSolution(bindingSet);
}
}
handler.endQueryResult();
}
/**
* Reports a graph query result to an {@link RDFHandler}. <br>
* The {@link GraphQueryResult#close()} method will always be called before this method returns.<br>
* If there is an exception generated by the GraphQueryResult, {@link RDFHandler#endRDF()} will not be called.
*
* @param graphQueryResult The query result to report.
* @param rdfHandler The handler to report the query result to.
* @throws RDFHandlerException If such an exception is thrown by the used RDF writer.
* @throws QueryEvaluationException
*/
public static void report(GraphQueryResult graphQueryResult, RDFHandler rdfHandler)
throws RDFHandlerException, QueryEvaluationException {
try (graphQueryResult) {
rdfHandler.startRDF();
for (Map.Entry<String, String> entry : graphQueryResult.getNamespaces().entrySet()) {
String prefix = entry.getKey();
String namespace = entry.getValue();
rdfHandler.handleNamespace(prefix, namespace);
}
while (graphQueryResult.hasNext()) {
Statement st = graphQueryResult.next();
rdfHandler.handleStatement(st);
}
}
rdfHandler.endRDF();
}
/**
* Compares two tuple query results and returns {@code true} if they are equal.Tuple query results are equal if they
* contain the same set of {@link BindingSet}s and have the same headers. Blank nodes identifiers are not relevant
* for equality, they are matched by trying to find compatible mappings between BindingSets. Note that the method
* consumes both query results fully.
*
* @param tqr1 the first {@link TupleQueryResult} to compare.
* @param tqr2 the second {@link TupleQueryResult} to compare.
* @return true if equal
* @throws QueryEvaluationException
*/
public static boolean equals(TupleQueryResult tqr1, TupleQueryResult tqr2) throws QueryEvaluationException {
try (tqr1; tqr2) {
List<BindingSet> list1 = Iterations.asList(tqr1);
List<BindingSet> list2 = Iterations.asList(tqr2);
// Compare the number of statements in both sets
if (list1.size() != list2.size()) {
return false;
}
return matchBindingSets(list1, list2);
}
}
public static boolean isSubset(TupleQueryResult tqr1, TupleQueryResult tqr2) throws QueryEvaluationException {
try (tqr1; tqr2) {
List<BindingSet> list1 = Iterations.asList(tqr1);
List<BindingSet> list2 = Iterations.asList(tqr2);
// Compare the number of statements in both sets
if (list1.size() > list2.size()) {
return false;
}
return matchBindingSets(list1, list2);
}
}
/**
* Compares two graph query results and returns {@code true} if they are equal. Two graph query results are
* considered equal if they are isomorphic graphs. Note that the method consumes both query results fully.
*
* @param result1 the first query result to compare
* @param result2 the second query result to compare.
* @return {@code true} if the supplied graph query results are isomorphic graphs, {@code false} otherwise.
* @throws QueryEvaluationException
* @see Models#isomorphic(Iterable, Iterable)
*/
public static boolean equals(GraphQueryResult result1, GraphQueryResult result2) throws QueryEvaluationException {
try (result1; result2) {
Set<? extends Statement> graph1 = Iterations.asSet(result1);
Set<? extends Statement> graph2 = Iterations.asSet(result2);
return Models.isomorphic(graph1, graph2);
}
}
private static boolean matchBindingSets(List<? extends BindingSet> queryResult1,
Iterable<? extends BindingSet> queryResult2) {
return matchBindingSets(queryResult1, queryResult2, new HashMap<>(), 0);
}
/**
* A recursive method for finding a complete mapping between blank nodes in queryResult1 and blank nodes in
* queryResult2. The algorithm does a depth-first search trying to establish a mapping for each blank node occurring
* in queryResult1.
*
* @return true if a complete mapping has been found, false otherwise.
*/
private static boolean matchBindingSets(List<? extends BindingSet> queryResult1,
Iterable<? extends BindingSet> queryResult2, Map<BNode, BNode> bNodeMapping, int idx) {
if (idx < queryResult1.size()) {
BindingSet bs1 = queryResult1.get(idx);
List<BindingSet> matchingBindingSets = findMatchingBindingSets(bs1, queryResult2, bNodeMapping);
for (BindingSet bs2 : matchingBindingSets) {
// Map bNodes in bs1 to bNodes in bs2
Map<BNode, BNode> newBNodeMapping = new HashMap<>(bNodeMapping);
for (Binding binding : bs1) {
if (binding.getValue() instanceof BNode) {
newBNodeMapping.put((BNode) binding.getValue(), (BNode) bs2.getValue(binding.getName()));
}
}
// FIXME: this recursive implementation has a high risk of
// triggering a stack overflow
// Enter recursion
if (matchBindingSets(queryResult1, queryResult2, newBNodeMapping, idx + 1)) {
// models match, look no further
return true;
}
}
} else {
// All statements have been mapped successfully
return true;
}
return false;
}
private static List<BindingSet> findMatchingBindingSets(BindingSet st, Iterable<? extends BindingSet> model,
Map<BNode, BNode> bNodeMapping) {
List<BindingSet> result = new ArrayList<>();
for (BindingSet modelSt : model) {
if (bindingSetsMatch(st, modelSt, bNodeMapping)) {
// All components possibly match
result.add(modelSt);
}
}
return result;
}
private static boolean bindingSetsMatch(BindingSet bs1, BindingSet bs2, Map<BNode, BNode> bNodeMapping) {
if (bs1.size() != bs2.size()) {
return false;
}
for (Binding binding1 : bs1) {
Value value1 = binding1.getValue();
Value value2 = bs2.getValue(binding1.getName());
if (value1 instanceof BNode && value2 instanceof BNode) {
BNode mappedBNode = bNodeMapping.get(value1);
if (mappedBNode != null) {
// bNode 'value1' was already mapped to some other bNode
if (!value2.equals(mappedBNode)) {
// 'value1' and 'value2' do not match
return false;
}
} else {
// 'value1' was not yet mapped, we need to check if 'value2' is a
// possible mapping candidate
if (bNodeMapping.containsValue(value2)) {
// 'value2' is already mapped to some other value.
return false;
}
}
} else {
// values are not (both) bNodes
if (value1 instanceof Literal && value2 instanceof Literal) {
// do literal value-based comparison for supported datatypes
Literal leftLit = (Literal) value1;
Literal rightLit = (Literal) value2;
IRI dt1 = leftLit.getDatatype();
IRI dt2 = rightLit.getDatatype();
if (dt1 != null && dt1.equals(dt2)
&& XMLDatatypeUtil.isValidValue(leftLit.getLabel(), dt1)
&& XMLDatatypeUtil.isValidValue(rightLit.getLabel(), dt2)) {
Integer compareResult = null;
if (dt1.equals(XSD.DOUBLE)) {
compareResult = Double.compare(leftLit.doubleValue(), rightLit.doubleValue());
} else if (dt1.equals(XSD.FLOAT)) {
compareResult = Float.compare(leftLit.floatValue(), rightLit.floatValue());
} else if (dt1.equals(XSD.DECIMAL)) {
compareResult = leftLit.decimalValue().compareTo(rightLit.decimalValue());
} else if (XMLDatatypeUtil.isIntegerDatatype(dt1)) {
compareResult = leftLit.integerValue().compareTo(rightLit.integerValue());
} else if (dt1.equals(XSD.BOOLEAN)) {
Boolean leftBool = leftLit.booleanValue();
Boolean rightBool = rightLit.booleanValue();
compareResult = leftBool.compareTo(rightBool);
} else if (XMLDatatypeUtil.isCalendarDatatype(dt1)) {
XMLGregorianCalendar left = leftLit.calendarValue();
XMLGregorianCalendar right = rightLit.calendarValue();
compareResult = left.compare(right);
}
if (compareResult != null) {
if (compareResult != 0) {
return false;
}
} else if (!value1.equals(value2)) {
return false;
}
} else if (!value1.equals(value2)) {
return false;
}
} else if (!value1.equals(value2)) {
return false;
}
}
}
return true;
}
/**
* Check whether two {@link BindingSet}s are compatible. Two binding sets are compatible if they have equal values
* for each variable that is bound in both binding sets.
*
* @param bs1
* @param bs2
* @return true if compatible
*/
public static boolean bindingSetsCompatible(BindingSet bs1, BindingSet bs2) {
Set<String> bs1BindingNames = bs1.getBindingNames();
if (bs1BindingNames.isEmpty()) {
return true;
}
Set<String> bs2BindingNames = bs2.getBindingNames();
for (Binding binding : bs1) {
if (bs2BindingNames.contains(binding.getName())) {
Value value1 = binding.getValue();
// if a variable is unbound in one set it is compatible
if (value1 != null) {
Value value2 = bs2.getValue(binding.getName());
// if a variable is unbound in one set it is compatible
if (value2 != null && !value1.equals(value2)) {
return false;
}
}
}
}
return true;
}
private static class GraphQueryResultFilter extends AbstractCloseableIteration<Statement>
implements GraphQueryResult {
private final DistinctIteration<Statement> filter;
private final GraphQueryResult unfiltered;
public GraphQueryResultFilter(GraphQueryResult wrappedResult) {
this(wrappedResult, new HashSet<>());
}
public GraphQueryResultFilter(GraphQueryResult wrappedResult, Set<Statement> distinctSet) {
this.filter = new DistinctIteration<>(wrappedResult, distinctSet);
this.unfiltered = wrappedResult;
}
@Override
public boolean hasNext() throws QueryEvaluationException {
if (isClosed()) {
return false;
}
boolean result = filter.hasNext();
if (!result) {
close();
}
return result;
}
@Override
public Statement next() throws QueryEvaluationException {
if (isClosed()) {
throw new NoSuchElementException("The iteration has been closed.");
}
try {
return filter.next();
} catch (NoSuchElementException e) {
close();
throw e;
}
}
@Override
public void remove() throws QueryEvaluationException {
if (isClosed()) {
throw new IllegalStateException("The iteration has been closed.");
}
try {
filter.remove();
} catch (IllegalStateException e) {
close();
throw e;
}
}
@Override
public void handleClose() throws QueryEvaluationException {
filter.close();
}
@Override
public Map<String, String> getNamespaces() throws QueryEvaluationException {
return unfiltered.getNamespaces();
}
}
private static class TupleQueryResultFilter extends AbstractCloseableIteration<BindingSet>
implements TupleQueryResult {
private final DistinctIteration<BindingSet> filter;
private final TupleQueryResult unfiltered;
public TupleQueryResultFilter(TupleQueryResult wrappedResult) {
this(wrappedResult, new HashSet<>());
}
public TupleQueryResultFilter(TupleQueryResult wrappedResult, Set<BindingSet> distinct) {
this.filter = new DistinctIteration<>(wrappedResult, distinct);
this.unfiltered = wrappedResult;
}
@Override
public boolean hasNext() throws QueryEvaluationException {
if (isClosed()) {
return false;
}
boolean result = filter.hasNext();
if (!result) {
close();
}
return result;
}
@Override
public BindingSet next() throws QueryEvaluationException {
if (isClosed()) {
throw new NoSuchElementException("The iteration has been closed.");
}
try {
return filter.next();
} catch (NoSuchElementException e) {
close();
throw e;
}
}
@Override
public void remove() throws QueryEvaluationException {
if (isClosed()) {
throw new IllegalStateException("The iteration has been closed.");
}
try {
filter.remove();
} catch (IllegalStateException e) {
close();
throw e;
}
}
@Override
public void handleClose() throws QueryEvaluationException {
filter.close();
}
@Override
public List<String> getBindingNames() throws QueryEvaluationException {
return unfiltered.getBindingNames();
}
}
private static class CleanerGraphQueryResult implements GraphQueryResult {
private static final Logger logger = LoggerFactory.getLogger(CleanerGraphQueryResult.class);
private final GraphQueryResult delegate;
private final Cleaner.Cleanable cleanable;
private final CleanableState state;
public CleanerGraphQueryResult(GraphQueryResult delegate, Cleaner cleaner) {
this.delegate = delegate;
this.state = new CleanableState(delegate);
this.cleanable = cleaner.register(this, state);
}
@Override
public void close() {
state.close();
cleanable.clean();
}
@Override
public boolean hasNext() {
return delegate.hasNext();
}
@Override
public Statement next() {
return delegate.next();
}
@Override
public void remove() {
delegate.remove();
}
@Override
public Map<String, String> getNamespaces() throws QueryEvaluationException {
return delegate.getNamespaces();
}
private final static class CleanableState implements Runnable {
private final GraphQueryResult iteration;
private boolean closed = false;
public CleanableState(GraphQueryResult iteration) {
this.iteration = iteration;
}
@Override
public void run() {
if (!closed) {
try {
logger.warn(
"Forced closing of unclosed iteration. Set the system property 'org.eclipse.rdf4j.repository.debug' to 'true' to get stack traces.");
iteration.close();
} catch (Exception e) {
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
throw new RuntimeException(e);
}
}
}
public void close() {
closed = true;
iteration.close();
}
}
}
}