diff --git a/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQuery.java b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQuery.java new file mode 100644 index 00000000000..28e9dfe5342 --- /dev/null +++ b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQuery.java @@ -0,0 +1,50 @@ +/* + * ***************************************************************************** + * 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.spring.dao.support.join; + +import java.util.function.Supplier; + +import org.eclipse.rdf4j.sparqlbuilder.core.SparqlBuilder; +import org.eclipse.rdf4j.sparqlbuilder.core.Variable; +import org.eclipse.rdf4j.spring.support.RDF4JTemplate; + +/** + * Creates a reusable {@link org.eclipse.rdf4j.query.TupleQuery} (and takes care of it getting reused properly using + * {@link RDF4JTemplate#tupleQuery(Class, String, Supplier)}). + * + *

+ * The JoinQuery is created using the {@link JoinQueryBuilder}. + * + *

+ * To set bindings and execute a {@link JoinQuery}, obtain the {@link JoinQueryEvaluationBuilder} via + * {@link #evaluationBuilder(RDF4JTemplate)}. + */ +public class JoinQuery { + + public static final Variable _sourceEntity = SparqlBuilder.var("sourceEntity"); + public static final Variable _targetEntity = SparqlBuilder.var("targetEntity"); + + private final String queryString; + + JoinQuery(JoinQueryBuilder joinQueryBuilder) { + this.queryString = joinQueryBuilder.makeQueryString(); + } + + public JoinQueryEvaluationBuilder evaluationBuilder(RDF4JTemplate rdf4JTemplate) { + return new JoinQueryEvaluationBuilder( + rdf4JTemplate.tupleQuery( + getClass(), + this.getClass().getName() + "@" + this.hashCode(), + () -> this.queryString)); + } +} diff --git a/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQueryBuilder.java b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQueryBuilder.java new file mode 100644 index 00000000000..034a75aa673 --- /dev/null +++ b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQueryBuilder.java @@ -0,0 +1,189 @@ +/* + * ***************************************************************************** + * 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.spring.dao.support.join; + +import static org.eclipse.rdf4j.sparqlbuilder.rdf.Rdf.iri; + +import java.util.function.Consumer; +import java.util.function.Function; + +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.sparqlbuilder.constraint.propertypath.builder.EmptyPropertyPathBuilder; +import org.eclipse.rdf4j.sparqlbuilder.constraint.propertypath.builder.PropertyPathBuilder; +import org.eclipse.rdf4j.sparqlbuilder.core.Projectable; +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.rdf.RdfPredicate; +import org.eclipse.rdf4j.spring.support.RDF4JTemplate; + +/** + * Builder for the {@link JoinQuery}. Allows for building the JoinQuery object directly via + * {@link #build(RDF4JTemplate)}, and for building a lazy initizalizer via {@link #buildLazyInitializer()}. + * + *

+ * You would use the lazy initializer like so: + * + *

+ * public class MyDao extends RDF4JDAO {
+ * 	// ...
+ *
+ * 	private static final LazyJoinQueryInitizalizer lazyJoinQuery = JoinQueryBuilder.of(SKOS.broader)
+ * 			// .. configure your join
+ * 			.buildLazyInitializer();
+ *
+ * 	public Map> getJoinedData(IRI sourceEntityId) {
+ * 		return lazyJoinQuery.get(getRdf4JTemplate())
+ * 				.withSourceEntityIdBinding(sourceEntityId)
+ * 				.buildOneToMany();
+ * 	}
+ *
+ * }
+ *
+ * 
+ */ +public class JoinQueryBuilder { + + private final RdfPredicate predicate; + private GraphPattern subjectConstraints = null; + private GraphPattern objectConstraints = null; + private JoinType joinType = JoinType.INNER; + + private JoinQueryBuilder(Consumer propertyPathConfigurer) { + EmptyPropertyPathBuilder propertyPathBuilder = new EmptyPropertyPathBuilder(); + propertyPathConfigurer.accept(propertyPathBuilder); + this.predicate = propertyPathBuilder.build(); + } + + private JoinQueryBuilder(RdfPredicate predicate) { + this.predicate = predicate; + } + + private JoinQueryBuilder(IRI predicate) { + this.predicate = iri(predicate); + } + + public static JoinQueryBuilder of(RdfPredicate rdfPredicate) { + return new JoinQueryBuilder(rdfPredicate); + } + + public static JoinQueryBuilder of(IRI predicate) { + return new JoinQueryBuilder(predicate); + } + + public static JoinQueryBuilder of(Consumer propertyPathConfigurer) { + return new JoinQueryBuilder(propertyPathConfigurer); + } + + public static JoinQueryBuilder of( + RDF4JTemplate rdf4JTemplate, PropertyPathBuilder propertyPathBuilder) { + return new JoinQueryBuilder(() -> propertyPathBuilder.build().getQueryString()); + } + + public JoinQueryBuilder sourceEntityConstraints( + Function constraintBuilder) { + this.subjectConstraints = constraintBuilder.apply(JoinQuery._sourceEntity); + return this; + } + + public JoinQueryBuilder targetEntityConstraints( + Function constraintBuilder) { + this.objectConstraints = constraintBuilder.apply(JoinQuery._targetEntity); + return this; + } + + /** + * Return only results where the relation is present and subjectConstraints and objectConstraints are satisfied. + * + * @return + */ + public JoinQueryBuilder innerJoin() { + this.joinType = JoinType.INNER; + return this; + } + + /** + * Return results where subjectConstraints are satisfied. The existence of the relation is optional, but + * objectConstraints must be satisfied where the relation exists. + * + * @return + */ + public JoinQueryBuilder leftOuterJoin() { + this.joinType = JoinType.LEFT_OUTER; + return this; + } + + /** + * Return results where objectConstraints are satisfied, The existence of the relation is optional, and + * subjectConstraints are satisfied where the relation exists. + * + * @return + */ + public JoinQueryBuilder rightOuterJoin() { + this.joinType = JoinType.RIGHT_OUTER; + return this; + } + + public JoinQuery build() { + return new JoinQuery(this); + } + + public LazyJoinQueryInitizalizer buildLazyInitializer() { + return new LazyJoinQueryInitizalizer(this); + } + + String makeQueryString() { + return Queries.SELECT(getProjection()).where(getWhereClause()).distinct().getQueryString(); + } + + private Projectable[] getProjection() { + return new Projectable[] { JoinQuery._sourceEntity, JoinQuery._targetEntity }; + } + + private GraphPattern andIfPresent(GraphPattern leftOrNull, GraphPattern rightOrNull) { + if (rightOrNull == null) { + return leftOrNull; + } + if (leftOrNull == null) { + if (rightOrNull == null) { + throw new UnsupportedOperationException("left or right parameter must be non-null"); + } + return rightOrNull; + } + return leftOrNull.and(rightOrNull); + } + + private GraphPattern optionalIfPresent(GraphPattern patternOrNull) { + if (patternOrNull == null) { + return null; + } + return patternOrNull.optional(); + } + + private GraphPattern getWhereClause() { + GraphPattern relation = JoinQuery._sourceEntity.has(predicate, JoinQuery._targetEntity); + switch (this.joinType) { + case INNER: + return andIfPresent( + andIfPresent(relation, this.subjectConstraints), this.objectConstraints); + case LEFT_OUTER: + return andIfPresent( + this.subjectConstraints, + andIfPresent(relation, this.objectConstraints).optional()); + case RIGHT_OUTER: + return andIfPresent( + this.objectConstraints, + andIfPresent(relation, this.subjectConstraints).optional()); + } + throw new UnsupportedOperationException("Join type Not supported: " + this.joinType); + } +} diff --git a/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQueryEvaluationBuilder.java b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQueryEvaluationBuilder.java new file mode 100644 index 00000000000..deb12cecddf --- /dev/null +++ b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinQueryEvaluationBuilder.java @@ -0,0 +1,318 @@ +/* + * ***************************************************************************** + * 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.spring.dao.support.join; + +import static org.eclipse.rdf4j.spring.util.QueryResultUtils.getIRI; +import static org.eclipse.rdf4j.spring.util.QueryResultUtils.getIRIOptional; + +import java.util.*; +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.sparqlbuilder.core.Variable; +import org.eclipse.rdf4j.spring.dao.support.bindingsBuilder.BindingsBuilder; +import org.eclipse.rdf4j.spring.dao.support.opbuilder.TupleQueryEvaluationBuilder; +import org.eclipse.rdf4j.spring.dao.support.operation.TupleQueryResultConverter; +import org.eclipse.rdf4j.spring.support.RDF4JTemplate; + +/** + * Encapsulates all the state required for one execution of the query and provides methods for obtaining the result in + * different forms + * + *

+ * Obtained via {@link JoinQuery#evaluationBuilder(RDF4JTemplate)}. To use a {@link JoinQueryEvaluationBuilder}: + * + *

    + *
  1. set its bindings + *
      + *
    • use {@link #withSourceEntityId(IRI)} and {@link #withTargetEntityId(IRI)} to set either side + *
    • use any `withBinding` method to set other variables you may have used + *
    + *
  2. obtain the results by using any of the `as` methods, such as {@link #asOneToMany()}, {@link #asOneToOne()} + * {@link #asIsPresent()}, etc. + *
+ */ +public class JoinQueryEvaluationBuilder { + private TupleQueryEvaluationBuilder tupleQueryEvaluationBuilder; + private final BindingsBuilder bindingsBuilder = new BindingsBuilder(); + + JoinQueryEvaluationBuilder(TupleQueryEvaluationBuilder tupleQueryEvaluationBuilder) { + this.tupleQueryEvaluationBuilder = tupleQueryEvaluationBuilder; + } + + /** + * Builds a List of 2-component arrays, with the source entity Id in position 0 and the target entity id in position + * 1. Depending on the configuration of the {@link JoinQuery}, either position may be null + * + * @return a list of (source, target) id pairs. + */ + public List asIdPairList() { + return makeTupleQueryBuilder() + .evaluateAndConvert() + .toList( + b -> new IRI[] { + getIRIOptional(b, JoinQuery._sourceEntity).orElse(null), + getIRIOptional(b, JoinQuery._targetEntity).orElse(null) + }); + } + + /** Builds a One-to-One Map using the configuration of this JoinQuery. */ + public Map> asOneToOne() { + return makeTupleQueryBuilder() + .evaluateAndConvert() + .toMap( + b -> getIRI(b, JoinQuery._sourceEntity), + b -> getIRIOptional(b, JoinQuery._targetEntity)); + } + + /** Builds a One-to-Many Map using the configuration of this JoinQuery. */ + public Map> asOneToMany() { + return makeTupleQueryBuilder() + .evaluateAndConvert() + .mapAndCollect( + Function.identity(), + Collectors.toMap( + b -> getIRI(b, JoinQuery._sourceEntity), + b -> getIRIOptional(b, JoinQuery._targetEntity) + .map(Set::of) + .orElseGet(Set::of), + JoinQueryEvaluationBuilder::mergeSets)); + } + + /** + * Returns only the left column of the join, i.e. all source entity ids, as a {@link Set} + * + * @return + */ + public Set asSourceEntityIdSet() { + return makeTupleQueryBuilder() + .evaluateAndConvert() + .toStream(b -> getIRIOptional(b, JoinQuery._sourceEntity)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + } + + /** + * Returns only the right column of the join, i.e. all target entity ids, as a {@link Set} + * + * @return + */ + public Set asTargetEntityIdSet() { + return makeTupleQueryBuilder() + .evaluateAndConvert() + .toStream(b -> getIRIOptional(b, JoinQuery._targetEntity)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + } + + /** + * Returns true if the join has no results, false otherwise. + * + * @return + */ + public boolean asIsEmpty() { + return makeTupleQueryBuilder() + .evaluateAndConvert() + .toSingletonOfWholeResult(result -> !result.hasNext()); + } + + /** + * Returns true if the join has one or more results, fals otherwise. + * + * @return + */ + public boolean asIsPresent() { + return makeTupleQueryBuilder() + .evaluateAndConvert() + .toSingletonOfWholeResult(result -> result.hasNext()); + } + + /** + * Returns a {@link TupleQueryResultConverter} so the client can convert the result as needed. + * + * @return + */ + public TupleQueryResultConverter evaluateAndConvert() { + return makeTupleQueryBuilder().evaluateAndConvert(); + } + + private static Set mergeSets(Set left, Set right) { + Set merged = new HashSet<>(left); + merged.addAll(right); + return merged; + } + + public JoinQueryEvaluationBuilder withSourceEntityId(IRI value) { + return withBinding(JoinQuery._sourceEntity, value); + } + + public JoinQueryEvaluationBuilder withTargetEntityId(IRI value) { + return withBinding(JoinQuery._targetEntity, value); + } + + public JoinQueryEvaluationBuilder withObjectBinding(IRI value) { + return withBinding(JoinQuery._sourceEntity, value); + } + + public JoinQueryEvaluationBuilder withSubjectBindingMaybe(IRI value) { + return withBindingMaybe(JoinQuery._sourceEntity, value); + } + + public JoinQueryEvaluationBuilder withBinding(Variable key, Value value) { + bindingsBuilder.add(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBinding(String key, Value value) { + bindingsBuilder.add(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBindingMaybe(Variable key, Value value) { + bindingsBuilder.addMaybe(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBindingMaybe(String key, Value value) { + bindingsBuilder.addMaybe(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBinding(Variable key, IRI value) { + bindingsBuilder.add(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBinding(String key, IRI value) { + bindingsBuilder.add(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBindingMaybe(Variable key, IRI value) { + bindingsBuilder.addMaybe(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBindingMaybe(Variable key, String value) { + bindingsBuilder.addMaybe(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBindingMaybe(String key, IRI value) { + bindingsBuilder.addMaybe(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBinding(Variable key, String value) { + bindingsBuilder.add(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBinding(String key, String value) { + bindingsBuilder.add(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBindingMaybe(String key, String value) { + bindingsBuilder.addMaybe(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBinding(Variable key, Integer value) { + bindingsBuilder.add(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBinding(String key, Integer value) { + bindingsBuilder.add(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBindingMaybe(Variable key, Integer value) { + bindingsBuilder.addMaybe(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBindingMaybe(String key, Integer value) { + bindingsBuilder.addMaybe(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBinding(Variable key, Boolean value) { + bindingsBuilder.add(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBinding(String key, Boolean value) { + bindingsBuilder.add(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBindingMaybe(Variable key, Boolean value) { + bindingsBuilder.addMaybe(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBindingMaybe(String key, Boolean value) { + bindingsBuilder.addMaybe(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBinding(Variable key, Float value) { + bindingsBuilder.add(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBinding(String key, Float value) { + bindingsBuilder.add(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBindingMaybe(Variable key, Float value) { + bindingsBuilder.addMaybe(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBindingMaybe(String key, Float value) { + bindingsBuilder.addMaybe(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBinding(Variable key, Double value) { + bindingsBuilder.add(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBinding(String key, Double value) { + bindingsBuilder.add(key, value); + return this; + } + + public JoinQueryEvaluationBuilder withBindingMaybe(Variable var, Double value) { + bindingsBuilder.addMaybe(var, value); + return this; + } + + public JoinQueryEvaluationBuilder withBindingMaybe(String key, Double value) { + bindingsBuilder.addMaybe(key, value); + return this; + } + + private TupleQueryEvaluationBuilder makeTupleQueryBuilder() { + return this.tupleQueryEvaluationBuilder.withBindings(bindingsBuilder.build()); + } +} diff --git a/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinType.java b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinType.java new file mode 100644 index 00000000000..39a6d4f9c14 --- /dev/null +++ b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/JoinType.java @@ -0,0 +1,18 @@ +/* + * ***************************************************************************** + * 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.spring.dao.support.join; + +public enum JoinType { + INNER, + LEFT_OUTER, + RIGHT_OUTER; +} diff --git a/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/LazyJoinQueryInitizalizer.java b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/LazyJoinQueryInitizalizer.java new file mode 100644 index 00000000000..9f1edc71468 --- /dev/null +++ b/spring-components/rdf4j-spring/src/main/java/org/eclipse/rdf4j/spring/dao/support/join/LazyJoinQueryInitizalizer.java @@ -0,0 +1,49 @@ +/* + * ***************************************************************************** + * 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.spring.dao.support.join; + +import org.eclipse.rdf4j.spring.support.RDF4JTemplate; + +/** + * Holds a fully configured {@link JoinQueryBuilder} until the time comes at which a {@link RDF4JTemplate} is available + * and the {@link JoinQuery} can be instantiated via {@link #get(RDF4JTemplate)}. Subsequent calls to + * {@link #get(RDF4JTemplate)} will always use the same {@link JoinQuery} object. + * + *

+ * This construct is thread-safe because the JoinQuery's internal state does not change after initialization; rather, + * when used to perform actual queries, it re-uses or creates a reusable TupleQuery using the {@link RDF4JTemplate}, and + * uses a {@link JoinQueryEvaluationBuilder} object to encapsulate per-evaluation state. + * + *

+ * Usually, you would assign this to a static member of a class and obtain the value in an instance method. + */ +public class LazyJoinQueryInitizalizer { + private JoinQueryBuilder joinQueryBuilder; + private JoinQuery joinQuery; + + LazyJoinQueryInitizalizer(JoinQueryBuilder joinQueryBuilder) { + this.joinQueryBuilder = joinQueryBuilder; + } + + public JoinQueryEvaluationBuilder get(RDF4JTemplate rdf4JTemplate) { + if (joinQuery != null) { + return joinQuery.evaluationBuilder(rdf4JTemplate); + } else { + synchronized (this) { + if (this.joinQuery == null) { + this.joinQuery = this.joinQueryBuilder.build(); + } + } + } + return joinQuery.evaluationBuilder(rdf4JTemplate); + } +} diff --git a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/dao/support/ServiceLayerTests.java b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/dao/support/ServiceLayerTests.java index 9cfc3b007d3..69813c31051 100644 --- a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/dao/support/ServiceLayerTests.java +++ b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/dao/support/ServiceLayerTests.java @@ -17,8 +17,12 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Set; + +import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.spring.RDF4JSpringTestBase; import org.eclipse.rdf4j.spring.domain.model.Artist; +import org.eclipse.rdf4j.spring.domain.model.EX; import org.eclipse.rdf4j.spring.domain.model.Painting; import org.eclipse.rdf4j.spring.domain.service.ArtService; import org.eclipse.rdf4j.spring.support.RDF4JTemplate; @@ -30,6 +34,8 @@ import org.springframework.transaction.support.DefaultTransactionStatus; import org.springframework.transaction.support.TransactionTemplate; +import shaded_package.org.bouncycastle.asn1.tsp.ArchiveTimeStamp; + /** * @author Florian Kleedorfer * @since 4.0.0 @@ -97,4 +103,26 @@ public void testRollbackOnException() { return null; }); } + + @Test + public void testGetPaintingsOfArtist() { + transactionTemplate.execute(status -> { + Set paintings = artService.getPaintingsOfArtist(EX.VanGogh); + assertEquals(3, paintings.size()); + assertTrue(paintings.stream().anyMatch(p -> p.getId().equals(EX.starryNight))); + assertTrue(paintings.stream().anyMatch(p -> p.getId().equals(EX.potatoEaters))); + assertTrue(paintings.stream().anyMatch(p -> p.getId().equals(EX.sunflowers))); + return null; + }); + } + + @Test + public void testGetArtistOfPainting() { + transactionTemplate.execute(status -> { + Set artists = artService.getArtistsOfPainting(EX.guernica); + assertEquals(1, artists.size()); + assertTrue(artists.stream().anyMatch(p -> p.getId().equals(EX.Picasso))); + return null; + }); + } } diff --git a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/ArtistDao.java b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/ArtistDao.java index bbe68a2738d..cfa2e2cd2c2 100644 --- a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/ArtistDao.java +++ b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/ArtistDao.java @@ -16,12 +16,17 @@ import static org.eclipse.rdf4j.spring.domain.model.Artist.ARTIST_ID; import static org.eclipse.rdf4j.spring.domain.model.Artist.ARTIST_LAST_NAME; +import java.util.Map; +import java.util.Set; + import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.vocabulary.FOAF; import org.eclipse.rdf4j.query.BindingSet; import org.eclipse.rdf4j.sparqlbuilder.core.query.Queries; import org.eclipse.rdf4j.spring.dao.SimpleRDF4JCRUDDao; import org.eclipse.rdf4j.spring.dao.support.bindingsBuilder.MutableBindings; +import org.eclipse.rdf4j.spring.dao.support.join.JoinQueryBuilder; +import org.eclipse.rdf4j.spring.dao.support.join.LazyJoinQueryInitizalizer; import org.eclipse.rdf4j.spring.dao.support.sparql.NamedSparqlSupplier; import org.eclipse.rdf4j.spring.domain.model.Artist; import org.eclipse.rdf4j.spring.domain.model.EX; @@ -40,6 +45,18 @@ public ArtistDao(RDF4JTemplate rdf4JTemplate) { super(rdf4JTemplate); } + private static final LazyJoinQueryInitizalizer getPaintingsIdsOfArtistQuery = JoinQueryBuilder.of(EX.creatorOf) + .sourceEntityConstraints(artist -> artist.isA(EX.Artist)) + .targetEntityConstraints(painting -> painting.isA(EX.Painting)) + .leftOuterJoin() + .buildLazyInitializer(); + + public Set getPaintingsIdsOfArtist(IRI artistId) { + return getPaintingsIdsOfArtistQuery.get(getRdf4JTemplate()) + .withSourceEntityId(artistId) + .asTargetEntityIdSet(); + } + @Override protected void populateIdBindings(MutableBindings bindingsBuilder, IRI iri) { bindingsBuilder.add(ARTIST_ID, iri); diff --git a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/PaintingDao.java b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/PaintingDao.java index 0cd1bd72034..5d3e70b3a9e 100644 --- a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/PaintingDao.java +++ b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/dao/PaintingDao.java @@ -17,12 +17,16 @@ import static org.eclipse.rdf4j.spring.domain.model.Painting.PAINTING_LABEL; import static org.eclipse.rdf4j.spring.domain.model.Painting.PAINTING_TECHNIQUE; +import java.util.Set; + import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.vocabulary.RDFS; import org.eclipse.rdf4j.query.BindingSet; import org.eclipse.rdf4j.sparqlbuilder.core.query.Queries; import org.eclipse.rdf4j.spring.dao.SimpleRDF4JCRUDDao; import org.eclipse.rdf4j.spring.dao.support.bindingsBuilder.MutableBindings; +import org.eclipse.rdf4j.spring.dao.support.join.JoinQueryBuilder; +import org.eclipse.rdf4j.spring.dao.support.join.LazyJoinQueryInitizalizer; import org.eclipse.rdf4j.spring.dao.support.sparql.NamedSparqlSupplier; import org.eclipse.rdf4j.spring.domain.model.EX; import org.eclipse.rdf4j.spring.domain.model.Painting; @@ -41,6 +45,19 @@ public PaintingDao(RDF4JTemplate rdf4JTemplate) { super(rdf4JTemplate); } + private static final LazyJoinQueryInitizalizer getArtistIdsOfPaintingQuery = JoinQueryBuilder + .of(p -> p.pred(EX.creatorOf).inv()) + .sourceEntityConstraints(artist -> artist.isA(EX.Painting)) + .targetEntityConstraints(painting -> painting.isA(EX.Artist)) + .leftOuterJoin() + .buildLazyInitializer(); + + public Set getArtistIdsOfPainting(IRI paintingId) { + return getArtistIdsOfPaintingQuery.get(getRdf4JTemplate()) + .withSourceEntityId(paintingId) + .asTargetEntityIdSet(); + } + @Override protected void populateIdBindings(MutableBindings bindingsBuilder, IRI iri) { bindingsBuilder.add(PAINTING_ID, iri); diff --git a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/service/ArtService.java b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/service/ArtService.java index 6dd563489c5..a91836cb7b3 100644 --- a/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/service/ArtService.java +++ b/spring-components/rdf4j-spring/src/test/java/org/eclipse/rdf4j/spring/domain/service/ArtService.java @@ -11,6 +11,9 @@ package org.eclipse.rdf4j.spring.domain.service; +import java.util.Set; +import java.util.stream.Collectors; + import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.spring.domain.dao.ArtistDao; import org.eclipse.rdf4j.spring.domain.dao.PaintingDao; @@ -46,4 +49,15 @@ public Painting createPainting(String title, String technique, IRI artist) { return paintingDao.save(painting); } + @Transactional(propagation = Propagation.REQUIRED) + public Set getPaintingsOfArtist(IRI artistId) { + Set paintingIds = artistDao.getPaintingsIdsOfArtist(artistId); + return paintingIds.stream().map(paintingDao::getById).collect(Collectors.toSet()); + } + + @Transactional(propagation = Propagation.REQUIRED) + public Set getArtistsOfPainting(IRI paintingId) { + Set artistIds = paintingDao.getArtistIdsOfPainting(paintingId); + return artistIds.stream().map(artistDao::getById).collect(Collectors.toSet()); + } }