Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f5c3529
[Upd] Update JaCoCo maven plugin.
ledsoft Jun 30, 2026
d0076f2
[Enhancement #448] Minor refactor of SOQL parsing to clarify count an…
ledsoft Jul 2, 2026
a724800
[Enhancement #448] Implement support for countDistinct in Criteria API.
ledsoft Jul 3, 2026
fff721c
[Fix] Disable flaky test on OWLAPI driver.
ledsoft Jul 3, 2026
9d93492
[Fix] Fix unknown Javadoc tag error.
ledsoft Jul 3, 2026
033a1ba
[Enhancement #451] Extend SOQL grammar with support for ASK queries.
ledsoft Jul 10, 2026
910b615
[Enhancement #451] Implement support for ASK queries in SOQL.
ledsoft Jul 10, 2026
bb5a268
[Enhancement #451] Implement support for ASK queries in Criteria API.
ledsoft Jul 10, 2026
fd4c91b
[Enhancement #451] Add an integration test for the feature.
ledsoft Jul 10, 2026
5bc7713
[Enhancement #373] Add IdentifierGenerator interface to jopa-api.
ledsoft Jul 10, 2026
31fc6c3
[Enhancement #373] Use identifier generator in JOPA instead of callin…
ledsoft Jul 10, 2026
1b8b952
[Enhancement #373] Remove identifier generation code from OntoDrivers.
ledsoft Jul 10, 2026
2daf37d
[Enhancement #373] Define annotation for specifying custom id generator.
ledsoft Jul 10, 2026
23f4792
[Enhancement #373] Discover an initialize custom id generators in ent…
ledsoft Jul 13, 2026
265fd0f
[Enhancement #373] Add an integration test for custom id generators.
ledsoft Jul 13, 2026
47d3a2b
[SCA] Minor SCA-based code improvements.
ledsoft Jul 13, 2026
6450e86
[2.11.0] Bump version, update changelog.
ledsoft Jul 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# JOPA - Change Log

### 2.11.0 - 2026-07-13

- Add support for `countDistinct` in Criteria API, properly implement COUNT DISTINCT in SOQL (Enhancement #448).
- Add support for ASK queries in SOQL/Criteria API (Enhancement #451).
- Add support for custom identifier generators (Enhancement #373).

### 2.10.1 - 2026-06-30

- Allow ordering by entity identifier in SOQL (Bug #439).
Expand Down
2 changes: 1 addition & 1 deletion datatype/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<artifactId>jopa-all</artifactId>
<groupId>cz.cvut.kbss.jopa</groupId>
<version>2.10.1</version>
<version>2.11.0</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion jopa-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>cz.cvut.kbss.jopa</groupId>
<artifactId>jopa-all</artifactId>
<version>2.10.1</version>
<version>2.11.0</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package cz.cvut.kbss.jopa.id;

import cz.cvut.kbss.jopa.model.metamodel.EntityType;
import cz.cvut.kbss.ontodriver.Connection;

/**
* Generates identifiers for entities.
*/
public interface IdentifierGenerator {

/**
* Generate identifier for the specified entity.
*
* @param entity Entity for which to generate the identifier
* @param entityClass Entity class
* @param connection Storage connection
* @return Generated identifier
*/
<T> Object generate(Object entity, EntityType<T> entityClass, Connection connection);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package cz.cvut.kbss.jopa.model.annotations;

import cz.cvut.kbss.jopa.id.IdentifierGenerator;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Specifies the identifier generator to use for the entity.
*/
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IdGenerator {

/**
* The identifier generator class.
*/
Class<? extends IdentifierGenerator> value();
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,23 @@ public interface CriteriaBuilder extends PredicateFactory {
<N extends Number> Expression<N> floor(Expression<N> x);

/**
* Create an aggregate expression applying the count operation. Return type of count function in SPARQL is
* xsd:integer which JOPA internally represents as Integer.
* Create an aggregate expression applying the count operation.
*
* @param x expression representing input value to count operation
* @return count expression
* @implNote Return type of count function in SPARQL is xsd:integer which JOPA internally represents as Integer.
*/
Expression<Integer> count(Expression<?> x);

/**
* Create an aggregate expression applying the count distinct operation.
*
* @param x expression representing input value to count operation
* @return count distinct expression
* @implNote Return type of count function in SPARQL is xsd:integer which JOPA internally represents as Integer.
*/
Expression<Integer> countDistinct(Expression<?> x);

/**
* Create expression to return length of a string.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ public interface CriteriaQuery<T> {
*/
CriteriaQuery<T> select(Selection<? extends T> selection);

/**
* Specify that the query is an ask query.
*
* @return the modified query
*/
CriteriaQuery<Boolean> ask();

/**
* Modify the query to restrict the query result according to the specified boolean expression. Replaces the
* previously added restriction(s), if any.
Expand Down
2 changes: 1 addition & 1 deletion jopa-distribution/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>cz.cvut.kbss.jopa</groupId>
<artifactId>jopa-all</artifactId>
<version>2.10.1</version>
<version>2.11.0</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion jopa-impl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>cz.cvut.kbss.jopa</groupId>
<artifactId>jopa-all</artifactId>
<version>2.10.1</version>
<version>2.11.0</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)* ;
Expand Down Expand Up @@ -152,6 +154,8 @@ comparisonOperator

SELECT: 'SELECT' ;

ASK: 'ASK' ;

WHERE: 'WHERE' ;

NOT: 'NOT' ;
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <T> URI generate(Object entity, EntityType<T> 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());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -45,6 +46,8 @@ public class CriteriaQueryImpl<T> implements CriteriaQuery<T> {
private final Metamodel metamodel;
private final CriteriaBuilderImpl cb;

private QueryType queryType = QueryType.SELECT;


public CriteriaQueryImpl(CriteriaQueryHolder<T> query, Metamodel metamodel, CriteriaBuilderImpl cb) {
this.query = Objects.requireNonNull(query);
Expand All @@ -61,17 +64,26 @@ public <X> Root<X> from(Class<X> entityClass) {

@Override
public <X> Root<X> from(EntityType<X> entity) {
RootImpl<X> root = new RootImpl<>(metamodel, null, entity.getBindableJavaType(), this.cb);
RootImpl<X> root = new RootImpl<>(metamodel, null, entity.getJavaType(), this.cb);
query.setRoot(root);
return root;
}

@Override
public CriteriaQuery<T> select(Selection<? extends T> selection) {
query.setSelection(selection);
this.queryType = QueryType.SELECT;
return this;
}

@Override
public CriteriaQuery<Boolean> ask() {
this.queryType = QueryType.ASK;
query.setSelection(null);
assert Boolean.class.equals(query.getResultType());
return (CriteriaQuery<Boolean>) this;
}

@Override
public CriteriaQuery<T> where(Expression<Boolean> expression) {
query.setWhere(cb.wrapExpressionToPredicateWithRepair(expression));
Expand Down Expand Up @@ -188,44 +200,69 @@ public CriteriaQuery<T> 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<Order> 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();
}
}
Loading
Loading