RelationMapBuilder.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;

import static org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf.iri;
import static org.eclipse.rdf4j.spring.util.QueryResultUtils.getIRI;
import static org.eclipse.rdf4j.spring.util.QueryResultUtils.getIRIOptional;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.sparqlbuilder.core.Projectable;
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.graphpattern.GraphPattern;
import org.eclipse.rdf4j.sparqlbuilder.graphpattern.TriplePattern;
import org.eclipse.rdf4j.sparqlbuilder.rdf.RdfPredicate;
import org.eclipse.rdf4j.spring.dao.support.bindingsBuilder.BindingsBuilder;
import org.eclipse.rdf4j.spring.dao.support.opbuilder.TupleQueryEvaluationBuilder;
import org.eclipse.rdf4j.spring.support.RDF4JTemplate;

/**
 * @author Florian Kleedorfer
 * @since 4.0.0
 */
public class RelationMapBuilder {
	public static final Variable _relSubject = SparqlBuilder.var("rel_subject");
	public static final Variable _relObject = SparqlBuilder.var("rel_object");
	private static final Variable _relKey = SparqlBuilder.var("rel_key");
	private static final Variable _relValue = SparqlBuilder.var("rel_value");
	private static final IRI NOTHING = SimpleValueFactory.getInstance()
			.createIRI("urn:java:relationDaoSupport:Nothing");
	private final RdfPredicate predicate;
	private GraphPattern[] constraints = new GraphPattern[0];
	private final RDF4JTemplate rdf4JTemplate;
	private boolean isRelationOptional = false;
	private boolean isSubjectKeyed = true;
	private final BindingsBuilder bindingsBuilder = new BindingsBuilder();

	public RelationMapBuilder(RDF4JTemplate rdf4JTemplate, RdfPredicate predicate) {
		this.rdf4JTemplate = rdf4JTemplate;
		this.predicate = predicate;
	}

	public RelationMapBuilder(RDF4JTemplate rdf4JTemplate, IRI predicate) {
		this.rdf4JTemplate = rdf4JTemplate;
		this.predicate = iri(predicate);
	}

	/**
	 * Constrains the result iff the {@link GraphPattern} contains the variables {@link RelationMapBuilder#_relSubject}
	 * and/or {@link RelationMapBuilder#_relObject}, which are the variables in the triple with the {@link RdfPredicate}
	 * specified in the constructor.
	 */
	public RelationMapBuilder constraints(GraphPattern... constraints) {
		this.constraints = constraints;
		return this;
	}

	/**
	 * Indicates that the existence of the triple is not required, allowing to use the constraints to select certain
	 * subjects and to answer the mapping to an empty Set in the {@link RelationMapBuilder#buildOneToMany()} case and
	 * {@link RelationMapBuilder#NOTHING} in the {@link RelationMapBuilder#buildOneToOne()} case.
	 *
	 * @return the builder
	 */
	public RelationMapBuilder relationIsOptional() {
		this.isRelationOptional = true;
		return this;
	}

	/**
	 * Indicates that the builder should use the triple's object for the key in the resulting {@link Map} instead of the
	 * subject (the default).
	 */
	public RelationMapBuilder useRelationObjectAsKey() {
		this.isSubjectKeyed = false;
		return this;
	}

	/**
	 * Builds a One-to-One Map using the configuration of this builder. Throws an Exception if more than one values are
	 * found for a given key. If {@link #isRelationOptional} is <code>true
	 * </code> and no triple is found for the key, {@link #NOTHING} is set as the value.
	 */
	public Map<IRI, IRI> buildOneToOne() {
		return makeTupleQueryBuilder()
				.evaluateAndConvert()
				.toMap(b -> getIRI(b, _relKey), this::getRelationValueOrNothing);
	}

	/**
	 * Builds a One-to-Many Map using the configuration of this builder.
	 */
	public Map<IRI, Set<IRI>> buildOneToMany() {
		return makeTupleQueryBuilder()
				.evaluateAndConvert()
				.mapAndCollect(
						Function.identity(),
						Collectors.toMap(
								b -> getIRI(b, _relKey),
								b -> getIRIOptional(b, _relValue)
										.map(Set::of)
										.orElseGet(Set::of),
								RelationMapBuilder::mergeSets));
	}

	private static <T> Set<T> mergeSets(Set<T> left, Set<T> right) {
		Set<T> merged = new HashSet<>(left);
		merged.addAll(right);
		return merged;
	}

	private IRI getRelationValue(BindingSet b) {
		if (isRelationOptional) {
			return getIRIOptional(b, _relValue).orElse(NOTHING);
		} else {
			return getIRI(b, _relValue);
		}
	}

	private IRI getRelationValueOrNothing(BindingSet b) {
		if (isRelationOptional) {
			return getIRIOptional(b, _relValue).orElse(NOTHING);
		} else {
			return getIRI(b, _relValue);
		}
	}

	private TupleQueryEvaluationBuilder makeTupleQueryBuilder() {
		return rdf4JTemplate
				.tupleQuery(makeQueryString())
				.withBindings(bindingsBuilder.build());
	}

	String makeQueryString() {
		return Queries.SELECT(getProjection())
				.where(getWhereClause())
				.distinct()
				.getQueryString();
	}

	private Projectable[] getProjection() {
		if (this.isSubjectKeyed) {
			return new Projectable[] {
					SparqlBuilder.as(_relSubject, _relKey), SparqlBuilder.as(_relObject, _relValue)
			};
		} else {
			return new Projectable[] {
					SparqlBuilder.as(_relSubject, _relValue), SparqlBuilder.as(_relObject, _relKey)
			};
		}
	}

	private GraphPattern[] getWhereClause() {
		TriplePattern tp = _relSubject.has(predicate, _relObject);
		GraphPattern[] ret = new GraphPattern[constraints.length + 1];
		if (this.isRelationOptional) {
			ret[constraints.length] = tp.optional();
		} else {
			ret[constraints.length] = tp;
		}
		System.arraycopy(constraints, 0, ret, 0, constraints.length);
		return ret;
	}

	public RelationMapBuilder withBinding(Variable key, Value value) {
		bindingsBuilder.add(key, value);
		return this;
	}

	public RelationMapBuilder withBinding(String key, Value value) {
		bindingsBuilder.add(key, value);
		return this;
	}

	public RelationMapBuilder withBindingMaybe(Variable key, Value value) {
		bindingsBuilder.addMaybe(key, value);
		return this;
	}

	public RelationMapBuilder withBindingMaybe(String key, Value value) {
		bindingsBuilder.addMaybe(key, value);
		return this;
	}

	public RelationMapBuilder withBinding(Variable key, IRI value) {
		bindingsBuilder.add(key, value);
		return this;
	}

	public RelationMapBuilder withBinding(String key, IRI value) {
		bindingsBuilder.add(key, value);
		return this;
	}

	public RelationMapBuilder withBindingMaybe(Variable key, IRI value) {
		bindingsBuilder.addMaybe(key, value);
		return this;
	}

	public RelationMapBuilder withBindingMaybe(Variable key, String value) {
		bindingsBuilder.addMaybe(key, value);
		return this;
	}

	public RelationMapBuilder withBindingMaybe(String key, IRI value) {
		bindingsBuilder.addMaybe(key, value);
		return this;
	}

	public RelationMapBuilder withBinding(Variable key, String value) {
		bindingsBuilder.add(key, value);
		return this;
	}

	public RelationMapBuilder withBinding(String key, String value) {
		bindingsBuilder.add(key, value);
		return this;
	}

	public RelationMapBuilder withBindingMaybe(String key, String value) {
		bindingsBuilder.addMaybe(key, value);
		return this;
	}

	public RelationMapBuilder withBinding(Variable key, Integer value) {
		bindingsBuilder.add(key, value);
		return this;
	}

	public RelationMapBuilder withBinding(String key, Integer value) {
		bindingsBuilder.add(key, value);
		return this;
	}

	public RelationMapBuilder withBindingMaybe(Variable key, Integer value) {
		bindingsBuilder.addMaybe(key, value);
		return this;
	}

	public RelationMapBuilder withBindingMaybe(String key, Integer value) {
		bindingsBuilder.addMaybe(key, value);
		return this;
	}

	public RelationMapBuilder withBinding(Variable key, Boolean value) {
		bindingsBuilder.add(key, value);
		return this;
	}

	public RelationMapBuilder withBinding(String key, Boolean value) {
		bindingsBuilder.add(key, value);
		return this;
	}

	public RelationMapBuilder withBindingMaybe(Variable key, Boolean value) {
		bindingsBuilder.addMaybe(key, value);
		return this;
	}

	public RelationMapBuilder withBindingMaybe(String key, Boolean value) {
		bindingsBuilder.addMaybe(key, value);
		return this;
	}

	public RelationMapBuilder withBinding(Variable key, Float value) {
		bindingsBuilder.add(key, value);
		return this;
	}

	public RelationMapBuilder withBinding(String key, Float value) {
		bindingsBuilder.add(key, value);
		return this;
	}

	public RelationMapBuilder withBindingMaybe(Variable key, Float value) {
		bindingsBuilder.addMaybe(key, value);
		return this;
	}

	public RelationMapBuilder withBindingMaybe(String key, Float value) {
		bindingsBuilder.addMaybe(key, value);
		return this;
	}

	public RelationMapBuilder withBinding(Variable key, Double value) {
		bindingsBuilder.add(key, value);
		return this;
	}

	public RelationMapBuilder withBinding(String key, Double value) {
		bindingsBuilder.add(key, value);
		return this;
	}

	public RelationMapBuilder withBindingMaybe(Variable var, Double value) {
		bindingsBuilder.addMaybe(var, value);
		return this;
	}

	public RelationMapBuilder withBindingMaybe(String key, Double value) {
		bindingsBuilder.addMaybe(key, value);
		return this;
	}
}