TupleQueryResultConverter.java

/*******************************************************************************
 * Copyright (c) 2021 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.spring.dao.support.operation;

import static java.util.stream.Collectors.mapping;

import static org.eclipse.rdf4j.spring.dao.exception.mapper.ExceptionMapper.mapException;
import static org.eclipse.rdf4j.spring.dao.support.operation.OperationUtils.require;

import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.rdf4j.common.exception.RDF4JException;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.query.TupleQueryResult;
import org.eclipse.rdf4j.spring.dao.support.BindingSetMapper;
import org.eclipse.rdf4j.spring.dao.support.MappingPostProcessor;
import org.eclipse.rdf4j.spring.dao.support.TupleQueryResultMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author Florian Kleedorfer
 * @since 4.0.0
 */
public class TupleQueryResultConverter {
	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
	private TupleQueryResult tupleQueryResult;

	public TupleQueryResultConverter(TupleQueryResult result) {
		Objects.requireNonNull(result);
		this.tupleQueryResult = result;
	}

	/**
	 * Passes the {@link TupleQueryResult} to the consumer and closes the result afterwards.
	 */
	public void consumeResult(Consumer<TupleQueryResult> consumer) {
		try {
			consumer.accept(tupleQueryResult);
		} catch (Exception e) {
			logger.debug("Caught execption while processing TupleQueryResult: {}", e.getMessage());
			RDF4JException mapped = mapException("Error processing TupleQueryResult", e);
			logger.debug("Re-throwing as {} ", mapped.getClass().getSimpleName());
			throw mapped;
		} finally {
			tupleQueryResult.close();
			tupleQueryResult = null;
		}
	}

	/**
	 * Applies the function to the {@link TupleQueryResult} and closes the result afterwards.
	 */
	public <T> T applyToResult(Function<TupleQueryResult, T> function) {
		try {
			return function.apply(tupleQueryResult);
		} catch (Exception e) {
			logger.debug("Caught execption while processing TupleQueryResult: {}", e.getMessage());
			RDF4JException mapped = mapException("Error processing TupleQueryResult", e);
			logger.debug("Re-throwing as {} ", mapped.getClass().getSimpleName());
			throw mapped;
		} finally {
			tupleQueryResult.close();
			tupleQueryResult = null;
		}
	}

	/**
	 * Obtains a stream of {@link BindingSet}s. The result is completely consumed and closed when the stream is
	 * returned.
	 */
	public Stream<BindingSet> toStream() {
		return applyToResult(r -> getBindingStream(r).collect(Collectors.toList()).stream());
	}

	private <T> Stream<T> toStreamInternal(Function<BindingSet, T> mapper) {
		return applyToResult(
				result -> getBindingStream(result)
						.map(mapper)
						.filter(Objects::nonNull)
						.collect(Collectors.toList())
						.stream());
	}

	/**
	 * Obtains a {@link Stream} of mapped query results. The result is completely consumed and closed when the stream is
	 * returned. Any null values are filterd from the resulting stream.
	 */
	public <T> Stream<T> toStream(BindingSetMapper<T> mapper) {
		return toStreamInternal(mapper);
	}

	/**
	 * Obtains a {@link Stream} of mapped query results, using the postprocessor to map it again. Any null values are
	 * filtered from the resulting stream.
	 */
	public <T, O> Stream<O> toStream(
			BindingSetMapper<T> mapper, MappingPostProcessor<T, O> postProcessor) {
		return toStreamInternal(andThenOrElseNull(mapper, postProcessor));
	}

	/**
	 * Maps the whole {@link TupleQueryResult} to one object, which may be null.
	 */
	public <T> T toSingletonMaybeOfWholeResult(TupleQueryResultMapper<T> mapper) {
		return applyToResult(mapper);
	}

	/**
	 * Maps the whole {@link TupleQueryResult} to one {@link Optional}.
	 */
	public <T> Optional<T> toSingletonOptionalOfWholeResult(TupleQueryResultMapper<T> mapper) {
		return Optional.ofNullable(toSingletonMaybeOfWholeResult(mapper));
	}

	/**
	 * Maps the whole {@link TupleQueryResult} to an object, throwing an exception if the mapper returns
	 * <code>null</code>.
	 *
	 * @throws org.eclipse.rdf4j.spring.dao.exception.IncorrectResultSetSizeException
	 */
	public <T> T toSingletonOfWholeResult(TupleQueryResultMapper<T> mapper) {
		return require(toSingletonOptionalOfWholeResult(mapper));
	}

	/**
	 * Maps the first {@link BindingSet} in the result if one exists, throwing an exception if there are more. Returns
	 * null if there are no results or if there is one result that is mapped to null by the specified mapper.
	 *
	 * @throws org.eclipse.rdf4j.spring.dao.exception.IncorrectResultSetSizeException
	 */
	public <T> T toSingletonMaybe(BindingSetMapper<T> mapper) {
		return mapAndCollect(mapper, OperationUtils.toSingletonMaybe());
	}

	/**
	 * Maps the first {@link BindingSet} in the result, throwing an exception if there are more than one. Returns an
	 * Optional, which is empty if there are no results or if there is one result that is mapped to null by the
	 * specified mapper.
	 */
	public <T> Optional<T> toSingletonOptional(BindingSetMapper<T> mapper) {
		return Optional.ofNullable(toSingletonMaybe(mapper));
	}

	/**
	 * Maps the first {@link BindingSet} in the result, throwing an exception if there are no results or more than one.
	 *
	 * @throws org.eclipse.rdf4j.spring.dao.exception.IncorrectResultSetSizeException
	 */
	public <T> T toSingleton(BindingSetMapper<T> mapper) {
		return require(toSingletonOptional(mapper));
	}

	/**
	 * Maps the first {@link BindingSet} in the result if one exists, throwing an exception if there are more.
	 *
	 * @throws org.eclipse.rdf4j.spring.dao.exception.IncorrectResultSetSizeException
	 */
	public <T, O> O toSingletonMaybe(
			BindingSetMapper<T> mapper, MappingPostProcessor<T, O> postProcessor) {
		return mapAndCollect(andThenOrElseNull(mapper, postProcessor), OperationUtils.toSingletonMaybe());
	}

	public <T, O> Optional<O> toSingletonOptional(
			BindingSetMapper<T> mapper, MappingPostProcessor<T, O> postProcessor) {
		return Optional.ofNullable(toSingletonMaybe(mapper, postProcessor));
	}

	/**
	 * Maps the first {@link BindingSet} in the result, throwing an exception if there are no results or more than one.
	 *
	 * @throws org.eclipse.rdf4j.spring.dao.exception.IncorrectResultSetSizeException
	 */
	public <T, O> O toSingleton(
			BindingSetMapper<T> mapper, MappingPostProcessor<T, O> postProcessor) {
		return require(toSingletonOptional(mapper, postProcessor));
	}

	public <T, A, R> R mapAndCollect(Function<BindingSet, T> mapper, Collector<T, A, R> collector) {
		return applyToResult(
				result -> getBindingStream(result)
						.map(mapper)
						.filter(Objects::nonNull)
						.collect(collector));
	}

	/**
	 * Maps the query result to a {@link List}.
	 */
	public <T> List<T> toList(BindingSetMapper<T> mapper) {
		return mapAndCollect(mapper, Collectors.toList());
	}

	/**
	 * Maps the query result to a {@link List}.
	 */
	public <T, O> List<O> toList(
			BindingSetMapper<T> mapper, MappingPostProcessor<T, O> postProcessor) {
		return mapAndCollect(andThenOrElseNull(mapper, postProcessor), Collectors.toList());
	}

	/**
	 * Maps the query result to a {@link Set}.
	 */
	public <T> Set<T> toSet(BindingSetMapper<T> mapper) {
		return mapAndCollect(mapper, Collectors.toSet());
	}

	/**
	 * Maps the query result to a {@link Set}.
	 */
	public <T, O> Set<O> toSet(
			BindingSetMapper<T> mapper, MappingPostProcessor<T, O> postProcessor) {
		return mapAndCollect(andThenOrElseNull(mapper, postProcessor), Collectors.toSet());
	}

	/**
	 * Maps the query result to a {@link Map}, throwing an Exception if there are multiple values for one key.
	 */
	public <K, V> Map<K, V> toMap(
			Function<BindingSet, K> keyMapper, Function<BindingSet, V> valueMapper) {
		return mapAndCollect(Function.identity(), Collectors.toMap(keyMapper, valueMapper));
	}

	/**
	 * Maps the query result to a {@link Map} of {@link Set}s.
	 */
	public <K, V> Map<K, Set<V>> toMapOfSet(
			Function<BindingSet, K> keyMapper, Function<BindingSet, V> valueMapper) {
		return mapAndCollect(
				Function.identity(),
				Collectors.groupingBy(
						keyMapper, Collectors.mapping(valueMapper, Collectors.toSet())));
	}

	/**
	 * Maps the query result to a {@link Map} of {@link List}s.
	 */
	public <K, V> Map<K, List<V>> toMapOfList(
			Function<BindingSet, K> keyMapper, Function<BindingSet, V> valueMapper) {
		return mapAndCollect(
				Function.identity(),
				Collectors.groupingBy(
						keyMapper, Collectors.mapping(valueMapper, Collectors.toList())));
	}

	/**
	 * Maps the query result to a {@link Map}, throwing an Exception if there are multiple values for one key.
	 */
	public <T, K, V> Map<K, V> toMap(
			BindingSetMapper<T> mapper, Function<T, K> keyMapper, Function<T, V> valueMapper) {
		return mapAndCollect(mapper, Collectors.toMap(keyMapper, valueMapper));
	}

	/**
	 * Maps the query result to a {@link Map}, throwing an Exception if there are multiple values for one key.
	 */
	public <K, V> Map<K, V> toMap(Function<BindingSet, Map.Entry<K, V>> entryMapper) {
		return mapAndCollect(
				Function.identity(),
				Collectors.toMap(
						bs -> entryMapper.apply(bs).getKey(),
						bs -> entryMapper.apply(bs).getValue()));
	}

	/**
	 * Maps the query result to a {@link Map} of {@link Set}s.
	 */
	public <T, K, V> Map<K, Set<V>> toMapOfSet(
			BindingSetMapper<T> mapper, Function<T, K> keyMapper, Function<T, V> valueMapper) {
		return mapAndCollect(
				mapper, Collectors.groupingBy(keyMapper, mapping(valueMapper, Collectors.toSet())));
	}

	/**
	 * Maps the query result to a {@link Map} of {@link List}s.
	 */
	public <T, K, V> Map<K, List<V>> toMapOfList(
			BindingSetMapper<T> mapper, Function<T, K> keyMapper, Function<T, V> valueMapper) {
		return mapAndCollect(
				mapper,
				Collectors.groupingBy(keyMapper, mapping(valueMapper, Collectors.toList())));
	}

	/**
	 * If the result has only one empty binding set, this method returns an empty stream, otherwise the stream of
	 * BindingSets
	 */
	public Stream<BindingSet> getBindingStream(TupleQueryResult result) {
		if (!result.hasNext()) {
			return Stream.empty();
		}
		BindingSet first = result.next();
		if (!result.hasNext() && first.isEmpty()) {
			return Stream.empty();
		}
		return Stream.concat(Stream.of(first), result.stream());
	}

	/**
	 * Executes <code>mapper.andThen(postProcessor)</code> unless the result of <code>mapper</code> is null, in which
	 * case the result is <code>null</code>.
	 */
	private <T, O> Function<BindingSet, O> andThenOrElseNull(
			BindingSetMapper<T> mapper, MappingPostProcessor<T, O> postProcessor) {
		return bindingSet -> Optional.ofNullable(mapper.apply(bindingSet))
				.map(postProcessor)
				.orElse(null);
	}

}