ask();
+
/**
* Modify the query to restrict the query result according to the specified boolean expression. Replaces the
* previously added restriction(s), if any.
diff --git a/jopa-distribution/pom.xml b/jopa-distribution/pom.xml
index e1294828b..b88a2e7a5 100644
--- a/jopa-distribution/pom.xml
+++ b/jopa-distribution/pom.xml
@@ -6,7 +6,7 @@
cz.cvut.kbss.jopa
jopa-all
- 2.10.1
+ 2.11.0
../pom.xml
diff --git a/jopa-impl/pom.xml b/jopa-impl/pom.xml
index 4912214e2..197a08d3d 100644
--- a/jopa-impl/pom.xml
+++ b/jopa-impl/pom.xml
@@ -6,7 +6,7 @@
cz.cvut.kbss.jopa
jopa-all
- 2.10.1
+ 2.11.0
../pom.xml
diff --git a/jopa-impl/src/main/antlr4/cz/cvut/kbss/jopa/query/soql/Soql.g4 b/jopa-impl/src/main/antlr4/cz/cvut/kbss/jopa/query/soql/Soql.g4
index 8091af490..9cdb4d8bb 100644
--- a/jopa-impl/src/main/antlr4/cz/cvut/kbss/jopa/query/soql/Soql.g4
+++ b/jopa-impl/src/main/antlr4/cz/cvut/kbss/jopa/query/soql/Soql.g4
@@ -2,10 +2,12 @@ grammar Soql;
start: querySentence EOF ;
-querySentence: selectStatement ;
+querySentence: selectStatement | askStatement ;
selectStatement: selectClause fromClause whereClause? groupByClause? orderByClause? ;
+askStatement: ASK fromClause whereClause? ;
+
singleValuedObjectPathExpression: simpleSubpath DOT singleValuedObjectField ;
simpleSubpath: singleValuedObjectField (DOT simpleSubpath)* ;
@@ -152,6 +154,8 @@ comparisonOperator
SELECT: 'SELECT' ;
+ASK: 'ASK' ;
+
WHERE: 'WHERE' ;
NOT: 'NOT' ;
diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/id/AbstractIdentifierGenerator.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/id/AbstractIdentifierGenerator.java
new file mode 100644
index 000000000..19faa6885
--- /dev/null
+++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/id/AbstractIdentifierGenerator.java
@@ -0,0 +1,39 @@
+package cz.cvut.kbss.jopa.id;
+
+import cz.cvut.kbss.ontodriver.Connection;
+import cz.cvut.kbss.ontodriver.exception.IdentifierGenerationException;
+import cz.cvut.kbss.ontodriver.exception.OntoDriverException;
+import cz.cvut.kbss.ontodriver.model.Assertion;
+import cz.cvut.kbss.ontodriver.model.AxiomImpl;
+import cz.cvut.kbss.ontodriver.model.NamedResource;
+import cz.cvut.kbss.ontodriver.model.Value;
+
+import java.net.URI;
+import java.util.Set;
+
+/**
+ * Common superclass with utilities for identifier generation.
+ */
+public abstract class AbstractIdentifierGenerator implements IdentifierGenerator {
+
+ /**
+ * Checks if a class assertion for the specified identifier exists in the repository.
+ *
+ * This check is done against the default context.
+ *
+ * @param identifier Instance identifier
+ * @param classIri Class identifier
+ * @param connection Repository connection
+ * @return {@code true} if the class assertion exists, {@code false} otherwise
+ * @throws IdentifierGenerationException If unable to check if the identifier exists
+ */
+ protected boolean exists(URI identifier, URI classIri, Connection connection) {
+ try {
+ return connection.contains(new AxiomImpl<>(NamedResource.create(identifier),
+ Assertion.createClassAssertion(false),
+ new Value<>(classIri)), Set.of());
+ } catch (OntoDriverException e) {
+ throw new IdentifierGenerationException("Unable to check if identifier '" + identifier + "' exists.", e);
+ }
+ }
+}
diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/id/RandomNumberIdentifierGenerator.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/id/RandomNumberIdentifierGenerator.java
new file mode 100644
index 000000000..5f621428d
--- /dev/null
+++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/id/RandomNumberIdentifierGenerator.java
@@ -0,0 +1,46 @@
+package cz.cvut.kbss.jopa.id;
+
+import cz.cvut.kbss.jopa.model.metamodel.EntityType;
+import cz.cvut.kbss.ontodriver.Connection;
+import cz.cvut.kbss.ontodriver.exception.IdentifierGenerationException;
+
+import java.net.URI;
+import java.util.Objects;
+import java.util.Random;
+
+public class RandomNumberIdentifierGenerator extends AbstractIdentifierGenerator {
+
+ private static final int GENERATION_THRESHOLD = 64;
+
+ private static final Random RANDOM = new Random();
+
+ @Override
+ public URI generate(Object entity, EntityType entityClass, Connection connection) {
+ Objects.requireNonNull(entity);
+ Objects.requireNonNull(entityClass);
+ Objects.requireNonNull(connection);
+
+ final URI classUri = entityClass.getIRI().toURI();
+ int counter = 0;
+ while (counter++ < GENERATION_THRESHOLD) {
+ final URI id = generate(classUri);
+ if (!exists(id, classUri, connection)) {
+ return id;
+ }
+ }
+ throw new IdentifierGenerationException("Unable to generate a unique identifier.");
+ }
+
+ private URI generate(URI classUri) {
+ if (classUri.getFragment() != null) {
+ return URI.create(classUri + "_instance" + RANDOM.nextInt());
+ } else {
+ String base = classUri.toString();
+ if (base.endsWith("/")) {
+ return URI.create(base + "instance" + RANDOM.nextInt());
+ } else {
+ return URI.create(base + "/instance" + RANDOM.nextInt());
+ }
+ }
+ }
+}
diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/CriteriaQueryImpl.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/CriteriaQueryImpl.java
index 12ac6ba82..4467deeac 100644
--- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/CriteriaQueryImpl.java
+++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/CriteriaQueryImpl.java
@@ -25,6 +25,7 @@
import cz.cvut.kbss.jopa.model.query.criteria.Predicate;
import cz.cvut.kbss.jopa.model.query.criteria.Root;
import cz.cvut.kbss.jopa.model.query.criteria.Selection;
+import cz.cvut.kbss.jopa.query.QueryType;
import cz.cvut.kbss.jopa.query.criteria.AbstractPredicate;
import cz.cvut.kbss.jopa.query.criteria.CriteriaBuilderImpl;
import cz.cvut.kbss.jopa.query.criteria.CriteriaParameterFiller;
@@ -45,6 +46,8 @@ public class CriteriaQueryImpl implements CriteriaQuery {
private final Metamodel metamodel;
private final CriteriaBuilderImpl cb;
+ private QueryType queryType = QueryType.SELECT;
+
public CriteriaQueryImpl(CriteriaQueryHolder query, Metamodel metamodel, CriteriaBuilderImpl cb) {
this.query = Objects.requireNonNull(query);
@@ -61,7 +64,7 @@ public Root from(Class entityClass) {
@Override
public Root from(EntityType entity) {
- RootImpl root = new RootImpl<>(metamodel, null, entity.getBindableJavaType(), this.cb);
+ RootImpl root = new RootImpl<>(metamodel, null, entity.getJavaType(), this.cb);
query.setRoot(root);
return root;
}
@@ -69,9 +72,18 @@ public Root from(EntityType entity) {
@Override
public CriteriaQuery select(Selection extends T> selection) {
query.setSelection(selection);
+ this.queryType = QueryType.SELECT;
return this;
}
+ @Override
+ public CriteriaQuery ask() {
+ this.queryType = QueryType.ASK;
+ query.setSelection(null);
+ assert Boolean.class.equals(query.getResultType());
+ return (CriteriaQuery) this;
+ }
+
@Override
public CriteriaQuery where(Expression expression) {
query.setWhere(cb.wrapExpressionToPredicateWithRepair(expression));
@@ -188,44 +200,69 @@ public CriteriaQuery having(Predicate... restrictions) {
* @return string representation of SOQL query
*/
public String translateQuery(CriteriaParameterFiller parameterFiller) {
- StringBuilder stringBuilder = new StringBuilder();
- stringBuilder.append(SoqlConstants.SELECT).append(' ');
+ StringBuilder soqlQuery = new StringBuilder();
+ soqlQuery.append(queryType.getKeyword());
if (isDistinct()) {
- stringBuilder.append(SoqlConstants.DISTINCT).append(' ');
+ soqlQuery.append(' ').append(SoqlConstants.DISTINCT);
}
- ((AbstractExpression) query.getSelection()).setExpressionToQuery(stringBuilder, parameterFiller);
+ if (queryType == QueryType.SELECT) {
+ soqlQuery.append(' ');
+ ((AbstractExpression) query.getSelection()).setExpressionToQuery(soqlQuery, parameterFiller);
+ }
+
+ appendFromClause(parameterFiller, soqlQuery);
+
+ appendWhereClause(parameterFiller, soqlQuery);
+
+ appendGroupByClause(parameterFiller, soqlQuery);
- stringBuilder.append(' ').append(SoqlConstants.FROM).append(' ').append(((RootImpl) query.getRoot()).getJavaType().getSimpleName()).append(' ');
- ((RootImpl) query.getRoot()).setExpressionToQuery(stringBuilder, parameterFiller);
+ appendHavingClause(parameterFiller, soqlQuery);
+ appendOrderByClause(parameterFiller, soqlQuery);
+
+ return soqlQuery.toString();
+ }
+
+ private void appendFromClause(CriteriaParameterFiller parameterFiller, StringBuilder soqlQuery) {
+ soqlQuery.append(' ').append(SoqlConstants.FROM).append(' ')
+ .append(((RootImpl) query.getRoot()).getJavaType().getSimpleName()).append(' ');
+ ((RootImpl) query.getRoot()).setExpressionToQuery(soqlQuery, parameterFiller);
+ }
+
+ private void appendWhereClause(CriteriaParameterFiller parameterFiller, StringBuilder soqlQuery) {
if (query.getWhere() != null && !query.getWhere().getExpressions().isEmpty()) {
- stringBuilder.append(' ').append(SoqlConstants.WHERE).append(' ');
- ((AbstractPredicate) query.getWhere()).setExpressionToQuery(stringBuilder, parameterFiller);
+ soqlQuery.append(' ').append(SoqlConstants.WHERE).append(' ');
+ ((AbstractPredicate) query.getWhere()).setExpressionToQuery(soqlQuery, parameterFiller);
}
+ }
+ private void appendGroupByClause(CriteriaParameterFiller parameterFiller, StringBuilder soqlQuery) {
if (query.getGroupBy() != null && !query.getGroupBy().isEmpty()) {
- stringBuilder.append(' ').append(SoqlConstants.GROUP_BY).append(' ');
+ soqlQuery.append(' ').append(SoqlConstants.GROUP_BY).append(' ');
for (Expression groupBy : query.getGroupBy()) {
- ((AbstractExpression) groupBy).setExpressionToQuery(stringBuilder, parameterFiller);
+ ((AbstractExpression) groupBy).setExpressionToQuery(soqlQuery, parameterFiller);
}
}
+ }
+ private void appendHavingClause(CriteriaParameterFiller parameterFiller, StringBuilder soqlQuery) {
if (query.getHaving() != null && !query.getHaving().getExpressions().isEmpty()) {
- stringBuilder.append(" HAVING ");
- ((AbstractPredicate) query.getHaving()).setExpressionToQuery(stringBuilder, parameterFiller);
+ soqlQuery.append(" HAVING ");
+ ((AbstractPredicate) query.getHaving()).setExpressionToQuery(soqlQuery, parameterFiller);
}
+ }
+ private void appendOrderByClause(CriteriaParameterFiller parameterFiller, StringBuilder soqlQuery) {
if (!getOrderList().isEmpty()) {
- stringBuilder.append(' ').append(SoqlConstants.ORDER_BY).append(' ');
+ soqlQuery.append(' ').append(SoqlConstants.ORDER_BY).append(' ');
List orders = getOrderList();
for (int i = 0; i < orders.size(); i++) {
- ((AbstractExpression) orders.get(i).getExpression()).setExpressionToQuery(stringBuilder, parameterFiller);
- stringBuilder.append(' ').append(orders.get(i).isAscending() ? SoqlConstants.ASC : SoqlConstants.DESC);
+ ((AbstractExpression) orders.get(i).getExpression()).setExpressionToQuery(soqlQuery, parameterFiller);
+ soqlQuery.append(' ').append(orders.get(i).isAscending() ? SoqlConstants.ASC : SoqlConstants.DESC);
if (orders.size() > 1 && (i + 1) != orders.size()) {
- stringBuilder.append(", ");
+ soqlQuery.append(", ");
}
}
}
- return stringBuilder.toString();
}
}
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 b39986af0..44a9f8018 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
@@ -18,6 +18,8 @@
package cz.cvut.kbss.jopa.model;
import cz.cvut.kbss.jopa.exceptions.OWLPersistenceException;
+import cz.cvut.kbss.jopa.id.IdentifierGenerator;
+import cz.cvut.kbss.jopa.id.RandomNumberIdentifierGenerator;
import cz.cvut.kbss.jopa.loaders.PersistenceUnitClassFinder;
import cz.cvut.kbss.jopa.model.metamodel.EntityType;
import cz.cvut.kbss.jopa.model.metamodel.IdentifiableEntityType;
@@ -61,6 +63,8 @@ public class MetamodelImpl implements Metamodel, MetamodelProvider, MetamodelCla
// Proxy classes for results of EntityManager.getReference
private final Map, Class>> referenceProxyClasses = new ConcurrentHashMap<>();
+ private Map, IdentifierGenerator> idGenerators;
+
private NamedQueryManager namedQueryManager;
private ResultSetMappingManager resultSetMappingManager;
private NamedEntityGraphManager namedEntityGraphManager;
@@ -108,6 +112,7 @@ private void initFromMetamodelBuilder(MetamodelBuilder metamodelBuilder) {
this.namespaceResolver = metamodelBuilder.getNamespaceResolver();
this.typeReferenceMap = metamodelBuilder.getTypeReferenceMap();
this.lazyLoadingProxyClasses.putAll(metamodelBuilder.getLazyLoadingEntityProxyClasses());
+ this.idGenerators = metamodelBuilder.getIdGenerators();
}
/**
@@ -176,6 +181,10 @@ public NamedEntityGraphManager getNamedEntityGraphManager() {
return namedEntityGraphManager;
}
+ public IdentifierGenerator getIdentifierGenerator(EntityType> entityType) {
+ return idGenerators.getOrDefault(entityType, new RandomNumberIdentifierGenerator());
+ }
+
@Override
public Set getModuleExtractionExtraSignature() {
return Collections.unmodifiableSet(getSignatureInternal());
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 4d751cbb5..733371e3e 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
@@ -18,11 +18,13 @@
package cz.cvut.kbss.jopa.model.metamodel;
import cz.cvut.kbss.jopa.exception.MetamodelInitializationException;
+import cz.cvut.kbss.jopa.id.IdentifierGenerator;
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.IdGenerator;
import cz.cvut.kbss.jopa.model.annotations.Inheritance;
import cz.cvut.kbss.jopa.model.annotations.InheritanceType;
import cz.cvut.kbss.jopa.model.annotations.OWLAnnotationProperty;
@@ -38,7 +40,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
@@ -333,6 +337,32 @@ public Map, Class>> getLazyLoadingEntityProxyClasses() {
return Collections.unmodifiableMap(lazyLoadingProxyClasses);
}
+ public Map, IdentifierGenerator> getIdGenerators() {
+ final Map, IdentifierGenerator> generators = new HashMap<>();
+ final Map, IdentifierGenerator> result = new HashMap<>();
+ for (EntityType> et : getEntities().values()) {
+ if (et.getJavaType().isInterface() || !et.getIdentifier().isGenerated()) {
+ continue;
+ }
+ final IdGenerator idGeneratorAnn = et.getIdentifier().getJavaField().getAnnotation(IdGenerator.class);
+ if (idGeneratorAnn == null) {
+ continue;
+ }
+ final IdentifierGenerator gen = generators.computeIfAbsent(idGeneratorAnn.value(), cls -> {
+ try {
+ final Constructor extends IdentifierGenerator> constructor = cls.getDeclaredConstructor();
+ return constructor.newInstance();
+ } catch (NoSuchMethodException | SecurityException e) {
+ throw new MetamodelInitializationException("Custom identifier generator class " + cls + " is missing public no-arg constructor.");
+ } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
+ throw new MetamodelInitializationException("Failed to create instance of custom identifier generator " + cls, e);
+ }
+ });
+ result.put(et, gen);
+ }
+ return result;
+ }
+
public Set getAnnotatedAccessorsForClass(IdentifiableType> k) {
return annotatedAccessors.getOrDefault(k, Collections.emptySet());
}
diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/StaticMetamodelInitializer.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/StaticMetamodelInitializer.java
index 76f6dc08a..fcc869eaa 100644
--- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/StaticMetamodelInitializer.java
+++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/model/metamodel/StaticMetamodelInitializer.java
@@ -179,7 +179,7 @@ private static void setPropertyIriFieldValue(ManagedType et, Field f) thr
}
}
- private FieldSpecification getMetamodelMember(Field field, ManagedType type) {
+ private static FieldSpecification getMetamodelMember(Field field, ManagedType type) {
LOG.trace("Finding metamodel member for static metamodel field {}.", field);
return getDeclaredIdentifier(field, type)
.orElseGet(() -> getDeclaredAttribute(field, type)
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 05817b7b8..7422dfceb 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
@@ -35,7 +35,7 @@ interface EntityMappingHelper extends ConfigurationHolder {
EntityType getEntityType(Class cls);
- URI generateIdentifier(EntityType> et);
+ Object generateIdentifier(Object entity, 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 4d8e6826e..c7b56a2c2 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
@@ -224,12 +224,12 @@ Object extractFieldValueFromInstance(Object instance) {
}
URI resolveValueIdentifier(E instance, EntityType valEt) {
- URI id = EntityPropertiesUtils.getIdentifier(instance, valEt);
+ Object id = EntityPropertiesUtils.getIdentifier(instance, valEt);
if (id == null) {
- id = mapper.generateIdentifier(valEt);
+ id = mapper.generateIdentifier(instance, valEt);
EntityPropertiesUtils.setIdentifier(id, instance, valEt);
}
- return id;
+ return id instanceof URI uri ? uri : URI.create(id.toString());
}
/**
diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/ObjectOntologyMapper.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/ObjectOntologyMapper.java
index f0851cf31..7b69bdf88 100644
--- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/ObjectOntologyMapper.java
+++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/oom/ObjectOntologyMapper.java
@@ -18,7 +18,6 @@
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.FieldSpecification;
import cz.cvut.kbss.jopa.oom.exception.UnpersistedChangeException;
import cz.cvut.kbss.jopa.sessions.util.AxiomBasedLoadingParameters;
@@ -81,14 +80,6 @@ public interface ObjectOntologyMapper {
*/
void loadFieldValue(T entity, FieldSpecification super T, ?> fieldSpec, Descriptor descriptor);
- /**
- * Generates a fresh identifier for an instance of the specified entity type.
- *
- * @param et Entity type used as base for the identifier
- * @return New entity identifier
- */
- URI generateIdentifier(EntityType> et);
-
/**
* Persists the specified entity into the underlying ontology.
*
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 dbefe25c0..5c79e73cb 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
@@ -252,12 +252,8 @@ public void persistEntity(URI identifier, T entity, Descriptor descriptor) {
}
@Override
- public URI generateIdentifier(EntityType> et) {
- try {
- return storageConnection.generateIdentifier(et.getIRI().toURI());
- } catch (OntoDriverException e) {
- throw new StorageAccessException(e);
- }
+ public Object generateIdentifier(Object entity, EntityType> et) {
+ return uow.getMetamodel().getIdentifierGenerator(et).generate(entity, et, storageConnection);
}
private void persistPendingReferences(T instance, NamedResource identifier) {
@@ -293,23 +289,25 @@ public T getEntityFromCacheOrOntology(Class cls, URI identifier, ObjectGr
if (orig != null) {
return orig;
}
- return defaultInstanceLoader.loadCached(getEntityType(cls), identifier, objectGraphInfo.descriptor()).orElseGet(() -> {
- if (instanceRegistry.containsKey(identifier)) {
- final Object existing = instanceRegistry.get(identifier);
- if (!cls.isAssignableFrom(existing.getClass())) {
- throw individualAlreadyManaged(identifier);
- }
- // This prevents endless cycles in bidirectional relationships
- return cls.cast(existing);
- } else {
- // setup loading params
- LoadingParameters params = initEntityLoadingParameters(cls, identifier, objectGraphInfo);
- return axiomsPerEntity.containsKey(identifier) ? loadEntityFromAxioms(params) : loadEntityInternal(params);
- }
- });
- }
-
- private LoadingParameters initEntityLoadingParameters(Class cls, URI identifier, ObjectGraphInfo objectGraphInfo) {
+ return defaultInstanceLoader.loadCached(getEntityType(cls), identifier, objectGraphInfo.descriptor())
+ .orElseGet(() -> {
+ if (instanceRegistry.containsKey(identifier)) {
+ final Object existing = instanceRegistry.get(identifier);
+ if (!cls.isAssignableFrom(existing.getClass())) {
+ throw individualAlreadyManaged(identifier);
+ }
+ // This prevents endless cycles in bidirectional relationships
+ return cls.cast(existing);
+ } else {
+ // setup loading params
+ LoadingParameters params = initEntityLoadingParameters(cls, identifier, objectGraphInfo);
+ return axiomsPerEntity.containsKey(identifier) ? loadEntityFromAxioms(params) : loadEntityInternal(params);
+ }
+ });
+ }
+
+ private LoadingParameters initEntityLoadingParameters(Class cls, URI identifier,
+ ObjectGraphInfo objectGraphInfo) {
return new LoadingParameters<>(cls, identifier, objectGraphInfo.descriptor(), objectGraphInfo.fetchGraph(), false, uow.isReadOnly());
}
diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/QueryType.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/QueryType.java
index c598766fa..f43acf4c5 100644
--- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/QueryType.java
+++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/QueryType.java
@@ -17,12 +17,14 @@
*/
package cz.cvut.kbss.jopa.query;
+import cz.cvut.kbss.jopa.query.soql.SoqlConstants;
+
/**
* Types of queries.
*/
public enum QueryType {
- SELECT("SELECT"),
- ASK("ASK"),
+ SELECT(SoqlConstants.SELECT),
+ ASK(SoqlConstants.ASK),
DESCRIBE("DESCRIBE"),
DELETE("DELETE"),
INSERT("INSERT"),
diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/criteria/CriteriaBuilderImpl.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/criteria/CriteriaBuilderImpl.java
index a6139c56f..c2ac1b38c 100644
--- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/criteria/CriteriaBuilderImpl.java
+++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/criteria/CriteriaBuilderImpl.java
@@ -29,6 +29,7 @@
import cz.cvut.kbss.jopa.query.criteria.expressions.AbstractExpression;
import cz.cvut.kbss.jopa.query.criteria.expressions.AbstractPathExpression;
import cz.cvut.kbss.jopa.query.criteria.expressions.CeilFunction;
+import cz.cvut.kbss.jopa.query.criteria.expressions.CountDistinctFunction;
import cz.cvut.kbss.jopa.query.criteria.expressions.CountFunction;
import cz.cvut.kbss.jopa.query.criteria.expressions.ExpressionEqualImpl;
import cz.cvut.kbss.jopa.query.criteria.expressions.ExpressionGreaterThanImpl;
@@ -84,6 +85,12 @@ public Expression count(Expression> x) {
return new CountFunction((AbstractPathExpression) x, this);
}
+ @Override
+ public Expression countDistinct(Expression> x) {
+ validateFunctionArgument(x);
+ return new CountDistinctFunction((AbstractPathExpression) x, this);
+ }
+
@Override
public Expression ceil(Expression x) {
validateFunctionArgument(x);
diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/criteria/expressions/AbstractFunctionExpression.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/criteria/expressions/AbstractFunctionExpression.java
index 46d49fcf7..03f9b279f 100644
--- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/criteria/expressions/AbstractFunctionExpression.java
+++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/criteria/expressions/AbstractFunctionExpression.java
@@ -34,13 +34,17 @@ public AbstractFunctionExpression(Class type, CriteriaBuilder cb, AbstractPat
@Override
public void setExpressionToQuery(StringBuilder query, CriteriaParameterFiller parameterFiller) {
query.append(getFunctionName()).append("(");
+ constructFunctionArguments(query, parameterFiller);
+ query.append(")");
+ }
+
+ protected void constructFunctionArguments(StringBuilder query, CriteriaParameterFiller parameterFiller) {
for (int i = 0; i < argumentExpression.size(); i++) {
argumentExpression.get(i).setExpressionToQuery(query, parameterFiller);
if (i < argumentExpression.size() - 1) {
query.append(',');
}
}
- query.append(")");
}
public abstract String getFunctionName();
diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/criteria/expressions/CountDistinctFunction.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/criteria/expressions/CountDistinctFunction.java
new file mode 100644
index 000000000..559f02a69
--- /dev/null
+++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/criteria/expressions/CountDistinctFunction.java
@@ -0,0 +1,17 @@
+package cz.cvut.kbss.jopa.query.criteria.expressions;
+
+import cz.cvut.kbss.jopa.model.query.criteria.CriteriaBuilder;
+import cz.cvut.kbss.jopa.query.criteria.CriteriaParameterFiller;
+
+public class CountDistinctFunction extends CountFunction {
+
+ public CountDistinctFunction(AbstractPathExpression expression, CriteriaBuilder cb) {
+ super(expression, cb);
+ }
+
+ @Override
+ protected void constructFunctionArguments(StringBuilder query, CriteriaParameterFiller parameterFiller) {
+ query.append("DISTINCT ");
+ super.constructFunctionArguments(query, parameterFiller);
+ }
+}
diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/soql/AggregateFunction.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/soql/AggregateFunction.java
new file mode 100644
index 000000000..6a3d349f4
--- /dev/null
+++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/soql/AggregateFunction.java
@@ -0,0 +1,42 @@
+/*
+ * 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.soql;
+
+/**
+ * Aggregate functions supported in SOQL select expressions.
+ */
+enum AggregateFunction {
+
+ COUNT(SoqlConstants.Functions.COUNT, "?count");
+
+ private final String soqlName;
+ private final String resultVariable;
+
+ AggregateFunction(String soqlName, String resultVariable) {
+ this.soqlName = soqlName;
+ this.resultVariable = resultVariable;
+ }
+
+ String soqlName() {
+ return soqlName;
+ }
+
+ String resultVariable() {
+ return resultVariable;
+ }
+}
diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/soql/SelectProjection.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/soql/SelectProjection.java
new file mode 100644
index 000000000..4a041be28
--- /dev/null
+++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/soql/SelectProjection.java
@@ -0,0 +1,24 @@
+package cz.cvut.kbss.jopa.query.soql;
+
+/**
+ * Captures the kind of projection in the SOQL SELECT clause.
+ *
+ * @param aggregateFunction the aggregate function wrapping the projected variable, or {@code null} for a plain
+ * projection
+ * @param distinct whether the outer {@code SELECT DISTINCT} modifier is present
+ * @param aggregateDistinct whether {@code DISTINCT} is present inside the aggregate argument
+ */
+record SelectProjection(AggregateFunction aggregateFunction, boolean distinct, boolean aggregateDistinct) {
+
+ SelectProjection withAggregateFunction(AggregateFunction fn) {
+ return new SelectProjection(fn, distinct, aggregateDistinct);
+ }
+
+ SelectProjection withDistinct(boolean value) {
+ return new SelectProjection(aggregateFunction, value, aggregateDistinct);
+ }
+
+ SelectProjection withAggregateDistinct(boolean value) {
+ return new SelectProjection(aggregateFunction, distinct, value);
+ }
+}
diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/soql/SoqlConstants.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/soql/SoqlConstants.java
index ab65a410c..aa377a149 100644
--- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/soql/SoqlConstants.java
+++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/soql/SoqlConstants.java
@@ -27,6 +27,11 @@ public class SoqlConstants {
*/
public static final String SELECT = "SELECT";
+ /**
+ * {@code ASK} keyword.
+ */
+ public static final String ASK = "ASK";
+
/**
* {@code DISTINCT} operator.
*/
diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/soql/SoqlQueryListener.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/soql/SoqlQueryListener.java
index f7a5dd759..5e26f35a6 100644
--- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/soql/SoqlQueryListener.java
+++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/query/soql/SoqlQueryListener.java
@@ -57,7 +57,7 @@ public class SoqlQueryListener extends SoqlBaseListener {
private String soql;
private String sparql;
- private String typeDef = SoqlConstants.SELECT;
+ private QueryType typeDef = QueryType.SELECT;
// keeps pointer at created object of SoqlAttribute while processing other necessary rules
private SoqlAttribute attrPointer;
@@ -78,9 +78,7 @@ public class SoqlQueryListener extends SoqlBaseListener {
private final Map, TriplePatternEnhancer> tpEnhancers = new HashMap<>();
- private boolean isSelectedParamDistinct = false;
-
- private boolean isSelectedParamCount = false;
+ private SelectProjection selectProjection = new SelectProjection(null, false, false);
private boolean isInObjectIdentifierExpression = false;
@@ -153,11 +151,14 @@ private SoqlNode linkSimpleSubpath(ParserRuleContext ctx) {
@Override
public void enterSelectClause(SoqlParser.SelectClauseContext ctx) {
- this.typeDef = QueryType.SELECT.getKeyword();
+ this.typeDef = QueryType.SELECT;
- if (ctx.DISTINCT() != null) {
- this.isSelectedParamDistinct = true;
- }
+ this.selectProjection = selectProjection.withDistinct(ctx.DISTINCT() != null);
+ }
+
+ @Override
+ public void enterAskStatement(SoqlParser.AskStatementContext ctx) {
+ this.typeDef = QueryType.ASK;
}
private void pushNewAttribute(SoqlAttribute myAttr) {
@@ -175,7 +176,7 @@ private void popAttribute() {
@Override
public void exitSelectExpression(SoqlParser.SelectExpressionContext ctx) {
- if (!isSelectedParamCount) {
+ if (selectProjection.aggregateFunction() == null) {
this.projectedVariable = ctx.getText();
}
}
@@ -183,10 +184,8 @@ public void exitSelectExpression(SoqlParser.SelectExpressionContext ctx) {
@Override
public void enterAggregateExpression(SoqlParser.AggregateExpressionContext ctx) {
if (ctx.COUNT() != null) {
- isSelectedParamCount = true;
- if (ctx.DISTINCT() != null) {
- isSelectedParamDistinct = true;
- }
+ selectProjection = selectProjection.withAggregateFunction(AggregateFunction.COUNT)
+ .withAggregateDistinct(ctx.DISTINCT() != null);
if (ctx.simpleSubpath() != null) {
this.projectedVariable = ctx.simpleSubpath().getText();
@@ -546,19 +545,11 @@ private void buildSparqlQueryString() {
return;
}
- // the first attribute is either a projected parameter or a type attribute
- String selectParameter = getSelectParameter(attributes.get(0));
-
- StringBuilder newQueryBuilder = new StringBuilder(typeDef);
- if (isSelectedParamCount) {
- newQueryBuilder.append(getCountPart(selectParameter));
- } else {
- if (isSelectedParamDistinct) {
- newQueryBuilder.append(' ').append(SoqlConstants.DISTINCT);
- }
- newQueryBuilder.append(' ').append(selectParameter).append(' ');
+ StringBuilder newQueryBuilder = new StringBuilder(typeDef.getKeyword());
+ if (typeDef == QueryType.SELECT) {
+ appendProjection(newQueryBuilder);
}
- newQueryBuilder.append("WHERE { ");
+ newQueryBuilder.append(" WHERE { ");
newQueryBuilder.append(processSupremeAttributes());
if (!objectOfNextOr.isEmpty()) {
newQueryBuilder.append("{ ");
@@ -578,13 +569,30 @@ private void buildSparqlQueryString() {
LOG.trace("Translated SOQL query '{}' to SPARQL '{}'.", soql, sparql);
}
- private StringBuilder getCountPart(String selectParameter) {
- StringBuilder countPart = new StringBuilder(" (COUNT(");
- if (isSelectedParamDistinct) {
- countPart.append(SoqlConstants.DISTINCT).append(' ');
+ private void appendProjection(StringBuilder sparqlBuilder) {
+ // the first attribute is either a projected parameter or a type attribute
+ final String selectParameter = getSelectParameter(attributes.get(0));
+ if (selectProjection.aggregateFunction() != null) {
+ if (selectProjection.distinct()) {
+ sparqlBuilder.append(' ').append(SoqlConstants.DISTINCT);
+ }
+ sparqlBuilder.append(getAggregatePart(selectParameter));
+ } else {
+ if (selectProjection.distinct()) {
+ sparqlBuilder.append(' ').append(SoqlConstants.DISTINCT);
+ }
+ sparqlBuilder.append(' ').append(selectParameter);
+ }
+ }
+
+ private StringBuilder getAggregatePart(String selectParameter) {
+ final AggregateFunction fn = selectProjection.aggregateFunction();
+ final StringBuilder aggregatePart = new StringBuilder(" (").append(fn.soqlName()).append('(');
+ if (selectProjection.aggregateDistinct()) {
+ aggregatePart.append(SoqlConstants.DISTINCT).append(' ');
}
- countPart.append(selectParameter).append(") AS ?count) ");
- return countPart;
+ aggregatePart.append(selectParameter).append(") AS ").append(fn.resultVariable()).append(")");
+ return aggregatePart;
}
private StringBuilder processSupremeAttributes() {
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 604a63da1..fcda18984 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
@@ -143,7 +143,7 @@ private boolean limitOrOffsetNotSet() {
* @param descriptor Descriptor to examine
* @return {@code true} if at most one context is used, {@code false} otherwise
*/
- private boolean descriptorSpecifiesAtMostOneContext(Descriptor descriptor) {
+ private static boolean descriptorSpecifiesAtMostOneContext(Descriptor descriptor) {
return descriptor.getContexts().size() <= 1 && descriptor.getAttributeDescriptors().stream()
.allMatch(d -> d.getContexts().size() <= 1);
}
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 d6a7e6045..76b543dd0 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
@@ -738,7 +738,7 @@ private Object initEntityIdentifier(Object entity, EntityType