diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 6ad2b4db7..bf1662822 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -18,5 +18,6 @@ jobs: with: distribution: 'temurin' java-version: 17 + cache: maven - name: Build with Maven run: mvn -P javadoc -B package --file pom.xml diff --git a/.gitignore b/.gitignore index 056b3fefa..387d0f610 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,5 @@ build.out **/catalog-v001.xml .project .classpath -org.* .factorypath **/gen diff --git a/CHANGELOG.md b/CHANGELOG.md index a5b0f03a1..9155f64df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # JOPA - Change Log +### 2.10.0 - 2026-05-29 + +- Support optimizing entity loading from query result by using [fetch graphs](https://github.com/kbss-cvut/jopa/wiki/Performance#fetch-graph-based-entity-loading) (Enhancement #425, #431, #434). +- Implement more predictable naming strategy in OWL2Java (Enhancement #432) - see the [README](jopa-owl2java/README.md) for details. +- Ensure annotation-based entity descriptor is honored when loading entities from query result (Enhancement #437). +- Dependency updates: RDF4J 5.3.1. + ### 2.9.4 - 2026-04-17 - Change return type of `CriteriaBuilder.langMatches` to `Predicate` to allow including it in predicate lists. diff --git a/datatype/pom.xml b/datatype/pom.xml index a38f766a4..7471a4449 100644 --- a/datatype/pom.xml +++ b/datatype/pom.xml @@ -6,7 +6,7 @@ jopa-all cz.cvut.kbss.jopa - 2.9.4 + 2.10.0 ../pom.xml diff --git a/jopa-api/pom.xml b/jopa-api/pom.xml index 295e01174..de6e3cb80 100644 --- a/jopa-api/pom.xml +++ b/jopa-api/pom.xml @@ -6,7 +6,7 @@ cz.cvut.kbss.jopa jopa-all - 2.9.4 + 2.10.0 ../pom.xml diff --git a/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/AttributeNode.java b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/AttributeNode.java new file mode 100644 index 000000000..b96d4d48a --- /dev/null +++ b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/AttributeNode.java @@ -0,0 +1,25 @@ +package cz.cvut.kbss.jopa.model; + +import java.util.Map; + +/** + * Represents an attribute node of an entity graph. + * + * @param The type of the attribute + */ +public interface AttributeNode { + + /** + * Gets the name of the attribute corresponding to the attribute node. + * + * @return name of the attribute + */ + String getAttributeName(); + + /** + * Return the map of subgraphs associated with this attribute node. + * + * @return Map of subgraphs associated with this attribute node or empty Map if none have been defined + */ + Map getSubgraphs(); +} diff --git a/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/EntityGraph.java b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/EntityGraph.java new file mode 100644 index 000000000..c7da3a44d --- /dev/null +++ b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/EntityGraph.java @@ -0,0 +1,108 @@ +package cz.cvut.kbss.jopa.model; + +import cz.cvut.kbss.jopa.model.metamodel.Attribute; + +import java.util.List; + +/** + * This type represents the root of an entity graph that will be used as a template to define the attribute nodes and + * boundaries of a graph of entities and entity relationships. + *

+ * The root must be an entity type. + *

+ * The methods to add subgraphs implicitly create the corresponding attribute nodes as well; such attribute nodes should + * not be redundantly specified. + * + * @param The type of the root entity + */ +public interface EntityGraph { + + /** + * Adds one or more attribute nodes to the entity graph. + * + * @param attribute Attribute to add + * @throws IllegalStateException If this {@code EntityGraph} has been statically defined + */ + void addAttributeNodes(Attribute... attribute); + + /** + * Adds one or more attribute nodes to the entity graph. + * + * @param attributeName Name of the attribute to add + * @throws IllegalStateException If this {@code EntityGraph} has been statically defined + * @throws IllegalArgumentException If the attribute is not an attribute of this managed type + */ + void addAttributeNodes(String... attributeName); + + /** + * Adds a node to the graph that corresponds to a managed type. + *

+ * This allows for construction of multi-node entity graphs that include related managed types. + * + * @param attribute Attribute to add + * @param Attribute type + * @return Subgraph for the attribute + * @throws IllegalStateException If this {@code EntityGraph} has been statically defined + * @throws IllegalArgumentException If the attribute's target type is not a managed type + */ + Subgraph addSubgraph(Attribute attribute); + + /** + * Adds a node to the graph that corresponds to a managed type. + *

+ * This allows for construction of multi-node entity graphs that include related managed types. + * + * @param attributeName Name of the attribute to add + * @param Attribute type + * @return Subgraph for the attribute + * @throws IllegalStateException If this {@code EntityGraph} has been statically defined + * @throws IllegalArgumentException If the attribute is not an attribute of this managed type + * @throws IllegalArgumentException If the attribute's target type is not a managed type + */ + Subgraph addSubgraph(String attributeName); + + /** + * Adds a node to the graph that corresponds to a managed type with inheritance. + *

+ * This allows for multiple subclass subgraphs to be defined for this node of the entity graph. Subclass subgraphs + * will automatically include the specified attributes of superclass subgraphs. + * + * @param attribute Attribute to add + * @param cls Entity subclass + * @param Attribute type + * @return Subgraph for the attribute + * @throws IllegalStateException If this {@code EntityGraph} has been statically defined + * @throws IllegalArgumentException If the attribute's target type is not a managed type + */ + Subgraph addSubgraph(Attribute attribute, Class cls); + + /** + * Add additional attributes to this entity graph that correspond to attributes of subclasses of this EntityGraph's + * entity type. + *

+ * Subclass subgraphs will automatically include the specified attributes of superclass subgraphs. + * + * @param cls Entity subclass + * @return Subgraph for the subclass + * @throws IllegalStateException If this {@code EntityGraph} has been statically defined + * @throws IllegalArgumentException If the type is not a managed type + */ + Subgraph addSubclassSubgraph(Class cls); + + /** + * Gets the attribute nodes corresponding to the attributes of this managed type that are included in the subgraph. + * + * @return List of attribute nodes included in the subgraph or empty list if none have been defined + */ + List> getAttributeNodes(); + + /** + * Gets the name of a named EntityGraph (an entity graph defined by means of the NamedEntityGraph annotation, or + * added by means of the addNamedEntityGraph method). + *

+ * Returns null if the EntityGraph is not a named EntityGraph. + * + * @return Entity graph name, {@code null} if it is not named + */ + String getName(); +} 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 727efdd64..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 @@ -353,6 +353,26 @@ public interface EntityManager extends AutoCloseable { */ Query createNativeQuery(String sparqlString, String resultSetMapping); + /** + * Returns a mutable {@link EntityGraph} that can be used to dynamically create an entity graph. + * + * @param rootType Entity class + * @param Root entity class type + * @return Entity graph + */ + 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/Subgraph.java b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/Subgraph.java new file mode 100644 index 000000000..49768d563 --- /dev/null +++ b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/Subgraph.java @@ -0,0 +1,88 @@ +package cz.cvut.kbss.jopa.model; + +import cz.cvut.kbss.jopa.model.metamodel.Attribute; + +import java.util.List; + +/** + * This type represents a subgraph for an attribute node that corresponds to a Managed Type. + *

+ * Using this class, an entity subgraph can be embedded within an EntityGraph. + * + * @param The type of the attribute + */ +public interface Subgraph { + + /** + * Adds one or more attribute nodes to the entity graph. + * + * @param attribute Attribute to add + * @throws IllegalStateException If this {@code EntityGraph} has been statically defined + */ + void addAttributeNodes(Attribute... attribute); + + /** + * Adds one or more attribute nodes to the entity graph. + * + * @param attributeName Name of the attribute to add + * @throws IllegalStateException If this {@code EntityGraph} has been statically defined + * @throws IllegalArgumentException If the attribute is not an attribute of this managed type + */ + void addAttributeNodes(String... attributeName); + + /** + * Adds a node to the graph that corresponds to a managed type. + *

+ * This allows for construction of multi-node entity graphs that include related managed types. + * + * @param attribute Attribute to add + * @param Attribute type + * @return Subgraph for the attribute + * @throws IllegalStateException If this {@code EntityGraph} has been statically defined + * @throws IllegalArgumentException If the attribute's target type is not a managed type + */ + Subgraph addSubgraph(Attribute attribute); + + /** + * Adds a node to the graph that corresponds to a managed type. + *

+ * This allows for construction of multi-node entity graphs that include related managed types. + * + * @param attributeName Name of the attribute to add + * @param Attribute type + * @return Subgraph for the attribute + * @throws IllegalStateException If this {@code EntityGraph} has been statically defined + * @throws IllegalArgumentException If the attribute is not an attribute of this managed type + * @throws IllegalArgumentException If the attribute's target type is not a managed type + */ + Subgraph addSubgraph(String attributeName); + + /** + * Adds a node to the graph that corresponds to a managed type with inheritance. + *

+ * This allows for multiple subclass subgraphs to be defined for this node of the entity graph. Subclass subgraphs + * will automatically include the specified attributes of superclass subgraphs. + * + * @param attribute Attribute to add + * @param cls Entity subclass + * @param Attribute type + * @return Subgraph for the attribute + * @throws IllegalStateException If this {@code EntityGraph} has been statically defined + * @throws IllegalArgumentException If the attribute's target type is not a managed type + */ + Subgraph addSubgraph(Attribute attribute, Class cls); + + /** + * Gets the attribute nodes corresponding to the attributes of this managed type that are included in the subgraph. + * + * @return List of attribute nodes included in the subgraph or empty list if none have been defined + */ + List> getAttributeNodes(); + + /** + * Gets the type for which this subgraph was defined. + * + * @return managed type referenced by the subgraph + */ + Class getClassType(); +} 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..697b2b352 --- /dev/null +++ b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/NamedEntityGraph.java @@ -0,0 +1,53 @@ +package cz.cvut.kbss.jopa.model.annotations; + +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; + +/** + * 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}. + */ +@Repeatable(NamedEntityGraphs.class) +@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() default ""; + + /** + * (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/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-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-distribution/pom.xml b/jopa-distribution/pom.xml index 2009c9c66..a3a98f345 100644 --- a/jopa-distribution/pom.xml +++ b/jopa-distribution/pom.xml @@ -6,7 +6,7 @@ cz.cvut.kbss.jopa jopa-all - 2.9.4 + 2.10.0 ../pom.xml diff --git a/jopa-impl/pom.xml b/jopa-impl/pom.xml index f74296bf7..c8a8b6fce 100644 --- a/jopa-impl/pom.xml +++ b/jopa-impl/pom.xml @@ -6,7 +6,7 @@ cz.cvut.kbss.jopa jopa-all - 2.9.4 + 2.10.0 ../pom.xml 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..0805b940d --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/loaders/NamedEntityGraphLoader.java @@ -0,0 +1,44 @@ +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.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 Map, Set> graphDeclarations = new HashMap<>(); + + @Override + public void accept(Class aClass) { + final NamedEntityGraph graphDeclaration = aClass.getDeclaredAnnotation(NamedEntityGraph.class); + if (graphDeclaration != null) { + addDeclaration(aClass, graphDeclaration); + } + final NamedEntityGraphs graphDeclarationSet = aClass.getDeclaredAnnotation(NamedEntityGraphs.class); + if (graphDeclarationSet != null) { + for (NamedEntityGraph g : graphDeclarationSet.value()) { + addDeclaration(aClass, g); + } + } + } + + 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 aa9c9f95b..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 @@ -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,19 @@ public Set getResultSetMappings() { return resultSetMappingLoader.getMappings(); } + /** + * 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 Map of classes to named entity graph annotations declared on them + */ + public Map, 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/AbstractQuery.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/AbstractQuery.java index fa356fc76..d08ecb15a 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/AbstractQuery.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/AbstractQuery.java @@ -24,11 +24,9 @@ import cz.cvut.kbss.jopa.model.query.Query; import cz.cvut.kbss.jopa.query.QueryHolder; import cz.cvut.kbss.jopa.sessions.ConnectionWrapper; -import cz.cvut.kbss.jopa.utils.ThrowingConsumer; import cz.cvut.kbss.ontodriver.ResultSet; import cz.cvut.kbss.ontodriver.Statement; import cz.cvut.kbss.ontodriver.exception.OntoDriverException; -import cz.cvut.kbss.ontodriver.iteration.ResultRow; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -121,7 +119,7 @@ public void executeUpdate() { } } - private Statement initQueryStatement() { + Statement initQueryStatement() { final Statement stmt = connection.createStatement(); applyQueryHints(stmt); logQuery(); @@ -265,21 +263,6 @@ void checkNumericParameter(int param, String name) { } } - /** - * Executes the query and lets the specified consumer deal with each row in the result set. - * - * @param consumer Called for every row in the result set - * @throws OntoDriverException When something goes wrong during query evaluation or result set processing - */ - void executeQuery(ThrowingConsumer consumer) throws OntoDriverException { - try (final Statement stmt = initQueryStatement()) { - final ResultSet rs = stmt.executeQuery(query.assembleQuery()); - for (ResultRow row : rs) { - consumer.accept(row); - } - } - } - private void applyQueryHints(Statement statement) { hints.forEach((hint, value) -> QueryHintsHandler.apply(hint, value, this, statement)); } @@ -287,6 +270,7 @@ private void applyQueryHints(Statement statement) { Stream executeQueryForStream(QueryResultLoader resultLoader) throws OntoDriverException { final Statement stmt = initQueryStatement(); final ResultSet rs = stmt.executeQuery(query.assembleQuery()); + resultLoader.init(rs); final Runnable closeHandler = () -> { try { stmt.close(); diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/AttributeNodeImpl.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/AttributeNodeImpl.java new file mode 100644 index 000000000..425e98a76 --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/AttributeNodeImpl.java @@ -0,0 +1,40 @@ +package cz.cvut.kbss.jopa.model; + +import cz.cvut.kbss.jopa.model.metamodel.Attribute; + +import java.util.HashMap; +import java.util.Map; + +public class AttributeNodeImpl implements AttributeNode { + + private final Attribute attribute; + + private final Map subgraphs = new HashMap<>(); + + public AttributeNodeImpl(Attribute attribute) { + this.attribute = attribute; + } + + public Attribute getAttribute() { + return attribute; + } + + @Override + public String getAttributeName() { + return attribute.getName(); + } + + @Override + public Map getSubgraphs() { + return subgraphs; + } + + void addSubgraph(EntityGraphImpl graph) { + subgraphs.put(graph.getClassType(), graph); + } + + @Override + public String toString() { + return attribute.toString(); + } +} 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 new file mode 100644 index 000000000..04d8b0b02 --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/EntityGraphImpl.java @@ -0,0 +1,99 @@ +package cz.cvut.kbss.jopa.model; + +import cz.cvut.kbss.jopa.model.metamodel.Attribute; +import cz.cvut.kbss.jopa.model.metamodel.ManagedType; +import cz.cvut.kbss.jopa.model.metamodel.MetamodelClassMapper; +import cz.cvut.kbss.jopa.model.metamodel.PluralAttribute; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class EntityGraphImpl implements EntityGraph, Subgraph { + + private final String name; + + private final ManagedType entityType; + private final MetamodelClassMapper metamodel; + + private final Map> attributeNodes = new HashMap<>(); + + public EntityGraphImpl(ManagedType entityType, MetamodelClassMapper metamodel) { + this.entityType = entityType; + this.metamodel = metamodel; + this.name = null; + } + + public EntityGraphImpl(String name, ManagedType entityType, MetamodelClassMapper metamodel) { + this.name = name; + this.entityType = entityType; + this.metamodel = metamodel; + } + + @Override + public void addAttributeNodes(Attribute... attribute) { + for (Attribute att : attribute) { + attributeNodes.put(att.getName(), new AttributeNodeImpl<>(att)); + } + } + + @Override + public void addAttributeNodes(String... attributeName) { + for (String attName : attributeName) { + final Attribute att = entityType.getAttribute(attName); + assert att != null; + attributeNodes.put(attName, new AttributeNodeImpl<>(att)); + } + } + + @Override + public EntityGraphImpl addSubgraph(Attribute attribute) { + final AttributeNodeImpl node = (AttributeNodeImpl) attributeNodes.computeIfAbsent(attribute.getName(), k -> new AttributeNodeImpl<>(attribute)); + Class javaType = attribute.getJavaType(); + if (attribute.isCollection()) { + javaType = ((PluralAttribute) attribute).getBindableJavaType(); + } + final EntityGraphImpl g = new EntityGraphImpl(metamodel.entity(javaType), metamodel); + node.addSubgraph(g); + return g; + } + + @Override + public Subgraph addSubgraph(String attributeName) { + final Attribute att = (Attribute) entityType.getAttribute(attributeName); + return addSubgraph(att); + } + + @Override + public Subgraph addSubgraph(Attribute attribute, Class cls) { + final EntityGraphImpl g = addSubgraph(attribute); + g.addSubclassSubgraph(cls); + return g; + } + + @Override + public Subgraph addSubclassSubgraph(Class cls) { + // TODO + return null; + } + + @Override + public List> getAttributeNodes() { + return new ArrayList<>(attributeNodes.values()); + } + + AttributeNodeImpl getAttributeNode(String name) { + return attributeNodes.get(name); + } + + @Override + public String getName() { + return name; + } + + @Override + public Class getClassType() { + return entityType.getJavaType(); + } +} 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 f75a83ece..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 @@ -469,9 +469,13 @@ public boolean isLoaded(Object object) { public QueryImpl createQuery(String qlString) { ensureOpen(); final QueryImpl q = getCurrentPersistenceContext().sparqlQueryFactory().createQuery(qlString); + initQueryCallbacks(q); + return q; + } + + private void initQueryCallbacks(AbstractQuery q) { q.setRollbackOnlyMarker(this::markTransactionForRollback); q.setEnsureOpenProcedure(this::ensureOpen); - return q; } @Override @@ -483,19 +487,25 @@ public TypedQuery createQuery(CriteriaQuery criteriaQuery) { LOG.debug("CriteriaQuery translate to SOQL query: {}", soqlQuery); final TypedQueryImpl q = getCurrentPersistenceContext().sparqlQueryFactory() .createQuery(soqlQuery, query.getResultType()); - q.setRollbackOnlyMarker(this::markTransactionForRollback); - q.setEnsureOpenProcedure(this::ensureOpen); + initQueryCallbacks(q); + initTypedQueryDescriptor(query.getResultType(), q); parameterFiller.setValuesToRegisteredParameters(q); return q; } + private void initTypedQueryDescriptor(Class resultClass, TypedQuery q) { + if (getCurrentPersistenceContext().isEntityType(resultClass)) { + q.setDescriptor(descriptorFactory.createDescriptor(resultClass)); + } + } + @Override public TypedQueryImpl createQuery(String query, Class resultClass) { ensureOpen(); final TypedQueryImpl q = getCurrentPersistenceContext().sparqlQueryFactory().createQuery(query, resultClass); - q.setRollbackOnlyMarker(this::markTransactionForRollback); - q.setEnsureOpenProcedure(this::ensureOpen); + initQueryCallbacks(q); + initTypedQueryDescriptor(resultClass, q); return q; } @@ -503,8 +513,7 @@ public TypedQueryImpl createQuery(String query, Class resultClass) { public QueryImpl createNativeQuery(String sparqlString) { ensureOpen(); final QueryImpl q = getCurrentPersistenceContext().sparqlQueryFactory().createNativeQuery(sparqlString); - q.setRollbackOnlyMarker(this::markTransactionForRollback); - q.setEnsureOpenProcedure(this::ensureOpen); + initQueryCallbacks(q); return q; } @@ -513,8 +522,8 @@ public TypedQueryImpl createNativeQuery(String sparqlString, Class res ensureOpen(); final TypedQueryImpl q = getCurrentPersistenceContext().sparqlQueryFactory() .createNativeQuery(sparqlString, resultClass); - q.setRollbackOnlyMarker(this::markTransactionForRollback); - q.setEnsureOpenProcedure(this::ensureOpen); + initQueryCallbacks(q); + initTypedQueryDescriptor(resultClass, q); return q; } @@ -523,8 +532,7 @@ public QueryImpl createNativeQuery(String sparqlString, String resultSetMapping) ensureOpen(); final QueryImpl q = getCurrentPersistenceContext().sparqlQueryFactory() .createNativeQuery(sparqlString, resultSetMapping); - q.setRollbackOnlyMarker(this::markTransactionForRollback); - q.setEnsureOpenProcedure(this::ensureOpen); + initQueryCallbacks(q); return q; } @@ -532,8 +540,7 @@ public QueryImpl createNativeQuery(String sparqlString, String resultSetMapping) public QueryImpl createNamedQuery(String name) { ensureOpen(); final QueryImpl q = getCurrentPersistenceContext().sparqlQueryFactory().createNamedQuery(name); - q.setRollbackOnlyMarker(this::markTransactionForRollback); - q.setEnsureOpenProcedure(this::ensureOpen); + initQueryCallbacks(q); return q; } @@ -542,8 +549,8 @@ public TypedQueryImpl createNamedQuery(String name, Class resultClass) ensureOpen(); final TypedQueryImpl q = getCurrentPersistenceContext().sparqlQueryFactory() .createNamedQuery(name, resultClass); - q.setRollbackOnlyMarker(this::markTransactionForRollback); - q.setEnsureOpenProcedure(this::ensureOpen); + initQueryCallbacks(q); + initTypedQueryDescriptor(resultClass, q); return q; } @@ -570,6 +577,18 @@ public CriteriaBuilder getCriteriaBuilder() { return getCurrentPersistenceContext().getCriteriaBuilder(); } + @Override + public EntityGraph createEntityGraph(Class rootType) { + ensureOpen(); + return getCurrentPersistenceContext().createEntityGraph(rootType); + } + + @Override + public EntityGraph getEntityGraph(String graphName) { + ensureOpen(); + return getCurrentPersistenceContext().getEntityGraph(graphName); + } + @Override public T unwrap(Class cls) { ensureOpen(); 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..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 @@ -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); @@ -62,6 +63,7 @@ public class MetamodelImpl implements Metamodel, MetamodelProvider { private NamedQueryManager namedQueryManager; private ResultSetMappingManager resultSetMappingManager; + private NamedEntityGraphManager namedEntityGraphManager; private NamespaceResolver namespaceResolver; @@ -95,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(); } @@ -169,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/model/NamedEntityGraphManager.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphManager.java new file mode 100644 index 000000000..089a94fc2 --- /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..6107b777a --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/NamedEntityGraphProcessor.java @@ -0,0 +1,117 @@ +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; + + private final MetamodelClassMapper metamodel; + + public NamedEntityGraphProcessor(MetamodelClassMapper metamodel) { + this.metamodel = metamodel; + this.manager = new NamedEntityGraphManager(); + } + + public NamedEntityGraphManager getManager() { + return manager; + } + + /** + * 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/QueryHintsHandler.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/QueryHintsHandler.java index 2e2053793..00db8dc74 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/QueryHintsHandler.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/QueryHintsHandler.java @@ -97,6 +97,7 @@ abstract static class Hint { registerHint(new DisableInferenceHint()); registerHint(new TargetOntologyHint()); registerHint(new OptimizedQueryResultEntityLoadingHint()); + registerHint(new FetchGraphHint()); } Hint(String name, Object defaultValue) { @@ -225,4 +226,15 @@ void applyToQuery(Object hintValue, AbstractQuery query, Statement statement) { // Do nothing, the optimizer is enabled as soon as the hint is set } } + + protected static class FetchGraphHint extends Hint { + public FetchGraphHint() { + super(QueryHints.FETCH_GRAPH, null); + } + + @Override + void applyToQuery(Object hintValue, AbstractQuery query, Statement statement) { + // Do nothing, the query pulls the fetch graph from the hint automatically + } + } } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/QueryImpl.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/QueryImpl.java index bead95ffc..c11ac3055 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/QueryImpl.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/QueryImpl.java @@ -23,6 +23,8 @@ import cz.cvut.kbss.jopa.model.query.Query; import cz.cvut.kbss.jopa.query.QueryHolder; import cz.cvut.kbss.jopa.sessions.ConnectionWrapper; +import cz.cvut.kbss.ontodriver.ResultSet; +import cz.cvut.kbss.ontodriver.Statement; import cz.cvut.kbss.ontodriver.exception.OntoDriverException; import cz.cvut.kbss.ontodriver.iteration.ResultRow; @@ -57,7 +59,12 @@ public List getResultList() { private List getResultListImpl() throws OntoDriverException { final List res = new ArrayList<>(); - executeQuery(rs -> res.add(extractRow(rs))); + try (final Statement stmt = initQueryStatement()) { + final ResultSet rs = stmt.executeQuery(query.assembleQuery()); + for (ResultRow row : rs) { + res.add(extractRow(row)); + } + } return res; } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/QueryResultLoader.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/QueryResultLoader.java index d5edc3948..6946df4f5 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/QueryResultLoader.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/QueryResultLoader.java @@ -17,6 +17,7 @@ */ package cz.cvut.kbss.jopa.model; +import cz.cvut.kbss.ontodriver.ResultSet; import cz.cvut.kbss.ontodriver.iteration.ResultRow; import java.util.Optional; @@ -28,6 +29,10 @@ */ public interface QueryResultLoader { + default void init(ResultSet resultSet) { + // Do nothing by default + } + /** * Loads the result from the given result row. * diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/TypedQueryImpl.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/TypedQueryImpl.java index 3012eacf5..cc1c5ec65 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/TypedQueryImpl.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/TypedQueryImpl.java @@ -25,9 +25,13 @@ import cz.cvut.kbss.jopa.model.query.TypedQuery; import cz.cvut.kbss.jopa.query.QueryHints; import cz.cvut.kbss.jopa.query.QueryHolder; -import cz.cvut.kbss.jopa.query.sparql.loader.QueryResultLoadingOptimizer; +import cz.cvut.kbss.jopa.query.sparql.loader.QueryResultLoadingStrategy; +import cz.cvut.kbss.jopa.query.sparql.loader.SparqlQueryResultLoadingOptimizer; import cz.cvut.kbss.jopa.sessions.ConnectionWrapper; +import cz.cvut.kbss.ontodriver.ResultSet; +import cz.cvut.kbss.ontodriver.Statement; import cz.cvut.kbss.ontodriver.exception.OntoDriverException; +import cz.cvut.kbss.ontodriver.iteration.ResultRow; import java.util.ArrayList; import java.util.List; @@ -38,12 +42,12 @@ public class TypedQueryImpl extends AbstractQuery implements TypedQuery { private final Class resultType; - private final QueryResultLoadingOptimizer queryResultLoadingOptimizer; + private final SparqlQueryResultLoadingOptimizer queryResultLoadingOptimizer; private Descriptor descriptor = new EntityDescriptor(); public TypedQueryImpl(QueryHolder query, Class resultType, ConnectionWrapper connection, - QueryResultLoadingOptimizer queryResultLoadingOptimizer) { + SparqlQueryResultLoadingOptimizer queryResultLoadingOptimizer) { super(query, connection); this.resultType = Objects.requireNonNull(resultType); this.queryResultLoadingOptimizer = queryResultLoadingOptimizer; @@ -65,14 +69,30 @@ public List getResultList() { private List getResultListImpl() throws OntoDriverException { final List res = new ArrayList<>(); - queryResultLoadingOptimizer.optimizeQueryAssembly(resultType, descriptor); - final QueryResultLoader resultLoader = queryResultLoadingOptimizer.getQueryResultLoader(resultType, descriptor); - - executeQuery(rr -> resultLoader.loadResult(rr).ifPresent(res::add)); + final EntityGraph fetchGraph = getFetchGraph(); + final QueryResultLoadingStrategy strategy = queryResultLoadingOptimizer.chooseStrategy(resultType, descriptor, fetchGraph); + final QueryResultLoader resultLoader = strategy.createLoader(); + + try (final Statement stmt = initQueryStatement()) { + final ResultSet rs = stmt.executeQuery(query.assembleQuery()); + resultLoader.init(rs); + for (ResultRow row : rs) { + resultLoader.loadResult(row).ifPresent(res::add); + } + } resultLoader.loadLastPending().ifPresent(res::add); return res; } + @SuppressWarnings("unchecked") + private EntityGraph getFetchGraph() { + final Object fetchGraph = getHints().get(QueryHints.FETCH_GRAPH); + if (fetchGraph != null && !(fetchGraph instanceof EntityGraph)) { + throw new IllegalArgumentException("Fetch graph query hint value must be an instance of " + EntityGraph.class.getName()); + } + return (EntityGraph) fetchGraph; + } + public Descriptor getDescriptor() { return descriptor; } @@ -102,8 +122,9 @@ public X getSingleResult() { @Override public Stream getResultStream() { - queryResultLoadingOptimizer.optimizeQueryAssembly(resultType, descriptor); - final QueryResultLoader resultLoader = queryResultLoadingOptimizer.getQueryResultLoader(resultType, descriptor); + final EntityGraph fetchGraph = getFetchGraph(); + final QueryResultLoadingStrategy strategy = queryResultLoadingOptimizer.chooseStrategy(resultType, descriptor, fetchGraph); + final QueryResultLoader resultLoader = strategy.createLoader(); try { return executeQueryForStream(resultLoader); } catch (OntoDriverException e) { diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/AbstractIdentifiableType.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/AbstractIdentifiableType.java index 64aadb6f2..765cb14b0 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/AbstractIdentifiableType.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/AbstractIdentifiableType.java @@ -285,6 +285,27 @@ public boolean hasVersionAttribute() { }); } + public AbstractAttribute getAttributeIncludingSubTypes(String name) { + return getDeclaredAttributeImpl(name).orElseGet(() -> { + final AbstractAttribute subAtt = recursivelyGetDeclaredAttribute(name); + if (subAtt != null) { + return subAtt; + } + if (classSupertype != null) { + return classSupertype.getAttribute(name, javaType); + } + throw attributeMissing(name, false); + }); + } + + private AbstractAttribute recursivelyGetDeclaredAttribute(String name) { + return getDeclaredAttributeImpl(name).orElseGet(() -> (AbstractAttribute) subtypes.stream() + .map(ait -> ait.recursivelyGetDeclaredAttribute(name)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null)); + } + /** * Gets attribute with the specified name. *

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..4d751cbb5 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,8 +20,9 @@ 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; import cz.cvut.kbss.jopa.model.annotations.InheritanceType; import cz.cvut.kbss.jopa.model.annotations.OWLAnnotationProperty; @@ -48,12 +49,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 +76,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 +92,8 @@ public void buildMetamodel(PersistenceUnitClassFinder classFinder) { processDeferredFields(); typeMap.values().forEach(AbstractIdentifiableType::finish); classFinder.getResultSetMappings().forEach(mappingProcessor::buildMapper); + classFinder.getNamedEntityGraphs().forEach((cls, graphs) -> + graphs.forEach(g -> entityGraphProcessor.buildEntityGraph(cls, g))); } /** @@ -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); } @@ -312,7 +322,8 @@ public TypeReferenceMap getTypeReferenceMap() { } void createLazyLoadingProxy(Attribute attribute) { - if (attribute.getFetchType() == FetchType.LAZY && !attribute.isCollection()) { + if (!attribute.isCollection() && hasManagedType(attribute.getJavaType())) { + // Even if the field is not lazy, it may be lazily initialized due to EntityGraph usage final Class cls = attribute.getJavaType(); lazyLoadingProxyClasses.computeIfAbsent(cls, lazyLoadingEntityProxyGenerator::generate); } 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); +} diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/EntityConstructor.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/EntityConstructor.java index 080180c81..5949f13f0 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/EntityConstructor.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/EntityConstructor.java @@ -34,9 +34,11 @@ import cz.cvut.kbss.jopa.oom.query.PluralQueryAttributeStrategy; import cz.cvut.kbss.jopa.oom.query.QueryFieldStrategy; import cz.cvut.kbss.jopa.oom.query.SingularQueryAttributeStrategy; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.query.sparql.SparqlQueryFactory; import cz.cvut.kbss.jopa.sessions.descriptor.LoadStateDescriptor; import cz.cvut.kbss.jopa.sessions.descriptor.LoadStateDescriptorFactory; +import cz.cvut.kbss.jopa.sessions.util.FetchGraphWrapper; import cz.cvut.kbss.jopa.sessions.util.LoadStateDescriptorRegistry; import cz.cvut.kbss.jopa.sessions.validator.IntegrityConstraintsValidator; import cz.cvut.kbss.jopa.utils.CollectionFactory; @@ -54,7 +56,6 @@ import java.util.Set; import static cz.cvut.kbss.jopa.model.metamodel.AbstractQueryAttribute.THIS_PARAMETER; -import static cz.cvut.kbss.jopa.sessions.validator.IntegrityConstraintsValidator.isNotLazy; class EntityConstructor { @@ -87,12 +88,14 @@ T reconstructEntity(EntityConstructionParameters constructionParams, Coll } final T instance = createEntityInstance(constructionParams.id(), et); mapper.registerInstance(constructionParams.id(), instance); - final LoadStateDescriptor loadStateDescriptor = LoadStateDescriptorFactory.createAllUnknown(instance, et); + final LoadStateDescriptor loadStateDescriptor = constructionParams.fetchGraph().isPresent() + ? LoadStateDescriptorFactory.createForFetchGraph(instance, et, constructionParams.fetchGraph()) + : LoadStateDescriptorFactory.createAllUnknown(instance, et); loadStateRegistry.put(instance, loadStateDescriptor); populateAttributes(instance, constructionParams, axioms, loadStateDescriptor); populateQueryAttributes(instance, et, loadStateDescriptor); processEmptyAttributes(instance, et, loadStateDescriptor); - validateIntegrityConstraints(instance, et); + validateIntegrityConstraints(instance, et, loadStateDescriptor); return instance; } @@ -126,14 +129,14 @@ private void populateAttributes(final T instance, EntityConstructionParamete continue; } final FieldStrategy, T> fs = getFieldLoader( - ax, attributes, fieldLoaders, et, constructionParams.descriptor()); + ax, attributes, fieldLoaders, constructionParams); if (fs == null) { if (!MappingUtils.isClassAssertion(ax)) { LOG.trace("No attribute found for property {}. Axiom {} will be skipped.", ax.getAssertion(), ax); } continue; } - if (fs.attribute.getFetchType() == FetchType.LAZY && !constructionParams.forceEager()) { + if (isLazilyLoaded(fs.attribute, constructionParams)) { fs.lazilyAddAxiomValue(ax); } else { fs.addAxiomValue(ax); @@ -142,7 +145,7 @@ private void populateAttributes(final T instance, EntityConstructionParamete // We need to build the field values separately because some may be plural, and we have to wait until all values are prepared for (FieldStrategy, ?> fs : fieldLoaders.values()) { fs.buildInstanceFieldValue(instance); - if (fs.attribute.getFetchType() == FetchType.LAZY && !constructionParams.forceEager() && fs.hasValue()) { + if (isLazilyLoaded(fs.attribute, constructionParams) && fs.hasValue()) { loadStateDescriptor.setLoaded((FieldSpecification) fs.attribute, LoadState.NOT_LOADED); } else { loadStateDescriptor.setLoaded((FieldSpecification) fs.attribute, LoadState.LOADED); @@ -165,22 +168,27 @@ private void populateAttributes(final T instance, EntityConstructionParamete Axiom ax, Map> attributes, Map, FieldStrategy, T>> loaders, - EntityType et, Descriptor desc) { + EntityConstructor.EntityConstructionParameters constructionParams) { final URI attId = ax.getAssertion().getIdentifier(); FieldSpecification att = attributes.get(attId); if (att == null) { - if (et.getProperties() != null) { - att = et.getProperties(); + if (constructionParams.entityType().getProperties() != null) { + att = constructionParams.entityType().getProperties(); } else { return null; } } if (!loaders.containsKey(att)) { - loaders.put(att, FieldStrategy.createFieldStrategy(et, att, desc, mapper)); + loaders.put(att, FieldStrategy.createFieldStrategy(constructionParams.entityType(), att, new ObjectGraphInfo(constructionParams.descriptor(), constructionParams.fetchGraph()), mapper)); } return loaders.get(att); } + private boolean isLazilyLoaded(FieldSpecification att, EntityConstructionParameters constructionParams) { + return att.getFetchType() == FetchType.LAZY && !constructionParams.fetchGraph() + .hasAttribute(att) && !constructionParams.forceEager(); + } + /** * Populate all query based attributes in the given instance. * @@ -274,7 +282,7 @@ private static void setAttributeQueryParameters(T instance, QueryAttribute void processEmptyAttributes(T entity, EntityType et, LoadStateDescriptor loadStateDescriptor) { et.getFieldSpecifications().stream() .filter(fs -> { - final Object value= EntityPropertiesUtils.getFieldValue(fs.getJavaField(), entity); + final Object value = EntityPropertiesUtils.getFieldValue(fs.getJavaField(), entity); return value == null || (value instanceof Collection && ((Collection) value).isEmpty()); }) .forEach(fs -> { @@ -291,11 +299,13 @@ private void processEmptyAttributes(T entity, EntityType et, LoadStateDes }); } - private void validateIntegrityConstraints(T entity, EntityType et) { + private void validateIntegrityConstraints(T entity, EntityType et, + LoadStateDescriptor loadStateDescriptor) { if (shouldSkipICValidationOnLoad()) { return; } - IntegrityConstraintsValidator.getValidator().validate(entity, et, isNotLazy()); + IntegrityConstraintsValidator.getValidator() + .validate(entity, et, fs -> loadStateDescriptor.isLoaded(fs) == LoadState.LOADED); } private boolean shouldSkipICValidationOnLoad() { @@ -315,7 +325,7 @@ void validateIntegrityConstraints(T entity, FieldSpecification void setFieldValue(T entity, FieldSpecification fieldSpec, Collection> axioms, EntityType et, Descriptor entityDescriptor) { final FieldStrategy, T> fs = FieldStrategy - .createFieldStrategy(et, fieldSpec, entityDescriptor, mapper); + .createFieldStrategy(et, fieldSpec, new ObjectGraphInfo(entityDescriptor), mapper); axioms.forEach(fs::addAxiomValue); fs.buildInstanceFieldValue(entity); validateIntegrityConstraints(entity, fieldSpec, et); @@ -332,9 +342,10 @@ void setQueryAttributeFieldValue(T entity, QueryAttribute quer * @param id Entity identifier * @param entityType Entity type * @param descriptor Entity descriptor, specifies e.g., repository contexts + * @param fetchGraph Entity fetch graph * @param forceEager Whether all attributes have to be loaded eagerly * @param Entity type */ record EntityConstructionParameters(URI id, IdentifiableEntityType entityType, Descriptor descriptor, - boolean forceEager) {} + FetchGraphWrapper fetchGraph, boolean forceEager) {} } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/EntityDeconstructor.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/EntityDeconstructor.java index 1115e6f79..f15001568 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/EntityDeconstructor.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/EntityDeconstructor.java @@ -22,6 +22,7 @@ import cz.cvut.kbss.jopa.model.metamodel.FieldSpecification; import cz.cvut.kbss.jopa.model.metamodel.QueryAttribute; import cz.cvut.kbss.jopa.oom.exception.EntityDeconstructionException; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.ontodriver.model.NamedResource; import java.net.URI; @@ -66,7 +67,7 @@ private void addAssertions(T entity, EntityType et, FieldSpecification fieldSpec, Descriptor entityDescriptor, final AxiomValueGatherer valueBuilder) { final FieldStrategy, T> fs = FieldStrategy - .createFieldStrategy(et, fieldSpec, entityDescriptor, mapper); + .createFieldStrategy(et, fieldSpec, new ObjectGraphInfo(entityDescriptor), mapper); fs.setReferenceSavingResolver(referenceSavingResolver); fs.buildAxiomValuesFromInstance(entity, valueBuilder); } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/EntityInstanceLoader.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/EntityInstanceLoader.java index b07938cf5..52053ce25 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/EntityInstanceLoader.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/EntityInstanceLoader.java @@ -127,7 +127,7 @@ U reconstructEntityFromAxioms(LoadingParameters loadingParam } } return axioms.isEmpty() ? null : entityBuilder.reconstructEntity( - new EntityConstructor.EntityConstructionParameters<>(loadingParameters.identifier(), et, loadingParameters.descriptor(), loadingParameters.forceEager()), + new EntityConstructor.EntityConstructionParameters<>(loadingParameters.identifier(), et, loadingParameters.descriptor(), loadingParameters.fetchGraph(), loadingParameters.forceEager()), axioms); } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/EntityMappingHelper.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/EntityMappingHelper.java index 766bc200e..05817b7b8 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/EntityMappingHelper.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/EntityMappingHelper.java @@ -17,8 +17,8 @@ */ package cz.cvut.kbss.jopa.oom; -import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.metamodel.EntityType; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.sessions.ConfigurationHolder; import cz.cvut.kbss.ontodriver.descriptor.ContainerDescriptor; import cz.cvut.kbss.ontodriver.descriptor.ReferencedListDescriptor; @@ -31,12 +31,10 @@ interface EntityMappingHelper extends ConfigurationHolder { - T getEntityFromCacheOrOntology(Class cls, URI identifier, Descriptor descriptor); + T getEntityFromCacheOrOntology(Class cls, URI identifier, ObjectGraphInfo objectGraphInfo); EntityType getEntityType(Class cls); - boolean isManagedType(Class cls); - URI generateIdentifier(EntityType et); T getOriginalInstance(T clone); diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/FieldStrategy.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/FieldStrategy.java index a16937df9..4d8e6826e 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/FieldStrategy.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/FieldStrategy.java @@ -32,6 +32,7 @@ import cz.cvut.kbss.jopa.model.metamodel.RdfContainerAttributeImpl; import cz.cvut.kbss.jopa.model.metamodel.SingularAttribute; import cz.cvut.kbss.jopa.model.metamodel.TypesSpecification; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.utils.EntityPropertiesUtils; import cz.cvut.kbss.ontodriver.model.Assertion; import cz.cvut.kbss.ontodriver.model.Axiom; @@ -45,6 +46,8 @@ import java.util.stream.Collectors; /** + * Handles actual mapping of a persistent field to/from axioms. + * * @param The attribute specification type, e.g. {@link SingularAttribute}, {@link ListAttribute} * @param Entity class */ @@ -67,16 +70,16 @@ abstract class FieldStrategy, X> { static FieldStrategy, X> createFieldStrategy(EntityType et, FieldSpecification att, - Descriptor entityDescriptor, + ObjectGraphInfo objectInfo, EntityMappingHelper mapper) { if (att.equals(et.getIdentifier())) { - return new IdentifierFieldStrategy<>(et, (Identifier) att, entityDescriptor, mapper); + return new IdentifierFieldStrategy<>(et, (Identifier) att, objectInfo.descriptor(), mapper); } if (att instanceof TypesSpecification) { - return new TypesFieldStrategy<>(et, (TypesSpecification) att, entityDescriptor, mapper); + return new TypesFieldStrategy<>(et, (TypesSpecification) att, objectInfo.descriptor(), mapper); } else if (att instanceof PropertiesSpecification) { return new PropertiesFieldStrategy<>(et, (PropertiesSpecification) att, - entityDescriptor, mapper); + objectInfo.descriptor(), mapper); } final AbstractAttribute attribute = (AbstractAttribute) att; if (attribute.isCollection()) { @@ -84,24 +87,24 @@ abstract class FieldStrategy, X> { case ANNOTATION: return createPluralAnnotationPropertyStrategy(et, (AbstractPluralAttribute) attribute, - entityDescriptor, mapper); + objectInfo.descriptor(), mapper); case DATA: return createPluralDataPropertyStrategy(et, (AbstractPluralAttribute) attribute, - entityDescriptor, mapper); + objectInfo.descriptor(), mapper); case OBJECT: return createPluralObjectPropertyStrategy(et, (AbstractPluralAttribute) attribute, - entityDescriptor, mapper); + objectInfo, mapper); default: break; } } else { switch (attribute.getPersistentAttributeType()) { case ANNOTATION: - return createSingularAnnotationPropertyStrategy(et, attribute, entityDescriptor, mapper); + return createSingularAnnotationPropertyStrategy(et, attribute, objectInfo.descriptor(), mapper); case DATA: - return createSingularDataPropertyStrategy(et, attribute, entityDescriptor, mapper); + return createSingularDataPropertyStrategy(et, attribute, objectInfo.descriptor(), mapper); case OBJECT: - return new SingularObjectPropertyStrategy<>(et, attribute, entityDescriptor, mapper); + return new SingularObjectPropertyStrategy<>(et, attribute, objectInfo, mapper); default: break; } @@ -129,7 +132,7 @@ abstract class FieldStrategy, X> { return new RdfContainerDataPropertyStrategy<>(et, (RdfContainerAttributeImpl) attribute, descriptor, mapper); } if (attribute.getCollectionType() == CollectionType.LIST) { - return createListPropertyStrategy(et, (ListAttributeImpl) attribute, descriptor, mapper); + return createListPropertyStrategy(et, (ListAttributeImpl) attribute, new ObjectGraphInfo(descriptor), mapper); } if (MultilingualString.class.equals(attribute.getElementType().getJavaType())) { return new PluralMultilingualStringFieldStrategy<>(et, @@ -141,32 +144,32 @@ abstract class FieldStrategy, X> { } private static FieldStrategy, Y> createPluralObjectPropertyStrategy( - EntityType et, AbstractPluralAttribute attribute, Descriptor descriptor, + EntityType et, AbstractPluralAttribute attribute, ObjectGraphInfo objectGraphInfo, EntityMappingHelper mapper) { if (attribute.isRdfContainer()) { - return new RdfContainerObjectPropertyStrategy<>(et, (RdfContainerAttributeImpl) attribute, descriptor, mapper); + return new RdfContainerObjectPropertyStrategy<>(et, (RdfContainerAttributeImpl) attribute, objectGraphInfo, mapper); } return switch (attribute.getCollectionType()) { - case LIST -> createListPropertyStrategy(et, (ListAttributeImpl) attribute, descriptor, + case LIST -> createListPropertyStrategy(et, (ListAttributeImpl) attribute, objectGraphInfo, mapper); - case COLLECTION, SET -> new SimpleSetPropertyStrategy<>(et, attribute, descriptor, mapper); + case COLLECTION, SET -> new SimpleSetPropertyStrategy<>(et, attribute, objectGraphInfo, mapper); default -> throw new UnsupportedOperationException( "Unsupported plural attribute collection type " + attribute.getCollectionType()); }; } private static FieldStrategy, Y> createListPropertyStrategy( - EntityType et, ListAttributeImpl attribute, Descriptor descriptor, + EntityType et, ListAttributeImpl attribute, ObjectGraphInfo objectGraphInfo, EntityMappingHelper mapper) { switch (attribute.getSequenceType()) { case referenced: if (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.OBJECT) { - return new ReferencedListPropertyStrategy<>(et, attribute, descriptor, mapper); + return new ReferencedListPropertyStrategy<>(et, attribute, objectGraphInfo, mapper); } else { - return new ReferencedListDataPropertyStrategy<>(et, attribute, descriptor, mapper); + return new ReferencedListDataPropertyStrategy<>(et, attribute, objectGraphInfo.descriptor(), mapper); } case simple: - return new SimpleListPropertyStrategy<>(et, attribute, descriptor, mapper); + return new SimpleListPropertyStrategy<>(et, attribute, objectGraphInfo, mapper); default: throw new UnsupportedOperationException( "Unsupported list attribute sequence type " + attribute.getSequenceType()); diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/ListPropertyStrategy.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/ListPropertyStrategy.java index 97ca5b1ea..c00a41582 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/ListPropertyStrategy.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/ListPropertyStrategy.java @@ -17,9 +17,9 @@ */ package cz.cvut.kbss.jopa.oom; -import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.jopa.model.metamodel.ListAttributeImpl; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.utils.EntityPropertiesUtils; import cz.cvut.kbss.jopa.utils.IdentifierTransformer; import cz.cvut.kbss.ontodriver.descriptor.ListDescriptor; @@ -34,9 +34,9 @@ abstract class ListPropertyStrategy extends PluralObjectPropertyStrategy, X> { - ListPropertyStrategy(EntityType et, ListAttributeImpl att, Descriptor descriptor, + ListPropertyStrategy(EntityType et, ListAttributeImpl att, ObjectGraphInfo objectGraphInfo, EntityMappingHelper mapper) { - super(et, att, descriptor, mapper); + super(et, att, objectGraphInfo, mapper); } @Override diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/ObjectOntologyMapperImpl.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/ObjectOntologyMapperImpl.java index 534f70809..dbefe25c0 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/ObjectOntologyMapperImpl.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/ObjectOntologyMapperImpl.java @@ -27,12 +27,14 @@ import cz.cvut.kbss.jopa.oom.exception.EntityDeconstructionException; import cz.cvut.kbss.jopa.oom.exception.EntityReconstructionException; import cz.cvut.kbss.jopa.oom.exception.UnpersistedChangeException; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.sessions.AbstractUnitOfWork; import cz.cvut.kbss.jopa.sessions.UnitOfWork; import cz.cvut.kbss.jopa.sessions.cache.CacheManager; import cz.cvut.kbss.jopa.sessions.cache.Descriptors; import cz.cvut.kbss.jopa.sessions.descriptor.LoadStateDescriptor; import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingParameters; +import cz.cvut.kbss.jopa.sessions.util.FetchGraphWrapper; import cz.cvut.kbss.jopa.sessions.util.LoadingParameters; import cz.cvut.kbss.jopa.utils.Configuration; import cz.cvut.kbss.jopa.utils.EntityPropertiesUtils; @@ -58,6 +60,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -75,14 +78,14 @@ public class ObjectOntologyMapperImpl implements ObjectOntologyMapper, EntityMap private final AxiomDescriptorFactory descriptorFactory; private final EntityConstructor entityBuilder; private final EntityDeconstructor entityBreaker; - private Map instanceRegistry; private final PendingReferenceRegistry pendingReferences; + private Map instanceRegistry; + private Map>> axiomsPerEntity = Map.of(); + private final EntityInstanceLoader defaultInstanceLoader; private final EntityInstanceLoader twoStepInstanceLoader; - private final EntityReferenceFactory referenceFactory; - public ObjectOntologyMapperImpl(AbstractUnitOfWork uow, Connection connection) { this.uow = Objects.requireNonNull(uow); this.storageConnection = Objects.requireNonNull(connection); @@ -102,7 +105,6 @@ public ObjectOntologyMapperImpl(AbstractUnitOfWork uow, Connection connection) { .descriptorFactory(descriptorFactory) .entityBuilder(entityBuilder).cache(getCache()) .loadStateRegistry(uow.getLoadStateRegistry()).build(); - this.referenceFactory = new EntityReferenceFactory(uow.getMetamodel(), uow); } private CacheManager getCache() { @@ -163,16 +165,26 @@ public T loadEntity(AxiomBasedLoadingParameters loadingParameters) { if (loadingParameters.axioms().isEmpty()) { return null; } - final URI identifier = loadingParameters.axioms().iterator().next().getSubject().getIdentifier(); - final IdentifiableEntityType et = getEntityType(loadingParameters.cls()); - final LoadingParameters params = new LoadingParameters<>(loadingParameters.cls(), identifier, loadingParameters.descriptor(), false, loadingParameters.bypassCache()); + this.axiomsPerEntity = new HashMap<>(); + loadingParameters.axioms().forEach(ax -> axiomsPerEntity.computeIfAbsent(ax.getSubject() + .getIdentifier(), k -> new HashSet<>()) + .add(ax)); + final LoadingParameters params = new LoadingParameters<>(loadingParameters.cls(), + loadingParameters.config().subject(), + loadingParameters.config().descriptor(), new FetchGraphWrapper(loadingParameters.config() + .fetchGraph()), false, loadingParameters.bypassCache()); + return loadEntityFromAxioms(params); + } + + private T loadEntityFromAxioms(LoadingParameters params) { + final IdentifiableEntityType et = getEntityType(params.entityClass()); final T result; if (et.hasSubtypes()) { - result = twoStepInstanceLoader.loadEntityFromAxioms(params, loadingParameters.axioms()); + result = twoStepInstanceLoader.loadEntityFromAxioms(params, axiomsPerEntity.get(params.identifier())); } else { - result = defaultInstanceLoader.loadEntityFromAxioms(params, loadingParameters.axioms()); + result = defaultInstanceLoader.loadEntityFromAxioms(params, axiomsPerEntity.get(params.identifier())); } - if (result != null) { + if (result != null && !params.fetchGraph().isPresent()) { cacheLoadedEntity(params, result); } return result; @@ -182,7 +194,7 @@ public T loadEntity(AxiomBasedLoadingParameters loadingParameters) { public T getReference(LoadingParameters loadingParameters) { assert loadingParameters != null; - return referenceFactory.createReferenceProxy(loadingParameters); + return new EntityReferenceFactory(uow.getMetamodel(), uow).createReferenceProxy(loadingParameters); } @Override @@ -190,11 +202,6 @@ public IdentifiableEntityType getEntityType(Class cls) { return uow.getMetamodel().entity(cls); } - @Override - public boolean isManagedType(Class cls) { - return uow.isEntityType(cls); - } - @Override public void loadFieldValue(T entity, FieldSpecification fieldSpec, Descriptor descriptor) { assert entity != null; @@ -281,12 +288,12 @@ private void persistPendingReferences(T instance, NamedResource identifier) } @Override - public T getEntityFromCacheOrOntology(Class cls, URI identifier, Descriptor descriptor) { - final T orig = uow.getManagedOriginal(cls, identifier, descriptor); + public T getEntityFromCacheOrOntology(Class cls, URI identifier, ObjectGraphInfo objectGraphInfo) { + final T orig = uow.getManagedOriginal(cls, identifier, objectGraphInfo.descriptor()); if (orig != null) { return orig; } - return defaultInstanceLoader.loadCached(getEntityType(cls), identifier, descriptor).orElseGet(() -> { + return defaultInstanceLoader.loadCached(getEntityType(cls), identifier, objectGraphInfo.descriptor()).orElseGet(() -> { if (instanceRegistry.containsKey(identifier)) { final Object existing = instanceRegistry.get(identifier); if (!cls.isAssignableFrom(existing.getClass())) { @@ -296,14 +303,14 @@ public T getEntityFromCacheOrOntology(Class cls, URI identifier, Descript return cls.cast(existing); } else { // setup loading params - LoadingParameters params = initEntityLoadingParameters(cls, identifier, descriptor); - return loadEntityInternal(params); + LoadingParameters params = initEntityLoadingParameters(cls, identifier, objectGraphInfo); + return axiomsPerEntity.containsKey(identifier) ? loadEntityFromAxioms(params) : loadEntityInternal(params); } }); } - private LoadingParameters initEntityLoadingParameters(Class cls, URI identifier, Descriptor descriptor) { - return new LoadingParameters<>(cls, identifier, descriptor, false, uow.isReadOnly()); + private LoadingParameters initEntityLoadingParameters(Class cls, URI identifier, ObjectGraphInfo objectGraphInfo) { + return new LoadingParameters<>(cls, identifier, objectGraphInfo.descriptor(), objectGraphInfo.fetchGraph(), false, uow.isReadOnly()); } @Override @@ -407,7 +414,8 @@ public Configuration getConfiguration() { public Set> getAttributeAxioms(T entity, FieldSpecification fieldSpec, Descriptor entityDescriptor) { final EntityType et = (EntityType) getEntityType(entity.getClass()); - return FieldStrategy.createFieldStrategy(et, fieldSpec, entityDescriptor, this).buildAxiomsFromInstance(entity); + return FieldStrategy.createFieldStrategy(et, fieldSpec, new ObjectGraphInfo(entityDescriptor), this) + .buildAxiomsFromInstance(entity); } @Override @@ -424,7 +432,7 @@ public boolean isInferred(Axiom axiom, URI context) { public boolean isInferred(T entity, FieldSpecification fieldSpec, Object value, Descriptor entityDescriptor) { final EntityType et = (EntityType) getEntityType(entity.getClass()); - final FieldStrategy fs = FieldStrategy.createFieldStrategy(et, fieldSpec, entityDescriptor, this); + final FieldStrategy fs = FieldStrategy.createFieldStrategy(et, fieldSpec, new ObjectGraphInfo(entityDescriptor), this); final Collection> values = fs.toAxiomValue(value); final Set contexts = entityDescriptor.getAttributeContexts(fieldSpec); final NamedResource subject = NamedResource.create(EntityPropertiesUtils.getIdentifier(entity, et)); diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/PluralObjectPropertyStrategy.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/PluralObjectPropertyStrategy.java index eafad92b5..71709ee70 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/PluralObjectPropertyStrategy.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/PluralObjectPropertyStrategy.java @@ -20,6 +20,8 @@ import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.metamodel.AbstractPluralAttribute; import cz.cvut.kbss.jopa.model.metamodel.EntityType; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; +import cz.cvut.kbss.jopa.sessions.util.FetchGraphWrapper; import cz.cvut.kbss.jopa.utils.CollectionFactory; import cz.cvut.kbss.jopa.utils.EntityPropertiesUtils; import cz.cvut.kbss.jopa.utils.IdentifierTransformer; @@ -44,12 +46,15 @@ abstract class PluralObjectPropertyStrategy values; private boolean hasLazyValues = false; - PluralObjectPropertyStrategy(EntityType et, Y att, Descriptor descriptor, EntityMappingHelper mapper) { - super(et, att, descriptor, mapper); + PluralObjectPropertyStrategy(EntityType et, Y att, ObjectGraphInfo objectGraphInfo, EntityMappingHelper mapper) { + super(et, att, objectGraphInfo.descriptor(), mapper); + this.fetchGraph = objectGraphInfo.fetchGraph(); this.values = CollectionFactory.createDefaultCollection(att.getCollectionType()); } @@ -66,7 +71,7 @@ void addAxiomValue(Axiom ax) { // with the 'else' branch behavior? values.add(attribute.getConverter().convertToAttribute(valueIdentifier)); } else { - final Object value = mapper.getEntityFromCacheOrOntology(elementType, valueIdentifier.getIdentifier(), getDescriptorForRetrieval()); + final Object value = mapper.getEntityFromCacheOrOntology(elementType, valueIdentifier.getIdentifier(), propagateObjectGraphInfo()); if (value != null) { values.add(value); } else { @@ -80,6 +85,10 @@ private Descriptor getDescriptorForRetrieval() { return entityDescriptor.getAttributeDescriptor(attribute).unwrap(); } + private ObjectGraphInfo propagateObjectGraphInfo() { + return new ObjectGraphInfo(getDescriptorForRetrieval(), fetchGraph.getAttributeSubgraph(attribute)); + } + @Override void lazilyAddAxiomValue(Axiom ax) { final Class elementType = attribute.getBindableJavaType(); diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/RdfContainerObjectPropertyStrategy.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/RdfContainerObjectPropertyStrategy.java index 96e02cb37..6e67d6652 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/RdfContainerObjectPropertyStrategy.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/RdfContainerObjectPropertyStrategy.java @@ -18,9 +18,9 @@ package cz.cvut.kbss.jopa.oom; import cz.cvut.kbss.jopa.model.annotations.RDFContainerType; -import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.jopa.model.metamodel.RdfContainerAttributeImpl; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.utils.EntityPropertiesUtils; import cz.cvut.kbss.jopa.utils.IdentifierTransformer; import cz.cvut.kbss.ontodriver.descriptor.ContainerDescriptor; @@ -39,8 +39,8 @@ class RdfContainerObjectPropertyStrategy extends PluralObjectPropertyStrategy private final RDFContainerType containerType; RdfContainerObjectPropertyStrategy(EntityType et, RdfContainerAttributeImpl att, - Descriptor descriptor, EntityMappingHelper mapper) { - super(et, att, descriptor, mapper); + ObjectGraphInfo objectGraphInfo, EntityMappingHelper mapper) { + super(et, att, objectGraphInfo, mapper); this.containerType = att.getContainerType(); } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/ReferencedListPropertyStrategy.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/ReferencedListPropertyStrategy.java index 4759c36f2..8637f7237 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/ReferencedListPropertyStrategy.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/ReferencedListPropertyStrategy.java @@ -17,9 +17,9 @@ */ package cz.cvut.kbss.jopa.oom; -import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.jopa.model.metamodel.ListAttributeImpl; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.utils.EntityPropertiesUtils; import cz.cvut.kbss.ontodriver.descriptor.ReferencedListDescriptor; import cz.cvut.kbss.ontodriver.descriptor.ReferencedListValueDescriptor; @@ -32,9 +32,9 @@ class ReferencedListPropertyStrategy extends ListPropertyStrategy, X> { - ReferencedListPropertyStrategy(EntityType et, ListAttributeImpl att, Descriptor descriptor, + ReferencedListPropertyStrategy(EntityType et, ListAttributeImpl att, ObjectGraphInfo objectGraphInfo, EntityMappingHelper mapper) { - super(et, att, descriptor, mapper); + super(et, att, objectGraphInfo, mapper); } @Override diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/SimpleListPropertyStrategy.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/SimpleListPropertyStrategy.java index 99697d0d2..a7b2ee824 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/SimpleListPropertyStrategy.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/SimpleListPropertyStrategy.java @@ -17,9 +17,9 @@ */ package cz.cvut.kbss.jopa.oom; -import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.jopa.model.metamodel.ListAttributeImpl; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.ontodriver.descriptor.SimpleListDescriptor; import cz.cvut.kbss.ontodriver.descriptor.SimpleListValueDescriptor; import cz.cvut.kbss.ontodriver.model.Axiom; @@ -31,9 +31,9 @@ class SimpleListPropertyStrategy extends ListPropertyStrategy { - SimpleListPropertyStrategy(EntityType et, ListAttributeImpl att, Descriptor descriptor, + SimpleListPropertyStrategy(EntityType et, ListAttributeImpl att, ObjectGraphInfo objectGraphInfo, EntityMappingHelper mapper) { - super(et, att, descriptor, mapper); + super(et, att, objectGraphInfo, mapper); } @Override diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/SimpleSetPropertyStrategy.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/SimpleSetPropertyStrategy.java index db54da028..9fe244d04 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/SimpleSetPropertyStrategy.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/SimpleSetPropertyStrategy.java @@ -17,9 +17,9 @@ */ package cz.cvut.kbss.jopa.oom; -import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.metamodel.AbstractPluralAttribute; import cz.cvut.kbss.jopa.model.metamodel.EntityType; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.utils.EntityPropertiesUtils; import cz.cvut.kbss.jopa.utils.IdentifierTransformer; import cz.cvut.kbss.ontodriver.model.NamedResource; @@ -33,9 +33,9 @@ class SimpleSetPropertyStrategy extends PluralObjectPropertyStrategy, X> { - SimpleSetPropertyStrategy(EntityType et, AbstractPluralAttribute att, Descriptor descriptor, - EntityMappingHelper mapper) { - super(et, att, descriptor, mapper); + SimpleSetPropertyStrategy(EntityType et, AbstractPluralAttribute att, + ObjectGraphInfo objectGraphInfo, EntityMappingHelper mapper) { + super(et, att, objectGraphInfo, mapper); } @Override diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/SingularObjectPropertyStrategy.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/SingularObjectPropertyStrategy.java index 3246b47b0..e89e8f636 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/SingularObjectPropertyStrategy.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/SingularObjectPropertyStrategy.java @@ -18,12 +18,17 @@ package cz.cvut.kbss.jopa.oom; import cz.cvut.kbss.jopa.exceptions.CardinalityConstraintViolatedException; -import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.metamodel.AbstractAttribute; import cz.cvut.kbss.jopa.model.metamodel.EntityType; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; +import cz.cvut.kbss.jopa.sessions.util.FetchGraphWrapper; import cz.cvut.kbss.jopa.utils.EntityPropertiesUtils; import cz.cvut.kbss.jopa.utils.IdentifierTransformer; -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.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,11 +41,14 @@ class SingularObjectPropertyStrategy extends FieldStrategy et, AbstractAttribute att, - Descriptor descriptor, EntityMappingHelper mapper) { - super(et, att, descriptor, mapper); + ObjectGraphInfo objectGraphInfo, EntityMappingHelper mapper) { + super(et, att, objectGraphInfo.descriptor(), mapper); + this.fetchGraph = objectGraphInfo.fetchGraph(); } @Override @@ -57,8 +65,7 @@ void addAxiomValue(Axiom ax) { // with the 'else' branch behavior? newValue = attribute.getConverter().convertToAttribute(valueIdentifier); } else { - newValue = mapper.getEntityFromCacheOrOntology(targetType, valueIdentifier.getIdentifier(), - entityDescriptor.getAttributeDescriptor(attribute)); + newValue = mapper.getEntityFromCacheOrOntology(targetType, valueIdentifier.getIdentifier(), propagateGraphInfo()); if (newValue == null) { LOG.trace("Value of axiom {} could not be loaded as entity filling attribute {}.", ax, attribute); return; @@ -68,6 +75,10 @@ void addAxiomValue(Axiom ax) { this.value = newValue; } + private ObjectGraphInfo propagateGraphInfo() { + return new ObjectGraphInfo(entityDescriptor.getAttributeDescriptor(attribute), fetchGraph.getAttributeSubgraph(attribute)); + } + private void verifyCardinality(NamedResource subject) { if (value != null) { throw new CardinalityConstraintViolatedException( diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/util/ObjectGraphInfo.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/util/ObjectGraphInfo.java new file mode 100644 index 000000000..476aad0e3 --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/util/ObjectGraphInfo.java @@ -0,0 +1,11 @@ +package cz.cvut.kbss.jopa.oom.util; + +import cz.cvut.kbss.jopa.model.descriptors.Descriptor; +import cz.cvut.kbss.jopa.sessions.util.FetchGraphWrapper; + +public record ObjectGraphInfo(Descriptor descriptor, FetchGraphWrapper fetchGraph) { + + public ObjectGraphInfo(Descriptor descriptor) { + this(descriptor, new FetchGraphWrapper()); + } +} diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/proxy/lazy/gen/LazyLoadingEntityProxyGenerator.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/proxy/lazy/gen/LazyLoadingEntityProxyGenerator.java index d952cafbf..c5f8bbe9e 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/proxy/lazy/gen/LazyLoadingEntityProxyGenerator.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/proxy/lazy/gen/LazyLoadingEntityProxyGenerator.java @@ -18,6 +18,7 @@ package cz.cvut.kbss.jopa.proxy.lazy.gen; import cz.cvut.kbss.jopa.exception.LazyLoadingException; +import cz.cvut.kbss.jopa.model.LoadState; import cz.cvut.kbss.jopa.model.metamodel.FieldSpecification; import cz.cvut.kbss.jopa.model.metamodel.gen.PersistenceContextAwareClassGenerator; import cz.cvut.kbss.jopa.model.metamodel.gen.PersistentPropertySetterMatcher; @@ -155,7 +156,7 @@ private ProxyMethodsInterceptor() { } public static boolean isLoaded(@This LazyLoadingEntityProxy proxy, @FieldValue("value") T value) { - return value != null; + return value != null || proxy.getPersistenceContext().isLoaded(proxy, proxy.getFieldSpec()) == LoadState.LOADED; } public static T getLoadedValue(@This LazyLoadingEntityProxy proxy, @FieldValue("value") T value) { diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/QueryHints.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/QueryHints.java index 4f59dfdd9..250b0ae6f 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/QueryHints.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/QueryHints.java @@ -50,6 +50,14 @@ public class QueryHints { */ public static final String ENABLE_ENTITY_LOADING_OPTIMIZER = "cz.cvut.kbss.jopa.query.enableEntityLoadingOptimizer"; + /** + * Allows specifying the graph to be used for fetching the entities. + *

+ * A {@literal fetch graph} specifies the attributes that the query should load. All other attributes are loaded + * lazily. + */ + public static final String FETCH_GRAPH = "cz.cvut.kbss.jopa.query.fetchGraph"; + private QueryHints() { throw new AssertionError(); } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/QueryAttributes.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/QueryAttributes.java index 92fb1b728..adf10a15b 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/QueryAttributes.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/QueryAttributes.java @@ -20,5 +20,5 @@ import cz.cvut.kbss.jopa.query.QueryType; import org.antlr.v4.runtime.Token; -public record QueryAttributes(QueryType queryType, boolean hasOffset, boolean hasLimit, boolean hasGraphOrService, Token lastClosingCurlyBraceToken) { +public record QueryAttributes(QueryType queryType, boolean hasOffset, boolean hasLimit, boolean hasOrderBy, boolean hasGraphOrService, Token lastClosingCurlyBraceToken) { } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/Sparql11QueryListener.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/Sparql11QueryListener.java index b31d4c38e..1fd7c7cba 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/Sparql11QueryListener.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/Sparql11QueryListener.java @@ -35,6 +35,7 @@ public class Sparql11QueryListener extends SparqlParserBaseListener { private boolean inProjection; private boolean hasLimit; private boolean hasOffset; + private boolean hasOrderBy; private boolean hasGraphOrService; private int depth = 0; @@ -112,6 +113,11 @@ public void enterOffsetClause(SparqlParser.OffsetClauseContext ctx) { this.hasOffset = true; } + @Override + public void enterOrderClause(SparqlParser.OrderClauseContext ctx) { + this.hasOrderBy = true; + } + @Override public void enterGraphGraphPattern(SparqlParser.GraphGraphPatternContext ctx) { this.hasGraphOrService = true; @@ -156,7 +162,7 @@ public void enterVar(SparqlParser.VarContext ctx) { } QueryAttributes getQueryAttributes() { - return new QueryAttributes(queryType, hasOffset, hasLimit, hasGraphOrService, lastClosingCurlyBrace); + return new QueryAttributes(queryType, hasOffset, hasLimit, hasOrderBy, hasGraphOrService, lastClosingCurlyBrace); } List> getParameters() { diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/SparqlQueryFactory.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/SparqlQueryFactory.java index cac977ab4..1d7a74f2b 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/SparqlQueryFactory.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/SparqlQueryFactory.java @@ -24,7 +24,6 @@ import cz.cvut.kbss.jopa.query.mapper.SparqlResultMapper; import cz.cvut.kbss.jopa.query.parameter.ParameterValueFactory; import cz.cvut.kbss.jopa.query.soql.SoqlQueryParser; -import cz.cvut.kbss.jopa.query.sparql.loader.QueryResultLoadingOptimizer; import cz.cvut.kbss.jopa.query.sparql.loader.SparqlQueryResultLoadingOptimizer; import cz.cvut.kbss.jopa.sessions.ConnectionWrapper; import cz.cvut.kbss.jopa.sessions.UnitOfWork; @@ -81,8 +80,7 @@ public TypedQueryImpl createNativeQuery(String sparql, Class resultCla private TypedQueryImpl createQueryImpl(String query, Class resultClass, QueryParser parser) { Objects.requireNonNull(resultClass); final TokenStreamSparqlQueryHolder queryHolder = (TokenStreamSparqlQueryHolder) parser.parseQuery(query, resultClass); - final QueryResultLoadingOptimizer - queryResultLoadingOptimizer = new SparqlQueryResultLoadingOptimizer(queryHolder, uow, connection); + final SparqlQueryResultLoadingOptimizer queryResultLoadingOptimizer = new SparqlQueryResultLoadingOptimizer(queryHolder, uow, connection); return new TypedQueryImpl<>(queryHolder, resultClass, connection, queryResultLoadingOptimizer); } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/AttributeBasedRowsToAxiomsQueryResultLoader.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/AttributeBasedRowsToAxiomsQueryResultLoader.java index 30537411f..e59746226 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/AttributeBasedRowsToAxiomsQueryResultLoader.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/AttributeBasedRowsToAxiomsQueryResultLoader.java @@ -19,13 +19,16 @@ import cz.cvut.kbss.jopa.exceptions.CardinalityConstraintViolatedException; import cz.cvut.kbss.jopa.exceptions.OWLPersistenceException; +import cz.cvut.kbss.jopa.model.EntityGraph; import cz.cvut.kbss.jopa.model.QueryResultLoader; import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.metamodel.Attribute; -import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.jopa.model.metamodel.FieldSpecification; +import cz.cvut.kbss.jopa.model.metamodel.IdentifiableEntityType; import cz.cvut.kbss.jopa.model.metamodel.TypesSpecification; import cz.cvut.kbss.jopa.sessions.UnitOfWork; +import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingConfigGroup; +import cz.cvut.kbss.ontodriver.ResultSet; import cz.cvut.kbss.ontodriver.exception.OntoDriverException; import cz.cvut.kbss.ontodriver.iteration.ResultRow; import cz.cvut.kbss.ontodriver.model.Assertion; @@ -37,9 +40,16 @@ import org.slf4j.LoggerFactory; import java.net.URI; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Loads query results as axioms and then uses {@link cz.cvut.kbss.jopa.sessions.UnitOfWork} to create the actual entity @@ -51,7 +61,7 @@ *

    *
  • Query result contains at least one column
  • *
  • The first column contains the subject URI
  • - *
  • The remaining columns contain attribute values. Each column is named as {@code subjectName + attributeName}
  • + *
  • The remaining columns contain attribute values. Each column is named as {@code subjectName + "_" + attributeName}
  • *
*

* If the loader is unable to load the entity (e.g., due to cardinality constraint violation), it falls back to @@ -66,16 +76,69 @@ class AttributeBasedRowsToAxiomsQueryResultLoader implements QueryResultLoade private final UnitOfWork uow; private final Class resultType; private final Descriptor descriptor; - private final EntityType entityType; + private final IdentifiableEntityType entityType; + private final EntityGraph fetchGraph; + private final Supplier> mappingSupplier; + + private List mappings; private Set> currentEntityAxioms = Set.of(); private NamedResource currentSubject; - AttributeBasedRowsToAxiomsQueryResultLoader(UnitOfWork uow, Class resultType, Descriptor descriptor) { + private boolean typesProjected = true; + + AttributeBasedRowsToAxiomsQueryResultLoader(UnitOfWork uow, Class resultType, Descriptor descriptor, + EntityGraph fetchGraph) { + this(uow, resultType, descriptor, fetchGraph, () -> null); + } + + AttributeBasedRowsToAxiomsQueryResultLoader(UnitOfWork uow, Class resultType, Descriptor descriptor, + EntityGraph fetchGraph, + Supplier> mappingSupplier) { this.uow = uow; this.resultType = resultType; this.descriptor = descriptor; + assert mappingSupplier != null; + this.mappingSupplier = mappingSupplier; this.entityType = uow.getMetamodel().entity(resultType); + this.fetchGraph = fetchGraph; + } + + @Override + public void init(ResultSet resultSet) { + this.mappings = mappingSupplier.get(); + } + + private void createProjectionMappings(List projectedVars) { + if (mappings == null) { + this.mappings = mappingSupplier.get(); + } + if (mappings != null) { + return; + } + this.typesProjected = false; + assert !projectedVars.isEmpty(); + final String subjectVar = projectedVars.get(0); + this.mappings = new ArrayList<>(projectedVars.size() - 1); + final Map> atts = FetchGraphProcessor.attributes(entityType).stream() + .collect(Collectors.toMap(Attribute::getName, Function.identity())); + for (int i = 1; i < projectedVars.size(); i++) { + final String projectedVar = projectedVars.get(i); + if (atts.containsKey(projectedVar)) { + // ?stringAttribute + mappings.add(new QueryVariableMapping(subjectVar, projectedVar, atts.get(projectedVar))); + } else if (atts.containsKey(projectedVar.substring(subjectVar.length() + 1))) { + // ?x_stringAttribute + mappings.add(new QueryVariableMapping(subjectVar, projectedVar, atts.get(projectedVar.substring(subjectVar.length() + 1)))); + } else if (projectedVar.endsWith(AttributeEnumeratingSparqlAssemblyModifier.TYPES_VAR_NAME) || (entityType.getTypes() != null && projectedVar.endsWith(entityType.getTypes() + .getName()))) { + // ?x_types or ?types + mappings.add(new QueryVariableMapping(subjectVar, projectedVar, entityType.getTypes())); + this.typesProjected = true; + } else { + LOG.warn("Variable '{}' projected from the query cannot be mapped to any attributes in entity class {}.", projectedVar, entityType); + } + } } @Override @@ -85,6 +148,7 @@ public Optional loadResult(ResultRow resultRow) { final URI subject = resultRow.getObject(0, URI.class); if (currentSubject == null) { reset(NamedResource.create(subject)); + createProjectionMappings(resultRow.getColumnNames()); } if (subject.equals(currentSubject.getIdentifier())) { rowToAxioms(resultRow); @@ -107,42 +171,53 @@ private void reset(NamedResource newSubject) { } private void rowToAxioms(ResultRow row) throws OntoDriverException { - final VariableNameMapper varNameMapper = createVarNameMapper(row); - for (Attribute attribute : entityType.getAttributes()) { - final String varName = varNameMapper.getAttributeVarName(attribute); - if (row.isBound(varName)) { - currentEntityAxioms.add(new AxiomImpl<>(currentSubject, attributeToAssertion(attribute), new Value<>(row.getObject(varName)))); + for (QueryVariableMapping mapping : mappings) { + if (row.isBound(mapping.attributeVar())) { + assert row.isBound(mapping.subjectVar()); + final NamedResource subject = NamedResource.create(row.getObject(mapping.subjectVar(), URI.class)); + final Assertion assertion = attributeToAssertion(mapping.attribute()); + extractValue(row, mapping).map(v -> new AxiomImpl<>(subject, assertion, v)) + .forEach(currentEntityAxioms::add); } } - final Optional> typesSpec = Optional.ofNullable(entityType.getTypes()); - final String varName = varNameMapper.getTypesVarName(typesSpec.orElse(null)); - if (row.isBound(varName)) { - currentEntityAxioms.add(new AxiomImpl<>(currentSubject, - Assertion.createClassAssertion(typesSpec.map(FieldSpecification::isInferred).orElse(false)), - new Value<>(row.getObject(varName)))); - } } - private VariableNameMapper createVarNameMapper(ResultRow row) { - assert !row.getColumnNames().isEmpty(); - final String subjectVarName = row.getColumnNames().get(0); - final boolean optimized = row.getColumnNames().stream() - .allMatch(name -> name.startsWith(subjectVarName)); - return optimized ? new OptimizedQueryVariableNameMapper(subjectVarName) : new DefaultVariableNameMapper(); + private static Assertion attributeToAssertion(FieldSpecification fs) { + if (fs != null && fs.isMappedAttribute()) { + final Attribute attribute = (Attribute) fs; + return switch (attribute.getPersistentAttributeType()) { + case OBJECT -> + Assertion.createObjectPropertyAssertion(attribute.getIRI().toURI(), attribute.isInferred()); + case DATA -> Assertion.createDataPropertyAssertion(attribute.getIRI().toURI(), attribute.isInferred()); + case ANNOTATION -> + Assertion.createAnnotationPropertyAssertion(attribute.getIRI().toURI(), attribute.isInferred()); + }; + } else { + if (fs != null) { + assert fs instanceof TypesSpecification; + return Assertion.createClassAssertion(fs.isInferred()); + } else { + return Assertion.createClassAssertion(false); + } + } } - private static Assertion attributeToAssertion(Attribute attribute) { - return switch (attribute.getPersistentAttributeType()) { - case OBJECT -> Assertion.createObjectPropertyAssertion(attribute.getIRI().toURI(), attribute.isInferred()); - case DATA -> Assertion.createDataPropertyAssertion(attribute.getIRI().toURI(), attribute.isInferred()); - case ANNOTATION -> - Assertion.createAnnotationPropertyAssertion(attribute.getIRI().toURI(), attribute.isInferred()); - }; + private Stream> extractValue(ResultRow row, + QueryVariableMapping variableMapping) throws OntoDriverException { + if (variableMapping.canGroupConcat()) { + return variableMapping.readGroupConcatValue(row); + } else { + return Stream.of(new Value<>(row.getObject(variableMapping.attributeVar()))); + } } private T loadEntity() { + if (!typesProjected) { + currentEntityAxioms.add(new AxiomImpl<>(currentSubject, + Assertion.createClassAssertion(false), new Value<>(entityType.getIRI().toURI()))); + } try { - return uow.readObjectFromAxioms(resultType, currentEntityAxioms, descriptor); + return uow.readObjectFromAxioms(resultType, currentEntityAxioms, new AxiomBasedLoadingConfigGroup<>(currentSubject.getIdentifier(), descriptor, fetchGraph)); } catch (CardinalityConstraintViolatedException e) { // Axioms may contain more statements than expected due to query evaluation containing inferred results. // If the entity class declares ICs on non-inferred attributes, this may lead to IC violation exception. @@ -161,33 +236,4 @@ public Optional loadLastPending() { } return Optional.empty(); } - - private interface VariableNameMapper { - String getAttributeVarName(Attribute attribute); - String getTypesVarName(TypesSpecification types); - } - - private record OptimizedQueryVariableNameMapper(String subjectVarName) implements VariableNameMapper { - @Override - public String getAttributeVarName(Attribute attribute) { - return subjectVarName + attribute.getName(); - } - - @Override - public String getTypesVarName(TypesSpecification types) { - return subjectVarName + AttributeEnumeratingSparqlAssemblyModifier.TYPES_VAR_SUFFIX; - } - } - - private static class DefaultVariableNameMapper implements VariableNameMapper { - @Override - public String getAttributeVarName(Attribute attribute) { - return attribute.getName(); - } - - @Override - public String getTypesVarName(TypesSpecification types) { - return types != null ? types.getName() : AttributeEnumeratingSparqlAssemblyModifier.TYPES_VAR_SUFFIX; - } - } } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/AttributeEnumeratingSparqlAssemblyModifier.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/AttributeEnumeratingSparqlAssemblyModifier.java index 1011b6467..562aca66a 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/AttributeEnumeratingSparqlAssemblyModifier.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/AttributeEnumeratingSparqlAssemblyModifier.java @@ -17,13 +17,10 @@ */ package cz.cvut.kbss.jopa.query.sparql.loader; -import cz.cvut.kbss.jopa.model.annotations.ParticipationConstraint; +import cz.cvut.kbss.jopa.model.EntityGraph; +import cz.cvut.kbss.jopa.model.MetamodelImpl; import cz.cvut.kbss.jopa.model.descriptors.Descriptor; -import cz.cvut.kbss.jopa.model.metamodel.AbstractIdentifiableType; -import cz.cvut.kbss.jopa.model.metamodel.Attribute; -import cz.cvut.kbss.jopa.model.metamodel.FieldSpecification; import cz.cvut.kbss.jopa.model.metamodel.IdentifiableEntityType; -import cz.cvut.kbss.jopa.model.metamodel.TypesSpecification; import cz.cvut.kbss.jopa.query.QueryType; import cz.cvut.kbss.jopa.query.sparql.QueryAttributes; import cz.cvut.kbss.jopa.query.sparql.TokenQueryParameter; @@ -31,14 +28,7 @@ import cz.cvut.kbss.jopa.sessions.ConnectionWrapper; import org.antlr.v4.runtime.TokenStreamRewriter; -import java.net.URI; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Comparator; import java.util.List; -import java.util.Objects; -import java.util.Optional; /** * Optimizes entity loading by modifying the query to fetch all named attributes. @@ -67,26 +57,40 @@ */ public class AttributeEnumeratingSparqlAssemblyModifier implements SparqlAssemblyModifier { - static final String TYPES_VAR_SUFFIX = "types"; + static final String TYPES_VAR_NAME = "types"; + static final String GROUP_CONCAT_SEPARATOR = "\u001F"; + + private final MetamodelImpl metamodel; private final IdentifiableEntityType resultType; private final Descriptor descriptor; + private final EntityGraph fetchGraph; + private final boolean inferredAttsInDefault; - public AttributeEnumeratingSparqlAssemblyModifier(IdentifiableEntityType resultType, Descriptor descriptor, + private List variableMapping; + + public AttributeEnumeratingSparqlAssemblyModifier(MetamodelImpl metamodel, IdentifiableEntityType resultType, + Descriptor descriptor, EntityGraph fetchGraph, ConnectionWrapper connection) { + this.metamodel = metamodel; this.resultType = resultType; this.descriptor = descriptor; + this.fetchGraph = fetchGraph != null ? fetchGraph : generateDefaultFetchGraph(); this.inferredAttsInDefault = resolveInferenceContext(connection); - assert resultType.getProperties() == null; + assert fetchGraph != null || resultType.getProperties() == null; } - private boolean resolveInferenceContext(ConnectionWrapper connection) { + private static boolean resolveInferenceContext(ConnectionWrapper connection) { return "GraphDB".equals(connection.getRepositoryMetadata().getProductName()); } + private EntityGraph generateDefaultFetchGraph() { + return new FetchGraphProcessor(metamodel).generateDefaultFetchGraph(resultType); + } + @Override public void modify(TokenStreamSparqlQueryHolder queryHolder, TokenStreamRewriter tokenRewriter, QueryAttributes queryAttributes) { @@ -94,70 +98,40 @@ public void modify(TokenStreamSparqlQueryHolder queryHolder, TokenStreamRewriter assert queryHolder.getProjectedQueryParameters().size() == 1; final TokenQueryParameter p = queryHolder.getProjectedQueryParameters().get(0); - final List variablesToProject = addAttributeSelection(queryHolder, tokenRewriter, queryAttributes); - tokenRewriter.insertAfter(p.getSingleToken(), " " + String.join(" ", variablesToProject)); + final String subjectParamName = UnboundPredicateObjectSparqlAssemblyModifier.getBaseParamName(p); + final EntityMappingQueryModifier queryModifier = new EntityMappingQueryModifier(metamodel, resultType, descriptor, inferredAttsInDefault); + final EntityMappingQueryModifier.QueryModification mod = queryModifier.modify(fetchGraph, subjectParamName); + this.variableMapping = mod.variables(); + tokenRewriter.insertBefore(queryAttributes.lastClosingCurlyBraceToken(), mod.queryPart()); + modifyProjection(p, tokenRewriter, queryAttributes, mod); } - private List addAttributeSelection(TokenStreamSparqlQueryHolder queryHolder, - TokenStreamRewriter tokenRewriter, - QueryAttributes queryAttributes) { - final StringBuilder attributePatterns = new StringBuilder(); - final List variables = new ArrayList<>(); - final String subjectParamName = UnboundPredicateObjectSparqlAssemblyModifier.getBaseParamName(queryHolder.getProjectedQueryParameters() - .get(0)); - final String subjectVariable = "?" + subjectParamName; - attributes().forEach(att -> { - final int min = Arrays.stream(att.getConstraints()).map(ParticipationConstraint::min) - .min(Comparator.naturalOrder()).orElse(0); - final String variable = "?" + subjectParamName + att.getName(); - variables.add(variable); - if (min < 1) { - attributePatterns.append("OPTIONAL { "); - } - final Optional ctx = context(att); - ctx.ifPresent(uri -> attributePatterns.append("GRAPH <").append(uri).append("> { ")); - attributePatterns.append(subjectVariable).append(" <").append(att.getIRI()) - .append("> ").append(variable).append(" . "); - if (min < 1) { - attributePatterns.append("} "); + private void modifyProjection(TokenQueryParameter firstProjected, TokenStreamRewriter tokenRewriter, + QueryAttributes queryAttributes, + EntityMappingQueryModifier.QueryModification queryMod) { + final StringBuilder projection = new StringBuilder(); + final StringBuilder groupBy = new StringBuilder(" GROUP BY " + firstProjected.getIdentifierAsQueryString()); + boolean hasGroupConcat = false; + for (QueryVariableMapping varMapping : queryMod.variables()) { + // PERF: GROUP_CONCAT may prevent combinatorial blowup of the number of rows + if (!queryAttributes.hasOrderBy() && varMapping.canGroupConcat()) { + projection.append(' ').append(varMapping.generateGroupConcat()); + hasGroupConcat = true; + } else { + final String variable = "?" + varMapping.attributeVar(); + projection.append(' ').append(variable); + groupBy.append(' ').append(variable); } - ctx.ifPresent(uri -> attributePatterns.append("} ")); - }); - final String variable = "?" + subjectParamName + TYPES_VAR_SUFFIX; - variables.add(variable); - final Optional ctx = typesContext(); - ctx.ifPresent(uri -> attributePatterns.append("GRAPH <").append(uri).append("> { ")); - attributePatterns.append(subjectVariable).append(" a ").append(variable).append(" . "); - ctx.ifPresent(uri -> attributePatterns.append("} ")); - tokenRewriter.insertBefore(queryAttributes.lastClosingCurlyBraceToken(), attributePatterns.toString()); - return variables; - } - - private Collection> attributes() { - final List> atts = new ArrayList<>(resultType.getAttributes()); - resultType.getSubtypes().stream() - .flatMap(subtype -> subtype.getAttributes().stream()) - .forEach(atts::add); - return atts; - } - - private Optional context(FieldSpecification att) { - assert descriptor.getAttributeContexts(att).size() <= 1; - if (att.isInferred() && inferredAttsInDefault) { - return Optional.empty(); } - return descriptor.getSingleAttributeContext(att).map(URI::toString); + tokenRewriter.insertAfter(firstProjected.getSingleToken(), projection.toString()); + if (hasGroupConcat) { + tokenRewriter.insertAfter(queryAttributes.lastClosingCurlyBraceToken(), groupBy.toString()); + // Add ordering to ensure stable results with rows with the same subject in sequence + tokenRewriter.insertAfter(queryAttributes.lastClosingCurlyBraceToken(), " ORDER BY " + firstProjected.getIdentifierAsQueryString()); + } } - private Optional typesContext() { - final Optional> optionalTs = resultType.getTypes() != null ? Optional.of(resultType.getTypes()) : resultType.getSubtypes() - .stream() - .map(AbstractIdentifiableType::getTypes) - .filter(Objects::nonNull) - .findFirst(); - return optionalTs.flatMap(ts -> { - assert descriptor.getAttributeContexts(ts).size() <= 1; - return descriptor.getSingleAttributeContext(ts); - }).map(URI::toString); + public List getVariableMapping() { + return variableMapping; } } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/EntityMappingQueryModifier.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/EntityMappingQueryModifier.java new file mode 100644 index 000000000..9034e2fe9 --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/EntityMappingQueryModifier.java @@ -0,0 +1,181 @@ +package cz.cvut.kbss.jopa.query.sparql.loader; + +import cz.cvut.kbss.jopa.model.AttributeNode; +import cz.cvut.kbss.jopa.model.AttributeNodeImpl; +import cz.cvut.kbss.jopa.model.EntityGraph; +import cz.cvut.kbss.jopa.model.MetamodelImpl; +import cz.cvut.kbss.jopa.model.Subgraph; +import cz.cvut.kbss.jopa.model.annotations.ParticipationConstraint; +import cz.cvut.kbss.jopa.model.descriptors.Descriptor; +import cz.cvut.kbss.jopa.model.metamodel.AbstractIdentifiableType; +import cz.cvut.kbss.jopa.model.metamodel.Attribute; +import cz.cvut.kbss.jopa.model.metamodel.FieldSpecification; +import cz.cvut.kbss.jopa.model.metamodel.IdentifiableEntityType; +import cz.cvut.kbss.jopa.model.metamodel.TypesSpecification; +import cz.cvut.kbss.jopa.utils.IdentifierTransformer; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static cz.cvut.kbss.jopa.query.sparql.loader.AttributeEnumeratingSparqlAssemblyModifier.TYPES_VAR_NAME; + +/** + * Creates a basic graph pattern corresponding to the specified entity fetch graph. + */ +class EntityMappingQueryModifier { + + private final MetamodelImpl metamodel; + private final IdentifiableEntityType resultType; + + private final Descriptor descriptor; + + private final boolean inferredAttsInDefault; + + EntityMappingQueryModifier(MetamodelImpl metamodel, IdentifiableEntityType resultType, Descriptor descriptor, + boolean inferredAttsInDefault) { + this.metamodel = metamodel; + this.resultType = resultType; + this.descriptor = descriptor; + this.inferredAttsInDefault = inferredAttsInDefault; + } + + /** + * Creates a query modification based on the specified fetch graph. + *

+ * That is only attributes specified in the graph are included in the query. + * + * @param graph Graph describing what to fetch + * @param subjectVar Name of the attributeVar representing the subject of the graph patterns. Will be used to + * generate the projected attributeVar names + * @return Query modification + */ + QueryModification modify(EntityGraph graph, String subjectVar) { + return modifyImpl(graph.getAttributeNodes(), subjectVar); + } + + /** + * Creates a query modification based on the specified fetch subgraph. + *

+ * That is only attributes specified in the subgraph are included in the query. + * + * @param graph Graph describing what to fetch + * @param subjectVar Name of the attributeVar representing the subject of the graph patterns. Will be used to + * generate the projected attributeVar names + * @return Query modification + */ + QueryModification modify(Subgraph graph, String subjectVar) { + return modifyImpl(graph.getAttributeNodes(), subjectVar); + } + + private QueryModification modifyImpl(List> attributeNodes, String subjectVarName) { + final StringBuilder attributePatterns = new StringBuilder(); + final StringBuilder optionalAttributesPatterns = new StringBuilder(); + final List variables = new ArrayList<>(); + final List variablesToAppend = new ArrayList<>(); + final String subjectVariable = "?" + subjectVarName; + attributeNodes.forEach(attNode -> { + final Attribute att = attribute(attNode); + final int min = Arrays.stream(att.getConstraints()).map(ParticipationConstraint::min) + .min(Comparator.naturalOrder()).orElse(0); + final String variable = "?" + varName(subjectVarName, att.getName()); + variables.add(new QueryVariableMapping(subjectVarName, varName(subjectVarName, att.getName()), att)); + final StringBuilder target = min < 1 ? optionalAttributesPatterns : attributePatterns; + if (min < 1) { + target.append("OPTIONAL { "); + } + final Optional ctx = context(att); + ctx.ifPresent(uri -> target.append("GRAPH <").append(uri).append("> { ")); + target.append(subjectVariable).append(" <").append(att.getIRI()) + .append("> ").append(variable).append(" . "); + if (!attNode.getSubgraphs().isEmpty()) { + attNode.getSubgraphs().forEach((cls, graph) -> { + final IdentifiableEntityType subType = metamodel.entity(cls); + final EntityMappingQueryModifier subModifier = new EntityMappingQueryModifier(metamodel, subType, descriptor.getAttributeDescriptor(att), inferredAttsInDefault); + final QueryModification mod = subModifier.modify(graph, varName(subjectVarName, att.getName())); + target.append(mod.queryPart()); + variablesToAppend.addAll(mod.variables()); + }); + } + if (min < 1) { + target.append("} "); + } + ctx.ifPresent(uri -> target.append("} ")); + }); + addTypesPattern(subjectVarName, variables, attributePatterns, subjectVariable); + if (variablesToAppend.stream().anyMatch(QueryVariableMapping::canGroupConcat)) { + variables.forEach(QueryVariableMapping::preventGroupConcat); + } + variables.addAll(variablesToAppend); + // PERF: Put OPTIONAL patterns to the end + return new QueryModification(variables, attributePatterns + optionalAttributesPatterns.toString()); + } + + private void addTypesPattern(String subjectVarName, List variables, + StringBuilder attributePatterns, + String subjectVariable) { + final String typesVariable = "?" + varName(subjectVarName, TYPES_VAR_NAME); + variables.add(new QueryVariableMapping(subjectVarName, varName(subjectVarName, TYPES_VAR_NAME), resultType.getTypes())); + final Optional ctx = typesContext(); + if (resultType.getTypes() != null) { + ctx.ifPresent(uri -> attributePatterns.append("GRAPH <").append(uri).append("> { ")); + attributePatterns.append(subjectVariable).append(" a ").append(typesVariable).append(" . "); + ctx.ifPresent(uri -> attributePatterns.append("} ")); + } else { + ctx.ifPresent(uri -> attributePatterns.append("GRAPH <").append(uri).append("> { ")); + final String typeIri = IdentifierTransformer.stringifyIri(resultType.getIRI()); + attributePatterns.append(subjectVariable).append(" a ") + .append(typeIri).append(" . "); + attributePatterns.append("BIND (").append(typeIri).append(" AS ").append(typesVariable).append(") "); + ctx.ifPresent(uri -> attributePatterns.append("} ")); + } + } + + static String varName(String subjectVar, String attName) { + return subjectVar + "_" + attName; + } + + private Attribute attribute(AttributeNode node) { + if (node instanceof AttributeNodeImpl impl) { + return impl.getAttribute(); + } + return resultType.getAttributeIncludingSubTypes(node.getAttributeName()); + } + + private Optional context(FieldSpecification att) { + assert descriptor.getAttributeContexts(att).size() <= 1; + if (att.isInferred() && inferredAttsInDefault) { + return Optional.empty(); + } + return descriptor.getSingleAttributeContext(att).map(URI::toString); + } + + private Optional typesContext() { + final Optional> optionalTs = resultType.getTypes() != null ? Optional.of(resultType.getTypes()) : resultType.getSubtypes() + .stream() + .map(AbstractIdentifiableType::getTypes) + .filter(Objects::nonNull) + .findFirst(); + return optionalTs.flatMap(ts -> { + assert descriptor.getAttributeContexts(ts).size() <= 1; + return descriptor.getSingleAttributeContext(ts); + }).map(URI::toString); + } + + /** + * Query modification. + * + * @param variables Variables to project + * @param queryPart Basic graph patterns to add to the query + */ + record QueryModification(List variables, String queryPart) { + + public List variableNames() { + return variables.stream().map(QueryVariableMapping::attributeVar).toList(); + } + } +} diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/FetchGraphProcessor.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/FetchGraphProcessor.java new file mode 100644 index 000000000..89a56e9a9 --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/FetchGraphProcessor.java @@ -0,0 +1,75 @@ +package cz.cvut.kbss.jopa.query.sparql.loader; + +import cz.cvut.kbss.jopa.model.AttributeNode; +import cz.cvut.kbss.jopa.model.AttributeNodeImpl; +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.annotations.FetchType; +import cz.cvut.kbss.jopa.model.metamodel.Attribute; +import cz.cvut.kbss.jopa.model.metamodel.IdentifiableEntityType; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +class FetchGraphProcessor { + + private final MetamodelImpl metamodel; + + FetchGraphProcessor(MetamodelImpl metamodel) { + this.metamodel = metamodel; + } + + EntityGraph generateDefaultFetchGraph(IdentifiableEntityType et) { + final EntityGraph graph = new EntityGraphImpl<>(et, metamodel); + graph.addAttributeNodes(attributes(et).toArray(Attribute[]::new)); + return graph; + } + + static Collection> attributes(IdentifiableEntityType et) { + final List> atts = new ArrayList<>(et.getAttributes().stream() + .filter(att -> att.getFetchType() == FetchType.EAGER) + .toList()); + et.getSubtypes().stream() + .flatMap(subtype -> subtype.getAttributes().stream()) + .filter(att -> att.getFetchType() == FetchType.EAGER) + .forEach(atts::add); + return atts; + } + + List mapFetchGraphToProjection(EntityGraph fetchGraph, + IdentifiableEntityType et, + String rootVariable) { + return mapFetchGraphToProjection(fetchGraph.getAttributeNodes(), et, rootVariable); + } + + private List mapFetchGraphToProjection(List> attributeNodes, + IdentifiableEntityType et, + String subjectVariable) { + final List mappings = new ArrayList<>(attributeNodes.size()); + final List mappingsToAppend = new ArrayList<>(); + attributeNodes.forEach(attNode -> { + final Attribute att = attribute(attNode, et); + final String attVarName = EntityMappingQueryModifier.varName(subjectVariable, attNode.getAttributeName()); + mappings.add(new QueryVariableMapping(subjectVariable, attVarName, att)); + if (!attNode.getSubgraphs().isEmpty()) { + attNode.getSubgraphs().forEach((cls, graph) -> { + final IdentifiableEntityType subType = metamodel.entity(cls); + mappingsToAppend.addAll(mapFetchGraphToProjection(graph.getAttributeNodes(), subType, attVarName)); + }); + } + }); + mappings.add(new QueryVariableMapping(subjectVariable, + EntityMappingQueryModifier.varName(subjectVariable, AttributeEnumeratingSparqlAssemblyModifier.TYPES_VAR_NAME), et.getTypes())); + mappings.addAll(mappingsToAppend); + return mappings; + } + + private Attribute attribute(AttributeNode node, IdentifiableEntityType et) { + if (node instanceof AttributeNodeImpl impl) { + return impl.getAttribute(); + } + return et.getAttributeIncludingSubTypes(node.getAttributeName()); + } +} diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/GroupConcatQueryModifier.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/GroupConcatQueryModifier.java new file mode 100644 index 000000000..c5d842003 --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/GroupConcatQueryModifier.java @@ -0,0 +1,40 @@ +package cz.cvut.kbss.jopa.query.sparql.loader; + +import cz.cvut.kbss.ontodriver.exception.OntoDriverException; +import cz.cvut.kbss.ontodriver.iteration.ResultRow; +import cz.cvut.kbss.ontodriver.model.Value; + +import java.util.stream.Stream; + +/** + * Supports modifying a query to project GROUP_CONCAT for a given attribute. + *

+ * Both generating the GROUP_CONCAT projection and reading the values from the result row are supported. + */ +abstract class GroupConcatQueryModifier { + + static final String GROUP_CONCAT_SEPARATOR = "\u001F"; + static final String GROUP_CONCAT_SUFFIX = "_gc"; + + final QueryVariableMapping variableMapping; + + GroupConcatQueryModifier(QueryVariableMapping variableMapping) { + this.variableMapping = variableMapping; + } + + /** + * Generates a {@literal GROUP_CONCAT} pattern for query projection. + * + * @return Pattern for query projection + */ + abstract String generateGroupConcat(); + + /** + * Reads and extracts values created by a {@literal GROUP_CONCAT} pattern. + * + * @param row Row to extract values from + * @return Extracted values + * @throws OntoDriverException If reading the values fails + */ + abstract Stream> readValue(ResultRow row) throws OntoDriverException; +} diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/IriGroupConcatQueryModifier.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/IriGroupConcatQueryModifier.java new file mode 100644 index 000000000..57e6dfa8b --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/IriGroupConcatQueryModifier.java @@ -0,0 +1,34 @@ +package cz.cvut.kbss.jopa.query.sparql.loader; + +import cz.cvut.kbss.ontodriver.exception.OntoDriverException; +import cz.cvut.kbss.ontodriver.iteration.ResultRow; +import cz.cvut.kbss.ontodriver.model.Value; + +import java.net.URI; +import java.util.stream.Stream; + +/** + * Supports modifying a query to project GROUP_CONCAT for a plural attribute whose values are identifiers, and for a + * types attributes. + */ +class IriGroupConcatQueryModifier extends GroupConcatQueryModifier { + + IriGroupConcatQueryModifier(QueryVariableMapping variableMapping) { + super(variableMapping); + } + + @Override + String generateGroupConcat() { + return "(GROUP_CONCAT(DISTINCT ?" + variableMapping.attributeVar() + "; SEPARATOR='" + + GROUP_CONCAT_SEPARATOR + "') AS ?" + variableMapping.attributeVar() + GROUP_CONCAT_SUFFIX + ")"; + } + + @Override + Stream> readValue(ResultRow row) throws OntoDriverException { + assert row.isBound(variableMapping.attributeVar()); + final String values = row.getString(variableMapping.attributeVar()); + // This does not handle blank nodes + return Stream.of(values.split(AttributeEnumeratingSparqlAssemblyModifier.GROUP_CONCAT_SEPARATOR)) + .map(URI::create).map(Value::new); + } +} diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/MultilingualStringGroupConcatQueryModifier.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/MultilingualStringGroupConcatQueryModifier.java new file mode 100644 index 000000000..b9e143931 --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/MultilingualStringGroupConcatQueryModifier.java @@ -0,0 +1,50 @@ +package cz.cvut.kbss.jopa.query.sparql.loader; + +import cz.cvut.kbss.ontodriver.exception.OntoDriverException; +import cz.cvut.kbss.ontodriver.iteration.ResultRow; +import cz.cvut.kbss.ontodriver.model.LangString; +import cz.cvut.kbss.ontodriver.model.Value; + +import java.util.stream.Stream; + +/** + * Supports generating GROUP_CONCAT projection for multilingual string attributes. + */ +class MultilingualStringGroupConcatQueryModifier extends GroupConcatQueryModifier { + + MultilingualStringGroupConcatQueryModifier(QueryVariableMapping variableMapping) { + super(variableMapping); + } + + @Override + String generateGroupConcat() { + // (GROUP_CONCAT(DISTINCT CONCAT(STR(?var), "@", LANG(?var)); SEPARATOR='') AS ?var_gc) + final String queryVar = "?" + variableMapping.attributeVar(); + return "(GROUP_CONCAT(DISTINCT CONCAT(STR(" + queryVar + "), \"@\", " + + "LANG(" + queryVar + ")); SEPARATOR='" + GROUP_CONCAT_SEPARATOR + "') " + + "AS " + queryVar + GROUP_CONCAT_SUFFIX + ")"; + } + + @Override + Stream> readValue(ResultRow row) throws OntoDriverException { + assert row.isBound(variableMapping.attributeVar()); + final String values = row.getString(variableMapping.attributeVar()); + if (values.isEmpty()) { + return Stream.empty(); + } + return Stream.of(values.split(GROUP_CONCAT_SEPARATOR)) + .map(MultilingualStringGroupConcatQueryModifier::toValue); + } + + private static Value toValue(String token) { + // The token is produced by CONCAT(STR(?var), "@", LANG(?var)). + // The lexical value may itself contain '@', so split on the last occurrence. + final int sep = token.lastIndexOf('@'); + if (sep < 0) { + return new Value<>(token); + } + final String value = token.substring(0, sep); + final String language = token.substring(sep + 1); + return new Value<>(language.isEmpty() ? value : new LangString(value, language)); + } +} diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/QueryResultLoadingOptimizer.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/QueryResultLoadingOptimizer.java deleted file mode 100644 index 67684a381..000000000 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/QueryResultLoadingOptimizer.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * JOPA - * Copyright (C) 2026 Czech Technical University in Prague - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3.0 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library. - */ -package cz.cvut.kbss.jopa.query.sparql.loader; - -import cz.cvut.kbss.jopa.model.QueryResultLoader; -import cz.cvut.kbss.jopa.model.descriptors.Descriptor; -import cz.cvut.kbss.jopa.query.QueryHolder; - -/** - * Allows optimizing query results by modifying the query assembly and providing a corresponding query result loader. - * - * @param Query holder type - */ -public abstract class QueryResultLoadingOptimizer { - - protected final H queryHolder; - - protected boolean optimizationEnabled; - - protected QueryResultLoadingOptimizer(H queryHolder) {this.queryHolder = queryHolder;} - - public void enableOptimization() {this.optimizationEnabled = true;} - - public void disableOptimization() {this.optimizationEnabled = false;} - - /** - * Modifies the query assembly for optimized entity loading, if possible. - * - * @param resultClass Query result class - * @param descriptor Descriptor specified for query result loading - */ - public abstract void optimizeQueryAssembly(Class resultClass, Descriptor descriptor); - - /** - * Gets loader of query results for the specified query. - *

- * If possible, a version supporting optimized entity loading is returned (depending on whether - * {@link #optimizeQueryAssembly(Class, Descriptor)} has been called and the assembled query optimized). - * - * @param resultClass Query result class - * @param descriptor Descriptor specified for results - * @param Result type - * @return Query result loader - */ - public abstract QueryResultLoader getQueryResultLoader(Class resultClass, Descriptor descriptor); -} diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/QueryResultLoadingStrategy.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/QueryResultLoadingStrategy.java new file mode 100644 index 000000000..3766b0760 --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/QueryResultLoadingStrategy.java @@ -0,0 +1,46 @@ +/* + * JOPA + * Copyright (C) 2026 Czech Technical University in Prague + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package cz.cvut.kbss.jopa.query.sparql.loader; + +import cz.cvut.kbss.jopa.model.QueryResultLoader; + +import java.util.function.Supplier; + +/** + * A strategy that combines a SPARQL query assembly modification with a corresponding query result loader. + *

+ * Each strategy expresses a single way of optimizing query result loading: it knows how to rewrite the SPARQL query + * (if at all) and how to decode the resulting rows into instances of the requested type. Pairing the two halves into + * a single object removes the need for a separate visitor double-dispatch between modifiers and loaders. + * + * @param assemblyModifier Modifier applied to the SPARQL query during assembly, or {@code null} if no rewrite is needed + * @param loaderFactory Factory creating a fresh {@link QueryResultLoader} bound to this strategy + * @param Result type + */ +public record QueryResultLoadingStrategy(SparqlAssemblyModifier assemblyModifier, + Supplier> loaderFactory) { + + /** + * Creates a new {@link QueryResultLoader} for this strategy. + * + * @return Query result loader + */ + public QueryResultLoader createLoader() { + return loaderFactory.get(); + } +} diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/QueryVariableMapping.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/QueryVariableMapping.java new file mode 100644 index 000000000..89df95d93 --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/QueryVariableMapping.java @@ -0,0 +1,110 @@ +package cz.cvut.kbss.jopa.query.sparql.loader; + +import cz.cvut.kbss.jopa.model.MultilingualString; +import cz.cvut.kbss.jopa.model.metamodel.FieldSpecification; +import cz.cvut.kbss.jopa.model.metamodel.PluralAttribute; +import cz.cvut.kbss.jopa.model.metamodel.TypesSpecification; +import cz.cvut.kbss.jopa.utils.IdentifierTransformer; +import cz.cvut.kbss.ontodriver.exception.OntoDriverException; +import cz.cvut.kbss.ontodriver.iteration.ResultRow; +import cz.cvut.kbss.ontodriver.model.Value; + +import java.util.Objects; +import java.util.stream.Stream; + +public final class QueryVariableMapping { + private final String subjectVar; + private String attributeVar; + private final FieldSpecification attribute; + + private GroupConcatQueryModifier groupConcatModifier; + + public QueryVariableMapping(String subjectVar, String attributeVar, FieldSpecification attribute) { + this.subjectVar = subjectVar; + this.attributeVar = attributeVar; + this.attribute = attribute; + this.groupConcatModifier = resolveCanGroupConcat(); + } + + private GroupConcatQueryModifier resolveCanGroupConcat() { + if (attribute == null) { + return null; + } + if (isPluralPlainIdentifierAttribute() || attribute instanceof TypesSpecification) { + return new IriGroupConcatQueryModifier(this); + } + if (isMultilingualStringAttribute() || isPluralStringAttribute()) { + return new MultilingualStringGroupConcatQueryModifier(this); + } + return null; + } + + private boolean isPluralPlainIdentifierAttribute() { + if (!attribute.isMappedAttribute() || !attribute.isCollection()) { + return false; + } + final PluralAttribute att = (PluralAttribute) attribute; + return att.isAssociation() && IdentifierTransformer.isValidIdentifierType(att.getBindableJavaType()); + } + + private boolean isMultilingualStringAttribute() { + return attribute.isMappedAttribute() && attribute.getJavaType().equals(MultilingualString.class) + || attribute.isCollection() && ((PluralAttribute) attribute).getBindableJavaType() + .equals(MultilingualString.class); + } + + private boolean isPluralStringAttribute() { + return attribute.isMappedAttribute() && attribute.isCollection() + && ((PluralAttribute) attribute).getBindableJavaType().equals(String.class); + } + + public String subjectVar() {return subjectVar;} + + public String attributeVar() {return attributeVar;} + + public FieldSpecification attribute() {return attribute;} + + public boolean canGroupConcat() { + return groupConcatModifier != null; + } + + public void preventGroupConcat() { + this.groupConcatModifier = null; + } + + public String generateGroupConcat() { + assert canGroupConcat(); + final String result = groupConcatModifier.generateGroupConcat(); + this.attributeVar = attributeVar + GroupConcatQueryModifier.GROUP_CONCAT_SUFFIX; + return result; + } + + public Stream> readGroupConcatValue(ResultRow row) throws OntoDriverException { + assert canGroupConcat(); + return groupConcatModifier.readValue(row); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) {return true;} + if (obj == null || obj.getClass() != this.getClass()) {return false;} + var that = (QueryVariableMapping) obj; + return Objects.equals(this.subjectVar, that.subjectVar) && + Objects.equals(this.attributeVar, that.attributeVar) && + Objects.equals(this.attribute, that.attribute); + } + + @Override + public int hashCode() { + return Objects.hash(subjectVar, attributeVar, attribute); + } + + @Override + public String toString() { + return "QueryVariableMapping[" + + "subjectVar=" + subjectVar + ", " + + "attributeVar=" + attributeVar + ", " + + "attribute=" + attribute + ']'; + } + +} diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/SparqlQueryResultLoadingOptimizer.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/SparqlQueryResultLoadingOptimizer.java index 47d1e034c..604a63da1 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/SparqlQueryResultLoadingOptimizer.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/SparqlQueryResultLoadingOptimizer.java @@ -18,8 +18,8 @@ package cz.cvut.kbss.jopa.query.sparql.loader; import cz.cvut.kbss.jopa.model.BaseEntityQueryResultLoader; +import cz.cvut.kbss.jopa.model.EntityGraph; import cz.cvut.kbss.jopa.model.NonEntityQueryResultLoader; -import cz.cvut.kbss.jopa.model.QueryResultLoader; import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.metamodel.IdentifiableEntityType; import cz.cvut.kbss.jopa.query.QueryType; @@ -30,45 +30,90 @@ import org.slf4j.LoggerFactory; /** - * Allows optimizing queries that load entity instances. + * Selects a {@link QueryResultLoadingStrategy} that combines an optional SPARQL query rewrite with a matching query + * result loader. */ -public class SparqlQueryResultLoadingOptimizer extends QueryResultLoadingOptimizer { +public class SparqlQueryResultLoadingOptimizer { private static final Logger LOG = LoggerFactory.getLogger(SparqlQueryResultLoadingOptimizer.class); + private final TokenStreamSparqlQueryHolder queryHolder; + private final UnitOfWork uow; private final ConnectionWrapper connection; + private boolean optimizationEnabled; + public SparqlQueryResultLoadingOptimizer(TokenStreamSparqlQueryHolder queryHolder, UnitOfWork uow, ConnectionWrapper connection) { - super(queryHolder); + this.queryHolder = queryHolder; this.uow = uow; this.connection = connection; } - @Override - public void optimizeQueryAssembly(Class resultClass, Descriptor descriptor) { - switch (resolveOptimizerType(resultClass, descriptor)) { - case TRIPLE_BASED: + public void enableOptimization() {this.optimizationEnabled = true;} + + public void disableOptimization() {this.optimizationEnabled = false;} + + /** + * Selects an appropriate {@link QueryResultLoadingStrategy} for the given query result configuration and registers + * its assembly modifier (if any) on the underlying query holder. + * + * @param resultClass Query result class + * @param descriptor Descriptor specified for query result loading + * @param fetchGraph Optional fetch graph specifying which properties should be loaded + * @param Result type + * @return Strategy used to load query results + */ + public QueryResultLoadingStrategy chooseStrategy(Class resultClass, Descriptor descriptor, + EntityGraph fetchGraph) { + final QueryResultLoadingStrategy strategy = buildStrategy(resultClass, descriptor, fetchGraph); + queryHolder.setAssemblyModifier(strategy.assemblyModifier()); + return strategy; + } + + private QueryResultLoadingStrategy buildStrategy(Class resultClass, Descriptor descriptor, + EntityGraph fetchGraph) { + switch (resolveOptimizerType(resultClass, descriptor, fetchGraph)) { + case TRIPLE_BASED -> { LOG.trace("Processing query results with triple-based optimized entity loading."); - queryHolder.setAssemblyModifier(new UnboundPredicateObjectSparqlAssemblyModifier()); - break; - case ATTRIBUTE_BASED: - LOG.trace("Processing query results with attribute enumeration-based optimized attribute loading."); - queryHolder.setAssemblyModifier(new AttributeEnumeratingSparqlAssemblyModifier(uow.getMetamodel() - .entity(resultClass), descriptor, connection)); - break; - default: - // Do nothing - break; + return new QueryResultLoadingStrategy<>( + new UnboundPredicateObjectSparqlAssemblyModifier(), + () -> new TripleBasedRowsToAxiomsQueryResultLoader<>(uow, resultClass, descriptor)); + } + case ATTRIBUTE_BASED, FETCH_GRAPH_BASED -> { + LOG.trace("Processing query results with attribute enumerating optimized entity loading."); + final AttributeEnumeratingSparqlAssemblyModifier modifier = new AttributeEnumeratingSparqlAssemblyModifier( + uow.getMetamodel(), uow.getMetamodel().entity(resultClass), descriptor, fetchGraph, connection); + return new QueryResultLoadingStrategy<>( + modifier, + () -> new AttributeBasedRowsToAxiomsQueryResultLoader<>(uow, resultClass, descriptor, fetchGraph, modifier::getVariableMapping)); + } + default -> { + if (uow.isEntityType(resultClass) && queryHolder.getProjectedQueryParameters().size() > 1) { + // Entity attributes are explicitly projected from the query; let the loader derive mappings + // from the result set columns. + return new QueryResultLoadingStrategy<>( + null, + () -> new AttributeBasedRowsToAxiomsQueryResultLoader<>(uow, resultClass, descriptor, fetchGraph)); + } + return new QueryResultLoadingStrategy<>( + null, + () -> uow.isEntityType(resultClass) + ? new BaseEntityQueryResultLoader<>(uow, resultClass, descriptor) + : new NonEntityQueryResultLoader<>(resultClass)); + } } } - private OptimizerType resolveOptimizerType(Class resultClass, Descriptor descriptor) { - if (!canOptimize(resultClass, descriptor)) { + private OptimizerType resolveOptimizerType(Class resultClass, Descriptor descriptor, EntityGraph fetchGraph) { + if (!canOptimize(resultClass, descriptor, fetchGraph)) { return OptimizerType.NONE; } + if (fetchGraph != null) { + return OptimizerType.FETCH_GRAPH_BASED; + } final IdentifiableEntityType et = uow.getMetamodel().entity(resultClass); if (et.getProperties() == null && !et.hasSubtypes()) { return OptimizerType.ATTRIBUTE_BASED; @@ -79,8 +124,8 @@ private OptimizerType resolveOptimizerType(Class resultClass, Descriptor desc return OptimizerType.NONE; } - private boolean canOptimize(Class resultClass, Descriptor descriptor) { - return optimizationEnabled && queryHolder.getQueryType() == QueryType.SELECT + private boolean canOptimize(Class resultClass, Descriptor descriptor, EntityGraph fetchGraph) { + return (optimizationEnabled || fetchGraph != null) && queryHolder.getQueryType() == QueryType.SELECT && projectsEntity(resultClass) && limitOrOffsetNotSet() && descriptorSpecifiesAtMostOneContext(descriptor); } @@ -109,42 +154,10 @@ private boolean queryContainsGraphOrServiceClause() { return queryHolder.getQueryAttributes().hasGraphOrService(); } - /** - * Gets loader of query results for the specified query. - *

- * If possible, a version supporting optimized entity loading is returned. - * - * @param resultClass Result class - * @param descriptor Descriptor specified for results - * @param Result type - * @return Query result loader - */ - @Override - public QueryResultLoader getQueryResultLoader(Class resultClass, Descriptor descriptor) { - final OptimizerType optimizerType = resolveOptimizerTypeForResultLoader(resultClass, descriptor); - return switch (optimizerType) { - case TRIPLE_BASED -> new TripleBasedRowsToAxiomsQueryResultLoader<>(uow, resultClass, descriptor); - case ATTRIBUTE_BASED -> new AttributeBasedRowsToAxiomsQueryResultLoader<>(uow, resultClass, descriptor); - default -> - uow.isEntityType(resultClass) ? new BaseEntityQueryResultLoader<>(uow, resultClass, descriptor) : new NonEntityQueryResultLoader<>(resultClass); - }; - } - - private OptimizerType resolveOptimizerTypeForResultLoader(Class resultClass, Descriptor descriptor) { - final OptimizerType ot = resolveOptimizerType(resultClass, descriptor); - if (ot != OptimizerType.NONE) { - return ot; - } - if (uow.isEntityType(resultClass) && queryHolder.getProjectedQueryParameters().size() > 1) { - // Entity attributes are projected from the query - return OptimizerType.ATTRIBUTE_BASED; - } - return ot; - } - private enum OptimizerType { TRIPLE_BASED, ATTRIBUTE_BASED, + FETCH_GRAPH_BASED, NONE } } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/TripleBasedRowsToAxiomsQueryResultLoader.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/TripleBasedRowsToAxiomsQueryResultLoader.java index 4f6377984..fe4319460 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/TripleBasedRowsToAxiomsQueryResultLoader.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/TripleBasedRowsToAxiomsQueryResultLoader.java @@ -24,6 +24,7 @@ import cz.cvut.kbss.jopa.model.metamodel.Attribute; import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.jopa.sessions.UnitOfWork; +import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingConfigGroup; import cz.cvut.kbss.jopa.vocabulary.RDF; import cz.cvut.kbss.ontodriver.exception.OntoDriverException; import cz.cvut.kbss.ontodriver.iteration.ResultRow; @@ -43,7 +44,7 @@ /** * Loads entity by aggregating rows with the same subject into axioms and then using - * {@link UnitOfWork#readObjectFromAxioms(Class, Collection, Descriptor)} to read the entity. + * {@link UnitOfWork#readObjectFromAxioms(Class, Collection, AxiomBasedLoadingConfigGroup)} to read the entity. *

* It expects the query result rows to have three columns corresponding to the triple subject, property and object. *

@@ -112,7 +113,7 @@ private Assertion propertyToAssertion(URI property) { private T loadEntity() { try { - return uow.readObjectFromAxioms(resultType, currentEntityAxioms, descriptor); + return uow.readObjectFromAxioms(resultType, currentEntityAxioms, new AxiomBasedLoadingConfigGroup<>(currentSubject.getIdentifier(), descriptor)); } catch (CardinalityConstraintViolatedException e) { // Axioms may contain more statements than expected due to query evaluation containing inferred results. // If the entity class declares ICs on non-inferred attributes, this may lead to IC violation exception. diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/UnboundPredicateObjectSparqlAssemblyModifier.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/UnboundPredicateObjectSparqlAssemblyModifier.java index e9c68f467..e4df9fe93 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/UnboundPredicateObjectSparqlAssemblyModifier.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/sparql/loader/UnboundPredicateObjectSparqlAssemblyModifier.java @@ -88,4 +88,5 @@ private static boolean requiresDot(TokenStream tokenStream, Token lastClosingCur } return true; } + } 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 949e70268..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 @@ -20,8 +20,8 @@ import cz.cvut.kbss.jopa.exceptions.EntityNotFoundException; import cz.cvut.kbss.jopa.exceptions.OWLEntityExistsException; import cz.cvut.kbss.jopa.exceptions.OWLPersistenceException; -import cz.cvut.kbss.jopa.proxy.IndirectWrapper; -import cz.cvut.kbss.jopa.sessions.cache.CacheManager; +import cz.cvut.kbss.jopa.model.EntityGraph; +import cz.cvut.kbss.jopa.model.EntityGraphImpl; import cz.cvut.kbss.jopa.model.EntityState; import cz.cvut.kbss.jopa.model.JOPAPersistenceProperties; import cz.cvut.kbss.jopa.model.LoadState; @@ -33,8 +33,10 @@ import cz.cvut.kbss.jopa.model.metamodel.FieldSpecification; import cz.cvut.kbss.jopa.model.metamodel.IdentifiableEntityType; import cz.cvut.kbss.jopa.model.query.criteria.CriteriaBuilder; +import cz.cvut.kbss.jopa.proxy.IndirectWrapper; import cz.cvut.kbss.jopa.proxy.lazy.LazyLoadingProxy; import cz.cvut.kbss.jopa.query.sparql.SparqlQueryFactory; +import cz.cvut.kbss.jopa.sessions.cache.CacheManager; import cz.cvut.kbss.jopa.sessions.cache.Descriptors; import cz.cvut.kbss.jopa.sessions.change.Change; import cz.cvut.kbss.jopa.sessions.change.ChangeCalculator; @@ -44,6 +46,7 @@ import cz.cvut.kbss.jopa.sessions.change.UnitOfWorkChangeSet; import cz.cvut.kbss.jopa.sessions.descriptor.LoadStateDescriptor; import cz.cvut.kbss.jopa.sessions.descriptor.LoadStateDescriptorFactory; +import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingConfigGroup; import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingParameters; import cz.cvut.kbss.jopa.sessions.util.CloneConfiguration; import cz.cvut.kbss.jopa.sessions.util.CloneRegistrationDescriptor; @@ -356,16 +359,17 @@ public T readObjectWithoutRegistration(Class cls, Object identifier, Desc } @Override - public T readObjectFromAxioms(Class cls, Collection> axioms, Descriptor descriptor) { + public T readObjectFromAxioms(Class cls, Collection> axioms, + AxiomBasedLoadingConfigGroup config) { Objects.requireNonNull(cls); Objects.requireNonNull(axioms); - Objects.requireNonNull(descriptor); + Objects.requireNonNull(config); - final T result = storage.loadFromAxioms(new AxiomBasedLoadingParameters<>(cls, descriptor, false, axioms)); + final T result = storage.loadFromAxioms(new AxiomBasedLoadingParameters<>(cls, axioms, config)); if (result == null) { return null; } - final Object clone = registerExistingObject(result, new CloneRegistrationDescriptor(descriptor).postCloneHandlers(List.of(new PostLoadInvoker(getMetamodel())))); + final Object clone = registerExistingObject(result, new CloneRegistrationDescriptor(config.descriptor()).postCloneHandlers(List.of(new PostLoadInvoker(getMetamodel())))); return cls.cast(clone); } @@ -486,8 +490,8 @@ protected IdentifiableEntityType entityType(Class cls) { } /** - * This method calculates the changes to the registered entities and adds these changes into the given - * change set for future commit to the ontology. + * This method calculates the changes to the registered entities and adds these changes into the given change set + * for future commit to the ontology. */ void calculateChanges() { if (hasNew) { @@ -951,7 +955,15 @@ public boolean isInferred(T entity, FieldSpecification attribu public LoadState isLoaded(Object entity, String attributeName) { Objects.requireNonNull(entity); final FieldSpecification fs = entityType(entity.getClass()).getFieldSpecification(attributeName); - return loadStateRegistry.contains(entity) ? loadStateRegistry.get(entity).isLoaded(fs) : LoadState.UNKNOWN; + return isLoaded(entity, fs); + } + + @Override + public LoadState isLoaded(Object entity, FieldSpecification fieldSpec) { + Objects.requireNonNull(entity); + Objects.requireNonNull(fieldSpec); + return loadStateRegistry.contains(entity) ? loadStateRegistry.get(entity) + .isLoaded(fieldSpec) : LoadState.UNKNOWN; } @Override @@ -960,14 +972,26 @@ public LoadState isLoaded(Object entity) { return loadStateRegistry.contains(entity) ? loadStateRegistry.get(entity).isLoaded() : LoadState.UNKNOWN; } + @Override public SparqlQueryFactory sparqlQueryFactory() { return queryFactory; } + @Override public CriteriaBuilder getCriteriaBuilder() { return parent.getCriteriaBuilder(); } + @Override + 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/ReadOnlyUnitOfWork.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/ReadOnlyUnitOfWork.java index 96f207f62..817fae2f1 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/ReadOnlyUnitOfWork.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/ReadOnlyUnitOfWork.java @@ -28,6 +28,7 @@ import cz.cvut.kbss.jopa.sessions.change.ObjectChangeSet; import cz.cvut.kbss.jopa.sessions.descriptor.LoadStateDescriptor; import cz.cvut.kbss.jopa.sessions.descriptor.LoadStateDescriptorFactory; +import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingConfigGroup; import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingParameters; import cz.cvut.kbss.jopa.sessions.util.CloneConfiguration; import cz.cvut.kbss.jopa.sessions.util.CloneRegistrationDescriptor; @@ -140,17 +141,18 @@ public T getManagedOriginal(Class cls, Object identifier, Descriptor desc } @Override - public T readObjectFromAxioms(Class cls, Collection> axioms, Descriptor descriptor) { + public T readObjectFromAxioms(Class cls, Collection> axioms, + AxiomBasedLoadingConfigGroup config) { Objects.requireNonNull(cls); Objects.requireNonNull(axioms); - Objects.requireNonNull(descriptor); + Objects.requireNonNull(config); - final AxiomBasedLoadingParameters loadingParameters = new AxiomBasedLoadingParameters<>(cls, descriptor, true, axioms); + final AxiomBasedLoadingParameters loadingParameters = new AxiomBasedLoadingParameters<>(cls, axioms, config); final T result = storage.loadFromAxioms(loadingParameters); if (result == null) { return null; } - return cls.cast(registerExistingObject(result, descriptor)); + return cls.cast(registerExistingObject(result, config.descriptor())); } /** 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 37a96bc50..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 @@ -18,12 +18,14 @@ package cz.cvut.kbss.jopa.sessions; import cz.cvut.kbss.jopa.exceptions.OWLPersistenceException; +import cz.cvut.kbss.jopa.model.EntityGraph; import cz.cvut.kbss.jopa.model.EntityState; import cz.cvut.kbss.jopa.model.LoadState; import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.metamodel.FieldSpecification; import cz.cvut.kbss.jopa.model.query.criteria.CriteriaBuilder; import cz.cvut.kbss.jopa.query.sparql.SparqlQueryFactory; +import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingConfigGroup; import cz.cvut.kbss.jopa.sessions.util.CloneRegistrationDescriptor; import cz.cvut.kbss.jopa.sessions.util.LoadStateDescriptorRegistry; import cz.cvut.kbss.jopa.utils.Wrapper; @@ -173,14 +175,17 @@ public interface UnitOfWork extends ConfigurationHolder, MetamodelProvider, Wrap * Reads an object from the specified axioms. *

* It is assumed the axioms correspond to the provided descriptor. + *

+ * The axioms may represent an entity graph, i.e. multiple interconnected entities. It is the responsibility of the + * loader to ensure the entity graph is reconstructed correctly w.r.t. the provided fetch graph. * - * @param cls Expected result class - * @param axioms Axioms representing the object - * @param descriptor Entity descriptor - * @param Return type + * @param cls Expected result class + * @param axioms Axioms representing the object(s) + * @param config Additional loading configuration + * @param Return type * @return The retrieved object or {@code null} if no object of the expected target class can be reconstructed */ - T readObjectFromAxioms(Class cls, Collection> axioms, Descriptor descriptor); + T readObjectFromAxioms(Class cls, Collection> axioms, AxiomBasedLoadingConfigGroup config); /** * Retrieves a reference to an object with the specified identifier. @@ -322,12 +327,22 @@ public interface UnitOfWork extends ConfigurationHolder, MetamodelProvider, Wrap * Gets the load status of the specified attribute on the specified entity. * * @param entity Entity instance - * @param attributeName Attribute whose load status is to be determined + * @param attributeName Name of the attribute whose load status is to be determined * @return Attribute load status * @see cz.cvut.kbss.jopa.model.ProviderUtil#isLoadedWithoutReference(Object, String) */ LoadState isLoaded(Object entity, String attributeName); + /** + * Gets the load status of the specified attribute on the specified entity. + * + * @param entity Entity instance + * @param fieldSpec Attribute whose load status is to be determined + * @return Attribute load status + * @see cz.cvut.kbss.jopa.model.ProviderUtil#isLoadedWithoutReference(Object, String) + */ + LoadState isLoaded(Object entity, FieldSpecification fieldSpec); + /** * Gets the load status of the specified entity. * @@ -418,4 +433,22 @@ public interface UnitOfWork extends ConfigurationHolder, MetamodelProvider, Wrap * @return Criteria query builder */ CriteriaBuilder getCriteriaBuilder(); + + /** + * Creates an {@link EntityGraph} instance that can be used to specify an entity graph. + * + * @param rootType Root entity class + * @param Root entity class type + * @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); } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/descriptor/LoadStateDescriptorFactory.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/descriptor/LoadStateDescriptorFactory.java index 06885a74e..91223158e 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/descriptor/LoadStateDescriptorFactory.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/descriptor/LoadStateDescriptorFactory.java @@ -17,11 +17,15 @@ */ package cz.cvut.kbss.jopa.sessions.descriptor; +import cz.cvut.kbss.jopa.model.AttributeNodeImpl; import cz.cvut.kbss.jopa.model.LoadState; import cz.cvut.kbss.jopa.model.annotations.FetchType; +import cz.cvut.kbss.jopa.model.metamodel.Attribute; import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.jopa.model.metamodel.FieldSpecification; +import cz.cvut.kbss.jopa.model.metamodel.IdentifiableEntityType; import cz.cvut.kbss.jopa.proxy.lazy.LazyLoadingProxy; +import cz.cvut.kbss.jopa.sessions.util.FetchGraphWrapper; import cz.cvut.kbss.jopa.utils.EntityPropertiesUtils; /** @@ -69,6 +73,34 @@ public static LoadStateDescriptor createAllUnknown(T instance, EntityType return new LoadStateDescriptor<>(instance, et, LoadState.UNKNOWN); } + /** + * Creates an instance descriptor for specified instance based on the specified fetch graph. + *

+ * The descriptor will look as follows: + *

    + *
  • Attributes specified in the fetch graph - {@link LoadState#LOADED}
  • + *
  • Object-property attributes - {@link LoadState#NOT_LOADED}
  • + *
  • Other attributes - {@link LoadState#UNKNOWN}
  • + *
+ * + * @param instance Instance to create descriptor for + * @param et Entity type of the instance + * @param fetchGraph Entity fetch graph + * @param Instance type + * @return Fresh instance descriptor + */ + public static LoadStateDescriptor createForFetchGraph(T instance, IdentifiableEntityType et, + FetchGraphWrapper fetchGraph) { + final LoadStateDescriptor descriptor = createAllUnknown(instance, et); + et.getAttributes().stream().filter(Attribute::isAssociation) + .forEach(att -> descriptor.setLoaded(att, LoadState.NOT_LOADED)); + fetchGraph.getAttributeNodes().forEach(an -> { + final Attribute fs = (an instanceof AttributeNodeImpl attNode) ? attNode.getAttribute() : et.getAttributeIncludingSubTypes(an.getAttributeName()); + descriptor.setLoaded(fs, LoadState.LOADED); + }); + return descriptor; + } + /** * Creates an instance descriptor which sets load status of attributes based on their value in the specified * instance as follows: diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/util/AxiomBasedLoadingConfigGroup.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/util/AxiomBasedLoadingConfigGroup.java new file mode 100644 index 000000000..d8c3dc83d --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/util/AxiomBasedLoadingConfigGroup.java @@ -0,0 +1,20 @@ +package cz.cvut.kbss.jopa.sessions.util; + +import cz.cvut.kbss.jopa.model.EntityGraph; +import cz.cvut.kbss.jopa.model.descriptors.Descriptor; + +import java.net.URI; + +/** + * Helper configuration for loading entities directly from axioms. + * + * @param subject (Root) entity identifier + * @param descriptor Entity data descriptor + * @param fetchGraph Fetch graph, possibly {@code null} + */ +public record AxiomBasedLoadingConfigGroup(URI subject, Descriptor descriptor, EntityGraph fetchGraph) { + + public AxiomBasedLoadingConfigGroup(URI subject, Descriptor descriptor) { + this(subject, descriptor, null); + } +} diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/util/AxiomBasedLoadingParameters.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/util/AxiomBasedLoadingParameters.java index 66135ad82..831ec9afa 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/util/AxiomBasedLoadingParameters.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/util/AxiomBasedLoadingParameters.java @@ -17,7 +17,6 @@ */ package cz.cvut.kbss.jopa.sessions.util; -import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.ontodriver.model.Axiom; import java.util.Collection; @@ -25,12 +24,21 @@ /** * Loading parameters for loading an entity from axioms. * - * @param cls Entity class - * @param descriptor Entity descriptor - * @param bypassCache Whether to bypass cache - * @param axioms Axioms to load the entity from - * @param Entity type + * @param cls Entity class + * @param axioms Axioms to load the entity from + * @param config Additional config for loading entity based on axioms + * @param Entity type */ -public record AxiomBasedLoadingParameters(Class cls, Descriptor descriptor, boolean bypassCache, - Collection> axioms) { +public record AxiomBasedLoadingParameters(Class cls, Collection> axioms, + AxiomBasedLoadingConfigGroup config) { + + /** + * Returns {@code true} to indicate that the cache should always be bypassed when loading entity directly from + * provided axioms. + * + * @return {@code true} + */ + public boolean bypassCache() { + return true; + } } diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/util/FetchGraphWrapper.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/util/FetchGraphWrapper.java new file mode 100644 index 000000000..f52b77678 --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/util/FetchGraphWrapper.java @@ -0,0 +1,79 @@ +package cz.cvut.kbss.jopa.sessions.util; + +import cz.cvut.kbss.jopa.model.AttributeNode; +import cz.cvut.kbss.jopa.model.EntityGraph; +import cz.cvut.kbss.jopa.model.Subgraph; +import cz.cvut.kbss.jopa.model.metamodel.FieldSpecification; + +import java.util.List; +import java.util.Objects; + +/** + * Wrapper for {@link EntityGraph} or {@link Subgraph} providing a unified API. + */ +public class FetchGraphWrapper { + + private final List> attributeNodes; + + public FetchGraphWrapper() { + this.attributeNodes = null; + } + + private FetchGraphWrapper(List> attributeNodes) { + this.attributeNodes = attributeNodes; + } + + public FetchGraphWrapper(EntityGraph entityGraph) { + this.attributeNodes = entityGraph != null ? entityGraph.getAttributeNodes() : null; + } + + public FetchGraphWrapper(Subgraph subgraph) { + assert subgraph != null; + this.attributeNodes = subgraph.getAttributeNodes(); + } + + public List> getAttributeNodes() { + return attributeNodes; + } + + public boolean isPresent() { + return attributeNodes != null; + } + + public boolean hasAttribute(FieldSpecification att) { + return isPresent() && attributeNodes.stream().anyMatch(an -> att.getName().equals(an.getAttributeName())); + } + + /** + * Gets a subgraph for the specified attribute (if present). + *

+ * Empty {@link FetchGraphWrapper} is returned if the attribute is not present or this wrapper is empty. + * + * @param att Attribute to get subgraph for + * @return Fetch graph wrapper + */ + public FetchGraphWrapper getAttributeSubgraph(FieldSpecification att) { + if (!isPresent()) { + return new FetchGraphWrapper(); + } + return attributeNodes.stream() + .filter(an -> att.getName().equals(an.getAttributeName())) + .findFirst().map(an -> new FetchGraphWrapper(an.getSubgraphs().values().stream() + .flatMap(sg -> sg.getAttributeNodes() + .stream()).toList())) + .orElse(new FetchGraphWrapper()); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof FetchGraphWrapper that)) { + return false; + } + return Objects.equals(attributeNodes, that.attributeNodes); + } + + @Override + public int hashCode() { + return Objects.hashCode(attributeNodes); + } +} diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/util/LoadingParameters.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/util/LoadingParameters.java index a77615ebe..60a065bcf 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/util/LoadingParameters.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/util/LoadingParameters.java @@ -21,17 +21,23 @@ import java.net.URI; -public record LoadingParameters (Class entityClass, URI identifier, Descriptor descriptor, boolean forceEager, boolean bypassCache) { +public record LoadingParameters(Class entityClass, URI identifier, Descriptor descriptor, + FetchGraphWrapper fetchGraph, boolean forceEager, boolean bypassCache) { public LoadingParameters(Class cls, URI identifier, Descriptor descriptor) { - this(cls, identifier, descriptor, false, false); + this(cls, identifier, descriptor, new FetchGraphWrapper(), false, false); } public LoadingParameters(Class cls, URI identifier, Descriptor descriptor, boolean forceEager) { - this(cls, identifier, descriptor, forceEager, false); + this(cls, identifier, descriptor, new FetchGraphWrapper(), forceEager, false); + } + + public LoadingParameters(Class cls, URI identifier, Descriptor descriptor, boolean forceEager, + boolean bypassCache) { + this(cls, identifier, descriptor, new FetchGraphWrapper(), forceEager, bypassCache); } public LoadingParameters withBypassCache() { - return new LoadingParameters<>(entityClass, identifier, descriptor, forceEager, true); + return new LoadingParameters<>(entityClass, identifier, descriptor, fetchGraph, forceEager, true); } } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/OWLClassL.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/OWLClassL.java index b5f71f453..e3b99b36e 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/OWLClassL.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/OWLClassL.java @@ -34,20 +34,20 @@ public class OWLClassL implements HasUri { @Sequence(type = SequenceType.simple) @OWLObjectProperty(iri = "http://krizik.felk.cvut.cz/ontologies/jopa/attributes#C-hasSimpleSequence") @ParticipationConstraints({ - @ParticipationConstraint(min = 1, owlObjectIRI = "http://krizik.felk.cvut.cz/ontologies/jopa/entities#OWLClassA") + @ParticipationConstraint(min = 1, owlObjectIRI = Vocabulary.c_OwlClassA) }) private List simpleList; @Sequence(type = SequenceType.referenced) @OWLObjectProperty(iri = "http://krizik.felk.cvut.cz/ontologies/jopa/attributes#C-hasReferencedSequence") @ParticipationConstraints({ - @ParticipationConstraint(max = 2, owlObjectIRI = "http://krizik.felk.cvut.cz/ontologies/jopa/entities#OWLClassA") + @ParticipationConstraint(max = 2, owlObjectIRI = Vocabulary.c_OwlClassA) }) private List referencedList; - @OWLObjectProperty(iri = "http://krizik.felk.cvut.cz/ontologies/jopa/attributes#hasA") + @OWLObjectProperty(iri = Vocabulary.p_h_hasA) @ParticipationConstraints({ - @ParticipationConstraint(min = 1, max = 5, owlObjectIRI = "http://krizik.felk.cvut.cz/ontologies/jopa/entities#OWLClassA") + @ParticipationConstraint(min = 1, max = 5, owlObjectIRI = Vocabulary.c_OwlClassA) }) private Set set; diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/OWLClassN.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/OWLClassN.java index 62fdd4190..1ff3075b6 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/OWLClassN.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/OWLClassN.java @@ -32,14 +32,14 @@ public class OWLClassN { @Id(generated = true) private String id; - @OWLAnnotationProperty(iri = "http://krizik.felk.cvut.cz/ontologies/jopa/attributes#annotationProperty") + @OWLAnnotationProperty(iri = Vocabulary.ATTRIBUTE_BASE + "annotationProperty") private String annotationProperty; - @OWLAnnotationProperty(iri = "http://krizik.felk.cvut.cz/ontologies/jopa/attributes#annotationUri") + @OWLAnnotationProperty(iri = Vocabulary.ATTRIBUTE_BASE + "annotationUri") private URI annotationUri; @ParticipationConstraints(nonEmpty = true) - @OWLDataProperty(iri = "http://krizik.felk.cvut.cz/ontologies/jopa/attributes#A-stringAttribute") + @OWLDataProperty(iri = Vocabulary.ATTRIBUTE_BASE + "A-stringAttribute") private String stringAttribute; @OWLAnnotationProperty(iri = DC.Terms.SOURCE) @@ -49,6 +49,13 @@ public class OWLClassN { @Properties(fetchType = FetchType.LAZY) private Map> properties; + public OWLClassN() { + } + + public OWLClassN(String id) { + this.id = id; + } + public String getId() { return id; } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/Person.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/Person.java index ae9d40fa9..de849ec05 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/Person.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/Person.java @@ -50,6 +50,13 @@ public class Person implements HasUri { @Types private Set types; + public Person() { + } + + public Person(URI uri) { + this.uri = uri; + } + public URI getUri() { return uri; } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/Phone.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/Phone.java index 7125c9d68..28e454c03 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/Phone.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/Phone.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.utils.IdentifierTransformer; import java.net.URI; @@ -34,4 +35,46 @@ public class Phone { @OWLDataProperty(iri = Vocabulary.p_p_phoneBrand, simpleLiteral = true) private String brand; + + public Phone() { + } + + public Phone(URI uri, String number, String brand) { + this.uri = uri; + this.number = number; + this.brand = brand; + } + + public URI getUri() { + return uri; + } + + public void setUri(URI uri) { + this.uri = uri; + } + + public String getNumber() { + return number; + } + + public void setNumber(String number) { + this.number = number; + } + + public String getBrand() { + return brand; + } + + public void setBrand(String brand) { + this.brand = brand; + } + + @Override + public String toString() { + return "Phone{" + + IdentifierTransformer.stringifyIri(uri) + + ", number='" + number + '\'' + + ", brand='" + brand + '\'' + + '}'; + } } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/utils/Generators.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/utils/Generators.java index c98b208c2..aeece9623 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/utils/Generators.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/utils/Generators.java @@ -163,9 +163,9 @@ public static List generateInstances(int size) { public static Set generateTypes(int count) { final Set types = new HashSet<>(count); - final int randomBase = RAND.nextInt(10000); + final int base = RAND.nextInt(10000); for (int i = 0; i < count; i++) { - types.add(Vocabulary.CLASS_BASE + (randomBase + i * 2)); + types.add(Vocabulary.CLASS_BASE + base + i); } return types; } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/utils/MetamodelFactory.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/utils/MetamodelFactory.java index ff8ebc437..c612e9944 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/utils/MetamodelFactory.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/environment/utils/MetamodelFactory.java @@ -170,7 +170,8 @@ public static void initOWLClassAMocks(IdentifiableEntityType etMock, initIdentifier(etMock, idMock, OWLClassA.class.getDeclaredField("uri"), false); } - private static void initEntityType(IdentifiableEntityType et, Class cls, EntityLifecycleListenerManager listenerManager) { + private static void initEntityType(IdentifiableEntityType et, Class cls, + EntityLifecycleListenerManager listenerManager) { when(et.getJavaType()).thenReturn(cls); when(et.getInstantiableJavaType()).thenReturn((Class) instantiableTypeGenerator.generate(cls)); when(et.getPersistenceType()).thenReturn(Type.PersistenceType.ENTITY); @@ -347,7 +348,7 @@ public static void initOWLClassCMocks(IdentifiableEntityType etMock, when(rdfCollectionMock.getName()).thenReturn(OWLClassC.getRdfCollectionField().getName()); when(etMock.getFieldSpecification(rdfCollectionMock.getName())).thenReturn(rdfCollectionMock); when(rdfCollectionMock.getIRI()).thenReturn(IRI.create(OWLClassC.getRdfCollectionField() - .getAnnotation(OWLObjectProperty.class).iri())); + .getAnnotation(OWLObjectProperty.class).iri())); when(rdfCollectionMock.getBindableJavaType()).thenReturn(OWLClassA.class); when(rdfCollectionMock.getElementType()).thenReturn(etAMock); when(rdfCollectionMock.getPersistentAttributeType()).thenReturn(Attribute.PersistentAttributeType.OBJECT); @@ -460,7 +461,7 @@ public static void initOWLClassHMocks(IdentifiableEntityType etMock, SingularAttributeImpl clsGMock, IdentifiableEntityType etAMock, IdentifiableEntityType etGMock, Identifier idMock) throws NoSuchFieldException, SecurityException { - initEntityType(etMock, OWLClassH.class, EntityLifecycleListenerManager.empty()); + initEntityType(etMock, OWLClassH.class, EntityLifecycleListenerManager.empty()); when(etMock.getAttributes()).thenReturn(Set.of(clsAMock, clsGMock)); when(etMock.getFieldSpecifications()).thenReturn(Set.of(clsAMock, clsGMock, idMock)); @@ -474,7 +475,7 @@ public static void initOWLClassHMocks(IdentifiableEntityType etMock, public static void initOWLClassIMocks(IdentifiableEntityType etMock, SingularAttributeImpl aAttMock, IdentifiableEntityType etAMock, Identifier idMock) throws NoSuchFieldException, SecurityException { - initEntityType(etMock, OWLClassI.class, EntityLifecycleListenerManager.empty()); + initEntityType(etMock, OWLClassI.class, EntityLifecycleListenerManager.empty()); when(etMock.getAttributes()).thenReturn(Collections.singleton(aAttMock)); when(etMock.getFieldSpecifications()).thenReturn(Set.of(aAttMock, idMock)); @@ -486,33 +487,33 @@ public static void initOWLClassIMocks(IdentifiableEntityType etMock, public static void initOWLClassJMocks(IdentifiableEntityType etMock, AbstractPluralAttribute setAMock, IdentifiableEntityType etAMock, Identifier idMock) throws NoSuchFieldException, SecurityException { - initEntityType(etMock, OWLClassJ.class,EntityLifecycleListenerManager.empty()); + initEntityType(etMock, OWLClassJ.class, EntityLifecycleListenerManager.empty()); when(etMock.getAttributes()).thenReturn(Collections.singleton(setAMock)); when(etMock.getFieldSpecifications()).thenReturn(Set.of(setAMock, idMock)); initAttribute(etMock, setAMock, new AttributeInfo(OWLClassJ.getOwlClassAField(), Attribute.PersistentAttributeType.OBJECT).collectionType(CollectionType.SET) .elementType(OWLClassA.class).valueType(etAMock) - .constraints(new ParticipationConstraint() { - @Override - public Class annotationType() { - return ParticipationConstraint.class; - } - - @Override - public String owlObjectIRI() { - return ""; - } - - @Override - public int min() { - return 1; - } - - @Override - public int max() { - return -1; - } - })); + .constraints(new ParticipationConstraint() { + @Override + public Class annotationType() { + return ParticipationConstraint.class; + } + + @Override + public String owlObjectIRI() { + return ""; + } + + @Override + public int min() { + return 1; + } + + @Override + public int max() { + return -1; + } + })); initIdentifier(etMock, idMock, OWLClassJ.class.getDeclaredField("uri"), false); } @@ -925,8 +926,8 @@ public static void initPhoneMocks(IdentifiableEntityType etMock, Singular SingularAttributeImpl phoneBrandAttMock, Identifier idMock) throws NoSuchFieldException, SecurityException { initEntityType(etMock, Phone.class, EntityLifecycleListenerManager.empty()); - when(etMock.getAttributes()).thenReturn(Collections.singleton(phoneNumberAttMock)); - when(etMock.getFieldSpecifications()).thenReturn(Set.of(phoneNumberAttMock, idMock)); + when(etMock.getAttributes()).thenReturn(Set.of(phoneNumberAttMock, phoneBrandAttMock)); + when(etMock.getFieldSpecifications()).thenReturn(Set.of(phoneNumberAttMock, phoneBrandAttMock, idMock)); initAttribute(etMock, phoneNumberAttMock, new AttributeInfo(Phone.class.getDeclaredField("number"), Attribute.PersistentAttributeType.DATA)); initAttribute(etMock, phoneBrandAttMock, new AttributeInfo(Phone.class.getDeclaredField("brand"), Attribute.PersistentAttributeType.DATA)); initIdentifier(etMock, idMock, Phone.class.getDeclaredField("uri"), false); @@ -943,8 +944,8 @@ public static void initPersonMocks(IdentifiableEntityType etMock, Identifier idMock) throws NoSuchFieldException, SecurityException { initEntityType(etMock, Person.class, EntityLifecycleListenerManager.empty()); initIdentifier(etMock, idMock, Person.class.getDeclaredField("uri"), false); - when(etMock.getAttributes()).thenReturn(Set.of(usernameAttMock, genderAttMock, ageAttMock, phoneAttMock)); - when(etMock.getFieldSpecifications()).thenReturn(Set.of(usernameAttMock, genderAttMock, ageAttMock, phoneAttMock, idMock)); + when(etMock.getAttributes()).thenReturn(Set.of(firstNameAttMock, lastNameAttMock, usernameAttMock, genderAttMock, ageAttMock, phoneAttMock)); + when(etMock.getFieldSpecifications()).thenReturn(Set.of(firstNameAttMock, lastNameAttMock, usernameAttMock, genderAttMock, ageAttMock, phoneAttMock, idMock)); initAttribute(etMock, firstNameAttMock, new AttributeInfo(Person.class.getDeclaredField("firstName"), Attribute.PersistentAttributeType.DATA).language(null)); diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/EntityManagerImplTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/EntityManagerImplTest.java index 91a44a079..6c658cfcb 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/EntityManagerImplTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/EntityManagerImplTest.java @@ -119,7 +119,7 @@ void setUp() throws Exception { when(emfMock.getMetamodel()).thenReturn(metamodelMock); when(emfMock.getServerSession()).thenReturn(serverSessionMock); final NamespaceResolver nsResolver = new NamespaceResolver(); - this.descriptorFactory = new DefaultEntityDescriptorFactory(metamodelMock, nsResolver); + this.descriptorFactory = spy(new DefaultEntityDescriptorFactory(metamodelMock, nsResolver)); when(metamodelMock.getNamespaceResolver()).thenReturn(nsResolver); this.mocks = new MetamodelMocks(); mocks.setMocks(metamodelMock); @@ -739,4 +739,28 @@ void cascadeRemoveTriggersLazyLoadingAndCascadesRemoval() { verify(connectorMock).remove(eq(toRemove.getUri()), eq(OWLClassJ.class), any(Descriptor.class)); verify(connectorMock).remove(eq(a.getUri()), eq(OWLClassA.class), any(Descriptor.class)); } + + @Test + void createTypedNativeQuerySetsDescriptorWhenResultTypeIsEntity() { + final EntityDescriptor d = new EntityDescriptor(Generators.createIndividualIdentifier()); + when(descriptorFactory.createDescriptor(OWLClassA.class)).thenReturn(d); + final TypedQueryImpl query = em.createNativeQuery("SELECT * WHERE { ?x a ?type . }", OWLClassA.class); + verify(descriptorFactory).createDescriptor(OWLClassA.class); + assertEquals(d, query.getDescriptor()); + } + + @Test + void createTypedQuerySetsDescriptorWhenResultTypeIsEntity() { + final EntityDescriptor d = new EntityDescriptor(Generators.createIndividualIdentifier()); + when(descriptorFactory.createDescriptor(OWLClassA.class)).thenReturn(d); + final TypedQueryImpl query = em.createQuery("SELECT a FROM OWLClassA a", OWLClassA.class); + verify(descriptorFactory).createDescriptor(OWLClassA.class); + assertEquals(d, query.getDescriptor()); + } + + @Test + void createTypedNativeQueryDoesNotSetDescriptorWhenResultTypeIsNotEntity() { + em.createNativeQuery("ASK WHERE { ?x a ?type } ", Boolean.class); + verify(descriptorFactory, never()).createDescriptor(Integer.class); + } } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/MetamodelImplInheritanceTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/MetamodelImplInheritanceTest.java index ff8c8ffd4..876639aaf 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/MetamodelImplInheritanceTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/MetamodelImplInheritanceTest.java @@ -204,14 +204,16 @@ void buildingMetamodelSupportsReferenceFromParentEntityToSubEntity() { assertNotNull(et.getIdentifier()); } + @TestLocal @MappedSuperclass - private static class ParentWithId { + public static class ParentWithId { @Id private URI id; } + @TestLocal @MappedSuperclass - private static class ParentWithCircular extends ParentWithId { + public static class ParentWithCircular extends ParentWithId { @OWLObjectProperty(iri = Vocabulary.ATTRIBUTE_BASE + "hasChild") private Set children; 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/model/QueryTestBase.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/QueryTestBase.java index 617e077dd..e3b86acd8 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/QueryTestBase.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/QueryTestBase.java @@ -303,13 +303,13 @@ void setParameterSupportsUsingEntityAsParameterValue() throws Exception { } @Test - void executeQueryAppliesQueryHints() throws Exception { + void executeQueryAppliesQueryHints() { final AbstractQuery q = createQuery(SELECT_QUERY, OWLClassA.class); final String hintName = "jopa.query.testHint"; final QueryHintsHandler.Hint hint = spy(new TestHint(hintName)); QueryHintsHandler.Hint.registerHint(hint); q.setHint(hintName, true); - q.executeQuery((r) -> {}); + q.getResultList(); verify(hint).apply(true, q, statementMock); } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/TypedQueryImplTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/TypedQueryImplTest.java index 348ae656c..12f909c8e 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/TypedQueryImplTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/TypedQueryImplTest.java @@ -34,6 +34,7 @@ import cz.cvut.kbss.jopa.query.QueryHints; import cz.cvut.kbss.jopa.query.QueryParameter; import cz.cvut.kbss.jopa.query.parameter.ParameterValueFactory; +import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingConfigGroup; import cz.cvut.kbss.jopa.vocabulary.RDF; import cz.cvut.kbss.ontodriver.exception.OntoDriverException; import org.junit.jupiter.api.BeforeEach; @@ -458,7 +459,8 @@ void optimizeEntityResultLoadingHintAppliesEntityLoadingOptimizer() throws Excep when(resultRow.getObject(1, URI.class)).thenReturn(URI.create(RDF.TYPE)); when(resultRow.getObject(2)).thenReturn(URI.create(Vocabulary.c_OwlClassB)); when(resultRow.getColumnCount()).thenReturn(3); - when(uowMock.readObjectFromAxioms(eq(OWLClassB.class), anyCollection(), any())).thenReturn(entity); + when(uowMock.readObjectFromAxioms(eq(OWLClassB.class), anyCollection(), + eq(new AxiomBasedLoadingConfigGroup<>(entity.getUri(), new EntityDescriptor())))).thenReturn(entity); final MetamodelImpl metamodel = mock(MetamodelImpl.class); when(uowMock.getMetamodel()).thenReturn(metamodel); when(uowMock.isEntityType(OWLClassB.class)).thenReturn(true); @@ -474,6 +476,10 @@ void optimizeEntityResultLoadingHintAppliesEntityLoadingOptimizer() throws Excep @Test void getResultListIsAbleToMapProjectedVariablesToEntityAttributes() throws Exception { final OWLClassA entity = Generators.generateOwlClassAInstance(); + final MetamodelImpl metamodel = mock(MetamodelImpl.class); + when(uowMock.getMetamodel()).thenReturn(metamodel); + final MetamodelMocks mocks = new MetamodelMocks(); + mocks.setMocks(metamodel); final TypedQuery sut = create("SELECT ?x ?stringAttribute ?types WHERE {" + "?x a " + stringifyIri(Vocabulary.c_OwlClassA) + " ;" + stringifyIri(Vocabulary.p_a_stringAttribute) + " ?stringAttribute ;" + @@ -483,16 +489,15 @@ void getResultListIsAbleToMapProjectedVariablesToEntityAttributes() throws Excep when(resultSetIterator.next()).thenReturn(resultRow); when(resultRow.getColumnNames()).thenReturn(List.of("x", "stringAttribute", "types")); when(resultRow.getObject(0, URI.class)).thenReturn(entity.getUri()); + when(resultRow.isBound("x")).thenReturn(true); + when(resultRow.getObject("x", URI.class)).thenReturn(entity.getUri()); when(resultRow.isBound("stringAttribute")).thenReturn(true); when(resultRow.getObject("stringAttribute")).thenReturn(entity.getStringAttribute()); when(resultRow.isBound("types")).thenReturn(true); - when(resultRow.getObject("types")).thenReturn(URI.create(Vocabulary.c_OwlClassA)); + when(resultRow.getString("types")).thenReturn(Vocabulary.c_OwlClassA); when(resultRow.getColumnCount()).thenReturn(3); - when(uowMock.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), any())).thenReturn(entity); - final MetamodelImpl metamodel = mock(MetamodelImpl.class); - when(uowMock.getMetamodel()).thenReturn(metamodel); - final MetamodelMocks mocks = new MetamodelMocks(); - mocks.setMocks(metamodel); + when(uowMock.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), + eq(new AxiomBasedLoadingConfigGroup<>(entity.getUri(), new EntityDescriptor())))).thenReturn(entity); final List result = sut.getResultList(); assertEquals(List.of(entity), result); diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/metamodel/MetamodelBuilderTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/metamodel/MetamodelBuilderTest.java index ed7679eed..97c7109aa 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/metamodel/MetamodelBuilderTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/model/metamodel/MetamodelBuilderTest.java @@ -396,12 +396,12 @@ void buildMetamodelSupportsMultipleInheritanceInterfaceOnly() { @TestLocal @OWLClass(iri = Vocabulary.CLASS_BASE + "AParentI") - private interface AParentI { + public interface AParentI { } @TestLocal @OWLClass(iri = Vocabulary.CLASS_BASE + "BParentI") - private interface BParentI { + public interface BParentI { } @TestLocal @@ -742,7 +742,7 @@ void buildMetamodelFinishesBuildingEntityTypes() { } @Test - void buildMetamodelEagerlyGeneratesLazyLoadingEntityProxiesForEntityClassesReferencedByLazilyFetchAttributes() { + void buildMetamodelEagerlyGeneratesLazyLoadingEntityProxiesForEntityClassesUsedAsAttributeValues() { when(finderMock.getEntities()).thenReturn(Set.of(OWLClassA.class, ClassWithLazySingularAttribute.class)); builder.buildMetamodel(finderMock); assertTrue(builder.getLazyLoadingEntityProxyClasses().containsKey(OWLClassA.class)); diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/DataPropertyFieldStrategyTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/DataPropertyFieldStrategyTest.java index 8b6ebc1fc..31648b149 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/DataPropertyFieldStrategyTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/DataPropertyFieldStrategyTest.java @@ -21,6 +21,7 @@ import cz.cvut.kbss.jopa.environment.utils.MetamodelMocks; import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.descriptors.EntityDescriptor; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -56,7 +57,7 @@ void setUp() throws Exception { void getLanguagesUsesAttributeConfiguredLanguageWhenDescriptorDoesNotSpecifyAny() { final DataPropertyFieldStrategy sut = (DataPropertyFieldStrategy) FieldStrategy .createFieldStrategy(metamodelMocks.forOwlClassA().entityType(), - metamodelMocks.forOwlClassA().stringAttribute(), descriptor, mapperMock); + metamodelMocks.forOwlClassA().stringAttribute(), new ObjectGraphInfo(descriptor), mapperMock); assertTrue(metamodelMocks.forOwlClassA().stringAttribute().hasLanguage()); assertEquals(Generators.LANG, sut.getLanguage()); } @@ -67,7 +68,7 @@ void getLanguageRetrievesAttributeLanguageFromEntityDescriptor() { descriptor.setAttributeLanguage(metamodelMocks.forOwlClassA().stringAttribute(), lang); final DataPropertyFieldStrategy sut = (DataPropertyFieldStrategy) FieldStrategy .createFieldStrategy(metamodelMocks.forOwlClassA().entityType(), - metamodelMocks.forOwlClassA().stringAttribute(), descriptor, mapperMock); + metamodelMocks.forOwlClassA().stringAttribute(), new ObjectGraphInfo(descriptor), mapperMock); assertEquals(lang, sut.getLanguage()); } @@ -75,7 +76,7 @@ void getLanguageRetrievesAttributeLanguageFromEntityDescriptor() { void getLanguageReturnsNullForSimpleLiteralAttribute() { final DataPropertyFieldStrategy sut = (DataPropertyFieldStrategy) FieldStrategy .createFieldStrategy(metamodelMocks.forOwlClassM().entityType(), - metamodelMocks.forOwlClassM().simpleLiteralAttribute(), descriptor, mapperMock); + metamodelMocks.forOwlClassM().simpleLiteralAttribute(), new ObjectGraphInfo(descriptor), mapperMock); assertNull(sut.getLanguage()); } } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/DefaultInstanceLoaderTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/DefaultInstanceLoaderTest.java index 7272cc8fa..9b545cf66 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/DefaultInstanceLoaderTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/DefaultInstanceLoaderTest.java @@ -24,6 +24,7 @@ import cz.cvut.kbss.jopa.environment.utils.MetamodelMocks; import cz.cvut.kbss.jopa.exceptions.StorageAccessException; import cz.cvut.kbss.jopa.model.metamodel.IdentifiableEntityType; +import cz.cvut.kbss.jopa.sessions.util.FetchGraphWrapper; import cz.cvut.kbss.jopa.sessions.util.LoadStateDescriptorRegistry; import cz.cvut.kbss.jopa.sessions.util.LoadingParameters; import cz.cvut.kbss.ontodriver.descriptor.AxiomDescriptor; @@ -101,7 +102,8 @@ void setUp() throws Exception { void testLoadEntity() throws Exception { final Collection> entityAAxioms = Generators.generateAxiomsForOWLClassA(IDENTIFIER); when(connectionMock.find(axiomDescriptor)).thenReturn(entityAAxioms); - when(entityConstructorMock.reconstructEntity(new EntityConstructor.EntityConstructionParameters<>(IDENTIFIER, etAMock, descriptor, false), entityAAxioms)) + when(entityConstructorMock.reconstructEntity(new EntityConstructor.EntityConstructionParameters<>(IDENTIFIER, etAMock, descriptor, new FetchGraphWrapper(), + false), entityAAxioms)) .thenReturn(entityA); final OWLClassA res = instanceLoader.loadEntity(loadingParameters); @@ -132,7 +134,7 @@ void loadEntityBypassesCacheWhenConfiguredTo() throws Exception { .thenReturn(axiomDescriptor); final Collection> entityAAxioms = Generators.generateAxiomsForOWLClassA(IDENTIFIER); when(connectionMock.find(axiomDescriptor)).thenReturn(entityAAxioms); - when(entityConstructorMock.reconstructEntity(new EntityConstructor.EntityConstructionParameters<>(IDENTIFIER, etAMock, descriptor, false), entityAAxioms)) + when(entityConstructorMock.reconstructEntity(new EntityConstructor.EntityConstructionParameters<>(IDENTIFIER, etAMock, descriptor, new FetchGraphWrapper(), false), entityAAxioms)) .thenReturn(entityA); final OWLClassA res = instanceLoader.loadEntity(loadingParameters); assertNotNull(res); @@ -147,7 +149,7 @@ void loadEntityReloadsQueryAttributesWhenInstanceIsRetrievedFromCache() { final OWLClassA res = instanceLoader.loadEntity(loadingParameters); assertEquals(entityA, res); verify(entityConstructorMock).populateQueryAttributes(entityA, etAMock); - verify(entityConstructorMock, never()).reconstructEntity(eq(new EntityConstructor.EntityConstructionParameters<>(loadingParameters.identifier(), etAMock, descriptor, false)), anyCollection()); + verify(entityConstructorMock, never()).reconstructEntity(eq(new EntityConstructor.EntityConstructionParameters<>(loadingParameters.identifier(), etAMock, descriptor, new FetchGraphWrapper(), false)), anyCollection()); } @Test diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/EntityConstructorPluralAttributesTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/EntityConstructorPluralAttributesTest.java index 5dc81289b..18cec4ca3 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/EntityConstructorPluralAttributesTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/EntityConstructorPluralAttributesTest.java @@ -24,8 +24,10 @@ import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.descriptors.EntityDescriptor; import cz.cvut.kbss.jopa.model.metamodel.ListAttribute; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.query.sparql.SparqlQueryFactory; import cz.cvut.kbss.jopa.sessions.UnitOfWork; +import cz.cvut.kbss.jopa.sessions.util.FetchGraphWrapper; import cz.cvut.kbss.jopa.sessions.util.LoadStateDescriptorRegistry; import cz.cvut.kbss.jopa.utils.Configuration; import cz.cvut.kbss.ontodriver.descriptor.SimpleListDescriptor; @@ -111,7 +113,8 @@ public void reconstructsEntityWithSimpleList() { final OWLClassC res = constructor .reconstructEntity(new EntityConstructor.EntityConstructionParameters<>(ID, metamodelMocks.forOwlClassC() - .entityType(), descriptor, true), + .entityType(), descriptor, new FetchGraphWrapper(), + true), axioms); assertNotNull(res); @@ -123,7 +126,7 @@ public void reconstructsEntityWithSimpleList() { private void prepareMapperMockForSimpleListLoad() { for (Entry e : LIST_CONTENT.entrySet()) { - when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(e.getKey()), any(Descriptor.class))).thenReturn(e.getValue()); + when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(e.getKey()), any(ObjectGraphInfo.class))).thenReturn(e.getValue()); } final Collection> listAxioms = initSimpleListAxioms(); when(mapperMock.loadSimpleList(any(SimpleListDescriptor.class))).thenReturn(listAxioms); diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/EntityConstructorQueryAttributesTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/EntityConstructorQueryAttributesTest.java index 22144f00e..cb9553147 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/EntityConstructorQueryAttributesTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/EntityConstructorQueryAttributesTest.java @@ -26,6 +26,7 @@ import cz.cvut.kbss.jopa.model.annotations.OWLObjectProperty; import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.descriptors.EntityDescriptor; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.query.sparql.SparqlQueryFactory; import cz.cvut.kbss.jopa.sessions.UnitOfWork; import cz.cvut.kbss.jopa.sessions.util.LoadStateDescriptorRegistry; @@ -141,7 +142,7 @@ void testReconstructEntityWithManagedTypeQueryAttribute() throws Exception { entityA.setUri(identifierTwo); entityA.setStringAttribute(stringValue); - when(mapperMock.getEntityFromCacheOrOntology(OWLClassA.class, identifierTwo, fieldDesc)) + when(mapperMock.getEntityFromCacheOrOntology(OWLClassA.class, identifierTwo, new ObjectGraphInfo(fieldDesc))) .thenReturn(entityA); doReturn(typedQueryMock) @@ -155,7 +156,7 @@ void testReconstructEntityWithManagedTypeQueryAttribute() throws Exception { .entityType(), descriptor), axioms); assertNotNull(res); assertEquals(IDENTIFIER, res.getUri()); - verify(mapperMock).getEntityFromCacheOrOntology(OWLClassA.class, identifierTwo, fieldDesc); + verify(mapperMock).getEntityFromCacheOrOntology(OWLClassA.class, identifierTwo, new ObjectGraphInfo(fieldDesc)); assertNotNull(res.getEntityAttribute()); assertEquals(identifierTwo, res.getEntityAttribute().getUri()); assertEquals(stringValue, res.getEntityAttribute().getStringAttribute()); diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/EntityConstructorTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/EntityConstructorTest.java index 3a8147aaa..cab9cf088 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/EntityConstructorTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/EntityConstructorTest.java @@ -31,8 +31,11 @@ import cz.cvut.kbss.jopa.environment.utils.MetamodelMocks; import cz.cvut.kbss.jopa.exceptions.CardinalityConstraintViolatedException; import cz.cvut.kbss.jopa.exceptions.IntegrityConstraintViolatedException; +import cz.cvut.kbss.jopa.model.EntityGraph; +import cz.cvut.kbss.jopa.model.EntityGraphImpl; import cz.cvut.kbss.jopa.model.JOPAPersistenceProperties; import cz.cvut.kbss.jopa.model.LoadState; +import cz.cvut.kbss.jopa.model.MetamodelImpl; import cz.cvut.kbss.jopa.model.SequencesVocabulary; import cz.cvut.kbss.jopa.model.annotations.FetchType; import cz.cvut.kbss.jopa.model.annotations.OWLAnnotationProperty; @@ -42,9 +45,11 @@ import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.descriptors.EntityDescriptor; import cz.cvut.kbss.jopa.model.metamodel.IdentifiableEntityType; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.query.sparql.SparqlQueryFactory; import cz.cvut.kbss.jopa.sessions.UnitOfWork; import cz.cvut.kbss.jopa.sessions.descriptor.LoadStateDescriptor; +import cz.cvut.kbss.jopa.sessions.util.FetchGraphWrapper; import cz.cvut.kbss.jopa.sessions.util.LoadStateDescriptorRegistry; import cz.cvut.kbss.jopa.utils.Configuration; import cz.cvut.kbss.jopa.vocabulary.DC; @@ -75,6 +80,7 @@ import java.util.Map.Entry; import java.util.Set; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -117,6 +123,9 @@ void setUp() throws Exception { when(mapperMock.getUow()).thenReturn(uowMock); when(uowMock.sparqlQueryFactory()).thenReturn(queryFactoryMock); this.mocks = new MetamodelMocks(); + final MetamodelImpl metamodel = mock(MetamodelImpl.class); + mocks.setMocks(metamodel); + when(uowMock.getMetamodel()).thenReturn(metamodel); this.descriptor = new EntityDescriptor(); this.constructor = new EntityConstructor(mapperMock, loadStateRegistry); } @@ -168,7 +177,7 @@ private Set> getTypesAxiomsForOwlClassA() { static EntityConstructor.EntityConstructionParameters constructionConfig(URI id, IdentifiableEntityType et, Descriptor descriptor) { - return new EntityConstructor.EntityConstructionParameters<>(id, et, descriptor, false); + return new EntityConstructor.EntityConstructionParameters<>(id, et, descriptor, new FetchGraphWrapper(), false); } @Test @@ -250,12 +259,12 @@ void testReconstructEntityWithObjectProperty() { final OWLClassA entityA = new OWLClassA(); entityA.setUri(ID_TWO); entityA.setStringAttribute(STRING_ATT); - when(mapperMock.getEntityFromCacheOrOntology(OWLClassA.class, ID_TWO, fieldDesc)).thenReturn(entityA); + when(mapperMock.getEntityFromCacheOrOntology(OWLClassA.class, ID_TWO, new ObjectGraphInfo(fieldDesc))).thenReturn(entityA); final OWLClassD res = constructor.reconstructEntity(constructionConfig(ID, mocks.forOwlClassD() .entityType(), descriptor), axiomsD); assertNotNull(res); assertEquals(ID, res.getUri()); - verify(mapperMock).getEntityFromCacheOrOntology(OWLClassA.class, ID_TWO, fieldDesc); + verify(mapperMock).getEntityFromCacheOrOntology(OWLClassA.class, ID_TWO, new ObjectGraphInfo(fieldDesc)); assertNotNull(res.getOwlClassA()); assertEquals(ID_TWO, res.getOwlClassA().getUri()); assertEquals(STRING_ATT, res.getOwlClassA().getStringAttribute()); @@ -327,7 +336,7 @@ void reconstructsEntityWithObjectPropertyWhereRangeDoesNotMatch() { final Descriptor fieldDescriptor = new EntityDescriptor(); descriptor.addAttributeDescriptor(mocks.forOwlClassD().owlClassAAtt(), fieldDescriptor); // The property value hasn't the corresponding type, so it cannot be returned by mapper - when(mapperMock.getEntityFromCacheOrOntology(OWLClassA.class, ID_TWO, fieldDescriptor)).thenReturn(null); + when(mapperMock.getEntityFromCacheOrOntology(OWLClassA.class, ID_TWO, new ObjectGraphInfo(fieldDescriptor))).thenReturn(null); final OWLClassD res = constructor.reconstructEntity(constructionConfig(ID, mocks.forOwlClassD() .entityType(), descriptor), axioms); assertNotNull(res); @@ -346,8 +355,8 @@ void throwsExceptionWhenObjectPropertyCardinalityRestrictionIsNotMet() { entityThree.setUri(pkThree); final Descriptor fieldDescriptor = new EntityDescriptor(); descriptor.addAttributeDescriptor(mocks.forOwlClassD().owlClassAAtt(), fieldDescriptor); - when(mapperMock.getEntityFromCacheOrOntology(OWLClassA.class, ID_TWO, fieldDescriptor)).thenReturn(entityA); - when(mapperMock.getEntityFromCacheOrOntology(OWLClassA.class, pkThree, fieldDescriptor)) + when(mapperMock.getEntityFromCacheOrOntology(OWLClassA.class, ID_TWO, new ObjectGraphInfo(fieldDescriptor))).thenReturn(entityA); + when(mapperMock.getEntityFromCacheOrOntology(OWLClassA.class, pkThree, new ObjectGraphInfo(fieldDescriptor))) .thenReturn(entityThree); assertThrows(CardinalityConstraintViolatedException.class, () -> constructor.reconstructEntity(constructionConfig(ID, mocks.forOwlClassD() @@ -371,8 +380,8 @@ void testSetFieldValue_DataProperty() throws Exception { @Test void testSetFieldValue_ObjectProperty() { final Set> axioms = new HashSet<>(); - final EntityDescriptor fieldDesc = mock(EntityDescriptor.class); - descriptor.addAttributeDescriptor(mocks.forOwlClassD().owlClassAAtt(), fieldDesc); + final EntityDescriptor fieldDescriptor = mock(EntityDescriptor.class); + descriptor.addAttributeDescriptor(mocks.forOwlClassD().owlClassAAtt(), fieldDescriptor); axioms.add(createObjectPropertyAxiomForD()); final OWLClassD entityD = new OWLClassD(); entityD.setUri(ID); @@ -380,14 +389,14 @@ void testSetFieldValue_ObjectProperty() { final OWLClassA entityA = new OWLClassA(); entityA.setUri(ID_TWO); entityA.setStringAttribute(STRING_ATT); - when(mapperMock.getEntityFromCacheOrOntology(OWLClassA.class, ID_TWO, fieldDesc)) + when(mapperMock.getEntityFromCacheOrOntology(OWLClassA.class, ID_TWO, new ObjectGraphInfo(fieldDescriptor))) .thenReturn(entityA); constructor.setFieldValue(entityD, mocks.forOwlClassD().owlClassAAtt(), axioms, mocks.forOwlClassD().entityType(), descriptor); assertNotNull(entityD.getOwlClassA()); assertEquals(ID_TWO, entityD.getOwlClassA().getUri()); - verify(mapperMock).getEntityFromCacheOrOntology(OWLClassA.class, ID_TWO, fieldDesc); + verify(mapperMock).getEntityFromCacheOrOntology(OWLClassA.class, ID_TWO, new ObjectGraphInfo(fieldDescriptor)); } @Test @@ -454,7 +463,7 @@ private Set initEntities(Descriptor desc) { a.setUri(URI.create("http://krizik.felk.cvut.cz/entityA" + i)); a.setStringAttribute(STRING_ATT); entities.add(a); - when(mapperMock.getEntityFromCacheOrOntology(OWLClassA.class, a.getUri(), desc)) + when(mapperMock.getEntityFromCacheOrOntology(OWLClassA.class, a.getUri(), new ObjectGraphInfo(desc))) .thenReturn(a); } return entities; @@ -513,7 +522,8 @@ void reconstructsEntityWithDataPropertiesOfBasicTypesAndStringIdentifier() throw assertEquals(character, res.getCharacterAttribute()); } - private Collection> createAxiomsForValues(Boolean b, Integer i, Long lng, Double d, Date date, Character character, + private Collection> createAxiomsForValues(Boolean b, Integer i, Long lng, Double d, Date date, + Character character, OWLClassM.Severity severity) throws Exception { final Collection> axioms = new ArrayList<>(); @@ -546,7 +556,8 @@ private Collection> createAxiomsForValues(Boolean b, Integer i, Long ln new Value<>(date))); } if (character != null) { - final String characterAttIri = OWLClassM.getCharacterAttributeField().getAnnotation(OWLDataProperty.class).iri(); + final String characterAttIri = OWLClassM.getCharacterAttributeField().getAnnotation(OWLDataProperty.class) + .iri(); axioms.add( new AxiomImpl<>(ID_RESOURCE, Assertion.createDataPropertyAssertion(URI.create(characterAttIri), false), new Value<>(character.toString()))); @@ -622,7 +633,7 @@ private Set> initInvalidFieldValuesForICValidation() throws NoSuchField for (int i = 0; i < max + 1; i++) { final URI uri = URI.create("http://value" + i); listAxioms.add(new AxiomImpl<>(ID_RESOURCE, nodeContent, new Value<>(NamedResource.create(uri)))); - when(mapperMock.getEntityFromCacheOrOntology(OWLClassA.class, uri, attDescriptor)) + when(mapperMock.getEntityFromCacheOrOntology(OWLClassA.class, uri, new ObjectGraphInfo(attDescriptor))) .thenReturn(new OWLClassA(uri)); } final Assertion assertion = Assertion.createObjectPropertyAssertion( @@ -676,7 +687,7 @@ void reconstructsInstanceWithMappedSuperclass() throws Exception { new Value<>(NamedResource.create(ID_TWO)))); final OWLClassA a = new OWLClassA(); a.setUri(ID_TWO); - when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(ID_TWO), any(Descriptor.class))) + when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(ID_TWO), any(ObjectGraphInfo.class))) .thenReturn(a); final OWLClassQ res = constructor.reconstructEntity(constructionConfig(ID, mocks.forOwlClassQ() @@ -807,10 +818,10 @@ void reconstructEntityLoadsLazyReferencesWhenForceEagerIsSpecifiedForReconstruct getClassAssertionAxiomForType(ID, OWLClassD.getClassIri()), new AxiomImpl<>(NamedResource.create(ID), getClassDObjectPropertyAssertion(), new Value<>(NamedResource.create(aInstance.getUri()))) ); - when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(aInstance.getUri()), any(Descriptor.class))).thenReturn(aInstance); + when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(aInstance.getUri()), any(ObjectGraphInfo.class))).thenReturn(aInstance); final OWLClassD result = constructor.reconstructEntity( new EntityConstructor.EntityConstructionParameters<>(ID, mocks.forOwlClassD() - .entityType(), descriptor, true), axioms); + .entityType(), descriptor, new FetchGraphWrapper(), true), axioms); assertNotNull(result); assertNotNull(result.getOwlClassA()); assertEquals(aInstance, result.getOwlClassA()); @@ -818,4 +829,42 @@ void reconstructEntityLoadsLazyReferencesWhenForceEagerIsSpecifiedForReconstruct assertNotNull(loadStates); assertEquals(LoadState.LOADED, loadStates.isLoaded(mocks.forOwlClassD().owlClassAAtt())); } + + @Test + void reconstructEntityMarksLazilyLoadedAttributeAsLoadedWhenNoAxiomIsAvailableForItAndItIsSpecifiedInFetchGraph() { + when(mocks.forOwlClassD().owlClassAAtt().getFetchType()).thenReturn(FetchType.LAZY); + final List> axioms = List.of( + getClassAssertionAxiomForType(ID, OWLClassD.getClassIri()), + new AxiomImpl<>(NamedResource.create(ID), Assertion.createObjectPropertyAssertion(URI.create(Vocabulary.P_HAS_A), false), new Value<>(NamedResource.create(Generators.createIndividualIdentifier()))) + ); + final EntityGraph fetchGraph = new EntityGraphImpl<>(mocks.forOwlClassD() + .entityType(), uowMock.getMetamodel()); + fetchGraph.addAttributeNodes("owlClassA"); + final EntityConstructor.EntityConstructionParameters constructionParams = new EntityConstructor.EntityConstructionParameters<>(ID, mocks.forOwlClassD() + .entityType(), descriptor, new FetchGraphWrapper(fetchGraph), false); + final OWLClassD result = constructor.reconstructEntity(constructionParams, axioms); + assertNull(result.getOwlClassA()); + final LoadStateDescriptor loadStates = loadStateRegistry.get(result); + assertNotNull(loadStates); + assertEquals(LoadState.LOADED, loadStates.isLoaded(mocks.forOwlClassD().owlClassAAtt())); + } + + @Test + void reconstructEntityValidatesIntegrityConstraintsOnlyOnLoadedAttributes() { + final OWLClassA aInstance = Generators.generateOwlClassAInstance(); + final List> axioms = List.of( + getClassAssertionAxiomForType(ID, OWLClassL.getClassIri()), + new AxiomImpl<>(NamedResource.create(ID), + Assertion.createObjectPropertyAssertion(URI.create(Vocabulary.P_HAS_A), false), + new Value<>(NamedResource.create(aInstance.getUri()))) + ); + when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(aInstance.getUri()), any())).thenReturn(aInstance); + final EntityGraph fetchGraph = new EntityGraphImpl<>(mocks.forOwlClassL() + .entityType(), uowMock.getMetamodel()); + fetchGraph.addAttributeNodes("set"); + final EntityConstructor.EntityConstructionParameters constructionParams = new EntityConstructor.EntityConstructionParameters<>(ID, mocks.forOwlClassL() + .entityType(), descriptor, new FetchGraphWrapper(fetchGraph), false); + assertDoesNotThrow(() -> constructor.reconstructEntity(constructionParams, axioms)); + + } } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/FieldStrategyTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/FieldStrategyTest.java index 73e0aa0ae..733ca6a60 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/FieldStrategyTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/FieldStrategyTest.java @@ -24,6 +24,7 @@ import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.descriptors.EntityDescriptor; import cz.cvut.kbss.jopa.model.metamodel.*; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -63,7 +64,7 @@ void getAttributeContextRetrievesContextFromEntityDescriptor() { final URI context = Generators.createIndividualIdentifier(); descriptor.addAttributeContext(metamodelMocks.forOwlClassA().stringAttribute(), context); final FieldStrategy sut = FieldStrategy.createFieldStrategy(metamodelMocks.forOwlClassA().entityType(), - metamodelMocks.forOwlClassA().stringAttribute(), descriptor, mapperMock); + metamodelMocks.forOwlClassA().stringAttribute(), new ObjectGraphInfo(descriptor), mapperMock); assertEquals(context, sut.getAttributeWriteContext()); verify(descriptor).getSingleAttributeContext(metamodelMocks.forOwlClassA().stringAttribute()); } @@ -78,7 +79,7 @@ void createFieldStrategyCreatesPluralDataPropertyStrategyForDataPropertyCollecti when(att.getBindableJavaType()).thenReturn(String.class); when(att.getElementType()).thenReturn(BasicTypeImpl.get(String.class)); when(att.getPersistentAttributeType()).thenReturn(Attribute.PersistentAttributeType.DATA); - final FieldStrategy result = FieldStrategy.createFieldStrategy(et, att, descriptor, mapperMock); + final FieldStrategy result = FieldStrategy.createFieldStrategy(et, att, new ObjectGraphInfo(descriptor), mapperMock); assertThat(result, instanceOf(PluralDataPropertyStrategy.class)); } @@ -92,14 +93,14 @@ void createFieldStrategyCreatesSimpleSetStrategyForObjectPropertyCollectionAttri when(att.getBindableJavaType()).thenReturn(OWLClassA.class); when(att.getElementType()).thenReturn(metamodelMocks.forOwlClassA().entityType()); when(att.getPersistentAttributeType()).thenReturn(Attribute.PersistentAttributeType.OBJECT); - final FieldStrategy result = FieldStrategy.createFieldStrategy(et, att, descriptor, mapperMock); + final FieldStrategy result = FieldStrategy.createFieldStrategy(et, att, new ObjectGraphInfo(descriptor), mapperMock); assertThat(result, instanceOf(SimpleSetPropertyStrategy.class)); } @Test void createFieldStrategyCreatesSingularMultilingualStringFieldStrategyForSingularMultilingualStringAttributeMappingDataProperty() { final FieldStrategy result = FieldStrategy.createFieldStrategy(metamodelMocks.forOwlClassU().entityType(), - metamodelMocks.forOwlClassU().uSingularStringAtt(), descriptor, mapperMock); + metamodelMocks.forOwlClassU().uSingularStringAtt(), new ObjectGraphInfo(descriptor), mapperMock); assertThat(result, instanceOf(SingularMultilingualStringFieldStrategy.class)); } @@ -108,7 +109,7 @@ void createFieldStrategyCreatesSingularMultilingualStringFieldStrategyForSingula final AbstractAttribute att = metamodelMocks.forOwlClassU().uSingularStringAtt(); when(att.getPersistentAttributeType()).thenReturn(Attribute.PersistentAttributeType.ANNOTATION); final FieldStrategy result = FieldStrategy.createFieldStrategy(metamodelMocks.forOwlClassU().entityType(), - metamodelMocks.forOwlClassU().uSingularStringAtt(), descriptor, mapperMock); + metamodelMocks.forOwlClassU().uSingularStringAtt(), new ObjectGraphInfo(descriptor), mapperMock); assertThat(result, instanceOf(SingularMultilingualStringFieldStrategy.class)); } @@ -122,7 +123,7 @@ void createFieldStrategyCreatesReferencedListDataPropertyStrategyForDataProperty when(att.getPersistentAttributeType()).thenReturn(Attribute.PersistentAttributeType.DATA); when(att.getElementType()).thenReturn(BasicTypeImpl.get(LocalDate.class)); - final FieldStrategy result = FieldStrategy.createFieldStrategy(et, att, descriptor, mapperMock); + final FieldStrategy result = FieldStrategy.createFieldStrategy(et, att, new ObjectGraphInfo(descriptor), mapperMock); assertThat(result, instanceOf(ReferencedListDataPropertyStrategy.class)); } } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/ObjectOntologicalMappingTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/ObjectOntologicalMappingTest.java new file mode 100644 index 000000000..8dcd15b80 --- /dev/null +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/ObjectOntologicalMappingTest.java @@ -0,0 +1,165 @@ +package cz.cvut.kbss.jopa.oom; + +import cz.cvut.kbss.jopa.environment.OWLClassA; +import cz.cvut.kbss.jopa.environment.OWLClassD; +import cz.cvut.kbss.jopa.environment.Person; +import cz.cvut.kbss.jopa.environment.Phone; +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.model.EntityGraph; +import cz.cvut.kbss.jopa.model.EntityGraphImpl; +import cz.cvut.kbss.jopa.model.LoadState; +import cz.cvut.kbss.jopa.model.MetamodelImpl; +import cz.cvut.kbss.jopa.model.Subgraph; +import cz.cvut.kbss.jopa.model.descriptors.EntityDescriptor; +import cz.cvut.kbss.jopa.model.metamodel.IdentifiableEntityType; +import cz.cvut.kbss.jopa.sessions.AbstractUnitOfWork; +import cz.cvut.kbss.jopa.sessions.cache.CacheManager; +import cz.cvut.kbss.jopa.sessions.descriptor.LoadStateDescriptor; +import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingConfigGroup; +import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingParameters; +import cz.cvut.kbss.jopa.sessions.util.LoadStateDescriptorRegistry; +import cz.cvut.kbss.jopa.utils.Configuration; +import cz.cvut.kbss.ontodriver.Connection; +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; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasProperty; +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.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class ObjectOntologicalMappingTest { + + @Mock + private Connection storageConnection; + + @Mock + private CacheManager cacheManager; + + @Mock + private AbstractUnitOfWork uow; + + @Mock + private MetamodelImpl metamodel; + + private ObjectOntologyMapperImpl sut; + + @BeforeEach + public void setUp() throws Exception { + final MetamodelMocks metamodelMocks = new MetamodelMocks(); + metamodelMocks.setMocks(metamodel); + when(uow.getMetamodel()).thenReturn(metamodel); + when(uow.getLiveObjectCache()).thenReturn(cacheManager); + when(uow.getLoadStateRegistry()).thenReturn(new LoadStateDescriptorRegistry(Object::toString)); + when(uow.getConfiguration()).thenReturn(new Configuration()); + this.sut = new ObjectOntologyMapperImpl(uow, storageConnection); + } + + @Test + void loadEntityFromAxiomsLoadsEntityGraphWhenAxiomsForMultipleRelatedEntitiesAreSupplied() { + final OWLClassD expected = new OWLClassD(Generators.createIndividualIdentifier()); + final OWLClassA aInstance = Generators.generateOwlClassAInstance(); + expected.setOwlClassA(aInstance); + final List> axioms = instanceAxioms(expected); + + final OWLClassD result = sut.loadEntity(new AxiomBasedLoadingParameters<>(OWLClassD.class, axioms, new AxiomBasedLoadingConfigGroup<>(expected.getUri(), new EntityDescriptor()))); + assertEquals(expected.getUri(), result.getUri()); + assertNotNull(result.getOwlClassA()); + assertThat(result.getOwlClassA(), allOf( + hasProperty("uri", equalTo(aInstance.getUri())), + hasProperty("stringAttribute", equalTo(aInstance.getStringAttribute())), + hasProperty("types", equalTo(aInstance.getTypes())) + )); + } + + private static List> instanceAxioms(OWLClassD d) { + final List> axioms = new ArrayList<>(); + final NamedResource dIndividual = NamedResource.create(d.getUri()); + axioms.add(new AxiomImpl<>(dIndividual, Assertion.createClassAssertion(false), new Value<>(URI.create(Vocabulary.c_OwlClassD)))); + axioms.add(new AxiomImpl<>(dIndividual, Assertion.createObjectPropertyAssertion(URI.create(Vocabulary.p_h_hasA), false), new Value<>(d.getOwlClassA() + .getUri()))); + final NamedResource aIndividual = NamedResource.create(d.getOwlClassA().getUri()); + axioms.add(new AxiomImpl<>(aIndividual, Assertion.createDataPropertyAssertion(URI.create(Vocabulary.p_a_stringAttribute), false), new Value<>(d.getOwlClassA() + .getStringAttribute()))); + axioms.add(new AxiomImpl<>(aIndividual, Assertion.createClassAssertion(false), new Value<>(URI.create(Vocabulary.c_OwlClassA)))); + d.getOwlClassA().getTypes() + .forEach(type -> axioms.add(new AxiomImpl<>(aIndividual, Assertion.createClassAssertion(false), new Value<>(URI.create(type))))); + return axioms; + } + + @Test + void loadEntityFromAxiomsLoadsEntityWithRelatedEntitiesAndPopulatesLoadStateDescriptorBasedOnFetchGraph() { + final Person expected = new Person(Generators.createIndividualIdentifier()); + expected.setFirstName("John"); + expected.setLastName("Doe"); + expected.setPhone(new Phone(Generators.createIndividualIdentifier(), "123456789", "Nokia")); + final List> axioms = instanceAxioms(expected); + + final IdentifiableEntityType et = metamodel.entity(Person.class); + final EntityGraph fetchGraph = new EntityGraphImpl<>(et, metamodel); + // Age is not in the data + fetchGraph.addAttributeNodes("firstName", "lastName", "age", "phone"); + final Subgraph subgraph = fetchGraph.addSubgraph("phone"); + subgraph.addAttributeNodes("number", "brand"); + final Person result = sut.loadEntity(new AxiomBasedLoadingParameters<>(Person.class, axioms, new AxiomBasedLoadingConfigGroup<>(expected.getUri(), new EntityDescriptor(), fetchGraph))); + assertEquals(expected.getUri(), result.getUri()); + assertEquals(expected.getFirstName(), result.getFirstName()); + assertEquals(expected.getLastName(), result.getLastName()); + assertNull(result.getAge()); + assertNotNull(result.getPhone()); + assertEquals(expected.getPhone().getUri(), result.getPhone().getUri()); + assertEquals(expected.getPhone().getNumber(), result.getPhone().getNumber()); + assertEquals(expected.getPhone().getBrand(), result.getPhone().getBrand()); + final LoadStateDescriptor personLoadState = uow.getLoadStateRegistry().get(result); + assertNotNull(personLoadState); + assertEquals(LoadState.LOADED, personLoadState.isLoaded(et.getAttribute("firstName"))); + assertEquals(LoadState.LOADED, personLoadState.isLoaded(et.getAttribute("lastName"))); + assertEquals(LoadState.LOADED, personLoadState.isLoaded(et.getAttribute("age"))); + assertEquals(LoadState.LOADED, personLoadState.isLoaded(et.getAttribute("phone"))); + assertEquals(LoadState.UNKNOWN, personLoadState.isLoaded(et.getAttribute("username"))); + assertEquals(LoadState.UNKNOWN, personLoadState.isLoaded(et.getAttribute("gender"))); + final LoadStateDescriptor phoneLoadState = uow.getLoadStateRegistry().get(result.getPhone()); + assertNotNull(phoneLoadState); + assertEquals(LoadState.LOADED, phoneLoadState.isLoaded(metamodel.entity(Phone.class).getAttribute("number"))); + assertEquals(LoadState.LOADED, phoneLoadState.isLoaded(metamodel.entity(Phone.class).getAttribute("brand"))); + } + + private static List> instanceAxioms(Person person) { + final List> axioms = new ArrayList<>(); + final NamedResource pIndividual = NamedResource.create(person.getUri()); + axioms.add(new AxiomImpl<>(pIndividual, Assertion.createClassAssertion(false), new Value<>(URI.create(Vocabulary.c_Person)))); + axioms.add(new AxiomImpl<>(pIndividual, Assertion.createDataPropertyAssertion(URI.create(Vocabulary.p_p_firstName), false), new Value<>(person.getFirstName()))); + axioms.add(new AxiomImpl<>(pIndividual, Assertion.createDataPropertyAssertion(URI.create(Vocabulary.p_p_lastName), false), new Value<>(person.getLastName()))); + final NamedResource phoneIndividual = NamedResource.create(person.getPhone().getUri()); + axioms.add(new AxiomImpl<>(pIndividual, Assertion.createObjectPropertyAssertion(URI.create(Vocabulary.p_p_hasPhone), false), new Value<>(person.getPhone() + .getUri()))); + axioms.add(new AxiomImpl<>(phoneIndividual, Assertion.createClassAssertion(false), new Value<>(URI.create(Vocabulary.c_Phone)))); + axioms.add(new AxiomImpl<>(phoneIndividual, Assertion.createDataPropertyAssertion(URI.create(Vocabulary.p_p_phoneNumber), false), new Value<>(person.getPhone() + .getNumber()))); + axioms.add(new AxiomImpl<>(phoneIndividual, Assertion.createDataPropertyAssertion(URI.create(Vocabulary.p_p_phoneBrand), false), new Value<>(person.getPhone() + .getBrand()))); + return axioms; + } +} diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/ObjectOntologyMapperTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/ObjectOntologyMapperTest.java index 680f0a914..db2433226 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/ObjectOntologyMapperTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/ObjectOntologyMapperTest.java @@ -38,12 +38,14 @@ import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.jopa.model.metamodel.FieldSpecification; import cz.cvut.kbss.jopa.oom.exception.UnpersistedChangeException; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.proxy.reference.EntityReferenceProxy; import cz.cvut.kbss.jopa.proxy.reference.EntityReferenceProxyGenerator; import cz.cvut.kbss.jopa.sessions.AbstractUnitOfWork; import cz.cvut.kbss.jopa.sessions.cache.CacheManager; import cz.cvut.kbss.jopa.sessions.cache.Descriptors; import cz.cvut.kbss.jopa.sessions.descriptor.LoadStateDescriptor; +import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingConfigGroup; import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingParameters; import cz.cvut.kbss.jopa.sessions.util.LoadStateDescriptorRegistry; import cz.cvut.kbss.jopa.sessions.util.LoadingParameters; @@ -239,7 +241,7 @@ void testGetEntityFromCacheOrOntologyFromCache() { final LoadStateDescriptor loadStateDescriptor = new LoadStateDescriptor<>(entityA, mocks.forOwlClassA() .entityType(), LoadState.UNKNOWN); doReturn(loadStateDescriptor).when(cacheMock).getLoadStateDescriptor(entityA); - final OWLClassA res = mapper.getEntityFromCacheOrOntology(OWLClassA.class, IDENTIFIER, aDescriptor); + final OWLClassA res = mapper.getEntityFromCacheOrOntology(OWLClassA.class, IDENTIFIER, new ObjectGraphInfo(aDescriptor)); assertNotNull(res); assertSame(entityA, res); verify(cacheMock).get(OWLClassA.class, IDENTIFIER, aDescriptor); @@ -249,8 +251,7 @@ void testGetEntityFromCacheOrOntologyFromCache() { void testGetEntityFromCacheOrOntologyFromRegisteredInstances() { when(cacheMock.contains(OWLClassA.class, IDENTIFIER, null)).thenReturn(Boolean.FALSE); mapper.registerInstance(IDENTIFIER, entityA); - final OWLClassA res = mapper.getEntityFromCacheOrOntology(OWLClassA.class, IDENTIFIER, - aDescriptor); + final OWLClassA res = mapper.getEntityFromCacheOrOntology(OWLClassA.class, IDENTIFIER, new ObjectGraphInfo(aDescriptor)); assertNotNull(res); assertSame(entityA, res); } @@ -267,7 +268,7 @@ void getEntityFromCacheOrOntologyLoadsEntityWhenItIsNotInCache() throws Exceptio final LoadStateDescriptor loadStateDescriptor = new LoadStateDescriptor<>(entityA, mocks.forOwlClassA() .entityType(), LoadState.UNKNOWN); loadStateRegistry.put(entityA, loadStateDescriptor); - final OWLClassA res = mapper.getEntityFromCacheOrOntology(OWLClassA.class, IDENTIFIER, aDescriptor); + final OWLClassA res = mapper.getEntityFromCacheOrOntology(OWLClassA.class, IDENTIFIER, new ObjectGraphInfo(aDescriptor)); assertSame(entityA, res); verify(loader).loadEntity(loadingParameters); } @@ -630,8 +631,8 @@ void persistSavesPendingReferencedListContainingPersistedInstance() throws Excep Assertion.createObjectPropertyAssertion(URI.create(SequencesVocabulary.s_p_hasNext), false); final Assertion valueProp = Assertion.createObjectPropertyAssertion(URI.create(SequencesVocabulary.s_p_hasContents), false); - final ReferencedListValueDescriptor listDesc = - new ReferencedListValueDescriptor(ID_RESOURCE, listProp, nextProp, valueProp); + final ReferencedListValueDescriptor listDesc = + new ReferencedListValueDescriptor<>(ID_RESOURCE, listProp, nextProp, valueProp); mapper.registerPendingListReference(pending, listDesc, owner.getReferencedList()); when(entityDeconstructorMock.mapEntityToAxioms(pending.getUri(), pending, etAMock, aDescriptor)) @@ -647,7 +648,7 @@ void persistSavesPendingReferencedListContainingPersistedInstance() throws Excep void getEntityFromCacheOrOntologyThrowsEntityExistsWhenObjectIsAlreadyRegisteredUnderDifferentType() { mapper.registerInstance(IDENTIFIER, entityA); assertThrows(OWLEntityExistsException.class, - () -> mapper.getEntityFromCacheOrOntology(OWLClassB.class, IDENTIFIER, aDescriptor)); + () -> mapper.getEntityFromCacheOrOntology(OWLClassB.class, IDENTIFIER, new ObjectGraphInfo(aDescriptor))); } @Test @@ -701,7 +702,7 @@ void loadEntityFromAxiomsReconstructsEntityFromAxiomsAndReturnsIt() { new AxiomImpl<>(ID_RESOURCE, Assertion.createDataPropertyAssertion(URI.create(Vocabulary.p_a_stringAttribute), false), new Value<>("value")) ); - final OWLClassA result = mapper.loadEntity(new AxiomBasedLoadingParameters<>(OWLClassA.class, aDescriptor, false, axioms)); + final OWLClassA result = mapper.loadEntity(new AxiomBasedLoadingParameters<>(OWLClassA.class, axioms, new AxiomBasedLoadingConfigGroup<>(IDENTIFIER, aDescriptor, null))); assertNotNull(result); assertEquals(IDENTIFIER, result.getUri()); assertEquals("value", result.getStringAttribute()); @@ -710,18 +711,19 @@ void loadEntityFromAxiomsReconstructsEntityFromAxiomsAndReturnsIt() { @Test void loadEntityFromAxiomsReturnsNullWhenAxiomsAreEmpty() { - assertNull(mapper.loadEntity(new AxiomBasedLoadingParameters<>(OWLClassA.class, aDescriptor, false, List.of()))); + assertNull(mapper.loadEntity(new AxiomBasedLoadingParameters<>(OWLClassA.class, List.of(), new AxiomBasedLoadingConfigGroup<>(IDENTIFIER, aDescriptor)))); } @Test - void loadEntityFromAxiomsPutsLoadedEntityToCache() { + void loadEntityFromAxiomsDoesNotPutLoadedEntityToCache() { final List> axioms = List.of( new AxiomImpl<>(ID_RESOURCE, Assertion.createClassAssertion(false), new Value<>(URI.create(OWLClassA.getClassIri()))), new AxiomImpl<>(ID_RESOURCE, Assertion.createDataPropertyAssertion(URI.create(Vocabulary.p_a_stringAttribute), false), new Value<>("value")) ); - final OWLClassA result = mapper.loadEntity(new AxiomBasedLoadingParameters<>(OWLClassA.class, aDescriptor, false, axioms)); - verify(cacheMock).add(IDENTIFIER, result, new Descriptors(aDescriptor, loadStateRegistry.get(result))); + final OWLClassA result = mapper.loadEntity(new AxiomBasedLoadingParameters<>(OWLClassA.class, axioms, new AxiomBasedLoadingConfigGroup<>(IDENTIFIER, aDescriptor))); + assertNotNull(result); + verify(cacheMock, never()).add(eq(IDENTIFIER), any(), any()); } @Test @@ -732,7 +734,7 @@ void loadEntityFromAxiomsResolvesConcreteClassForEntityTypeWithSubTypes() { new AxiomImpl<>(ID_RESOURCE, Assertion.createDataPropertyAssertion(URI.create(RDFS.LABEL), false), new Value<>("value")) ); - final OWLClassS result = mapper.loadEntity(new AxiomBasedLoadingParameters<>(OWLClassS.class, new EntityDescriptor(), false, axioms)); + final OWLClassS result = mapper.loadEntity(new AxiomBasedLoadingParameters<>(OWLClassS.class, axioms, new AxiomBasedLoadingConfigGroup<>(IDENTIFIER, new EntityDescriptor(), null))); assertInstanceOf(OWLClassR.class, result); } } 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 3fbd827dd..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 @@ -25,7 +25,11 @@ 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.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; import cz.cvut.kbss.jopa.model.descriptors.EntityDescriptor; @@ -35,8 +39,10 @@ import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.jopa.model.metamodel.Identifier; import cz.cvut.kbss.jopa.oom.converter.ObjectOneOfEnumConverter; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.proxy.lazy.LazyLoadingSetProxy; import cz.cvut.kbss.jopa.sessions.UnitOfWork; +import cz.cvut.kbss.jopa.sessions.util.FetchGraphWrapper; import cz.cvut.kbss.jopa.vocabulary.OWL; import cz.cvut.kbss.ontodriver.model.Assertion; import cz.cvut.kbss.ontodriver.model.Axiom; @@ -99,11 +105,11 @@ void addValueFromAxiomLoadsInstanceAndAddsItIntoAttributeCollection() { final OWLClassJ instance = new OWLClassJ(ID); final URI aReference = Generators.createIndividualIdentifier(); final OWLClassA aInstance = new OWLClassA(aReference); - when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(aReference), any(Descriptor.class))) + when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(aReference), any(ObjectGraphInfo.class))) .thenReturn(aInstance); final Axiom axiom = new AxiomImpl<>(NamedResource.create(ID), - Assertion.createObjectPropertyAssertion( - URI.create(Vocabulary.P_HAS_A), false), new Value<>( + Assertion.createObjectPropertyAssertion( + URI.create(Vocabulary.P_HAS_A), false), new Value<>( NamedResource.create(aReference))); sut.addAxiomValue(axiom); @@ -111,12 +117,12 @@ void addValueFromAxiomLoadsInstanceAndAddsItIntoAttributeCollection() { assertNotNull(instance.getOwlClassA()); assertEquals(1, instance.getOwlClassA().size()); assertTrue(instance.getOwlClassA().contains(aInstance)); - verify(mapperMock).getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(aReference), any(Descriptor.class)); + verify(mapperMock).getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(aReference), any(ObjectGraphInfo.class)); } private PluralObjectPropertyStrategy strategy() { return new SimpleSetPropertyStrategy<>(mocks.forOwlClassJ().entityType(), mocks.forOwlClassJ().setAttribute(), - descriptor, mapperMock); + new ObjectGraphInfo(descriptor), mapperMock); } @Test @@ -124,18 +130,18 @@ void addValueFromAxiomDoesNothingWhenInstanceCannotBeLoaded() { final PluralObjectPropertyStrategy sut = strategy(); final OWLClassJ instance = new OWLClassJ(ID); final URI aReference = Generators.createIndividualIdentifier(); - when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(aReference), any(Descriptor.class))) + when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(aReference), any(ObjectGraphInfo.class))) .thenReturn(null); final Axiom axiom = new AxiomImpl<>(NamedResource.create(ID), - Assertion.createObjectPropertyAssertion( - URI.create(Vocabulary.P_HAS_A), false), new Value<>( + Assertion.createObjectPropertyAssertion( + URI.create(Vocabulary.P_HAS_A), false), new Value<>( NamedResource.create(aReference))); sut.addAxiomValue(axiom); sut.buildInstanceFieldValue(instance); assertNotNull(instance.getOwlClassA()); assertTrue(instance.getOwlClassA().isEmpty()); - verify(mapperMock).getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(aReference), any(Descriptor.class)); + verify(mapperMock).getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(aReference), any(ObjectGraphInfo.class)); } @Test @@ -144,21 +150,21 @@ void addValueFromAxiomGetsAttributeDescriptorFromEntityDescriptorForLoading() { descriptor.addAttributeDescriptor(mocks.forOwlClassJ().setAttribute(), aDescriptor); final PluralObjectPropertyStrategy sut = strategy(); final URI aReference = Generators.createIndividualIdentifier(); - when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(aReference), any(Descriptor.class))) + when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(aReference), any(ObjectGraphInfo.class))) .thenReturn(null); final Axiom axiom = new AxiomImpl<>(NamedResource.create(ID), - Assertion.createObjectPropertyAssertion( - URI.create(Vocabulary.P_HAS_A), false), new Value<>( + Assertion.createObjectPropertyAssertion( + URI.create(Vocabulary.P_HAS_A), false), new Value<>( NamedResource.create(aReference))); sut.addAxiomValue(axiom); - verify(mapperMock).getEntityFromCacheOrOntology(OWLClassA.class, aReference, aDescriptor); + verify(mapperMock).getEntityFromCacheOrOntology(OWLClassA.class, aReference, new ObjectGraphInfo(aDescriptor)); } @Test void buildAxiomValuesFromInstanceChecksForReferenceExistenceUsingTargetReferenceContext() { final Descriptor aDescriptor = new ObjectPropertyCollectionDescriptor(Generators.createIndividualIdentifier(), - mocks.forOwlClassJ().setAttribute()); + mocks.forOwlClassJ().setAttribute()); descriptor.addAttributeDescriptor(mocks.forOwlClassJ().setAttribute(), aDescriptor); final PluralObjectPropertyStrategy sut = strategy(); final ReferenceSavingResolver resolverMock = mock(ReferenceSavingResolver.class); @@ -173,11 +179,11 @@ void buildAxiomValuesFromInstanceChecksForReferenceExistenceUsingTargetReference @Test void buildAxiomValuesFromInstanceChecksForListItemReferenceExistenceUsingTargetReferenceContext() { final Descriptor aDescriptor = new ObjectPropertyCollectionDescriptor(Generators.createIndividualIdentifier(), - mocks.forOwlClassC().simpleListAtt()); + mocks.forOwlClassC().simpleListAtt()); descriptor.addAttributeDescriptor(mocks.forOwlClassC().simpleListAtt(), aDescriptor); final SimpleListPropertyStrategy sut = new SimpleListPropertyStrategy<>(mocks.forOwlClassC().entityType(), - mocks.forOwlClassC().simpleListAtt(), descriptor, mapperMock); + mocks.forOwlClassC().simpleListAtt(), new ObjectGraphInfo(descriptor), mapperMock); final ReferenceSavingResolver resolverMock = mock(ReferenceSavingResolver.class); sut.setReferenceSavingResolver(resolverMock); final OWLClassC instance = new OWLClassC(ID); @@ -193,7 +199,7 @@ void buildAxiomsFromInstanceReturnsAxiomsCorrespondingToAttributeValue() { instance.setSimpleList(Generators.generateInstances(5)); final SimpleListPropertyStrategy sut = new SimpleListPropertyStrategy<>(mocks.forOwlClassC().entityType(), - mocks.forOwlClassC().simpleListAtt(), descriptor, mapperMock); + mocks.forOwlClassC().simpleListAtt(), new ObjectGraphInfo(descriptor), mapperMock); when(mapperMock.getEntityType(OWLClassA.class)).thenReturn(mocks.forOwlClassA().entityType()); final Set> result = sut.buildAxiomsFromInstance(instance); @@ -204,14 +210,14 @@ void buildAxiomsFromInstanceReturnsAxiomsCorrespondingToAttributeValue() { .toURI(), false); instance.getSimpleList().forEach( a -> assertThat(result, hasItem(new AxiomImpl<>(subject, assertion, - new Value<>(NamedResource.create(a.getUri())))))); + new Value<>(NamedResource.create(a.getUri())))))); } @Test void buildAxiomsFromInstanceReturnsAxiomsWithIdentifiersWhenAttributeValueIsCollectionOfIdentifiers() { final SimpleSetPropertyStrategy sut = new SimpleSetPropertyStrategy<>(mocks.forOwlClassP().entityType(), - mocks.forOwlClassP().pUrlsAttribute(), descriptor, mapperMock); + mocks.forOwlClassP().pUrlsAttribute(), new ObjectGraphInfo(descriptor), mapperMock); final OWLClassP p = new OWLClassP(); p.setUri(Generators.createIndividualIdentifier()); p.setIndividualUrls(IntStream.range(0, 5).mapToObj(i -> { @@ -237,7 +243,7 @@ void buildAxiomsFromInstanceReturnsEmptyCollectionWhenAttributeValueIsEmpty() { instance.setSimpleList(null); final SimpleListPropertyStrategy sut = new SimpleListPropertyStrategy<>(mocks.forOwlClassC().entityType(), - mocks.forOwlClassC().simpleListAtt(), descriptor, mapperMock); + mocks.forOwlClassC().simpleListAtt(), new ObjectGraphInfo(descriptor), mapperMock); final Set> result = sut.buildAxiomsFromInstance(instance); assertNotNull(result); @@ -268,7 +274,7 @@ private SimpleSetPropertyStrategy initEnumAt final Identifier id = mock(Identifier.class); when(id.getJavaField()).thenReturn(EntityWithPluralObjectPropertyEnum.class.getDeclaredField("uri")); when(et.getIdentifier()).thenReturn(id); - return new SimpleSetPropertyStrategy<>(et, att, descriptor, mapperMock); + return new SimpleSetPropertyStrategy<>(et, att, new ObjectGraphInfo(descriptor), mapperMock); } static class EntityWithPluralObjectPropertyEnum { @@ -292,19 +298,21 @@ void buildAxiomsFromInstanceConvertsEnumConstantsToNamedResourcesForEnumValuedOb Assertion.createObjectPropertyAssertion(URI.create(Vocabulary.p_m_objectOneOfEnumAttribute), false); assertThat(result, hasItems( new AxiomImpl<>(NamedResource.create(ID), assertion, - new Value<>(NamedResource.create(OWL.ANNOTATION_PROPERTY))), + new Value<>(NamedResource.create(OWL.ANNOTATION_PROPERTY))), new AxiomImpl<>(NamedResource.create(ID), assertion, - new Value<>(NamedResource.create(OWL.OBJECT_PROPERTY))) + new Value<>(NamedResource.create(OWL.OBJECT_PROPERTY))) )); } @Test void buildInstanceFieldValueSetsInstanceFieldValueToEmptyCollectionWhenNoAxiomsWereAdded() { // This ensures lazy loading proxy is replaced with empty collection when lazy loaded field loading is triggered - final SimpleSetPropertyStrategy sut = new SimpleSetPropertyStrategy<>(mocks.forOwlClassJ().entityType(), mocks.forOwlClassJ() - .setAttribute(), descriptor, mapperMock); + final SimpleSetPropertyStrategy sut = new SimpleSetPropertyStrategy<>(mocks.forOwlClassJ() + .entityType(), mocks.forOwlClassJ() + .setAttribute(), new ObjectGraphInfo(descriptor), mapperMock); final OWLClassJ instance = new OWLClassJ(ID); - instance.setOwlClassA(new LazyLoadingSetProxy<>(instance, mocks.forOwlClassJ().setAttribute(), mock(UnitOfWork.class))); + instance.setOwlClassA(new LazyLoadingSetProxy<>(instance, mocks.forOwlClassJ() + .setAttribute(), mock(UnitOfWork.class))); sut.buildInstanceFieldValue(instance); assertNotNull(instance.getOwlClassA()); assertTrue(instance.getOwlClassA().isEmpty()); @@ -312,8 +320,9 @@ void buildInstanceFieldValueSetsInstanceFieldValueToEmptyCollectionWhenNoAxiomsW @Test void lazilyAddAxiomValueSetsHasValueButDoesNotLoadReference() { - final SimpleSetPropertyStrategy sut = new SimpleSetPropertyStrategy<>(mocks.forOwlClassJ().entityType(), mocks.forOwlClassJ() - .setAttribute(), descriptor, mapperMock); + final SimpleSetPropertyStrategy sut = new SimpleSetPropertyStrategy<>(mocks.forOwlClassJ() + .entityType(), mocks.forOwlClassJ() + .setAttribute(), new ObjectGraphInfo(descriptor), mapperMock); final URI aReference = Generators.createIndividualIdentifier(); final Axiom axiom = new AxiomImpl<>(NamedResource.create(ID), Assertion.createObjectPropertyAssertion( @@ -326,19 +335,39 @@ void lazilyAddAxiomValueSetsHasValueButDoesNotLoadReference() { } @Test - void addValueFromAxiomUsesEntityDescriptorToGetInstanceFromCacheOrOntology() { + void addAxiomValueUsesEntityDescriptorToGetInstanceFromCacheOrOntology() { final PluralObjectPropertyStrategy sut = strategy(); final URI aReference = Generators.createIndividualIdentifier(); final OWLClassA aInstance = new OWLClassA(aReference); - when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(aReference), any(Descriptor.class))) + when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(aReference), any(ObjectGraphInfo.class))) .thenReturn(aInstance); final Axiom axiom = new AxiomImpl<>(NamedResource.create(ID), Assertion.createObjectPropertyAssertion( URI.create(Vocabulary.P_HAS_A), false), new Value<>(NamedResource.create(aReference))); sut.addAxiomValue(axiom); - final ArgumentCaptor captor = ArgumentCaptor.forClass(Descriptor.class); + final ArgumentCaptor captor = ArgumentCaptor.forClass(ObjectGraphInfo.class); verify(mapperMock).getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(aReference), captor.capture()); - assertInstanceOf(EntityDescriptor.class, captor.getValue()); + assertInstanceOf(EntityDescriptor.class, captor.getValue().descriptor()); + } + + @Test + void addAxiomValuePropagatesFetchGraphToReferencedEntityLoading() { + final MetamodelImpl metamodel = mock(MetamodelImpl.class); + mocks.setMocks(metamodel); + final EntityGraph fetchGraph = new EntityGraphImpl<>(mocks.forOwlClassJ().entityType(), metamodel); + final Subgraph subgraph = fetchGraph.addSubgraph("owlClassA"); + subgraph.addAttributeNodes("stringAttribute"); + final PluralObjectPropertyStrategy sut = new SimpleSetPropertyStrategy<>(mocks.forOwlClassJ() + .entityType(), mocks.forOwlClassJ() + .setAttribute(), + new ObjectGraphInfo(descriptor, new FetchGraphWrapper(fetchGraph)), mapperMock); + + sut.addAxiomValue(new AxiomImpl<>(NamedResource.create(ID), + Assertion.createObjectPropertyAssertion(URI.create(Vocabulary.P_HAS_A), false), + new Value<>(Generators.createIndividualIdentifier()))); + final ArgumentCaptor captor = ArgumentCaptor.forClass(ObjectGraphInfo.class); + verify(mapperMock).getEntityFromCacheOrOntology(eq(OWLClassA.class), any(), captor.capture()); + assertEquals(subgraph.getAttributeNodes(), captor.getValue().fetchGraph().getAttributeNodes()); } } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/RdfContainerObjectPropertyStrategyTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/RdfContainerObjectPropertyStrategyTest.java index 976b5892f..dfc511dc0 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/RdfContainerObjectPropertyStrategyTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/RdfContainerObjectPropertyStrategyTest.java @@ -30,6 +30,7 @@ import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.jopa.model.metamodel.Identifier; import cz.cvut.kbss.jopa.model.metamodel.RdfContainerAttributeImpl; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.vocabulary.RDF; import cz.cvut.kbss.ontodriver.Connection; import cz.cvut.kbss.ontodriver.Containers; @@ -82,7 +83,7 @@ void addAxiomValueLoadsContainerContentAndAddsValuesFromIt() throws Exception { when(att.getIRI()).thenReturn(IRI.create(Vocabulary.ATTRIBUTE_BASE + "levels")); when(att.getCollectionType()).thenReturn(CollectionType.LIST); when(att.getBindableJavaType()).thenReturn(URI.class); - final RdfContainerObjectPropertyStrategy sut = new RdfContainerObjectPropertyStrategy<>(et, att, descriptor, mappingHelper); + final RdfContainerObjectPropertyStrategy sut = new RdfContainerObjectPropertyStrategy<>(et, att, new ObjectGraphInfo(descriptor), mappingHelper); sut.addAxiomValue(new AxiomImpl<>(NamedResource.create(ID), Assertion.createObjectPropertyAssertion(URI.create(Vocabulary.ATTRIBUTE_BASE + "levels"), false), new Value<>(NamedResource.create(Generators.createIndividualIdentifier())))); final EntityWithContainer entity = new EntityWithContainer(); @@ -118,7 +119,7 @@ void buildAxiomValuesFromInstanceAddsContainerDescriptorWithListValuesToValueGat final Identifier idAtt = mock(Identifier.class); when(et.getIdentifier()).thenReturn(idAtt); when(idAtt.getJavaField()).thenReturn(EntityWithContainer.class.getDeclaredField("uri")); - final RdfContainerObjectPropertyStrategy sut = new RdfContainerObjectPropertyStrategy<>(et, att, descriptor, mappingHelper); + final RdfContainerObjectPropertyStrategy sut = new RdfContainerObjectPropertyStrategy<>(et, att, new ObjectGraphInfo(descriptor), mappingHelper); final EntityWithContainer entity = new EntityWithContainer(); entity.uri = ID; entity.levels = List.of(Generators.createIndividualIdentifier(), Generators.createIndividualIdentifier()); diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/ReferencedListPropertyStrategyTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/ReferencedListPropertyStrategyTest.java index 35b2b3cf6..9c9beb8ea 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/ReferencedListPropertyStrategyTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/ReferencedListPropertyStrategyTest.java @@ -22,10 +22,10 @@ import cz.cvut.kbss.jopa.environment.OWLClassP; import cz.cvut.kbss.jopa.environment.OneOfEnum; import cz.cvut.kbss.jopa.model.annotations.OWLObjectProperty; -import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.jopa.model.metamodel.Identifier; import cz.cvut.kbss.jopa.model.metamodel.ListAttributeImpl; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.vocabulary.OWL; import cz.cvut.kbss.ontodriver.descriptor.ReferencedListDescriptor; import cz.cvut.kbss.ontodriver.descriptor.ReferencedListValueDescriptor; @@ -81,8 +81,8 @@ static void setUpBeforeClass() { public void setUp() throws Exception { super.setUp(); this.refListMock = mocks.forOwlClassC().referencedListAtt(); - this.strategy = new ReferencedListPropertyStrategy<>(mocks.forOwlClassC().entityType(), refListMock, descriptor, - mapperMock); + this.strategy = new ReferencedListPropertyStrategy<>(mocks.forOwlClassC() + .entityType(), refListMock, new ObjectGraphInfo(descriptor), mapperMock); strategy.setReferenceSavingResolver(new ReferenceSavingResolver(mapperMock)); } @@ -136,7 +136,7 @@ private List> initRefListAxioms(boolean includeNodes) throws Exception refListMock.getHasContentsPropertyIRI() .toURI(), refListMock.isInferred()), new Value<>(NamedResource.create(a.getUri()))); - when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(a.getUri()), any(Descriptor.class))).thenReturn(a); + when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(a.getUri()), any(ObjectGraphInfo.class))).thenReturn(a); axioms.add(content); previous = nodeUri; i++; @@ -164,7 +164,8 @@ void buildsInstanceFieldFromAxiomsWithoutNodes() throws Exception { void buildsInstanceFieldWithPlainIdentifiers() throws Exception { final ListAttributeImpl listAtt = mocks.forOwlClassP().pReferencedListAttribute(); final ReferencedListPropertyStrategy strategy = - new ReferencedListPropertyStrategy<>(mocks.forOwlClassP().entityType(), listAtt, descriptor, + new ReferencedListPropertyStrategy<>(mocks.forOwlClassP() + .entityType(), listAtt, new ObjectGraphInfo(descriptor), mapperMock); final List> axioms = initRefListAxioms(true); when(mapperMock.loadReferencedList(any(ReferencedListDescriptor.class))).thenReturn(axioms); @@ -182,7 +183,6 @@ void buildsInstanceFieldWithPlainIdentifiers() throws Exception { @Test void extractsValuesIntoAxiomsForSave() throws Exception { - when(mapperMock.isManagedType(OWLClassA.class)).thenReturn(true); final OWLClassC c = new OWLClassC(IDENTIFIER); c.setReferencedList(list); strategy.buildAxiomValuesFromInstance(c, builder); @@ -249,7 +249,8 @@ void extractsValuesIntoAxiomsForSaveFromNullList() throws Exception { void extractsValuesIntoAxiomsFromListOfPlainIdentifiers() throws Exception { final ListAttributeImpl listAtt = mocks.forOwlClassP().pReferencedListAttribute(); final ReferencedListPropertyStrategy strategy = - new ReferencedListPropertyStrategy<>(mocks.forOwlClassP().entityType(), listAtt, descriptor, + new ReferencedListPropertyStrategy<>(mocks.forOwlClassP() + .entityType(), listAtt, new ObjectGraphInfo(descriptor), mapperMock); final OWLClassP p = new OWLClassP(); p.setUri(IDENTIFIER); @@ -269,7 +270,8 @@ void extractValuesFromListSkipsNullItemsInListOfPlainIdentifiers() throws Except final List nonNulls = p.getReferencedList().stream().filter(Objects::nonNull).collect(Collectors.toList()); final ListAttributeImpl refList = mocks.forOwlClassP().pReferencedListAttribute(); final ReferencedListPropertyStrategy strategy = - new ReferencedListPropertyStrategy<>(mocks.forOwlClassP().entityType(), refList, descriptor, + new ReferencedListPropertyStrategy<>(mocks.forOwlClassP() + .entityType(), refList, new ObjectGraphInfo(descriptor), mapperMock); strategy.buildAxiomValuesFromInstance(p, builder); @@ -297,7 +299,7 @@ void extractListValuesConvertsEnumConstantsToNamedResourcesForEnumValuedObjectPr when(et.getIdentifier()).thenReturn(id); final ListAttributeImpl att = initEnumListAttribute(); final ReferencedListPropertyStrategy sut = - new ReferencedListPropertyStrategy<>(et, att, descriptor, mapperMock); + new ReferencedListPropertyStrategy<>(et, att, new ObjectGraphInfo(descriptor), mapperMock); final WithEnumList instance = new WithEnumList(); instance.uri = IDENTIFIER; instance.enumList = Arrays.asList(OneOfEnum.DATATYPE_PROPERTY, OneOfEnum.OBJECT_PROPERTY); diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/SimpleListPropertyStrategyTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/SimpleListPropertyStrategyTest.java index 2171b902e..177358cc9 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/SimpleListPropertyStrategyTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/SimpleListPropertyStrategyTest.java @@ -24,11 +24,11 @@ import cz.cvut.kbss.jopa.environment.utils.Generators; import cz.cvut.kbss.jopa.model.annotations.OWLObjectProperty; import cz.cvut.kbss.jopa.model.annotations.Sequence; -import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.jopa.model.metamodel.Identifier; import cz.cvut.kbss.jopa.model.metamodel.ListAttribute; import cz.cvut.kbss.jopa.model.metamodel.ListAttributeImpl; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.vocabulary.OWL; import cz.cvut.kbss.ontodriver.descriptor.SimpleListDescriptor; import cz.cvut.kbss.ontodriver.descriptor.SimpleListValueDescriptor; @@ -78,7 +78,7 @@ public void setUp() throws Exception { super.setUp(); this.simpleList = mocks.forOwlClassC().simpleListAtt(); this.strategy = - new SimpleListPropertyStrategy<>(mocks.forOwlClassC().entityType(), simpleList, descriptor, mapperMock); + new SimpleListPropertyStrategy<>(mocks.forOwlClassC().entityType(), simpleList, new ObjectGraphInfo(descriptor), mapperMock); strategy.setReferenceSavingResolver(new ReferenceSavingResolver(mapperMock)); } @@ -111,7 +111,7 @@ private Collection> buildAxiomsForList(ListAttribute Assertion.createObjectPropertyAssertion(la.getHasNextPropertyIRI().toURI(), false), new Value<>(NamedResource.create(item.getUri()))); axioms.add(a); - when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(item.getUri()), any(Descriptor.class))) + when(mapperMock.getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(item.getUri()), any(ObjectGraphInfo.class))) .thenReturn(item); previous = item.getUri(); } @@ -122,7 +122,7 @@ private Collection> buildAxiomsForList(ListAttribute void buildsInstanceFieldOfPlainIdentifiersFromAxioms() { final ListAttributeImpl simpleList = mocks.forOwlClassP().pSimpleListAttribute(); final SimpleListPropertyStrategy strategy = - new SimpleListPropertyStrategy<>(mocks.forOwlClassP().entityType(), simpleList, descriptor, mapperMock); + new SimpleListPropertyStrategy<>(mocks.forOwlClassP().entityType(), simpleList, new ObjectGraphInfo(descriptor), mapperMock); final Axiom ax = new AxiomImpl<>(NamedResource.create(IDENTIFIER), Assertion.createObjectPropertyAssertion(simpleList.getIRI() .toURI(), false), new Value<>(Generators.createIndividualIdentifier())); @@ -234,7 +234,7 @@ void extractValuesFromListOfPlainIdentifiersForPersist() throws Exception { p.setSimpleList(generateListOfIdentifiers()); final ListAttributeImpl simpleList = mocks.forOwlClassP().pSimpleListAttribute(); final SimpleListPropertyStrategy strategy = - new SimpleListPropertyStrategy<>(mocks.forOwlClassP().entityType(), simpleList, descriptor, mapperMock); + new SimpleListPropertyStrategy<>(mocks.forOwlClassP().entityType(), simpleList, new ObjectGraphInfo(descriptor), mapperMock); strategy.buildAxiomValuesFromInstance(p, builder); final SimpleListValueDescriptor valueDescriptor = listValueDescriptor(); @@ -250,7 +250,7 @@ void extractValuesFromListSkipsNullItemsInListOfPlainIdentifiers() throws Except final List nonNulls = p.getSimpleList().stream().filter(Objects::nonNull).collect(Collectors.toList()); final ListAttributeImpl simpleList = mocks.forOwlClassP().pSimpleListAttribute(); final SimpleListPropertyStrategy strategy = - new SimpleListPropertyStrategy<>(mocks.forOwlClassP().entityType(), simpleList, descriptor, mapperMock); + new SimpleListPropertyStrategy<>(mocks.forOwlClassP().entityType(), simpleList, new ObjectGraphInfo(descriptor), mapperMock); strategy.buildAxiomValuesFromInstance(p, builder); final SimpleListValueDescriptor valueDescriptor = listValueDescriptor(); @@ -277,7 +277,7 @@ void extractListValuesConvertsEnumConstantsToNamedResourcesForEnumValuedObjectPr when(et.getIdentifier()).thenReturn(id); final ListAttributeImpl att = initEnumListAttribute(); final SimpleListPropertyStrategy sut = - new SimpleListPropertyStrategy<>(et, att, descriptor, mapperMock); + new SimpleListPropertyStrategy<>(et, att, new ObjectGraphInfo(descriptor), mapperMock); final WithEnumList instance = new WithEnumList(); instance.uri = IDENTIFIER; instance.enumList = Arrays.asList(OneOfEnum.DATATYPE_PROPERTY, OneOfEnum.OBJECT_PROPERTY); diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/SimpleSetPropertyStrategyTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/SimpleSetPropertyStrategyTest.java index d3c48079f..8434426ba 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/SimpleSetPropertyStrategyTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/SimpleSetPropertyStrategyTest.java @@ -33,6 +33,7 @@ import cz.cvut.kbss.jopa.model.metamodel.CollectionType; import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.jopa.oom.converter.ObjectOneOfEnumConverter; +import cz.cvut.kbss.jopa.oom.util.ObjectGraphInfo; import cz.cvut.kbss.jopa.vocabulary.OWL; import cz.cvut.kbss.ontodriver.descriptor.AxiomValueDescriptor; import cz.cvut.kbss.ontodriver.model.Assertion; @@ -113,7 +114,7 @@ void extractsValuesFromInstance() throws Exception { } private SimpleSetPropertyStrategy strategy(EntityType et, AbstractPluralAttribute, E> att) { - return new SimpleSetPropertyStrategy<>(et, att, descriptor, mapperMock); + return new SimpleSetPropertyStrategy<>(et, att, new ObjectGraphInfo(descriptor), mapperMock); } private void verifyExtractedValues(Set expected) throws Exception { 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 f48de39a1..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,22 +17,41 @@ */ 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; import cz.cvut.kbss.jopa.model.descriptors.EntityDescriptor; 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.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; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; @@ -43,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) @@ -88,7 +118,7 @@ void buildInstanceFieldSupportsPlainIdentifierValues() throws Exception { private FieldStrategy, T> strategy(EntityType et, AbstractAttribute att) { - return new SingularObjectPropertyStrategy<>(et, att, descriptor, mapperMock); + return new SingularObjectPropertyStrategy<>(et, att, new ObjectGraphInfo(descriptor), mapperMock); } private Assertion propertyP() throws Exception { @@ -246,7 +276,7 @@ void addValueGetsAttributeDescriptorFromEntityDescriptorForLoading() { final Assertion assertion = Assertion.createObjectPropertyAssertion(URI.create(Vocabulary.P_HAS_A), false); sut.addAxiomValue( new AxiomImpl<>(NamedResource.create(IDENTIFIER), assertion, new Value<>(NamedResource.create(VALUE)))); - verify(mapperMock).getEntityFromCacheOrOntology(OWLClassA.class, VALUE, aDescriptor); + verify(mapperMock).getEntityFromCacheOrOntology(OWLClassA.class, VALUE, new ObjectGraphInfo(aDescriptor)); } @Test @@ -362,4 +392,23 @@ void addValueSupportsUriAxiomValue() throws Exception { strategy.buildInstanceFieldValue(p); assertEquals(VALUE, p.getIndividualUri()); } + + @Test + void addAxiomValuePropagatesFetchGraphToReferencedEntityLoading() { + final MetamodelImpl metamodel = mock(MetamodelImpl.class); + metamodelMocks.setMocks(metamodel); + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassD() + .entityType(), metamodel); + final Subgraph sg = fetchGraph.addSubgraph("owlClassA"); + sg.addAttributeNodes("stringAttribute"); + final FieldStrategy, OWLClassD> sut = new SingularObjectPropertyStrategy<>(metamodelMocks.forOwlClassD() + .entityType(), + metamodelMocks.forOwlClassD() + .owlClassAAtt(), new ObjectGraphInfo(descriptor, new FetchGraphWrapper(fetchGraph)), mapperMock); + sut.setReferenceSavingResolver(referenceResolverMock); + sut.addAxiomValue(new AxiomImpl<>(NamedResource.create(IDENTIFIER), Assertion.createObjectPropertyAssertion(URI.create(Vocabulary.P_HAS_A), false), new Value<>(VALUE))); + final ArgumentCaptor captor = ArgumentCaptor.forClass(ObjectGraphInfo.class); + verify(mapperMock).getEntityFromCacheOrOntology(eq(OWLClassA.class), eq(VALUE), captor.capture()); + assertEquals(sg.getAttributeNodes(), captor.getValue().fetchGraph().getAttributeNodes()); + } } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/TwoStepInstanceLoaderTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/TwoStepInstanceLoaderTest.java index c52e55c59..1d5cd64fb 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/TwoStepInstanceLoaderTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/oom/TwoStepInstanceLoaderTest.java @@ -23,6 +23,7 @@ import cz.cvut.kbss.jopa.environment.Vocabulary; import cz.cvut.kbss.jopa.environment.utils.MetamodelMocks; import cz.cvut.kbss.jopa.exceptions.StorageAccessException; +import cz.cvut.kbss.jopa.sessions.util.FetchGraphWrapper; import cz.cvut.kbss.jopa.sessions.util.LoadStateDescriptorRegistry; import cz.cvut.kbss.jopa.sessions.util.LoadingParameters; import cz.cvut.kbss.ontodriver.Types; @@ -107,7 +108,8 @@ void loadEntityLoadsEntityFromStorageWhenEntityTypeIsDetermined() throws Excepti final Collection> axioms = new HashSet<>(types); when(connectionMock.find(axiomDescriptor)).thenReturn(axioms); when(entityConstructorMock - .reconstructEntity(new EntityConstructor.EntityConstructionParameters<>(IDENTIFIER, metamodelMock.entity(OWLClassR.class), descriptor, false), + .reconstructEntity(new EntityConstructor.EntityConstructionParameters<>(IDENTIFIER, metamodelMock.entity(OWLClassR.class), descriptor, new FetchGraphWrapper(), + false), axioms)) .thenReturn(entityR); @@ -126,7 +128,7 @@ void loadEntityResolvesTypeFromProvidedAxiomsAndReconstructsEntity() { new Value<>(new LangString("Test", "en")))); final OWLClassR entityR = new OWLClassR(); when(entityConstructorMock - .reconstructEntity(new EntityConstructor.EntityConstructionParameters<>(IDENTIFIER, metamodelMock.entity(OWLClassR.class), descriptor, false), + .reconstructEntity(new EntityConstructor.EntityConstructionParameters<>(IDENTIFIER, metamodelMock.entity(OWLClassR.class), descriptor, new FetchGraphWrapper(), false), axioms)) .thenReturn(entityR); diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/AttributeBasedRowsToAxiomsQueryResultLoaderTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/AttributeBasedRowsToAxiomsQueryResultLoaderTest.java index 0621bcee1..188d3634d 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/AttributeBasedRowsToAxiomsQueryResultLoaderTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/AttributeBasedRowsToAxiomsQueryResultLoaderTest.java @@ -17,16 +17,32 @@ */ package cz.cvut.kbss.jopa.query.sparql.loader; +import cz.cvut.kbss.jopa.datatype.util.Pair; import cz.cvut.kbss.jopa.environment.OWLClassA; +import cz.cvut.kbss.jopa.environment.OWLClassD; +import cz.cvut.kbss.jopa.environment.OWLClassN; +import cz.cvut.kbss.jopa.environment.OWLClassU; +import cz.cvut.kbss.jopa.environment.Phone; +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.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.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.descriptors.EntityDescriptor; +import cz.cvut.kbss.jopa.model.metamodel.Attribute; +import cz.cvut.kbss.jopa.model.metamodel.IdentifiableEntityType; import cz.cvut.kbss.jopa.sessions.UnitOfWork; -import cz.cvut.kbss.ontodriver.exception.OntoDriverException; +import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingConfigGroup; +import cz.cvut.kbss.ontodriver.ResultSet; import cz.cvut.kbss.ontodriver.iteration.ResultRow; +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.LangString; +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; @@ -43,18 +59,22 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.emptyCollectionOf; +import static org.hamcrest.Matchers.hasItem; 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.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -69,60 +89,61 @@ class AttributeBasedRowsToAxiomsQueryResultLoaderTest { @Mock private MetamodelImpl metamodel; + private MetamodelMocks metamodelMocks; + private final Descriptor descriptor = new EntityDescriptor(); @BeforeEach void setUp() throws Exception { - MetamodelMocks metamodelMocks = new MetamodelMocks(); + this.metamodelMocks = new MetamodelMocks(); metamodelMocks.setMocks(metamodel); when(uow.getMetamodel()).thenReturn(metamodel); } @Test - void loadEntityInstanceReadsRowIntoAxiomsAndLoadsEntityFromThem() throws OntoDriverException { - final AttributeBasedRowsToAxiomsQueryResultLoader sut = - new AttributeBasedRowsToAxiomsQueryResultLoader<>(uow, OWLClassA.class, descriptor); + void loadEntityInstanceReadsRowIntoAxiomsAndLoadsEntityFromThem() throws Exception { + final EntityGraph fetchGraph = createFetchGraph(); + final AttributeBasedRowsToAxiomsQueryResultLoader sut = createOwlClassASut(fetchGraph); final OWLClassA instance = Generators.generateOwlClassAInstance(); instance.setTypes(Set.of()); final List resultRows = Stream.concat(mockResultRows(instance).stream(), mockResultRows( Generators.generateOwlClassAInstance()).stream()) .toList(); - when(uow.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(descriptor))).thenReturn(instance); + when(uow.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), any(AxiomBasedLoadingConfigGroup.class))).thenReturn(instance); final Optional result = resultRows.stream().map(sut::loadResult).filter(Optional::isPresent) .map(Optional::get).findFirst(); assertTrue(result.isPresent()); assertEquals(instance, result.get()); - verify(uow).readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(descriptor)); + verify(uow).readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(new AxiomBasedLoadingConfigGroup<>(instance.getUri(), descriptor, fetchGraph))); + } + + private EntityGraph createFetchGraph() { + final IdentifiableEntityType et = metamodel.entity(OWLClassA.class); + final EntityGraph fetchGraph = new EntityGraphImpl<>(et, metamodel); + fetchGraph.addAttributeNodes(et.getAttributes().toArray(Attribute[]::new)); + return fetchGraph; } - private static List mockResultRows(OWLClassA instance) throws OntoDriverException { + private static List mockResultRows(OWLClassA instance) throws Exception { final List allTypes = Stream.concat(Stream.of(OWLClassA.getClassIri()), instance.getTypes().stream()) .toList(); - final List rows = new ArrayList<>(allTypes.size()); - for (String type : allTypes) { - final ResultRow row = mock(ResultRow.class); - when(row.getColumnCount()).thenReturn(3); - when(row.getColumnNames()).thenReturn(List.of("x", "xstringAttribute", "xtypes")); - when(row.getObject(0, URI.class)).thenReturn(instance.getUri()); - if (instance.getStringAttribute() != null) { - when(row.isBound("xstringAttribute")).thenReturn(true); - when(row.getObject("xstringAttribute")).thenReturn(instance.getStringAttribute()); - } - when(row.isBound("xtypes")).thenReturn(true); - when(row.getObject("xtypes")).thenReturn(URI.create(type)); - rows.add(row); + final List> values = new ArrayList<>(); + values.add(new Pair<>("x", instance.getUri())); + if (instance.getStringAttribute() != null) { + values.add(new Pair<>("x_stringAttribute", instance.getStringAttribute())); } - return rows; + values.add(new Pair<>("x_types", String.join(AttributeEnumeratingSparqlAssemblyModifier.GROUP_CONCAT_SEPARATOR, allTypes))); + return List.of(mockRow(values)); } @Test void loadLastPendingLoadsLastPendingEntity() throws Exception { - final AttributeBasedRowsToAxiomsQueryResultLoader sut = - new AttributeBasedRowsToAxiomsQueryResultLoader<>(uow, OWLClassA.class, descriptor); + final EntityGraph fetchGraph = createFetchGraph(); + final AttributeBasedRowsToAxiomsQueryResultLoader sut = createOwlClassASut(fetchGraph); final OWLClassA instance = Generators.generateOwlClassAInstance(); instance.setTypes(Set.of()); final List resultRows = mockResultRows(instance); - when(uow.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(descriptor))).thenReturn(instance); + when(uow.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), any(AxiomBasedLoadingConfigGroup.class))).thenReturn(instance); resultRows.forEach(row -> { final Optional opt = sut.loadResult(row); assertFalse(opt.isPresent()); @@ -130,17 +151,26 @@ void loadLastPendingLoadsLastPendingEntity() throws Exception { final Optional result = sut.loadLastPending(); assertTrue(result.isPresent()); assertEquals(instance, result.get()); - verify(uow).readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(descriptor)); + verify(uow).readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(new AxiomBasedLoadingConfigGroup<>(instance.getUri(), descriptor, fetchGraph))); + } + + private AttributeBasedRowsToAxiomsQueryResultLoader createOwlClassASut( + EntityGraph fetchGraph) { + final List mappings = List.of( + new QueryVariableMapping("x", "x_stringAttribute", metamodelMocks.forOwlClassA().stringAttribute()), + new QueryVariableMapping("x", "x_types", metamodelMocks.forOwlClassA().typesSpec()) + ); + return new AttributeBasedRowsToAxiomsQueryResultLoader<>(uow, OWLClassA.class, descriptor, fetchGraph, () -> mappings); } @Test void loadResultDoesNotDuplicateAlreadyPresentAttributes() throws Exception { - final AttributeBasedRowsToAxiomsQueryResultLoader sut = - new AttributeBasedRowsToAxiomsQueryResultLoader<>(uow, OWLClassA.class, descriptor); + final EntityGraph fetchGraph = createFetchGraph(); + final AttributeBasedRowsToAxiomsQueryResultLoader sut = createOwlClassASut(fetchGraph); final OWLClassA instance = Generators.generateOwlClassAInstance(); instance.setTypes(Set.of()); final List resultRows = mockResultRows(instance); - when(uow.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(descriptor))).thenReturn(instance); + when(uow.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), any(AxiomBasedLoadingConfigGroup.class))).thenReturn(instance); resultRows.forEach(row -> { final Optional opt = sut.loadResult(row); assertFalse(opt.isPresent()); @@ -148,20 +178,19 @@ void loadResultDoesNotDuplicateAlreadyPresentAttributes() throws Exception { final Optional result = sut.loadLastPending(); assertTrue(result.isPresent()); final ArgumentCaptor>> captor = ArgumentCaptor.forClass(Collection.class); - verify(uow).readObjectFromAxioms(eq(OWLClassA.class), captor.capture(), eq(descriptor)); + verify(uow).readObjectFromAxioms(eq(OWLClassA.class), captor.capture(), eq(new AxiomBasedLoadingConfigGroup<>(instance.getUri(), descriptor, fetchGraph))); assertEquals(1, captor.getValue().stream() .filter(ax -> ax.getValue().equals(new Value<>(instance.getStringAttribute()))).count()); } @Test void loadResultHandlesUnboundAttributeVariables() throws Exception { - final AttributeBasedRowsToAxiomsQueryResultLoader sut = - new AttributeBasedRowsToAxiomsQueryResultLoader<>(uow, OWLClassA.class, descriptor); + final AttributeBasedRowsToAxiomsQueryResultLoader sut = createOwlClassASut(createFetchGraph()); final OWLClassA instance = Generators.generateOwlClassAInstance(); instance.setTypes(Set.of()); instance.setStringAttribute(null); final List resultRows = mockResultRows(instance); - when(uow.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(descriptor))).thenReturn(instance); + when(uow.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), any(AxiomBasedLoadingConfigGroup.class))).thenReturn(instance); resultRows.forEach(row -> { final Optional opt = sut.loadResult(row); assertFalse(opt.isPresent()); @@ -171,4 +200,184 @@ void loadResultHandlesUnboundAttributeVariables() throws Exception { assertNull(result.get().getStringAttribute()); assertThat(result.get().getTypes(), anyOf(nullValue(), emptyCollectionOf(String.class))); } + + @Test + void loadResultSupportsFetchGraphWithMultipleEntities() throws Exception { + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodel.entity(OWLClassD.class), metamodel); + final Subgraph sg = fetchGraph.addSubgraph("owlClassA"); + sg.addAttributeNodes("stringAttribute"); + final List owlClassDMappings = List.of( + new QueryVariableMapping("x", "x_types", null), + new QueryVariableMapping("x", "x_owlClassA", metamodelMocks.forOwlClassD().owlClassAAtt()), + new QueryVariableMapping("x_owlClassA", "x_owlClassA_stringAttribute", metamodelMocks.forOwlClassA() + .stringAttribute()), + new QueryVariableMapping("x_owlClassA", "x_owlClassA_types", metamodelMocks.forOwlClassA().typesSpec()) + ); + final AttributeBasedRowsToAxiomsQueryResultLoader sut = new AttributeBasedRowsToAxiomsQueryResultLoader<>( + uow, OWLClassD.class, descriptor, fetchGraph, () -> owlClassDMappings); + final OWLClassD instance = new OWLClassD(Generators.createIndividualIdentifier()); + instance.setOwlClassA(Generators.generateOwlClassAInstance()); + final List resultRows = mockResultRows(instance); + resultRows.forEach(row -> { + final Optional opt = sut.loadResult(row); + assertFalse(opt.isPresent()); + }); + sut.loadLastPending(); + final ArgumentCaptor>> captor = ArgumentCaptor.forClass(Collection.class); + verify(uow).readObjectFromAxioms(eq(OWLClassD.class), captor.capture(), eq(new AxiomBasedLoadingConfigGroup<>(instance.getUri(), descriptor, fetchGraph))); + assertThat(captor.getValue(), hasItem( + new AxiomImpl<>(NamedResource.create(instance.getUri()), Assertion.createClassAssertion(false), new Value<>(URI.create(Vocabulary.c_OwlClassD))))); + assertThat(captor.getValue(), hasItem(new AxiomImpl<>(NamedResource.create(instance.getUri()), Assertion.createObjectPropertyAssertion(URI.create(Vocabulary.p_h_hasA), false), new Value<>(instance.getOwlClassA() + .getUri())))); + assertThat(captor.getValue(), hasItem(new AxiomImpl<>(NamedResource.create(instance.getOwlClassA() + .getUri()), Assertion.createDataPropertyAssertion(URI.create(Vocabulary.p_a_stringAttribute), false), new Value<>(instance.getOwlClassA() + .getStringAttribute())))); + assertThat(captor.getValue(), hasItem(new AxiomImpl<>(NamedResource.create(instance.getOwlClassA() + .getUri()), Assertion.createClassAssertion(false), new Value<>(URI.create(Vocabulary.c_OwlClassA))))); + + instance.getOwlClassA().getTypes() + .forEach(t -> assertThat(captor.getValue(), hasItem(new AxiomImpl<>(NamedResource.create(instance.getOwlClassA() + .getUri()), Assertion.createClassAssertion(false), new Value<>(URI.create(t)))))); + } + + private static List mockResultRows(OWLClassD instance) throws Exception { + final List aTypes = Stream.concat(Stream.of(OWLClassA.getClassIri()), instance.getOwlClassA().getTypes() + .stream()).toList(); + final List rows = new ArrayList<>(aTypes.size()); + for (String type : aTypes) { + rows.add(mockRow(List.of( + new Pair<>("x", instance.getUri()), + new Pair<>("x_types", URI.create(Vocabulary.c_OwlClassD)), + new Pair<>("x_owlClassA", instance.getOwlClassA().getUri()), + new Pair<>("x_owlClassA_stringAttribute", instance.getOwlClassA().getStringAttribute()), + new Pair<>("x_owlClassA_types", type) + ))); + } + return rows; + } + + @Test + void loadResultSupportsGroupConcatenatedTypes() throws Exception { + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassA() + .entityType(), metamodel); + fetchGraph.addAttributeNodes("stringAttribute"); + final AttributeBasedRowsToAxiomsQueryResultLoader sut = new AttributeBasedRowsToAxiomsQueryResultLoader<>(uow, OWLClassA.class, descriptor, fetchGraph); + final OWLClassA instance = Generators.generateOwlClassAInstance(); + final Stream aTypes = Stream.concat(Stream.of(Vocabulary.c_OwlClassA), instance.getTypes().stream()); + final ResultRow row = mockRow(List.of( + new Pair<>("x", instance.getUri()), + new Pair<>("x_stringAttribute", instance.getStringAttribute()), + new Pair<>("x_types", aTypes.collect(Collectors.joining(AttributeEnumeratingSparqlAssemblyModifier.GROUP_CONCAT_SEPARATOR))) + )); + when(uow.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), any(AxiomBasedLoadingConfigGroup.class))).thenReturn(instance); + + final Optional intermediate = sut.loadResult(row); + assertFalse(intermediate.isPresent()); + final Optional result = sut.loadLastPending(); + assertTrue(result.isPresent()); + final ArgumentCaptor>> captor = ArgumentCaptor.forClass(Collection.class); + verify(uow).readObjectFromAxioms(eq(OWLClassA.class), captor.capture(), any(AxiomBasedLoadingConfigGroup.class)); + captor.getValue().forEach(ax -> assertEquals(instance.getUri(), ax.getSubject().getIdentifier())); + instance.getTypes().forEach(type -> assertTrue(captor.getValue().stream() + .anyMatch(ax -> ax.getValue().stringValue() + .equals(type)))); + assertTrue(captor.getValue().stream() + .anyMatch(ax -> ax.getValue().stringValue().equals(Vocabulary.c_OwlClassA))); + assertTrue(captor.getValue().stream() + .anyMatch(ax -> ax.getValue().stringValue().equals(instance.getStringAttribute()))); + } + + @Test + void loadResultAutomaticallyAddsClassAssertionForDirectEntityMappingWithoutProjectedTypes() throws Exception { + final AttributeBasedRowsToAxiomsQueryResultLoader sut = new AttributeBasedRowsToAxiomsQueryResultLoader<>(uow, Phone.class, descriptor, null); + final Phone phone = new Phone(Generators.createIndividualIdentifier(), "123456789", "Nokia"); + final ResultRow row = mockRow(List.of( + new Pair<>("x", phone.getUri()), + new Pair<>("x_number", phone.getNumber()), + new Pair<>("x_brand", phone.getBrand()) + )); + final ResultSet resultSet = mock(ResultSet.class); + when(resultSet.getColumnNames()).thenReturn(List.of("x", "x_number", "x_brand")); + when(uow.readObjectFromAxioms(eq(Phone.class), anyCollection(), any(AxiomBasedLoadingConfigGroup.class))).thenReturn(phone); + + sut.init(resultSet); + final Optional intermediate = sut.loadResult(row); + assertFalse(intermediate.isPresent()); + final Optional result = sut.loadLastPending(); + assertTrue(result.isPresent()); + final ArgumentCaptor>> captor = ArgumentCaptor.forClass(Collection.class); + verify(uow).readObjectFromAxioms(eq(Phone.class), captor.capture(), any(AxiomBasedLoadingConfigGroup.class)); + assertThat(captor.getValue(), hasItem(new AxiomImpl<>(NamedResource.create(phone.getUri()), Assertion.createClassAssertion(false), new Value<>(URI.create(Vocabulary.c_Phone))))); + } + + @Test + void loadResultSupportsGroupConcatenatedMultilingualStrings() throws Exception { + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassU() + .entityType(), metamodel); + fetchGraph.addAttributeNodes("singularStringAtt"); + final AttributeBasedRowsToAxiomsQueryResultLoader sut = new AttributeBasedRowsToAxiomsQueryResultLoader<>(uow, OWLClassU.class, descriptor, fetchGraph); + final OWLClassU instance = new OWLClassU(Generators.createIndividualIdentifier()); + final String concatenated = "value-en@en" + + AttributeEnumeratingSparqlAssemblyModifier.GROUP_CONCAT_SEPARATOR + "hodnota-cs@cs"; + final ResultRow row = mockRow(List.of( + new Pair<>("x", instance.getId()), + new Pair<>("x_singularStringAtt", concatenated) + )); + when(uow.readObjectFromAxioms(eq(OWLClassU.class), anyCollection(), any(AxiomBasedLoadingConfigGroup.class))).thenReturn(instance); + + final Optional intermediate = sut.loadResult(row); + assertFalse(intermediate.isPresent()); + final Optional result = sut.loadLastPending(); + assertTrue(result.isPresent()); + final ArgumentCaptor>> captor = ArgumentCaptor.forClass(Collection.class); + verify(uow).readObjectFromAxioms(eq(OWLClassU.class), captor.capture(), any(AxiomBasedLoadingConfigGroup.class)); + captor.getValue().forEach(ax -> assertEquals(instance.getId(), ax.getSubject().getIdentifier())); + assertTrue(captor.getValue().stream() + .anyMatch(ax -> ax.getValue().getValue().equals(new LangString("value-en", "en")))); + assertTrue(captor.getValue().stream() + .anyMatch(ax -> ax.getValue().getValue().equals(new LangString("hodnota-cs", "cs")))); + } + + @Test + void loadResultSupportsGroupConcatenatedStringsWithoutLanguageTags() throws Exception { + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassN() + .entityType(), metamodel); + fetchGraph.addAttributeNodes("pluralAnnotation"); + final AttributeBasedRowsToAxiomsQueryResultLoader sut = new AttributeBasedRowsToAxiomsQueryResultLoader<>(uow, OWLClassN.class, descriptor, fetchGraph); + final OWLClassN instance = new OWLClassN(Generators.createIndividualIdentifier().toString()); + final ResultRow row = mockRow(List.of(new Pair<>("x", URI.create(instance.getId())), new Pair<>("x_pluralAnnotation", "value-one@" + AttributeEnumeratingSparqlAssemblyModifier.GROUP_CONCAT_SEPARATOR + "value-two@"))); + when(uow.readObjectFromAxioms(eq(OWLClassN.class), anyCollection(), any(AxiomBasedLoadingConfigGroup.class))).thenReturn(instance); + + final Optional intermediate = sut.loadResult(row); + assertFalse(intermediate.isPresent()); + final Optional result = sut.loadLastPending(); + assertTrue(result.isPresent()); + final ArgumentCaptor>> captor = ArgumentCaptor.forClass(Collection.class); + verify(uow).readObjectFromAxioms(eq(OWLClassN.class), captor.capture(), any(AxiomBasedLoadingConfigGroup.class)); + captor.getValue().forEach(ax -> assertEquals(instance.getId(), ax.getSubject().getIdentifier().toString())); + assertTrue(captor.getValue().stream() + .anyMatch(ax -> ax.getValue().getValue().equals("value-one"))); + assertTrue(captor.getValue().stream() + .anyMatch(ax -> ax.getValue().getValue().equals("value-two"))); + + } + + private static ResultRow mockRow(List> values) throws Exception { + final ResultRow row = mock(ResultRow.class); + when(row.getColumnCount()).thenReturn(values.size()); + when(row.getColumnNames()).thenReturn(values.stream().map(Pair::first).toList()); + when(row.getObject(0, URI.class)).thenReturn((URI) values.get(0).second()); + for (Pair v : values) { + when(row.isBound(v.first())).thenReturn(true); + final Object val = v.second(); + if (val instanceof String str) { + when(row.getString(v.first())).thenReturn(str); + when(row.getObject(v.first())).thenReturn(str); + } else { + doReturn(val).when(row).getObject(v.first(), val.getClass()); + doReturn(val).when(row).getObject(v.first()); + } + } + return row; + } } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/AttributeEnumeratingSparqlAssemblyModifierTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/AttributeEnumeratingSparqlAssemblyModifierTest.java index 9fc9aa330..16bc5a6d3 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/AttributeEnumeratingSparqlAssemblyModifierTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/AttributeEnumeratingSparqlAssemblyModifierTest.java @@ -17,9 +17,17 @@ */ package cz.cvut.kbss.jopa.query.sparql.loader; +import cz.cvut.kbss.jopa.environment.OWLClassJ; +import cz.cvut.kbss.jopa.environment.OWLClassN; +import cz.cvut.kbss.jopa.environment.OWLClassP; +import cz.cvut.kbss.jopa.environment.OWLClassU; 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.model.EntityGraph; +import cz.cvut.kbss.jopa.model.EntityGraphImpl; +import cz.cvut.kbss.jopa.model.MetamodelImpl; +import cz.cvut.kbss.jopa.model.annotations.FetchType; import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.descriptors.EntityDescriptor; import cz.cvut.kbss.jopa.model.metamodel.IdentifiableEntityType; @@ -29,22 +37,28 @@ import cz.cvut.kbss.jopa.sessions.ConnectionWrapper; import cz.cvut.kbss.jopa.sessions.MetamodelProvider; import cz.cvut.kbss.jopa.utils.IdentifierTransformer; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import java.net.URI; +import static cz.cvut.kbss.jopa.query.sparql.loader.AttributeEnumeratingSparqlAssemblyModifier.GROUP_CONCAT_SEPARATOR; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalToCompressingWhiteSpace; +import static org.hamcrest.Matchers.not; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) class AttributeEnumeratingSparqlAssemblyModifierTest { private static MetamodelMocks metamodelMocks; @@ -52,6 +66,9 @@ class AttributeEnumeratingSparqlAssemblyModifierTest { @Mock private ConnectionWrapper connectionWrapper; + @Mock + private MetamodelImpl metamodel; + private final ParameterValueFactory valueFactory = new ParameterValueFactory(mock(MetamodelProvider.class)); private final Sparql11QueryParser parser = new Sparql11QueryParser(valueFactory); @@ -64,18 +81,22 @@ static void setUpBeforeAll() throws Exception { @BeforeEach void setUp() { when(connectionWrapper.getRepositoryMetadata()).thenReturn(() -> "test"); + metamodelMocks.setMocks(metamodel); } @Test - void modifyAlwaysAddsTriplePatternForEntityTypes() { + void modifyAlwaysAddsTriplePatternForEntityType() { final TokenStreamSparqlQueryHolder holder = parser.parseQuery("SELECT ?x WHERE { ?x a ?type . }"); final AttributeEnumeratingSparqlAssemblyModifier sut = createSut(metamodelMocks.forOwlClassD().entityType()); holder.setAssemblyModifier(sut); final String result = holder.assembleQuery(); - assertThat(result, equalToCompressingWhiteSpace("SELECT ?x ?xowlClassA ?xtypes WHERE { ?x a ?type . " + - "OPTIONAL { ?x " + strUri(Vocabulary.p_h_hasA) + " ?xowlClassA . } " + - "?x a ?xtypes . }")); + assertThat(result, projectsVariable("x")); + assertThat(result, containsString("?x a " + strUri(Vocabulary.c_OwlClassD) + " .")); + } + + private static ProjectsVariableMatcher projectsVariable(String variable) { + return new ProjectsVariableMatcher(variable); } private static String strUri(String uri) { @@ -83,12 +104,12 @@ private static String strUri(String uri) { } private AttributeEnumeratingSparqlAssemblyModifier createSut(IdentifiableEntityType et) { - return createSut(et, new EntityDescriptor()); + return createSut(et, new EntityDescriptor(), null); } private AttributeEnumeratingSparqlAssemblyModifier createSut(IdentifiableEntityType et, - Descriptor descriptor) { - return new AttributeEnumeratingSparqlAssemblyModifier(et, descriptor, connectionWrapper); + Descriptor descriptor, EntityGraph fetchGraph) { + return new AttributeEnumeratingSparqlAssemblyModifier(metamodel, et, descriptor, fetchGraph, connectionWrapper); } @Test @@ -98,9 +119,9 @@ void modifyAddsOptionalTriplePatternForEntityAttributesAndProjectsAttributeValue holder.setAssemblyModifier(sut); final String result = holder.assembleQuery(); - assertThat(result, equalToCompressingWhiteSpace("SELECT ?x ?xstringAttribute ?xtypes WHERE { ?x a ?type . " + - "OPTIONAL { ?x " + strUri(Vocabulary.p_a_stringAttribute) + " ?xstringAttribute . } " + - "?x a ?xtypes . }")); + assertThat(result, projectsVariable("x")); + assertThat(result, projectsVariable("x_stringAttribute")); + assertThat(result, containsString("OPTIONAL { ?x " + strUri(Vocabulary.p_a_stringAttribute) + " ?x_stringAttribute . }")); } @Test @@ -109,13 +130,12 @@ void modifyAddsOptionalTriplePatternWithGraphWhenDescriptorWithNonDefaultContext final URI ctx = Generators.createIndividualIdentifier(); final EntityDescriptor descriptor = new EntityDescriptor(ctx); final AttributeEnumeratingSparqlAssemblyModifier sut = createSut(metamodelMocks.forOwlClassA() - .entityType(), descriptor); + .entityType(), descriptor, null); holder.setAssemblyModifier(sut); final String result = holder.assembleQuery(); - assertThat(result, equalToCompressingWhiteSpace("SELECT ?x ?xstringAttribute ?xtypes WHERE { ?x a ?type . " + - "OPTIONAL { GRAPH <" + ctx + "> { ?x " + strUri(Vocabulary.p_a_stringAttribute) + " ?xstringAttribute . } } " + - "GRAPH <" + ctx + "> { ?x a ?xtypes . } }")); + assertThat(result, containsString("GRAPH <" + ctx + "> { ?x a ?x_types . }")); + assertThat(result, containsString("OPTIONAL { GRAPH <" + ctx + "> { ?x " + strUri(Vocabulary.p_a_stringAttribute) + " ?x_stringAttribute . } }")); } @Test @@ -125,36 +145,56 @@ void modifyAddsOptionalTriplePatternWithAttributeSpecifiedContextResolvedFromDes final EntityDescriptor descriptor = new EntityDescriptor(); descriptor.addAttributeContext(metamodelMocks.forOwlClassA().entityType().getAttribute("stringAttribute"), ctx); final AttributeEnumeratingSparqlAssemblyModifier sut = createSut(metamodelMocks.forOwlClassA() - .entityType(), descriptor); + .entityType(), descriptor, null); holder.setAssemblyModifier(sut); final String result = holder.assembleQuery(); - assertThat(result, equalToCompressingWhiteSpace("SELECT ?x ?xstringAttribute ?xtypes WHERE { ?x a ?type . " + - "OPTIONAL { GRAPH <" + ctx + "> { ?x " + strUri(Vocabulary.p_a_stringAttribute) + " ?xstringAttribute . } } " + - "?x a ?xtypes . }")); + assertThat(result, containsString("?x a ?x_types .")); + assertThat(result, containsString("OPTIONAL { GRAPH <" + ctx + "> { ?x " + strUri(Vocabulary.p_a_stringAttribute) + " ?x_stringAttribute . } }")); } @Test void modifyAddsOptionalTriplePatternsForSubtypeAttributes() { - final TokenStreamSparqlQueryHolder holder = parser.parseQuery("SELECT ?x WHERE { ?x a ?type . }"); - final AttributeEnumeratingSparqlAssemblyModifier sut = createSut(metamodelMocks.forOwlClassS().entityType()); - holder.setAssemblyModifier(sut); + when(metamodelMocks.forOwlClassR().rOwlClassAAtt().getFetchType()).thenReturn(FetchType.EAGER); + try { + final TokenStreamSparqlQueryHolder holder = parser.parseQuery("SELECT ?x WHERE { ?x a ?type . }"); + final AttributeEnumeratingSparqlAssemblyModifier sut = createSut(metamodelMocks.forOwlClassS() + .entityType()); + holder.setAssemblyModifier(sut); - final String result = holder.assembleQuery(); - assertThat(result, containsString(Vocabulary.P_R_STRING_ATTRIBUTE)); - assertThat(result, containsString(Vocabulary.P_HAS_A)); + final String result = holder.assembleQuery(); + assertThat(result, containsString(Vocabulary.P_R_STRING_ATTRIBUTE)); + assertThat(result, containsString(Vocabulary.P_HAS_A)); + } finally { + when(metamodelMocks.forOwlClassR().rOwlClassAAtt().getFetchType()).thenReturn(FetchType.LAZY); + } } @Test void modifyAddsTriplePatternWithoutOptionalWhenMinimumParticipationConstraintIsPresentAndGreaterThanZero() { + when(metamodelMocks.forOwlClassJ().setAttribute().getFetchType()).thenReturn(FetchType.EAGER); + try { + final TokenStreamSparqlQueryHolder holder = parser.parseQuery("SELECT ?x WHERE { ?x a ?type . }"); + final AttributeEnumeratingSparqlAssemblyModifier sut = createSut(metamodelMocks.forOwlClassJ() + .entityType()); + holder.setAssemblyModifier(sut); + + final String result = holder.assembleQuery(); + assertThat(result, containsString("?x " + strUri(Vocabulary.p_h_hasA) + " ?x_owlClassA . ")); + } finally { + when(metamodelMocks.forOwlClassJ().setAttribute().getFetchType()).thenReturn(FetchType.LAZY); + } + } + + @Test + void modifyExcludesLazilyLoadedAttributesFromFetching() { final TokenStreamSparqlQueryHolder holder = parser.parseQuery("SELECT ?x WHERE { ?x a ?type . }"); final AttributeEnumeratingSparqlAssemblyModifier sut = createSut(metamodelMocks.forOwlClassJ().entityType()); holder.setAssemblyModifier(sut); final String result = holder.assembleQuery(); - assertThat(result, equalToCompressingWhiteSpace("SELECT ?x ?xowlClassA ?xtypes WHERE { ?x a ?type . " + - "?x " + strUri(Vocabulary.p_h_hasA) + " ?xowlClassA . " + - "?x a ?xtypes . }")); + assertThat(result, not(projectsVariable("x_owlClassA"))); + assertThat(result, not(containsString("?x " + strUri(Vocabulary.p_h_hasA) + " ?x_owlClassA . "))); } @Test @@ -164,10 +204,135 @@ void modifyUsesDefaultContextForInferredAttributeWhenRepositoryProductIsGraphDBA final EntityDescriptor descriptor = new EntityDescriptor(ctx); when(connectionWrapper.getRepositoryMetadata()).thenReturn(() -> "GraphDB"); final AttributeEnumeratingSparqlAssemblyModifier sut = createSut(metamodelMocks.forOwlClassF() - .entityType(), descriptor); + .entityType(), descriptor, null); + holder.setAssemblyModifier(sut); + + final String result = holder.assembleQuery(); + assertThat(result, containsString("OPTIONAL { ?x " + strUri(Vocabulary.p_f_stringAttribute) + " ?x_secondStringAttribute . }")); + } + + @Test + void modifyUsesGroupConcatForPluralAssociationAttributesWithPlainIdentifierValueToReduceRowCountBlowup() { + final TokenStreamSparqlQueryHolder holder = parser.parseQuery("SELECT ?x WHERE { ?x a ?type . }"); + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassP() + .entityType(), metamodel); + fetchGraph.addAttributeNodes("individualUrls"); + final AttributeEnumeratingSparqlAssemblyModifier sut = createSut(metamodelMocks.forOwlClassP() + .entityType(), new EntityDescriptor(), fetchGraph); + holder.setAssemblyModifier(sut); + + final String result = holder.assembleQuery(); + assertThat(result, containsString("GROUP_CONCAT(DISTINCT ?x_individualUrls; SEPARATOR='" + GROUP_CONCAT_SEPARATOR + "') AS ?x_individualUrls_gc")); + assertThat(result, containsString("GROUP BY ?x")); + } + + @Test + void modifyUsesGroupConcatForTypesToReduceRowCountBlowup() { + final TokenStreamSparqlQueryHolder holder = parser.parseQuery("SELECT ?x WHERE { ?x a ?type . }"); + final AttributeEnumeratingSparqlAssemblyModifier sut = createSut(metamodelMocks.forOwlClassA().entityType()); + holder.setAssemblyModifier(sut); + + final String result = holder.assembleQuery(); + assertThat(result, containsString("GROUP_CONCAT(DISTINCT ?x_types; SEPARATOR='" + GROUP_CONCAT_SEPARATOR + "') AS ?x_types_gc")); + assertThat(result, containsString("GROUP BY ?x ?x_stringAttribute")); + } + + @Test + void modifyGroupConcatenatesOnlyMostSpecificEligibleAttributeToPreserveGrouping() { + final TokenStreamSparqlQueryHolder holder = parser.parseQuery("SELECT ?x WHERE { ?x a ?type . }"); + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassJ() + .entityType(), metamodel); + fetchGraph.addSubgraph("owlClassA"); + final AttributeEnumeratingSparqlAssemblyModifier sut = createSut(metamodelMocks.forOwlClassJ() + .entityType(), new EntityDescriptor(), fetchGraph); holder.setAssemblyModifier(sut); final String result = holder.assembleQuery(); - assertThat(result, containsString("OPTIONAL { ?x " + strUri(Vocabulary.p_f_stringAttribute) + " ?xsecondStringAttribute . }")); + assertThat(result, not(containsString("GROUP_CONCAT(DISTINCT ?x_owlClassA; SEPARATOR='" + GROUP_CONCAT_SEPARATOR + "') AS ?x_owlClassA_gc"))); + assertThat(result, containsString("GROUP_CONCAT(DISTINCT ?x_owlClassA_types; SEPARATOR='" + GROUP_CONCAT_SEPARATOR + "') AS ?x_owlClassA_types_gc")); + assertThat(result, containsString("GROUP BY ?x ?x_owlClassA")); + } + + @Test + void modifyGroupConcatenatesSingularMultilingualStringAttributeValues() { + final TokenStreamSparqlQueryHolder holder = parser.parseQuery("SELECT ?x WHERE { ?x a ?type . }"); + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassU() + .entityType(), metamodel); + fetchGraph.addAttributeNodes("singularStringAtt", "modified"); + final AttributeEnumeratingSparqlAssemblyModifier sut = createSut(metamodelMocks.forOwlClassU() + .entityType(), new EntityDescriptor(), fetchGraph); + holder.setAssemblyModifier(sut); + + final String result = holder.assembleQuery(); + assertThat(result, containsString("(GROUP_CONCAT(DISTINCT CONCAT(STR(?x_singularStringAtt), \"@\", LANG(?x_singularStringAtt)); SEPARATOR='" + GROUP_CONCAT_SEPARATOR + "') AS ?x_singularStringAtt_gc)")); + assertThat(result, containsString("GROUP BY ?x ?x_modified")); + } + + @Test + void modifyGroupConcatenatesPluralMultilingualStringAttributeValues() { + final TokenStreamSparqlQueryHolder holder = parser.parseQuery("SELECT ?x WHERE { ?x a ?type . }"); + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassU() + .entityType(), metamodel); + fetchGraph.addAttributeNodes("pluralStringAtt", "modified"); + final AttributeEnumeratingSparqlAssemblyModifier sut = createSut(metamodelMocks.forOwlClassU() + .entityType(), new EntityDescriptor(), fetchGraph); + holder.setAssemblyModifier(sut); + + final String result = holder.assembleQuery(); + assertThat(result, containsString("(GROUP_CONCAT(DISTINCT CONCAT(STR(?x_pluralStringAtt), \"@\", LANG(?x_pluralStringAtt)); SEPARATOR='" + GROUP_CONCAT_SEPARATOR + "') AS ?x_pluralStringAtt_gc)")); + assertThat(result, containsString("GROUP BY ?x ?x_modified")); + } + + @Test + void modifyGroupConcatenatesPluralStringAttributeValues() { + final TokenStreamSparqlQueryHolder holder = parser.parseQuery("SELECT ?x WHERE { ?x a ?type . }"); + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassN() + .entityType(), metamodel); + fetchGraph.addAttributeNodes("pluralAnnotation"); + final AttributeEnumeratingSparqlAssemblyModifier sut = createSut(metamodelMocks.forOwlClassN() + .entityType(), new EntityDescriptor(), fetchGraph); + holder.setAssemblyModifier(sut); + + final String result = holder.assembleQuery(); + assertThat(result, containsString("GROUP_CONCAT(DISTINCT CONCAT(STR(?x_pluralAnnotation), \"@\", LANG(?x_pluralAnnotation)); SEPARATOR='" + GROUP_CONCAT_SEPARATOR + "') AS ?x_pluralAnnotation_gc")); + assertThat(result, containsString("GROUP BY ?x")); + } + + @Test + void modifyDoesNotAddGroupConcatWhenQueryHasOrderBy() { + final TokenStreamSparqlQueryHolder holder = parser.parseQuery("SELECT ?x WHERE { ?x a ?type ; ?hasString ?stringAttribute } ORDER BY ?stringAttribute"); + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassN() + .entityType(), metamodel); + fetchGraph.addAttributeNodes("pluralAnnotation"); + final AttributeEnumeratingSparqlAssemblyModifier sut = createSut(metamodelMocks.forOwlClassN() + .entityType(), new EntityDescriptor(), fetchGraph); + holder.setAssemblyModifier(sut); + + final String result = holder.assembleQuery(); + assertThat(result, not(containsString("GROUP_CONCAT"))); + } + + /** + * Simplified Hamcrest matcher that checks if a SPARQL query projects a specific variable. The matcher extracts the + * projection part of the query (everything before the WHERE clause) and checks if it contains the specified + * variable prefixed with '?'. + */ + private static class ProjectsVariableMatcher extends TypeSafeMatcher { + private final String variable; + + public ProjectsVariableMatcher(String variable) { + this.variable = variable; + } + + @Override + protected boolean matchesSafely(String query) { + final String projectionPart = query.substring(0, query.indexOf("WHERE")); + return projectionPart.contains("?" + variable); + } + + @Override + public void describeTo(Description description) { + description.appendText("a SPARQL query that projects variable ?" + variable); + } } } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/EntityMappingQueryModifierTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/EntityMappingQueryModifierTest.java new file mode 100644 index 000000000..2fd2ee408 --- /dev/null +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/EntityMappingQueryModifierTest.java @@ -0,0 +1,107 @@ +package cz.cvut.kbss.jopa.query.sparql.loader; + +import cz.cvut.kbss.jopa.environment.OWLClassA; +import cz.cvut.kbss.jopa.environment.OWLClassD; +import cz.cvut.kbss.jopa.environment.OWLClassE; +import cz.cvut.kbss.jopa.environment.Vocabulary; +import cz.cvut.kbss.jopa.environment.utils.MetamodelMocks; +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.descriptors.EntityDescriptor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.util.List; +import java.util.Optional; + +import static cz.cvut.kbss.jopa.utils.IdentifierTransformer.stringifyIri; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItems; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class EntityMappingQueryModifierTest { + + private MetamodelMocks metamodelMocks; + + @Mock + private MetamodelImpl metamodel; + + @BeforeEach + void setUp() throws Exception { + this.metamodelMocks = new MetamodelMocks(); + metamodelMocks.setMocks(metamodel); + } + + @Test + void modifyGeneratesBasicGraphPatternForSpecifiedFetchGraph() { + final EntityMappingQueryModifier sut = new EntityMappingQueryModifier(metamodel, metamodelMocks.forOwlClassD() + .entityType(), + new EntityDescriptor(), false); + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassD() + .entityType(), metamodel); + fetchGraph.addAttributeNodes("owlClassA"); + final EntityMappingQueryModifier.QueryModification result = sut.modify(fetchGraph, "x"); + assertEquals(List.of("x_owlClassA", "x_types"), result.variableNames()); + assertThat(result.queryPart(), containsString("OPTIONAL { ?x " + stringifyIri(Vocabulary.P_HAS_A) + " ?x_owlClassA . }")); + assertThat(result.queryPart(), containsString("?x a " + stringifyIri(Vocabulary.c_OwlClassD) + " .")); + } + + @Test + void modifyGeneratesSubGraphPatternForSpecifiedFetchGraph() { + final EntityMappingQueryModifier sut = new EntityMappingQueryModifier(metamodel, metamodelMocks.forOwlClassD() + .entityType(), + new EntityDescriptor(), false); + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassD() + .entityType(), metamodel); + final Subgraph sg = fetchGraph.addSubgraph("owlClassA"); + sg.addAttributeNodes("stringAttribute"); + final EntityMappingQueryModifier.QueryModification result = sut.modify(fetchGraph, "x"); + assertThat(result.variableNames(), hasItems("x_owlClassA", "x_owlClassA_stringAttribute", "x_owlClassA_types")); + assertThat(result.queryPart(), containsString("?x a " + stringifyIri(Vocabulary.c_OwlClassD) + " .")); + assertThat(result.queryPart(), containsString("OPTIONAL { ?x " + stringifyIri(Vocabulary.P_HAS_A) + " ?x_owlClassA . " + + "?x_owlClassA a ?x_owlClassA_types . " + + "OPTIONAL { ?x_owlClassA " + stringifyIri(Vocabulary.p_a_stringAttribute) + " ?x_owlClassA_stringAttribute . } }")); + } + + @Test + void modifyGeneratesExactTypeTriplePatternWhenEntityClassDoesNotHaveTypesField() { + final EntityMappingQueryModifier sut = new EntityMappingQueryModifier(metamodel, metamodelMocks.forOwlClassE() + .entityType(), + new EntityDescriptor(), false); + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassE() + .entityType(), metamodel); + fetchGraph.addAttributeNodes("stringAttribute"); + final EntityMappingQueryModifier.QueryModification result = sut.modify(fetchGraph, "x"); + assertThat(result.queryPart(), containsString("?x a " + stringifyIri(Vocabulary.c_OwlClassE) + " .")); + } + + @Test + void modifyBindsEntityTypeAndGeneratesQueryVariableMappingForItWhenEntityClassHasNoTypesField() { + final EntityMappingQueryModifier sut = new EntityMappingQueryModifier(metamodel, metamodelMocks.forOwlClassE() + .entityType(), + new EntityDescriptor(), false); + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassE() + .entityType(), metamodel); + fetchGraph.addAttributeNodes("stringAttribute"); + final EntityMappingQueryModifier.QueryModification result = sut.modify(fetchGraph, "x"); + assertEquals(List.of("x_stringAttribute", "x_types"), result.variableNames()); + final Optional typesMapping = result.variables().stream() + .filter(qvm -> "x_types".equals(qvm.attributeVar())) + .findFirst(); + assertTrue(typesMapping.isPresent()); + assertNull(typesMapping.get().attribute()); + assertThat(result.queryPart(), containsString("BIND (" + stringifyIri(Vocabulary.c_OwlClassE) + " AS ?x_types)")); + } +} diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/FetchGraphProcessorTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/FetchGraphProcessorTest.java new file mode 100644 index 000000000..4729e4388 --- /dev/null +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/FetchGraphProcessorTest.java @@ -0,0 +1,73 @@ +package cz.cvut.kbss.jopa.query.sparql.loader; + +import cz.cvut.kbss.jopa.environment.OWLClassA; +import cz.cvut.kbss.jopa.environment.OWLClassD; +import cz.cvut.kbss.jopa.environment.utils.MetamodelMocks; +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 org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItems; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class FetchGraphProcessorTest { + + @Mock + private MetamodelImpl metamodel; + + private static MetamodelMocks metamodelMocks; + + @BeforeAll + static void setUpBeforeAll() throws Exception { + metamodelMocks = new MetamodelMocks(); + } + + @BeforeEach + void setUp() { + metamodelMocks.setMocks(metamodel); + } + + @Test + void mapFetchGraphToProjectionCreatesMappingsForAttributesOfSingleEntityClass() { + final FetchGraphProcessor sut = new FetchGraphProcessor(metamodel); + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassA() + .entityType(), metamodel); + fetchGraph.addAttributeNodes(metamodelMocks.forOwlClassA().stringAttribute()); + final List result = sut.mapFetchGraphToProjection(fetchGraph, metamodelMocks.forOwlClassA() + .entityType(), "x"); + assertThat(result, hasItems(new QueryVariableMapping("x", "x_stringAttribute", metamodelMocks.forOwlClassA() + .stringAttribute()), + new QueryVariableMapping("x", "x_types", metamodelMocks.forOwlClassA().typesSpec()))); + } + + @Test + void mapFetchGraphToProjectionCreatesMappingsForAttributesOfTwoLinkedEntities() { + final FetchGraphProcessor sut = new FetchGraphProcessor(metamodel); + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassD() + .entityType(), metamodel); + final Subgraph sg = fetchGraph.addSubgraph(metamodelMocks.forOwlClassD().owlClassAAtt()); + sg.addAttributeNodes(metamodelMocks.forOwlClassA().stringAttribute()); + final List result = sut.mapFetchGraphToProjection(fetchGraph, metamodelMocks.forOwlClassD() + .entityType(), "x"); + assertThat(result, hasItems( + new QueryVariableMapping("x", "x_owlClassA", metamodelMocks.forOwlClassD() + .owlClassAAtt()), + new QueryVariableMapping("x_owlClassA", "x_owlClassA_stringAttribute", metamodelMocks.forOwlClassA() + .stringAttribute()), + new QueryVariableMapping("x_owlClassA", "x_owlClassA_types", metamodelMocks.forOwlClassA() + .typesSpec()))); + } +} diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/SparqlQueryResultLoadingOptimizerTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/SparqlQueryResultLoadingOptimizerTest.java index 321165852..0aedc02e1 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/SparqlQueryResultLoadingOptimizerTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/SparqlQueryResultLoadingOptimizerTest.java @@ -19,10 +19,12 @@ import cz.cvut.kbss.jopa.environment.OWLClassA; import cz.cvut.kbss.jopa.environment.OWLClassB; +import cz.cvut.kbss.jopa.environment.OWLClassS; import cz.cvut.kbss.jopa.environment.Vocabulary; import cz.cvut.kbss.jopa.environment.utils.Generators; +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.QueryResultLoader; import cz.cvut.kbss.jopa.model.descriptors.EntityDescriptor; import cz.cvut.kbss.jopa.model.metamodel.IdentifiableEntityType; import cz.cvut.kbss.jopa.model.metamodel.PropertiesSpecification; @@ -43,11 +45,8 @@ import static cz.cvut.kbss.jopa.utils.IdentifierTransformer.stringifyIri; import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.mockito.ArgumentMatchers.any; +import static org.junit.jupiter.api.Assertions.assertNull; 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) @@ -67,8 +66,8 @@ void setUp() { } @Test - void optimizeQueryAssemblySetsUnboundPredicateObjectAssemblyModifierWhenQueryIsSelectAndResultClassIsEntityTypeWithProperties() { - final TokenStreamSparqlQueryHolder qh = spy(parser.parseQuery("SELECT ?s WHERE { ?s a ?type }")); + void chooseStrategyUsesUnboundPredicateObjectAssemblyModifierWhenQueryIsSelectAndResultClassIsEntityTypeWithProperties() { + final TokenStreamSparqlQueryHolder qh = parser.parseQuery("SELECT ?s WHERE { ?s a ?type }"); final SparqlQueryResultLoadingOptimizer sut = new SparqlQueryResultLoadingOptimizer(qh, uow, connectionWrapper); sut.enableOptimization(); when(uow.isEntityType(OWLClassB.class)).thenReturn(true); @@ -77,13 +76,13 @@ void optimizeQueryAssemblySetsUnboundPredicateObjectAssemblyModifierWhenQueryIsS final IdentifiableEntityType et = mock(IdentifiableEntityType.class); when(et.getProperties()).thenReturn(mock(PropertiesSpecification.class)); when(metamodel.entity(OWLClassB.class)).thenReturn(et); - sut.optimizeQueryAssembly(OWLClassB.class, new EntityDescriptor()); - verify(qh).setAssemblyModifier(any(UnboundPredicateObjectSparqlAssemblyModifier.class)); + final QueryResultLoadingStrategy strategy = sut.chooseStrategy(OWLClassB.class, new EntityDescriptor(), null); + assertInstanceOf(UnboundPredicateObjectSparqlAssemblyModifier.class, strategy.assemblyModifier()); } @Test - void optimizeQueryAssemblySetsAttributeEnumeratingAssemblyModifierWhenQueryIsSelectAndResultClassIsEntityTypeWithoutProperties() { - final TokenStreamSparqlQueryHolder qh = spy(parser.parseQuery("SELECT ?s WHERE { ?s a ?type }")); + void chooseStrategyUsesAttributeEnumeratingAssemblyModifierWhenQueryIsSelectAndResultClassIsEntityTypeWithoutProperties() { + final TokenStreamSparqlQueryHolder qh = parser.parseQuery("SELECT ?s WHERE { ?s a ?type }"); when(connectionWrapper.getRepositoryMetadata()).thenReturn(mock(RepositoryMetadata.class)); final SparqlQueryResultLoadingOptimizer sut = new SparqlQueryResultLoadingOptimizer(qh, uow, connectionWrapper); sut.enableOptimization(); @@ -93,42 +92,42 @@ void optimizeQueryAssemblySetsAttributeEnumeratingAssemblyModifierWhenQueryIsSel final IdentifiableEntityType et = mock(IdentifiableEntityType.class); when(et.getProperties()).thenReturn(null); when(metamodel.entity(OWLClassA.class)).thenReturn(et); - sut.optimizeQueryAssembly(OWLClassA.class, new EntityDescriptor()); - verify(qh).setAssemblyModifier(any(AttributeEnumeratingSparqlAssemblyModifier.class)); + final QueryResultLoadingStrategy strategy = sut.chooseStrategy(OWLClassA.class, new EntityDescriptor(), null); + assertInstanceOf(AttributeEnumeratingSparqlAssemblyModifier.class, strategy.assemblyModifier()); } @Test - void optimizeQueryAssemblyDoesNotOptimizeQueryWhenResultClassIsNotEntity() { - final TokenStreamSparqlQueryHolder qh = spy(parser.parseQuery("SELECT ?s WHERE { ?s a ?type }")); + void chooseStrategyDoesNotModifyQueryWhenResultClassIsNotEntity() { + final TokenStreamSparqlQueryHolder qh = parser.parseQuery("SELECT ?s WHERE { ?s a ?type }"); final SparqlQueryResultLoadingOptimizer sut = new SparqlQueryResultLoadingOptimizer(qh, uow, connectionWrapper); sut.enableOptimization(); - sut.optimizeQueryAssembly(URI.class, new EntityDescriptor()); - verify(qh, never()).setAssemblyModifier(any()); + final QueryResultLoadingStrategy strategy = sut.chooseStrategy(URI.class, new EntityDescriptor(), null); + assertNull(strategy.assemblyModifier()); } @Test - void optimizeQueryAssemblyDoesNotOptimizeQueryWhenLimitIsSet() { - final TokenStreamSparqlQueryHolder qh = spy(parser.parseQuery("SELECT ?s WHERE { ?s a ?type } LIMIT 1")); + void chooseStrategyDoesNotModifyQueryWhenLimitIsSet() { + final TokenStreamSparqlQueryHolder qh = parser.parseQuery("SELECT ?s WHERE { ?s a ?type } LIMIT 1"); final SparqlQueryResultLoadingOptimizer sut = new SparqlQueryResultLoadingOptimizer(qh, uow, connectionWrapper); sut.enableOptimization(); when(uow.isEntityType(OWLClassA.class)).thenReturn(true); - sut.optimizeQueryAssembly(OWLClassA.class, new EntityDescriptor()); - verify(qh, never()).setAssemblyModifier(any()); + final QueryResultLoadingStrategy strategy = sut.chooseStrategy(OWLClassA.class, new EntityDescriptor(), null); + assertNull(strategy.assemblyModifier()); } @Test - void optimizeQueryAssemblyDoesNotOptimizeQueryWhenOffsetIsSet() { - final TokenStreamSparqlQueryHolder qh = spy(parser.parseQuery("SELECT ?s WHERE { ?s a ?type } OFFSET 1")); + void chooseStrategyDoesNotModifyQueryWhenOffsetIsSet() { + final TokenStreamSparqlQueryHolder qh = parser.parseQuery("SELECT ?s WHERE { ?s a ?type } OFFSET 1"); final SparqlQueryResultLoadingOptimizer sut = new SparqlQueryResultLoadingOptimizer(qh, uow, connectionWrapper); sut.enableOptimization(); when(uow.isEntityType(OWLClassA.class)).thenReturn(true); - sut.optimizeQueryAssembly(OWLClassA.class, new EntityDescriptor()); - verify(qh, never()).setAssemblyModifier(any()); + final QueryResultLoadingStrategy strategy = sut.chooseStrategy(OWLClassA.class, new EntityDescriptor(), null); + assertNull(strategy.assemblyModifier()); } @Test - void optimizeQueryAssemblyOptimizesQueryWhenItContainsGraphClauseAndAttributeBasedOptimizerCanBeUser() { - final TokenStreamSparqlQueryHolder qh = spy(parser.parseQuery("SELECT ?s WHERE { GRAPH ?g { ?s a ?type } }")); + void chooseStrategyOptimizesQueryWhenItContainsGraphClauseAndAttributeBasedOptimizerCanBeUsed() { + final TokenStreamSparqlQueryHolder qh = parser.parseQuery("SELECT ?s WHERE { GRAPH ?g { ?s a ?type } }"); when(connectionWrapper.getRepositoryMetadata()).thenReturn(mock(RepositoryMetadata.class)); final SparqlQueryResultLoadingOptimizer sut = new SparqlQueryResultLoadingOptimizer(qh, uow, connectionWrapper); sut.enableOptimization(); @@ -137,22 +136,22 @@ void optimizeQueryAssemblyOptimizesQueryWhenItContainsGraphClauseAndAttributeBas when(uow.getMetamodel()).thenReturn(metamodel); final IdentifiableEntityType et = mock(IdentifiableEntityType.class); when(metamodel.entity(OWLClassA.class)).thenReturn(et); - sut.optimizeQueryAssembly(OWLClassA.class, new EntityDescriptor()); - verify(qh).setAssemblyModifier(any(AttributeEnumeratingSparqlAssemblyModifier.class)); + final QueryResultLoadingStrategy strategy = sut.chooseStrategy(OWLClassA.class, new EntityDescriptor(), null); + assertInstanceOf(AttributeEnumeratingSparqlAssemblyModifier.class, strategy.assemblyModifier()); } @Test - void optimizeQueryAssemblyDoesNotOptimizeQueryWhenProvidedDescriptorSpecifiesMoreThanOneContext() { - final TokenStreamSparqlQueryHolder qh = spy(parser.parseQuery("SELECT ?s WHERE { ?s a ?type }")); + void chooseStrategyDoesNotModifyQueryWhenProvidedDescriptorSpecifiesMoreThanOneContext() { + final TokenStreamSparqlQueryHolder qh = parser.parseQuery("SELECT ?s WHERE { ?s a ?type }"); final SparqlQueryResultLoadingOptimizer sut = new SparqlQueryResultLoadingOptimizer(qh, uow, connectionWrapper); sut.enableOptimization(); when(uow.isEntityType(OWLClassA.class)).thenReturn(true); - sut.optimizeQueryAssembly(OWLClassA.class, new EntityDescriptor(Set.of(Generators.createIndividualIdentifier(), Generators.createIndividualIdentifier()))); - verify(qh, never()).setAssemblyModifier(any()); + final QueryResultLoadingStrategy strategy = sut.chooseStrategy(OWLClassA.class, new EntityDescriptor(Set.of(Generators.createIndividualIdentifier(), Generators.createIndividualIdentifier())), null); + assertNull(strategy.assemblyModifier()); } @Test - void getQueryResultLoaderReturnsAttributeBasedRowsToAxiomsResultLoaderWhenQueryProjectsMultipleVariablesAndResultTypeIsEntity() { + void chooseStrategyReturnsAttributeBasedRowsToAxiomsResultLoaderWhenQueryProjectsMultipleVariablesAndResultTypeIsEntity() { final TokenStreamSparqlQueryHolder qh = parser.parseQuery("SELECT ?x ?stringAttribute ?types WHERE {" + "?x a " + stringifyIri(Vocabulary.c_OwlClassA) + " ;" + stringifyIri(Vocabulary.p_a_stringAttribute) + " ?stringAttribute ;" + @@ -162,7 +161,39 @@ void getQueryResultLoaderReturnsAttributeBasedRowsToAxiomsResultLoaderWhenQueryP final SparqlQueryResultLoadingOptimizer sut = new SparqlQueryResultLoadingOptimizer(qh, uow, connectionWrapper); sut.disableOptimization(); when(uow.isEntityType(OWLClassA.class)).thenReturn(true); - final QueryResultLoader result = sut.getQueryResultLoader(OWLClassA.class, new EntityDescriptor()); - assertInstanceOf(AttributeBasedRowsToAxiomsQueryResultLoader.class, result); + final QueryResultLoadingStrategy strategy = sut.chooseStrategy(OWLClassA.class, new EntityDescriptor(), null); + assertNull(strategy.assemblyModifier()); + assertInstanceOf(AttributeBasedRowsToAxiomsQueryResultLoader.class, strategy.createLoader()); + } + + @Test + void chooseStrategyUsesAttributeEnumeratingSparqlAssemblyModifierForEntityWithSubTypesWhenFetchGraphIsProvided() { + when(connectionWrapper.getRepositoryMetadata()).thenReturn(() -> ""); + final TokenStreamSparqlQueryHolder qh = parser.parseQuery("SELECT ?s WHERE { ?s a ?type }"); + final SparqlQueryResultLoadingOptimizer sut = new SparqlQueryResultLoadingOptimizer(qh, uow, connectionWrapper); + sut.enableOptimization(); + when(uow.isEntityType(OWLClassS.class)).thenReturn(true); + final MetamodelImpl metamodel = mock(MetamodelImpl.class); + when(uow.getMetamodel()).thenReturn(metamodel); + final IdentifiableEntityType et = mock(IdentifiableEntityType.class); + when(metamodel.entity(OWLClassS.class)).thenReturn(et); + final EntityGraph fetchGraph = new EntityGraphImpl<>(et, metamodel); + final QueryResultLoadingStrategy strategy = sut.chooseStrategy(OWLClassS.class, new EntityDescriptor(), fetchGraph); + assertInstanceOf(AttributeEnumeratingSparqlAssemblyModifier.class, strategy.assemblyModifier()); + } + + @Test + void chooseStrategyUsesAttributeEnumeratingSparqlAssemblyModifierForEntityWithPropertiesWhenFetchGraphIsProvided() { + when(connectionWrapper.getRepositoryMetadata()).thenReturn(() -> ""); + final TokenStreamSparqlQueryHolder qh = parser.parseQuery("SELECT ?s WHERE { ?s a ?type }"); + final SparqlQueryResultLoadingOptimizer sut = new SparqlQueryResultLoadingOptimizer(qh, uow, connectionWrapper); + sut.enableOptimization(); + when(uow.isEntityType(OWLClassB.class)).thenReturn(true); + final MetamodelImpl metamodel = mock(MetamodelImpl.class); + when(uow.getMetamodel()).thenReturn(metamodel); + final IdentifiableEntityType et = mock(IdentifiableEntityType.class); + when(metamodel.entity(OWLClassB.class)).thenReturn(et); + final QueryResultLoadingStrategy strategy = sut.chooseStrategy(OWLClassB.class, new EntityDescriptor(), null); + assertInstanceOf(AttributeEnumeratingSparqlAssemblyModifier.class, strategy.assemblyModifier()); } } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/TripleBasedRowsToAxiomsQueryResultLoaderTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/TripleBasedRowsToAxiomsQueryResultLoaderTest.java index 1518fee4e..29529c94b 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/TripleBasedRowsToAxiomsQueryResultLoaderTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/query/sparql/loader/TripleBasedRowsToAxiomsQueryResultLoaderTest.java @@ -26,6 +26,7 @@ import cz.cvut.kbss.jopa.model.descriptors.Descriptor; import cz.cvut.kbss.jopa.model.descriptors.EntityDescriptor; import cz.cvut.kbss.jopa.sessions.UnitOfWork; +import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingConfigGroup; import cz.cvut.kbss.jopa.vocabulary.RDF; import cz.cvut.kbss.ontodriver.exception.OntoDriverException; import cz.cvut.kbss.ontodriver.iteration.ResultRow; @@ -45,6 +46,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -83,13 +85,13 @@ void loadEntityInstanceReadsInitialRowsIntoAxiomsAndLoadsEntityFromThem() throws when(lastRow.getObject(1, URI.class)).thenReturn(URI.create(RDF.TYPE)); when(lastRow.getObject(2)).thenReturn(URI.create(Vocabulary.c_OwlClassA)); final List rows = Stream.concat(firstResultRows.stream(), Stream.of(lastRow)).toList(); - when(uow.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(descriptor))).thenReturn(instance); + when(uow.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), any(AxiomBasedLoadingConfigGroup.class))).thenReturn(instance); final Optional result = rows.stream().map(sut::loadResult).filter(Optional::isPresent) .map(Optional::get).findFirst(); assertTrue(result.isPresent()); assertEquals(instance, result.get()); - verify(uow).readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(descriptor)); + verify(uow).readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(new AxiomBasedLoadingConfigGroup<>(instance.getUri(), descriptor))); } private static List mockResultRows(OWLClassA instance) throws OntoDriverException { @@ -122,13 +124,13 @@ void loadResultLoadsMultipleInstancesFromConsecutiveRows() throws OntoDriverExce final List rows = Stream.concat(firstResultRows.stream(), secondResultRows.stream()) .collect(Collectors.toList()); rows.add(lastRow); - when(uow.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(descriptor))).thenReturn(instanceOne) - .thenReturn(instanceTwo); + when(uow.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), any(AxiomBasedLoadingConfigGroup.class))).thenReturn(instanceOne) + .thenReturn(instanceTwo); final List result = rows.stream().map(sut::loadResult).filter(Optional::isPresent) .map(Optional::get).toList(); assertEquals(List.of(instanceOne, instanceTwo), result); - verify(uow, times(2)).readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(descriptor)); + verify(uow, times(2)).readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), any(AxiomBasedLoadingConfigGroup.class)); } @Test @@ -137,13 +139,13 @@ void loadLastPendingLoadsEntityInstanceFromPendingAxioms() throws OntoDriverExce sut = new TripleBasedRowsToAxiomsQueryResultLoader<>(uow, OWLClassA.class, descriptor); final OWLClassA instance = Generators.generateOwlClassAInstance(); final List firstResultRows = mockResultRows(instance); - when(uow.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(descriptor))).thenReturn(instance); + when(uow.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), any(AxiomBasedLoadingConfigGroup.class))).thenReturn(instance); assertTrue(firstResultRows.stream().map(sut::loadResult).noneMatch(Optional::isPresent)); final Optional result = sut.loadLastPending(); assertTrue(result.isPresent()); assertEquals(instance, result.get()); - verify(uow).readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(descriptor)); + verify(uow).readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(new AxiomBasedLoadingConfigGroup<>(instance.getUri(), descriptor))); } @Test @@ -158,13 +160,13 @@ void loadEntityInstanceLoadsEntityUsingNormalUoWLoadWhenLoadFromAxiomsThrowsCard when(lastRow.getObject(1, URI.class)).thenReturn(URI.create(RDF.TYPE)); when(lastRow.getObject(2)).thenReturn(URI.create(Vocabulary.c_OwlClassA)); final List rows = Stream.concat(firstResultRows.stream(), Stream.of(lastRow)).toList(); - when(uow.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(descriptor))).thenThrow(CardinalityConstraintViolatedException.class); + when(uow.readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), any(AxiomBasedLoadingConfigGroup.class))).thenThrow(CardinalityConstraintViolatedException.class); when(uow.readObject(OWLClassA.class, instance.getUri(), descriptor)).thenReturn(instance); final Optional result = rows.stream().map(sut::loadResult).filter(Optional::isPresent) .map(Optional::get).findFirst(); assertTrue(result.isPresent()); - verify(uow).readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(descriptor)); + verify(uow).readObjectFromAxioms(eq(OWLClassA.class), anyCollection(), eq(new AxiomBasedLoadingConfigGroup<>(instance.getUri(), descriptor))); verify(uow).readObject(OWLClassA.class, instance.getUri(), descriptor); } } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/AbstractUnitOfWorkTestRunner.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/AbstractUnitOfWorkTestRunner.java index 591556e47..31b9ba99f 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/AbstractUnitOfWorkTestRunner.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/AbstractUnitOfWorkTestRunner.java @@ -33,6 +33,7 @@ import cz.cvut.kbss.jopa.proxy.lazy.gen.LazyLoadingEntityProxyGenerator; import cz.cvut.kbss.jopa.sessions.descriptor.LoadStateDescriptor; import cz.cvut.kbss.jopa.sessions.descriptor.LoadStateDescriptorFactory; +import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingConfigGroup; import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingParameters; import cz.cvut.kbss.jopa.sessions.util.CloneRegistrationDescriptor; import cz.cvut.kbss.jopa.sessions.util.LoadingParameters; @@ -562,14 +563,14 @@ void readObjectFromAxiomsLoadsEntityFromSpecifiedAxiomsAndReturnsIt() { new AxiomImpl<>(idResource, Assertion.createClassAssertion(false), new Value<>(NamedResource.create(OWLClassA.getClassIri()))), new AxiomImpl<>(idResource, Assertion.createDataPropertyAssertion(URI.create(Vocabulary.p_a_stringAttribute), false), new Value<>(entityA.getStringAttribute())) ); - when(storageMock.loadFromAxioms(new AxiomBasedLoadingParameters<>(OWLClassA.class, descriptor, false, axioms))).thenReturn(entityA); + when(storageMock.loadFromAxioms(new AxiomBasedLoadingParameters<>(OWLClassA.class, axioms, new AxiomBasedLoadingConfigGroup<>(id, descriptor)))).thenReturn(entityA); - final OWLClassA result = uow.readObjectFromAxioms(OWLClassA.class, axioms, descriptor); + final OWLClassA result = uow.readObjectFromAxioms(OWLClassA.class, axioms, new AxiomBasedLoadingConfigGroup<>(id, descriptor)); assertNotNull(result); assertEquals(entityA.getUri(), result.getUri()); assertEquals(entityA.getStringAttribute(), result.getStringAttribute()); assertTrue(uow.contains(result)); - verify(storageMock).loadFromAxioms(new AxiomBasedLoadingParameters<>(OWLClassA.class, descriptor, false, axioms)); + verify(storageMock).loadFromAxioms(new AxiomBasedLoadingParameters<>(OWLClassA.class, axioms, new AxiomBasedLoadingConfigGroup<>(id, descriptor))); assertNotSame(entityA, result); } } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/ReadOnlyUnitOfWorkTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/ReadOnlyUnitOfWorkTest.java index 41860fb3c..d8ca377ec 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/ReadOnlyUnitOfWorkTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/ReadOnlyUnitOfWorkTest.java @@ -37,6 +37,7 @@ import cz.cvut.kbss.jopa.sessions.change.ObjectChangeSet; import cz.cvut.kbss.jopa.sessions.descriptor.LoadStateDescriptor; import cz.cvut.kbss.jopa.sessions.descriptor.LoadStateDescriptorFactory; +import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingConfigGroup; import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingParameters; import cz.cvut.kbss.jopa.sessions.util.CloneRegistrationDescriptor; import cz.cvut.kbss.jopa.sessions.util.LoadingParameters; @@ -730,14 +731,14 @@ void readObjectFromAxiomsLoadsEntityFromSpecifiedAxiomsAndReturnsIt() { new AxiomImpl<>(idResource, Assertion.createClassAssertion(false), new Value<>(NamedResource.create(OWLClassA.getClassIri()))), new AxiomImpl<>(idResource, Assertion.createDataPropertyAssertion(URI.create(Vocabulary.p_a_stringAttribute), false), new Value<>(entityA.getStringAttribute())) ); - when(storageMock.loadFromAxioms(new AxiomBasedLoadingParameters<>(OWLClassA.class, descriptor, true, axioms))).thenReturn(entityA); + when(storageMock.loadFromAxioms(new AxiomBasedLoadingParameters<>(OWLClassA.class, axioms, new AxiomBasedLoadingConfigGroup<>(id, descriptor, null)))).thenReturn(entityA); - final OWLClassA result = uow.readObjectFromAxioms(OWLClassA.class, axioms, descriptor); + final OWLClassA result = uow.readObjectFromAxioms(OWLClassA.class, axioms, new AxiomBasedLoadingConfigGroup<>(id, descriptor, null)); assertNotNull(result); assertEquals(entityA.getUri(), result.getUri()); assertEquals(entityA.getStringAttribute(), result.getStringAttribute()); assertTrue(uow.contains(result)); - verify(storageMock).loadFromAxioms(new AxiomBasedLoadingParameters<>(OWLClassA.class, descriptor, true, axioms)); + verify(storageMock).loadFromAxioms(new AxiomBasedLoadingParameters<>(OWLClassA.class, axioms, new AxiomBasedLoadingConfigGroup<>(id, descriptor, null))); assertSame(entityA, result); } } diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/cache/TtlCacheManagerTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/cache/TtlCacheManagerTest.java index cf09e5689..249decb4f 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/cache/TtlCacheManagerTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/cache/TtlCacheManagerTest.java @@ -19,6 +19,7 @@ import cz.cvut.kbss.jopa.model.JOPAPersistenceProperties; import cz.cvut.kbss.jopa.model.descriptors.Descriptor; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -41,6 +42,11 @@ public void setUp() throws Exception { this.manager = new TtlCacheManager(Collections.emptyMap()); } + @AfterEach + public void tearDown() { + manager.close(); + } + @Test public void testConstructorNumberInvalid() throws Exception { final Map m = new HashMap<>(); 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 6dcd4df15..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 @@ -19,11 +19,16 @@ import cz.cvut.kbss.jopa.environment.OWLClassA; import cz.cvut.kbss.jopa.environment.OWLClassC; +import cz.cvut.kbss.jopa.environment.OWLClassL; import cz.cvut.kbss.jopa.environment.OWLClassM; import cz.cvut.kbss.jopa.environment.utils.Generators; import cz.cvut.kbss.jopa.environment.utils.MetamodelMocks; +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.MetamodelImpl; import cz.cvut.kbss.jopa.proxy.lazy.LazyLoadingListProxy; +import cz.cvut.kbss.jopa.sessions.util.FetchGraphWrapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -121,4 +126,43 @@ void createCreatesDescriptorWithNotLoadedForLazyLoadedAttributesContainingProxyI assertEquals(LoadState.LOADED, result.isLoaded(metamodelMocks.forOwlClassC().referencedListAtt())); assertEquals(LoadState.NOT_LOADED, result.isLoaded(metamodelMocks.forOwlClassC().simpleListAtt())); } + + @Test + void createForFetchGraphCreatesLoadStateDescriptorWithAttributesInFetchGraphLoadedAndOtherDataAttributesAsUnknown() { + final OWLClassM instance = new OWLClassM(); + instance.setKey(Generators.createIndividualIdentifier().toString()); + final MetamodelImpl metamodel = mock(MetamodelImpl.class); + metamodelMocks.setMocks(metamodel); + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassM() + .entityType(), metamodel); + fetchGraph.addAttributeNodes("intAttribute", "longAttribute", "doubleAttribute", "characterAttribute"); + final LoadStateDescriptor result = LoadStateDescriptorFactory.createForFetchGraph(instance, metamodelMocks.forOwlClassM() + .entityType(), new FetchGraphWrapper(fetchGraph)); + assertEquals(LoadState.LOADED, result.isLoaded(metamodelMocks.forOwlClassM().identifier())); + assertEquals(LoadState.LOADED, result.isLoaded(metamodelMocks.forOwlClassM().integerAttribute())); + assertEquals(LoadState.LOADED, result.isLoaded(metamodelMocks.forOwlClassM().longAttribute())); + assertEquals(LoadState.LOADED, result.isLoaded(metamodelMocks.forOwlClassM().doubleAttribute())); + assertEquals(LoadState.LOADED, result.isLoaded(metamodelMocks.forOwlClassM().characterAttribute())); + assertEquals(LoadState.UNKNOWN, result.isLoaded(metamodelMocks.forOwlClassM().dateAttribute())); + assertEquals(LoadState.UNKNOWN, result.isLoaded(metamodelMocks.forOwlClassM().integerSetAttribute())); + assertEquals(LoadState.UNKNOWN, result.isLoaded(metamodelMocks.forOwlClassM().booleanAttribute())); + assertEquals(LoadState.UNKNOWN, result.isLoaded(metamodelMocks.forOwlClassM().lexicalFormAttribute())); + } + + @Test + void createForFetchGraphCreatesLoadStateDescriptorWithObjectPropertyAttributesNotInFetchGraphAsNotLoaded() { + final OWLClassL instance = new OWLClassL(Generators.createIndividualIdentifier()); + final MetamodelImpl metamodel = mock(MetamodelImpl.class); + metamodelMocks.setMocks(metamodel); + final EntityGraph fetchGraph = new EntityGraphImpl<>(metamodelMocks.forOwlClassL() + .entityType(), metamodel); + fetchGraph.addAttributeNodes("simpleList", "singleA"); + final LoadStateDescriptor result = LoadStateDescriptorFactory.createForFetchGraph(instance, metamodelMocks.forOwlClassL() + .entityType(), new FetchGraphWrapper(fetchGraph)); + assertEquals(LoadState.LOADED, result.isLoaded(metamodelMocks.forOwlClassL().identifier())); + assertEquals(LoadState.LOADED, result.isLoaded(metamodelMocks.forOwlClassL().simpleListAtt())); + assertEquals(LoadState.LOADED, result.isLoaded(metamodelMocks.forOwlClassL().owlClassAAtt())); + assertEquals(LoadState.NOT_LOADED, result.isLoaded(metamodelMocks.forOwlClassL().referencedListAtt())); + assertEquals(LoadState.NOT_LOADED, result.isLoaded(metamodelMocks.forOwlClassL().setAttribute())); + } } diff --git a/jopa-integration-tests-jena/pom.xml b/jopa-integration-tests-jena/pom.xml index bbb68a512..7991c944a 100644 --- a/jopa-integration-tests-jena/pom.xml +++ b/jopa-integration-tests-jena/pom.xml @@ -5,7 +5,7 @@ cz.cvut.kbss.jopa jopa-all - 2.9.4 + 2.10.0 ../pom.xml 4.0.0 diff --git a/jopa-integration-tests-owlapi/pom.xml b/jopa-integration-tests-owlapi/pom.xml index a091e97bb..87361ee5b 100644 --- a/jopa-integration-tests-owlapi/pom.xml +++ b/jopa-integration-tests-owlapi/pom.xml @@ -5,7 +5,7 @@ cz.cvut.kbss.jopa jopa-all - 2.9.4 + 2.10.0 ../pom.xml 4.0.0 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 a4ae28d6a..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 @@ -123,4 +123,29 @@ public void querySupportsOptimizedEntityLoading() { public void optimizedEntityLoadingWorksForEntityClassesWithoutTypesField() { // OWL2Query does not support OPTIONAL } + + @Disabled + @Test + @Override + 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-rdf4j/pom.xml b/jopa-integration-tests-rdf4j/pom.xml index 0bf6b1496..6b29370b6 100644 --- a/jopa-integration-tests-rdf4j/pom.xml +++ b/jopa-integration-tests-rdf4j/pom.xml @@ -6,7 +6,7 @@ cz.cvut.kbss.jopa jopa-all - 2.9.4 + 2.10.0 ../pom.xml jopa-integration-tests-rdf4j diff --git a/jopa-integration-tests-rdf4j/src/test/java/cz/cvut/kbss/jopa/test/integration/rdf4j/FetchGraphTest.java b/jopa-integration-tests-rdf4j/src/test/java/cz/cvut/kbss/jopa/test/integration/rdf4j/FetchGraphTest.java new file mode 100644 index 000000000..7469acfa3 --- /dev/null +++ b/jopa-integration-tests-rdf4j/src/test/java/cz/cvut/kbss/jopa/test/integration/rdf4j/FetchGraphTest.java @@ -0,0 +1,16 @@ +package cz.cvut.kbss.jopa.test.integration.rdf4j; + +import cz.cvut.kbss.jopa.test.environment.Rdf4jDataAccessor; +import cz.cvut.kbss.jopa.test.environment.Rdf4jPersistenceFactory; +import cz.cvut.kbss.jopa.test.runner.FetchGraphRunner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FetchGraphTest extends FetchGraphRunner { + + private static final Logger LOG = LoggerFactory.getLogger(FetchGraphTest.class); + + public FetchGraphTest() { + super(LOG, new Rdf4jPersistenceFactory(), new Rdf4jDataAccessor()); + } +} diff --git a/jopa-integration-tests-virtuoso/pom.xml b/jopa-integration-tests-virtuoso/pom.xml index 0b858e2c1..686a042f4 100644 --- a/jopa-integration-tests-virtuoso/pom.xml +++ b/jopa-integration-tests-virtuoso/pom.xml @@ -6,7 +6,7 @@ cz.cvut.kbss.jopa jopa-all - 2.9.4 + 2.10.0 jopa-integration-tests-virtuoso diff --git a/jopa-integration-tests/pom.xml b/jopa-integration-tests/pom.xml index 92cfb0d90..bd076a8d2 100644 --- a/jopa-integration-tests/pom.xml +++ b/jopa-integration-tests/pom.xml @@ -6,7 +6,7 @@ cz.cvut.kbss.jopa jopa-all - 2.9.4 + 2.10.0 ../pom.xml 4.0.0 diff --git a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassDD.java b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassDD.java deleted file mode 100644 index cc973a420..000000000 --- a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassDD.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * JOPA - * Copyright (C) 2026 Czech Technical University in Prague - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3.0 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library. - */ -package cz.cvut.kbss.jopa.test; - -import cz.cvut.kbss.jopa.model.annotations.*; - -import java.net.URI; - -@SparqlResultSetMapping(name = OWLClassD.MAPPING_NAME, entities = { - @EntityResult(entityClass = OWLClassD.class, fields = { - @FieldResult(name = "uri", variable = "x"), - @FieldResult(name = "owlClassA", variable = "y") - }) -}) -@OWLClass(iri = Vocabulary.C_OWL_CLASS_D) -public class OWLClassDD implements HasUri { - - public static final String MAPPING_NAME = "OWLClassD.entityMapping"; - - @Id - private URI uri; - - @OWLObjectProperty(iri = Vocabulary.P_HAS_OWL_CLASS_A, fetch = FetchType.LAZY) - // @ParticipationConstraints({ - // @ParticipationConstraint(owlObjectIRI="http://new.owl#OWLClassA", min=1, - // max=1) - // }) - private OWLClassA owlClassA; - - public OWLClassDD() { - // Default public no-arg constructor - } - - public OWLClassDD(URI uri) { - this.uri = uri; - } - - /** - * @param uri the uri to set - */ - public void setUri(URI uri) { - this.uri = uri; - } - - /** - * @return the uri - */ - @Override - public URI getUri() { - return uri; - } - - public void setOwlClassA(OWLClassA owlClassA) { - this.owlClassA = owlClassA; - } - - public OWLClassA getOwlClassA() { - return owlClassA; - } - - @Override - public String toString() { - String out = "OWLClassD: uri = " + uri; - out += ", owlClassA = " + owlClassA; - return out; - } -} diff --git a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassEE.java b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassEE.java new file mode 100644 index 000000000..344a5024a --- /dev/null +++ b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassEE.java @@ -0,0 +1,48 @@ +package cz.cvut.kbss.jopa.test; + +import cz.cvut.kbss.jopa.model.annotations.Id; +import cz.cvut.kbss.jopa.model.annotations.OWLAnnotationProperty; +import cz.cvut.kbss.jopa.model.annotations.OWLClass; +import cz.cvut.kbss.jopa.model.annotations.OWLObjectProperty; +import cz.cvut.kbss.jopa.vocabulary.RDFS; + +import java.net.URI; +import java.util.Set; + +@OWLClass(iri = Vocabulary.C_OWL_CLASS_EE) +public class OWLClassEE implements HasUri { + + @Id(generated = true) + private URI uri; + + @OWLAnnotationProperty(iri = RDFS.LABEL) + private String label; + + @OWLObjectProperty(iri = Vocabulary.P_EE_HAS_E) + private Set eInstances; + + @Override + public URI getUri() { + return uri; + } + + public void setUri(URI uri) { + this.uri = uri; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public Set geteInstances() { + return eInstances; + } + + public void seteInstances(Set eInstances) { + this.eInstances = eInstances; + } +} diff --git a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassFF.java b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassFF.java deleted file mode 100644 index c549942e2..000000000 --- a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassFF.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * JOPA - * Copyright (C) 2026 Czech Technical University in Prague - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3.0 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library. - */ -package cz.cvut.kbss.jopa.test; - -import cz.cvut.kbss.jopa.model.annotations.FetchType; -import cz.cvut.kbss.jopa.model.annotations.Id; -import cz.cvut.kbss.jopa.model.annotations.Inferred; -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 java.net.URI; -import java.util.Set; - -@OWLClass(iri = Vocabulary.C_OWL_CLASS_F) -public class OWLClassFF implements HasUri { - - @Id - private URI uri; - - @Inferred - @OWLDataProperty(iri = Vocabulary.P_F_STRING_ATTRIBUTE) - private String secondStringAttribute; - - @OWLObjectProperty(iri = Vocabulary.P_F_HAS_SIMPLE_SET, fetch = FetchType.EAGER) - private Set simpleSet; - - public OWLClassFF() { - } - - public OWLClassFF(URI uri) { - this.uri = uri; - } - - @Override - public URI getUri() { - return uri; - } - - public void setUri(URI uri) { - this.uri = uri; - } - - public String getSecondStringAttribute() { - return secondStringAttribute; - } - - public void setSecondStringAttribute(String secondStringAttribute) { - this.secondStringAttribute = secondStringAttribute; - } - - public Set getSimpleSet() { - return simpleSet; - } - - public void setSimpleSet(Set simpleSet) { - this.simpleSet = simpleSet; - } - - @Override - public String toString() { - String out = "OWLClassFF: uri = " + uri; - out += ", secondStringAttribute = " + secondStringAttribute; - return out; - } -} 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/Vocabulary.java b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/Vocabulary.java index bc1511fd2..97f0fe8b0 100644 --- a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/Vocabulary.java +++ b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/Vocabulary.java @@ -62,6 +62,7 @@ public class Vocabulary { public static final String C_OWL_CLASS_Z = CLASS_IRI_BASE + "OWLClassZ"; public static final String C_OWL_CLASS_PART_CONSTR_IN_PARENT = CLASS_IRI_BASE + "OWLClassWithPartConstraintsInInterfaceParent"; public static final String C_OWL_CLASS_BB = CLASS_IRI_BASE + "OWLClassBB"; + public static final String C_OWL_CLASS_EE = CLASS_IRI_BASE + "OWLClassEE"; public static final String C_OWL_CLASS_Z_CHILD = CLASS_IRI_BASE + "OWLClassZChild"; public static final String C_OwlClassWithQueryAttr = CLASS_IRI_BASE + "OWLClassWithQueryAttr"; public static final String C_OwlClassWithQueryAttr2 = CLASS_IRI_BASE + "OWLClassWithQueryAttr2"; @@ -163,6 +164,7 @@ public class Vocabulary { public static final String P_BB_DOUBLE_ATTRIBUTE = ATTRIBUTE_IRI_BASE + "bbDoubleAttribute"; public static final String P_BB_CHAR_ATTRIBUTE = ATTRIBUTE_IRI_BASE + "bbCharAttribute"; + public static final String P_EE_HAS_E = ATTRIBUTE_IRI_BASE + "eeHasE"; public static final String P_HAS_H = ATTRIBUTE_IRI_BASE + "hasH"; diff --git a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/query/QueryTestEnvironment.java b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/query/QueryTestEnvironment.java index 79192dfa6..c40d1c05c 100644 --- a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/query/QueryTestEnvironment.java +++ b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/query/QueryTestEnvironment.java @@ -20,15 +20,34 @@ import cz.cvut.kbss.jopa.model.EntityManager; import cz.cvut.kbss.jopa.model.MultilingualString; import cz.cvut.kbss.jopa.model.descriptors.EntityDescriptor; -import cz.cvut.kbss.jopa.test.*; +import cz.cvut.kbss.jopa.test.OWLClassA; +import cz.cvut.kbss.jopa.test.OWLClassB; +import cz.cvut.kbss.jopa.test.OWLClassC; +import cz.cvut.kbss.jopa.test.OWLClassD; +import cz.cvut.kbss.jopa.test.OWLClassE; +import cz.cvut.kbss.jopa.test.OWLClassJ; +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; +import cz.cvut.kbss.jopa.test.Vocabulary; import cz.cvut.kbss.jopa.test.environment.Generators; import cz.cvut.kbss.jopa.test.environment.TestEnvironment; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Set; public final class QueryTestEnvironment { @@ -52,8 +71,7 @@ private QueryTestEnvironment() { } /** - * Generates and persists test data into the default context of the - * specified entity manager. + * Generates and persists test data into the default context of the specified entity manager. * * @param em EntityManager */ @@ -68,8 +86,7 @@ public static void generateTestData(EntityManager em) { /** * Generates and persists test data into the specified contexts. *

- * This method distributes the data approximately uniformly into all the - * specified contexts. + * This method distributes the data approximately uniformly into all the specified contexts. * * @param em EntityManager * @param contexts A collection of target contexts @@ -175,6 +192,9 @@ private static Map, List> generate() { m.put(OWLClassJ.class, generateOwlClassJInstances(aa)); m.put(OWLClassM.class, generateOwlClassMInstances()); m.put(OWLClassT.class, generateOwlClassTInstances(aa)); + final List things = generateThings(); + m.put(Thing.class, things); + m.put(OWLClassV.class, generateOwlClassVInstances(things)); m.put(OWLClassY.class, generateOwlClassYInstances()); return m; } @@ -311,6 +331,34 @@ private static List generateOwlClassMInstances() { return lst; } + private static List generateThings() { + final List lst = new ArrayList<>(); + for (int i = 0; i < ITEM_COUNT; i++) { + final Thing t = new Thing(); + t.setName("Thing-" + i + "-" + i); + t.setDescription(t.getName() + " description"); + lst.add(t); + } + return lst; + } + + private static List generateOwlClassVInstances(List things) { + final List lst = new ArrayList<>(); + for (int i = 0; i < ITEM_COUNT; i++) { + final OWLClassV v = new OWLClassV(); + v.setName("TestV-" + i); + v.setDescription(v.getName() + " description"); + v.setThings(new HashSet<>()); + for (Thing thing : things) { + if (Generators.randomBoolean()) { + v.getThings().add(thing); + } + } + lst.add(v); + } + return lst; + } + private static List generateOwlClassYInstances() { final List lst = new ArrayList<>(); for (int i = 0; i < ITEM_COUNT; i++) { @@ -327,6 +375,14 @@ private static List generateOwlClassYInstances() { y.getSingularString().set("fr", "nombre de valeurs de test " + i); break; } + y.setPluralString(Set.of( + new MultilingualString(Map.of("en", "Alt label one - " + i, + "cs", "Synonymum jedna - " + i, + "de", "Synonymum eins - " + i)), + new MultilingualString(Map.of("en", "Alt label two - " + i, + "cs", "Synonymum dva i " + i, + "de", "Synonymum zwei - " + i)) + )); lst.add(y); } return lst; 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 be02e6e30..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 @@ -19,14 +19,21 @@ import cz.cvut.kbss.jopa.exceptions.NoResultException; import cz.cvut.kbss.jopa.exceptions.NoUniqueResultException; +import cz.cvut.kbss.jopa.model.EntityGraph; +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; import cz.cvut.kbss.jopa.test.Vocabulary; import cz.cvut.kbss.jopa.test.environment.DataAccessor; import cz.cvut.kbss.jopa.test.environment.Generators; @@ -43,16 +50,21 @@ import java.util.Comparator; import java.util.Date; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; 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; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -450,4 +462,113 @@ public void querySupportsMappingResultsDirectlyToEntity() { assertThat(result.get(i).getTypes(), hasItems(entities.get(i).getTypes().toArray(new String[]{}))); } } + + @Test + public void querySupportsFetchGraphs() { + final EntityGraph fetchGraph = getEntityManager().createEntityGraph(OWLClassV.class); + fetchGraph.addAttributeNodes("name", "description"); + final Subgraph subgraph = fetchGraph.addSubgraph("things"); + subgraph.addAttributeNodes("name"); + final List entities = QueryTestEnvironment.getData(OWLClassV.class); + final List result = getEntityManager().createNativeQuery("SELECT ?x WHERE { ?x a ?type . }", OWLClassV.class) + .setParameter("type", URI.create(Vocabulary.C_OWL_CLASS_V)) + .setHint(QueryHints.FETCH_GRAPH, fetchGraph) + .getResultList(); + assertEquals(entities.size(), result.size()); + result.forEach(c -> { + final Optional expected = entities.stream().filter(v -> v.getUri().equals(c.getUri())) + .findFirst(); + assertTrue(expected.isPresent()); + assertEquals(expected.get().getName(), c.getName()); + assertEquals(expected.get().getDescription(), c.getDescription()); + assertNotNull(c.getThings()); + assertEquals(expected.get().getThings().size(), c.getThings().size()); + c.getThings().forEach(t -> { + assertNotNull(t.getName()); + assertNull(t.getDescription()); + }); + }); + } + + @Test + public void querySupportsMappingResultsDirectlyToEntityWithoutProjectingTypes() { + final List entities = QueryTestEnvironment.getData(OWLClassE.class); + final List result = getEntityManager().createNativeQuery(""" + SELECT ?x ?stringAttribute WHERE { + ?x a ?type ; + ?hasString ?stringAttribute . + }""", OWLClassE.class) + .setParameter("type", URI.create(Vocabulary.C_OWL_CLASS_E)) + .setParameter("hasString", URI.create(Vocabulary.P_E_STRING_ATTRIBUTE)) + .getResultList(); + entities.sort(Comparator.comparing(OWLClassE::getUri)); + result.sort(Comparator.comparing(OWLClassE::getUri)); // Sort here instead of in the query due to OWL2Query lacking proper sorting support + assertEquals(entities.size(), result.size()); + for (int i = 0; i < entities.size(); i++) { + assertEquals(entities.get(i).getUri(), result.get(i).getUri()); + assertEquals(entities.get(i).getStringAttribute(), result.get(i).getStringAttribute()); + } + } + + @Test + public void querySupportsGroupConcatMultilingualStringsBasedOnFetchGraph() { + final List instances = QueryTestEnvironment.getData(OWLClassY.class); + instances.sort(Comparator.comparing(OWLClassY::getUri)); + final EntityGraph fetchGraph = getEntityManager().createEntityGraph(OWLClassY.class); + fetchGraph.addAttributeNodes("singularString", "pluralString"); + + final List result = getEntityManager().createQuery("SELECT y FROM OWLClassY y", OWLClassY.class) + .setHint(QueryHints.FETCH_GRAPH, fetchGraph) + .getResultList(); + result.sort(Comparator.comparing(OWLClassY::getUri)); + assertEquals(instances.size(), result.size()); + for (int i = 0; i < instances.size(); i++) { + assertEquals(instances.get(i).getUri(), result.get(i).getUri()); + assertEquals(instances.get(i).getSingularString(), result.get(i).getSingularString()); + assertEquals(instances.get(i).getPluralString().size(), result.get(i).getPluralString().size()); + final int index = i; + instances.get(i).getPluralString().forEach((mls) -> mls.getValue() + .forEach((lang, value) -> assertTrue(result.get(index) + .getPluralString() + .stream() + .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()); + }); + } } diff --git a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/runner/FetchGraphRunner.java b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/runner/FetchGraphRunner.java new file mode 100644 index 000000000..eb6b1de3f --- /dev/null +++ b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/runner/FetchGraphRunner.java @@ -0,0 +1,61 @@ +package cz.cvut.kbss.jopa.test.runner; + +import cz.cvut.kbss.jopa.model.EntityGraph; +import cz.cvut.kbss.jopa.model.Subgraph; +import cz.cvut.kbss.jopa.query.QueryHints; +import cz.cvut.kbss.jopa.test.OWLClassE; +import cz.cvut.kbss.jopa.test.OWLClassEE; +import cz.cvut.kbss.jopa.test.environment.DataAccessor; +import cz.cvut.kbss.jopa.test.environment.PersistenceFactory; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public abstract class FetchGraphRunner extends BaseRunner { + + public FetchGraphRunner(Logger logger, PersistenceFactory persistenceFactory, DataAccessor dataAccessor) { + super(logger, persistenceFactory, dataAccessor); + } + + @Test + void selectWithFetchGraphSupportsPluralReferenceAttributes() { + this.em = getEntityManager("selectWithFetchGraphSupportsPluralReferenceAttributes", false); + final OWLClassEE instance = new OWLClassEE(); + instance.setLabel("Label"); + instance.seteInstances(IntStream.range(0, 3).mapToObj(i -> { + final OWLClassE e = new OWLClassE(); + e.setStringAttribute("String " + i); + return e; + }).collect(Collectors.toSet())); + transactional(() -> { + em.persist(instance); + instance.geteInstances().forEach(em::persist); + }); + + final EntityGraph fetchGraph = em.createEntityGraph(OWLClassEE.class); + final Subgraph subgraph = fetchGraph.addSubgraph("eInstances"); + subgraph.addAttributeNodes("stringAttribute"); + final List result = em.createQuery("SELECT e FROM " + OWLClassEE.class.getSimpleName() + " e", OWLClassEE.class) + .setHint(QueryHints.FETCH_GRAPH, fetchGraph) + .getResultList(); + assertEquals(1, result.size()); + final OWLClassEE eeResult = result.get(0); + assertNull(eeResult.getLabel()); + assertEquals(instance.geteInstances().size(), eeResult.geteInstances().size()); + instance.geteInstances().forEach(e -> { + final Optional eResult = eeResult.geteInstances().stream() + .filter(eeInstance -> eeInstance.getUri().equals(e.getUri())) + .findFirst(); + assertTrue(eResult.isPresent()); + assertEquals(e.getStringAttribute(), eResult.get().getStringAttribute()); + }); + } +} diff --git a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/runner/UpdateOperationsMultiContextRunner.java b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/runner/UpdateOperationsMultiContextRunner.java index 1fcf0580f..8560f4399 100644 --- a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/runner/UpdateOperationsMultiContextRunner.java +++ b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/runner/UpdateOperationsMultiContextRunner.java @@ -25,6 +25,7 @@ import cz.cvut.kbss.jopa.test.environment.Generators; import cz.cvut.kbss.jopa.test.environment.PersistenceFactory; import cz.cvut.kbss.jopa.test.environment.TestEnvironmentUtils; +import cz.cvut.kbss.jopa.vocabulary.RDFS; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -238,8 +239,7 @@ void testUpdateRemoveFromReferencedListInContext() { em.getTransaction().commit(); final OWLClassC resC = findRequired(OWLClassC.class, entityC.getUri(), cDescriptor); - assertEquals(entityC.getReferencedList().size() - removed.size(), resC.getReferencedList() - .size()); + assertEquals(entityC.getReferencedList().size() - removed.size(), resC.getReferencedList().size()); for (OWLClassA a : removed) { final OWLClassA resA = em.find(OWLClassA.class, a.getUri(), lstDescriptor); assertNotNull(resA); @@ -315,8 +315,8 @@ void testUpdateObjectPropertyInContextWithAssertionInContext() { }); assertTrue(em.createNativeQuery("ASK { GRAPH ?g { ?s ?p ?o. }}", Boolean.class).setParameter("g", CONTEXT_TWO) - .setParameter("s", entityD.getUri()).setParameter("p", URI.create(Vocabulary.P_HAS_OWL_CLASS_A)) - .setParameter("o", newA.getUri()).getSingleResult()); + .setParameter("s", entityD.getUri()).setParameter("p", URI.create(Vocabulary.P_HAS_OWL_CLASS_A)) + .setParameter("o", newA.getUri()).getSingleResult()); final OWLClassD result = findRequired(OWLClassD.class, entityD.getUri(), dDescriptor); assertNotNull(result.getOwlClassA()); assertEquals(newA.getUri(), result.getOwlClassA().getUri()); @@ -336,7 +336,7 @@ void updateSupportsObjectPropertyWithMultipleContexts() { entityF.setSimpleSet(new HashSet<>(Arrays.asList(entityA, entityA2))); final Descriptor descriptor = new EntityDescriptor(); descriptor.addAttributeContext(fieldSpecification(OWLClassF.class, "simpleSet"), CONTEXT_ONE) - .addAttributeContext(fieldSpecification(OWLClassF.class, "simpleSet"), CONTEXT_TWO); + .addAttributeContext(fieldSpecification(OWLClassF.class, "simpleSet"), CONTEXT_TWO); transactional(() -> em.merge(entityF, descriptor)); final OWLClassF result = findRequired(OWLClassF.class, entityF.getUri(), descriptor); @@ -354,7 +354,7 @@ void updateWithinTransactionSupportsObjectPropertyWithMultipleContexts() { final Descriptor descriptor = new EntityDescriptor(); descriptor.addAttributeContext(fieldSpecification(OWLClassF.class, "simpleSet"), CONTEXT_ONE) - .addAttributeContext(fieldSpecification(OWLClassF.class, "simpleSet"), CONTEXT_TWO); + .addAttributeContext(fieldSpecification(OWLClassF.class, "simpleSet"), CONTEXT_TWO); transactional(() -> { final OWLClassF toUpdate = findRequired(OWLClassF.class, entityF.getUri(), descriptor); toUpdate.setSimpleSet(new HashSet<>(Arrays.asList(entityA, entityA2))); @@ -367,4 +367,32 @@ void updateWithinTransactionSupportsObjectPropertyWithMultipleContexts() { assertTrue(result.getSimpleSet().stream().anyMatch(a -> a.getUri().equals(entityA.getUri()))); assertTrue(result.getSimpleSet().stream().anyMatch(a -> a.getUri().equals(entityA2.getUri()))); } + + /** + * Bug #437 + */ + @Test + void updateOnEntityInAnnotationBasedContextLoadedWithQuerySavesValueInCorrectContext() { + this.em = getEntityManager("updateOnEntityInAnnotationBasedContextLoadedWithQuerySavesValueInCorrectContext", false); + final ClassInContext entity = new ClassInContext(); + entity.setLabel("Initial label"); + transactional(() -> em.persist(entity)); + + transactional(() -> { + final List loaded = em.createNativeQuery("SELECT ?x WHERE { GRAPH ?g {?x a ?type } }", ClassInContext.class) + .setParameter("type", URI.create(Vocabulary.CLASS_IRI_BASE + "classInContext")) + .getResultList(); + assertEquals(1, loaded.size()); + final ClassInContext loadedEntity = loaded.get(0); + loadedEntity.setLabel("Updated label"); + }); + + final ClassInContext updatedEntity = em.find(ClassInContext.class, entity.getId()); + assertEquals("Updated label", updatedEntity.getLabel()); + assertTrue(em.createNativeQuery("ASK WHERE { GRAPH ?g { ?entity ?hasLabel ?label . } }", Boolean.class) + .setParameter("entity", entity) + .setParameter("g", URI.create("https://example.com/context")) + .setParameter("hasLabel", URI.create(RDFS.LABEL)) + .getSingleResult()); + } } diff --git a/jopa-integration-tests/src/test/java/cz/cvut/kbss/jopa/test/integration/BugTest.java b/jopa-integration-tests/src/test/java/cz/cvut/kbss/jopa/test/integration/BugTest.java index 134730856..f254b47b7 100644 --- a/jopa-integration-tests/src/test/java/cz/cvut/kbss/jopa/test/integration/BugTest.java +++ b/jopa-integration-tests/src/test/java/cz/cvut/kbss/jopa/test/integration/BugTest.java @@ -312,7 +312,10 @@ void isInferredTriggersLazyLoadingForLazyLoadingProxy() throws Exception { ownerDesc.addAssertion(Assertion.createObjectPropertyAssertion(URI.create(Vocabulary.p_l_aSetAttribute), false)); final List> ownerAxioms = List.of( new AxiomImpl<>(subject, classAssertion, new Value<>(URI.create(Vocabulary.C_OWL_CLASS_L))), - new AxiomImpl<>(subject, hasAAssertion, new Value<>(NamedResource.create(ref.getUri()))) + new AxiomImpl<>(subject, hasAAssertion, new Value<>(NamedResource.create(ref.getUri()))), + new AxiomImpl<>(subject, Assertion.createObjectPropertyAssertion(URI.create(Vocabulary.p_l_simpleListAttribute), false), new Value<>(NamedResource.create(ref.getUri()))), + new AxiomImpl<>(subject, Assertion.createObjectPropertyAssertion(URI.create(Vocabulary.p_l_referencedListAttribute), false), new Value<>(NamedResource.create(ref.getUri()))), + new AxiomImpl<>(subject, Assertion.createObjectPropertyAssertion(URI.create(Vocabulary.p_l_aSetAttribute), false), new Value<>(NamedResource.create(ref.getUri()))) ); when(connectionMock.find(ownerDesc)).thenReturn(ownerAxioms); final AxiomDescriptor singleAAttDesc = new AxiomDescriptor(subject); diff --git a/jopa-integration-tests/src/test/java/cz/cvut/kbss/jopa/test/integration/FetchGraphEntityLoadingTest.java b/jopa-integration-tests/src/test/java/cz/cvut/kbss/jopa/test/integration/FetchGraphEntityLoadingTest.java new file mode 100644 index 000000000..a1d46facb --- /dev/null +++ b/jopa-integration-tests/src/test/java/cz/cvut/kbss/jopa/test/integration/FetchGraphEntityLoadingTest.java @@ -0,0 +1,148 @@ +package cz.cvut.kbss.jopa.test.integration; + +import cz.cvut.kbss.jopa.model.EntityGraph; +import cz.cvut.kbss.jopa.model.Subgraph; +import cz.cvut.kbss.jopa.query.QueryHints; +import cz.cvut.kbss.jopa.test.OWLClassA; +import cz.cvut.kbss.jopa.test.OWLClassD; +import cz.cvut.kbss.jopa.test.OWLClassS; +import cz.cvut.kbss.jopa.test.OWLClassU; +import cz.cvut.kbss.jopa.test.Vocabulary; +import cz.cvut.kbss.jopa.test.environment.Generators; +import cz.cvut.kbss.ontodriver.ResultSet; +import cz.cvut.kbss.ontodriver.Statement; +import cz.cvut.kbss.ontodriver.exception.OntoDriverException; +import cz.cvut.kbss.ontodriver.iteration.ResultRow; +import cz.cvut.kbss.ontodriver.iteration.ResultSetIterator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.net.URI; +import java.util.List; + +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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class FetchGraphEntityLoadingTest extends IntegrationTestBase { + + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + } + + @Test + void fetchGraphBasedQueryResultLoadingWorks() throws OntoDriverException { + when(connectionMock.getRepositoryMetadata()).thenReturn(() -> ""); + final OWLClassD expected = new OWLClassD(Generators.generateUri()); + final OWLClassA expectedA = Generators.generateOwlClassA(); + expected.setOwlClassA(expectedA); + mockQueryResultSet(expected); + final EntityGraph fetchGraph = em.createEntityGraph(OWLClassD.class); + final Subgraph subgraph = fetchGraph.addSubgraph("owlClassA"); + subgraph.addAttributeNodes("stringAttribute"); + + final List result = em.createQuery("SELECT d FROM OWLClassD d", OWLClassD.class) + .setHint(QueryHints.FETCH_GRAPH, fetchGraph) + .getResultList(); + assertEquals(1, result.size()); + assertEquals(expected.getUri(), result.get(0).getUri()); + assertNotNull(result.get(0).getOwlClassA()); + assertEquals(expectedA.getUri(), result.get(0).getOwlClassA().getUri()); + assertEquals(expectedA.getStringAttribute(), result.get(0).getOwlClassA().getStringAttribute()); + verify(connectionMock, never()).find(any()); + } + + private void mockQueryResultSet(OWLClassD expected) throws OntoDriverException { + final Statement statementMock = mock(Statement.class); + final ResultSet resultSetMock = mock(ResultSet.class); + final ResultSetIterator resultSetIteratorMock = mock(ResultSetIterator.class); + final ResultRow resultRowMock = mock(ResultRow.class); + when(connectionMock.createStatement()).thenReturn(statementMock); + when(statementMock.executeQuery(anyString())).thenReturn(resultSetMock); + when(resultSetMock.iterator()).thenReturn(resultSetIteratorMock); + when(resultSetIteratorMock.next()).thenReturn(resultRowMock); + when(resultSetIteratorMock.hasNext()).thenReturn(true, false); + when(resultRowMock.getColumnCount()).thenReturn(5); + when(resultRowMock.isBound("x")).thenReturn(true); + when(resultRowMock.getObject("x", URI.class)).thenReturn(expected.getUri()); + when(resultRowMock.getObject(0, URI.class)).thenReturn(expected.getUri()); + when(resultRowMock.isBound("x_types")).thenReturn(true); + when(resultRowMock.getObject("x_types")).thenReturn(URI.create(Vocabulary.C_OWL_CLASS_D)); + when(resultRowMock.isBound("x_owlClassA")).thenReturn(true); + when(resultRowMock.getObject("x_owlClassA")).thenReturn(expected.getOwlClassA().getUri()); + when(resultRowMock.getObject("x_owlClassA", URI.class)).thenReturn(expected.getOwlClassA().getUri()); + when(resultRowMock.isBound("x_owlClassA_types_gc")).thenReturn(true); + when(resultRowMock.getString("x_owlClassA_types_gc")).thenReturn(Vocabulary.C_OWL_CLASS_A); + when(resultRowMock.isBound("x_owlClassA_stringAttribute")).thenReturn(true); + when(resultRowMock.getObject("x_owlClassA_stringAttribute")).thenReturn(expected.getOwlClassA() + .getStringAttribute()); + } + + @Test + void fetchGraphBasedQueryResultLoadingDoesNotLoadAttributesNotSpecifiedInFetchGraph() throws OntoDriverException { + when(connectionMock.getRepositoryMetadata()).thenReturn(() -> ""); + final OWLClassU expected = new OWLClassU(); + expected.setUri(Generators.generateUri()); + expected.setName("test"); + expected.setDescription("test description"); + final OWLClassS reference = new OWLClassS(); + reference.setUri(Generators.generateUri()); + reference.setName("test reference"); + reference.setDescription("test reference description"); + expected.setOwlClassS(reference); + mockQueryResultSet(expected); + final EntityGraph fetchGraph = em.createEntityGraph(OWLClassU.class); + fetchGraph.addAttributeNodes("name"); + final Subgraph subgraph = fetchGraph.addSubgraph("owlClassS"); + subgraph.addAttributeNodes("name"); + + final List result = em.createQuery("SELECT u FROM OWLClassU u", OWLClassU.class) + .setHint(QueryHints.FETCH_GRAPH, fetchGraph) + .getResultList(); + assertEquals(1, result.size()); + assertEquals(expected.getUri(), result.get(0).getUri()); + assertEquals(expected.getName(), result.get(0).getName()); + assertNull(result.get(0).getDescription()); + assertNotNull(result.get(0).getOwlClassS()); + assertEquals(reference.getUri(), result.get(0).getOwlClassS().getUri()); + assertEquals(reference.getName(), result.get(0).getOwlClassS().getName()); + assertNull(result.get(0).getOwlClassS().getDescription()); + } + + private void mockQueryResultSet(OWLClassU expected) throws OntoDriverException { + final Statement statementMock = mock(Statement.class); + final ResultSet resultSetMock = mock(ResultSet.class); + final ResultSetIterator resultSetIteratorMock = mock(ResultSetIterator.class); + final ResultRow resultRowMock = mock(ResultRow.class); + when(connectionMock.createStatement()).thenReturn(statementMock); + when(statementMock.executeQuery(anyString())).thenReturn(resultSetMock); + when(resultSetMock.iterator()).thenReturn(resultSetIteratorMock); + when(resultSetIteratorMock.next()).thenReturn(resultRowMock); + when(resultSetIteratorMock.hasNext()).thenReturn(true, false); + when(resultRowMock.getColumnCount()).thenReturn(6); + when(resultRowMock.isBound("x")).thenReturn(true); + when(resultRowMock.getObject("x", URI.class)).thenReturn(expected.getUri()); + when(resultRowMock.getObject(0, URI.class)).thenReturn(expected.getUri()); + when(resultRowMock.isBound("x_name")).thenReturn(true); + when(resultRowMock.getObject("x_name")).thenReturn(expected.getName()); + when(resultRowMock.isBound("x_types")).thenReturn(true); + when(resultRowMock.getObject("x_types")).thenReturn(URI.create(Vocabulary.C_OWL_CLASS_U)); + when(resultRowMock.isBound("x_owlClassS")).thenReturn(true); + when(resultRowMock.getObject("x_owlClassS")).thenReturn(expected.getOwlClassS().getUri()); + when(resultRowMock.getObject("x_owlClassS", URI.class)).thenReturn(expected.getOwlClassS().getUri()); + when(resultRowMock.isBound("x_owlClassS_name")).thenReturn(true); + when(resultRowMock.getObject("x_owlClassS_name")).thenReturn(expected.getOwlClassS().getName()); + when(resultRowMock.isBound("x_owlClassS_types_gc")).thenReturn(true); + when(resultRowMock.getString("x_owlClassS_types_gc")).thenReturn(Vocabulary.C_OWL_CLASS_S); + } +} diff --git a/jopa-maven-plugin/README.md b/jopa-maven-plugin/README.md index f84e82932..9a82b4a56 100644 --- a/jopa-maven-plugin/README.md +++ b/jopa-maven-plugin/README.md @@ -1,36 +1,73 @@ # JOPA Maven Plugin -JOPA Maven plugin can generate an object model based on the specified ontology. Or, if object model generation is +The JOPA Maven plugin provides object model-related generators. + +## OWL2Java Mojo + +The OWL2Java Mojo can generate an object model based on the specified ontology. Or, if object model generation is too much, it can generate a `Vocabulary` file containing all classes and properties found in the ontology, so that they can be used in the mapping annotations in the object model. Internally, the plugin uses [OWL2Java](../jopa-owl2java). -## Goals +### Goals - `owl2java-transform` - run the OWL2Java transformation -## Phase +### Phase - JOPA Maven plugin is by default executed during the `generate-sources` build phase. -## Configuration - -| Parameter | Default value | Description | -|:------------------------------|:-----------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------------------------------| -| `mapping-file` | "" | Path to the mapping file which allows resolving ontology IRIs to local files. | -| `package` | "" | Name of the target package into which the model/vocabulary should be generated. | -| `context-name` | "" | Name of context specified by integrity constraint annotations. If specified, only axioms annotated with the correct context will be processed. | -| `ontology-iri` | "" | IRI of the ontology that should be processed. | -| `output-directory` | `${project.basedir}/src/main/generated-sources` | Target directory into which the model/vocabulary should be generated. | -| `with-owlapi` | `false` | Whether to also generate OWLAPI IRIs for the generated vocabulary terms. | -| `with-java-uris` | `false` | Whether to also generate Java URIs for the generated vocabulary terms. | -| `whole-ontology-as-ics` | `false` | Whether to use the whole ontology for generation. If specified, `context-name` is ignored. | -| `vocabulary-only` | `false` | Whether to generate only the vocabulary file or the object model as well. | -| `ignore-failed-imports` | `false` | Whether to ignore failed ontology imports (owl:import). | -| `properties-type` | `string` | Java type to use as value in the unmapped properties attribute definition. Options are `string` or `object`. | -| `javadoc-from-rdfs-comment` | `true` | Whether to use RDFS comment annotations to generate Javadoc for classes and attributes. | -| `prefer-multilingual-strings` | `true` | Whether to use `MultilingualString` type for fields whose property range is langString. | -| `generate-annotation-fields` | `true` | Whether to generate annotation fields (name, description) for all entities. | -| `generate-thing` | `true` | Whether to generate an entity classes corresponding to `owl:Thing`. | +### Configuration + +| Parameter | Default value | Description | +|:---------------------------------|:-----------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------------------------------| +| `mapping-file` | "" | Path to the mapping file which allows resolving ontology IRIs to local files. | +| `package` | "" | Name of the target package into which the model/vocabulary should be generated. | +| `context-name` | "" | Name of context specified by integrity constraint annotations. If specified, only axioms annotated with the correct context will be processed. | +| `ontology-iri` | "" | IRI of the ontology that should be processed. | +| `output-directory` | `${project.basedir}/src/main/generated-sources` | Target directory into which the model/vocabulary should be generated. | +| `with-owlapi` | `false` | Whether to also generate OWLAPI IRIs for the generated vocabulary terms. | +| `with-java-uris` | `false` | Whether to also generate Java URIs for the generated vocabulary terms. | +| `whole-ontology-as-ics` | `false` | Whether to use the whole ontology for generation. If specified, `context-name` is ignored. | +| `vocabulary-only` | `false` | Whether to generate only the vocabulary file or the object model as well. | +| `ignore-failed-imports` | `false` | Whether to ignore failed ontology imports (owl:import). | +| `properties-type` | `string` | Java type to use as value in the unmapped properties attribute definition. Options are `string` or `object`. | +| `javadoc-from-rdfs-comment` | `true` | Whether to use RDFS comment annotations to generate Javadoc for classes and attributes. | +| `prefer-multilingual-strings` | `true` | Whether to use `MultilingualString` type for fields whose property range is langString. | +| `generate-annotation-fields` | `true` | Whether to generate annotation fields (name, description) for all entities. | +| `generate-thing` | `true` | Whether to generate an entity classes corresponding to `owl:Thing`. | +| `ontology-prefix-property` | `http://purl.org/vocab/vann/preferredNamespacePrefix` | IRI of the annotation property whose value is used as the preferred prefix for an ontology when generating vocabulary and model. | +| `ontology-namespace-property` | `http://purl.org/vocab/vann/preferredNamespaceUri` | IRI of the annotation property whose value is used as the preferred namespace for an ontology when generating vocabulary and model. | +| `vocabulary-always-use-prefixes` | `true` | Whether to always use the ontology prefix (when available) to shorten IRIs in the generated vocabulary file. | +| `model-always-use-prefixes` | `false` | Whether to always use the ontology prefix (when available) when generating entity class names in the model. | +| `prefix-mapping-file` | "" | Path to a file containing additional ontology prefix mappings (one `namespace=prefix` per line). | + +## ModelGen Mojo + +The ModelGen Mojo can generate static metamodel classes based on the application object model. Static metamodel can be +used, for example, +when creating Criteria API queries, building Descriptors for entity manager operations etc. + +Internally, it uses the [ModelGen](../modelgen) module. + +### Goals + +- `modelgen` - run the static metamodel generation + +### Phase + +JOPA Maven plugin is by default executed during the `generate-sources` build phase. + +### Configuration + +| Parameter | Default value | Description | +|:-----------------------|:---------------------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `output-directory` | `./target/generated-sources/static-metamodel` | Target directory into which the generated static metamodel classes should be written. | +| `source-package` | "" | If set, only entity classes whose fully qualified name contains the specified package are processed. | +| `additional-sources` | "" | Additional source directory to process besides the project's compile source roots. | +| `debug-option` | `false` | Whether to log additional debug messages during static metamodel generation. | +| `output-property-iris` | `false` | Whether to generate constants holding IRIs of mapped properties in the generated metamodel classes. | +| `output-iri-as-string` | `false` | Whether the generated property IRI constants should be of type `String` instead of `cz.cvut.kbss.jopa.model.IRI`. | +| `initialize-iris` | `false` | Whether the generated property IRI constants should be initialized with their values (otherwise they are declared without value and initialized by JOPA when the runtime metamodel is built). | diff --git a/jopa-maven-plugin/pom.xml b/jopa-maven-plugin/pom.xml index 156c57b66..6f127dc49 100644 --- a/jopa-maven-plugin/pom.xml +++ b/jopa-maven-plugin/pom.xml @@ -5,7 +5,7 @@ jopa-all cz.cvut.kbss.jopa - 2.9.4 + 2.10.0 ../pom.xml 4.0.0 diff --git a/jopa-maven-plugin/src/main/java/cz/cvut/kbss/jopa/maven/OWL2JavaMojo.java b/jopa-maven-plugin/src/main/java/cz/cvut/kbss/jopa/maven/OWL2JavaMojo.java index c50ef8002..2ede52cc8 100644 --- a/jopa-maven-plugin/src/main/java/cz/cvut/kbss/jopa/maven/OWL2JavaMojo.java +++ b/jopa-maven-plugin/src/main/java/cz/cvut/kbss/jopa/maven/OWL2JavaMojo.java @@ -44,7 +44,9 @@ public class OWL2JavaMojo extends AbstractMojo { private static final String GENERATE_ANNOTATION_FIELDS = "generate-annotation-fields"; private static final String GENERATE_THING = "generate-thing"; private static final String ONTOLOGY_PREFIX_PROPERTY = "ontology-prefix-property"; - private static final String ALWAYS_USE_PREFIXES = "always-use-prefixes"; + private static final String ONTOLOGY_NAMESPACE_PROPERTY = "ontology-namespace-property"; + private static final String VOCABULARY_ALWAYS_USE_PREFIXES = "vocabulary-always-use-prefixes"; + private static final String MODEL_ALWAYS_USE_PREFIXES = "model-always-use-prefixes"; private static final String PREFIX_MAPPING_FILE = "prefix-mapping-file"; @Parameter(name = MAPPING_FILE_PARAM) @@ -95,8 +97,14 @@ public class OWL2JavaMojo extends AbstractMojo { @Parameter(name = ONTOLOGY_PREFIX_PROPERTY) private String ontologyPrefixProperty; - @Parameter(name = ALWAYS_USE_PREFIXES, defaultValue = "true") - private boolean alwaysUsePrefixes; + @Parameter(name = ONTOLOGY_NAMESPACE_PROPERTY) + private String ontologyNamespaceProperty; + + @Parameter(name = VOCABULARY_ALWAYS_USE_PREFIXES, defaultValue = "true") + private boolean vocabularyAlwaysUsePrefixes; + + @Parameter(name = MODEL_ALWAYS_USE_PREFIXES, defaultValue = "false") + private boolean modelAlwaysUsePrefixes; @Parameter(name = PREFIX_MAPPING_FILE) private String prefixMappingFile; @@ -135,7 +143,10 @@ public void execute() { .addOwlapiIris(withOwlapi).addJavaUris(withJavaUris) .generateJavadoc(javadocFromRdfsComment).preferMultilingualStrings(preferMultilingualStrings) .generateAnnotationFields(generateAnnotationFields).generateThing(generateThing) - .ontologyPrefixProperty(ontologyPrefixProperty).alwaysUseOntologyPrefix(alwaysUsePrefixes) + .ontologyPrefixProperty(ontologyPrefixProperty) + .ontologyNamespaceProperty(ontologyNamespaceProperty) + .alwaysUseOntologyPrefixForVocabulary(vocabularyAlwaysUsePrefixes) + .alwaysUseOntologyPrefixForModel(modelAlwaysUsePrefixes) .prefixMappingFile(prefixMappingFile).build(); if (vocabularyOnly) { @@ -164,7 +175,9 @@ private void printParameterValues() { Utils.logParameterValue(GENERATE_ANNOTATION_FIELDS, generateAnnotationFields, getLog()); Utils.logParameterValue(GENERATE_THING, generateThing, getLog()); Utils.logParameterValue(ONTOLOGY_PREFIX_PROPERTY, ontologyPrefixProperty, getLog()); - Utils.logParameterValue(ALWAYS_USE_PREFIXES, alwaysUsePrefixes, getLog()); + Utils.logParameterValue(ONTOLOGY_NAMESPACE_PROPERTY, ontologyNamespaceProperty, getLog()); + Utils.logParameterValue(VOCABULARY_ALWAYS_USE_PREFIXES, vocabularyAlwaysUsePrefixes, getLog()); + Utils.logParameterValue(MODEL_ALWAYS_USE_PREFIXES, modelAlwaysUsePrefixes, getLog()); Utils.logParameterValue(PREFIX_MAPPING_FILE, prefixMappingFile, getLog()); } diff --git a/jopa-owl2java/README.md b/jopa-owl2java/README.md index 5c260a088..3ee85677f 100644 --- a/jopa-owl2java/README.md +++ b/jopa-owl2java/README.md @@ -8,21 +8,70 @@ annotation "http://krizik.felk.cvut.cz/ontologies/2009/ic.owl#isIntegrityConstra Possible configuration parameters are: -| Parameter | Default value | Explanation | -|:------------:|:-----------------------------------------------------------------:|-----------------------------------------------------------------------------------------------------------------------------------| -| -m | "" | Path to a mapping file which allows resolving ontology IRIs to local files. | -| -c | "" | Name of context specified by IC annotations. If specified, only axioms annotated with the correct context will be processed. | -| -p | `generated` | Name of the package into which the vocabulary file and the model will be generated. | -| -d | "" | Directory into which the target files (with package) will be generated. | -| -i | `false` | Whether to interpret all the axioms in the ontology as integrity constraints. If `true`, context is ignored. | -| -w | `false` | Whether to add OWLAPI IRIs for terms generated into the vocabulary file (normally, only String constants are generated). | -| -u | `false` | Whether to add Java URIs for terms generated into the vocabulary file (normally, only String constants are generated). | -| -f | `false` | Whether to ignore missing/failed ontology imports. | -| -jca | "http://krizik.felk.cvut.cz/ontologies/2009/ic.owl#javaClassName" | IRI of annotation used to specify Java class name to which the annotated OWL class is mapped. Relevant only for model generation. | -| -langStrings | `true` | Whether to prefer generating string fields as `MultilingualString` when their range is langString. | -| -pt | `string` | Type to use for `@Properties` value. Options are `String` and `Object` . | -| -doc | `true` | Whether to generate Javadoc using values of `rdfs:comment` axioms. | -| -ann | `true` | Whether to automatically generate annotation fields corresponding to `rdfs:label` and `dc:description` for all entity classes. | -| -thing | `true` | Whether to automatically generate an entity class corresponding to `owl:Thing`. | +| Parameter | Default value | Explanation | +|:----------------------:|:-----------------------------------------------------------------:|-------------------------------------------------------------------------------------------------------------------------------------| +| -m | "" | Path to a mapping file which allows resolving ontology IRIs to local files. | +| -c | "" | Name of context specified by IC annotations. If specified, only axioms annotated with the correct context will be processed. | +| -p | `generated` | Name of the package into which the vocabulary file and the model will be generated. | +| -d | "" | Directory into which the target files (with package) will be generated. | +| -i | `false` | Whether to interpret all the axioms in the ontology as integrity constraints. If `true`, context is ignored. | +| -w | `false` | Whether to add OWLAPI IRIs for terms generated into the vocabulary file (normally, only String constants are generated). | +| -u | `false` | Whether to add Java URIs for terms generated into the vocabulary file (normally, only String constants are generated). | +| -f | `false` | Whether to ignore missing/failed ontology imports. | +| -jca | "http://krizik.felk.cvut.cz/ontologies/2009/ic.owl#javaClassName" | IRI of annotation used to specify Java class name to which the annotated OWL class is mapped. Relevant only for model generation. | +| -langStrings | `true` | Whether to prefer generating string fields as `MultilingualString` when their range is langString. | +| -pt | `string` | Type to use for `@Properties` value. Options are `String` and `Object` . | +| -doc | `true` | Whether to generate Javadoc using values of `rdfs:comment` axioms. | +| -ann | `true` | Whether to automatically generate annotation fields corresponding to `rdfs:label` and `dc:description` for all entity classes. | +| -thing | `true` | Whether to automatically generate an entity class corresponding to `owl:Thing`. | +| -prefixProperty | `http://purl.org/vocab/vann/preferredNamespacePrefix` | IRI of the annotation property whose value is used as the preferred prefix for an ontology when generating vocabulary and model. | +| -namespaceProperty | `http://purl.org/vocab/vann/preferredNamespaceUri` | IRI of the annotation property whose value is used as the preferred namespace for an ontology when generating vocabulary and model. | +| -vocabularyUsePrefixes | `false` | Whether to always use the ontology prefix (when available) to shorten IRIs in the generated vocabulary file. | +| -modelUsePrefixes | `false` | Whether to always use the ontology prefix (when available) when generating entity class names in the model. | +| -prefixFile | "" | Path to a file containing additional ontology prefix mappings (one `namespace=prefix` per line). | OWL2Java CLI prints help for all the supported tasks, including explanation of all options. + +## Java Name Generation + +Java name generation follows a set of rules that attempt to prevent name collisions and generate human-readable names. + +### Vocabulary + +This concerns generation of the `Vocabulary` class containing Java constants for all entities in the processed ontologies. + +1. Ontology IRI generation: + - All ontology IRI constants use the prefix `ONTOLOGY_IRI_` + - If it is one of the well-known ontologies (such as SKOS, OWL, etc.), use a predefined name + - Otherwise, extract the name from ontology IRI local name (i.e. the part after the last `/` or `#`) + - If the field name is not unique and the ontology has a prefix, use the prefix + - Otherwise, use the local name and append `_A` to disambiguate (repeatedly if necessary) +2. Entity constants generation + - Classes have prefix `s_c`, properties have prefix `s_p`, individuals have prefix `s_i` + - If `vocabularyUsePrefixes` is `true`, use the ontology prefix (when available) + - Extract local name from IRI and append it to the prefix + - If `vocabularyUsePrefixes` is `false` and the field is not unique, try using the ontology prefix (if available) + - If the field is still not unique, append `_A` to disambiguate (repeatedly if necessary) + +Examples: + +```java +// Known ontology +public static final String ONTOLOGY_IRI_DC_TERMS = "http://purl.org/dc/terms/"; +// Name extracted from IRI +public static final String ONTOLOGY_IRI_TERMIT = "http://onto.fel.cvut.cz/ontologies/application/termit"; +public static final String ONTOLOGY_IRI_MODEL = "http://onto.fel.cvut.cz/ontologies/application/termit/model"; +// Name extracted from IRI, not unique, but prefix is available +public static final String ONTOLOGY_IRI_DD_MODEL = "http://onto.fel.cvut.cz/ontologies/application/dd/model"; +// Name extracted from IRI, not unique, and prefix is not available +public static final String ONTOLOGY_IRI_MODEL_A = "http://onto.fel.cvut.cz/ontologies/application/dd/model"; +``` + +### Model + +1. If `modelUsePrefixes` is `true`, use the ontology prefix (when available) +2. Extract entity local name from IRI and use it +3. If the name is not unique, try using the prefix (if available) +4. If the name is still not unique, append `_A` to disambiguate (repeatedly if necessary) + + diff --git a/jopa-owl2java/pom.xml b/jopa-owl2java/pom.xml index 809e41555..b422aa4d6 100644 --- a/jopa-owl2java/pom.xml +++ b/jopa-owl2java/pom.xml @@ -6,7 +6,7 @@ cz.cvut.kbss.jopa jopa-all - 2.9.4 + 2.10.0 ../pom.xml diff --git a/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/JavaNameGenerator.java b/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/JavaNameGenerator.java index 1efd7aa85..176bd7a5f 100644 --- a/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/JavaNameGenerator.java +++ b/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/JavaNameGenerator.java @@ -18,12 +18,15 @@ package cz.cvut.kbss.jopa.owl2java; import cz.cvut.kbss.jopa.owl2java.prefix.PrefixMap; +import cz.cvut.kbss.jopa.vocabulary.DC; +import cz.cvut.kbss.jopa.vocabulary.RDF; +import org.eclipse.rdf4j.model.vocabulary.RDFS; import org.semanticweb.owlapi.model.IRI; -import org.semanticweb.owlapi.model.OWLOntologyID; import javax.lang.model.SourceVersion; import java.text.Normalizer; import java.util.Arrays; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -34,6 +37,16 @@ public class JavaNameGenerator { private static final String[] JAVA_KEYWORDS = {"abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private", "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "try", "void", "volatile", "while"}; + private static final Map KNOWN_ONTOLOGIES = Map.of( + "http://www.w3.org/2002/07/owl", "OWL_2", + RDFS.NAMESPACE, "RDFS", + RDF.NAMESPACE, "RDF", + DC.Terms.NAMESPACE, "DC_TERMS", + DC.Elements.NAMESPACE, "DC_ELEMENTS", + "http://xmlns.com/foaf/0.1/", "FOAF", + "http://www.w3.org/2004/02/skos/core", "SKOS" + ); + private static final char SEPARATOR = '_'; private final PrefixMap prefixMap; @@ -41,49 +54,34 @@ public class JavaNameGenerator { public JavaNameGenerator(PrefixMap prefixMap) {this.prefixMap = prefixMap;} /** - * Returns a valid Java identifier extracted from the specified IRI. - *

- * If the IRI contains a non-empty fragment, it is used. Otherwise, the part after the last slash is used as the - * name. + * Generates ontology local name based on the specified ontology IRI. * - * @param iri IRI to extract name from - * @return Java name based on the specified IRI + * @param iri Ontology IRI + * @return ontology local name for Java constant */ - public String generateJavaNameForIri(IRI iri) { - if (iri.getFragment() != null && !iri.getFragment().isEmpty()) { - return makeNameValidJava(iri.getFragment()); - } else { - String strIri = iri.toString(); - if (strIri.charAt(strIri.length() - 1) == '/') { - strIri = strIri.substring(0, strIri.length() - 1); - } - int x = strIri.lastIndexOf("/"); - return makeNameValidJava(strIri.substring(x + 1)); + public String generateOntologyName(IRI iri) { + if (KNOWN_ONTOLOGIES.containsKey(iri.getIRIString())) { + return KNOWN_ONTOLOGIES.get(iri.getIRIString()); } + return generateJavaNameForIri(iri); } /** - * Returns a valid Java identifier extracted from the specified IRI, prefixed with prefix registered for the - * specified ontology IRI (if available). + * Returns a valid Java identifier extracted from the specified IRI, prefixed with prefix registered for the IRIs + * namespace (if available). *

* If the IRI contains a non-empty fragment, it is used. Otherwise, the part after the last slash is used as the * name. *

- * If the ontology is anonymous, no prefix is added to the extracted name. If no prefix is registered for the - * ontology, a prefix represented by extracting a java name from the ontology IRI is used. + * If no prefix is registered for the namespace, a Java name extracted from the IRI without prefix is used. * - * @param iri IRI to extract name from - * @param ontologyId Ontology identifier + * @param iri IRI to extract name from * @return Java name based on the specified IRI */ - public String generatePrefixedJavaNameForIri(IRI iri, OWLOntologyID ontologyId) { - if (ontologyId.isAnonymous()) { - return generateJavaNameForIri(iri); - } - assert ontologyId.getOntologyIRI().isPresent(); - final IRI ontologyIri = ontologyId.getOntologyIRI().get(); - return makeNameValidJava(prefixMap.getPrefix(ontologyIri) - .orElse(generateJavaNameForIri(ontologyIri))) + SEPARATOR + generateJavaNameForIri(iri); + public String generatePrefixedJavaNameForIri(IRI iri) { + return prefixMap.getPrefix(iri) + .map(prefix -> makeNameValidJava(prefix + SEPARATOR + generateJavaNameForIri(iri))) + .orElseGet(() -> generateJavaNameForIri(iri)); } /** @@ -94,17 +92,39 @@ public String generatePrefixedJavaNameForIri(IRI iri, OWLOntologyID ontologyId) */ public Optional getOntologyPrefix(IRI ontologyIri) { Objects.requireNonNull(ontologyIri); - return prefixMap.getPrefix(ontologyIri); + return prefixMap.getNamespacePrefix(ontologyIri); } /** - * Checks whether a prefix exists for the specified ontology identifier. + * Checks whether a prefix exists for the specified identifier. * - * @param ontologyIri Ontology IRI - * @return {@code true} if a prefix has been resolved for ontology IRI, {@code false} otherwise + * @param iri IRI + * @return {@code true} if a prefix has been resolved for IRI namespace, {@code false} otherwise */ - public boolean hasPrefix(IRI ontologyIri) { - return prefixMap.hasPrefix(ontologyIri); + public boolean hasPrefix(IRI iri) { + return prefixMap.hasPrefix(iri); + } + + /** + * Returns a valid Java identifier extracted from the specified IRI. + *

+ * If the IRI contains a non-empty fragment, it is used. Otherwise, the part after the last slash is used as the + * name. + * + * @param iri IRI to extract name from + * @return Java name based on the specified IRI + */ + public static String generateJavaNameForIri(IRI iri) { + if (iri.getFragment() != null && !iri.getFragment().isEmpty()) { + return makeNameValidJava(iri.getFragment()); + } else { + String strIri = iri.toString(); + if (strIri.charAt(strIri.length() - 1) == '/') { + strIri = strIri.substring(0, strIri.length() - 1); + } + int x = strIri.lastIndexOf("/"); + return makeNameValidJava(strIri.substring(x + 1)); + } } /** @@ -128,11 +148,10 @@ public static String makeNameValidJava(String name) { } /** - * Filters the specified name so that it contains only characters allowed for Java identifiers. - * If the first character (before filtering) must not be at the start, - * an underscore ("_") is prepended. - * If the resulting field name (after filtering) matches any Java keyword, - * an underscore ("_") is appended. + * Filters the specified name so that it contains only characters allowed for Java identifiers. If the first + * character (before filtering) must not be at the start, an underscore ("_") is prepended. If the resulting field + * name (after filtering) matches any Java keyword, an underscore ("_") is appended. + * * @param name the name to filter * @return a valid Java identifier * @see Java 17 Spec diff --git a/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/JavaTransformer.java b/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/JavaTransformer.java index 5830e4e32..b260c0ee4 100644 --- a/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/JavaTransformer.java +++ b/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/JavaTransformer.java @@ -61,7 +61,6 @@ import org.semanticweb.owlapi.model.OWLDatatype; import org.semanticweb.owlapi.model.OWLEntity; import org.semanticweb.owlapi.model.OWLOntology; -import org.semanticweb.owlapi.model.OWLOntologyID; import org.semanticweb.owlapi.model.OWLOntologyManager; import org.semanticweb.owlapi.search.EntitySearcher; import org.slf4j.Logger; @@ -225,9 +224,11 @@ private void generateOntologyIrisConstants(OWLOntologyManager ontologyManager) { .sorted(Comparator.comparing(IRI::getIRIString)) .toList(); ontologyIris.forEach(iri -> { - final String fieldName = ensureVocabularyItemUniqueIdentifier("ONTOLOGY_IRI_" + JavaNameGenerator.makeNameValidJava(nameGenerator.getOntologyPrefix(iri) - .orElseGet(() -> nameGenerator.generateJavaNameForIri(iri))) - .toUpperCase()); + String fieldName = "ONTOLOGY_IRI_" + nameGenerator.generateOntologyName(iri); + if (voc.fields().containsKey(fieldName.toUpperCase()) && nameGenerator.hasPrefix(iri)) { + fieldName = "ONTOLOGY_IRI_" + nameGenerator.getOntologyPrefix(iri).orElse(""); + } + fieldName = ensureVocabularyItemUniqueIdentifier(JavaNameGenerator.makeNameValidJava(fieldName.toUpperCase())); voc.field(JMod.PUBLIC | JMod.STATIC | JMod.FINAL, String.class, fieldName, JExpr.lit(iri.toString())); }); } @@ -239,12 +240,10 @@ private Optional generateFieldName(OWLEntity c, OWLOntologyManager ontol return prefix; } final Optional containingOntology = resolveContainingOntology(c, ontologyManager); - String fieldName = PREFIX_STRING + prefix.get() + nameGenerator.generateJavaNameForIri(c.getIRI()); + String fieldName = PREFIX_STRING + prefix.get() + JavaNameGenerator.generateJavaNameForIri(c.getIRI()); if (voc.fields().containsKey(fieldName) || ( - containingOntology.isPresent() && isPrefixedVersionRequired(containingOntology.get() - .getOntologyID()))) { - fieldName = PREFIX_STRING + prefix.get() + nameGenerator.generatePrefixedJavaNameForIri(c.getIRI(), containingOntology.get() - .getOntologyID()); + containingOntology.isPresent() && configuration.shouldAlwaysUseOntologyPrefixForVocabulary() && nameGenerator.hasPrefix(c.getIRI()))) { + fieldName = PREFIX_STRING + prefix.get() + nameGenerator.generatePrefixedJavaNameForIri(c.getIRI()); } return Optional.of(ensureVocabularyItemUniqueIdentifier(fieldName)); } @@ -367,7 +366,7 @@ private void generateObjectProperty(final OWLOntology ontology, if (Card.NO != comp.getCard()) { JClass filler = ensureEntityClassExists(pkg, cm, comp.getFiller(), ontology); - final String fieldName = nameGenerator.generateJavaNameForIri(prop.getIRI()); + final String fieldName = JavaNameGenerator.generateJavaNameForIri(prop.getIRI()); switch (comp.getCard()) { case MULTIPLE: @@ -449,7 +448,7 @@ private void generateDataProperty(final OWLOntology ontology, final JType obj = cm._ref(resolveFieldType(comp.getFiller())); - final String fieldName = nameGenerator.generateJavaNameForIri(prop.getIRI()); + final String fieldName = JavaNameGenerator.generateJavaNameForIri(prop.getIRI()); JFieldVar fv = switch (comp.getCard()) { case MULTIPLE -> addField(fieldName, subj, cm.ref(Set.class).narrow(obj)); @@ -499,6 +498,7 @@ private JDefinedClass createEntityClass(final String pkg, final JCodeModel cm, f try { cls = cm._class(name); + LOG.debug("Generating class '{}' for OWL class <{}>.", cls.name(), clazz.getIRI()); cls.annotate(cz.cvut.kbss.jopa.model.annotations.OWLClass.class).param("iri", entities.get(clazz)); cls._implements(Serializable.class); @@ -513,15 +513,13 @@ private JDefinedClass createEntityClass(final String pkg, final JCodeModel cm, f private String javaClassId(OWLOntology rootOntology, OWLClass owlClass, String pkg, JCodeModel codeModel) { - final Optional containingOntology = resolveContainingOntology(owlClass, rootOntology.getOWLOntologyManager()); - final OWLOntology onto = containingOntology.orElse(rootOntology); String className = resolveExplicitClassName(rootOntology, owlClass) - .orElseGet(() -> JavaNameGenerator.toCamelCaseNotation(nameGenerator.generateJavaNameForIri(owlClass.getIRI()))); + .orElseGet(() -> JavaNameGenerator.toCamelCaseNotation(JavaNameGenerator.generateJavaNameForIri(owlClass.getIRI()))); - if (isClassNameUnique(pkg, className, codeModel) && !isPrefixedVersionRequired(onto.getOntologyID())) { + if (isClassNameUnique(pkg, className, codeModel) && (!configuration.shouldAlwaysUseOntologyPrefixForModel() || !nameGenerator.hasPrefix(owlClass.getIRI()))) { return fqn(pkg, className); } - className = JavaNameGenerator.toCamelCaseNotation(nameGenerator.generatePrefixedJavaNameForIri(owlClass.getIRI(), onto.getOntologyID())); + className = JavaNameGenerator.toCamelCaseNotation(nameGenerator.generatePrefixedJavaNameForIri(owlClass.getIRI())); while (!isClassNameUnique(pkg, className, codeModel)) { className += DISAMBIGUATION_SUFFIX; } @@ -550,11 +548,6 @@ private static String fqn(String pkg, String simpleName) { return pkg + PACKAGE_SEPARATOR + simpleName; } - private boolean isPrefixedVersionRequired(OWLOntologyID ontologyId) { - return configuration.shouldAlwaysUseOntologyPrefix() - && (ontologyId.isAnonymous() || nameGenerator.hasPrefix(ontologyId.getOntologyIRI().get())); - } - private void generateClassJavadoc(OWLOntology ontology, OWLEntity owlEntity, JDocCommentable javaElem) { final boolean generated = generateJavadoc(ontology, owlEntity, javaElem); if (generated) { diff --git a/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/cli/Option.java b/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/cli/Option.java index 0056ce540..c741874e0 100644 --- a/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/cli/Option.java +++ b/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/cli/Option.java @@ -121,9 +121,19 @@ public enum Option { ONTOLOGY_PREFIX_PROPERTY("prefixProperty", "identifier of the property whose value represents the ontology IRI prefix"), /** - * Specifies whether to always use ontology prefix (if available) when generating vocabulary and model. + * Property whose value represents the ontology IRI namespace. */ - ALWAYS_USE_ONTOLOGY_PREFIX("usePrefixes", "whether to always use ontology prefix for generating vocabulary and model"), + ONTOLOGY_NAMESPACE_PROPERTY("namespaceProperty", "identifier of the property whose value represents the ontology IRI namespace"), + + /** + * Specifies whether to always use ontology prefix (if available) when generating vocabulary. + */ + ALWAYS_USE_ONTOLOGY_PREFIX_FOR_VOCABULARY("vocabularyUsePrefixes", "whether to always use ontology prefix for generating vocabulary"), + + /** + * Specifies whether to always use ontology prefix (if available) when generating model. + */ + ALWAYS_USE_ONTOLOGY_PREFIX_FOR_MODEL("modelUsePrefixes", "whether to always use ontology prefix for generating model"), /** * Specifies file containing ontology prefix mapping. diff --git a/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/config/Defaults.java b/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/config/Defaults.java index ca39813f5..e825f6bb7 100644 --- a/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/config/Defaults.java +++ b/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/config/Defaults.java @@ -92,9 +92,19 @@ public class Defaults { public static final String ONTOLOGY_PREFIX_PROPERTY = "http://purl.org/vocab/vann/preferredNamespacePrefix"; /** - * @see Option#ALWAYS_USE_ONTOLOGY_PREFIX + * @see Option#ONTOLOGY_NAMESPACE_PROPERTY */ - public static final boolean ALWAYS_USE_ONTOLOGY_PREFIX = true; + public static final String ONTOLOGY_NAMESPACE_PROPERTY = "http://purl.org/vocab/vann/preferredNamespaceUri"; + + /** + * @see Option#ALWAYS_USE_ONTOLOGY_PREFIX_FOR_VOCABULARY + */ + public static final boolean ALWAYS_USE_ONTOLOGY_PREFIX_FOR_VOCABULARY = false; + + /** + * @see Option#ALWAYS_USE_ONTOLOGY_PREFIX_FOR_MODEL + */ + public static final boolean ALWAYS_USE_ONTOLOGY_PREFIX_FOR_MODEL = false; private Defaults() { throw new AssertionError(); diff --git a/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/config/TransformationConfiguration.java b/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/config/TransformationConfiguration.java index 4fb07da04..ecf125954 100644 --- a/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/config/TransformationConfiguration.java +++ b/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/config/TransformationConfiguration.java @@ -47,7 +47,11 @@ public class TransformationConfiguration { private final String ontologyPrefixProperty; - private final boolean alwaysUseOntologyPrefix; + private final String ontologyNamespaceProperty; + + private final boolean alwaysUseOntologyPrefixForVocabulary; + + private final boolean alwaysUseOntologyPrefixForModel; private final String prefixMappingFile; @@ -67,7 +71,9 @@ private TransformationConfiguration(TransformationConfigurationBuilder builder) this.generateAnnotationFields = builder.generateAnnotationFields; this.generateThing = builder.generateThing; this.ontologyPrefixProperty = builder.ontologyPrefixProperty; - this.alwaysUseOntologyPrefix = builder.alwaysUseOntologyPrefix; + this.ontologyNamespaceProperty = builder.ontologyNamespaceProperty; + this.alwaysUseOntologyPrefixForVocabulary = builder.alwaysUseOntologyPrefixForVocabulary; + this.alwaysUseOntologyPrefixForModel = builder.alwaysUseOntologyPrefixForModel; this.prefixMappingFile = builder.prefixMappingFile; this.remotePrefixResolver = builder.remotePrefixResolver; this.cliParams = CliParams.empty(); @@ -88,7 +94,10 @@ private TransformationConfiguration(CliParams cliParams) { this.generateThing = cliParams.is(Option.GENERATE_THING.arg, Defaults.GENERATE_THING); this.ontologyPrefixProperty = cliParams.has(Option.ONTOLOGY_PREFIX_PROPERTY.arg) ? cliParams.valueOf(Option.ONTOLOGY_PREFIX_PROPERTY.arg) .toString() : Defaults.ONTOLOGY_PREFIX_PROPERTY; - this.alwaysUseOntologyPrefix = cliParams.is(Option.ALWAYS_USE_ONTOLOGY_PREFIX.arg, Defaults.ALWAYS_USE_ONTOLOGY_PREFIX); + this.ontologyNamespaceProperty = cliParams.has(Option.ONTOLOGY_NAMESPACE_PROPERTY.arg) ? cliParams.valueOf(Option.ONTOLOGY_NAMESPACE_PROPERTY.arg) + .toString() : Defaults.ONTOLOGY_NAMESPACE_PROPERTY; + this.alwaysUseOntologyPrefixForVocabulary = cliParams.is(Option.ALWAYS_USE_ONTOLOGY_PREFIX_FOR_VOCABULARY.arg, Defaults.ALWAYS_USE_ONTOLOGY_PREFIX_FOR_VOCABULARY); + this.alwaysUseOntologyPrefixForModel = cliParams.is(Option.ALWAYS_USE_ONTOLOGY_PREFIX_FOR_MODEL.arg, Defaults.ALWAYS_USE_ONTOLOGY_PREFIX_FOR_MODEL); this.prefixMappingFile = cliParams.has(Option.PREFIX_MAPPING_FILE.arg) ? cliParams.valueOf(Option.PREFIX_MAPPING_FILE.arg) .toString() : null; this.remotePrefixResolver = new PrefixCcRemotePrefixResolver(); @@ -142,8 +151,16 @@ public String getOntologyPrefixProperty() { return ontologyPrefixProperty; } - public boolean shouldAlwaysUseOntologyPrefix() { - return alwaysUseOntologyPrefix; + public String getOntologyNamespaceProperty() { + return ontologyNamespaceProperty; + } + + public boolean shouldAlwaysUseOntologyPrefixForVocabulary() { + return alwaysUseOntologyPrefixForVocabulary; + } + + public boolean shouldAlwaysUseOntologyPrefixForModel() { + return alwaysUseOntologyPrefixForModel; } public String getPrefixMappingFile() { @@ -178,7 +195,10 @@ public static class TransformationConfigurationBuilder { private boolean generateAnnotationFields = Defaults.GENERATE_ANNOTATION_FIELDS; private boolean generateThing = Defaults.GENERATE_THING; private String ontologyPrefixProperty = Defaults.ONTOLOGY_PREFIX_PROPERTY; - private boolean alwaysUseOntologyPrefix = Defaults.ALWAYS_USE_ONTOLOGY_PREFIX; + private String ontologyNamespaceProperty = Defaults.ONTOLOGY_NAMESPACE_PROPERTY; + private boolean alwaysUseOntologyPrefixForVocabulary = Defaults.ALWAYS_USE_ONTOLOGY_PREFIX_FOR_VOCABULARY; + private boolean alwaysUseOntologyPrefixForModel = Defaults.ALWAYS_USE_ONTOLOGY_PREFIX_FOR_MODEL; + private String prefixMappingFile = null; private RemotePrefixResolver remotePrefixResolver = new PrefixCcRemotePrefixResolver(); @@ -239,8 +259,21 @@ public TransformationConfigurationBuilder ontologyPrefixProperty(String ontology return this; } - public TransformationConfigurationBuilder alwaysUseOntologyPrefix(boolean alwaysUseOntologyPrefix) { - this.alwaysUseOntologyPrefix = alwaysUseOntologyPrefix; + public TransformationConfigurationBuilder ontologyNamespaceProperty(String ontologyNamespaceProperty) { + if (ontologyNamespaceProperty != null && !ontologyNamespaceProperty.isBlank()) { + this.ontologyNamespaceProperty = ontologyNamespaceProperty; + } + return this; + } + + public TransformationConfigurationBuilder alwaysUseOntologyPrefixForVocabulary( + boolean alwaysUseOntologyPrefix) { + this.alwaysUseOntologyPrefixForVocabulary = alwaysUseOntologyPrefix; + return this; + } + + public TransformationConfigurationBuilder alwaysUseOntologyPrefixForModel(boolean alwaysUseOntologyPrefix) { + this.alwaysUseOntologyPrefixForModel = alwaysUseOntologyPrefix; return this; } diff --git a/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/prefix/PrefixMap.java b/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/prefix/PrefixMap.java index bf7e498ac..c6c437603 100644 --- a/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/prefix/PrefixMap.java +++ b/jopa-owl2java/src/main/java/cz/cvut/kbss/jopa/owl2java/prefix/PrefixMap.java @@ -24,13 +24,12 @@ import cz.cvut.kbss.jopa.vocabulary.RDF; import cz.cvut.kbss.jopa.vocabulary.RDFS; import cz.cvut.kbss.jopa.vocabulary.SKOS; -import org.semanticweb.owlapi.model.AxiomType; import org.semanticweb.owlapi.model.IRI; -import org.semanticweb.owlapi.model.OWLAnnotationProperty; import org.semanticweb.owlapi.model.OWLDataFactory; -import org.semanticweb.owlapi.model.OWLDataProperty; +import org.semanticweb.owlapi.model.OWLLiteral; import org.semanticweb.owlapi.model.OWLOntology; import org.semanticweb.owlapi.model.OWLOntologyManager; +import org.semanticweb.owlapi.search.EntitySearcher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,6 +38,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -49,10 +49,11 @@ /** * Keeps a map of prefixes resolved from provided ontologies. *

- * Prefixes are resolved using the following strategy: + * The following prefix sources are used: *

    - *
  • If an ontology has an explicitly specified prefix (using the configured prefix property), it is used
  • - *
  • + *
  • An ontology may specify a prefix using the configured prefix property. If the ontology specifies a namespace, it is used, otherwise the ontology IRI is used as the namespace
  • + *
  • Remove prefix resolver may be configured
  • + *
  • A prefix mapping file may be configured
  • *
*/ public class PrefixMap { @@ -74,52 +75,70 @@ public PrefixMap(OWLOntologyManager ontologyManager, TransformationConfiguration * @param ontologyManager Manager of ontologies to process */ private static Map resolvePrefixes(OWLOntologyManager ontologyManager, - TransformationConfiguration config) { + TransformationConfiguration config) { final Map result = new ConcurrentHashMap<>(builtInPrefixes()); ontologyManager.ontologies().parallel().filter(o -> o.getOntologyID().isNamed()).forEach(o -> { assert o.getOntologyID().getOntologyIRI().isPresent(); - result.putAll(resolveOntologyPrefixes(o, config.getOntologyPrefixProperty(), config.getRemotePrefixResolver())); + result.putAll(resolveOntologyPrefixes(o, config.getOntologyPrefixProperty(), config.getOntologyNamespaceProperty(), config.getRemotePrefixResolver())); }); result.putAll(resolvePrefixesFromPrefixMappingFile(config.getPrefixMappingFile())); - LOG.debug("Resolved prefix map: {}", result); + LOG.info("Resolved prefix map: {}", result); return result; } private static Map resolveOntologyPrefixes(OWLOntology ontology, String prefixProperty, - RemotePrefixResolver remotePrefixResolver) { + String namespaceProperty, + RemotePrefixResolver remotePrefixResolver) { final Map result = new HashMap<>(); - final OWLDataFactory df = ontology.getOWLOntologyManager().getOWLDataFactory(); - final OWLAnnotationProperty annProperty = df.getOWLAnnotationProperty(prefixProperty); assert ontology.getOntologyID().getOntologyIRI().isPresent(); final IRI ontologyIri = ontology.getOntologyID().getOntologyIRI().get(); - ontology.axioms(AxiomType.ANNOTATION_ASSERTION) - .filter(ax -> ax.getProperty().equals(annProperty) && ax.getValue().isLiteral() && ax.getSubject() - .isIRI()) - .forEach(ax -> { - assert ax.getSubject().asIRI().isPresent(); - assert ax.getValue().asLiteral().isPresent(); - - result.put(ax.getSubject().asIRI().get().getIRIString(), ax.getValue().asLiteral().get() - .getLiteral()); - }); - final OWLDataProperty dataProperty = df.getOWLDataProperty(prefixProperty); - ontology.axioms(AxiomType.DATA_PROPERTY_ASSERTION) - .filter(ax -> ax.getProperty().equals(dataProperty) && ax.getSubject().isIndividual()) - .forEach(ax -> result.put(ax.getSubject().asOWLNamedIndividual().toStringID(), ax.getObject() - .getLiteral())); - if (!result.containsKey(ontologyIri.getIRIString())) { + final Optional prefix = resolveOntologyAnnotationValues(ontology, prefixProperty); + if (prefix.isPresent()) { + final Optional namespace = resolveOntologyAnnotationValues(ontology, namespaceProperty); + if (namespace.isPresent()) { + LOG.debug("Registering prefix {} for namespace {}.", prefix.get(), namespace.get()); + result.put(namespace.get(), prefix.get()); + } + // Register prefix both for namespace and ontology IRI + LOG.debug("Registering prefix {} for ontology {}.", prefix.get(), ontologyIri.getIRIString()); + result.put(ontologyIri.getIRIString(), prefix.get()); + } else { remotePrefixResolver.resolvePrefix(ontologyIri).ifPresent(p -> result.put(ontologyIri.getIRIString(), p)); } return result; } + private static Optional resolveOntologyAnnotationValues(OWLOntology ontology, String property) { + final OWLDataFactory df = ontology.getOWLOntologyManager().getOWLDataFactory(); + assert ontology.getOntologyID().getOntologyIRI().isPresent(); + final Optional annotation = ontology.annotations(df.getOWLAnnotationProperty(property)) + .filter(ax -> ax.getValue().isLiteral()) + .map(ax -> ax.getValue().asLiteral().get().getLiteral()).findAny(); + if (annotation.isPresent()) { + return annotation; + } + final Optional annStr = EntitySearcher.getAnnotationAssertionAxioms(ontology.getOntologyID() + .getOntologyIRI() + .get(), ontology) + .filter(ax -> ax.getAnnotation().getProperty().getIRI().toString() + .equals(property) && ax.getValue().isLiteral()) + .map(ax -> ax.getValue().asLiteral().get().getLiteral()) + .findAny(); + if (annStr.isPresent()) { + return annStr; + } + return EntitySearcher.getDataPropertyValues(df.getOWLNamedIndividual(ontology.getOntologyID().getOntologyIRI() + .get()), df.getOWLDataProperty(property), ontology) + .map(OWLLiteral::getLiteral).findAny(); + } + private static Map resolvePrefixesFromPrefixMappingFile(String mappingFilePath) { if (mappingFilePath == null) { return Collections.emptyMap(); } final File file = new File(mappingFilePath); try { - LOG.debug("Loading prefix mapping from file '{}'.", file); + LOG.info("Loading prefix mapping from file '{}'.", file); final List lines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8); final Map prefixMap = new HashMap<>(lines.size()); lines.stream().filter(line -> !line.isBlank()).forEach(line -> { @@ -137,25 +156,48 @@ private static Map resolvePrefixesFromPrefixMappingFile(String m } /** - * Gets prefix for an ontology with the specified IRI. + * Gets prefix for the specified namespace or ontology IRI. * - * @param ontologyIri Ontology IRI + * @param namespace Namespace IRI or ontology IRI * @return Resolved prefix, if available */ - public Optional getPrefix(IRI ontologyIri) { - Objects.requireNonNull(ontologyIri); - return Optional.ofNullable(prefixes.get(ontologyIri.getIRIString())); + public Optional getNamespacePrefix(IRI namespace) { + Objects.requireNonNull(namespace); + return Optional.ofNullable(prefixes.get(namespace.getIRIString())); + } + + /** + * Gets prefix for a resource with the specified IRI. + *

+ * Primarily, this means extracting the namespace from the IRI and looking up the prefix for it. If no exact match + * is found, prefix with the longest matching namespace is returned. + * + * @param iri Resource IRI + * @return Resolved prefix, if available + */ + public Optional getPrefix(IRI iri) { + Objects.requireNonNull(iri); + final String namespace = iri.getNamespace(); + final Optional prefix = getNamespacePrefix(IRI.create(namespace)); + if (prefix.isPresent()) { + return prefix; + } + final String strIri = iri.getIRIString(); + return prefixes.keySet().stream().filter(strIri::startsWith).max(Comparator.comparingInt(String::length)) + .map(prefixes::get); } /** - * Checks whether a prefix has been resolved for the specified ontology IRI. + * Checks whether a prefix is available for the specified IRI. + *

+ * The prefix may be exact match for the namespace or the longest matching namespace for the IRI. * - * @param ontologyIri Ontology IRI - * @return {@code true} if a prefix is registered for the ontology IRI, {@code false} otherwise + * @param iri IRI to check + * @return {@code true} if a prefix is registered for the IRI, {@code false} otherwise */ - public boolean hasPrefix(IRI ontologyIri) { - Objects.requireNonNull(ontologyIri); - return prefixes.containsKey(ontologyIri.getIRIString()); + public boolean hasPrefix(IRI iri) { + final String strIri = iri.getIRIString(); + return prefixes.containsKey(iri.getNamespace()) || prefixes.keySet().stream().anyMatch(strIri::startsWith); } private static Map builtInPrefixes() { @@ -164,7 +206,9 @@ private static Map builtInPrefixes() { RDFS.NAMESPACE, RDFS.PREFIX, OWL.NAMESPACE, OWL.PREFIX, SKOS.NAMESPACE, SKOS.PREFIX, - DC.Terms.NAMESPACE, "dcterms" + DC.Terms.NAMESPACE, "dcterms", + DC.Elements.NAMESPACE, "dc", + "http://xmlns.com/foaf/0.1/", "foaf" ); } } diff --git a/jopa-owl2java/src/test/java/cz/cvut/kbss/jopa/owl2java/JavaNameGeneratorTest.java b/jopa-owl2java/src/test/java/cz/cvut/kbss/jopa/owl2java/JavaNameGeneratorTest.java index 280aea5e5..c78e3c3e7 100644 --- a/jopa-owl2java/src/test/java/cz/cvut/kbss/jopa/owl2java/JavaNameGeneratorTest.java +++ b/jopa-owl2java/src/test/java/cz/cvut/kbss/jopa/owl2java/JavaNameGeneratorTest.java @@ -25,14 +25,11 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.semanticweb.owlapi.model.IRI; -import org.semanticweb.owlapi.model.OWLOntologyID; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -82,32 +79,23 @@ void generateJavaNameForIriUsesPartBetweenLastAndSecondToLastSlashWhenSlashIsLas } @Test - void generatePrefixedJavaNameForIriUsesPrefixRegisteredForOntologyIri() { - final IRI ontologyIri = IRI.create(RDFS.NAMESPACE); - when(prefixMap.getPrefix(ontologyIri)).thenReturn(Optional.of(RDFS.PREFIX)); - assertEquals("rdfs_label", sut.generatePrefixedJavaNameForIri(IRI.create(RDFS.LABEL), new OWLOntologyID(ontologyIri))); + void generatePrefixedJavaNameForIriUsesPrefixRegisteredForNamespace() { + final IRI iri = IRI.create(RDFS.LABEL); + when(prefixMap.getPrefix(iri)).thenReturn(Optional.of(RDFS.PREFIX)); + assertEquals("rdfs_label", sut.generatePrefixedJavaNameForIri(iri)); } @Test - void generatePrefixedJavaNameForIriResolvesPrefixFromOntologyIriWhenNoPrefixIsRegisteredInPrefixMap() { - final IRI ontologyIri = IRI.create("http://www.w3.org/ns/activitystreams#"); + void generatePrefixedJavaNameForIriReturnsNameWithoutPrefixWhenNoPrefixIsRegistered() { final IRI iri = IRI.create("https://www.w3.org/ns/activitystreams#Event"); when(prefixMap.getPrefix(any())).thenReturn(Optional.empty()); - assertEquals("activitystreams_Event", sut.generatePrefixedJavaNameForIri(iri, new OWLOntologyID(ontologyIri))); - } - - @Test - void generatePrefixedJavaNameForIriReturnsNameWithoutPrefixWhenOntologyIdIsAnonymous() { - final IRI iri = IRI.create("https://www.w3.org/ns/activitystreams#Event"); - assertEquals("Event", sut.generatePrefixedJavaNameForIri(iri, new OWLOntologyID())); - verify(prefixMap, never()).getPrefix(any()); + assertEquals("Event", sut.generatePrefixedJavaNameForIri(iri)); } @Test void generatedPrefixedJavaNameForIriReturnsValidJavaNameWhenPrefixContainsDashes() { - final IRI ontologyIri = IRI.create("http://onto.fel.cvut.cz/ontologies/slovn\\u00edk/agendov\\u00fd/popis-dat"); final IRI iri = IRI.create("http://onto.fel.cvut.cz/ontologies/slovn\\u00edk/agendov\\u00fd/popis-dat/pojem/atribut"); - when(prefixMap.getPrefix(ontologyIri)).thenReturn(Optional.of("popis-dat")); - assertEquals("popis_dat_atribut", sut.generatePrefixedJavaNameForIri(iri, new OWLOntologyID(ontologyIri))); + when(prefixMap.getPrefix(iri)).thenReturn(Optional.of("popis-dat")); + assertEquals("popis_dat_atribut", sut.generatePrefixedJavaNameForIri(iri)); } } diff --git a/jopa-owl2java/src/test/java/cz/cvut/kbss/jopa/owl2java/JavaTransformerTest.java b/jopa-owl2java/src/test/java/cz/cvut/kbss/jopa/owl2java/JavaTransformerTest.java index 80430afb3..9dbccf7ca 100644 --- a/jopa-owl2java/src/test/java/cz/cvut/kbss/jopa/owl2java/JavaTransformerTest.java +++ b/jopa-owl2java/src/test/java/cz/cvut/kbss/jopa/owl2java/JavaTransformerTest.java @@ -57,8 +57,8 @@ class JavaTransformerTest { - private static final String PREFIX = "http://onto.fel.cvut.cz/ontologies/jopa/"; - private static final String ONTOLOGY_IRI = "http://onto.fel.cvut.cz/ontologies/owl2java/java-transformer-test/"; + private static final String ONTOLOGY_IRI = "http://onto.fel.cvut.cz/ontologies/owl2java/java-transformer-test"; + private static final String NAMESPACE = ONTOLOGY_IRI + "/"; private OWLOntologyManager ontologyManager; @@ -74,7 +74,7 @@ void setUp() throws Exception { this.ontology = ontologyManager.createOntology(IRI.create(ONTOLOGY_IRI)); this.dataFactory = new OWLDataFactoryImpl(); this.sut = new JavaTransformer(TransformationConfiguration.builder().packageName("") - .alwaysUseOntologyPrefix(false).build()); + .alwaysUseOntologyPrefixForVocabulary(false).build()); } @Test @@ -93,7 +93,7 @@ void generatesValidJavaIdentifiersForIrisWithNonAsciiCharacters() { @Test void generateModelCreatesToStringMethodForGeneratedModelClasses() { final String className = "TestClass"; - final IRI iri = IRI.create(PREFIX + className); + final IRI iri = IRI.create(NAMESPACE + className); ontology.add(dataFactory.getOWLDeclarationAxiom(dataFactory.getOWLClass(iri))); final ContextDefinition context = new ContextDefinition(); context.add(dataFactory.getOWLClass(iri)); @@ -108,7 +108,7 @@ void generateModelCreatesToStringMethodForGeneratedModelClasses() { @Test void shouldGenerateIdAndTypesFields() { final String className = "TestClass"; - final IRI iri = IRI.create(PREFIX + className); + final IRI iri = IRI.create(NAMESPACE + className); ontology.add(dataFactory.getOWLDeclarationAxiom(dataFactory.getOWLClass(iri))); final ContextDefinition context = new ContextDefinition(); context.add(dataFactory.getOWLClass(iri)); @@ -129,9 +129,9 @@ private void checkHasFieldWithName(final JDefinedClass cls, final String name) { @Test void shouldNotGenerateIdAndTypesFieldsInSubclasses() { final String className1 = "TestClass1"; - final IRI iri1 = IRI.create(PREFIX + className1); + final IRI iri1 = IRI.create(NAMESPACE + className1); final String className2 = "TestClass2"; - final IRI iri2 = IRI.create(PREFIX + className2); + final IRI iri2 = IRI.create(NAMESPACE + className2); ontology.add(dataFactory.getOWLDeclarationAxiom(dataFactory.getOWLClass(iri1))); ontology.add(dataFactory.getOWLDeclarationAxiom(dataFactory.getOWLClass(iri2))); @@ -174,9 +174,9 @@ void generateModelGeneratesFieldOfTypeMultilingualStringForLangStringRangeWhenCo } private ContextDefinition generateAxiomsForLangStrings(String className, String fieldName) { - final IRI classIri = IRI.create(PREFIX + className); + final IRI classIri = IRI.create(NAMESPACE + className); ontology.add(dataFactory.getOWLDeclarationAxiom(dataFactory.getOWLClass(classIri))); - final IRI propertyIri = IRI.create(PREFIX + fieldName); + final IRI propertyIri = IRI.create(NAMESPACE + fieldName); ontology.add(dataFactory.getOWLDeclarationAxiom(dataFactory.getOWLDataProperty(propertyIri))); ontology.add(dataFactory.getOWLDataPropertyRangeAxiom(dataFactory.getOWLDataProperty(propertyIri), OWL2Datatype.RDF_LANG_STRING)); @@ -194,7 +194,7 @@ private ContextDefinition generateAxiomsForLangStrings(String className, String @Test void generateModelGeneratesFieldOfTypeStringForLangStringRangeWhenConfiguredNotToPreferMultilingualStrings() { this.sut = new JavaTransformer(TransformationConfiguration.builder().preferMultilingualStrings(false) - .alwaysUseOntologyPrefix(false) + .alwaysUseOntologyPrefixForVocabulary(false) .packageName("").build()); final String className = "TestClass"; final String fieldName = "multilingualString"; @@ -211,7 +211,7 @@ void generateModelGeneratesFieldOfTypeStringForLangStringRangeWhenConfiguredNotT @Test void generateModelGeneratesJavadocWithCommentInDefaultLanguage() { final String className = "TestClass"; - final IRI iri = IRI.create(PREFIX + className); + final IRI iri = IRI.create(NAMESPACE + className); final OWLClass owlClass = dataFactory.getOWLClass(iri); ontology.add(dataFactory.getOWLDeclarationAxiom(owlClass)); final String expectedComment = "Comment in default language"; @@ -229,7 +229,7 @@ void generateModelGeneratesJavadocWithCommentInDefaultLanguage() { @Test void generateModelGeneratesJavadocWithCommentUsingLanguageLessAnnotationValue() { final String className = "TestClass"; - final IRI iri = IRI.create(PREFIX + className); + final IRI iri = IRI.create(NAMESPACE + className); final OWLClass owlClass = dataFactory.getOWLClass(iri); ontology.add(dataFactory.getOWLDeclarationAxiom(owlClass)); final String expectedComment = "Comment in default language"; @@ -246,10 +246,10 @@ void generateModelGeneratesJavadocWithCommentUsingLanguageLessAnnotationValue() @Test void generateModelDoesNotGenerateLabelAndDescriptionFieldsWhenDisabled() { this.sut = new JavaTransformer(TransformationConfiguration.builder().packageName("") - .alwaysUseOntologyPrefix(false) + .alwaysUseOntologyPrefixForVocabulary(false) .generateAnnotationFields(false).build()); final String className = "TestClass"; - final IRI iri = IRI.create(PREFIX + className); + final IRI iri = IRI.create(NAMESPACE + className); final OWLClass owlClass = dataFactory.getOWLClass(iri); ontology.add(dataFactory.getOWLDeclarationAxiom(owlClass)); final ContextDefinition context = new ContextDefinition(); @@ -278,9 +278,9 @@ void generateModelDoesNotGenerateThingEntityClassWhenDisabled() { @Test void generateModelDisambiguateClassesWithIdenticalJavaName() { this.sut = new JavaTransformer(TransformationConfiguration.builder().packageName("").generateThing(false) - .alwaysUseOntologyPrefix(false).build()); + .alwaysUseOntologyPrefixForVocabulary(false).build()); final String className = "Concept"; - final IRI iriOne = IRI.create(PREFIX + className); + final IRI iriOne = IRI.create(NAMESPACE + className); final IRI iriTwo = IRI.create(SKOS.CONCEPT); ontology.add(dataFactory.getOWLDeclarationAxiom(dataFactory.getOWLClass(iriOne))); ontology.add(dataFactory.getOWLDeclarationAxiom(dataFactory.getOWLClass(iriTwo))); @@ -303,7 +303,7 @@ void generateModelDisambiguateClassesWithIdenticalJavaName() { void generateVocabularyDisambiguateResourcesWithSameNameUsingPrefix() throws Exception { final OWLAnnotationProperty prefixProperty = dataFactory.getOWLAnnotationProperty(Defaults.ONTOLOGY_PREFIX_PROPERTY); ontology.add(dataFactory.getOWLAnnotationAssertionAxiom(prefixProperty, IRI.create(ONTOLOGY_IRI), dataFactory.getOWLLiteral("test"))); - final IRI personIri = IRI.create(PREFIX + "Person"); + final IRI personIri = IRI.create(NAMESPACE + "Person"); ontology.add(dataFactory.getOWLDeclarationAxiom(dataFactory.getOWLClass(personIri))); final ContextDefinition context = new ContextDefinition(); context.add(dataFactory.getOWLClass(personIri)); @@ -324,20 +324,28 @@ void generateVocabularyDisambiguateResourcesWithSameNameUsingPrefix() throws Exc } @Test - void generateVocabularyUsesOntologyPrefixesInFieldNamesWhenGeneratingOntologyIriConstants() throws Exception { + void generateVocabularyUsesOntologyIriLocalNameWhenGeneratingOntologyIriConstants() { + final ObjectModel result = sut.generateVocabulary(ontology, new ContextDefinition()); + final JDefinedClass vocabClass = result.codeModel()._getClass(Constants.VOCABULARY_CLASS); + assertNotNull(vocabClass); + final Map fields = vocabClass.fields(); + assertThat(fields.keySet(), hasItem("ONTOLOGY_IRI_JAVA_TRANSFORMER_TEST")); + } + + @Test + void generateVocabularyUsesPredefinedLocalNamesForKnownOntologiesWhenGeneratingOntologyIriConstants() throws Exception { final ContextDefinition context = new ContextDefinition(); - final OWLAnnotationProperty prefixProperty = dataFactory.getOWLAnnotationProperty(Defaults.ONTOLOGY_PREFIX_PROPERTY); - ontology.add(dataFactory.getOWLAnnotationAssertionAxiom(prefixProperty, IRI.create(ONTOLOGY_IRI), dataFactory.getOWLLiteral("test"))); final IRI foafOntIri = IRI.create("http://xmlns.com/foaf/0.1/"); final OWLOntology foafOnto = ontology.getOWLOntologyManager().createOntology(foafOntIri); - foafOnto.add(dataFactory.getOWLAnnotationAssertionAxiom(prefixProperty, foafOntIri, dataFactory.getOWLLiteral("foaf"))); + final IRI foafPersonIri = IRI.create(foafOntIri.getIRIString() + "Person"); + foafOnto.add(dataFactory.getOWLDeclarationAxiom(dataFactory.getOWLClass(foafPersonIri))); + context.add(dataFactory.getOWLClass(foafPersonIri)); final OWLOntology merged = new OWLOntologyMerger(ontologyManager).createMergedOntology(ontologyManager, null); - final ObjectModel result = sut.generateVocabulary(merged, context); + final ObjectModel result = sut.generateVocabulary(merged, new ContextDefinition()); final JDefinedClass vocabClass = result.codeModel()._getClass(Constants.VOCABULARY_CLASS); assertNotNull(vocabClass); final Map fields = vocabClass.fields(); - assertThat(fields.keySet(), hasItem("ONTOLOGY_IRI_TEST")); assertThat(fields.keySet(), hasItem("ONTOLOGY_IRI_FOAF")); } @@ -346,7 +354,7 @@ void generateModelDisambiguateClassesWithIdenticalJavaNameUsingOntologyPrefixes( this.sut = new JavaTransformer(TransformationConfiguration.builder().packageName("").generateThing(false) .build()); final String className = "Concept"; - final IRI iriOne = IRI.create(PREFIX + className); + final IRI iriOne = IRI.create(NAMESPACE + className); final IRI iriTwo = IRI.create(SKOS.CONCEPT); ontology.add(dataFactory.getOWLDeclarationAxiom(dataFactory.getOWLClass(iriOne))); final IRI skosOntIri = IRI.create(SKOS.NAMESPACE); @@ -374,10 +382,10 @@ void generateModelDisambiguateClassesWithIdenticalJavaNameUsingOntologyPrefixes( @Test void generateVocabularyAlwaysOutputsPrefixedNamesWhenConfiguredTo() { this.sut = new JavaTransformer(TransformationConfiguration.builder().packageName("") - .alwaysUseOntologyPrefix(true).build()); + .alwaysUseOntologyPrefixForVocabulary(true).build()); final OWLAnnotationProperty prefixProperty = dataFactory.getOWLAnnotationProperty(Defaults.ONTOLOGY_PREFIX_PROPERTY); ontology.add(dataFactory.getOWLAnnotationAssertionAxiom(prefixProperty, IRI.create(ONTOLOGY_IRI), dataFactory.getOWLLiteral("test"))); - final IRI personIri = IRI.create(PREFIX + "Person"); + final IRI personIri = IRI.create(NAMESPACE + "Person"); ontology.add(dataFactory.getOWLDeclarationAxiom(dataFactory.getOWLClass(personIri))); final ContextDefinition context = new ContextDefinition(); context.add(dataFactory.getOWLClass(personIri)); @@ -392,10 +400,11 @@ void generateVocabularyAlwaysOutputsPrefixedNamesWhenConfiguredTo() { @Test void generateModelAlwaysOutputsPrefixedEntityClassNamesWhenConfiguredTo() { this.sut = new JavaTransformer(TransformationConfiguration.builder().packageName("") - .alwaysUseOntologyPrefix(true) + .alwaysUseOntologyPrefixForVocabulary(true) + .alwaysUseOntologyPrefixForModel(true) .build()); final String className = "Concept"; - final IRI iriOne = IRI.create(PREFIX + className); + final IRI iriOne = IRI.create(NAMESPACE + className); final OWLAnnotationProperty prefixProperty = dataFactory.getOWLAnnotationProperty(Defaults.ONTOLOGY_PREFIX_PROPERTY); ontology.add(dataFactory.getOWLDeclarationAxiom(dataFactory.getOWLClass(iriOne))); ontology.add(dataFactory.getOWLAnnotationAssertionAxiom(prefixProperty, IRI.create(ONTOLOGY_IRI), dataFactory.getOWLLiteral("test"))); diff --git a/jopa-owl2java/src/test/java/cz/cvut/kbss/jopa/owl2java/OWL2JavaTransformerTest.java b/jopa-owl2java/src/test/java/cz/cvut/kbss/jopa/owl2java/OWL2JavaTransformerTest.java index bfe7d8dc1..161c2d14a 100644 --- a/jopa-owl2java/src/test/java/cz/cvut/kbss/jopa/owl2java/OWL2JavaTransformerTest.java +++ b/jopa-owl2java/src/test/java/cz/cvut/kbss/jopa/owl2java/OWL2JavaTransformerTest.java @@ -51,6 +51,7 @@ import static cz.cvut.kbss.jopa.owl2java.TestUtils.VOCABULARY_FILE; import static cz.cvut.kbss.jopa.owl2java.TestUtils.getTempDirectory; import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.hasItems; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.anyOf; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -59,7 +60,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class OWL2JavaTransformerTest { - + private static final String PACKAGE = "cz.cvut.kbss"; // Thing is always generated by OWL2Java @@ -140,7 +141,7 @@ private void verifyGeneratedModel(File currentDir) { currentDir = new File(currentDir + File.separator + Constants.MODEL_PACKAGE); final List classNames = Arrays.stream(currentDir.list()) .map(fn -> fn.substring(0, fn.indexOf('.'))).toList(); - assertTrue(classNames.containsAll(KNOWN_CLASSES)); + assertThat(classNames, hasItems(KNOWN_CLASSES.toArray(new String[0]))); } @Test @@ -481,7 +482,8 @@ public void generateVocabularyDoesNotGenerateDuplicateConstantsForImportedOntolo @ParameterizedTest @CsvSource({"true,true", "true,false", "false,true", "false,false"}) - public void generateVocabularyWithJavaUrisOrOwlIrisGeneratesClassWithRequestedFields(boolean withOwlApiIris, boolean withJavaUris) throws Exception { + public void generateVocabularyWithJavaUrisOrOwlIrisGeneratesClassWithRequestedFields(boolean withOwlApiIris, + boolean withJavaUris) throws Exception { this.targetDir = getTempDirectory(); this.targetDir = getTempDirectory(); transformer.setOntology(IC_ONTOLOGY_IRI, mappingFilePath); @@ -508,7 +510,19 @@ void generateVocabularyUsesUTF8ForIrisAndMappingFileOnWindows() throws Exception final File vocabularyFile = targetDir.listFiles()[0]; final String fileContents = readFile(vocabularyFile); assertThat(fileContents, anyOf(containsString("http://onto.fel.cvut.cz/ontologies/owl2java/slovník#vytvořil"), - containsString("http://onto.fel.cvut.cz/ontologies/owl2java/slovn\\u00edk#vytvo\\u0159il") + containsString("http://onto.fel.cvut.cz/ontologies/owl2java/slovn\\u00edk#vytvo\\u0159il") )); } + + @Test + public void generateVocabularyCorrectlyDisambiguatesPrefixesByNamespace() throws Exception { + this.targetDir = getTempDirectory(); + transformer.setOntology("http://onto.fel.cvut.cz/ontologies/jopa/with-name-collision", mappingFilePath); + transformer.generateVocabulary(config(null, "", targetDir.getAbsolutePath(), false, false).alwaysUseOntologyPrefixForVocabulary(true) + .build()); + final File vocabularyFile = targetDir.listFiles()[0]; + final String fileContents = readFile(vocabularyFile); + assertThat(fileContents, containsString("s_p_dcterms_isVersionOf")); + assertThat(fileContents, containsString("s_p_dcat_isVersionOf")); + } } diff --git a/jopa-owl2java/src/test/java/cz/cvut/kbss/jopa/owl2java/prefix/PrefixMapTest.java b/jopa-owl2java/src/test/java/cz/cvut/kbss/jopa/owl2java/prefix/PrefixMapTest.java index 074171cd4..6f2f1e1d3 100644 --- a/jopa-owl2java/src/test/java/cz/cvut/kbss/jopa/owl2java/prefix/PrefixMapTest.java +++ b/jopa-owl2java/src/test/java/cz/cvut/kbss/jopa/owl2java/prefix/PrefixMapTest.java @@ -32,6 +32,8 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.semanticweb.owlapi.apibinding.OWLManager; +import org.semanticweb.owlapi.formats.TurtleDocumentFormat; +import org.semanticweb.owlapi.io.StreamDocumentSource; import org.semanticweb.owlapi.model.IRI; import org.semanticweb.owlapi.model.OWLAnnotationProperty; import org.semanticweb.owlapi.model.OWLDataFactory; @@ -61,13 +63,13 @@ class PrefixMapTest { private final RemotePrefixResolver remotePrefixResolver = mock(RemotePrefixResolver.class); @Test - void getPrefixReturnsPrefixResolvedFromSingleStringAnnotationPropertyValue() { + void getNamespacePrefixReturnsOntologyPrefixResolvedFromSingleStringAnnotationPropertyValue() { final TransformationConfiguration config = configBuilder().build(); final String prefix = "owl2java"; final IRI ontologyIri = IRI.create(Generator.generateUri()); assertOntologyPrefixAnnotation(prefix, ontologyIri); final PrefixMap sut = new PrefixMap(ontologyManager, config); - final Optional result = sut.getPrefix(ontologyIri); + final Optional result = sut.getNamespacePrefix(ontologyIri); assertTrue(result.isPresent()); assertEquals(prefix, result.get()); } @@ -77,7 +79,7 @@ private TransformationConfiguration.TransformationConfigurationBuilder configBui } @Test - void getPrefixReturnsPrefixResolvedFromSingleStringDataPropertyValue() throws Exception { + void getNamespacePrefixReturnsOntologyPrefixResolvedFromSingleStringDataPropertyValue() throws Exception { final TransformationConfiguration config = configBuilder().build(); final String prefix = "owl2java"; final IRI ontologyIri = IRI.create(Generator.generateUri()); @@ -85,13 +87,13 @@ void getPrefixReturnsPrefixResolvedFromSingleStringDataPropertyValue() throws Ex final OWLDataProperty prefixProperty = dataFactory.getOWLDataProperty(Defaults.ONTOLOGY_PREFIX_PROPERTY); ontology.add(dataFactory.getOWLDataPropertyAssertionAxiom(prefixProperty, dataFactory.getOWLNamedIndividual(ontologyIri), dataFactory.getOWLLiteral(prefix))); final PrefixMap sut = new PrefixMap(ontologyManager, config); - final Optional result = sut.getPrefix(ontologyIri); + final Optional result = sut.getNamespacePrefix(ontologyIri); assertTrue(result.isPresent()); assertEquals(prefix, result.get()); } @Test - void getPrefixReturnsPrefixesOfMultipleOntologies() { + void getNamespacePrefixReturnsPrefixesOfMultipleOntologies() { final TransformationConfiguration config = configBuilder().build(); final Map prefixes = Map.of( "owl2java", IRI.create(Generator.generateUri()), @@ -101,37 +103,17 @@ void getPrefixReturnsPrefixesOfMultipleOntologies() { final PrefixMap sut = new PrefixMap(ontologyManager, config); prefixes.forEach((prefix, iri) -> { - final Optional result = sut.getPrefix(iri); + final Optional result = sut.getNamespacePrefix(iri); assertTrue(result.isPresent()); assertEquals(prefix, result.get()); }); } - @Test - void getPrefixReturnsPrefixWhenMultiplePrefixAssertionsAreInMergedOntology() { - final TransformationConfiguration config = configBuilder().build(); - final Map prefixes = Map.of( - "owl2java", IRI.create(Generator.generateUri()), - "jopa", IRI.create(Generator.generateUri()) - ); - final OWLOntology ontology = assertOntologyPrefixAnnotation("owl2java", prefixes.get("owl2java")); - final OWLAnnotationProperty prefixProperty = dataFactory.getOWLAnnotationProperty(Defaults.ONTOLOGY_PREFIX_PROPERTY); - ontology.add(dataFactory.getOWLAnnotationAssertionAxiom(prefixProperty, prefixes.get("jopa"), dataFactory.getOWLLiteral("jopa"))); - - final PrefixMap sut = new PrefixMap(ontologyManager, config); - prefixes.forEach((prefix, iri) -> { - final Optional result = sut.getPrefix(iri); - assertTrue(result.isPresent()); - assertEquals(prefix, result.get()); - }); - } - - private OWLOntology assertOntologyPrefixAnnotation(String prefix, IRI ontologyIri) { + private void assertOntologyPrefixAnnotation(String prefix, IRI ontologyIri) { try { final OWLOntology ontology = ontologyManager.createOntology(ontologyIri); final OWLAnnotationProperty prefixProperty = dataFactory.getOWLAnnotationProperty(Defaults.ONTOLOGY_PREFIX_PROPERTY); ontology.add(dataFactory.getOWLAnnotationAssertionAxiom(prefixProperty, ontologyIri, dataFactory.getOWLLiteral(prefix))); - return ontology; } catch (OWLOntologyCreationException e) { throw new RuntimeException(e); } @@ -139,13 +121,13 @@ private OWLOntology assertOntologyPrefixAnnotation(String prefix, IRI ontologyIr @ParameterizedTest @MethodSource("predefinedPrefixes") - void getPrefixReturnsPredefinedPrefixesForSelectedVocabularies(String expectedPrefix, IRI iri) { + void getNamespacePrefixReturnsPredefinedPrefixesForSelectedVocabularies(String expectedPrefix, IRI iri) { final TransformationConfiguration config = configBuilder().build(); final String prefix = "owl2java"; final IRI ontologyIri = IRI.create(Generator.generateUri()); assertOntologyPrefixAnnotation(prefix, ontologyIri); final PrefixMap sut = new PrefixMap(ontologyManager, config); - final Optional result = sut.getPrefix(iri); + final Optional result = sut.getNamespacePrefix(iri); assertTrue(result.isPresent()); assertEquals(expectedPrefix, result.get()); } @@ -161,14 +143,14 @@ static Stream predefinedPrefixes() { } @Test - void getPrefixReturnsPrefixResolvedFromPrefixMappingFileSpecifiedInConfiguration() { + void getPrefixReturnsPrefixResolvedFromOntologyPrefixMappingFileSpecifiedInConfiguration() { final String prefixMappingFilePath = TestUtils.resolveTestResourcesFilePath("prefixMappingFile"); final TransformationConfiguration config = configBuilder().prefixMappingFile(prefixMappingFilePath).build(); final PrefixMap sut = new PrefixMap(ontologyManager, config); - final Optional siocPrefix = sut.getPrefix(IRI.create("http://rdfs.org/sioc/ns#")); + final Optional siocPrefix = sut.getNamespacePrefix(IRI.create("http://rdfs.org/sioc/ns#")); assertTrue(siocPrefix.isPresent()); assertEquals("sioc", siocPrefix.get()); - final Optional ddoPrefix = sut.getPrefix(IRI.create("http://onto.fel.cvut.cz/ontologies/dataset-descriptor/")); + final Optional ddoPrefix = sut.getNamespacePrefix(IRI.create("http://onto.fel.cvut.cz/ontologies/dataset-descriptor/")); assertTrue(ddoPrefix.isPresent()); assertEquals("ddo", ddoPrefix.get()); } @@ -187,9 +169,60 @@ void prefixResolvingUsesRemotePrefixResolverWhenOntologyPrefixCannotBeResolvedLo ontologyManager.createOntology(ontologyIri); when(remotePrefixResolver.resolvePrefix(ontologyIri)).thenReturn(Optional.of(prefix)); final PrefixMap sut = new PrefixMap(ontologyManager, config); - final Optional result = sut.getPrefix(ontologyIri); + final Optional result = sut.getNamespacePrefix(ontologyIri); assertTrue(result.isPresent()); assertEquals(prefix, result.get()); verify(remotePrefixResolver).resolvePrefix(ontologyIri); } + + @Test + void prefixResolvingUsesPrefixAndNamespaceDeclaredInOntology() throws Exception { + final TransformationConfiguration config = configBuilder().build(); + ontologyManager.loadOntologyFromOntologyDocument(new StreamDocumentSource(PrefixMapTest.class.getClassLoader() + .getResourceAsStream("with-prefix.ttl"), + IRI.create("http://onto.fel.cvut.cz/ontologies/application/termit"), new TurtleDocumentFormat(), "text/turtle")); + final PrefixMap sut = new PrefixMap(ontologyManager, config); + + final Optional result = sut.getNamespacePrefix(IRI.create("http://onto.fel.cvut.cz/ontologies/application/termit")); + assertTrue(result.isPresent()); + assertEquals("termit", result.get()); + } + + @Test + void getPrefixReturnsPrefixResolvedForNamespaceOfResourceWithSlashDelimiter() throws Exception { + final TransformationConfiguration config = configBuilder().build(); + ontologyManager.loadOntologyFromOntologyDocument(new StreamDocumentSource(PrefixMapTest.class.getClassLoader() + .getResourceAsStream("with-prefix.ttl"), + IRI.create("http://onto.fel.cvut.cz/ontologies/application/termit"), new TurtleDocumentFormat(), "text/turtle")); + final PrefixMap sut = new PrefixMap(ontologyManager, config); + + final Optional result = sut.getPrefix(IRI.create("http://onto.fel.cvut.cz/ontologies/application/termit/term/vocabulary")); + assertTrue(result.isPresent()); + assertEquals("termit", result.get()); + } + + @Test + void getPrefixReturnsPrefixResolvedForNamespaceOfResourceWithHashtagDelimiter() { + final TransformationConfiguration config = configBuilder().build(); + final String prefix = "owl2java"; + final IRI ontologyIri = IRI.create(Generator.generateUri()); + assertOntologyPrefixAnnotation(prefix, ontologyIri); + final PrefixMap sut = new PrefixMap(ontologyManager, config); + + final Optional result = sut.getPrefix(IRI.create(RDFS.CLASS)); + assertTrue(result.isPresent()); + assertEquals("rdfs", result.get()); + } + + @Test + void getPrefixAttemptsToMatchLongestRegisteredNamespaceAsResourceIriSubstringForPrefixResolution() { + final TransformationConfiguration config = configBuilder().build(); + final String prefix = "owl2java"; + final IRI ontologyIri = IRI.create("http://onto.fel.cvut.cz/ontologies/jopa/owl2java"); + assertOntologyPrefixAnnotation(prefix, ontologyIri); + final PrefixMap sut = new PrefixMap(ontologyManager, config); + final Optional result = sut.getPrefix(IRI.create("http://onto.fel.cvut.cz/ontologies/jopa/owl2java/term/vocabulary")); + assertTrue(result.isPresent()); + assertEquals(prefix, result.get()); + } } diff --git a/jopa-owl2java/src/test/resources/gh-432.ttl b/jopa-owl2java/src/test/resources/gh-432.ttl new file mode 100644 index 000000000..8c792c4a0 --- /dev/null +++ b/jopa-owl2java/src/test/resources/gh-432.ttl @@ -0,0 +1,9 @@ +@prefix owl: . +@prefix test: . + + rdf:type owl:Ontology ; + owl:imports . + +test:ontologyIdentifier rdf:type owl:ObjectProperty ; + rdfs:comment "Identifier of the ontology published in the series"@en ; + rdfs:label "Ontology identifier"@en . diff --git a/jopa-owl2java/src/test/resources/mapping b/jopa-owl2java/src/test/resources/mapping index 674462fef..05395a5de 100644 --- a/jopa-owl2java/src/test/resources/mapping +++ b/jopa-owl2java/src/test/resources/mapping @@ -6,4 +6,5 @@ http://krizik.felk.cvut.cz/ontologies/import-with-same-property.owl > ./import-w http://krizik.felk.cvut.cz/ontologies/onto-with-same-property-in-import.owl > ./onto-with-same-property-in-import.owl http://krizik.felk.cvut.cz/ontologies/model > modelOne.owl http://onto.fel.cvut.cz/ontologies/model > modelTwo.owl -http://onto.fel.cvut.cz/ontologies/owl2java/slovník > slovník.owl \ No newline at end of file +http://onto.fel.cvut.cz/ontologies/owl2java/slovník > slovník.owl +http://onto.fel.cvut.cz/ontologies/jopa/with-name-collision > gh-432.ttl diff --git a/jopa-owl2java/src/test/resources/with-prefix.ttl b/jopa-owl2java/src/test/resources/with-prefix.ttl new file mode 100644 index 000000000..1552456ac --- /dev/null +++ b/jopa-owl2java/src/test/resources/with-prefix.ttl @@ -0,0 +1,17 @@ +@prefix rdf: . +@prefix owl: . +@prefix xsd: . +@prefix rdfs: . + + a owl:Ontology ; + + "2020-05-26" ; + + "Slovník systému TermIt - slovník"@cs , "Vocabulary of system TermIt - vocabulary"@en ; + + "Specifikace"@cs , "Specification"@en ; + + "termit" ; + + "http://onto.fel.cvut.cz/ontologies/application/termit/term/" ; + owl:versionIRI . diff --git a/jopa-owlapi-utils/pom.xml b/jopa-owlapi-utils/pom.xml index d6e97a42d..564c64e68 100644 --- a/jopa-owlapi-utils/pom.xml +++ b/jopa-owlapi-utils/pom.xml @@ -6,7 +6,7 @@ cz.cvut.kbss.jopa jopa-all - 2.9.4 + 2.10.0 ../pom.xml diff --git a/modelgen/pom.xml b/modelgen/pom.xml index 53ed75dda..0b864bc15 100644 --- a/modelgen/pom.xml +++ b/modelgen/pom.xml @@ -6,7 +6,7 @@ jopa-all cz.cvut.kbss.jopa - 2.9.4 + 2.10.0 ../pom.xml diff --git a/ontodriver-api/pom.xml b/ontodriver-api/pom.xml index 0c54de62c..d77f3c157 100644 --- a/ontodriver-api/pom.xml +++ b/ontodriver-api/pom.xml @@ -6,7 +6,7 @@ jopa-all cz.cvut.kbss.jopa - 2.9.4 + 2.10.0 ../pom.xml diff --git a/ontodriver-jena/pom.xml b/ontodriver-jena/pom.xml index f13e29357..564c7ef57 100644 --- a/ontodriver-jena/pom.xml +++ b/ontodriver-jena/pom.xml @@ -8,7 +8,7 @@ cz.cvut.kbss.jopa jopa-all - 2.9.4 + 2.10.0 ../pom.xml @@ -57,6 +57,12 @@ xml-apis 1.4.01 + + org.junit.platform + junit-platform-launcher + ${org.junit.jupiter.version} + test + diff --git a/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/AxiomLoaderTestBase.java b/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/AxiomLoaderTestBase.java index c11b94737..f3ec8dc4a 100644 --- a/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/AxiomLoaderTestBase.java +++ b/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/AxiomLoaderTestBase.java @@ -27,12 +27,10 @@ import java.util.HashMap; import java.util.Map; -import static org.apache.jena.rdf.model.ResourceFactory.createResource; - abstract class AxiomLoaderTestBase { static final NamedResource SUBJECT = NamedResource.create(Generator.generateUri()); - static final Resource SUBJECT_RES = createResource(SUBJECT.getIdentifier().toString()); + static final Resource SUBJECT_RES = Generator.generateResource(SUBJECT.getIdentifier().toString()); static final URI CONTEXT = Generator.generateUri(); Map mapAssertions(AxiomDescriptor descriptor) { diff --git a/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/EpistemicAxiomRemoverTest.java b/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/EpistemicAxiomRemoverTest.java index 6ccf2cac1..187364708 100644 --- a/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/EpistemicAxiomRemoverTest.java +++ b/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/EpistemicAxiomRemoverTest.java @@ -24,7 +24,6 @@ import cz.cvut.kbss.ontodriver.model.Assertion; import cz.cvut.kbss.ontodriver.model.NamedResource; import org.apache.jena.rdf.model.Resource; -import org.apache.jena.rdf.model.ResourceFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -42,7 +41,7 @@ public class EpistemicAxiomRemoverTest { private static final NamedResource SUBJECT = NamedResource.create(Generator.generateUri()); - private static final Resource SUBJECT_RESOURCE = ResourceFactory.createResource(SUBJECT.getIdentifier().toString()); + private static final Resource SUBJECT_RESOURCE = Generator.generateResource(SUBJECT.getIdentifier().toString()); @Mock private StorageConnector connectorMock; diff --git a/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/JenaAdapterTest.java b/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/JenaAdapterTest.java index 5c141a231..6c59caf6b 100644 --- a/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/JenaAdapterTest.java +++ b/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/JenaAdapterTest.java @@ -68,7 +68,7 @@ class JenaAdapterTest { private static final NamedResource SUBJECT = NamedResource.create(Generator.generateUri()); - private static final Resource SUBJECT_RESOURCE = ResourceFactory.createResource(SUBJECT.getIdentifier().toString()); + private static final Resource SUBJECT_RESOURCE = Generator.generateResource(SUBJECT.getIdentifier().toString()); @Mock private StorageConnector connectorMock; diff --git a/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/MainAxiomLoaderTest.java b/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/MainAxiomLoaderTest.java index 1a82c3ba2..4623a1d3d 100644 --- a/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/MainAxiomLoaderTest.java +++ b/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/MainAxiomLoaderTest.java @@ -21,7 +21,11 @@ import cz.cvut.kbss.ontodriver.jena.connector.InferredStorageConnector; import cz.cvut.kbss.ontodriver.jena.connector.StorageConnector; import cz.cvut.kbss.ontodriver.jena.environment.Generator; -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.apache.jena.rdf.model.Property; import org.apache.jena.rdf.model.Resource; import org.apache.jena.vocabulary.RDF; @@ -35,18 +39,25 @@ import java.util.Collection; import java.util.Collections; -import static org.apache.jena.rdf.model.ResourceFactory.*; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.apache.jena.rdf.model.ResourceFactory.createProperty; +import static org.apache.jena.rdf.model.ResourceFactory.createResource; +import static org.apache.jena.rdf.model.ResourceFactory.createStatement; +import static org.apache.jena.rdf.model.ResourceFactory.createTypedLiteral; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class MainAxiomLoaderTest { private static final NamedResource SUBJECT = NamedResource.create(Generator.generateUri()); - private static final Resource SUBJECT_RES = createResource(SUBJECT.getIdentifier().toString()); + private static final Resource SUBJECT_RES = Generator.generateResource(SUBJECT.getIdentifier().toString()); private static final Property PROPERTY = createProperty(Generator.generateUri().toString()); private static final NamedResource OBJECT = NamedResource.create(Generator.generateUri()); - private static final Resource OBJECT_RES = createResource(OBJECT.getIdentifier().toString()); + private static final Resource OBJECT_RES = Generator.generateResource(OBJECT.getIdentifier().toString()); @Mock private StorageConnector connectorMock; diff --git a/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/environment/Generator.java b/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/environment/Generator.java index 93c089471..e71287377 100644 --- a/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/environment/Generator.java +++ b/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/environment/Generator.java @@ -17,6 +17,9 @@ */ package cz.cvut.kbss.ontodriver.jena.environment; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; + import java.net.URI; import java.util.Random; @@ -41,4 +44,14 @@ public static int randomInt(int max) { public static boolean randomBoolean() { return RANDOM.nextBoolean(); } + + // Synchronize resource creation to prevent race conditions in concurrent tests. + public static synchronized Resource generateResource() { + return ResourceFactory.createResource(generateUri().toString()); + } + + // Synchronize resource creation to prevent race conditions in concurrent tests. + public static synchronized Resource generateResource(String iri) { + return ResourceFactory.createResource(iri); + } } diff --git a/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/environment/TestEnvironmentInit.java b/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/environment/TestEnvironmentInit.java new file mode 100644 index 000000000..2a02b3430 --- /dev/null +++ b/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/environment/TestEnvironmentInit.java @@ -0,0 +1,24 @@ +package cz.cvut.kbss.ontodriver.jena.environment; + +import org.apache.jena.rdf.model.ResourceFactory; +import org.apache.jena.sys.JenaSystem; +import org.junit.platform.launcher.LauncherSession; +import org.junit.platform.launcher.LauncherSessionListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TestEnvironmentInit implements LauncherSessionListener { + + private static final Logger LOG = LoggerFactory.getLogger(TestEnvironmentInit.class); + + /** + * Initializes Jena environment once before running all tests to prevent class initialization deadlock of concurrent + * tests. + */ + @Override + public void launcherSessionOpened(LauncherSession session) { + LOG.info("Initializing Jena environment"); + JenaSystem.init(); + ResourceFactory.createResource("urn:warmup"); + } +} diff --git a/ontodriver-jena/src/test/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener b/ontodriver-jena/src/test/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener new file mode 100644 index 000000000..682fb3606 --- /dev/null +++ b/ontodriver-jena/src/test/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener @@ -0,0 +1 @@ +cz.cvut.kbss.ontodriver.jena.environment.TestEnvironmentInit diff --git a/ontodriver-owlapi/pom.xml b/ontodriver-owlapi/pom.xml index 7ff0213e5..b4c73de42 100644 --- a/ontodriver-owlapi/pom.xml +++ b/ontodriver-owlapi/pom.xml @@ -11,7 +11,7 @@ cz.cvut.kbss.jopa jopa-all - 2.9.4 + 2.10.0 ../pom.xml diff --git a/ontodriver-rdf4j/pom.xml b/ontodriver-rdf4j/pom.xml index fc04f00e3..92339c595 100644 --- a/ontodriver-rdf4j/pom.xml +++ b/ontodriver-rdf4j/pom.xml @@ -10,11 +10,11 @@ jopa-all cz.cvut.kbss.jopa - 2.9.4 + 2.10.0 - 5.3.0 + 5.3.1 diff --git a/ontodriver-virtuoso/pom.xml b/ontodriver-virtuoso/pom.xml index 077d34210..ce5523947 100644 --- a/ontodriver-virtuoso/pom.xml +++ b/ontodriver-virtuoso/pom.xml @@ -6,7 +6,7 @@ cz.cvut.kbss.jopa jopa-all - 2.9.4 + 2.10.0 ontodriver-virtuoso diff --git a/pom.xml b/pom.xml index 1ef892312..25a5f616a 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 cz.cvut.kbss.jopa - 2.9.4 + 2.10.0 jopa-all pom JOPA