From 90b2b80a63e754bafb5fddb5314674c6aa07525a Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Tue, 26 May 2026 14:29:12 +0200 Subject: [PATCH 1/6] [Enhancement #431] Add annotations for declaring named entity graphs. --- .../cvut/kbss/jopa/model/EntityManager.java | 11 ++++ .../model/annotations/NamedAttributeNode.java | 28 ++++++++++ .../model/annotations/NamedEntityGraph.java | 51 +++++++++++++++++++ .../jopa/model/annotations/NamedSubgraph.java | 31 +++++++++++ .../kbss/jopa/model/EntityManagerImpl.java | 7 +++ 5 files changed, 128 insertions(+) create mode 100644 jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedAttributeNode.java create mode 100644 jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedEntityGraph.java create mode 100644 jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedSubgraph.java diff --git a/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/EntityManager.java b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/EntityManager.java index 495a7d48a..bcb39ae37 100644 --- a/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/EntityManager.java +++ b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/EntityManager.java @@ -362,6 +362,17 @@ public interface EntityManager extends AutoCloseable { */ EntityGraph createEntityGraph(Class rootType); + /** + * Obtain a named {@link EntityGraph}. + *

+ * The returned instance of EntityGraph should be considered immutable. + * + * @param graphName Name of an existing entity graph + * @return Named entity graph + * @throws IllegalArgumentException If there is no entity graph with the specified name + */ + EntityGraph getEntityGraph(String graphName); + /** * Return an object of the specified type to allow access to the provider-specific API. If the provider's * EntityManager implementation does not support the specified class, the {@link OWLPersistenceException} is diff --git a/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedAttributeNode.java b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedAttributeNode.java new file mode 100644 index 000000000..f82e88053 --- /dev/null +++ b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedAttributeNode.java @@ -0,0 +1,28 @@ +package cz.cvut.kbss.jopa.model.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A NamedAttributeNode is a member element of a {@link NamedEntityGraph}. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface NamedAttributeNode { + + /** + * (Required) The name of the attribute that must be included in the graph. + */ + String value(); + + /** + * (Optional) If the attribute references a managed type that has its own AttributeNodes, this element is used to + * refer to that {@link NamedSubgraph} definition. + *

+ * If the target type has inheritance, multiple subgraphs can be specified.These additional subgraphs are intended + * to add subclass-specific attributes. Superclass subgraph entries will be merged into subclass subgraphs. + *

+ * The value of this element is the name of the subgraph as specified by the name element of the corresponding + * NamedSubgraph element. If multiple subgraphs are specified due to inheritance, they are referenced by this name. + */ + String subgraph() default ""; +} diff --git a/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedEntityGraph.java b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedEntityGraph.java new file mode 100644 index 000000000..42af4e6aa --- /dev/null +++ b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedEntityGraph.java @@ -0,0 +1,51 @@ +package cz.cvut.kbss.jopa.model.annotations; + +import cz.cvut.kbss.jopa.model.EntityManager; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines a named entity graph. + *

+ * This annotation must be applied to the root entity of the graph, and specifies the limits of the graph of associated + * attributes and entities fetched when an operation which retrieves an instance or instances of the root entity is + * executed. + *

+ * A reference to a named entity graph may be obtained by calling {@link EntityManager#getEntityGraph(String)}, and may + * be passed as hint to {@link cz.cvut.kbss.jopa.model.query.TypedQuery}. + */ +@Target(value = ElementType.TYPE) +@Retention(value = RetentionPolicy.RUNTIME) +public @interface NamedEntityGraph { + + /** + * (Optional) The name used to identify the entity graph in calls to {@link EntityManager#getEntityGraph(String)}. + *

+ * If no name is explicitly specified, the name defaults to the entity name of the annotated root entity. Entity + * graph names must be unique within the persistence unit. + */ + String name(); + + /** + * (Optional) A list of attributes of the entity that are included in this graph. + */ + NamedAttributeNode[] attributeNodes() default {}; + + /** + * (Optional) Includes all the attributes of the annotated entity class as attribute nodes in the + * {@code NamedEntityGraph} without the need to explicitly list them. + *

+ * Included attributes can still be fully specified by an attribute node referencing a subgraph. + */ + boolean includeAllAttributes() default false; + + /** + * (Optional) A list of subgraphs that are included in the entity graph. + *

+ * These are referenced by name from {@code NamedAttributeNode} definitions. + */ + NamedSubgraph[] subgraphs() default {}; +} diff --git a/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedSubgraph.java b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedSubgraph.java new file mode 100644 index 000000000..f8857523b --- /dev/null +++ b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedSubgraph.java @@ -0,0 +1,31 @@ +package cz.cvut.kbss.jopa.model.annotations; + +/** + * A NamedSubgraph is a member element of a {@link NamedEntityGraph}. + *

+ * The NamedSubgraph is only referenced from within its containing {@code NamedEntityGraph} and cannot be referenced + * independently. It is referenced by its {@link #name()} from a {@link NamedAttributeNode} element of the + * {@code NamedEntityGraph}. + */ +public @interface NamedSubgraph { + + /** + * (Required) The name of the subgraph as referenced from a NamedAttributeNode element. + */ + String name(); + + /** + * (Required) The list of the attributes of the class that must be included. + *

+ * If the named subgraph corresponds to a subclass of the class referenced by the corresponding attribute node, then + * only subclass-specific attributes are listed. + */ + NamedAttributeNode[] attributeNodes(); + + /** + * (Optional) The type represented by this subgraph. + *

+ * The element must be specified when this subgraph is extending a definition on behalf of a subclass. + */ + Class type() default void.class; +} diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/EntityManagerImpl.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/EntityManagerImpl.java index c91f636ea..25858b850 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/EntityManagerImpl.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/EntityManagerImpl.java @@ -583,6 +583,13 @@ public EntityGraph createEntityGraph(Class rootType) { return getCurrentPersistenceContext().createEntityGraph(rootType); } + @Override + public EntityGraph getEntityGraph(String graphName) { + ensureOpen(); + // TODO + return null; + } + @Override public T unwrap(Class cls) { ensureOpen(); From 2697754ab916236a2e74b6b28a42dc03c0cf8ce9 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Tue, 26 May 2026 15:49:13 +0200 Subject: [PATCH 2/6] [Enhancement #431] Create basic infrastructure for processing named entity graphs. --- .../model/annotations/NamedEntityGraph.java | 2 + .../model/annotations/NamedEntityGraphs.java | 16 ++++++ .../jopa/loaders/NamedEntityGraphLoader.java | 34 +++++++++++ .../loaders/PersistenceUnitClassFinder.java | 13 +++++ .../cvut/kbss/jopa/model/EntityGraphImpl.java | 8 +-- .../cvut/kbss/jopa/model/MetamodelImpl.java | 3 +- .../jopa/model/NamedEntityGraphManager.java | 56 +++++++++++++++++++ .../jopa/model/NamedEntityGraphProcessor.java | 24 ++++++++ .../model/metamodel/MetamodelBuilder.java | 12 +++- .../model/metamodel/MetamodelClassMapper.java | 16 ++++++ 10 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedEntityGraphs.java create mode 100644 jopa-impl/src/main/java/cz/cvut/kbss/jopa/loaders/NamedEntityGraphLoader.java create mode 100644 jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphManager.java create mode 100644 jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphProcessor.java create mode 100644 jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/MetamodelClassMapper.java diff --git a/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedEntityGraph.java b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedEntityGraph.java index 42af4e6aa..97ced5401 100644 --- a/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedEntityGraph.java +++ b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedEntityGraph.java @@ -3,6 +3,7 @@ import cz.cvut.kbss.jopa.model.EntityManager; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -17,6 +18,7 @@ * A reference to a named entity graph may be obtained by calling {@link EntityManager#getEntityGraph(String)}, and may * be passed as hint to {@link cz.cvut.kbss.jopa.model.query.TypedQuery}. */ +@Repeatable(NamedEntityGraphs.class) @Target(value = ElementType.TYPE) @Retention(value = RetentionPolicy.RUNTIME) public @interface NamedEntityGraph { diff --git a/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedEntityGraphs.java b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedEntityGraphs.java new file mode 100644 index 000000000..3448f4564 --- /dev/null +++ b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedEntityGraphs.java @@ -0,0 +1,16 @@ +package cz.cvut.kbss.jopa.model.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used to group {@link NamedEntityGraph} annotations. + */ +@Target(value = ElementType.TYPE) +@Retention(value = RetentionPolicy.RUNTIME) +public @interface NamedEntityGraphs { + + NamedEntityGraph[] value(); +} diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/loaders/NamedEntityGraphLoader.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/loaders/NamedEntityGraphLoader.java new file mode 100644 index 000000000..9e82599f1 --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/loaders/NamedEntityGraphLoader.java @@ -0,0 +1,34 @@ +package cz.cvut.kbss.jopa.loaders; + +import cz.cvut.kbss.jopa.model.annotations.NamedEntityGraph; +import cz.cvut.kbss.jopa.model.annotations.NamedEntityGraphs; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; + +/** + * Manages {@link cz.cvut.kbss.jopa.model.annotations.NamedEntityGraph} instances discovered during classpath + * processing. + */ +public class NamedEntityGraphLoader implements Consumer> { + + private final Set graphDeclarations = new HashSet<>(); + + @Override + public void accept(Class aClass) { + final NamedEntityGraph graphDeclaration = aClass.getDeclaredAnnotation(NamedEntityGraph.class); + if (graphDeclaration != null) { + graphDeclarations.add(graphDeclaration); + } + final NamedEntityGraphs graphDeclarationSet = aClass.getDeclaredAnnotation(NamedEntityGraphs.class); + if (graphDeclarationSet != null) { + graphDeclarations.addAll(Arrays.asList(graphDeclarationSet.value())); + } + } + + public Set getGraphDeclarations() { + return graphDeclarations; + } +} diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/loaders/PersistenceUnitClassFinder.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/loaders/PersistenceUnitClassFinder.java index aa9c9f95b..83d1630c7 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/loaders/PersistenceUnitClassFinder.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/loaders/PersistenceUnitClassFinder.java @@ -19,6 +19,7 @@ import cz.cvut.kbss.jopa.exception.MetamodelInitializationException; import cz.cvut.kbss.jopa.model.JOPAPersistenceProperties; +import cz.cvut.kbss.jopa.model.annotations.NamedEntityGraph; import cz.cvut.kbss.jopa.model.annotations.SparqlResultSetMapping; import cz.cvut.kbss.jopa.oom.converter.ConverterWrapper; import cz.cvut.kbss.jopa.utils.Configuration; @@ -39,6 +40,7 @@ public class PersistenceUnitClassFinder { private final EntityLoader entityLoader = new EntityLoader(); private final ResultSetMappingLoader resultSetMappingLoader = new ResultSetMappingLoader(); private final ConverterLoader converterLoader = new ConverterLoader(); + private final NamedEntityGraphLoader namedEntityGraphLoader = new NamedEntityGraphLoader(); private boolean scanned = false; @@ -65,6 +67,7 @@ public void scanClasspath(Configuration configuration) { classpathScanner.addListener(entityLoader); classpathScanner.addListener(resultSetMappingLoader); classpathScanner.addListener(converterLoader); + classpathScanner.addListener(namedEntityGraphLoader); Stream.of(toScan).map(String::trim).forEach(classpathScanner::processClasses); this.scanned = true; } @@ -100,6 +103,16 @@ public Set getResultSetMappings() { return resultSetMappingLoader.getMappings(); } + /** + * Gets {@link NamedEntityGraph}s found during classpath scanning. + * + * @return Set of named entity graph annotations discovered on classpath + */ + public Set getNamedEntityGraphs() { + assert scanned; + return namedEntityGraphLoader.getGraphDeclarations(); + } + /** * Gets {@link cz.cvut.kbss.jopa.model.AttributeConverter} implementations found during classpath scanning. *

diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/EntityGraphImpl.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/EntityGraphImpl.java index 8fa818d3b..bb9511ec5 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/EntityGraphImpl.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/EntityGraphImpl.java @@ -2,7 +2,7 @@ import cz.cvut.kbss.jopa.model.metamodel.Attribute; import cz.cvut.kbss.jopa.model.metamodel.ManagedType; -import cz.cvut.kbss.jopa.model.metamodel.Metamodel; +import cz.cvut.kbss.jopa.model.metamodel.MetamodelClassMapper; import cz.cvut.kbss.jopa.model.metamodel.PluralAttribute; import java.util.ArrayList; @@ -15,17 +15,17 @@ public class EntityGraphImpl implements EntityGraph, Subgraph { private final String name; private final ManagedType entityType; - private final Metamodel metamodel; + private final MetamodelClassMapper metamodel; private final Map> attributeNodes = new HashMap<>(); - public EntityGraphImpl(ManagedType entityType, Metamodel metamodel) { + public EntityGraphImpl(ManagedType entityType, MetamodelClassMapper metamodel) { this.entityType = entityType; this.metamodel = metamodel; this.name = null; } - public EntityGraphImpl(String name, ManagedType entityType, Metamodel metamodel) { + public EntityGraphImpl(String name, ManagedType entityType, MetamodelClassMapper metamodel) { this.name = name; this.entityType = entityType; this.metamodel = metamodel; diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/MetamodelImpl.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/MetamodelImpl.java index 62f30cf72..e4cf70cc1 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/MetamodelImpl.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/MetamodelImpl.java @@ -24,6 +24,7 @@ import cz.cvut.kbss.jopa.model.metamodel.ManagedType; import cz.cvut.kbss.jopa.model.metamodel.Metamodel; import cz.cvut.kbss.jopa.model.metamodel.MetamodelBuilder; +import cz.cvut.kbss.jopa.model.metamodel.MetamodelClassMapper; import cz.cvut.kbss.jopa.model.metamodel.StaticMetamodelInitializer; import cz.cvut.kbss.jopa.proxy.lazy.gen.LazyLoadingEntityProxyGenerator; import cz.cvut.kbss.jopa.proxy.reference.EntityReferenceProxyGenerator; @@ -47,7 +48,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -public class MetamodelImpl implements Metamodel, MetamodelProvider { +public class MetamodelImpl implements Metamodel, MetamodelProvider, MetamodelClassMapper { private static final Logger LOG = LoggerFactory.getLogger(MetamodelImpl.class); diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphManager.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphManager.java new file mode 100644 index 000000000..e28257106 --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphManager.java @@ -0,0 +1,56 @@ +package cz.cvut.kbss.jopa.model; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Manages named {@link EntityGraph}s, which are used to define fetching strategies for entities. + *

+ * This class is not synchronized because it is expected that modifications are done serially when building the + * persistence unit and all further interactions are read-only. + */ +public class NamedEntityGraphManager { + + private final Map> namedEntityGraphs = new HashMap<>(); + + /** + * Registers the specified {@link EntityGraph} under the specified name. + * + * @param graphName Name of the named entity graph + * @param entityGraph Entity graph to register + * @throws IllegalArgumentException If a named entity graph with the same name already exists + */ + public void addEntityGraph(String graphName, EntityGraph entityGraph) { + Objects.requireNonNull(graphName); + Objects.requireNonNull(entityGraph); + if (namedEntityGraphs.containsKey(graphName)) { + throw new IllegalArgumentException("Entity graph " + graphName + " already exists in this persistence unit."); + } + namedEntityGraphs.put(graphName, entityGraph); + } + + /** + * Gets an {@link EntityGraph} with the specified name. + * + * @param graphName Name of entity graph to retrieve + * @return Matching entity graph + * @throws IllegalArgumentException If no such entity graph is found + */ + public EntityGraph getEntityGraph(String graphName) { + if (!namedEntityGraphs.containsKey(graphName)) { + throw new IllegalArgumentException("Entity graph " + graphName + " not found in this persistence unit."); + } + return namedEntityGraphs.get(graphName); + } + + /** + * Checks whether this manager contains a graph with the specified name + * + * @param graphName Name to search for + * @return {@code true if such graph is registered here, {@code false} otherwise + */ + public boolean hasEntityGraph(String graphName) { + return namedEntityGraphs.containsKey(graphName); + } +} diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphProcessor.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphProcessor.java new file mode 100644 index 000000000..7b223adb6 --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphProcessor.java @@ -0,0 +1,24 @@ +package cz.cvut.kbss.jopa.model; + +import cz.cvut.kbss.jopa.model.annotations.NamedEntityGraph; +import cz.cvut.kbss.jopa.model.metamodel.MetamodelClassMapper; + +public class NamedEntityGraphProcessor { + + private final NamedEntityGraphManager manager; + + private final MetamodelClassMapper metamodel; + + public NamedEntityGraphProcessor(MetamodelClassMapper metamodel) { + this.metamodel = metamodel; + this.manager = new NamedEntityGraphManager(); + } + + public NamedEntityGraphManager getManager() { + return manager; + } + + public void buildEntityGraph(NamedEntityGraph declaration) { + + } +} diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/MetamodelBuilder.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/MetamodelBuilder.java index 0ed5b0da9..8edd382c9 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/MetamodelBuilder.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/MetamodelBuilder.java @@ -20,6 +20,8 @@ import cz.cvut.kbss.jopa.exception.MetamodelInitializationException; import cz.cvut.kbss.jopa.loaders.PersistenceUnitClassFinder; import cz.cvut.kbss.jopa.model.JOPAPersistenceProperties; +import cz.cvut.kbss.jopa.model.NamedEntityGraphManager; +import cz.cvut.kbss.jopa.model.NamedEntityGraphProcessor; import cz.cvut.kbss.jopa.model.TypeReferenceMap; import cz.cvut.kbss.jopa.model.annotations.FetchType; import cz.cvut.kbss.jopa.model.annotations.Inheritance; @@ -48,12 +50,13 @@ import java.util.Map; import java.util.Set; -public class MetamodelBuilder { +public class MetamodelBuilder implements MetamodelClassMapper { private static final Logger LOG = LoggerFactory.getLogger(MetamodelBuilder.class); private final NamedNativeQueryProcessor queryProcessor = new NamedNativeQueryProcessor(); private final ResultSetMappingProcessor mappingProcessor; + private final NamedEntityGraphProcessor entityGraphProcessor; private final Map, AbstractIdentifiableType> typeMap = new HashMap<>(); private final Set> inferredClasses = new HashSet<>(); @@ -74,6 +77,7 @@ public class MetamodelBuilder { public MetamodelBuilder(Configuration configuration) { this.configuration = configuration; this.mappingProcessor = new ResultSetMappingProcessor(this); + this.entityGraphProcessor = new NamedEntityGraphProcessor(this); this.converterResolver = new ConverterResolver(new Converters(configuration)); } @@ -89,6 +93,7 @@ public void buildMetamodel(PersistenceUnitClassFinder classFinder) { processDeferredFields(); typeMap.values().forEach(AbstractIdentifiableType::finish); classFinder.getResultSetMappings().forEach(mappingProcessor::buildMapper); + classFinder.getNamedEntityGraphs().forEach(entityGraphProcessor::buildEntityGraph); } /** @@ -250,6 +255,7 @@ public Map, ManagedType> getTypeMap() { return Collections.unmodifiableMap(typeMap); } + @Override public AbstractIdentifiableType entity(Class cls) { return (AbstractIdentifiableType) typeMap.get(cls); } @@ -273,6 +279,10 @@ public ResultSetMappingManager getResultSetMappingManager() { return mappingProcessor.getManager(); } + public NamedEntityGraphManager getNamedEntityGraphManager() { + return entityGraphProcessor.getManager(); + } + void addInferredClass(Class cls) { inferredClasses.add(cls); } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/MetamodelClassMapper.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/MetamodelClassMapper.java new file mode 100644 index 000000000..bd6963f2b --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/MetamodelClassMapper.java @@ -0,0 +1,16 @@ +package cz.cvut.kbss.jopa.model.metamodel; + +/** + * Minimal interface for mapping entity classes to the corresponding metamodel objects. + */ +public interface MetamodelClassMapper { + + /** + * Maps the specified entity class to the corresponding metamodel object. + * + * @param cls Class to map + * @param Class type + * @return Entity type + */ + AbstractIdentifiableType entity(Class cls); +} From 3995ee8604b1a4f722a6e22907372a2c8d0686c9 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Wed, 27 May 2026 12:21:04 +0200 Subject: [PATCH 3/6] [Enhancement #431] Implement creation of EntityGraphs from NamedEntityGraph annotation instances. --- .../model/annotations/NamedEntityGraph.java | 2 +- .../jopa/loaders/NamedEntityGraphLoader.java | 20 +- .../loaders/PersistenceUnitClassFinder.java | 7 +- .../cvut/kbss/jopa/model/EntityGraphImpl.java | 4 + .../jopa/model/NamedEntityGraphProcessor.java | 95 ++++++++- .../model/metamodel/MetamodelBuilder.java | 3 +- .../model/NamedEntityGraphProcessorTest.java | 190 ++++++++++++++++++ .../oom/PluralObjectPropertyStrategyTest.java | 4 +- .../SingularObjectPropertyStrategyTest.java | 35 +++- .../LoadStateDescriptorFactoryTest.java | 6 +- 10 files changed, 345 insertions(+), 21 deletions(-) create mode 100644 jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/NamedEntityGraphProcessorTest.java diff --git a/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedEntityGraph.java b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedEntityGraph.java index 97ced5401..697b2b352 100644 --- a/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedEntityGraph.java +++ b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedEntityGraph.java @@ -29,7 +29,7 @@ * If no name is explicitly specified, the name defaults to the entity name of the annotated root entity. Entity * graph names must be unique within the persistence unit. */ - String name(); + String name() default ""; /** * (Optional) A list of attributes of the entity that are included in this graph. diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/loaders/NamedEntityGraphLoader.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/loaders/NamedEntityGraphLoader.java index 9e82599f1..0805b940d 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/loaders/NamedEntityGraphLoader.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/loaders/NamedEntityGraphLoader.java @@ -3,32 +3,42 @@ import cz.cvut.kbss.jopa.model.annotations.NamedEntityGraph; import cz.cvut.kbss.jopa.model.annotations.NamedEntityGraphs; -import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; import java.util.function.Consumer; /** * Manages {@link cz.cvut.kbss.jopa.model.annotations.NamedEntityGraph} instances discovered during classpath * processing. + *

+ * The discovered annotations are tracked together with the class on which they were declared, so that the root entity + * type can be later resolved when the entity graph is being built. */ public class NamedEntityGraphLoader implements Consumer> { - private final Set graphDeclarations = new HashSet<>(); + private final Map, Set> graphDeclarations = new HashMap<>(); @Override public void accept(Class aClass) { final NamedEntityGraph graphDeclaration = aClass.getDeclaredAnnotation(NamedEntityGraph.class); if (graphDeclaration != null) { - graphDeclarations.add(graphDeclaration); + addDeclaration(aClass, graphDeclaration); } final NamedEntityGraphs graphDeclarationSet = aClass.getDeclaredAnnotation(NamedEntityGraphs.class); if (graphDeclarationSet != null) { - graphDeclarations.addAll(Arrays.asList(graphDeclarationSet.value())); + for (NamedEntityGraph g : graphDeclarationSet.value()) { + addDeclaration(aClass, g); + } } } - public Set getGraphDeclarations() { + private void addDeclaration(Class aClass, NamedEntityGraph declaration) { + graphDeclarations.computeIfAbsent(aClass, k -> new HashSet<>()).add(declaration); + } + + public Map, Set> getGraphDeclarations() { return graphDeclarations; } } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/loaders/PersistenceUnitClassFinder.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/loaders/PersistenceUnitClassFinder.java index 83d1630c7..049ca4349 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/loaders/PersistenceUnitClassFinder.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/loaders/PersistenceUnitClassFinder.java @@ -105,10 +105,13 @@ public Set getResultSetMappings() { /** * Gets {@link NamedEntityGraph}s found during classpath scanning. + *

+ * The annotations are mapped by the class on which they were declared, so that the root entity type can be later + * resolved when the entity graph is being built. * - * @return Set of named entity graph annotations discovered on classpath + * @return Map of classes to named entity graph annotations declared on them */ - public Set getNamedEntityGraphs() { + public Map, Set> getNamedEntityGraphs() { assert scanned; return namedEntityGraphLoader.getGraphDeclarations(); } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/EntityGraphImpl.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/EntityGraphImpl.java index bb9511ec5..04d8b0b02 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/EntityGraphImpl.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/EntityGraphImpl.java @@ -83,6 +83,10 @@ public List> getAttributeNodes() { return new ArrayList<>(attributeNodes.values()); } + AttributeNodeImpl getAttributeNode(String name) { + return attributeNodes.get(name); + } + @Override public String getName() { return name; diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphProcessor.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphProcessor.java index 7b223adb6..6107b777a 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphProcessor.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphProcessor.java @@ -1,8 +1,20 @@ package cz.cvut.kbss.jopa.model; +import cz.cvut.kbss.jopa.model.annotations.NamedAttributeNode; import cz.cvut.kbss.jopa.model.annotations.NamedEntityGraph; +import cz.cvut.kbss.jopa.model.annotations.NamedSubgraph; +import cz.cvut.kbss.jopa.model.metamodel.AbstractIdentifiableType; +import cz.cvut.kbss.jopa.model.metamodel.Attribute; import cz.cvut.kbss.jopa.model.metamodel.MetamodelClassMapper; +import cz.cvut.kbss.jopa.model.metamodel.PluralAttribute; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Builds {@link EntityGraphImpl} instances from {@link NamedEntityGraph} annotations discovered on entity classes. + */ public class NamedEntityGraphProcessor { private final NamedEntityGraphManager manager; @@ -18,7 +30,88 @@ public NamedEntityGraphManager getManager() { return manager; } - public void buildEntityGraph(NamedEntityGraph declaration) { + /** + * Builds an entity graph from the specified {@link NamedEntityGraph} declaration and registers it in the manager. + * + * @param rootType The class on which the {@code NamedEntityGraph} annotation was declared + * @param declaration Named entity graph annotation + */ + public void buildEntityGraph(Class rootType, NamedEntityGraph declaration) { + Objects.requireNonNull(rootType); + Objects.requireNonNull(declaration); + final String name = graphName(rootType, declaration); + manager.addEntityGraph(name, createGraph(rootType, name, declaration)); + } + + private static String graphName(Class rootType, NamedEntityGraph declaration) { + if (declaration.name().isEmpty()) { + return rootType.getSimpleName(); + } + return declaration.name(); + } + + private EntityGraphImpl createGraph(Class rootType, String name, NamedEntityGraph declaration) { + final AbstractIdentifiableType entityType = metamodel.entity((Class) rootType); + final EntityGraphImpl graph = new EntityGraphImpl<>(name, entityType, metamodel); + final Map namedSubgraphs = indexSubgraphs(declaration.subgraphs()); + addAttributeNodes(declaration.attributeNodes(), graph, entityType, namedSubgraphs); + return graph; + } + + private void addAttributeNodes(NamedAttributeNode[] nodes, EntityGraphImpl graph, + AbstractIdentifiableType entityType, + Map namedSubgraphs) { + for (NamedAttributeNode node : nodes) { + graph.addAttributeNodes(node.value()); + if (!node.subgraph().isEmpty()) { + attachSubgraph(graph, entityType, node, namedSubgraphs); + } + } + } + + private void attachSubgraph(EntityGraphImpl parent, AbstractIdentifiableType rootEntityType, + NamedAttributeNode node, Map namedSubgraphs) { + final NamedSubgraph subDecl = namedSubgraphs.get(node.subgraph()); + if (subDecl == null) { + throw new IllegalArgumentException( + "Named subgraph '" + node.subgraph() + "' referenced by attribute '" + node.value() + + "' is not declared in the entity graph."); + } + final Class subType = resolveSubgraphType(rootEntityType, node.value(), subDecl); + final EntityGraphImpl sub = buildSubgraph(subType, subDecl, namedSubgraphs); + final AttributeNodeImpl attrNode = parent.getAttributeNode(node.value()); + attrNode.addSubgraph(sub); + } + + private EntityGraphImpl buildSubgraph(Class subType, NamedSubgraph declaration, + Map namedSubgraphs) { + final AbstractIdentifiableType entityType = metamodel.entity((Class) subType); + final EntityGraphImpl graph = new EntityGraphImpl<>(declaration.name(), entityType, metamodel); + addAttributeNodes(declaration.attributeNodes(), graph, entityType, namedSubgraphs); + return graph; + } + + private static Class resolveSubgraphType(AbstractIdentifiableType rootEntityType, String attributeName, + NamedSubgraph subDecl) { + if (subDecl.type() != void.class) { + return subDecl.type(); + } + final Attribute att = rootEntityType.getAttribute(attributeName); + assert att != null; + if (att.isCollection()) { + return ((PluralAttribute) att).getBindableJavaType(); + } + return att.getJavaType(); + } + private static Map indexSubgraphs(NamedSubgraph[] subgraphs) { + if (subgraphs.length == 0) { + return Map.of(); + } + final Map result = new HashMap<>(subgraphs.length); + for (NamedSubgraph s : subgraphs) { + result.put(s.name(), s); + } + return result; } } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/MetamodelBuilder.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/MetamodelBuilder.java index 8edd382c9..0198fae77 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/MetamodelBuilder.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/MetamodelBuilder.java @@ -93,7 +93,8 @@ public void buildMetamodel(PersistenceUnitClassFinder classFinder) { processDeferredFields(); typeMap.values().forEach(AbstractIdentifiableType::finish); classFinder.getResultSetMappings().forEach(mappingProcessor::buildMapper); - classFinder.getNamedEntityGraphs().forEach(entityGraphProcessor::buildEntityGraph); + classFinder.getNamedEntityGraphs().forEach((cls, graphs) -> + graphs.forEach(g -> entityGraphProcessor.buildEntityGraph(cls, g))); } /** diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/NamedEntityGraphProcessorTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/NamedEntityGraphProcessorTest.java new file mode 100644 index 000000000..af194d493 --- /dev/null +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/NamedEntityGraphProcessorTest.java @@ -0,0 +1,190 @@ +package cz.cvut.kbss.jopa.model; + +import cz.cvut.kbss.jopa.environment.Vocabulary; +import cz.cvut.kbss.jopa.environment.utils.TestLocal; +import cz.cvut.kbss.jopa.model.annotations.Id; +import cz.cvut.kbss.jopa.model.annotations.NamedAttributeNode; +import cz.cvut.kbss.jopa.model.annotations.NamedEntityGraph; +import cz.cvut.kbss.jopa.model.annotations.NamedEntityGraphs; +import cz.cvut.kbss.jopa.model.annotations.NamedSubgraph; +import cz.cvut.kbss.jopa.model.annotations.OWLAnnotationProperty; +import cz.cvut.kbss.jopa.model.annotations.OWLObjectProperty; +import cz.cvut.kbss.jopa.model.metamodel.AbstractAttribute; +import cz.cvut.kbss.jopa.model.metamodel.AbstractIdentifiableType; +import cz.cvut.kbss.jopa.model.metamodel.MetamodelClassMapper; +import cz.cvut.kbss.jopa.vocabulary.RDFS; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.net.URI; +import java.util.List; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class NamedEntityGraphProcessorTest { + + @Mock + private MetamodelClassMapper metamodelMapper; + + @InjectMocks + private NamedEntityGraphProcessor processor; + + @Test + void buildEntityGraphBuildsEntityGraphFromAnnotationDeclaringAttributeNodes() { + final AbstractIdentifiableType ait = mock(AbstractIdentifiableType.class); + when(metamodelMapper.entity(EntityWithAttributes.class)).thenReturn(ait); + final AbstractAttribute nameAtt = mock(AbstractAttribute.class); + when(nameAtt.getName()).thenReturn("name"); + final AbstractAttribute descriptionAtt = mock(AbstractAttribute.class); + when(descriptionAtt.getName()).thenReturn("description"); + when(ait.getAttribute("name")).thenReturn(nameAtt); + when(ait.getAttribute("description")).thenReturn(descriptionAtt); + final NamedEntityGraphs ne = EntityWithAttributes.class.getAnnotation(NamedEntityGraphs.class); + processor.buildEntityGraph(EntityWithAttributes.class, ne.value()[1]); + + final EntityGraph eg = (EntityGraph) processor.getManager() + .getEntityGraph("ExplicitlyNamedEntityWithAttributes"); + assertNotNull(eg); + assertEquals(2, eg.getAttributeNodes().size()); + final List attributeNames = eg.getAttributeNodes().stream().map(AttributeNode::getAttributeName) + .collect(Collectors.toList()); + assertThat(attributeNames, containsInAnyOrder("name", "description")); + } + + @NamedEntityGraph(attributeNodes = { + @NamedAttributeNode("name") + }) + @NamedEntityGraph(name = "ExplicitlyNamedEntityWithAttributes", attributeNodes = { + @NamedAttributeNode("name"), + @NamedAttributeNode("description") + }) + @TestLocal + public static class EntityWithAttributes { + @Id + private URI uri; + + @OWLAnnotationProperty(iri = RDFS.LABEL) + private String name; + + @OWLAnnotationProperty(iri = RDFS.COMMENT) + private String description; + } + + @Test + void buildEntityGraphDefaultNameToAnnotatedEntityClassName() { + final AbstractIdentifiableType ait = mock(AbstractIdentifiableType.class); + when(metamodelMapper.entity(EntityWithAttributes.class)).thenReturn(ait); + when(ait.getAttribute(anyString())).thenAnswer(inv -> mock(AbstractAttribute.class)); + final NamedEntityGraphs ne = EntityWithAttributes.class.getAnnotation(NamedEntityGraphs.class); + processor.buildEntityGraph(EntityWithAttributes.class, ne.value()[0]); + + final EntityGraph result = (EntityGraph) processor.getManager() + .getEntityGraph(EntityWithAttributes.class.getSimpleName()); + assertNotNull(result); + } + + @Test + void buildEntityGraphSupportsReferenceToOtherEntityGraph() { + final AbstractIdentifiableType rootAit = mock(AbstractIdentifiableType.class); + when(metamodelMapper.entity(ReferencingEntity.class)).thenReturn(rootAit); + final AbstractAttribute referencedEntityAtt = mock(AbstractAttribute.class); + when(referencedEntityAtt.getJavaType()).thenReturn(EntityWithAttributes.class); + when(rootAit.getAttribute("referencedEntity")).thenReturn(referencedEntityAtt); + final AbstractIdentifiableType subgraphAit = mock(AbstractIdentifiableType.class); + when(metamodelMapper.entity(EntityWithAttributes.class)).thenReturn(subgraphAit); + when(subgraphAit.getAttribute("name")).thenReturn(mock(AbstractAttribute.class)); + when(subgraphAit.getJavaType()).thenReturn(EntityWithAttributes.class); + final NamedEntityGraph ne = ReferencingEntity.class.getAnnotation(NamedEntityGraph.class); + processor.buildEntityGraph(ReferencingEntity.class, ne); + + final EntityGraph result = (EntityGraph) processor.getManager() + .getEntityGraph("ReferencingEntity"); + assertNotNull(result); + assertEquals(1, result.getAttributeNodes().size()); + final Subgraph subgraph = result.getAttributeNodes().get(0).getSubgraphs() + .get(EntityWithAttributes.class); + assertNotNull(subgraph); + assertEquals(1, subgraph.getAttributeNodes().size()); + } + + + @NamedEntityGraph( + attributeNodes = {@NamedAttributeNode(value = "referencedEntity", subgraph = "referencedEntity")}, + subgraphs = { + @NamedSubgraph(name = "referencedEntity", + attributeNodes = {@NamedAttributeNode("name")}) + }) + @TestLocal + public static class ReferencingEntity { + @Id + private URI uri; + + @OWLObjectProperty(iri = Vocabulary.ATTRIBUTE_BASE + "referencedEntity") + private EntityWithAttributes referencedEntity; + } + + @Test + void buildEntityGraphSupportsTraversalAcrossMultipleLevels() { + final AbstractIdentifiableType rootAit = mock(AbstractIdentifiableType.class); + when(metamodelMapper.entity(ReferencingReferencingEntity.class)).thenReturn(rootAit); + final AbstractAttribute referencedEntityAtt = mock(AbstractAttribute.class); + when(rootAit.getAttribute("referencedEntity")).thenReturn(referencedEntityAtt); + final AbstractIdentifiableType middleAit = mock(AbstractIdentifiableType.class); + when(metamodelMapper.entity(ReferencingEntity.class)).thenReturn(middleAit); + when(middleAit.getJavaType()).thenReturn(ReferencingEntity.class); + final AbstractAttribute middleReferencedEntityAtt = mock(AbstractAttribute.class); + when(middleAit.getAttribute("referencedEntity")).thenReturn(middleReferencedEntityAtt); + final AbstractIdentifiableType subgraphAit = mock(AbstractIdentifiableType.class); + when(metamodelMapper.entity(EntityWithAttributes.class)).thenReturn(subgraphAit); + final AbstractAttribute nameAtt = mock(AbstractAttribute.class); + when(nameAtt.getName()).thenReturn("name"); + when(subgraphAit.getAttribute("name")).thenReturn(nameAtt); + when(subgraphAit.getJavaType()).thenReturn(EntityWithAttributes.class); + final NamedEntityGraph ne = ReferencingReferencingEntity.class.getAnnotation(NamedEntityGraph.class); + processor.buildEntityGraph(ReferencingReferencingEntity.class, ne); + + final EntityGraph result = (EntityGraph) processor.getManager() + .getEntityGraph("RootReferencingEntity"); + assertNotNull(result); + assertEquals(1, result.getAttributeNodes().size()); + final Subgraph subgraph = result.getAttributeNodes().get(0).getSubgraphs() + .get(ReferencingEntity.class); + assertNotNull(subgraph); + assertEquals(1, subgraph.getAttributeNodes().size()); + final Subgraph subSubgraph = subgraph.getAttributeNodes().get(0).getSubgraphs() + .get(EntityWithAttributes.class); + assertNotNull(subSubgraph); + assertEquals(1, subSubgraph.getAttributeNodes().size()); + assertEquals("name", subSubgraph.getAttributeNodes().get(0).getAttributeName()); + } + + @NamedEntityGraph(name = "RootReferencingEntity", attributeNodes = { + @NamedAttributeNode(value = "referencedEntity", subgraph = "root.referencedEntity") + }, subgraphs = { + @NamedSubgraph(name = "root.referencedEntity", + attributeNodes = {@NamedAttributeNode(value = "referencedEntity", + subgraph = "root.referencedEntity.referencedEntity")}, + type = ReferencingEntity.class), + @NamedSubgraph(name = "root.referencedEntity.referencedEntity", + attributeNodes = {@NamedAttributeNode("name")}, type = EntityWithAttributes.class) + }) + @TestLocal + public static class ReferencingReferencingEntity { + @Id + private URI uri; + + @OWLObjectProperty(iri = Vocabulary.ATTRIBUTE_BASE + "referencedEntity") + private ReferencingEntity referencedEntity; + } +} diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/PluralObjectPropertyStrategyTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/PluralObjectPropertyStrategyTest.java index 019048aea..681925fb5 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/PluralObjectPropertyStrategyTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/PluralObjectPropertyStrategyTest.java @@ -28,6 +28,7 @@ import cz.cvut.kbss.jopa.model.EntityGraph; import cz.cvut.kbss.jopa.model.EntityGraphImpl; import cz.cvut.kbss.jopa.model.IRI; +import cz.cvut.kbss.jopa.model.MetamodelImpl; import cz.cvut.kbss.jopa.model.Subgraph; import cz.cvut.kbss.jopa.model.annotations.Id; import cz.cvut.kbss.jopa.model.descriptors.Descriptor; @@ -37,7 +38,6 @@ import cz.cvut.kbss.jopa.model.metamodel.CollectionType; import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.jopa.model.metamodel.Identifier; -import cz.cvut.kbss.jopa.model.metamodel.Metamodel; import cz.cvut.kbss.jopa.oom.converter.ObjectOneOfEnumConverter; import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.proxy.lazy.LazyLoadingSetProxy; @@ -353,7 +353,7 @@ void addAxiomValueUsesEntityDescriptorToGetInstanceFromCacheOrOntology() { @Test void addAxiomValuePropagatesFetchGraphToReferencedEntityLoading() { - final Metamodel metamodel = mock(Metamodel.class); + final MetamodelImpl metamodel = mock(MetamodelImpl.class); mocks.setMocks(metamodel); final EntityGraph fetchGraph = new EntityGraphImpl<>(mocks.forOwlClassJ().entityType(), metamodel); final Subgraph subgraph = fetchGraph.addSubgraph("owlClassA"); diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/SingularObjectPropertyStrategyTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/SingularObjectPropertyStrategyTest.java index 7dc1f5e95..278ac52c4 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/SingularObjectPropertyStrategyTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/SingularObjectPropertyStrategyTest.java @@ -17,12 +17,21 @@ */ package cz.cvut.kbss.jopa.oom; -import cz.cvut.kbss.jopa.environment.*; +import cz.cvut.kbss.jopa.environment.OWLClassA; +import cz.cvut.kbss.jopa.environment.OWLClassD; +import cz.cvut.kbss.jopa.environment.OWLClassM; +import cz.cvut.kbss.jopa.environment.OWLClassP; +import cz.cvut.kbss.jopa.environment.OWLClassR; +import cz.cvut.kbss.jopa.environment.OWLClassS; +import cz.cvut.kbss.jopa.environment.OWLClassT; +import cz.cvut.kbss.jopa.environment.OneOfEnum; +import cz.cvut.kbss.jopa.environment.Vocabulary; import cz.cvut.kbss.jopa.environment.utils.Generators; import cz.cvut.kbss.jopa.environment.utils.MetamodelMocks; import cz.cvut.kbss.jopa.exceptions.CardinalityConstraintViolatedException; import cz.cvut.kbss.jopa.model.EntityGraph; import cz.cvut.kbss.jopa.model.EntityGraphImpl; +import cz.cvut.kbss.jopa.model.MetamodelImpl; import cz.cvut.kbss.jopa.model.Subgraph; import cz.cvut.kbss.jopa.model.annotations.OWLObjectProperty; import cz.cvut.kbss.jopa.model.descriptors.Descriptor; @@ -30,12 +39,15 @@ import cz.cvut.kbss.jopa.model.metamodel.AbstractAttribute; import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.jopa.model.metamodel.FieldSpecification; -import cz.cvut.kbss.jopa.model.metamodel.Metamodel; import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.sessions.util.FetchGraphWrapper; import cz.cvut.kbss.jopa.vocabulary.OWL; import cz.cvut.kbss.ontodriver.descriptor.AxiomValueDescriptor; -import cz.cvut.kbss.ontodriver.model.*; +import cz.cvut.kbss.ontodriver.model.Assertion; +import cz.cvut.kbss.ontodriver.model.Axiom; +import cz.cvut.kbss.ontodriver.model.AxiomImpl; +import cz.cvut.kbss.ontodriver.model.NamedResource; +import cz.cvut.kbss.ontodriver.model.Value; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -50,8 +62,19 @@ import java.util.List; import java.util.Set; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -372,7 +395,7 @@ void addValueSupportsUriAxiomValue() throws Exception { @Test void addAxiomValuePropagatesFetchGraphToReferencedEntityLoading() { - final Metamodel metamodel = mock(Metamodel.class); + final MetamodelImpl metamodel = mock(MetamodelImpl.class); metamodelMocks.setMocks(metamodel); final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassD() .entityType(), metamodel); diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/descriptor/LoadStateDescriptorFactoryTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/descriptor/LoadStateDescriptorFactoryTest.java index 1fe40d029..eebca2d2a 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/descriptor/LoadStateDescriptorFactoryTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/descriptor/LoadStateDescriptorFactoryTest.java @@ -26,7 +26,7 @@ import cz.cvut.kbss.jopa.model.EntityGraph; import cz.cvut.kbss.jopa.model.EntityGraphImpl; import cz.cvut.kbss.jopa.model.LoadState; -import cz.cvut.kbss.jopa.model.metamodel.Metamodel; +import cz.cvut.kbss.jopa.model.MetamodelImpl; import cz.cvut.kbss.jopa.proxy.lazy.LazyLoadingListProxy; import cz.cvut.kbss.jopa.sessions.util.FetchGraphWrapper; import org.junit.jupiter.api.BeforeEach; @@ -131,7 +131,7 @@ void createCreatesDescriptorWithNotLoadedForLazyLoadedAttributesContainingProxyI void createForFetchGraphCreatesLoadStateDescriptorWithAttributesInFetchGraphLoadedAndOtherDataAttributesAsUnknown() { final OWLClassM instance = new OWLClassM(); instance.setKey(Generators.createIndividualIdentifier().toString()); - final Metamodel metamodel = mock(Metamodel.class); + final MetamodelImpl metamodel = mock(MetamodelImpl.class); metamodelMocks.setMocks(metamodel); final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassM() .entityType(), metamodel); @@ -152,7 +152,7 @@ void createForFetchGraphCreatesLoadStateDescriptorWithAttributesInFetchGraphLoad @Test void createForFetchGraphCreatesLoadStateDescriptorWithObjectPropertyAttributesNotInFetchGraphAsNotLoaded() { final OWLClassL instance = new OWLClassL(Generators.createIndividualIdentifier()); - final Metamodel metamodel = mock(Metamodel.class); + final MetamodelImpl metamodel = mock(MetamodelImpl.class); metamodelMocks.setMocks(metamodel); final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassL() .entityType(), metamodel); From 19e13f3364b8284c93d53919abfb97fb71bcf3ce Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Wed, 27 May 2026 12:26:57 +0200 Subject: [PATCH 4/6] [Enhancement #431] Provide access to named entity graphs. Implement EntityManager.getEntityGraph. --- .../java/cz/cvut/kbss/jopa/model/EntityManagerImpl.java | 3 +-- .../main/java/cz/cvut/kbss/jopa/model/MetamodelImpl.java | 6 ++++++ .../cz/cvut/kbss/jopa/sessions/AbstractUnitOfWork.java | 5 +++++ .../main/java/cz/cvut/kbss/jopa/sessions/UnitOfWork.java | 9 +++++++++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/EntityManagerImpl.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/EntityManagerImpl.java index 25858b850..8815a9fc1 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/EntityManagerImpl.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/EntityManagerImpl.java @@ -586,8 +586,7 @@ public EntityGraph createEntityGraph(Class rootType) { @Override public EntityGraph getEntityGraph(String graphName) { ensureOpen(); - // TODO - return null; + return getCurrentPersistenceContext().getEntityGraph(graphName); } @Override diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/MetamodelImpl.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/MetamodelImpl.java index e4cf70cc1..b39986af0 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/MetamodelImpl.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/MetamodelImpl.java @@ -63,6 +63,7 @@ public class MetamodelImpl implements Metamodel, MetamodelProvider, MetamodelCla private NamedQueryManager namedQueryManager; private ResultSetMappingManager resultSetMappingManager; + private NamedEntityGraphManager namedEntityGraphManager; private NamespaceResolver namespaceResolver; @@ -96,6 +97,7 @@ public void build(PersistenceUnitClassFinder classFinder) { initFromMetamodelBuilder(metamodelBuilder); this.namedQueryManager = metamodelBuilder.getNamedQueryManager(); this.resultSetMappingManager = metamodelBuilder.getResultSetMappingManager(); + this.namedEntityGraphManager = metamodelBuilder.getNamedEntityGraphManager(); new StaticMetamodelInitializer(this).initializeStaticMetamodel(); } @@ -170,6 +172,10 @@ public ResultSetMappingManager getResultSetMappingManager() { return resultSetMappingManager; } + public NamedEntityGraphManager getNamedEntityGraphManager() { + return namedEntityGraphManager; + } + @Override public Set getModuleExtractionExtraSignature() { return Collections.unmodifiableSet(getSignatureInternal()); diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/AbstractUnitOfWork.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/AbstractUnitOfWork.java index 9931f1fe0..d6a7e6045 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/AbstractUnitOfWork.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/AbstractUnitOfWork.java @@ -987,6 +987,11 @@ public EntityGraph createEntityGraph(Class rootType) { return new EntityGraphImpl<>(getMetamodel().entity(rootType), getMetamodel()); } + @Override + public EntityGraph getEntityGraph(String name) { + return getMetamodel().getNamedEntityGraphManager().getEntityGraph(name); + } + void registerEntityWithOntologyContext(Object entity, Descriptor descriptor) { assert descriptor != null; assert entity != null; diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/UnitOfWork.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/UnitOfWork.java index d01d9f43b..89fbcae15 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/UnitOfWork.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/UnitOfWork.java @@ -442,4 +442,13 @@ public interface UnitOfWork extends ConfigurationHolder, MetamodelProvider, Wrap * @return Entity graph */ EntityGraph createEntityGraph(Class rootType); + + /** + * Gets an {@link EntityGraph} with the specified name. + * + * @param name Entity graph name, as specified by {@link cz.cvut.kbss.jopa.model.annotations.NamedEntityGraph} + * @return Entity graph with the specified name + * @throws IllegalArgumentException If no entity graph with the specified name is registered + */ + EntityGraph getEntityGraph(String name); } From d435dd6883fb22a6ba8dd33f74075dc81db38147 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Wed, 27 May 2026 13:05:30 +0200 Subject: [PATCH 5/6] [Enhancement #431] Add integration tests for the feature. --- .../test/query/owlapi/TypedQueryTest.java | 13 ++++++ .../cvut/kbss/jopa/test/OWLClassSParent.java | 2 + .../cz/cvut/kbss/jopa/test/OWLClassT.java | 31 +++++++++++++- .../test/query/runner/TypedQueryRunner.java | 42 +++++++++++++++++++ 4 files changed, 87 insertions(+), 1 deletion(-) diff --git a/jopa-integration-tests-owlapi/src/test/java/cz/cvut/kbss/jopa/test/query/owlapi/TypedQueryTest.java b/jopa-integration-tests-owlapi/src/test/java/cz/cvut/kbss/jopa/test/query/owlapi/TypedQueryTest.java index f11547a44..5141384e8 100644 --- a/jopa-integration-tests-owlapi/src/test/java/cz/cvut/kbss/jopa/test/query/owlapi/TypedQueryTest.java +++ b/jopa-integration-tests-owlapi/src/test/java/cz/cvut/kbss/jopa/test/query/owlapi/TypedQueryTest.java @@ -131,8 +131,21 @@ public void querySupportsFetchGraphs() { // OWL2Query does not support OPTIONAL } + @Disabled @Override public void querySupportsGroupConcatMultilingualStringsBasedOnFetchGraph() { // OWL2Query does not support OPTIONAL and GROUP_CONCAT } + + @Disabled + @Override + public void querySupportsNamedFetchGraphs() { + // OWL2Query does not support OPTIONAL + } + + @Disabled + @Override + public void querySupportsNestedNamedFetchGraphs() { + // OWL2Query does not support OPTIONAL + } } diff --git a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassSParent.java b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassSParent.java index d82028850..62af75ffd 100644 --- a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassSParent.java +++ b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassSParent.java @@ -20,6 +20,7 @@ import cz.cvut.kbss.jopa.model.annotations.Id; import cz.cvut.kbss.jopa.model.annotations.OWLClass; import cz.cvut.kbss.jopa.model.annotations.OWLDataProperty; +import cz.cvut.kbss.jopa.model.annotations.PrePersist; import cz.cvut.kbss.jopa.model.annotations.PreUpdate; import cz.cvut.kbss.jopa.vocabulary.DC; @@ -52,6 +53,7 @@ public void setModified(Instant modified) { this.modified = modified; } + @PrePersist @PreUpdate void preUpdate() { setModified(Instant.now()); diff --git a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassT.java b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassT.java index 8b7e1d789..0c1cd1059 100644 --- a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassT.java +++ b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassT.java @@ -17,8 +17,37 @@ */ package cz.cvut.kbss.jopa.test; -import cz.cvut.kbss.jopa.model.annotations.*; +import cz.cvut.kbss.jopa.model.annotations.EntityResult; +import cz.cvut.kbss.jopa.model.annotations.FieldResult; +import cz.cvut.kbss.jopa.model.annotations.NamedAttributeNode; +import cz.cvut.kbss.jopa.model.annotations.NamedEntityGraph; +import cz.cvut.kbss.jopa.model.annotations.NamedEntityGraphs; +import cz.cvut.kbss.jopa.model.annotations.NamedSubgraph; +import cz.cvut.kbss.jopa.model.annotations.OWLClass; +import cz.cvut.kbss.jopa.model.annotations.OWLDataProperty; +import cz.cvut.kbss.jopa.model.annotations.OWLObjectProperty; +import cz.cvut.kbss.jopa.model.annotations.SparqlResultSetMapping; +@NamedEntityGraphs({ + @NamedEntityGraph(name = "OWLClassT.basic", + attributeNodes = { + @NamedAttributeNode("name"), + @NamedAttributeNode("description"), + @NamedAttributeNode("modified") + }), + @NamedEntityGraph(name = "OWLClassT.withA", + attributeNodes = { + @NamedAttributeNode("name"), + @NamedAttributeNode("description"), + @NamedAttributeNode("modified"), + @NamedAttributeNode(value = "owlClassA", subgraph = "OWLClassA.basic") + }, + subgraphs = { + @NamedSubgraph(name = "OWLClassA.basic", attributeNodes = { + @NamedAttributeNode("stringAttribute") + }, type = OWLClassA.class) + }) +}) @SparqlResultSetMapping(name = OWLClassT.MAPPING_NAME, entities = { @EntityResult(entityClass = OWLClassT.class, fields = { @FieldResult(name = "uri", variable = "x"), diff --git a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/query/runner/TypedQueryRunner.java b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/query/runner/TypedQueryRunner.java index e4b3758fd..7f18dd2f9 100644 --- a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/query/runner/TypedQueryRunner.java +++ b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/query/runner/TypedQueryRunner.java @@ -23,12 +23,14 @@ import cz.cvut.kbss.jopa.model.Subgraph; import cz.cvut.kbss.jopa.model.annotations.OWLClass; import cz.cvut.kbss.jopa.model.query.TypedQuery; +import cz.cvut.kbss.jopa.proxy.lazy.gen.LazyLoadingEntityProxy; import cz.cvut.kbss.jopa.query.QueryHints; import cz.cvut.kbss.jopa.test.OWLClassA; import cz.cvut.kbss.jopa.test.OWLClassB; import cz.cvut.kbss.jopa.test.OWLClassD; import cz.cvut.kbss.jopa.test.OWLClassE; import cz.cvut.kbss.jopa.test.OWLClassM; +import cz.cvut.kbss.jopa.test.OWLClassT; import cz.cvut.kbss.jopa.test.OWLClassV; import cz.cvut.kbss.jopa.test.OWLClassY; import cz.cvut.kbss.jopa.test.Thing; @@ -55,7 +57,10 @@ import static cz.cvut.kbss.jopa.test.environment.util.ContainsSameEntities.containsSameEntities; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -529,4 +534,41 @@ public void querySupportsGroupConcatMultilingualStringsBasedOnFetchGraph() { .anyMatch(resMls -> value.equals(resMls.get(lang)))))); } } + + @Test + public void querySupportsNamedFetchGraphs() { + final List instances = QueryTestEnvironment.getData(OWLClassT.class); + final EntityGraph fetchGraph = getEntityManager().getEntityGraph("OWLClassT.basic"); + final List result = getEntityManager().createQuery("SELECT t FROM OWLClassT t", OWLClassT.class) + .setHint(QueryHints.FETCH_GRAPH, fetchGraph) + .getResultList(); + + assertEquals(instances.size(), result.size()); + result.forEach(r -> { + assertNotNull(r.getName()); + assertNotNull(r.getDescription()); + assertNotNull(r.getModified()); + assertNull(r.getIntAttribute()); + assertThat(r.getOwlClassA(), anyOf(nullValue(), instanceOf(LazyLoadingEntityProxy.class))); + }); + } + + @Test + public void querySupportsNestedNamedFetchGraphs() { + final List instances = QueryTestEnvironment.getData(OWLClassT.class); + final EntityGraph fetchGraph = getEntityManager().getEntityGraph("OWLClassT.withA"); + final List result = getEntityManager().createQuery("SELECT t FROM OWLClassT t", OWLClassT.class) + .setHint(QueryHints.FETCH_GRAPH, fetchGraph) + .getResultList(); + + assertEquals(instances.size(), result.size()); + result.forEach(r -> { + assertNotNull(r.getName()); + assertNotNull(r.getDescription()); + assertNotNull(r.getModified()); + assertNotNull(r.getOwlClassA()); + assertNotNull(r.getOwlClassA().getStringAttribute()); + assertNull(r.getIntAttribute()); + }); + } } From 1c2968da5bab50b9162d54b1393fc2fb3fa12051 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Thu, 28 May 2026 10:13:51 +0200 Subject: [PATCH 6/6] [Fix] Fix javadoc error. --- .../java/cz/cvut/kbss/jopa/model/NamedEntityGraphManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphManager.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphManager.java index e28257106..089a94fc2 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphManager.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphManager.java @@ -48,7 +48,7 @@ public EntityGraph getEntityGraph(String graphName) { * Checks whether this manager contains a graph with the specified name * * @param graphName Name to search for - * @return {@code true if such graph is registered here, {@code false} otherwise + * @return {@code true} if such graph is registered here, {@code false} otherwise */ public boolean hasEntityGraph(String graphName) { return namedEntityGraphs.containsKey(graphName);