From 710866c1617e44915cb80fce150204b7717d1f4b Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Fri, 10 Jul 2026 14:43:24 +0200 Subject: [PATCH 1/6] [Enhancement #373] Add IdentifierGenerator interface to jopa-api. Replicate implementation from all drivers into RandomNumberIdentifierGenerator. --- .../kbss/jopa/id/IdentifierGenerator.java | 20 +++ .../jopa/id/AbstractIdentifierGenerator.java | 53 +++++++ .../id/RandomNumberIdentifierGenerator.java | 46 ++++++ .../RandomNumberIdentifierGeneratorTest.java | 139 ++++++++++++++++++ .../IdentifierGenerationException.java | 4 + 5 files changed, 262 insertions(+) create mode 100644 jopa-api/src/main/java/cz/cvut/kbss/jopa/id/IdentifierGenerator.java create mode 100644 jopa-impl/src/main/java/cz/cvut/kbss/jopa/id/AbstractIdentifierGenerator.java create mode 100644 jopa-impl/src/main/java/cz/cvut/kbss/jopa/id/RandomNumberIdentifierGenerator.java create mode 100644 jopa-impl/src/test/java/cz/cvut/kbss/jopa/id/RandomNumberIdentifierGeneratorTest.java diff --git a/jopa-api/src/main/java/cz/cvut/kbss/jopa/id/IdentifierGenerator.java b/jopa-api/src/main/java/cz/cvut/kbss/jopa/id/IdentifierGenerator.java new file mode 100644 index 000000000..65706614b --- /dev/null +++ b/jopa-api/src/main/java/cz/cvut/kbss/jopa/id/IdentifierGenerator.java @@ -0,0 +1,20 @@ +package cz.cvut.kbss.jopa.id; + +import cz.cvut.kbss.jopa.model.metamodel.Metamodel; +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 metamodel Entity class metamodel + * @param connection Storage connection + * @return Generated identifier + */ + Object generate(Object entity, Metamodel metamodel, Connection connection); +} 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..0305f4b89 --- /dev/null +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/id/AbstractIdentifierGenerator.java @@ -0,0 +1,53 @@ +package cz.cvut.kbss.jopa.id; + +import cz.cvut.kbss.jopa.model.metamodel.EntityType; +import cz.cvut.kbss.jopa.model.metamodel.Metamodel; +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 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); + } + } + + /** + * Resolves the identifier of the ontological class of the specified entity. + * + * @param entity Entity whose class IRI to resolve + * @param metamodel Metamodel + * @return Class IRI + */ + protected URI getEntityClassUri(Object entity, Metamodel metamodel) { + final EntityType et = metamodel.entity(entity.getClass()); + return et.getIRI().toURI(); + } +} 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..b695dcc87 --- /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.Metamodel; +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, Metamodel metamodel, Connection connection) { + Objects.requireNonNull(entity); + Objects.requireNonNull(metamodel); + Objects.requireNonNull(connection); + + final URI classUri = getEntityClassUri(entity, metamodel); + 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/test/java/cz/cvut/kbss/jopa/id/RandomNumberIdentifierGeneratorTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/id/RandomNumberIdentifierGeneratorTest.java new file mode 100644 index 000000000..353138195 --- /dev/null +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/id/RandomNumberIdentifierGeneratorTest.java @@ -0,0 +1,139 @@ +package cz.cvut.kbss.jopa.id; + +import cz.cvut.kbss.jopa.model.IRI; +import cz.cvut.kbss.jopa.model.metamodel.EntityType; +import cz.cvut.kbss.jopa.model.metamodel.Metamodel; +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.Axiom; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +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.regex.Pattern; + +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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class RandomNumberIdentifierGeneratorTest { + + private static final URI CLASS_URI = URI.create("http://example.org/entities#TestEntity"); + + @Mock + private Metamodel metamodel; + + @Mock + private Connection connection; + + private final RandomNumberIdentifierGenerator sut = new RandomNumberIdentifierGenerator(); + + @BeforeEach + void setUp() { + reset(metamodel, connection); + } + + private void configureMetamodel(URI classUri) { + final EntityType entityType = mock(EntityType.class); + when(entityType.getIRI()).thenReturn(IRI.create(classUri.toString())); + when(metamodel.entity(TestEntity.class)).thenReturn(entityType); + } + + @Test + void generateReturnsUniqueIdentifierWhenItDoesNotExist() throws Exception { + configureMetamodel(CLASS_URI); + when(connection.contains(any(Axiom.class), any())).thenReturn(false); + + final URI result = sut.generate(new TestEntity(), metamodel, connection); + + assertNotNull(result); + assertTrue(result.toString().startsWith(CLASS_URI.toString())); + final ArgumentCaptor captor = ArgumentCaptor.forClass(Axiom.class); + verify(connection).contains(captor.capture(), any()); + assertEquals(CLASS_URI, captor.getValue().getValue().getValue()); + assertTrue(captor.getValue().getAssertion().isClassAssertion()); + assertFalse(captor.getValue().getAssertion().isInferred()); + } + + @Test + void generateThrowsIdentifierGenerationExceptionWhenAllGeneratedIdentifiersExist() throws Exception { + configureMetamodel(CLASS_URI); + when(connection.contains(any(Axiom.class), any())).thenReturn(true); + + final IdentifierGenerationException ex = assertThrows(IdentifierGenerationException.class, + () -> sut.generate(new TestEntity(), metamodel, connection)); + assertEquals("Unable to generate a unique identifier.", ex.getMessage()); + verify(connection, times(64)).contains(any(Axiom.class), any()); + } + + @Test + void generateRetriesUntilItFindsUniqueIdentifier() throws Exception { + configureMetamodel(CLASS_URI); + when(connection.contains(any(Axiom.class), any())).thenReturn(true, true, false); + + final URI result = sut.generate(new TestEntity(), metamodel, connection); + + assertNotNull(result); + verify(connection, times(3)).contains(any(Axiom.class), any()); + } + + @Test + void generateThrowsIdentifierGenerationExceptionWhenConnectionFails() throws Exception { + configureMetamodel(CLASS_URI); + when(connection.contains(any(Axiom.class), any())).thenThrow(new OntoDriverException("Storage error")); + + final IdentifierGenerationException ex = assertThrows(IdentifierGenerationException.class, + () -> sut.generate(new TestEntity(), metamodel, connection)); + assertTrue(ex.getMessage().contains("Unable to check if identifier")); + assertEquals(OntoDriverException.class, ex.getCause().getClass()); + } + + @ParameterizedTest + @ValueSource(strings = { + "http://example.org/Entity#Test", + "http://example.org/Entity/", + "http://example.org/Entity" + }) + void generateBuildsUriAccordingToClassIriFormat(String classUriString) throws Exception { + final URI classUri = URI.create(classUriString); + configureMetamodel(classUri); + when(connection.contains(any(Axiom.class), any())).thenReturn(false); + + final URI result = sut.generate(new TestEntity(), metamodel, connection); + + final String prefix = expectedPrefix(classUri); + assertTrue(result.toString().matches(Pattern.quote(prefix) + "-?\\d+")); + } + + private static String expectedPrefix(URI classUri) { + if (classUri.getFragment() != null) { + return classUri + "_instance"; + } else if (classUri.toString().endsWith("/")) { + return classUri + "instance"; + } else { + return classUri + "/instance"; + } + } + + private static class TestEntity { + } +} diff --git a/ontodriver-api/src/main/java/cz/cvut/kbss/ontodriver/exception/IdentifierGenerationException.java b/ontodriver-api/src/main/java/cz/cvut/kbss/ontodriver/exception/IdentifierGenerationException.java index 9518de5e7..46d94f991 100644 --- a/ontodriver-api/src/main/java/cz/cvut/kbss/ontodriver/exception/IdentifierGenerationException.java +++ b/ontodriver-api/src/main/java/cz/cvut/kbss/ontodriver/exception/IdentifierGenerationException.java @@ -22,4 +22,8 @@ public class IdentifierGenerationException extends OntoDriverRuntimeException { public IdentifierGenerationException(String message) { super(message); } + + public IdentifierGenerationException(String message, Throwable cause) { + super(message, cause); + } } From de4ed38963cdbcb0cded5b2190998f27fcd19447 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Fri, 10 Jul 2026 17:03:05 +0200 Subject: [PATCH 2/6] [Enhancement #373] Use identifier generator in JOPA instead of calling OntoDriver. --- .../kbss/jopa/id/IdentifierGenerator.java | 10 ++--- .../jopa/id/AbstractIdentifierGenerator.java | 16 +------ .../id/RandomNumberIdentifierGenerator.java | 8 ++-- .../cvut/kbss/jopa/model/MetamodelImpl.java | 9 ++++ .../kbss/jopa/oom/EntityMappingHelper.java | 2 +- .../cz/cvut/kbss/jopa/oom/FieldStrategy.java | 6 +-- .../kbss/jopa/oom/ObjectOntologyMapper.java | 7 +-- .../jopa/oom/ObjectOntologyMapperImpl.java | 44 +++++++++---------- .../jopa/sessions/AbstractUnitOfWork.java | 2 +- .../kbss/jopa/sessions/ConnectionWrapper.java | 9 ++-- .../feature/EntityLifecycleListenersTest.java | 6 +-- .../RandomNumberIdentifierGeneratorTest.java | 41 +++++++---------- .../sessions/ReadWriteUnitOfWorkTest.java | 10 ++++- 13 files changed, 78 insertions(+), 92 deletions(-) diff --git a/jopa-api/src/main/java/cz/cvut/kbss/jopa/id/IdentifierGenerator.java b/jopa-api/src/main/java/cz/cvut/kbss/jopa/id/IdentifierGenerator.java index 65706614b..5e3db2929 100644 --- a/jopa-api/src/main/java/cz/cvut/kbss/jopa/id/IdentifierGenerator.java +++ b/jopa-api/src/main/java/cz/cvut/kbss/jopa/id/IdentifierGenerator.java @@ -1,6 +1,6 @@ package cz.cvut.kbss.jopa.id; -import cz.cvut.kbss.jopa.model.metamodel.Metamodel; +import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.ontodriver.Connection; /** @@ -11,10 +11,10 @@ public interface IdentifierGenerator { /** * Generate identifier for the specified entity. * - * @param entity Entity for which to generate the identifier - * @param metamodel Entity class metamodel - * @param connection Storage connection + * @param entity Entity for which to generate the identifier + * @param entityClass Entity class + * @param connection Storage connection * @return Generated identifier */ - Object generate(Object entity, Metamodel metamodel, Connection connection); + Object generate(Object entity, EntityType entityClass, Connection connection); } 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 index 0305f4b89..19faa6885 100644 --- 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 @@ -1,7 +1,5 @@ package cz.cvut.kbss.jopa.id; -import cz.cvut.kbss.jopa.model.metamodel.EntityType; -import cz.cvut.kbss.jopa.model.metamodel.Metamodel; import cz.cvut.kbss.ontodriver.Connection; import cz.cvut.kbss.ontodriver.exception.IdentifierGenerationException; import cz.cvut.kbss.ontodriver.exception.OntoDriverException; @@ -14,7 +12,7 @@ import java.util.Set; /** - * Common utilities for identifier generation. + * Common superclass with utilities for identifier generation. */ public abstract class AbstractIdentifierGenerator implements IdentifierGenerator { @@ -38,16 +36,4 @@ protected boolean exists(URI identifier, URI classIri, Connection connection) { throw new IdentifierGenerationException("Unable to check if identifier '" + identifier + "' exists.", e); } } - - /** - * Resolves the identifier of the ontological class of the specified entity. - * - * @param entity Entity whose class IRI to resolve - * @param metamodel Metamodel - * @return Class IRI - */ - protected URI getEntityClassUri(Object entity, Metamodel metamodel) { - final EntityType et = metamodel.entity(entity.getClass()); - return et.getIRI().toURI(); - } } 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 index b695dcc87..5f621428d 100644 --- 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 @@ -1,6 +1,6 @@ package cz.cvut.kbss.jopa.id; -import cz.cvut.kbss.jopa.model.metamodel.Metamodel; +import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.ontodriver.Connection; import cz.cvut.kbss.ontodriver.exception.IdentifierGenerationException; @@ -15,12 +15,12 @@ public class RandomNumberIdentifierGenerator extends AbstractIdentifierGenerator private static final Random RANDOM = new Random(); @Override - public URI generate(Object entity, Metamodel metamodel, Connection connection) { + public URI generate(Object entity, EntityType entityClass, Connection connection) { Objects.requireNonNull(entity); - Objects.requireNonNull(metamodel); + Objects.requireNonNull(entityClass); Objects.requireNonNull(connection); - final URI classUri = getEntityClassUri(entity, metamodel); + final URI classUri = entityClass.getIRI().toURI(); int counter = 0; while (counter++ < GENERATION_THRESHOLD) { final URI id = generate(classUri); 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..03974867a 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; @@ -40,6 +42,7 @@ import java.net.URI; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; @@ -61,6 +64,8 @@ public class MetamodelImpl implements Metamodel, MetamodelProvider, MetamodelCla // Proxy classes for results of EntityManager.getReference private final Map, Class> referenceProxyClasses = new ConcurrentHashMap<>(); + private final Map, IdentifierGenerator> idGenerators = new HashMap<>(); + private NamedQueryManager namedQueryManager; private ResultSetMappingManager resultSetMappingManager; private NamedEntityGraphManager namedEntityGraphManager; @@ -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/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..db49843cc 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 @@ -82,12 +82,13 @@ public interface ObjectOntologyMapper { void loadFieldValue(T entity, FieldSpecification fieldSpec, Descriptor descriptor); /** - * Generates a fresh identifier for an instance of the specified entity type. + * Generates a fresh identifier for the specified entity. * - * @param et Entity type used as base for the identifier + * @param entity Entity for which to generate the identifier + * @param et Entity type * @return New entity identifier */ - URI generateIdentifier(EntityType et); + Object generateIdentifier(Object entity, 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/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 et) { Object id = getIdentifier(entity); if (id == null) { EntityPropertiesUtils.verifyIdentifierIsGenerated(entity, et); - id = storage.generateIdentifier(et); + id = getMetamodel().getIdentifierGenerator(et).generate(entity, et, storage.getConnection()); EntityPropertiesUtils.setIdentifier(id, entity, et); } return id; diff --git a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/ConnectionWrapper.java b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/ConnectionWrapper.java index 8211234b5..09595d494 100644 --- a/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/ConnectionWrapper.java +++ b/jopa-impl/src/main/java/cz/cvut/kbss/jopa/sessions/ConnectionWrapper.java @@ -19,7 +19,6 @@ import cz.cvut.kbss.jopa.exceptions.OWLPersistenceException; 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.ObjectOntologyMapper; import cz.cvut.kbss.jopa.oom.ObjectOntologyMapperImpl; @@ -76,10 +75,6 @@ public void merge(T entity, FieldSpecification fieldSpec, Desc mapper.updateFieldValue(entity, fieldSpec, descriptor); } - public URI generateIdentifier(EntityType et) { - return mapper.generateIdentifier(et); - } - public void persist(Object identifier, T entity, Descriptor descriptor) { final URI idUri = getIdentifierAsUri(identifier); mapper.persistEntity(idUri, entity, descriptor); @@ -165,6 +160,10 @@ public RepositoryMetadata getRepositoryMetadata() { return connection.getRepositoryMetadata(); } + public Connection getConnection() { + return connection; + } + @Override public T unwrap(Class cls) { try { diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/feature/EntityLifecycleListenersTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/feature/EntityLifecycleListenersTest.java index f817959c9..a8478f2d2 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/feature/EntityLifecycleListenersTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/feature/EntityLifecycleListenersTest.java @@ -123,8 +123,7 @@ public void prePersistLifecycleListenerIsCalledBeforeInstanceIsInsertedIntoPersi final Map mockMap = spy(new HashMap<>()); TestEnvironmentUtils.setMock(uow, AbstractUnitOfWork.class.getDeclaredField("newObjectsKeyToClone"), mockMap); final URI rId = Generators.createIndividualIdentifier(); - final OWLClassR rInstance = spy(new OWLClassR()); - when(storageMock.generateIdentifier(metamodelMock.entity(OWLClassR.class))).thenReturn(rId); + final OWLClassR rInstance = spy(new OWLClassR(rId)); uow.registerNewObject(rInstance, descriptor); final InOrder inOrder = inOrder(rInstance, parentListenerMock, concreteListenerMock, anotherListenerMock, storageMock, mockMap); @@ -156,9 +155,8 @@ private T createInstance(Class cls) { @Test public void postPersistEntityLifecycleListenerIsCalledAfterInstanceIsInsertedIntoStorage() { final URI rId = Generators.createIndividualIdentifier(); - final OWLClassR rInstance = spy(new OWLClassR()); + final OWLClassR rInstance = spy(new OWLClassR(rId)); when(cloneBuilderMock.buildClone(eq(rInstance), any(CloneConfiguration.class))).thenReturn(rInstance); - when(storageMock.generateIdentifier(metamodelMock.entity(OWLClassR.class))).thenReturn(rId); uow.registerNewObject(rInstance, descriptor); uow.commit(); final InOrder inOrder = inOrder(rInstance, concreteListenerMock, storageMock); diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/id/RandomNumberIdentifierGeneratorTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/id/RandomNumberIdentifierGeneratorTest.java index 353138195..2bf2260cc 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/id/RandomNumberIdentifierGeneratorTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/id/RandomNumberIdentifierGeneratorTest.java @@ -2,21 +2,19 @@ import cz.cvut.kbss.jopa.model.IRI; import cz.cvut.kbss.jopa.model.metamodel.EntityType; -import cz.cvut.kbss.jopa.model.metamodel.Metamodel; 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.Axiom; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; 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.regex.Pattern; @@ -28,42 +26,33 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) +@Execution(ExecutionMode.CONCURRENT) class RandomNumberIdentifierGeneratorTest { private static final URI CLASS_URI = URI.create("http://example.org/entities#TestEntity"); - @Mock - private Metamodel metamodel; - @Mock private Connection connection; private final RandomNumberIdentifierGenerator sut = new RandomNumberIdentifierGenerator(); - @BeforeEach - void setUp() { - reset(metamodel, connection); - } - - private void configureMetamodel(URI classUri) { + private EntityType configureMetamodel(URI classUri) { final EntityType entityType = mock(EntityType.class); when(entityType.getIRI()).thenReturn(IRI.create(classUri.toString())); - when(metamodel.entity(TestEntity.class)).thenReturn(entityType); + return entityType; } @Test void generateReturnsUniqueIdentifierWhenItDoesNotExist() throws Exception { - configureMetamodel(CLASS_URI); + final EntityType entityType = configureMetamodel(CLASS_URI); when(connection.contains(any(Axiom.class), any())).thenReturn(false); - final URI result = sut.generate(new TestEntity(), metamodel, connection); + final URI result = sut.generate(new TestEntity(), entityType, connection); assertNotNull(result); assertTrue(result.toString().startsWith(CLASS_URI.toString())); @@ -76,21 +65,21 @@ void generateReturnsUniqueIdentifierWhenItDoesNotExist() throws Exception { @Test void generateThrowsIdentifierGenerationExceptionWhenAllGeneratedIdentifiersExist() throws Exception { - configureMetamodel(CLASS_URI); + final EntityType entityType = configureMetamodel(CLASS_URI); when(connection.contains(any(Axiom.class), any())).thenReturn(true); final IdentifierGenerationException ex = assertThrows(IdentifierGenerationException.class, - () -> sut.generate(new TestEntity(), metamodel, connection)); + () -> sut.generate(new TestEntity(), entityType, connection)); assertEquals("Unable to generate a unique identifier.", ex.getMessage()); verify(connection, times(64)).contains(any(Axiom.class), any()); } @Test void generateRetriesUntilItFindsUniqueIdentifier() throws Exception { - configureMetamodel(CLASS_URI); + final EntityType entityType = configureMetamodel(CLASS_URI); when(connection.contains(any(Axiom.class), any())).thenReturn(true, true, false); - final URI result = sut.generate(new TestEntity(), metamodel, connection); + final URI result = sut.generate(new TestEntity(), entityType, connection); assertNotNull(result); verify(connection, times(3)).contains(any(Axiom.class), any()); @@ -98,11 +87,11 @@ void generateRetriesUntilItFindsUniqueIdentifier() throws Exception { @Test void generateThrowsIdentifierGenerationExceptionWhenConnectionFails() throws Exception { - configureMetamodel(CLASS_URI); + final EntityType entityType = configureMetamodel(CLASS_URI); when(connection.contains(any(Axiom.class), any())).thenThrow(new OntoDriverException("Storage error")); final IdentifierGenerationException ex = assertThrows(IdentifierGenerationException.class, - () -> sut.generate(new TestEntity(), metamodel, connection)); + () -> sut.generate(new TestEntity(), entityType, connection)); assertTrue(ex.getMessage().contains("Unable to check if identifier")); assertEquals(OntoDriverException.class, ex.getCause().getClass()); } @@ -115,10 +104,10 @@ void generateThrowsIdentifierGenerationExceptionWhenConnectionFails() throws Exc }) void generateBuildsUriAccordingToClassIriFormat(String classUriString) throws Exception { final URI classUri = URI.create(classUriString); - configureMetamodel(classUri); + final EntityType entityType = configureMetamodel(classUri); when(connection.contains(any(Axiom.class), any())).thenReturn(false); - final URI result = sut.generate(new TestEntity(), metamodel, connection); + final URI result = sut.generate(new TestEntity(), entityType, connection); final String prefix = expectedPrefix(classUri); assertTrue(result.toString().matches(Pattern.quote(prefix) + "-?\\d+")); diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/ReadWriteUnitOfWorkTest.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/ReadWriteUnitOfWorkTest.java index bb7aa61c0..b17e31caf 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/ReadWriteUnitOfWorkTest.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/sessions/ReadWriteUnitOfWorkTest.java @@ -29,6 +29,7 @@ import cz.cvut.kbss.jopa.exception.IdentifierNotSetException; import cz.cvut.kbss.jopa.exceptions.CardinalityConstraintViolatedException; import cz.cvut.kbss.jopa.exceptions.EntityNotFoundException; +import cz.cvut.kbss.jopa.id.IdentifierGenerator; import cz.cvut.kbss.jopa.model.EntityState; import cz.cvut.kbss.jopa.model.LoadState; import cz.cvut.kbss.jopa.model.MultilingualString; @@ -44,6 +45,7 @@ import cz.cvut.kbss.jopa.sessions.util.CloneConfiguration; import cz.cvut.kbss.jopa.sessions.util.CloneRegistrationDescriptor; import cz.cvut.kbss.jopa.sessions.util.LoadingParameters; +import cz.cvut.kbss.ontodriver.Connection; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -157,10 +159,14 @@ void registerNewObjectGeneratesIdentifierWhenInstancesDoesNotHaveOne() { final OWLClassE entity = new OWLClassE(); entity.setStringAttribute("Test value"); final URI id = Generators.createIndividualIdentifier(); - when(storageMock.generateIdentifier(any(EntityType.class))).thenReturn(id); + final IdentifierGenerator idGenerator = mock(IdentifierGenerator.class); + when(idGenerator.generate(eq(entity), any(), any())).thenReturn(id); + final Connection conn = mock(Connection.class); + when(storageMock.getConnection()).thenReturn(conn); + when(metamodelMock.getIdentifierGenerator(any(EntityType.class))).thenReturn(idGenerator); uow.registerNewObject(entity, descriptor); assertEquals(id, entity.getUri()); - verify(storageMock).generateIdentifier(metamodelMocks.forOwlClassE().entityType()); + verify(idGenerator).generate(entity, metamodelMocks.forOwlClassE().entityType(), conn); } @Test From 1f370f8cc4bca468866bd424edd5a5603578cc36 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Fri, 10 Jul 2026 17:26:19 +0200 Subject: [PATCH 3/6] [Enhancement #373] Remove identifier generation code from OntoDrivers. --- .../kbss/jopa/oom/ObjectOntologyMapper.java | 10 --- .../kbss/jopa/accessors/DataSourceStub.java | 5 -- .../cz/cvut/kbss/ontodriver/Connection.java | 11 --- .../kbss/ontodriver/util/IdentifierUtils.java | 24 ------ .../ontodriver/util/IdentifierUtilsTest.java | 51 ----------- .../kbss/ontodriver/jena/JenaAdapter.java | 6 -- .../kbss/ontodriver/jena/JenaConnection.java | 7 -- .../jena/util/IdentifierGenerator.java | 65 -------------- .../ontodriver/jena/JenaConnectionTest.java | 15 ---- .../jena/util/IdentifierGeneratorTest.java | 80 ----------------- .../kbss/ontodriver/owlapi/OwlapiAdapter.java | 6 -- .../ontodriver/owlapi/OwlapiConnection.java | 7 -- .../owlapi/container/ContainerHandler.java | 23 +++-- .../owlapi/util/IdentifierGenerator.java | 62 -------------- .../owlapi/OwlapiConnectionTest.java | 18 ---- .../container/ContainerHandlerTest.java | 4 - .../owlapi/util/IdentifierGeneratorTest.java | 85 ------------------- .../kbss/ontodriver/rdf4j/Rdf4jAdapter.java | 35 -------- .../ontodriver/rdf4j/Rdf4jConnection.java | 12 --- .../ontodriver/rdf4j/Rdf4jAdapterTest.java | 48 ----------- .../ontodriver/rdf4j/Rdf4jConnectionTest.java | 35 ++++---- 21 files changed, 36 insertions(+), 573 deletions(-) delete mode 100644 ontodriver-api/src/test/java/cz/cvut/kbss/ontodriver/util/IdentifierUtilsTest.java delete mode 100644 ontodriver-jena/src/main/java/cz/cvut/kbss/ontodriver/jena/util/IdentifierGenerator.java delete mode 100644 ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/util/IdentifierGeneratorTest.java delete mode 100644 ontodriver-owlapi/src/main/java/cz/cvut/kbss/ontodriver/owlapi/util/IdentifierGenerator.java delete mode 100644 ontodriver-owlapi/src/test/java/cz/cvut/kbss/ontodriver/owlapi/util/IdentifierGeneratorTest.java 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 db49843cc..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,15 +80,6 @@ public interface ObjectOntologyMapper { */ void loadFieldValue(T entity, FieldSpecification fieldSpec, Descriptor descriptor); - /** - * Generates a fresh identifier for the specified entity. - * - * @param entity Entity for which to generate the identifier - * @param et Entity type - * @return New entity identifier - */ - Object generateIdentifier(Object entity, EntityType et); - /** * Persists the specified entity into the underlying ontology. * diff --git a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/accessors/DataSourceStub.java b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/accessors/DataSourceStub.java index 1e8357ea2..5611845cf 100644 --- a/jopa-impl/src/test/java/cz/cvut/kbss/jopa/accessors/DataSourceStub.java +++ b/jopa-impl/src/test/java/cz/cvut/kbss/jopa/accessors/DataSourceStub.java @@ -142,11 +142,6 @@ public Collection> find(AxiomDescriptor descriptor) { @Override public void persist(AxiomValueDescriptor descriptor) {} - @Override - public URI generateIdentifier(URI classUri) { - return null; - } - @Override public void update(AxiomValueDescriptor descriptor) {} diff --git a/ontodriver-api/src/main/java/cz/cvut/kbss/ontodriver/Connection.java b/ontodriver-api/src/main/java/cz/cvut/kbss/ontodriver/Connection.java index 5f1ebfa43..ca53547e6 100644 --- a/ontodriver-api/src/main/java/cz/cvut/kbss/ontodriver/Connection.java +++ b/ontodriver-api/src/main/java/cz/cvut/kbss/ontodriver/Connection.java @@ -187,17 +187,6 @@ public interface Connection extends AutoCloseable, Wrapper { */ void persist(AxiomValueDescriptor descriptor) throws OntoDriverException; - /** - * Generates a new unique identifier based on the specified type. - *

- * The identifier is required to be unique in the whole repository. - * - * @param classUri OWL class identifier - * @return Unique identifier - * @throws OntoDriverException If an ontology access error occurs - */ - URI generateIdentifier(URI classUri) throws OntoDriverException; - /** * Persists the values specified by this descriptor, removing existing property values from the ontology. *

diff --git a/ontodriver-api/src/main/java/cz/cvut/kbss/ontodriver/util/IdentifierUtils.java b/ontodriver-api/src/main/java/cz/cvut/kbss/ontodriver/util/IdentifierUtils.java index fcd4a478a..0887ca5ba 100644 --- a/ontodriver-api/src/main/java/cz/cvut/kbss/ontodriver/util/IdentifierUtils.java +++ b/ontodriver-api/src/main/java/cz/cvut/kbss/ontodriver/util/IdentifierUtils.java @@ -21,7 +21,6 @@ import java.net.URI; import java.net.URL; -import java.util.Objects; import java.util.Random; import java.util.Set; @@ -38,29 +37,6 @@ private IdentifierUtils() { throw new AssertionError(); } - /** - * Generates a (pseudo) random identifier based on the specified class URI. - *

- * The identifier consists of the class URI and then contains the string 'instance' and a random integer to ensure - * uniqueness. The 'instance' part is appended after a slash or a _, if the class URI contains a hash fragment. - * - * @param classUri Class URI used as identifier base - * @return Generated identifier - */ - public static URI generateIdentifier(URI classUri) { - Objects.requireNonNull(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()); - } - } - } - /** * Generates a (pseudo) random integer. * diff --git a/ontodriver-api/src/test/java/cz/cvut/kbss/ontodriver/util/IdentifierUtilsTest.java b/ontodriver-api/src/test/java/cz/cvut/kbss/ontodriver/util/IdentifierUtilsTest.java deleted file mode 100644 index 858c4e12b..000000000 --- a/ontodriver-api/src/test/java/cz/cvut/kbss/ontodriver/util/IdentifierUtilsTest.java +++ /dev/null @@ -1,51 +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.ontodriver.util; - -import org.junit.jupiter.api.Test; - -import java.net.URI; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.MatcherAssert.assertThat; - -class IdentifierUtilsTest { - - private static final String URI_WITH_SLASH = "http://onto.fel.cvut.cz/ontologies/jopa/ClassA"; - - @Test - void generateIdentifierAppendsInstanceIdentifierToUriWithHashFragment() { - final URI clsUri = URI.create("http://onto.fel.cvut.cz/ontologies/jopa#IdentifierUtilsTest"); - final URI result = IdentifierUtils.generateIdentifier(clsUri); - assertThat(result.toString(), containsString("_instance")); - } - - @Test - void generateIdentifierAppendsIdentifierToUriEndingWithSlash() { - final URI clsUri = URI.create(URI_WITH_SLASH); - final URI result = IdentifierUtils.generateIdentifier(clsUri); - assertThat(result.toString(), containsString("/instance")); - } - - @Test - void generateIdentifierAppendsIdentifierWithSlashToUriWithoutHashFragment() { - final URI clsUri = URI.create(URI_WITH_SLASH); - final URI result = IdentifierUtils.generateIdentifier(clsUri); - assertThat(result.toString(), containsString("/instance")); - } -} diff --git a/ontodriver-jena/src/main/java/cz/cvut/kbss/ontodriver/jena/JenaAdapter.java b/ontodriver-jena/src/main/java/cz/cvut/kbss/ontodriver/jena/JenaAdapter.java index ef0a407c5..2afc99201 100644 --- a/ontodriver-jena/src/main/java/cz/cvut/kbss/ontodriver/jena/JenaAdapter.java +++ b/ontodriver-jena/src/main/java/cz/cvut/kbss/ontodriver/jena/JenaAdapter.java @@ -30,7 +30,6 @@ import cz.cvut.kbss.ontodriver.jena.list.ReferencedListHandler; import cz.cvut.kbss.ontodriver.jena.list.SimpleListHandler; import cz.cvut.kbss.ontodriver.jena.query.AbstractResultSet; -import cz.cvut.kbss.ontodriver.jena.util.IdentifierGenerator; import cz.cvut.kbss.ontodriver.model.Axiom; import cz.cvut.kbss.ontodriver.util.Transaction; import org.apache.jena.query.Query; @@ -118,11 +117,6 @@ List getContext() { return connector.getContexts().stream().map(URI::create).collect(Collectors.toList()); } - URI generateIdentifier(URI classUri) { - beginTransactionIfNotActive(); - return new IdentifierGenerator(connector).generateIdentifier(classUri); - } - boolean isConsistent(URI context) { beginTransactionIfNotActive(); return inferenceConnector.isConsistent(context != null ? context.toString() : null); diff --git a/ontodriver-jena/src/main/java/cz/cvut/kbss/ontodriver/jena/JenaConnection.java b/ontodriver-jena/src/main/java/cz/cvut/kbss/ontodriver/jena/JenaConnection.java index 65950952f..5738a4aa8 100644 --- a/ontodriver-jena/src/main/java/cz/cvut/kbss/ontodriver/jena/JenaConnection.java +++ b/ontodriver-jena/src/main/java/cz/cvut/kbss/ontodriver/jena/JenaConnection.java @@ -178,13 +178,6 @@ public void persist(AxiomValueDescriptor descriptor) throws JenaDriverException } } - @Override - public URI generateIdentifier(URI classUri) { - ensureOpen(); - Objects.requireNonNull(classUri); - return adapter.generateIdentifier(classUri); - } - @Override public void update(AxiomValueDescriptor descriptor) throws OntoDriverException { ensureOpen(); diff --git a/ontodriver-jena/src/main/java/cz/cvut/kbss/ontodriver/jena/util/IdentifierGenerator.java b/ontodriver-jena/src/main/java/cz/cvut/kbss/ontodriver/jena/util/IdentifierGenerator.java deleted file mode 100644 index 97d07261d..000000000 --- a/ontodriver-jena/src/main/java/cz/cvut/kbss/ontodriver/jena/util/IdentifierGenerator.java +++ /dev/null @@ -1,65 +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.ontodriver.jena.util; - -import cz.cvut.kbss.ontodriver.exception.IdentifierGenerationException; -import cz.cvut.kbss.ontodriver.jena.connector.StorageConnector; -import cz.cvut.kbss.ontodriver.util.IdentifierUtils; -import cz.cvut.kbss.ontodriver.util.Vocabulary; -import org.apache.jena.rdf.model.Property; -import org.apache.jena.rdf.model.RDFNode; -import org.apache.jena.rdf.model.ResourceFactory; - -import java.net.URI; -import java.util.Collections; - -public class IdentifierGenerator { - - private static final int GENERATOR_THRESHOLD = 64; - - private final StorageConnector storageConnector; - - public IdentifierGenerator(StorageConnector storageConnector) { - this.storageConnector = storageConnector; - } - - /** - * Generates a unique identifier based on the specified class URI. - * - * @param classUri Type URI, used as the identifier base - * @return Generated identifier - */ - public URI generateIdentifier(URI classUri) { - int i = 0; - boolean exists; - final Property property = ResourceFactory.createProperty(Vocabulary.RDF_TYPE); - final RDFNode type = ResourceFactory.createResource(classUri.toString()); - URI result; - do { - result = IdentifierUtils.generateIdentifier(classUri); - exists = storageConnector.contains(ResourceFactory.createResource(result.toString()), property, type, - Collections.emptySet()); - i++; - } while (exists && i < GENERATOR_THRESHOLD); - if (i >= GENERATOR_THRESHOLD) { - throw new IdentifierGenerationException( - "Failed to generate a unique identifier in " + GENERATOR_THRESHOLD + " attempts."); - } - return result; - } -} diff --git a/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/JenaConnectionTest.java b/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/JenaConnectionTest.java index 5533c8f6d..608866201 100644 --- a/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/JenaConnectionTest.java +++ b/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/JenaConnectionTest.java @@ -210,21 +210,6 @@ public void getContextsThrowsIllegalStateExceptionForClosedConnection() throws E } } - @Test - public void generateIdentifierCallsAdapter() { - final URI uri = Generator.generateUri(); - when(adapterMock.generateIdentifier(uri)).thenReturn(uri); - final URI result = connection.generateIdentifier(uri); - assertNotNull(result); - verify(adapterMock).generateIdentifier(uri); - } - - @Test - public void generateIdentifierThrowsIllegalStateExceptionForClosedConnection() throws Exception { - connection.close(); - assertThrows(IllegalStateException.class, () -> connection.generateIdentifier(Generator.generateUri())); - } - @Test public void findCallsAdapterWithDescriptor() throws Exception { final AxiomDescriptor descriptor = new AxiomDescriptor(SUBJECT); diff --git a/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/util/IdentifierGeneratorTest.java b/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/util/IdentifierGeneratorTest.java deleted file mode 100644 index 0cc0ba2cd..000000000 --- a/ontodriver-jena/src/test/java/cz/cvut/kbss/ontodriver/jena/util/IdentifierGeneratorTest.java +++ /dev/null @@ -1,80 +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.ontodriver.jena.util; - -import cz.cvut.kbss.ontodriver.exception.IdentifierGenerationException; -import cz.cvut.kbss.ontodriver.jena.connector.StorageConnector; -import cz.cvut.kbss.ontodriver.jena.environment.Generator; -import cz.cvut.kbss.ontodriver.util.Vocabulary; -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; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.net.URI; -import java.util.Collections; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anySet; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -public class IdentifierGeneratorTest { - - private static final URI TYPE_URI = Generator.generateUri(); - - @Mock - private StorageConnector connectorMock; - - private IdentifierGenerator generator; - - @BeforeEach - public void setUp() { - this.generator = new IdentifierGenerator(connectorMock); - } - - @Test - public void generateIdentifierCreatesIdentifierBasedOnClassUri() { - final URI result = generator.generateIdentifier(TYPE_URI); - assertNotNull(result); - assertTrue(result.toString().contains(TYPE_URI.toString())); - } - - @Test - public void generateIdentifierChecksForExistenceOfNewlyGeneratedIdentifier() { - when(connectorMock.contains(any(), any(), any(), anySet())).thenReturn(false); - final URI result = generator.generateIdentifier(TYPE_URI); - assertNotNull(result); - verify(connectorMock) - .contains(ResourceFactory.createResource(result.toString()), ResourceFactory.createProperty( - Vocabulary.RDF_TYPE), ResourceFactory.createResource(TYPE_URI.toString()), - Collections.emptySet()); - } - - @Test - public void throwsIdentifierGenerationExceptionWhenMaximumAttemptsToGenerateAreExceeded() { - when(connectorMock.contains(any(), any(), any(), any())).thenReturn(true); - assertThrows(IdentifierGenerationException.class, () -> generator.generateIdentifier(TYPE_URI)); - } -} diff --git a/ontodriver-owlapi/src/main/java/cz/cvut/kbss/ontodriver/owlapi/OwlapiAdapter.java b/ontodriver-owlapi/src/main/java/cz/cvut/kbss/ontodriver/owlapi/OwlapiAdapter.java index 2dabff194..a8daece22 100644 --- a/ontodriver-owlapi/src/main/java/cz/cvut/kbss/ontodriver/owlapi/OwlapiAdapter.java +++ b/ontodriver-owlapi/src/main/java/cz/cvut/kbss/ontodriver/owlapi/OwlapiAdapter.java @@ -30,7 +30,6 @@ import cz.cvut.kbss.ontodriver.owlapi.query.OwlapiPreparedStatement; import cz.cvut.kbss.ontodriver.owlapi.query.OwlapiStatement; import cz.cvut.kbss.ontodriver.owlapi.query.StatementExecutorFactory; -import cz.cvut.kbss.ontodriver.owlapi.util.IdentifierGenerator; import org.semanticweb.owlapi.model.OWLAxiom; import org.semanticweb.owlapi.model.OWLDataFactory; import org.semanticweb.owlapi.model.OWLOntology; @@ -182,11 +181,6 @@ public void persist(AxiomValueDescriptor descriptor) { new AxiomSaver(this, ontologySnapshot).persist(descriptor); } - public URI generateIdentifier(URI classUri) { - startTransactionIfNotActive(); - return new IdentifierGenerator(ontology()).generateIdentifier(classUri); - } - public void update(AxiomValueDescriptor descriptor) { startTransactionIfNotActive(); new EpistemicAxiomRemover(this, ontologySnapshot).remove(descriptor); diff --git a/ontodriver-owlapi/src/main/java/cz/cvut/kbss/ontodriver/owlapi/OwlapiConnection.java b/ontodriver-owlapi/src/main/java/cz/cvut/kbss/ontodriver/owlapi/OwlapiConnection.java index 59bade1b1..ec53ab40c 100644 --- a/ontodriver-owlapi/src/main/java/cz/cvut/kbss/ontodriver/owlapi/OwlapiConnection.java +++ b/ontodriver-owlapi/src/main/java/cz/cvut/kbss/ontodriver/owlapi/OwlapiConnection.java @@ -193,13 +193,6 @@ public void persist(AxiomValueDescriptor descriptor) throws OntoDriverException } } - @Override - public URI generateIdentifier(URI classUri) { - ensureOpen(); - Objects.requireNonNull(classUri); - return adapter.generateIdentifier(classUri); - } - @Override public void update(AxiomValueDescriptor descriptor) throws OntoDriverException { ensureOpen(); diff --git a/ontodriver-owlapi/src/main/java/cz/cvut/kbss/ontodriver/owlapi/container/ContainerHandler.java b/ontodriver-owlapi/src/main/java/cz/cvut/kbss/ontodriver/owlapi/container/ContainerHandler.java index af45444f4..4e4cc7cbd 100644 --- a/ontodriver-owlapi/src/main/java/cz/cvut/kbss/ontodriver/owlapi/container/ContainerHandler.java +++ b/ontodriver-owlapi/src/main/java/cz/cvut/kbss/ontodriver/owlapi/container/ContainerHandler.java @@ -18,7 +18,6 @@ package cz.cvut.kbss.ontodriver.owlapi.container; import com.google.common.collect.Multimap; -import cz.cvut.kbss.jopa.vocabulary.RDFS; import cz.cvut.kbss.ontodriver.descriptor.ContainerDescriptor; import cz.cvut.kbss.ontodriver.descriptor.ContainerValueDescriptor; import cz.cvut.kbss.ontodriver.exception.IntegrityConstraintViolatedException; @@ -35,6 +34,7 @@ import cz.cvut.kbss.ontodriver.owlapi.exception.OwlapiDriverException; import cz.cvut.kbss.ontodriver.owlapi.list.ListContentStorageHelper; import cz.cvut.kbss.ontodriver.owlapi.util.OwlapiUtils; +import cz.cvut.kbss.ontodriver.util.IdentifierUtils; import org.semanticweb.owlapi.model.IRI; import org.semanticweb.owlapi.model.OWLAnnotation; import org.semanticweb.owlapi.model.OWLAnnotationValue; @@ -235,20 +235,29 @@ public void persistContainer(ContainerValueDescriptor descriptor) { final OWLNamedIndividual owner = dataFactory.getOWLNamedIndividual(IRI.create(descriptor.getOwner() .getIdentifier())); final List changes = new ArrayList<>(descriptor.getValues().size() + 1); - final OWLNamedIndividual container = createContainer(owner, descriptor.getProperty(), descriptor.getType(), changes); + final OWLNamedIndividual container = createContainer(owner, descriptor, changes); changes.addAll(createContainerContent(container, descriptor.getProperty(), descriptor.getValues())); owlapiAdapter.addTransactionalChanges(snapshot.applyChanges(changes)); } - private OWLNamedIndividual createContainer(OWLNamedIndividual owner, Assertion property, URI containerType, - List changes) { - final OWLNamedIndividual container = dataFactory.getOWLNamedIndividual(IRI.create(owlapiAdapter.generateIdentifier(URI.create(RDFS.CONTAINER)))); - final OWLObjectProperty containerOwnerProperty = dataFactory.getOWLObjectProperty(IRI.create(property.getIdentifier())); + private OWLNamedIndividual createContainer(OWLNamedIndividual owner, ContainerValueDescriptor descriptor, List changes) { + final OWLNamedIndividual container = dataFactory.getOWLNamedIndividual(generateContainerIri(descriptor)); + final OWLObjectProperty containerOwnerProperty = dataFactory.getOWLObjectProperty(IRI.create(descriptor.getProperty().getIdentifier())); changes.add(new MutableAddAxiom(ontology, dataFactory.getOWLObjectPropertyAssertionAxiom(containerOwnerProperty, owner, container))); - changes.add(new MutableAddAxiom(ontology, dataFactory.getOWLClassAssertionAxiom(dataFactory.getOWLClass(IRI.create(containerType)), container))); + changes.add(new MutableAddAxiom(ontology, dataFactory.getOWLClassAssertionAxiom(dataFactory.getOWLClass(IRI.create(descriptor.getType())), container))); return container; } + private IRI generateContainerIri(ContainerValueDescriptor descriptor) { + IRI iri; + do { + iri = IRI.create(descriptor.getOwner().getIdentifier() + .toString() + descriptor.getType().getFragment() + .toLowerCase() + "_" + IdentifierUtils.randomInt()); + } while (ontology.containsIndividualInSignature(iri)); + return iri; + } + private List createContainerContent(OWLIndividual container, Assertion property, List values) { diff --git a/ontodriver-owlapi/src/main/java/cz/cvut/kbss/ontodriver/owlapi/util/IdentifierGenerator.java b/ontodriver-owlapi/src/main/java/cz/cvut/kbss/ontodriver/owlapi/util/IdentifierGenerator.java deleted file mode 100644 index 7d757c90a..000000000 --- a/ontodriver-owlapi/src/main/java/cz/cvut/kbss/ontodriver/owlapi/util/IdentifierGenerator.java +++ /dev/null @@ -1,62 +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.ontodriver.owlapi.util; - -import cz.cvut.kbss.ontodriver.exception.IdentifierGenerationException; -import cz.cvut.kbss.ontodriver.util.IdentifierUtils; -import org.semanticweb.owlapi.model.IRI; -import org.semanticweb.owlapi.model.OWLOntology; - -import java.net.URI; - -public class IdentifierGenerator { - - private static final int GENERATION_THRESHOLD = 100; - - private final OWLOntology ontology; - - public IdentifierGenerator(OWLOntology ontology) { - assert ontology != null; - this.ontology = ontology; - } - - /** - * Generates an identifier which is unique w.r.t. individuals in the known ontology. - * - * @param classUri URI of individual's class, used as base for the identifier - * @return Unique identifier - * @throws IdentifierGenerationException If unable to generate unique identifier - */ - public URI generateIdentifier(URI classUri) { - boolean unique = false; - URI id = null; - int counter = 0; - while (!unique && counter++ < GENERATION_THRESHOLD) { - id = IdentifierUtils.generateIdentifier(classUri); - unique = isIdentifierUnique(id); - } - if (!unique) { - throw new IdentifierGenerationException("Unable to generate a unique identifier."); - } - return id; - } - - private boolean isIdentifierUnique(URI uri) { - return !ontology.containsIndividualInSignature(IRI.create(uri)); - } -} diff --git a/ontodriver-owlapi/src/test/java/cz/cvut/kbss/ontodriver/owlapi/OwlapiConnectionTest.java b/ontodriver-owlapi/src/test/java/cz/cvut/kbss/ontodriver/owlapi/OwlapiConnectionTest.java index 7821fe5cf..2b1398348 100644 --- a/ontodriver-owlapi/src/test/java/cz/cvut/kbss/ontodriver/owlapi/OwlapiConnectionTest.java +++ b/ontodriver-owlapi/src/test/java/cz/cvut/kbss/ontodriver/owlapi/OwlapiConnectionTest.java @@ -44,9 +44,7 @@ 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.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -153,22 +151,6 @@ public void typesReturnsTypesHandlerForOwlapiDriver() { assertEquals(types, result); } - @Test - public void generateIdentifierGetsNewIdentifier() { - final URI identifier = URI.create("http://newIdentifier"); - when(adapterMock.generateIdentifier(any(URI.class))).thenReturn(identifier); - - final URI result = connection.generateIdentifier(URI.create("http://baseUri")); - assertEquals(identifier, result); - } - - @Test - public void generateIdentifierOnClosedThrowsException() { - connection.close(); - assertThrows(IllegalStateException.class, () -> connection.generateIdentifier(URI.create("http://baseUri"))); - verify(adapterMock, never()).generateIdentifier(any(URI.class)); - } - @Test public void propertiesReturnPropertiesHandlerForOwlapiDriver() { final OwlapiProperties properties = mock(OwlapiProperties.class); diff --git a/ontodriver-owlapi/src/test/java/cz/cvut/kbss/ontodriver/owlapi/container/ContainerHandlerTest.java b/ontodriver-owlapi/src/test/java/cz/cvut/kbss/ontodriver/owlapi/container/ContainerHandlerTest.java index 5fae18d47..b80aa8b6d 100644 --- a/ontodriver-owlapi/src/test/java/cz/cvut/kbss/ontodriver/owlapi/container/ContainerHandlerTest.java +++ b/ontodriver-owlapi/src/test/java/cz/cvut/kbss/ontodriver/owlapi/container/ContainerHandlerTest.java @@ -59,8 +59,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class ContainerHandlerTest { @@ -205,7 +203,6 @@ void persistContainerCreatesContainerAndAddsSpecifiedValuesToIt() { final List values = List.of(NamedResource.create(Generator.generateUri()), NamedResource.create(Generator.generateUri())); final ContainerValueDescriptor descriptor = ContainerValueDescriptor.bagValueDescriptor(owner, property); values.forEach(descriptor::addValue); - when(owlapiAdapter.generateIdentifier(any())).thenReturn(Generator.generateUri()); sut.persistContainer(descriptor); verifyContainerContent(property, RDF.BAG, values); @@ -319,7 +316,6 @@ void persistContainerSupportsTranslationsAsContent() { new Translations(Map.of("en", "Two", "cs", "Dva"))); final ContainerValueDescriptor descriptor = ContainerValueDescriptor.bagValueDescriptor(owner, property); values.forEach(descriptor::addValue); - when(owlapiAdapter.generateIdentifier(any())).thenReturn(Generator.generateUri()); sut.persistContainer(descriptor); diff --git a/ontodriver-owlapi/src/test/java/cz/cvut/kbss/ontodriver/owlapi/util/IdentifierGeneratorTest.java b/ontodriver-owlapi/src/test/java/cz/cvut/kbss/ontodriver/owlapi/util/IdentifierGeneratorTest.java deleted file mode 100644 index 87ca81118..000000000 --- a/ontodriver-owlapi/src/test/java/cz/cvut/kbss/ontodriver/owlapi/util/IdentifierGeneratorTest.java +++ /dev/null @@ -1,85 +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.ontodriver.owlapi.util; - -import cz.cvut.kbss.ontodriver.exception.IdentifierGenerationException; -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.semanticweb.owlapi.model.IRI; -import org.semanticweb.owlapi.model.OWLOntology; - -import java.lang.reflect.Field; -import java.net.URI; -import java.util.HashSet; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertEquals; -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.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class IdentifierGeneratorTest { - - @Mock - private OWLOntology ontologyMock; - - private IdentifierGenerator generator; - - @BeforeEach - void setUp() { - this.generator = new IdentifierGenerator(ontologyMock); - } - - @Test - void generatesUniqueIdentifier() { - final URI baseUri = URI.create("http://baseUri/"); - when(ontologyMock.containsIndividualInSignature(any(IRI.class))).thenReturn(false); - final Set results = new HashSet<>(); - int tries = 100; - for (int i = 0; i < tries; i++) { - results.add(generator.generateIdentifier(baseUri)); - } - results.forEach(uri -> assertTrue(uri.toString().contains(baseUri.toString()))); - assertEquals(tries, results.size()); // All tries lead to unique URI - } - - @Test - void throwsExceptionWhenUnableToGenerateUriWithinThreshold() throws Exception { - final URI baseUri = URI.create("http://baseUri/"); - final int threshold = extractThreshold(); - when(ontologyMock.containsIndividualInSignature(any(IRI.class))).thenReturn(true); - try { - assertThrows(IdentifierGenerationException.class, () -> generator.generateIdentifier(baseUri)); - } finally { - verify(ontologyMock, times(threshold)).containsIndividualInSignature(any(IRI.class)); - } - } - - private int extractThreshold() throws Exception { - final Field thresholdField = IdentifierGenerator.class.getDeclaredField("GENERATION_THRESHOLD"); - thresholdField.setAccessible(true); - return (int) thresholdField.get(null); - } -} diff --git a/ontodriver-rdf4j/src/main/java/cz/cvut/kbss/ontodriver/rdf4j/Rdf4jAdapter.java b/ontodriver-rdf4j/src/main/java/cz/cvut/kbss/ontodriver/rdf4j/Rdf4jAdapter.java index 9505fd6ec..75225aea9 100644 --- a/ontodriver-rdf4j/src/main/java/cz/cvut/kbss/ontodriver/rdf4j/Rdf4jAdapter.java +++ b/ontodriver-rdf4j/src/main/java/cz/cvut/kbss/ontodriver/rdf4j/Rdf4jAdapter.java @@ -22,7 +22,6 @@ import cz.cvut.kbss.ontodriver.Wrapper; import cz.cvut.kbss.ontodriver.descriptor.AxiomDescriptor; import cz.cvut.kbss.ontodriver.descriptor.AxiomValueDescriptor; -import cz.cvut.kbss.ontodriver.exception.IdentifierGenerationException; import cz.cvut.kbss.ontodriver.exception.OntoDriverException; import cz.cvut.kbss.ontodriver.model.Axiom; import cz.cvut.kbss.ontodriver.rdf4j.config.Constants; @@ -35,30 +34,22 @@ import cz.cvut.kbss.ontodriver.rdf4j.list.SimpleListHandler; import cz.cvut.kbss.ontodriver.rdf4j.query.QuerySpecification; import cz.cvut.kbss.ontodriver.rdf4j.util.Rdf4jUtils; -import cz.cvut.kbss.ontodriver.util.IdentifierUtils; import cz.cvut.kbss.ontodriver.util.Transaction; import org.eclipse.rdf4j.model.Resource; import org.eclipse.rdf4j.model.Statement; import org.eclipse.rdf4j.model.Value; import org.eclipse.rdf4j.model.ValueFactory; -import org.eclipse.rdf4j.model.vocabulary.RDF; import org.eclipse.rdf4j.query.TupleQueryResult; import java.net.URI; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; public class Rdf4jAdapter implements StatementExecutor, Closeable, Wrapper { - /** - * Maximum number of attempts to generate a unique identifier - */ - private static final int ID_GENERATION_THRESHOLD = 64; - private final RepoConnection connector; private final ValueFactory valueFactory; private final RuntimeConfiguration config; @@ -143,22 +134,6 @@ List getContexts() throws Rdf4jDriverException { return contexts; } - URI generateIdentifier(URI classUri) throws Rdf4jDriverException { - startTransactionIfNotActive(); - boolean unique = false; - URI id = null; - int counter = 0; - while (!unique && counter++ < ID_GENERATION_THRESHOLD) { - id = IdentifierUtils.generateIdentifier(classUri); - unique = isIdentifierUnique(id, classUri); - } - if (!unique) { - throw new IdentifierGenerationException("Unable to generate a unique identifier."); - } - return id; - - } - private void startTransactionIfNotActive() throws Rdf4jDriverException { if (!transaction.isActive()) { connector.begin(); @@ -166,12 +141,6 @@ private void startTransactionIfNotActive() throws Rdf4jDriverException { } } - private boolean isIdentifierUnique(URI identifier, URI classUri) throws Rdf4jDriverException { - return !connector.containsStatement( - Rdf4jUtils.toRdf4jIri(identifier, valueFactory), RDF.TYPE, - Rdf4jUtils.toRdf4jIri(classUri, valueFactory), true, Collections.emptySet()); - } - boolean contains(Axiom axiom, Set contexts) throws Rdf4jDriverException { startTransactionIfNotActive(); final Value value = axiomObjectToRdf4jValue(axiom); @@ -225,10 +194,6 @@ void remove(AxiomDescriptor axiomDescriptor) throws Rdf4jDriverException { new EpistemicAxiomRemover(connector, valueFactory).remove(axiomDescriptor); } - StatementExecutor getQueryExecutor() { - return connector; - } - SimpleListHandler getSimpleListHandler() throws Rdf4jDriverException { startTransactionIfNotActive(); return new SimpleListHandler(connector, valueFactory); diff --git a/ontodriver-rdf4j/src/main/java/cz/cvut/kbss/ontodriver/rdf4j/Rdf4jConnection.java b/ontodriver-rdf4j/src/main/java/cz/cvut/kbss/ontodriver/rdf4j/Rdf4jConnection.java index f84d6974c..d2f7f674d 100644 --- a/ontodriver-rdf4j/src/main/java/cz/cvut/kbss/ontodriver/rdf4j/Rdf4jConnection.java +++ b/ontodriver-rdf4j/src/main/java/cz/cvut/kbss/ontodriver/rdf4j/Rdf4jConnection.java @@ -27,7 +27,6 @@ import cz.cvut.kbss.ontodriver.Types; import cz.cvut.kbss.ontodriver.descriptor.AxiomDescriptor; import cz.cvut.kbss.ontodriver.descriptor.AxiomValueDescriptor; -import cz.cvut.kbss.ontodriver.exception.IdentifierGenerationException; import cz.cvut.kbss.ontodriver.exception.OntoDriverException; import cz.cvut.kbss.ontodriver.model.Axiom; import cz.cvut.kbss.ontodriver.rdf4j.exception.Rdf4jDriverException; @@ -174,17 +173,6 @@ public List getContexts() throws OntoDriverException { return adapter.getContexts(); } - @Override - public URI generateIdentifier(URI classUri) throws OntoDriverException { - ensureOpen(); - Objects.requireNonNull(classUri); - try { - return adapter.generateIdentifier(classUri); - } catch (IdentifierGenerationException e) { - throw new Rdf4jDriverException(e); - } - } - @Override public boolean contains(Axiom axiom, Set contexts) throws OntoDriverException { ensureOpen(); diff --git a/ontodriver-rdf4j/src/test/java/cz/cvut/kbss/ontodriver/rdf4j/Rdf4jAdapterTest.java b/ontodriver-rdf4j/src/test/java/cz/cvut/kbss/ontodriver/rdf4j/Rdf4jAdapterTest.java index a279408a1..489a443e9 100644 --- a/ontodriver-rdf4j/src/test/java/cz/cvut/kbss/ontodriver/rdf4j/Rdf4jAdapterTest.java +++ b/ontodriver-rdf4j/src/test/java/cz/cvut/kbss/ontodriver/rdf4j/Rdf4jAdapterTest.java @@ -21,7 +21,6 @@ import cz.cvut.kbss.ontodriver.config.DriverConfiguration; import cz.cvut.kbss.ontodriver.descriptor.AxiomDescriptor; import cz.cvut.kbss.ontodriver.descriptor.AxiomValueDescriptor; -import cz.cvut.kbss.ontodriver.exception.IdentifierGenerationException; import cz.cvut.kbss.ontodriver.model.Assertion; import cz.cvut.kbss.ontodriver.model.Axiom; import cz.cvut.kbss.ontodriver.model.AxiomImpl; @@ -65,10 +64,8 @@ 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.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.anyBoolean; @@ -568,51 +565,6 @@ void testFindEntityWithBlankNodeInTypes() throws Exception { assertEquals(type, ax.getValue().stringValue()); } - @Test - void testGenerateIdentifier_ClassWithHash() throws Exception { - final URI clsUri = URI.create("http://someClass.cz#class"); - when(connectorMock.containsStatement(any(Resource.class), eq(RDF.TYPE), - eq(VF.createIRI(clsUri.toString())), eq(true), eq(Collections.emptySet()))).thenReturn(false); - final URI res = adapter.generateIdentifier(clsUri); - assertNotNull(res); - assertTrue(res.toString().contains(clsUri.toString())); - verify(connectorMock).containsStatement(VF.createIRI(res.toString()), RDF.TYPE, - VF.createIRI(clsUri.toString()), true, Collections.emptySet()); - } - - @Test - void testGenerateIdentifier_ClassWithoutHash() throws Exception { - final URI clsUri = URI.create("http://someClass.cz/class"); - when(connectorMock.containsStatement(any(Resource.class), eq(RDF.TYPE), - eq(VF.createIRI(clsUri.toString())), eq(true), eq(Collections.emptySet()))).thenReturn(false); - final URI res = adapter.generateIdentifier(clsUri); - assertNotNull(res); - assertTrue(res.toString().contains(clsUri.toString())); - assertTrue(res.toString().contains("/instance")); - verify(connectorMock).containsStatement(VF.createIRI(res.toString()), RDF.TYPE, - VF.createIRI(clsUri.toString()), true, Collections.emptySet()); - } - - @Test - void testGenerateIdentifier_ClassEndsWithSlash() throws Exception { - final URI clsUri = URI.create("http://someClass.cz/class/"); - when(connectorMock.containsStatement(any(Resource.class), eq(RDF.TYPE), - eq(VF.createIRI(clsUri.toString())), eq(true), eq(Collections.emptySet()))).thenReturn(false); - final URI res = adapter.generateIdentifier(clsUri); - assertNotNull(res); - assertTrue(res.toString().contains(clsUri.toString())); - verify(connectorMock).containsStatement(VF.createIRI(res.toString()), RDF.TYPE, - VF.createIRI(clsUri.toString()), true, Collections.emptySet()); - } - - @Test - void testGenerateIdentifierNeverUnique() throws Exception { - final URI clsUri = URI.create("http://someClass.cz#class"); - when(connectorMock.containsStatement(any(Resource.class), eq(RDF.TYPE), - eq(VF.createIRI(clsUri.toString())), eq(true), eq(Collections.emptySet()))).thenReturn(true); - assertThrows(IdentifierGenerationException.class, () -> adapter.generateIdentifier(clsUri)); - } - @Test void testRemove() throws Exception { final AxiomDescriptor desc = new AxiomDescriptor(SUBJECT); diff --git a/ontodriver-rdf4j/src/test/java/cz/cvut/kbss/ontodriver/rdf4j/Rdf4jConnectionTest.java b/ontodriver-rdf4j/src/test/java/cz/cvut/kbss/ontodriver/rdf4j/Rdf4jConnectionTest.java index 47e6a246c..65e7a156d 100644 --- a/ontodriver-rdf4j/src/test/java/cz/cvut/kbss/ontodriver/rdf4j/Rdf4jConnectionTest.java +++ b/ontodriver-rdf4j/src/test/java/cz/cvut/kbss/ontodriver/rdf4j/Rdf4jConnectionTest.java @@ -23,7 +23,6 @@ import cz.cvut.kbss.ontodriver.descriptor.AxiomDescriptor; import cz.cvut.kbss.ontodriver.descriptor.AxiomValueDescriptor; import cz.cvut.kbss.ontodriver.model.Axiom; -import cz.cvut.kbss.ontodriver.rdf4j.environment.Generator; import cz.cvut.kbss.ontodriver.rdf4j.query.Rdf4jPreparedStatement; import cz.cvut.kbss.ontodriver.rdf4j.query.Rdf4jStatement; import org.junit.jupiter.api.BeforeEach; @@ -33,10 +32,26 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.net.URI; -import java.util.*; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +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.anySet; +import static org.mockito.Mockito.eq; +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 Rdf4jConnectionTest { @@ -149,16 +164,6 @@ public void testGetContextsOnClosed() throws Exception { verify(adapterMock, never()).getContexts(); } - @Test - public void testGenerateIdentifier() throws Exception { - final URI classUri = URI.create("https://onto.fel.cvut.cz/ontologies/jopa/owlClassA"); - final URI identifier = Generator.generateUri(); - when(adapterMock.generateIdentifier(classUri)).thenReturn(identifier); - final URI res = connection.generateIdentifier(classUri); - assertEquals(identifier, res); - verify(adapterMock).generateIdentifier(classUri); - } - @Test public void testContainsAxiom() throws Exception { final Axiom ax = mock(Axiom.class); From f65eb4b9268e3c950a35db3e3f53c188b73c2e8c Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Fri, 10 Jul 2026 17:51:53 +0200 Subject: [PATCH 4/6] [Enhancement #373] Define annotation for specifying custom id generator. --- .../jopa/model/annotations/IdGenerator.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/IdGenerator.java diff --git a/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/IdGenerator.java b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/IdGenerator.java new file mode 100644 index 000000000..fc423cb71 --- /dev/null +++ b/jopa-api/src/main/java/cz/cvut/kbss/jopa/model/annotations/IdGenerator.java @@ -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 value(); +} From d1f2b288e82681e4982fad1a7e2607b307df3f63 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Mon, 13 Jul 2026 13:57:14 +0200 Subject: [PATCH 5/6] [Enhancement #373] Discover an initialize custom id generators in entity classes. --- .../cvut/kbss/jopa/model/MetamodelImpl.java | 4 +-- .../model/metamodel/MetamodelBuilder.java | 30 +++++++++++++++++++ .../model/metamodel/MetamodelBuilderTest.java | 28 +++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) 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 03974867a..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 @@ -42,7 +42,6 @@ import java.net.URI; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; @@ -64,7 +63,7 @@ public class MetamodelImpl implements Metamodel, MetamodelProvider, MetamodelCla // Proxy classes for results of EntityManager.getReference private final Map, Class> referenceProxyClasses = new ConcurrentHashMap<>(); - private final Map, IdentifierGenerator> idGenerators = new HashMap<>(); + private Map, IdentifierGenerator> idGenerators; private NamedQueryManager namedQueryManager; private ResultSetMappingManager resultSetMappingManager; @@ -113,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(); } /** 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 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/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 97c7109aa..b4a62e95f 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 @@ -31,10 +31,12 @@ import cz.cvut.kbss.jopa.environment.utils.TestLocal; import cz.cvut.kbss.jopa.exception.InvalidFieldMappingException; import cz.cvut.kbss.jopa.exception.MetamodelInitializationException; +import cz.cvut.kbss.jopa.id.AbstractIdentifierGenerator; import cz.cvut.kbss.jopa.loaders.PersistenceUnitClassFinder; import cz.cvut.kbss.jopa.model.MultilingualString; import cz.cvut.kbss.jopa.model.annotations.FetchType; import cz.cvut.kbss.jopa.model.annotations.Id; +import cz.cvut.kbss.jopa.model.annotations.IdGenerator; import cz.cvut.kbss.jopa.model.annotations.Inferred; import cz.cvut.kbss.jopa.model.annotations.MappedSuperclass; import cz.cvut.kbss.jopa.model.annotations.Namespace; @@ -63,6 +65,7 @@ import cz.cvut.kbss.jopa.vocabulary.DC; import cz.cvut.kbss.jopa.vocabulary.RDF; import cz.cvut.kbss.jopa.vocabulary.RDFS; +import cz.cvut.kbss.ontodriver.Connection; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -77,6 +80,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import static org.hamcrest.CoreMatchers.containsStringIgnoringCase; import static org.hamcrest.CoreMatchers.hasItem; @@ -781,4 +785,28 @@ public static class MappedSuperclassWithInferredAttribute { @OWLClass(iri = Vocabulary.CLASS_BASE + "SubClassWithInferredParent") public static class SubClassWithInferredParent extends MappedSuperclassWithInferredAttribute { } + + @Test + void buildMetamodelResolvesCustomIdentifierGeneratorForEntityClass() { + when(finderMock.getEntities()).thenReturn(Set.of(ClassWithIdGenerator.class)); + builder.buildMetamodel(finderMock); + final EntityType et = (EntityType) builder.getEntityClass(ClassWithIdGenerator.class); + assertInstanceOf(CustomIdGenerator.class, builder.getIdGenerators().get(et)); + } + + @TestLocal + @OWLClass(iri = Vocabulary.CLASS_BASE + "ClassWithIdGenerator") + public static class ClassWithIdGenerator { + @Id(generated = true) + @IdGenerator(CustomIdGenerator.class) + private URI id; + } + + public static class CustomIdGenerator extends AbstractIdentifierGenerator { + @Override + public Object generate(Object entity, EntityType entityClass, Connection connection) { + final String classIri = entityClass.getIRI().toString(); + return classIri + "_" + UUID.randomUUID().toString(); + } + } } From 3468cb2f24631743b04cffda48af29523268e9ee Mon Sep 17 00:00:00 2001 From: Martin Ledvinka Date: Mon, 13 Jul 2026 14:14:37 +0200 Subject: [PATCH 6/6] [Enhancement #373] Add an integration test for custom id generators. --- .../cz/cvut/kbss/jopa/test/OWLClassZChild.java | 18 +++++++++++++++++- .../test/runner/CreateOperationsRunner.java | 14 ++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassZChild.java b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassZChild.java index 3b4b34e28..be663fbba 100644 --- a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassZChild.java +++ b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/OWLClassZChild.java @@ -17,14 +17,18 @@ */ package cz.cvut.kbss.jopa.test; +import cz.cvut.kbss.jopa.id.AbstractIdentifierGenerator; import cz.cvut.kbss.jopa.model.annotations.CascadeType; import cz.cvut.kbss.jopa.model.annotations.FetchType; import cz.cvut.kbss.jopa.model.annotations.Id; +import cz.cvut.kbss.jopa.model.annotations.IdGenerator; 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.ParticipationConstraints; +import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.jopa.vocabulary.RDFS; +import cz.cvut.kbss.ontodriver.Connection; import java.net.URI; import java.util.HashSet; @@ -34,7 +38,8 @@ @OWLClass(iri = Vocabulary.C_OWL_CLASS_Z_CHILD) public class OWLClassZChild { - @Id + @Id(generated = true) + @IdGenerator(NamedBasedIdGenerator.class) private URI id; @ParticipationConstraints(nonEmpty = true) @@ -92,4 +97,15 @@ public String toString() { ", name='" + name + '\'' + '}'; } + + public static class NamedBasedIdGenerator extends AbstractIdentifierGenerator { + + @Override + public Object generate(Object entity, EntityType entityClass, Connection connection) { + final String name = ((OWLClassZChild) entity).getName(); + final URI result = URI.create(Vocabulary.C_OWL_CLASS_Z_CHILD + "/" + name); + assert !exists(result, entityClass.getIRI().toURI(), connection); + return result; + } + } } diff --git a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/runner/CreateOperationsRunner.java b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/runner/CreateOperationsRunner.java index 46cdc8927..14782192a 100644 --- a/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/runner/CreateOperationsRunner.java +++ b/jopa-integration-tests/src/main/java/cz/cvut/kbss/jopa/test/runner/CreateOperationsRunner.java @@ -40,6 +40,7 @@ import cz.cvut.kbss.jopa.test.OWLClassWithQueryAttr4; import cz.cvut.kbss.jopa.test.OWLClassWithQueryAttr5; import cz.cvut.kbss.jopa.test.OWLClassX; +import cz.cvut.kbss.jopa.test.OWLClassZChild; import cz.cvut.kbss.jopa.test.Vocabulary; import cz.cvut.kbss.jopa.test.environment.DataAccessor; import cz.cvut.kbss.jopa.test.environment.Generators; @@ -766,4 +767,17 @@ void persistSupportsResourceAnnotationPropertiesWithFieldOfTypeObject() throws E new Quad(URI.create(entityM.getKey()), URI.create(Vocabulary.p_m_objectAnnotationProperty), entityM.getObjectAnnotation())), em); } + + @Test + void persistUsesCustomIdentifierGeneratorDeclaredForEntityClass() { + this.em = getEntityManager("persistUsesCustomIdentifierGeneratorDeclaredForEntityClass", false); + final OWLClassZChild instance = new OWLClassZChild(); + instance.setName("test-instance"); + persist(instance); + + assertNotNull(instance.getId()); + assertEquals(Vocabulary.C_OWL_CLASS_Z_CHILD + "/test-instance", instance.getId().toString()); + final OWLClassZChild result = findRequired(OWLClassZChild.class, instance.getId()); + assertNotNull(result); + } }