diff --git a/challenger/pom.xml b/challenger/pom.xml
index 13ed900b..4c442e0c 100644
--- a/challenger/pom.xml
+++ b/challenger/pom.xml
@@ -19,6 +19,12 @@
${thingifier.version}
+
+ org.xerial
+ sqlite-jdbc
+ ${sqlite-jdbc-version}
+
+
io.swagger.core.v3
swagger-models
@@ -166,4 +172,4 @@ https://www.slf4j.org/manual.html
- initial release
--->
\ No newline at end of file
+-->
diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeMain.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeMain.java
index 47e4022b..e3996999 100644
--- a/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeMain.java
+++ b/challenger/src/main/java/uk/co/compendiumdev/challenge/ChallengeMain.java
@@ -7,6 +7,7 @@
import uk.co.compendiumdev.thingifier.Thingifier;
import uk.co.compendiumdev.thingifier.application.MainImplementation;
import uk.co.compendiumdev.thingifier.application.httprouting.ThingifierHttpApiRoutings;
+import uk.co.compendiumdev.thingifier.core.repository.ThingRepositoryProviderConfig;
public class ChallengeMain {
@@ -19,7 +20,9 @@ public static void main(String[] args) {
logger.info("Starting Challenger");
MainImplementation app = new MainImplementation();
- Thingifier thingifier = new ChallengeApiModel().get();
+ ThingRepositoryProviderConfig repositoryConfig = ThingRepositoryProviderConfig.fromArgs(args);
+ logger.info("Using Thingifier repository {}", repositoryConfig.describe());
+ Thingifier thingifier = new ChallengeApiModel().get(repositoryConfig.createProvider());
app.registerModel("challengeapi", thingifier);
// add any additional thingifier configurations here if more needed than model has defined
@@ -115,6 +118,9 @@ public static ChallengeRouteHandler getChallenger(){
}
public static void stop(){
+ if (challenger != null) {
+ challenger.getThingifier().close();
+ }
challenger = null;
}
}
diff --git a/challenger/src/main/java/uk/co/compendiumdev/challenge/apimodel/ChallengeApiModel.java b/challenger/src/main/java/uk/co/compendiumdev/challenge/apimodel/ChallengeApiModel.java
index 6f10cb33..8f2836bc 100644
--- a/challenger/src/main/java/uk/co/compendiumdev/challenge/apimodel/ChallengeApiModel.java
+++ b/challenger/src/main/java/uk/co/compendiumdev/challenge/apimodel/ChallengeApiModel.java
@@ -6,12 +6,21 @@
import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType;
import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field;
import uk.co.compendiumdev.thingifier.core.domain.definitions.validation.VRule;
+import uk.co.compendiumdev.thingifier.core.repository.ThingRepositoryProvider;
import static uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType.STRING;
public class ChallengeApiModel {
public Thingifier get() {
- Thingifier todoList = new Thingifier();
+ return get(new EntityRelModel());
+ }
+
+ public Thingifier get(final ThingRepositoryProvider repositoryProvider) {
+ return get(new EntityRelModel(repositoryProvider));
+ }
+
+ public Thingifier get(final EntityRelModel entityRelModel) {
+ Thingifier todoList = new Thingifier(entityRelModel);
StringBuilder para = new StringBuilder();
diff --git a/challenger/src/test/java/uk/co/compendiumdev/challenge/apimodel/ChallengeApiModelRepositoryTest.java b/challenger/src/test/java/uk/co/compendiumdev/challenge/apimodel/ChallengeApiModelRepositoryTest.java
new file mode 100644
index 00000000..1db0df1b
--- /dev/null
+++ b/challenger/src/test/java/uk/co/compendiumdev/challenge/apimodel/ChallengeApiModelRepositoryTest.java
@@ -0,0 +1,215 @@
+package uk.co.compendiumdev.challenge.apimodel;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import uk.co.compendiumdev.thingifier.Thingifier;
+import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest;
+import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser;
+import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock;
+import uk.co.compendiumdev.thingifier.api.response.ApiResponse;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.Cardinality;
+import uk.co.compendiumdev.thingifier.core.EntityRelModel;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType;
+import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
+import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams;
+import uk.co.compendiumdev.thingifier.core.repository.SqliteThingRepository;
+import uk.co.compendiumdev.thingifier.core.repository.SqliteThingRepositoryProvider;
+import uk.co.compendiumdev.thingifier.core.repository.ThingRepository;
+
+public class ChallengeApiModelRepositoryTest {
+
+ @Test
+ public void challengeApiCanUseSqliteInMemoryRepository() {
+ try (SqliteThingRepositoryProvider provider = SqliteThingRepositoryProvider.inMemory()) {
+ Thingifier thingifier = new ChallengeApiModel().get(provider);
+
+ Assertions.assertTrue(
+ thingifier.getRepository(EntityRelModel.DEFAULT_DATABASE_NAME)
+ instanceof SqliteThingRepository);
+ Assertions.assertEquals(
+ 10,
+ thingifier.getThingInstancesNamed("todo", EntityRelModel.DEFAULT_DATABASE_NAME).
+ countInstances());
+ }
+ }
+
+ @Test
+ public void challengeApiGetUsesSqliteRepositoryReadsForSimpleTodoQueries() {
+ try (SqliteThingRepositoryProvider provider = SqliteThingRepositoryProvider.inMemory()) {
+ Thingifier thingifier = new ChallengeApiModel().get(provider);
+ ThingRepository repository = thingifier.getRepository(EntityRelModel.DEFAULT_DATABASE_NAME);
+ EntityDefinition todo = thingifier.getDefinitionNamed("todo");
+
+ EntityInstance firstTodo = repository.findInstanceByQueryIdentifier(todo, "1");
+ firstTodo.setValue("doneStatus", "true");
+ repository.updateInstance(firstTodo);
+
+ ApiResponse allTodos = thingifier.api().get(
+ "/todos", new QueryFilterParams(), new HttpHeadersBlock());
+ Assertions.assertEquals(200, allTodos.getStatusCode());
+ Assertions.assertEquals(10, allTodos.getReturnedInstanceCollection().size());
+
+ ApiResponse todoById = thingifier.api().get(
+ "/todos/1", new QueryFilterParams(), new HttpHeadersBlock());
+ Assertions.assertEquals(200, todoById.getStatusCode());
+ Assertions.assertEquals(1, todoById.getReturnedInstanceCollection().size());
+ Assertions.assertEquals(
+ "1",
+ todoById.getReturnedInstanceCollection().get(0).getPrimaryKeyValue());
+
+ QueryFilterParams doneStatusFilter = new QueryFilterParams();
+ doneStatusFilter.put("doneStatus", "false");
+ ApiResponse notDoneTodos = thingifier.api().get(
+ "/todos", doneStatusFilter, new HttpHeadersBlock());
+ Assertions.assertEquals(200, notDoneTodos.getStatusCode());
+ Assertions.assertEquals(9, notDoneTodos.getReturnedInstanceCollection().size());
+
+ QueryFilterParams idRangeFilter = new QueryFilterParams();
+ idRangeFilter.put("id", ">=2");
+ ApiResponse idRangeTodos = thingifier.api().get(
+ "/todos", idRangeFilter, new HttpHeadersBlock());
+ Assertions.assertEquals(200, idRangeTodos.getStatusCode());
+ Assertions.assertEquals(9, idRangeTodos.getReturnedInstanceCollection().size());
+
+ QueryFilterParams sortByDescendingId = new QueryFilterParams();
+ sortByDescendingId.put("sortBy", "-id");
+ ApiResponse sortedTodos = thingifier.api().get(
+ "/todos", sortByDescendingId, new HttpHeadersBlock());
+ Assertions.assertEquals(200, sortedTodos.getStatusCode());
+ Assertions.assertEquals(
+ "10",
+ sortedTodos.getReturnedInstanceCollection().get(0).getPrimaryKeyValue());
+ }
+ }
+
+ @Test
+ public void relationshipGetRoutesStillFallBackToLegacyQueryTraversal() {
+ try (SqliteThingRepositoryProvider provider = SqliteThingRepositoryProvider.inMemory()) {
+ Thingifier thingifier = new Thingifier(new EntityRelModel(provider));
+
+ EntityDefinition project = thingifier.defineThing("project", "projects");
+ project.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT));
+ project.addField(Field.is("title", FieldType.STRING));
+
+ EntityDefinition task = thingifier.defineThing("task", "tasks");
+ task.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT));
+ task.addField(Field.is("title", FieldType.STRING));
+
+ thingifier.defineRelationship(project, task, "tasks", Cardinality.ONE_TO_MANY()).
+ whenReversed(Cardinality.ONE_TO_ONE(), "task-of");
+
+ ThingRepository repository = thingifier.getRepository(EntityRelModel.DEFAULT_DATABASE_NAME);
+ EntityInstance projectInstance = new EntityInstance(project).setValue("title", "Office");
+ EntityInstance taskInstance = new EntityInstance(task).setValue("title", "File paperwork");
+ repository.addInstance(projectInstance);
+ repository.addInstance(taskInstance);
+ repository.connectRelationship(projectInstance, "tasks", taskInstance);
+
+ ApiResponse response = thingifier.api().get(
+ "/project/1/tasks", new QueryFilterParams(), new HttpHeadersBlock());
+
+ Assertions.assertEquals(200, response.getStatusCode());
+ Assertions.assertEquals(1, response.getReturnedInstanceCollection().size());
+ Assertions.assertEquals(
+ "File paperwork",
+ response.getReturnedInstanceCollection().get(0).getFieldValue("title").asString());
+ }
+ }
+
+ @Test
+ public void simpleEntityWritesUseSqliteRepositoryWithoutLoadingCompatibilitySnapshot() {
+ try (SqliteThingRepositoryProvider provider = SqliteThingRepositoryProvider.inMemory()) {
+ Thingifier thingifier = new Thingifier(new EntityRelModel(provider));
+
+ EntityDefinition note = thingifier.defineThing("note", "notes");
+ note.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT));
+ note.addField(Field.is("title", FieldType.STRING));
+
+ ThingRepository repository = thingifier.getRepository(EntityRelModel.DEFAULT_DATABASE_NAME);
+
+ ApiResponse create = thingifier.api().post(
+ "/notes",
+ bodyParser(thingifier, "{\"title\":\"created\"}"),
+ new HttpHeadersBlock());
+
+ Assertions.assertEquals(201, create.getStatusCode());
+ Assertions.assertFalse(repository.hasLoadedCompatibilitySnapshot());
+ Assertions.assertEquals(1, repository.listInstances(note).size());
+
+ ApiResponse postAmend = thingifier.api().post(
+ "/notes/1",
+ bodyParser(thingifier, "{\"title\":\"posted\"}"),
+ new HttpHeadersBlock());
+
+ Assertions.assertEquals(200, postAmend.getStatusCode());
+ Assertions.assertEquals(
+ "posted",
+ repository.findInstanceByQueryIdentifier(note, "1").
+ getFieldValue("title").asString());
+ Assertions.assertFalse(repository.hasLoadedCompatibilitySnapshot());
+
+ ApiResponse putAmend = thingifier.api().put(
+ "/notes/1",
+ bodyParser(thingifier, "{\"title\":\"put\"}"),
+ new HttpHeadersBlock());
+
+ Assertions.assertEquals(200, putAmend.getStatusCode());
+ Assertions.assertEquals(
+ "put",
+ repository.findInstanceByQueryIdentifier(note, "1").
+ getFieldValue("title").asString());
+ Assertions.assertFalse(repository.hasLoadedCompatibilitySnapshot());
+
+ ApiResponse delete = thingifier.api().delete(
+ "/notes/1", new HttpHeadersBlock());
+
+ Assertions.assertEquals(200, delete.getStatusCode());
+ Assertions.assertEquals(0, repository.listInstances(note).size());
+ Assertions.assertFalse(repository.hasLoadedCompatibilitySnapshot());
+ }
+ }
+
+ @Test
+ public void sqliteMemoryProvidersHaveSeparateNamedDatabaseLifetimes() {
+ EntityRelModel firstModel = null;
+ EntityRelModel secondModel = null;
+ try (SqliteThingRepositoryProvider firstProvider = SqliteThingRepositoryProvider.inMemory();
+ SqliteThingRepositoryProvider secondProvider = SqliteThingRepositoryProvider.inMemory()) {
+ firstModel = new EntityRelModel(firstProvider);
+ secondModel = new EntityRelModel(secondProvider);
+
+ EntityDefinition firstNote = firstModel.createEntityDefinition("note", "notes");
+ firstNote.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT));
+ firstNote.addField(Field.is("title", FieldType.STRING));
+
+ EntityDefinition secondNote = secondModel.createEntityDefinition("note", "notes");
+ secondNote.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT));
+ secondNote.addField(Field.is("title", FieldType.STRING));
+
+ firstModel.getRepository(EntityRelModel.DEFAULT_DATABASE_NAME).
+ addInstance(new EntityInstance(firstNote).setValue("title", "first"));
+
+ Assertions.assertEquals(
+ 1,
+ firstModel.getRepository(EntityRelModel.DEFAULT_DATABASE_NAME).
+ listInstances(firstNote).size());
+ Assertions.assertEquals(
+ 0,
+ secondModel.getRepository(EntityRelModel.DEFAULT_DATABASE_NAME).
+ listInstances(secondNote).size());
+ } finally {
+ if (firstModel != null) {
+ firstModel.close();
+ }
+ if (secondModel != null) {
+ secondModel.close();
+ }
+ }
+ }
+
+ private BodyParser bodyParser(final Thingifier thingifier, final String json) {
+ return new BodyParser(new HttpApiRequest("/path").setBody(json), thingifier.getThingNames());
+ }
+}
diff --git a/challengerAuto/pom.xml b/challengerAuto/pom.xml
index 34beb8d4..6d2eda4e 100644
--- a/challengerAuto/pom.xml
+++ b/challengerAuto/pom.xml
@@ -24,6 +24,13 @@
test
+
+ org.xerial
+ sqlite-jdbc
+ ${sqlite-jdbc-version}
+ test
+
+
org.junit.jupiter
junit-jupiter-api
@@ -100,4 +107,4 @@
-
\ No newline at end of file
+
diff --git a/challengerAuto/src/test/java/uk/co/compendiumdev/sparkstart/Environment.java b/challengerAuto/src/test/java/uk/co/compendiumdev/sparkstart/Environment.java
index 79e893bb..5f06988f 100644
--- a/challengerAuto/src/test/java/uk/co/compendiumdev/sparkstart/Environment.java
+++ b/challengerAuto/src/test/java/uk/co/compendiumdev/sparkstart/Environment.java
@@ -37,10 +37,10 @@ public static String getBaseUri() {
String [] args;
if(SINGLE_PLAYER_MODE){
- args = "".split(",");
+ args = "-sqlite-memory".split(",");
}else{
- args = "-multiplayer".split(",");
- };
+ args = "-sqlite-memory,-multiplayer".split(",");
+ }
ChallengeMain.main(args);
diff --git a/docs/repository-sqlite-migration-audit.md b/docs/repository-sqlite-migration-audit.md
new file mode 100644
index 00000000..92b3c50f
--- /dev/null
+++ b/docs/repository-sqlite-migration-audit.md
@@ -0,0 +1,44 @@
+# Repository SQLite Migration Audit
+
+This note classifies the current `getInstanceData()` and `getThingInstancesNamed()` usage during the SQLite repository experiment.
+
+## Normal API Request Paths
+
+Simple entity `GET` requests use `ThingRepository` for `/entity`, `/entities`, `/entity/{id}`, and `/entities/{id}`.
+
+Simple relationship `GET` traversal, such as `/projects/{id}/tasks`, now uses `ThingRepository.listRelatedInstances(...)`. SQLite executes the relationship lookup against relationship tables and materializes only the returned rows.
+
+Simple entity `POST`, `PUT`, and `DELETE` requests resolve entity definitions and entity instances through `ThingRepository` for root collection and root instance paths. These paths should not call `getInstanceData()` for SQLite unless a payload uses unsupported legacy behavior.
+
+Relationship create/delete URL paths still use the legacy `SimpleQuery` fallback. This includes write paths such as `POST /projects/{id}/tasks` and `DELETE /projects/{id}/tasks/{id}`.
+
+## Relationship Payload Helpers
+
+Relationship references in request bodies now resolve target instances through `ThingRepository` rather than `EntityInstanceCollection` scans. This keeps normal create/amend payloads from requiring a full SQLite compatibility snapshot merely to find a related target by id, guid, or other supported field.
+
+## Compatibility Snapshot Paths
+
+`EntityRelModel.getInstanceData()` and `ThingRepository.getInstanceData()` are compatibility/export paths. For SQLite, `getInstanceData()` explicitly hydrates the legacy in-memory snapshot.
+
+Known compatibility users:
+
+- data populator and JSON import paths;
+- admin/debug/export views and reporting;
+- legacy data populators that have not implemented `RepositoryDataPopulator`;
+- relationship write URL fallback paths.
+
+The default Todo API and Todo Manager seed populators now implement `RepositoryDataPopulator`, so SQLite-backed Todo apps can seed through repository writes without first hydrating `ERInstanceData`.
+
+The base GUI data explorer list/detail pages now read via `ThingRepository` and render relationship sections via `ThingRepository.listRelatedInstances(...)`.
+
+These remaining compatibility paths are acceptable for this phase, but they are the next places to reduce if memory efficiency needs to extend beyond simple entity reads and Todo-style population.
+
+## Test Helpers
+
+Most `getInstanceData()` and `getThingInstancesNamed()` usage is in `src/test`. These are test fixture and assertion helpers, not runtime leaks, unless a test is specifically exercising SQLite memory efficiency.
+
+## Runtime Lifecycle
+
+SQLite in-memory repositories use named database URLs scoped to a single `SqliteThingRepositoryProvider` lifetime. Each logical database key receives its own SQLite in-memory database name inside that provider namespace.
+
+The repository/provider owns the SQLite connection. `Thingifier.close()`, `EntityRelModel.close()`, `MainImplementation.close()`, and the shutdown route close repository providers so the in-memory database lifetime is explicit and connection-bound.
diff --git a/ercoremodel/pom.xml b/ercoremodel/pom.xml
index dd693d48..29250f06 100644
--- a/ercoremodel/pom.xml
+++ b/ercoremodel/pom.xml
@@ -46,6 +46,11 @@
${junit.jupiter.version}
test
+
+ org.xerial
+ sqlite-jdbc
+ ${sqlite-jdbc-version}
+
@@ -100,4 +105,4 @@
-
\ No newline at end of file
+
diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/EntityRelModel.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/EntityRelModel.java
index 5691b993..1e469f75 100644
--- a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/EntityRelModel.java
+++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/EntityRelModel.java
@@ -1,12 +1,16 @@
package uk.co.compendiumdev.thingifier.core;
import uk.co.compendiumdev.thingifier.core.domain.datapopulator.DataPopulator;
+import uk.co.compendiumdev.thingifier.core.domain.datapopulator.RepositoryDataPopulator;
import uk.co.compendiumdev.thingifier.core.domain.definitions.Cardinality;
import uk.co.compendiumdev.thingifier.core.domain.definitions.ERSchema;
import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
import uk.co.compendiumdev.thingifier.core.domain.definitions.relationship.RelationshipDefinition;
import uk.co.compendiumdev.thingifier.core.domain.instances.ERInstanceData;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
+import uk.co.compendiumdev.thingifier.core.repository.InMemoryThingRepositoryProvider;
+import uk.co.compendiumdev.thingifier.core.repository.ThingRepository;
+import uk.co.compendiumdev.thingifier.core.repository.ThingRepositoryProvider;
import java.util.*;
@@ -15,27 +19,39 @@
Schema and instances are separate to allow us to have multiple
'databases' in memory at the same time built from the same schema.
*/
-public class EntityRelModel {
+public class EntityRelModel implements AutoCloseable {
public static final String DEFAULT_DATABASE_NAME = "__default";
- // a Map so that key, database can be used
- // e.g. key from a 'session', or 'custom' or 'default'
- private final Map databases;
+ // a provider so that key, database can be backed by memory, files, SQLite, etc.
private final ERSchema schema; // all the definitions
+ private final ThingRepositoryProvider repositories;
private DataPopulator dataPopulator;
public EntityRelModel(){
schema = new ERSchema();
- databases = new HashMap();
- databases.put(DEFAULT_DATABASE_NAME, new ERInstanceData());
+ repositories = new InMemoryThingRepositoryProvider();
dataPopulator = null;
}
public EntityRelModel(final ERSchema schema, final ERInstanceData erInstanceData) {
this.schema = schema;
- this.databases = new HashMap();
- this.databases.put(DEFAULT_DATABASE_NAME,erInstanceData);
+ this.repositories = new InMemoryThingRepositoryProvider(erInstanceData);
+ dataPopulator = null;
+ }
+
+ public EntityRelModel(final ThingRepositoryProvider repositories) {
+ this.schema = new ERSchema();
+ this.repositories = repositories;
+ this.repositories.getDefaultRepository().initializeFrom(schema);
+ dataPopulator = null;
+ }
+
+ public EntityRelModel(final ERSchema schema, final ThingRepositoryProvider repositories) {
+ this.schema = schema;
+ this.repositories = repositories;
+ this.repositories.getDefaultRepository().initializeFrom(schema);
+ dataPopulator = null;
}
public EntityDefinition createEntityDefinition(final String entityName, final String pluralName) {
@@ -44,8 +60,8 @@ public EntityDefinition createEntityDefinition(final String entityName, final St
public EntityDefinition createEntityDefinition(final String entityName, final String pluralName, int maximumNumberOfInstances) {
EntityDefinition defn = schema.defineEntity(entityName, pluralName, maximumNumberOfInstances);
- for(ERInstanceData database : databases.values()){
- database.createInstanceCollectionFor(defn);
+ for(String databaseKey : repositories.getRepositoryNames()){
+ repositories.getRepository(databaseKey).createInstanceCollectionFor(defn);
}
return defn;
}
@@ -57,19 +73,36 @@ public ERSchema getSchema(){
// TODO: use of this is basically deprecated since is refers to the default database
@Deprecated() // we should use the parameterised version
public ERInstanceData getInstanceData(){
- return databases.get(DEFAULT_DATABASE_NAME);
+ return getInstanceData(DEFAULT_DATABASE_NAME);
}
public ERInstanceData getInstanceDataAsJson(){
- return databases.get(DEFAULT_DATABASE_NAME);
+ return getInstanceData(DEFAULT_DATABASE_NAME);
}
public ERInstanceData getInstanceData(String databaseKey) {
- return databases.get(databaseKey);
+ ThingRepository repository = repositories.getRepository(databaseKey);
+ if(repository==null){
+ return null;
+ }
+ return repository.getInstanceData();
}
public Set getDatabaseNames(){
- return databases.keySet();
+ return repositories.getRepositoryNames();
+ }
+
+ public ThingRepository getRepository(String databaseKey) {
+ return repositories.getRepository(databaseKey);
+ }
+
+ public ThingRepositoryProvider getRepositoryProvider() {
+ return repositories;
+ }
+
+ @Override
+ public void close() {
+ repositories.close();
}
// ERM Object Level
@@ -97,7 +130,9 @@ public Collection getEntityDefinitions() {
public RelationshipDefinition createRelationshipDefinition(
EntityDefinition from, EntityDefinition to, final String named, final Cardinality of) {
- return schema.defineRelationship(from, to, named, of);
+ RelationshipDefinition relationship = schema.defineRelationship(from, to, named, of);
+ refreshRepositorySchemas();
+ return relationship;
}
public boolean hasRelationshipNamed(final String relationshipName) {
@@ -120,7 +155,7 @@ public EntityDefinition getEntityDefinitionNamed(final String term){
// Multiple Databases
public void createInstanceDatabase(String databaseKey) {
- if(databases.containsKey(databaseKey)){
+ if(repositories.getRepository(databaseKey)!=null){
throw new IllegalStateException("ERM Database Already Exists with name " + databaseKey);
}
@@ -132,22 +167,16 @@ public void deleteInstanceDatabase(String databaseKey) {
if(databaseKey.equals(DEFAULT_DATABASE_NAME)){
throw new IllegalStateException("Cannot delete default database");
}
- databases.remove(databaseKey);
+ repositories.deleteRepository(databaseKey);
}
public boolean createInstanceDatabaseIfNotExisting(String databaseKey) {
- if(databases.containsKey(databaseKey)){
- return false;
- }
-
- ERInstanceData aDatabase = new ERInstanceData();
- aDatabase.createInstanceCollectionFrom(this.schema);
- databases.put(databaseKey, aDatabase);
- return true;
+ return repositories.createRepositoryIfNotExisting(databaseKey, this.schema);
}
public boolean populateDatabase(String databaseKey){
- if(!databases.containsKey(databaseKey)){
+ ThingRepository repository = repositories.getRepository(databaseKey);
+ if(repository==null){
return false;
}
@@ -155,10 +184,8 @@ public boolean populateDatabase(String databaseKey){
return false;
}
- dataPopulator.populate(
- getSchema(),
- getInstanceData(databaseKey)
- );
+ repository.refreshSchema(getSchema());
+ populateRepository(repository);
return true;
}
@@ -166,4 +193,20 @@ public boolean populateDatabase(String databaseKey){
public void setDataGenerator(DataPopulator dataPopulator) {
this.dataPopulator = dataPopulator;
}
+
+ private void refreshRepositorySchemas() {
+ for(String databaseKey : repositories.getRepositoryNames()){
+ repositories.getRepository(databaseKey).refreshSchema(schema);
+ }
+ }
+
+ private void populateRepository(final ThingRepository repository) {
+ if(dataPopulator instanceof RepositoryDataPopulator) {
+ ((RepositoryDataPopulator) dataPopulator).populate(getSchema(), repository);
+ return;
+ }
+
+ dataPopulator.populate(getSchema(), repository.getInstanceData());
+ repository.flush();
+ }
}
diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/datapopulator/RepositoryDataPopulator.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/datapopulator/RepositoryDataPopulator.java
new file mode 100644
index 00000000..8a30d2ff
--- /dev/null
+++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/datapopulator/RepositoryDataPopulator.java
@@ -0,0 +1,9 @@
+package uk.co.compendiumdev.thingifier.core.domain.datapopulator;
+
+import uk.co.compendiumdev.thingifier.core.domain.definitions.ERSchema;
+import uk.co.compendiumdev.thingifier.core.repository.ThingRepository;
+
+public interface RepositoryDataPopulator extends DataPopulator {
+
+ void populate(ERSchema schema, ThingRepository repository);
+}
diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/EntityInstance.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/EntityInstance.java
index 541b7cac..021d1339 100644
--- a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/EntityInstance.java
+++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/EntityInstance.java
@@ -20,10 +20,14 @@ public class EntityInstance {
private final UUID internalId;
public EntityInstance(EntityDefinition eDefn) {
+ this(eDefn, UUID.randomUUID());
+ }
+
+ public EntityInstance(EntityDefinition eDefn, UUID internalId) {
this.entityDefinition = eDefn;
this.instanceFields = eDefn.instantiateFields();
this.relationships = new EntityInstanceRelationships(this);
- internalId = UUID.randomUUID();
+ this.internalId = internalId;
}
public EntityInstance addAutoGUIDstoInstance(){
diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/EntityInstanceRelationships.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/EntityInstanceRelationships.java
index f30e34ab..517cc5c0 100644
--- a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/EntityInstanceRelationships.java
+++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/domain/instances/EntityInstanceRelationships.java
@@ -216,6 +216,10 @@ public boolean hasAnyRelationshipInstances() {
return !relationships.isEmpty();
}
+ public List getRelationshipInstances() {
+ return new ArrayList<>(relationships);
+ }
+
public ValidationReport validateRelationships() {
ValidationReport report = new ValidationReport();
diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/QueryResult.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/QueryResult.java
new file mode 100644
index 00000000..25fbbb81
--- /dev/null
+++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/QueryResult.java
@@ -0,0 +1,23 @@
+package uk.co.compendiumdev.thingifier.core.query;
+
+import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
+import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
+
+import java.util.List;
+
+public interface QueryResult {
+
+ boolean wasQueryIntendedToMatchAnInstance();
+
+ boolean isResultACollection();
+
+ List getListEntityInstances();
+
+ EntityInstance getLastInstance();
+
+ boolean lastMatchWasInstance();
+
+ boolean lastMatchWasNothing();
+
+ EntityDefinition resultContainsDefn();
+}
diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/RepositoryBackedSimpleQuery.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/RepositoryBackedSimpleQuery.java
new file mode 100644
index 00000000..2f1a35df
--- /dev/null
+++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/RepositoryBackedSimpleQuery.java
@@ -0,0 +1,171 @@
+package uk.co.compendiumdev.thingifier.core.query;
+
+import uk.co.compendiumdev.thingifier.core.domain.definitions.ERSchema;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
+import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
+import uk.co.compendiumdev.thingifier.core.repository.ThingRepository;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class RepositoryBackedSimpleQuery implements QueryResult {
+
+ private final ERSchema schema;
+ private final ThingRepository repository;
+ private final String query;
+
+ private boolean isCollection;
+ private boolean wasIntentToMatchInstance;
+ private boolean lastMatchWasInstance;
+ private boolean lastMatchWasNothing = true;
+ private EntityDefinition resultContainsDefinition;
+ private EntityInstance currentInstance;
+ private List foundItems = new ArrayList<>();
+
+ public RepositoryBackedSimpleQuery(
+ final ERSchema schema,
+ final ThingRepository repository,
+ final String query) {
+ this.schema = schema;
+ this.repository = repository;
+ if (query.startsWith("/")) {
+ this.query = query.substring(1);
+ } else {
+ this.query = query;
+ }
+ }
+
+ public static boolean canHandle(final ERSchema schema, final String query) {
+ String[] terms = termsFrom(query);
+ if (terms.length == 0 || terms.length > 3) {
+ return false;
+ }
+
+ EntityDefinition entity = entityForTerm(schema, terms[0]);
+ if (entity == null) {
+ return false;
+ }
+
+ if (terms.length == 1) {
+ return true;
+ }
+
+ String identifierCandidate = terms[1];
+ if (schema.hasRelationshipNamed(identifierCandidate) ||
+ entityForTerm(schema, identifierCandidate) != null) {
+ return false;
+ }
+
+ if (terms.length == 2) {
+ return true;
+ }
+
+ return entity.related().hasRelationship(terms[2]);
+ }
+
+ public RepositoryBackedSimpleQuery performQuery(final QueryFilterParams queryParams) {
+ String[] terms = termsFrom(query);
+ EntityDefinition entity = entityForTerm(schema, terms[0]);
+ resultContainsDefinition = entity;
+
+ if (terms.length == 1) {
+ isCollection = true;
+ foundItems = new ArrayList<>(repository.listInstances(entity, queryParams));
+ lastMatchWasNothing = false;
+ return this;
+ }
+
+ wasIntentToMatchInstance = true;
+ currentInstance = repository.findInstanceByQueryIdentifier(entity, terms[1]);
+ if (currentInstance == null) {
+ foundItems = new ArrayList<>();
+ lastMatchWasNothing = true;
+ lastMatchWasInstance = false;
+ return this;
+ }
+
+ if (terms.length == 3) {
+ wasIntentToMatchInstance = true;
+ isCollection = true;
+ foundItems = new ArrayList<>(
+ repository.listRelatedInstances(currentInstance, terms[2], queryParams));
+ resultContainsDefinition = relatedEntityFor(currentInstance, terms[2]);
+ lastMatchWasNothing = false;
+ lastMatchWasInstance = false;
+ return this;
+ }
+
+ foundItems = new ArrayList<>();
+ foundItems.add(currentInstance);
+ lastMatchWasNothing = false;
+ lastMatchWasInstance = true;
+ return this;
+ }
+
+ @Override
+ public boolean wasQueryIntendedToMatchAnInstance() {
+ return wasIntentToMatchInstance;
+ }
+
+ @Override
+ public boolean isResultACollection() {
+ return isCollection;
+ }
+
+ @Override
+ public List getListEntityInstances() {
+ return new ArrayList<>(foundItems);
+ }
+
+ @Override
+ public EntityInstance getLastInstance() {
+ return currentInstance;
+ }
+
+ @Override
+ public boolean lastMatchWasInstance() {
+ return lastMatchWasInstance;
+ }
+
+ @Override
+ public boolean lastMatchWasNothing() {
+ return lastMatchWasNothing;
+ }
+
+ @Override
+ public EntityDefinition resultContainsDefn() {
+ return resultContainsDefinition;
+ }
+
+ private static EntityDefinition entityForTerm(final ERSchema schema, final String term) {
+ if (schema.hasEntityNamed(term)) {
+ return schema.getEntityDefinitionNamed(term);
+ }
+ if (schema.hasEntityWithPluralNamed(term)) {
+ return schema.getEntityDefinitionWithPluralNamed(term);
+ }
+ return null;
+ }
+
+ private static String[] termsFrom(final String query) {
+ String normalized = query == null ? "" : query.trim();
+ while (normalized.startsWith("/")) {
+ normalized = normalized.substring(1);
+ }
+ while (normalized.endsWith("/")) {
+ normalized = normalized.substring(0, normalized.length() - 1);
+ }
+ if (normalized.isEmpty()) {
+ return new String[0];
+ }
+ return normalized.split("/");
+ }
+
+ private EntityDefinition relatedEntityFor(
+ final EntityInstance instance, final String relationshipName) {
+ if (instance.getEntity().related().getRelationships(relationshipName).isEmpty()) {
+ return null;
+ }
+ return instance.getEntity().related().getRelationships(relationshipName).get(0).getTo();
+ }
+}
diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/SimpleQuery.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/SimpleQuery.java
index 561cc051..c43b34d2 100644
--- a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/SimpleQuery.java
+++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/SimpleQuery.java
@@ -14,18 +14,25 @@
import static uk.co.compendiumdev.thingifier.core.query.SimpleQuery.LastMatchValue.*;
-/*
-Note this is not the same as a GET e.g.
-- /item will always mark the query as not a collection
-- /items will always mark the query as a collection
-
-This is a simple query to then build more complex or specific query
-processing on top.
-
-Use the isResultACollection, wasIntentToMatchACollection, lastMatchWasInstance
-in the calling method.
+/**
+ * Legacy ERInstanceData-backed query engine.
+ *
+ * Note this is not the same as a GET e.g.
+ * - /item will always mark the query as not a collection
+ * - /items will always mark the query as a collection
+ *
+ * This is a simple query to then build more complex or specific query
+ * processing on top.
+ *
+ * Use the isResultACollection, wasIntentToMatchACollection, lastMatchWasInstance
+ * in the calling method.
+ *
+ * @deprecated Runtime API paths should use repository-backed query resolution so
+ * repositories such as SQLite can query without hydrating compatibility snapshots.
+ * Keep this class only for compatibility/fallback paths until replacement is complete.
*/
-public final class SimpleQuery {
+@Deprecated
+public final class SimpleQuery implements QueryResult {
private final ERInstanceData database;
private final ERSchema schema; // all the definitions
@@ -229,10 +236,12 @@ public SimpleQuery performQuery() {
}
// i.e. did the query end with an identifier which was a primary key
+ @Override
public boolean wasQueryIntendedToMatchAnInstance(){
return wasIntentToMatchInstance;
}
+ @Override
public boolean isResultACollection() {
return isCollection;
}
@@ -259,6 +268,7 @@ public SimpleQuery performQuery(final QueryFilterParams queryParams) {
return this;
}
+ @Override
public List getListEntityInstances() {
List returnThis = new ArrayList<>();
@@ -303,18 +313,22 @@ public boolean wasItemFoundUnderARelationship() {
return foundItemsHistoryList.get(foundItemsHistoryList.size() - 2) instanceof RelationshipVectorDefinition;
}
+ @Override
public EntityInstance getLastInstance() {
return currentInstance;
}
+ @Override
public boolean lastMatchWasInstance() {
return lastMatch == CURRENT_INSTANCE;
}
+ @Override
public boolean lastMatchWasNothing() {
return lastMatch == NOTHING;
}
+ @Override
public EntityDefinition resultContainsDefn() {
return resultContainsDefinition;
}
diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/SortByFieldName.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/SortByFieldName.java
index 541dae46..c4fd773b 100644
--- a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/SortByFieldName.java
+++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/query/SortByFieldName.java
@@ -4,6 +4,14 @@ public class SortByFieldName {
int order = 1;
String fieldName = "";
+ public int getOrder() {
+ return order;
+ }
+
+ public String getFieldName() {
+ return fieldName;
+ }
+
public static boolean isSortByParam(final String key) {
return (key.equalsIgnoreCase("sortby") ||
key.equalsIgnoreCase("sort_by"));
diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/InMemoryThingRepository.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/InMemoryThingRepository.java
new file mode 100644
index 00000000..d5c05165
--- /dev/null
+++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/InMemoryThingRepository.java
@@ -0,0 +1,235 @@
+package uk.co.compendiumdev.thingifier.core.repository;
+
+import uk.co.compendiumdev.thingifier.core.domain.definitions.ERSchema;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue;
+import uk.co.compendiumdev.thingifier.core.domain.instances.AutoIncrement;
+import uk.co.compendiumdev.thingifier.core.domain.instances.ERInstanceData;
+import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
+import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceCollection;
+import uk.co.compendiumdev.thingifier.core.query.EntityInstanceListFilter;
+import uk.co.compendiumdev.thingifier.core.query.EntityInstanceListSorter;
+import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams;
+import uk.co.compendiumdev.thingifier.core.reporting.ValidationReport;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public class InMemoryThingRepository implements ThingRepository {
+
+ private final String databaseKey;
+ private final ERInstanceData instanceData;
+
+ public InMemoryThingRepository(final String databaseKey) {
+ this(databaseKey, new ERInstanceData());
+ }
+
+ public InMemoryThingRepository(final String databaseKey, final ERInstanceData instanceData) {
+ this.databaseKey = databaseKey;
+ this.instanceData = instanceData;
+ }
+
+ @Override
+ public String databaseKey() {
+ return databaseKey;
+ }
+
+ @Override
+ public void initializeFrom(final ERSchema schema) {
+ refreshSchema(schema);
+ }
+
+ @Override
+ public void refreshSchema(final ERSchema schema) {
+ createInstanceCollectionsFrom(schema);
+ }
+
+ @Override
+ public ERInstanceData getInstanceData() {
+ return instanceData;
+ }
+
+ @Override
+ public EntityInstanceCollection createInstanceCollectionFor(final EntityDefinition definition) {
+ EntityInstanceCollection existing = getInstanceCollectionForEntityNamed(definition.getName());
+ if (existing != null) {
+ return existing;
+ }
+ return instanceData.createInstanceCollectionFor(definition);
+ }
+
+ @Override
+ public void createInstanceCollectionsFrom(final ERSchema schema) {
+ for (EntityDefinition defn : schema.getEntityDefinitions()) {
+ createInstanceCollectionFor(defn);
+ }
+ }
+
+ @Override
+ public List getAllInstanceCollections() {
+ return instanceData.getAllInstanceCollections();
+ }
+
+ @Override
+ public EntityInstanceCollection getInstanceCollectionForEntityNamed(final String entityName) {
+ return instanceData.getInstanceCollectionForEntityNamed(entityName);
+ }
+
+ @Override
+ public EntityInstance findEntityInstanceByGUID(final String thingGUID) {
+ return instanceData.findEntityInstanceByGUID(thingGUID);
+ }
+
+ @Override
+ public EntityInstance findInstanceByPrimaryKey(
+ final EntityDefinition entity, final String primaryKeyValue) {
+ EntityInstanceCollection collection = getInstanceCollectionForEntityNamed(entity.getName());
+ if (collection == null) {
+ return null;
+ }
+ return collection.findInstanceByPrimaryKey(primaryKeyValue);
+ }
+
+ @Override
+ public EntityInstance findInstanceByFieldNameAndValue(
+ final EntityDefinition entity, final String fieldName, final String fieldValue) {
+ EntityInstanceCollection collection = getInstanceCollectionForEntityNamed(entity.getName());
+ if (collection == null) {
+ return null;
+ }
+ return collection.findInstanceByFieldNameAndValue(fieldName, fieldValue);
+ }
+
+ @Override
+ public Collection listInstances(final EntityDefinition entity) {
+ EntityInstanceCollection collection = getInstanceCollectionForEntityNamed(entity.getName());
+ if (collection == null) {
+ return List.of();
+ }
+ return collection.getInstances();
+ }
+
+ @Override
+ public List listInstances(
+ final EntityDefinition entity, final QueryFilterParams queryParams) {
+ List instances = new ArrayList<>(listInstances(entity));
+ QueryFilterParams params = queryParams == null ? new QueryFilterParams() : queryParams;
+ instances = new EntityInstanceListFilter(params).filter(instances);
+ return new EntityInstanceListSorter(params).sort(instances);
+ }
+
+ @Override
+ public EntityInstance findInstanceByQueryIdentifier(
+ final EntityDefinition entity, final String identifier) {
+ EntityInstanceCollection collection = getInstanceCollectionForEntityNamed(entity.getName());
+ if (collection == null) {
+ return null;
+ }
+
+ for (EntityInstance instance : collection.getInstances()) {
+ if (matchesQueryIdentifier(instance, identifier)) {
+ return instance;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public EntityInstance addInstance(final EntityInstance instance) {
+ getInstanceCollectionForEntityNamed(instance.getEntity().getName()).addInstance(instance);
+ return instance;
+ }
+
+ @Override
+ public EntityInstance updateInstance(final EntityInstance instance) {
+ return instance;
+ }
+
+ @Override
+ public void deleteEntityInstance(final EntityInstance instance) {
+ instanceData.deleteEntityInstance(instance);
+ }
+
+ @Override
+ public void clearAllData() {
+ instanceData.clearAllData();
+ }
+
+ @Override
+ public void clearInstanceDataFor(final String entityName) {
+ instanceData.clearInstanceDataFor(entityName);
+ }
+
+ @Override
+ public ValidationReport checkFieldsForUniqueNess(
+ final EntityInstance instance, final boolean isAmendment) {
+ return getInstanceCollectionForEntityNamed(instance.getEntity().getName()).
+ checkFieldsForUniqueNess(instance, isAmendment);
+ }
+
+ @Override
+ public Map countersFor(final EntityDefinition entity) {
+ return getInstanceCollectionForEntityNamed(entity.getName()).getCounters();
+ }
+
+ @Override
+ public void setNextIdCountersToAccomodate(
+ final EntityDefinition entity, final List fieldValues) {
+ getInstanceCollectionForEntityNamed(entity.getName()).
+ setNextIdCountersToAccomodate(fieldValues);
+ }
+
+ @Override
+ public void connectRelationship(
+ final EntityInstance from, final String relationshipName, final EntityInstance to) {
+ from.getRelationships().connect(relationshipName, to);
+ }
+
+ @Override
+ public List removeRelationshipsInvolving(
+ final EntityInstance parent,
+ final EntityInstance child,
+ final String relationshipName) {
+ return parent.getRelationships().removeRelationshipsInvolving(child, relationshipName);
+ }
+
+ @Override
+ public List removeAllRelationships(final EntityInstance instance) {
+ return instance.getRelationships().removeAllRelationships();
+ }
+
+ @Override
+ public Collection getConnectedItems(
+ final EntityInstance instance, final String relationshipName) {
+ return instance.getRelationships().getConnectedItems(relationshipName);
+ }
+
+ @Override
+ public List listRelatedInstances(
+ final EntityInstance instance,
+ final String relationshipName,
+ final QueryFilterParams queryParams) {
+ QueryFilterParams params = queryParams == null ? new QueryFilterParams() : queryParams;
+ List instances = new ArrayList<>(
+ getConnectedItems(instance, relationshipName));
+ instances = new EntityInstanceListFilter(params).filter(instances);
+ return new EntityInstanceListSorter(params).sort(instances);
+ }
+
+ private boolean matchesQueryIdentifier(final EntityInstance instance, final String identifier) {
+ for (Field autoIncrementField : instance.getEntity().getFieldsOfType(FieldType.AUTO_INCREMENT)) {
+ String idValue = instance.getFieldValue(autoIncrementField.getName()).asString();
+ if (idValue.contentEquals(identifier)) {
+ return true;
+ }
+ break;
+ }
+
+ String primaryKeyValue = instance.getPrimaryKeyValue();
+ return primaryKeyValue != null && primaryKeyValue.contentEquals(identifier);
+ }
+}
diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/InMemoryThingRepositoryProvider.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/InMemoryThingRepositoryProvider.java
new file mode 100644
index 00000000..a44e07c9
--- /dev/null
+++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/InMemoryThingRepositoryProvider.java
@@ -0,0 +1,72 @@
+package uk.co.compendiumdev.thingifier.core.repository;
+
+import uk.co.compendiumdev.thingifier.core.EntityRelModel;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.ERSchema;
+import uk.co.compendiumdev.thingifier.core.domain.instances.ERInstanceData;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class InMemoryThingRepositoryProvider implements ThingRepositoryProvider {
+
+ private final Map repositories;
+
+ public InMemoryThingRepositoryProvider() {
+ this(new InMemoryThingRepository(EntityRelModel.DEFAULT_DATABASE_NAME));
+ }
+
+ public InMemoryThingRepositoryProvider(final ERInstanceData defaultInstanceData) {
+ this(new InMemoryThingRepository(EntityRelModel.DEFAULT_DATABASE_NAME, defaultInstanceData));
+ }
+
+ public InMemoryThingRepositoryProvider(final ThingRepository defaultRepository) {
+ repositories = new HashMap<>();
+ repositories.put(EntityRelModel.DEFAULT_DATABASE_NAME, defaultRepository);
+ }
+
+ @Override
+ public ThingRepository getDefaultRepository() {
+ return repositories.get(EntityRelModel.DEFAULT_DATABASE_NAME);
+ }
+
+ @Override
+ public ThingRepository getRepository(final String databaseKey) {
+ return repositories.get(databaseKey);
+ }
+
+ @Override
+ public Set getRepositoryNames() {
+ return new HashSet<>(repositories.keySet());
+ }
+
+ @Override
+ public ThingRepository createRepository(final String databaseKey, final ERSchema schema) {
+ if (repositories.containsKey(databaseKey)) {
+ throw new IllegalStateException("ERM Database Already Exists with name " + databaseKey);
+ }
+
+ ThingRepository repository = new InMemoryThingRepository(databaseKey);
+ repository.initializeFrom(schema);
+ repositories.put(databaseKey, repository);
+ return repository;
+ }
+
+ @Override
+ public boolean createRepositoryIfNotExisting(final String databaseKey, final ERSchema schema) {
+ if (repositories.containsKey(databaseKey)) {
+ return false;
+ }
+
+ ThingRepository repository = new InMemoryThingRepository(databaseKey);
+ repository.initializeFrom(schema);
+ repositories.put(databaseKey, repository);
+ return true;
+ }
+
+ @Override
+ public void deleteRepository(final String databaseKey) {
+ repositories.remove(databaseKey);
+ }
+}
diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/SqliteRegexFilterPolicy.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/SqliteRegexFilterPolicy.java
new file mode 100644
index 00000000..07f1bff3
--- /dev/null
+++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/SqliteRegexFilterPolicy.java
@@ -0,0 +1,44 @@
+package uk.co.compendiumdev.thingifier.core.repository;
+
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+final class SqliteRegexFilterPolicy {
+
+ private static final int MAX_REGEX_LENGTH = 512;
+ private static final Pattern NESTED_QUANTIFIER =
+ Pattern.compile("\\((?:\\\\.|[^\\\\)])*[+*](?:\\\\.|[^\\\\)])*\\)\\s*[+*?{]");
+
+ private SqliteRegexFilterPolicy() {
+ }
+
+ static Pattern compileSupported(final String regex) {
+ if (regex == null) {
+ throw new UnsupportedRegexFilterException("Regex value is null");
+ }
+ if (regex.length() > MAX_REGEX_LENGTH) {
+ throw new UnsupportedRegexFilterException(
+ "Regex exceeds maximum supported length of " + MAX_REGEX_LENGTH);
+ }
+ if (NESTED_QUANTIFIER.matcher(regex).find()) {
+ throw new UnsupportedRegexFilterException(
+ "Regex uses nested quantifiers that are not allowed for SQLite filtering");
+ }
+
+ try {
+ return Pattern.compile(regex);
+ } catch (PatternSyntaxException e) {
+ throw new UnsupportedRegexFilterException("Invalid regex", e);
+ }
+ }
+
+ static final class UnsupportedRegexFilterException extends RuntimeException {
+ UnsupportedRegexFilterException(final String message) {
+ super(message);
+ }
+
+ UnsupportedRegexFilterException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+ }
+}
diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/SqliteRegexToLikeConverter.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/SqliteRegexToLikeConverter.java
new file mode 100644
index 00000000..416913ca
--- /dev/null
+++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/SqliteRegexToLikeConverter.java
@@ -0,0 +1,156 @@
+package uk.co.compendiumdev.thingifier.core.repository;
+
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+final class SqliteRegexToLikeConverter {
+
+ private SqliteRegexToLikeConverter() {
+ }
+
+ static Conversion convert(final String regex) {
+ validateRegex(regex);
+
+ String pattern = regex;
+ if (pattern.startsWith("^")) {
+ pattern = pattern.substring(1);
+ }
+ if (hasTrailingUnescapedDollar(pattern)) {
+ pattern = pattern.substring(0, pattern.length() - 1);
+ }
+
+ StringBuilder exactValue = new StringBuilder();
+ StringBuilder likeValue = new StringBuilder();
+ boolean usesLikeWildcard = false;
+
+ for (int index = 0; index < pattern.length(); index++) {
+ char current = pattern.charAt(index);
+
+ if (current == '\\') {
+ if (index == pattern.length() - 1) {
+ throw new RegexToLikeConversionException("Trailing regex escape");
+ }
+ char escaped = pattern.charAt(++index);
+ if (isUnsupportedEscape(escaped)) {
+ throw new RegexToLikeConversionException("Unsupported regex escape");
+ }
+ appendLiteral(exactValue, likeValue, escaped);
+ continue;
+ }
+
+ if (current == '.') {
+ if (index < pattern.length() - 1 && pattern.charAt(index + 1) == '*') {
+ likeValue.append('%');
+ usesLikeWildcard = true;
+ index++;
+ } else {
+ likeValue.append('_');
+ usesLikeWildcard = true;
+ }
+ continue;
+ }
+
+ if (isUnsupportedRegexMeta(current)) {
+ throw new RegexToLikeConversionException("Unsupported regex operator");
+ }
+
+ appendLiteral(exactValue, likeValue, current);
+ }
+
+ if (usesLikeWildcard) {
+ return Conversion.like(likeValue.toString());
+ }
+ return Conversion.equalTo(exactValue.toString());
+ }
+
+ private static void validateRegex(final String regex) {
+ if (regex == null) {
+ throw new RegexToLikeConversionException("Regex value is null");
+ }
+ try {
+ Pattern.compile(regex);
+ } catch (PatternSyntaxException e) {
+ throw new RegexToLikeConversionException("Invalid regex", e);
+ }
+ }
+
+ private static void appendLiteral(
+ final StringBuilder exactValue,
+ final StringBuilder likeValue,
+ final char literal) {
+ exactValue.append(literal);
+ if (literal == '\\' || literal == '%' || literal == '_') {
+ likeValue.append('\\');
+ }
+ likeValue.append(literal);
+ }
+
+ private static boolean isUnsupportedEscape(final char escaped) {
+ return Character.isLetterOrDigit(escaped);
+ }
+
+ private static boolean isUnsupportedRegexMeta(final char current) {
+ return current == '[' ||
+ current == ']' ||
+ current == '(' ||
+ current == ')' ||
+ current == '{' ||
+ current == '}' ||
+ current == '+' ||
+ current == '?' ||
+ current == '*' ||
+ current == '|' ||
+ current == '^' ||
+ current == '$';
+ }
+
+ private static boolean hasTrailingUnescapedDollar(final String pattern) {
+ if (pattern.isEmpty() || pattern.charAt(pattern.length() - 1) != '$') {
+ return false;
+ }
+
+ int backslashes = 0;
+ for (int index = pattern.length() - 2;
+ index >= 0 && pattern.charAt(index) == '\\';
+ index--) {
+ backslashes++;
+ }
+ return backslashes % 2 == 0;
+ }
+
+ static final class Conversion {
+ private final boolean equality;
+ private final String sqlValue;
+
+ private Conversion(final boolean equality, final String sqlValue) {
+ this.equality = equality;
+ this.sqlValue = sqlValue;
+ }
+
+ static Conversion equalTo(final String sqlValue) {
+ return new Conversion(true, sqlValue);
+ }
+
+ static Conversion like(final String sqlValue) {
+ return new Conversion(false, sqlValue);
+ }
+
+ boolean isEquality() {
+ return equality;
+ }
+
+ String sqlValue() {
+ return sqlValue;
+ }
+ }
+
+ static final class RegexToLikeConversionException extends RuntimeException {
+ RegexToLikeConversionException(final String message) {
+ super(message);
+ }
+
+ RegexToLikeConversionException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+ }
+}
diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/SqliteThingRepository.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/SqliteThingRepository.java
new file mode 100644
index 00000000..624feceb
--- /dev/null
+++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/SqliteThingRepository.java
@@ -0,0 +1,1161 @@
+package uk.co.compendiumdev.thingifier.core.repository;
+
+import org.sqlite.Function;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.ERSchema;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.FieldValue;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.relationship.RelationshipDefinition;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.relationship.RelationshipVectorDefinition;
+import uk.co.compendiumdev.thingifier.core.domain.instances.AutoIncrement;
+import uk.co.compendiumdev.thingifier.core.domain.instances.ERInstanceData;
+import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
+import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceCollection;
+import uk.co.compendiumdev.thingifier.core.domain.instances.RelationshipVectorInstance;
+import uk.co.compendiumdev.thingifier.core.query.EntityInstanceListSorter;
+import uk.co.compendiumdev.thingifier.core.query.EntityListSortParamParser;
+import uk.co.compendiumdev.thingifier.core.query.FilterBy;
+import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams;
+import uk.co.compendiumdev.thingifier.core.query.SortByFieldName;
+import uk.co.compendiumdev.thingifier.core.reporting.ValidationReport;
+
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+public class SqliteThingRepository extends InMemoryThingRepository {
+
+ private static final Logger LOGGER =
+ Logger.getLogger(SqliteThingRepository.class.getName());
+ private static final String INTERNAL_ID_COLUMN = "__internal_id";
+ private static final String COUNTERS_TABLE = "__thingifier_counters";
+
+ private final String jdbcUrl;
+ private ERSchema schema;
+ private Connection connection;
+ private boolean legacySnapshotLoaded;
+ private boolean closed;
+
+ public SqliteThingRepository(final String databaseKey, final String jdbcUrl) {
+ super(databaseKey, new ERInstanceData());
+ this.jdbcUrl = jdbcUrl;
+ }
+
+ public static SqliteThingRepository inMemory(final String databaseKey) {
+ return new SqliteThingRepository(databaseKey, "jdbc:sqlite::memory:");
+ }
+
+ public static SqliteThingRepository fileBacked(final String databaseKey, final Path databasePath) {
+ String path = databasePath.toAbsolutePath().toString().replace("\\", "/");
+ return new SqliteThingRepository(databaseKey, "jdbc:sqlite:" + path);
+ }
+
+ @Override
+ public void initializeFrom(final ERSchema schema) {
+ this.schema = schema;
+ openConnection();
+ createMetadataTables();
+ refreshSchema(schema);
+ }
+
+ @Override
+ public ERInstanceData getInstanceData() {
+ if (!legacySnapshotLoaded) {
+ loadExistingData();
+ legacySnapshotLoaded = true;
+ }
+ return super.getInstanceData();
+ }
+
+ @Override
+ public boolean hasLoadedCompatibilitySnapshot() {
+ return legacySnapshotLoaded;
+ }
+
+ @Override
+ public void refreshSchema(final ERSchema schema) {
+ this.schema = schema;
+ openConnection();
+ super.refreshSchema(schema);
+ for (EntityDefinition entity : schema.getEntityDefinitions()) {
+ createEntityTable(entity);
+ }
+ for (RelationshipVectorDefinition vector : relationshipVectors()) {
+ createRelationshipTable(vector);
+ }
+ }
+
+ @Override
+ public EntityInstanceCollection createInstanceCollectionFor(final EntityDefinition definition) {
+ EntityInstanceCollection collection = super.createInstanceCollectionFor(definition);
+ if (connection != null) {
+ createEntityTable(definition);
+ }
+ return collection;
+ }
+
+ @Override
+ public EntityInstance findEntityInstanceByGUID(final String thingGUID) {
+ ensureSchemaReady();
+ for (EntityDefinition entity : schema.getEntityDefinitions()) {
+ for (Field guidField : entity.getFieldsOfType(FieldType.AUTO_GUID)) {
+ EntityInstance instance = findInstanceByFieldNameAndValue(
+ entity, guidField.getName(), thingGUID);
+ if (instance != null) {
+ return instance;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public EntityInstance findInstanceByPrimaryKey(
+ final EntityDefinition entity, final String primaryKeyValue) {
+ if (!entity.hasPrimaryKeyField()) {
+ return null;
+ }
+ return findInstanceByFieldNameAndValue(
+ entity, entity.getPrimaryKeyField().getName(), primaryKeyValue);
+ }
+
+ @Override
+ public EntityInstance findInstanceByFieldNameAndValue(
+ final EntityDefinition entity, final String fieldName, final String fieldValue) {
+ ensureSchemaReady();
+ if (!entity.hasFieldNameDefined(fieldName)) {
+ return null;
+ }
+
+ String sql = "SELECT * FROM " + table(entity) +
+ " WHERE " + identifier(fieldName) + " = ? LIMIT 1";
+ List instances = queryEntityRows(entity, sql, List.of(fieldValue));
+ if (instances.isEmpty()) {
+ return null;
+ }
+ return instances.get(0);
+ }
+
+ @Override
+ public Collection listInstances(final EntityDefinition entity) {
+ return listInstances(entity, new QueryFilterParams());
+ }
+
+ @Override
+ public List listInstances(
+ final EntityDefinition entity, final QueryFilterParams queryParams) {
+ ensureSchemaReady();
+ QueryFilterParams params = queryParams == null ? new QueryFilterParams() : queryParams;
+
+ SqlQuery sqlQuery = selectInstancesSql(entity, params);
+ return queryEntityRows(entity, sqlQuery.sql, sqlQuery.parameters);
+ }
+
+ @Override
+ public EntityInstance findInstanceByQueryIdentifier(
+ final EntityDefinition entity, final String identifierValue) {
+ ensureSchemaReady();
+ List clauses = new ArrayList<>();
+ List parameters = new ArrayList<>();
+
+ for (Field autoIncrementField : entity.getFieldsOfType(FieldType.AUTO_INCREMENT)) {
+ clauses.add(identifier(autoIncrementField.getName()) + " = ?");
+ parameters.add(identifierValue);
+ break;
+ }
+
+ if (entity.hasPrimaryKeyField()) {
+ clauses.add(identifier(entity.getPrimaryKeyField().getName()) + " = ?");
+ parameters.add(identifierValue);
+ }
+
+ if (clauses.isEmpty()) {
+ return null;
+ }
+
+ String sql = "SELECT * FROM " + table(entity) +
+ " WHERE " + String.join(" OR ", clauses) +
+ " LIMIT 1";
+ List instances = queryEntityRows(entity, sql, parameters);
+ if (instances.isEmpty()) {
+ return null;
+ }
+ return instances.get(0);
+ }
+
+ @Override
+ public EntityInstance addInstance(final EntityInstance instance) {
+ ensureSchemaReady();
+ runInTransaction(() -> {
+ super.addInstance(instance);
+ persistInstance(instance);
+ persistCountersFor(instance.getEntity());
+ });
+ return instance;
+ }
+
+ @Override
+ public EntityInstance updateInstance(final EntityInstance instance) {
+ ensureSchemaReady();
+ runInTransaction(() -> {
+ persistInstance(instance);
+ persistCountersFor(instance.getEntity());
+ });
+ return instance;
+ }
+
+ @Override
+ public void deleteEntityInstance(final EntityInstance instance) {
+ ensureSchemaReady();
+ if (!legacySnapshotLoaded) {
+ runInTransaction(() -> {
+ super.deleteEntityInstance(instance);
+ deletePersistedInstance(instance.getInternalId());
+ });
+ return;
+ }
+
+ Set idsBeforeDelete = currentInternalIds();
+ super.deleteEntityInstance(instance);
+ Set idsAfterDelete = currentInternalIds();
+
+ runInTransaction(() -> {
+ idsBeforeDelete.removeAll(idsAfterDelete);
+ for (String internalId : idsBeforeDelete) {
+ deletePersistedInstance(internalId);
+ }
+ replaceAllRelationshipRowsFromCache();
+ });
+ }
+
+ @Override
+ public void clearAllData() {
+ ensureSchemaReady();
+ super.clearAllData();
+ runInTransaction(() -> {
+ for (EntityDefinition entity : schema.getEntityDefinitions()) {
+ executeSql("DELETE FROM " + table(entity));
+ persistCountersFor(entity);
+ }
+ clearRelationshipRows();
+ });
+ }
+
+ @Override
+ public void clearInstanceDataFor(final String entityName) {
+ ensureSchemaReady();
+ EntityInstanceCollection collection = getInstanceCollectionForEntityNamed(entityName);
+ if (collection == null) {
+ return;
+ }
+ super.clearInstanceDataFor(entityName);
+ runInTransaction(() -> {
+ executeSql("DELETE FROM " + table(collection.definition()));
+ persistCountersFor(collection.definition());
+ replaceAllRelationshipRowsFromCache();
+ });
+ }
+
+ @Override
+ public void connectRelationship(
+ final EntityInstance from, final String relationshipName, final EntityInstance to) {
+ ensureSchemaReady();
+ super.connectRelationship(from, relationshipName, to);
+ runInTransaction(() -> persistRelationship(from, relationshipName, to));
+ }
+
+ @Override
+ public List removeRelationshipsInvolving(
+ final EntityInstance parent,
+ final EntityInstance child,
+ final String relationshipName) {
+ ensureSchemaReady();
+ List removed = super.removeRelationshipsInvolving(parent, child, relationshipName);
+ runInTransaction(() -> deleteRelationshipRowsInvolving(parent, child, relationshipName));
+ return removed;
+ }
+
+ @Override
+ public List removeAllRelationships(final EntityInstance instance) {
+ ensureSchemaReady();
+ List removed = super.removeAllRelationships(instance);
+ runInTransaction(() -> deleteRelationshipRowsInvolving(instance));
+ return removed;
+ }
+
+ @Override
+ public Collection getConnectedItems(
+ final EntityInstance instance, final String relationshipName) {
+ ensureSchemaReady();
+ Set connected = new HashSet<>();
+
+ for (RelationshipVectorDefinition vector :
+ instance.getEntity().related().getRelationships(relationshipName)) {
+ addConnectedItemsFromVectorTable(connected, instance, vector);
+ if (vector.getRelationshipDefinition().isTwoWay()) {
+ RelationshipVectorDefinition otherVector =
+ vector.getRelationshipDefinition().otherVectorOf(vector);
+ if (otherVector != null) {
+ addConnectedItemsFromVectorTable(connected, instance, otherVector);
+ }
+ }
+ }
+
+ if (connected.isEmpty() && legacySnapshotLoaded) {
+ return super.getConnectedItems(instance, relationshipName);
+ }
+ return connected;
+ }
+
+ @Override
+ public List listRelatedInstances(
+ final EntityInstance instance,
+ final String relationshipName,
+ final QueryFilterParams queryParams) {
+ ensureSchemaReady();
+ QueryFilterParams params = queryParams == null ? new QueryFilterParams() : queryParams;
+
+ Map related = new LinkedHashMap<>();
+ for (RelationshipVectorDefinition vector :
+ relationshipVectorsFor(instance, relationshipName)) {
+ SqlQuery query = selectRelatedInstancesSql(instance, vector, params);
+ if (query == null) {
+ continue;
+ }
+ for (EntityInstance item : queryEntityRows(query.entity, query.sql, query.parameters)) {
+ related.put(item.getInternalId(), item);
+ }
+ }
+
+ return new EntityInstanceListSorter(params).
+ sort(new ArrayList<>(related.values()));
+ }
+
+ @Override
+ public void flush() {
+ if (!legacySnapshotLoaded) {
+ return;
+ }
+ refreshSchema(schema);
+ runInTransaction(() -> {
+ for (EntityDefinition entity : schema.getEntityDefinitions()) {
+ executeSql("DELETE FROM " + table(entity));
+ }
+ clearRelationshipRows();
+
+ for (EntityInstanceCollection collection : getAllInstanceCollections()) {
+ for (EntityInstance instance : collection.getInstances()) {
+ persistInstance(instance);
+ }
+ persistCountersFor(collection.definition());
+ }
+
+ replaceAllRelationshipRowsFromCache();
+ });
+ }
+
+ @Override
+ public void close() {
+ closed = true;
+ if (connection == null) {
+ return;
+ }
+ try {
+ connection.close();
+ connection = null;
+ } catch (SQLException e) {
+ throw new IllegalStateException("Could not close SQLite repository", e);
+ }
+ }
+
+ private void openConnection() {
+ if (closed) {
+ throw new IllegalStateException("SQLite repository has been closed");
+ }
+ if (connection != null) {
+ return;
+ }
+ try {
+ Class.forName("org.sqlite.JDBC");
+ connection = DriverManager.getConnection(jdbcUrl);
+ configureConnection();
+ } catch (ClassNotFoundException e) {
+ throw new IllegalStateException(
+ "SQLite repository requires org.xerial:sqlite-jdbc on the runtime classpath", e);
+ } catch (SQLException e) {
+ throw new IllegalStateException("Could not open SQLite repository " + jdbcUrl, e);
+ }
+ }
+
+ private void configureConnection() {
+ registerRegexpFunction();
+ executeSql("PRAGMA case_sensitive_like = ON");
+ }
+
+ private void registerRegexpFunction() {
+ try {
+ Function.create(
+ connection,
+ "regexp",
+ new JavaRegexpFunction(),
+ 2,
+ Function.FLAG_DETERMINISTIC);
+ } catch (SQLException e) {
+ throw new IllegalStateException("Could not register SQLite REGEXP function", e);
+ }
+ }
+
+ private void ensureSchemaReady() {
+ if (schema != null) {
+ refreshSchema(schema);
+ }
+ }
+
+ private void createMetadataTables() {
+ executeSql(
+ "CREATE TABLE IF NOT EXISTS " + identifier(COUNTERS_TABLE) + " (" +
+ "entity_name TEXT NOT NULL, " +
+ "field_name TEXT NOT NULL, " +
+ "next_value INTEGER NOT NULL, " +
+ "PRIMARY KEY(entity_name, field_name))");
+ }
+
+ private void createEntityTable(final EntityDefinition entity) {
+ executeSql(
+ "CREATE TABLE IF NOT EXISTS " + table(entity) + " (" +
+ identifier(INTERNAL_ID_COLUMN) + " TEXT PRIMARY KEY)");
+
+ Set existingColumns = columnNames(entityTableName(entity));
+ for (String fieldName : entity.getFieldNames()) {
+ if (!existingColumns.contains(fieldName)) {
+ Field field = entity.getField(fieldName);
+ executeSql(
+ "ALTER TABLE " + table(entity) + " ADD COLUMN " +
+ identifier(fieldName) + " " + sqlType(field.getType()));
+ }
+ }
+
+ if (entity.hasPrimaryKeyField()) {
+ createUniqueIndex(entity, entity.getPrimaryKeyField());
+ }
+ for (String fieldName : entity.getFieldNames()) {
+ Field field = entity.getField(fieldName);
+ if (field.mustBeUnique()) {
+ createUniqueIndex(entity, field);
+ }
+ }
+ }
+
+ private void createUniqueIndex(final EntityDefinition entity, final Field field) {
+ String indexName = "__thingifier_idx_" +
+ Integer.toHexString((entity.getName() + "_" + field.getName()).hashCode());
+ executeSql(
+ "CREATE UNIQUE INDEX IF NOT EXISTS " + identifier(indexName) +
+ " ON " + table(entity) + " (" + identifier(field.getName()) + ")");
+ }
+
+ private void createRelationshipTable(final RelationshipVectorDefinition vector) {
+ executeSql(
+ "CREATE TABLE IF NOT EXISTS " + relationshipTable(vector) + " (" +
+ "from_internal_id TEXT NOT NULL, " +
+ "to_internal_id TEXT NOT NULL, " +
+ "PRIMARY KEY(from_internal_id, to_internal_id))");
+ createRelationshipIndex(vector, "from_internal_id");
+ createRelationshipIndex(vector, "to_internal_id");
+ }
+
+ private void createRelationshipIndex(
+ final RelationshipVectorDefinition vector, final String columnName) {
+ String indexName = "__thingifier_rel_idx_" +
+ Integer.toHexString((relationshipTableName(vector) + "_" + columnName).hashCode());
+ executeSql(
+ "CREATE INDEX IF NOT EXISTS " + identifier(indexName) +
+ " ON " + relationshipTable(vector) +
+ " (" + identifier(columnName) + ")");
+ }
+
+ private void loadExistingData() {
+ for (EntityDefinition entity : schema.getEntityDefinitions()) {
+ loadEntityRows(entity);
+ restoreCountersFor(entity);
+ }
+ loadRelationshipRows();
+ }
+
+ private void loadEntityRows(final EntityDefinition entity) {
+ EntityInstanceCollection collection = getInstanceCollectionForEntityNamed(entity.getName());
+ String sql = "SELECT * FROM " + table(entity);
+ try (Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(sql)) {
+ while (resultSet.next()) {
+ String internalId = resultSet.getString(INTERNAL_ID_COLUMN);
+ if (collection.findInstanceByInternalID(internalId) != null) {
+ continue;
+ }
+
+ EntityInstance instance = new EntityInstance(entity, UUID.fromString(internalId));
+ for (String fieldName : entity.getFieldNames()) {
+ String value = resultSet.getString(fieldName);
+ if (value != null) {
+ instance.overrideValue(fieldName, value);
+ }
+ }
+ collection.addInstance(instance);
+ }
+ } catch (SQLException e) {
+ throw new IllegalStateException("Could not load SQLite entity rows for " + entity.getName(), e);
+ }
+ }
+
+ private void loadRelationshipRows() {
+ Map instances = instancesByInternalId();
+ for (RelationshipVectorDefinition vector : relationshipVectors()) {
+ String sql = "SELECT from_internal_id, to_internal_id FROM " + relationshipTable(vector);
+ try (Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(sql)) {
+ while (resultSet.next()) {
+ EntityInstance from = instances.get(resultSet.getString("from_internal_id"));
+ EntityInstance to = instances.get(resultSet.getString("to_internal_id"));
+ if (from != null && to != null &&
+ !from.getRelationships().getConnectedItems(vector.getName()).contains(to)) {
+ from.getRelationships().connect(vector.getName(), to);
+ }
+ }
+ } catch (SQLException e) {
+ throw new IllegalStateException("Could not load SQLite relationship rows", e);
+ }
+ }
+ }
+
+ private void restoreCountersFor(final EntityDefinition entity) {
+ EntityInstanceCollection collection = getInstanceCollectionForEntityNamed(entity.getName());
+ String sql = "SELECT field_name, next_value FROM " + identifier(COUNTERS_TABLE) +
+ " WHERE entity_name = ?";
+ try (PreparedStatement statement = connection.prepareStatement(sql)) {
+ statement.setString(1, entity.getName());
+ try (ResultSet resultSet = statement.executeQuery()) {
+ while (resultSet.next()) {
+ AutoIncrement counter = collection.getCounters().get(resultSet.getString("field_name"));
+ if (counter != null) {
+ int nextValue = resultSet.getInt("next_value");
+ if (counter.getCurrentValue() < nextValue) {
+ counter.incrementToNextAbove(nextValue - 1);
+ }
+ }
+ }
+ }
+ } catch (SQLException e) {
+ throw new IllegalStateException("Could not restore SQLite counters for " + entity.getName(), e);
+ }
+ }
+
+ private SqlQuery selectInstancesSql(
+ final EntityDefinition entity, final QueryFilterParams queryParams) {
+ StringBuilder sql = new StringBuilder("SELECT * FROM " + table(entity));
+ List whereClauses = new ArrayList<>();
+ List parameters = new ArrayList<>();
+
+ for (FilterBy filterBy : queryParams.toList()) {
+ if (EntityListSortParamParser.isSortByParam(filterBy.fieldName)) {
+ continue;
+ }
+ if (!entity.hasFieldNameDefined(filterBy.fieldName)) {
+ continue;
+ }
+
+ String column = identifier(filterBy.fieldName);
+ switch (filterBy.filterOperation) {
+ case "=":
+ whereClauses.add(column + " = ?");
+ parameters.add(filterBy.fieldValue);
+ break;
+ case "!":
+ case "!=":
+ whereClauses.add(column + " <> ?");
+ parameters.add(filterBy.fieldValue);
+ break;
+ case "<":
+ case ">":
+ case "<=":
+ case ">=":
+ whereClauses.add(column + " " + filterBy.filterOperation + " ?");
+ parameters.add(filterBy.fieldValue);
+ break;
+ case "*=":
+ whereClauses.add(column + " LIKE ? ESCAPE '\\'");
+ parameters.add(sqlLikeWildcard(filterBy.fieldValue));
+ break;
+ case "~=":
+ whereClauses.add(regexSqlCondition(column, filterBy.fieldValue, parameters));
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (!whereClauses.isEmpty()) {
+ sql.append(" WHERE ").append(String.join(" AND ", whereClauses));
+ }
+
+ appendOrderBy(sql, entity, queryParams, "");
+
+ return new SqlQuery(entity, sql.toString(), parameters);
+ }
+
+ private SqlQuery selectRelatedInstancesSql(
+ final EntityInstance instance,
+ final RelationshipVectorDefinition vector,
+ final QueryFilterParams queryParams) {
+ EntityDefinition connectedEntity;
+ String targetJoinColumn;
+ String sourceWhereColumn;
+
+ if (vector.getFrom() == instance.getEntity()) {
+ connectedEntity = vector.getTo();
+ targetJoinColumn = "to_internal_id";
+ sourceWhereColumn = "from_internal_id";
+ } else if (vector.getTo() == instance.getEntity()) {
+ connectedEntity = vector.getFrom();
+ targetJoinColumn = "from_internal_id";
+ sourceWhereColumn = "to_internal_id";
+ } else {
+ return null;
+ }
+
+ StringBuilder sql = new StringBuilder(
+ "SELECT target.* FROM " + relationshipTable(vector) + " rel " +
+ "JOIN " + table(connectedEntity) + " target " +
+ "ON target." + identifier(INTERNAL_ID_COLUMN) +
+ " = rel." + targetJoinColumn +
+ " WHERE rel." + sourceWhereColumn + " = ?");
+ List parameters = new ArrayList<>();
+ parameters.add(instance.getInternalId());
+
+ for (FilterBy filterBy : queryParams.toList()) {
+ if (EntityListSortParamParser.isSortByParam(filterBy.fieldName)) {
+ continue;
+ }
+ if (!connectedEntity.hasFieldNameDefined(filterBy.fieldName)) {
+ continue;
+ }
+
+ String column = "target." + identifier(filterBy.fieldName);
+ switch (filterBy.filterOperation) {
+ case "=":
+ sql.append(" AND ").append(column).append(" = ?");
+ parameters.add(filterBy.fieldValue);
+ break;
+ case "!":
+ case "!=":
+ sql.append(" AND ").append(column).append(" <> ?");
+ parameters.add(filterBy.fieldValue);
+ break;
+ case "<":
+ case ">":
+ case "<=":
+ case ">=":
+ sql.append(" AND ").append(column).append(" ")
+ .append(filterBy.filterOperation).append(" ?");
+ parameters.add(filterBy.fieldValue);
+ break;
+ case "*=":
+ sql.append(" AND ").append(column).append(" LIKE ? ESCAPE '\\'");
+ parameters.add(sqlLikeWildcard(filterBy.fieldValue));
+ break;
+ case "~=":
+ sql.append(" AND ").
+ append(regexSqlCondition(column, filterBy.fieldValue, parameters));
+ break;
+ default:
+ break;
+ }
+ }
+
+ appendOrderBy(sql, connectedEntity, queryParams, "target.");
+
+ return new SqlQuery(connectedEntity, sql.toString(), parameters);
+ }
+
+ private void appendOrderBy(
+ final StringBuilder sql,
+ final EntityDefinition entity,
+ final QueryFilterParams queryParams,
+ final String columnPrefix) {
+ List orderClauses = new ArrayList<>();
+ for (SortByFieldName sortBy : new EntityListSortParamParser(queryParams).sortBys()) {
+ if (!entity.hasFieldNameDefined(sortBy.getFieldName())) {
+ continue;
+ }
+ orderClauses.add(columnPrefix + identifier(sortBy.getFieldName()) +
+ (sortBy.getOrder() < 0 ? " ASC" : " DESC"));
+ }
+ if (!orderClauses.isEmpty()) {
+ sql.append(" ORDER BY ").append(String.join(", ", orderClauses));
+ }
+ }
+
+ private String regexSqlCondition(
+ final String column,
+ final String regex,
+ final List parameters) {
+ try {
+ SqliteRegexToLikeConverter.Conversion conversion =
+ SqliteRegexToLikeConverter.convert(regex);
+ parameters.add(conversion.sqlValue());
+ if (conversion.isEquality()) {
+ return column + " = ?";
+ }
+ return column + " LIKE ? ESCAPE '\\'";
+ } catch (SqliteRegexToLikeConverter.RegexToLikeConversionException e) {
+ SqliteRegexFilterPolicy.compileSupported(regex);
+ LOGGER.warning("SQLite regex filter could not be converted to LIKE; using REGEXP: " +
+ e.getMessage());
+ parameters.add(regex);
+ return column + " REGEXP ?";
+ }
+ }
+
+ private List queryEntityRows(
+ final EntityDefinition entity, final String sql, final List parameters) {
+ List instances = new ArrayList<>();
+ try (PreparedStatement statement = connection.prepareStatement(sql)) {
+ for (int index = 0; index < parameters.size(); index++) {
+ statement.setObject(index + 1, parameters.get(index));
+ }
+ try (ResultSet resultSet = statement.executeQuery()) {
+ while (resultSet.next()) {
+ instances.add(instanceFromRow(entity, resultSet));
+ }
+ }
+ } catch (SQLException e) {
+ throw new IllegalStateException("Could not query SQLite entity rows for " + entity.getName(), e);
+ }
+ return instances;
+ }
+
+ private EntityInstance instanceFromRow(
+ final EntityDefinition entity, final ResultSet resultSet) throws SQLException {
+ String internalId = resultSet.getString(INTERNAL_ID_COLUMN);
+ EntityInstanceCollection collection = getInstanceCollectionForEntityNamed(entity.getName());
+ EntityInstance existing = collection.findInstanceByInternalID(internalId);
+ if (existing != null) {
+ return existing;
+ }
+
+ EntityInstance instance = new EntityInstance(
+ entity, UUID.fromString(internalId));
+ for (String fieldName : entity.getFieldNames()) {
+ String value = resultSet.getString(fieldName);
+ if (value != null) {
+ instance.overrideValue(fieldName, value);
+ }
+ }
+ collection.addInstance(instance);
+ return instance;
+ }
+
+ private String sqlLikeWildcard(final String wildcard) {
+ return wildcard.
+ replace("\\", "\\\\").
+ replace("%", "\\%").
+ replace("_", "\\_").
+ replace("*", "%").
+ replace("?", "_");
+ }
+
+ private void addConnectedItemsFromVectorTable(
+ final Set connected,
+ final EntityInstance instance,
+ final RelationshipVectorDefinition vector) {
+ EntityDefinition connectedEntity;
+ String sql;
+
+ if (vector.getFrom() == instance.getEntity()) {
+ connectedEntity = vector.getTo();
+ sql = "SELECT target.* FROM " + relationshipTable(vector) + " rel " +
+ "JOIN " + table(connectedEntity) + " target " +
+ "ON target." + identifier(INTERNAL_ID_COLUMN) + " = rel.to_internal_id " +
+ "WHERE rel.from_internal_id = ?";
+ } else if (vector.getTo() == instance.getEntity()) {
+ connectedEntity = vector.getFrom();
+ sql = "SELECT target.* FROM " + relationshipTable(vector) + " rel " +
+ "JOIN " + table(connectedEntity) + " target " +
+ "ON target." + identifier(INTERNAL_ID_COLUMN) + " = rel.from_internal_id " +
+ "WHERE rel.to_internal_id = ?";
+ } else {
+ return;
+ }
+
+ connected.addAll(queryEntityRows(connectedEntity, sql, List.of(instance.getInternalId())));
+ }
+
+ private List relationshipVectorsFor(
+ final EntityInstance instance, final String relationshipName) {
+ List vectors = new ArrayList<>();
+ Set seenTables = new HashSet<>();
+
+ for (RelationshipVectorDefinition vector :
+ instance.getEntity().related().getRelationships(relationshipName)) {
+ addRelationshipVectorIfNew(vectors, seenTables, vector);
+ if (vector.getRelationshipDefinition().isTwoWay()) {
+ RelationshipVectorDefinition otherVector =
+ vector.getRelationshipDefinition().otherVectorOf(vector);
+ if (otherVector != null) {
+ addRelationshipVectorIfNew(vectors, seenTables, otherVector);
+ }
+ }
+ }
+
+ return vectors;
+ }
+
+ private void addRelationshipVectorIfNew(
+ final List vectors,
+ final Set seenTables,
+ final RelationshipVectorDefinition vector) {
+ String tableName = relationshipTableName(vector);
+ if (seenTables.add(tableName)) {
+ vectors.add(vector);
+ }
+ }
+
+ private void persistInstance(final EntityInstance instance) {
+ EntityDefinition entity = instance.getEntity();
+ List columns = new ArrayList<>();
+ columns.add(INTERNAL_ID_COLUMN);
+ columns.addAll(entity.getFieldNames());
+
+ StringBuilder names = new StringBuilder();
+ StringBuilder placeholders = new StringBuilder();
+ String separator = "";
+ for (String column : columns) {
+ names.append(separator).append(identifier(column));
+ placeholders.append(separator).append("?");
+ separator = ", ";
+ }
+
+ String sql = "INSERT OR REPLACE INTO " + table(entity) +
+ " (" + names + ") VALUES (" + placeholders + ")";
+
+ try (PreparedStatement statement = connection.prepareStatement(sql)) {
+ statement.setString(1, instance.getInternalId());
+ int parameterIndex = 2;
+ for (String fieldName : entity.getFieldNames()) {
+ Field field = entity.getField(fieldName);
+ FieldValue value = instance.getFieldValue(fieldName);
+ if (value == null) {
+ statement.setString(parameterIndex, null);
+ } else {
+ statement.setString(parameterIndex, field.getActualValueToAdd(value));
+ }
+ parameterIndex++;
+ }
+ statement.executeUpdate();
+ } catch (SQLException e) {
+ throw new IllegalStateException("Could not persist SQLite instance", e);
+ }
+ }
+
+ private void persistRelationship(
+ final EntityInstance from, final String relationshipName, final EntityInstance to) {
+ RelationshipVectorDefinition vector =
+ from.getEntity().getNamedRelationshipTo(relationshipName, to.getEntity());
+ if (vector == null) {
+ throw new IllegalArgumentException(
+ "Unknown relationship " + relationshipName + " between " +
+ from.getEntity().getName() + " and " + to.getEntity().getName());
+ }
+
+ String sql = "INSERT OR IGNORE INTO " + relationshipTable(vector) +
+ " (from_internal_id, to_internal_id) VALUES (?, ?)";
+ try (PreparedStatement statement = connection.prepareStatement(sql)) {
+ statement.setString(1, from.getInternalId());
+ statement.setString(2, to.getInternalId());
+ statement.executeUpdate();
+ } catch (SQLException e) {
+ throw new IllegalStateException("Could not persist SQLite relationship", e);
+ }
+ }
+
+ private void persistCountersFor(final EntityDefinition entity) {
+ executePreparedSql(
+ "DELETE FROM " + identifier(COUNTERS_TABLE) +
+ " WHERE entity_name = ?",
+ List.of(entity.getName()));
+
+ String sql = "INSERT INTO " + identifier(COUNTERS_TABLE) +
+ " (entity_name, field_name, next_value) VALUES (?, ?, ?)";
+ try (PreparedStatement statement = connection.prepareStatement(sql)) {
+ for (AutoIncrement counter : countersFor(entity).values()) {
+ statement.setString(1, entity.getName());
+ statement.setString(2, counter.getName());
+ statement.setInt(3, counter.getCurrentValue());
+ statement.executeUpdate();
+ }
+ } catch (SQLException e) {
+ throw new IllegalStateException("Could not persist SQLite counters for " + entity.getName(), e);
+ }
+ }
+
+ private void deletePersistedInstance(final String internalId) {
+ for (EntityDefinition entity : schema.getEntityDefinitions()) {
+ executePreparedSql(
+ "DELETE FROM " + table(entity) +
+ " WHERE " + identifier(INTERNAL_ID_COLUMN) + " = ?",
+ List.of(internalId));
+ }
+ for (RelationshipVectorDefinition vector : relationshipVectors()) {
+ executePreparedSql(
+ "DELETE FROM " + relationshipTable(vector) +
+ " WHERE from_internal_id = ? OR to_internal_id = ?",
+ List.of(internalId, internalId));
+ }
+ }
+
+ private void deleteRelationshipRowsInvolving(
+ final EntityInstance parent,
+ final EntityInstance child,
+ final String relationshipName) {
+ for (RelationshipVectorDefinition vector : relationshipVectors()) {
+ if (!vector.getRelationshipDefinition().isKnownAs(relationshipName)) {
+ continue;
+ }
+ executePreparedSql(
+ "DELETE FROM " + relationshipTable(vector) +
+ " WHERE (from_internal_id = ? AND to_internal_id = ?)" +
+ " OR (from_internal_id = ? AND to_internal_id = ?)",
+ List.of(
+ parent.getInternalId(),
+ child.getInternalId(),
+ child.getInternalId(),
+ parent.getInternalId()));
+ }
+ }
+
+ private void deleteRelationshipRowsInvolving(final EntityInstance instance) {
+ for (RelationshipVectorDefinition vector : relationshipVectors()) {
+ executePreparedSql(
+ "DELETE FROM " + relationshipTable(vector) +
+ " WHERE from_internal_id = ? OR to_internal_id = ?",
+ List.of(instance.getInternalId(), instance.getInternalId()));
+ }
+ }
+
+ private void replaceAllRelationshipRowsFromCache() {
+ clearRelationshipRows();
+ Set persisted = new HashSet<>();
+
+ String sql = "INSERT OR IGNORE INTO %s (from_internal_id, to_internal_id) VALUES (?, ?)";
+ for (EntityInstance instance : instancesByInternalId().values()) {
+ for (RelationshipVectorInstance relationship : instance.getRelationships().getRelationshipInstances()) {
+ RelationshipVectorDefinition vector = relationship.getDefinition();
+ String key = vector.getName() + "|" +
+ relationship.getFrom().getInternalId() + "|" +
+ relationship.getTo().getInternalId();
+ if (!persisted.add(key)) {
+ continue;
+ }
+ try (PreparedStatement statement = connection.prepareStatement(
+ String.format(sql, relationshipTable(vector)))) {
+ statement.setString(1, relationship.getFrom().getInternalId());
+ statement.setString(2, relationship.getTo().getInternalId());
+ statement.executeUpdate();
+ } catch (SQLException e) {
+ throw new IllegalStateException("Could not persist SQLite relationship", e);
+ }
+ }
+ }
+ }
+
+ private void clearRelationshipRows() {
+ for (RelationshipVectorDefinition vector : relationshipVectors()) {
+ executeSql("DELETE FROM " + relationshipTable(vector));
+ }
+ }
+
+ private Set currentInternalIds() {
+ return instancesByInternalId().keySet();
+ }
+
+ private Map instancesByInternalId() {
+ Map instances = new HashMap<>();
+ for (EntityInstanceCollection collection : getAllInstanceCollections()) {
+ for (EntityInstance instance : collection.getInstances()) {
+ instances.put(instance.getInternalId(), instance);
+ }
+ }
+ return instances;
+ }
+
+ private List relationshipVectors() {
+ List vectors = new ArrayList<>();
+ if (schema == null) {
+ return vectors;
+ }
+ for (RelationshipDefinition relationship : schema.getRelationships()) {
+ vectors.add(relationship.getFromRelationship());
+ if (relationship.isTwoWay()) {
+ vectors.add(relationship.getReversedRelationship());
+ }
+ }
+ return vectors;
+ }
+
+ private Set columnNames(final String tableName) {
+ Set names = new HashSet<>();
+ String sql = "PRAGMA table_info(" + identifier(tableName) + ")";
+ try (Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(sql)) {
+ while (resultSet.next()) {
+ names.add(resultSet.getString("name"));
+ }
+ } catch (SQLException e) {
+ throw new IllegalStateException("Could not read SQLite table info for " + tableName, e);
+ }
+ return names;
+ }
+
+ private void executeSql(final String sql) {
+ try (Statement statement = connection.createStatement()) {
+ statement.executeUpdate(sql);
+ } catch (SQLException e) {
+ throw new IllegalStateException("Could not execute SQLite SQL: " + sql, e);
+ }
+ }
+
+ private void executePreparedSql(final String sql, final List parameters) {
+ try (PreparedStatement statement = connection.prepareStatement(sql)) {
+ for (int index = 0; index < parameters.size(); index++) {
+ statement.setObject(index + 1, parameters.get(index));
+ }
+ statement.executeUpdate();
+ } catch (SQLException e) {
+ throw new IllegalStateException("Could not execute SQLite SQL: " + sql, e);
+ }
+ }
+
+ private void runInTransaction(final Runnable operation) {
+ try {
+ boolean originalAutoCommit = connection.getAutoCommit();
+ connection.setAutoCommit(false);
+ try {
+ operation.run();
+ connection.commit();
+ } catch (RuntimeException e) {
+ connection.rollback();
+ throw e;
+ } finally {
+ connection.setAutoCommit(originalAutoCommit);
+ }
+ } catch (SQLException e) {
+ throw new IllegalStateException("Could not run SQLite transaction", e);
+ }
+ }
+
+ private String sqlType(final FieldType type) {
+ switch (type) {
+ case AUTO_INCREMENT:
+ case INTEGER:
+ return "INTEGER";
+ case FLOAT:
+ return "REAL";
+ case BOOLEAN:
+ case AUTO_GUID:
+ case DATE:
+ case ENUM:
+ case OBJECT:
+ case STRING:
+ default:
+ return "TEXT";
+ }
+ }
+
+ private String table(final EntityDefinition entity) {
+ return identifier(entityTableName(entity));
+ }
+
+ private String entityTableName(final EntityDefinition entity) {
+ return "thing_" + entity.getName();
+ }
+
+ private String relationshipTable(final RelationshipVectorDefinition vector) {
+ return identifier(relationshipTableName(vector));
+ }
+
+ private String relationshipTableName(final RelationshipVectorDefinition vector) {
+ return "thing_rel_" +
+ vector.getFrom().getName() + "_" +
+ vector.getName() + "_" +
+ vector.getTo().getName();
+ }
+
+ private String identifier(final String rawIdentifier) {
+ return "\"" + rawIdentifier.replace("\"", "\"\"") + "\"";
+ }
+
+ private static class JavaRegexpFunction extends Function {
+ private String cachedRegex;
+ private Pattern cachedPattern;
+
+ @Override
+ protected void xFunc() throws SQLException {
+ if (args() != 2) {
+ result(0);
+ return;
+ }
+
+ String regex = value_text(0);
+ String value = value_text(1);
+ if (regex == null || value == null) {
+ result(0);
+ return;
+ }
+
+ Pattern pattern = patternFor(regex);
+ result(pattern.matcher(value).matches() ? 1 : 0);
+ }
+
+ private Pattern patternFor(final String regex) throws SQLException {
+ if (!regex.equals(cachedRegex)) {
+ try {
+ cachedPattern = SqliteRegexFilterPolicy.compileSupported(regex);
+ cachedRegex = regex;
+ } catch (SqliteRegexFilterPolicy.UnsupportedRegexFilterException e) {
+ throw new SQLException("Unsupported SQLite regex filter: " + e.getMessage(), e);
+ }
+ }
+ return cachedPattern;
+ }
+ }
+
+ private static class SqlQuery {
+ private final EntityDefinition entity;
+ private final String sql;
+ private final List parameters;
+
+ private SqlQuery(
+ final EntityDefinition entity,
+ final String sql,
+ final List parameters) {
+ this.entity = entity;
+ this.sql = sql;
+ this.parameters = parameters;
+ }
+ }
+}
diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/SqliteThingRepositoryProvider.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/SqliteThingRepositoryProvider.java
new file mode 100644
index 00000000..835c629d
--- /dev/null
+++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/SqliteThingRepositoryProvider.java
@@ -0,0 +1,101 @@
+package uk.co.compendiumdev.thingifier.core.repository;
+
+import uk.co.compendiumdev.thingifier.core.EntityRelModel;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.ERSchema;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+
+public class SqliteThingRepositoryProvider implements ThingRepositoryProvider {
+
+ private final Map repositories;
+ private final Function jdbcUrlFactory;
+
+ private SqliteThingRepositoryProvider(final Function jdbcUrlFactory) {
+ this.repositories = new HashMap<>();
+ this.jdbcUrlFactory = jdbcUrlFactory;
+ repositories.put(
+ EntityRelModel.DEFAULT_DATABASE_NAME,
+ new SqliteThingRepository(
+ EntityRelModel.DEFAULT_DATABASE_NAME,
+ jdbcUrlFactory.apply(EntityRelModel.DEFAULT_DATABASE_NAME)));
+ }
+
+ public static SqliteThingRepositoryProvider inMemory() {
+ String providerName = "thingifier_" + UUID.randomUUID().toString().replace("-", "");
+ return new SqliteThingRepositoryProvider(
+ databaseKey -> "jdbc:sqlite:file:" + providerName + "_" + safeName(databaseKey) +
+ "?mode=memory&cache=shared");
+ }
+
+ public static SqliteThingRepositoryProvider fileBacked(final Path directory) {
+ try {
+ Files.createDirectories(directory);
+ } catch (IOException e) {
+ throw new IllegalStateException("Could not create SQLite repository directory " + directory, e);
+ }
+
+ return new SqliteThingRepositoryProvider(databaseKey -> {
+ Path databasePath = directory.resolve(safeName(databaseKey) + ".sqlite");
+ return "jdbc:sqlite:" + databasePath.toAbsolutePath().toString().replace("\\", "/");
+ });
+ }
+
+ @Override
+ public ThingRepository getDefaultRepository() {
+ return repositories.get(EntityRelModel.DEFAULT_DATABASE_NAME);
+ }
+
+ @Override
+ public ThingRepository getRepository(final String databaseKey) {
+ return repositories.get(databaseKey);
+ }
+
+ @Override
+ public Set getRepositoryNames() {
+ return new HashSet<>(repositories.keySet());
+ }
+
+ @Override
+ public ThingRepository createRepository(final String databaseKey, final ERSchema schema) {
+ if (repositories.containsKey(databaseKey)) {
+ throw new IllegalStateException("ERM Database Already Exists with name " + databaseKey);
+ }
+
+ ThingRepository repository = new SqliteThingRepository(databaseKey, jdbcUrlFactory.apply(databaseKey));
+ repository.initializeFrom(schema);
+ repositories.put(databaseKey, repository);
+ return repository;
+ }
+
+ @Override
+ public boolean createRepositoryIfNotExisting(final String databaseKey, final ERSchema schema) {
+ if (repositories.containsKey(databaseKey)) {
+ return false;
+ }
+
+ ThingRepository repository = new SqliteThingRepository(databaseKey, jdbcUrlFactory.apply(databaseKey));
+ repository.initializeFrom(schema);
+ repositories.put(databaseKey, repository);
+ return true;
+ }
+
+ @Override
+ public void deleteRepository(final String databaseKey) {
+ ThingRepository repository = repositories.remove(databaseKey);
+ if (repository != null) {
+ repository.close();
+ }
+ }
+
+ private static String safeName(final String databaseKey) {
+ return databaseKey.replaceAll("[^A-Za-z0-9._-]", "_");
+ }
+}
diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/ThingRepository.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/ThingRepository.java
new file mode 100644
index 00000000..049e30bb
--- /dev/null
+++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/ThingRepository.java
@@ -0,0 +1,94 @@
+package uk.co.compendiumdev.thingifier.core.repository;
+
+import uk.co.compendiumdev.thingifier.core.domain.definitions.ERSchema;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue;
+import uk.co.compendiumdev.thingifier.core.domain.instances.AutoIncrement;
+import uk.co.compendiumdev.thingifier.core.domain.instances.ERInstanceData;
+import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
+import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceCollection;
+import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams;
+import uk.co.compendiumdev.thingifier.core.reporting.ValidationReport;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public interface ThingRepository extends AutoCloseable {
+
+ String databaseKey();
+
+ void initializeFrom(ERSchema schema);
+
+ void refreshSchema(ERSchema schema);
+
+ @Deprecated
+ ERInstanceData getInstanceData();
+
+ EntityInstanceCollection createInstanceCollectionFor(EntityDefinition definition);
+
+ void createInstanceCollectionsFrom(ERSchema schema);
+
+ List getAllInstanceCollections();
+
+ @Deprecated
+ EntityInstanceCollection getInstanceCollectionForEntityNamed(String entityName);
+
+ EntityInstance findEntityInstanceByGUID(String thingGUID);
+
+ EntityInstance findInstanceByPrimaryKey(EntityDefinition entity, String primaryKeyValue);
+
+ EntityInstance findInstanceByFieldNameAndValue(EntityDefinition entity, String fieldName, String fieldValue);
+
+ Collection listInstances(EntityDefinition entity);
+
+ List listInstances(EntityDefinition entity, QueryFilterParams queryParams);
+
+ EntityInstance findInstanceByQueryIdentifier(EntityDefinition entity, String identifier);
+
+ default List listRelatedInstances(
+ EntityInstance instance, String relationshipName) {
+ return listRelatedInstances(instance, relationshipName, new QueryFilterParams());
+ }
+
+ List listRelatedInstances(
+ EntityInstance instance, String relationshipName, QueryFilterParams queryParams);
+
+ EntityInstance addInstance(EntityInstance instance);
+
+ EntityInstance updateInstance(EntityInstance instance);
+
+ void deleteEntityInstance(EntityInstance instance);
+
+ void clearAllData();
+
+ void clearInstanceDataFor(String entityName);
+
+ ValidationReport checkFieldsForUniqueNess(EntityInstance instance, boolean isAmendment);
+
+ Map countersFor(EntityDefinition entity);
+
+ void setNextIdCountersToAccomodate(EntityDefinition entity, List fieldValues);
+
+ void connectRelationship(EntityInstance from, String relationshipName, EntityInstance to);
+
+ List removeRelationshipsInvolving(
+ EntityInstance parent, EntityInstance child, String relationshipName);
+
+ List removeAllRelationships(EntityInstance instance);
+
+ Collection getConnectedItems(EntityInstance instance, String relationshipName);
+
+ default boolean hasLoadedCompatibilitySnapshot() {
+ return true;
+ }
+
+ default void flush() {
+ // Allows persistent repositories to sync legacy direct collection mutations.
+ }
+
+ @Override
+ default void close() {
+ // Most repository implementations do not own closeable resources.
+ }
+}
diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/ThingRepositoryProvider.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/ThingRepositoryProvider.java
new file mode 100644
index 00000000..c2721a0b
--- /dev/null
+++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/ThingRepositoryProvider.java
@@ -0,0 +1,27 @@
+package uk.co.compendiumdev.thingifier.core.repository;
+
+import uk.co.compendiumdev.thingifier.core.domain.definitions.ERSchema;
+
+import java.util.Set;
+
+public interface ThingRepositoryProvider extends AutoCloseable {
+
+ ThingRepository getDefaultRepository();
+
+ ThingRepository getRepository(String databaseKey);
+
+ Set getRepositoryNames();
+
+ ThingRepository createRepository(String databaseKey, ERSchema schema);
+
+ boolean createRepositoryIfNotExisting(String databaseKey, ERSchema schema);
+
+ void deleteRepository(String databaseKey);
+
+ @Override
+ default void close() {
+ for (String databaseKey : getRepositoryNames()) {
+ getRepository(databaseKey).close();
+ }
+ }
+}
diff --git a/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/ThingRepositoryProviderConfig.java b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/ThingRepositoryProviderConfig.java
new file mode 100644
index 00000000..b9c55e4b
--- /dev/null
+++ b/ercoremodel/src/main/java/uk/co/compendiumdev/thingifier/core/repository/ThingRepositoryProviderConfig.java
@@ -0,0 +1,131 @@
+package uk.co.compendiumdev.thingifier.core.repository;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class ThingRepositoryProviderConfig {
+
+ public static final String DEFAULT_REPOSITORY_MODE = "memory";
+ public static final String ARG_REPOSITORY_MODE = "-thingifier-repository";
+ public static final String ARG_SQLITE_DIRECTORY = "-thingifier-sqlite-directory";
+ public static final String ARG_SQLITE_MEMORY = "-sqlite-memory";
+ public static final String ENV_REPOSITORY_MODE = "THINGIFIER_REPOSITORY";
+ public static final String ENV_SQLITE_DIRECTORY = "THINGIFIER_SQLITE_DIRECTORY";
+ public static final String PROPERTY_REPOSITORY_MODE = "thingifier.repository";
+ public static final String PROPERTY_SQLITE_DIRECTORY = "thingifier.sqlite.directory";
+
+ private final String repositoryMode;
+ private final Path sqliteDirectory;
+
+ public ThingRepositoryProviderConfig(final String repositoryMode, final Path sqliteDirectory) {
+ this.repositoryMode = normalize(repositoryMode);
+ this.sqliteDirectory = sqliteDirectory;
+ }
+
+ public static ThingRepositoryProviderConfig fromArgs(final String[] args) {
+ String repositoryMode;
+ if (hasArg(args, ARG_SQLITE_MEMORY)) {
+ repositoryMode = "sqlite-memory";
+ } else {
+ repositoryMode = firstNonBlank(
+ argValue(args, ARG_REPOSITORY_MODE),
+ System.getProperty(PROPERTY_REPOSITORY_MODE),
+ System.getenv(ENV_REPOSITORY_MODE),
+ DEFAULT_REPOSITORY_MODE);
+ }
+
+ String sqliteDirectory = firstNonBlank(
+ argValue(args, ARG_SQLITE_DIRECTORY),
+ System.getProperty(PROPERTY_SQLITE_DIRECTORY),
+ System.getenv(ENV_SQLITE_DIRECTORY),
+ "thingifier-sqlite");
+
+ return new ThingRepositoryProviderConfig(repositoryMode, Paths.get(sqliteDirectory));
+ }
+
+ public ThingRepositoryProvider createProvider() {
+ switch (repositoryMode) {
+ case "memory":
+ case "in-memory":
+ case "custom-memory":
+ return new InMemoryThingRepositoryProvider();
+ case "sqlite":
+ case "sqlite-memory":
+ case "sqlite-in-memory":
+ return SqliteThingRepositoryProvider.inMemory();
+ case "sqlite-file":
+ case "sqlite-disk":
+ case "file":
+ return SqliteThingRepositoryProvider.fileBacked(sqliteDirectory);
+ default:
+ throw new IllegalArgumentException(
+ "Unknown Thingifier repository mode " + repositoryMode +
+ ". Expected memory, sqlite-memory, or sqlite-file.");
+ }
+ }
+
+ public String getRepositoryMode() {
+ return repositoryMode;
+ }
+
+ public Path getSqliteDirectory() {
+ return sqliteDirectory;
+ }
+
+ public String describe() {
+ if (repositoryMode.equals("sqlite-file") ||
+ repositoryMode.equals("sqlite-disk") ||
+ repositoryMode.equals("file")) {
+ return repositoryMode + " at " + sqliteDirectory.toAbsolutePath();
+ }
+ return repositoryMode;
+ }
+
+ private static String argValue(final String[] args, final String argName) {
+ if (args == null) {
+ return null;
+ }
+
+ for (String arg : args) {
+ if (arg == null) {
+ continue;
+ }
+ if (arg.equalsIgnoreCase(argName)) {
+ return "";
+ }
+ if (arg.toLowerCase().startsWith(argName.toLowerCase() + "=")) {
+ return arg.substring(arg.indexOf("=") + 1).trim();
+ }
+ }
+ return null;
+ }
+
+ private static boolean hasArg(final String[] args, final String argName) {
+ if (args == null) {
+ return false;
+ }
+
+ for (String arg : args) {
+ if (arg != null && arg.equalsIgnoreCase(argName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static String firstNonBlank(final String... values) {
+ for (String value : values) {
+ if (value != null && !value.trim().isEmpty()) {
+ return value.trim();
+ }
+ }
+ return "";
+ }
+
+ private static String normalize(final String mode) {
+ return firstNonBlank(mode, DEFAULT_REPOSITORY_MODE).
+ trim().
+ toLowerCase().
+ replace("_", "-");
+ }
+}
diff --git a/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/EntityRelModelTest.java b/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/EntityRelModelTest.java
index 49f05091..7f39fb19 100644
--- a/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/EntityRelModelTest.java
+++ b/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/EntityRelModelTest.java
@@ -3,6 +3,7 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import uk.co.compendiumdev.thingifier.core.domain.datapopulator.DataPopulator;
+import uk.co.compendiumdev.thingifier.core.domain.datapopulator.RepositoryDataPopulator;
import uk.co.compendiumdev.thingifier.core.domain.definitions.Cardinality;
import uk.co.compendiumdev.thingifier.core.domain.definitions.ERSchema;
import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
@@ -12,6 +13,11 @@
import uk.co.compendiumdev.thingifier.core.domain.instances.ERInstanceData;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceCollection;
+import uk.co.compendiumdev.thingifier.core.repository.SqliteThingRepositoryProvider;
+import uk.co.compendiumdev.thingifier.core.repository.ThingRepository;
+
+import java.util.ArrayList;
+import java.util.List;
public class EntityRelModelTest {
@@ -245,4 +251,38 @@ public void populate(final ERSchema schema, final ERInstanceData database) {
Assertions.assertEquals(1, erm.getInstanceData().getAllInstanceCollections().size());
}
+
+ @Test
+ public void populateDatabaseUsesRepositoryDataPopulatorWithoutCompatibilitySnapshot(){
+ try (EntityRelModel erm = new EntityRelModel(SqliteThingRepositoryProvider.inMemory())) {
+ EntityDefinition thing = erm.createEntityDefinition("thing", "things");
+ thing.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT));
+ thing.addField(Field.is("name", FieldType.STRING));
+
+ RepositoryDataPopulator dataPopulator = new RepositoryDataPopulator() {
+ @Override
+ public void populate(final ERSchema schema, final ERInstanceData database) {
+ Assertions.fail("Repository-aware data populators should not use ERInstanceData");
+ }
+
+ @Override
+ public void populate(final ERSchema schema, final ThingRepository repository) {
+ repository.addInstance(new EntityInstance(
+ schema.getEntityDefinitionNamed("thing")).
+ setValue("name", "repository"));
+ }
+ };
+
+ erm.setDataGenerator(dataPopulator);
+
+ Assertions.assertTrue(erm.populateDatabase(EntityRelModel.DEFAULT_DATABASE_NAME));
+
+ ThingRepository repository = erm.getRepository(EntityRelModel.DEFAULT_DATABASE_NAME);
+ List instances = new ArrayList<>(repository.listInstances(thing));
+
+ Assertions.assertEquals(1, instances.size());
+ Assertions.assertEquals("repository", instances.get(0).getFieldValue("name").asString());
+ Assertions.assertFalse(repository.hasLoadedCompatibilitySnapshot());
+ }
+ }
}
diff --git a/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/repository/SqliteRegexToLikeConverterTest.java b/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/repository/SqliteRegexToLikeConverterTest.java
new file mode 100644
index 00000000..8bded696
--- /dev/null
+++ b/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/repository/SqliteRegexToLikeConverterTest.java
@@ -0,0 +1,105 @@
+package uk.co.compendiumdev.thingifier.core.repository;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class SqliteRegexToLikeConverterTest {
+
+ @Test
+ public void convertsExactLiteralRegexToEquality() {
+ SqliteRegexToLikeConverter.Conversion conversion =
+ SqliteRegexToLikeConverter.convert("^abc$");
+
+ Assertions.assertTrue(conversion.isEquality());
+ Assertions.assertEquals("abc", conversion.sqlValue());
+ }
+
+ @Test
+ public void convertsPrefixRegexToLike() {
+ SqliteRegexToLikeConverter.Conversion conversion =
+ SqliteRegexToLikeConverter.convert("^abc.*");
+
+ Assertions.assertFalse(conversion.isEquality());
+ Assertions.assertEquals("abc%", conversion.sqlValue());
+ }
+
+ @Test
+ public void convertsContainsRegexToLike() {
+ SqliteRegexToLikeConverter.Conversion conversion =
+ SqliteRegexToLikeConverter.convert(".*abc.*");
+
+ Assertions.assertFalse(conversion.isEquality());
+ Assertions.assertEquals("%abc%", conversion.sqlValue());
+ }
+
+ @Test
+ public void convertsSuffixRegexToLike() {
+ SqliteRegexToLikeConverter.Conversion conversion =
+ SqliteRegexToLikeConverter.convert(".*abc$");
+
+ Assertions.assertFalse(conversion.isEquality());
+ Assertions.assertEquals("%abc", conversion.sqlValue());
+ }
+
+ @Test
+ public void convertsSingleCharacterWildcardRegexToLike() {
+ SqliteRegexToLikeConverter.Conversion conversion =
+ SqliteRegexToLikeConverter.convert("a.b");
+
+ Assertions.assertFalse(conversion.isEquality());
+ Assertions.assertEquals("a_b", conversion.sqlValue());
+ }
+
+ @Test
+ public void convertsMultiCharacterWildcardRegexToLike() {
+ SqliteRegexToLikeConverter.Conversion conversion =
+ SqliteRegexToLikeConverter.convert("a.*b");
+
+ Assertions.assertFalse(conversion.isEquality());
+ Assertions.assertEquals("a%b", conversion.sqlValue());
+ }
+
+ @Test
+ public void escapesLikeWildcardsInRegexLiterals() {
+ SqliteRegexToLikeConverter.Conversion conversion =
+ SqliteRegexToLikeConverter.convert("a.*%_");
+
+ Assertions.assertFalse(conversion.isEquality());
+ Assertions.assertEquals("a%\\%\\_", conversion.sqlValue());
+ }
+
+ @Test
+ public void rejectsRegexCharacterClasses() {
+ Assertions.assertThrows(
+ SqliteRegexToLikeConverter.RegexToLikeConversionException.class,
+ () -> SqliteRegexToLikeConverter.convert("Repository [Pp]roject"));
+ }
+
+ @Test
+ public void rejectsRegexAlternation() {
+ Assertions.assertThrows(
+ SqliteRegexToLikeConverter.RegexToLikeConversionException.class,
+ () -> SqliteRegexToLikeConverter.convert("cat|dog"));
+ }
+
+ @Test
+ public void rejectsRegexGroups() {
+ Assertions.assertThrows(
+ SqliteRegexToLikeConverter.RegexToLikeConversionException.class,
+ () -> SqliteRegexToLikeConverter.convert("(cat)"));
+ }
+
+ @Test
+ public void rejectsRegexShorthandCharacterClasses() {
+ Assertions.assertThrows(
+ SqliteRegexToLikeConverter.RegexToLikeConversionException.class,
+ () -> SqliteRegexToLikeConverter.convert("\\d+"));
+ }
+
+ @Test
+ public void rejectsMalformedRegex() {
+ Assertions.assertThrows(
+ SqliteRegexToLikeConverter.RegexToLikeConversionException.class,
+ () -> SqliteRegexToLikeConverter.convert("["));
+ }
+}
diff --git a/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/repository/ThingRepositoryContractTest.java b/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/repository/ThingRepositoryContractTest.java
new file mode 100644
index 00000000..1ee5966d
--- /dev/null
+++ b/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/repository/ThingRepositoryContractTest.java
@@ -0,0 +1,502 @@
+package uk.co.compendiumdev.thingifier.core.repository;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import uk.co.compendiumdev.thingifier.core.EntityRelModel;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.Cardinality;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.ERSchema;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType;
+import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
+import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceCollection;
+import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams;
+import uk.co.compendiumdev.thingifier.core.query.RepositoryBackedSimpleQuery;
+
+import java.nio.file.Path;
+import java.util.List;
+
+public class ThingRepositoryContractTest {
+
+ @TempDir
+ Path tempDir;
+
+ @Test
+ public void inMemoryRepositorySupportsTheContract() {
+ ThingRepository repository = new InMemoryThingRepository(EntityRelModel.DEFAULT_DATABASE_NAME);
+
+ exerciseRepositoryContract(repository);
+ }
+
+ @Test
+ public void sqliteInMemoryRepositorySupportsTheContract() {
+ try (ThingRepository repository = SqliteThingRepository.inMemory(EntityRelModel.DEFAULT_DATABASE_NAME)) {
+ exerciseRepositoryContract(repository);
+ }
+ }
+
+ @Test
+ public void inMemoryProviderKeepsLogicalDatabasesSeparate() {
+ ThingRepositoryProvider provider = new InMemoryThingRepositoryProvider();
+
+ exerciseProviderIsolation(provider);
+ }
+
+ @Test
+ public void sqliteProviderKeepsLogicalDatabasesSeparate() {
+ try (ThingRepositoryProvider provider = SqliteThingRepositoryProvider.inMemory()) {
+ exerciseProviderIsolation(provider);
+ }
+ }
+
+ @Test
+ public void sqliteRepositoryCannotBeUsedAfterClose() {
+ ERSchema schema = todoSchema();
+ ThingRepository repository = SqliteThingRepository.inMemory(EntityRelModel.DEFAULT_DATABASE_NAME);
+ repository.initializeFrom(schema);
+
+ repository.close();
+
+ Assertions.assertThrows(
+ IllegalStateException.class,
+ () -> repository.listInstances(schema.getEntityDefinitionNamed("project")));
+ }
+
+ @Test
+ public void sqliteFileBackedRepositorySurvivesCloseAndReopen() {
+ ERSchema schema = todoSchema();
+ Path databasePath = tempDir.resolve("thingifier.sqlite");
+
+ try (ThingRepository repository =
+ SqliteThingRepository.fileBacked(EntityRelModel.DEFAULT_DATABASE_NAME, databasePath)) {
+ repository.initializeFrom(schema);
+
+ EntityDefinition projectDefinition = schema.getEntityDefinitionNamed("project");
+ EntityDefinition taskDefinition = schema.getEntityDefinitionNamed("task");
+ EntityInstance project = new EntityInstance(projectDefinition).setValue("title", "Persisted");
+ EntityInstance task = new EntityInstance(taskDefinition).setValue("title", "Loaded relationship");
+
+ repository.addInstance(project);
+ repository.addInstance(task);
+ repository.connectRelationship(task, "task-of", project);
+ }
+
+ try (ThingRepository reopened =
+ SqliteThingRepository.fileBacked(EntityRelModel.DEFAULT_DATABASE_NAME, databasePath)) {
+ reopened.initializeFrom(schema);
+
+ EntityDefinition projectDefinition = schema.getEntityDefinitionNamed("project");
+ EntityDefinition taskDefinition = schema.getEntityDefinitionNamed("task");
+
+ EntityInstance project = reopened.findInstanceByPrimaryKey(projectDefinition, "1");
+ EntityInstance task = reopened.findInstanceByFieldNameAndValue(
+ taskDefinition, "title", "Loaded relationship");
+
+ Assertions.assertNotNull(project);
+ Assertions.assertNotNull(task);
+ Assertions.assertEquals("Persisted", project.getFieldValue("title").asString());
+ Assertions.assertTrue(reopened.getConnectedItems(task, "task-of").contains(project));
+ Assertions.assertTrue(reopened.getConnectedItems(project, "tasks").contains(task));
+ }
+ }
+
+ @Test
+ public void sqliteRepositoryQuotesGeneratedSqlIdentifiers() {
+ ERSchema schema = new ERSchema();
+ EntityDefinition select = schema.defineEntity("select", "selects", -1);
+ select.addAsPrimaryKeyField(Field.is("key-id", FieldType.AUTO_GUID));
+ select.addField(Field.is("field with space", FieldType.STRING));
+
+ try (ThingRepository repository = SqliteThingRepository.inMemory("quoted")) {
+ repository.initializeFrom(schema);
+
+ EntityInstance instance = new EntityInstance(select);
+ instance.setValue("field with space", "works");
+ repository.addInstance(instance);
+
+ EntityInstance found = repository.findInstanceByFieldNameAndValue(
+ select, "field with space", "works");
+
+ Assertions.assertNotNull(found);
+ Assertions.assertEquals("works", found.getFieldValue("field with space").asString());
+ }
+ }
+
+ @Test
+ public void sqliteFileBackedRepositoryOnlyHydratesCompatibilitySnapshotWhenRequested() {
+ ERSchema schema = todoSchema();
+ EntityDefinition projectDefinition = schema.getEntityDefinitionNamed("project");
+ Path databasePath = tempDir.resolve("lazy.sqlite");
+
+ try (ThingRepository repository =
+ SqliteThingRepository.fileBacked(EntityRelModel.DEFAULT_DATABASE_NAME, databasePath)) {
+ repository.initializeFrom(schema);
+
+ repository.addInstance(new EntityInstance(projectDefinition).setValue("title", "First"));
+ repository.addInstance(new EntityInstance(projectDefinition).setValue("title", "Second"));
+ }
+
+ try (ThingRepository reopened =
+ SqliteThingRepository.fileBacked(EntityRelModel.DEFAULT_DATABASE_NAME, databasePath)) {
+ reopened.initializeFrom(schema);
+
+ EntityInstanceCollection cachedProjects =
+ reopened.getInstanceCollectionForEntityNamed("project");
+ Assertions.assertEquals(0, cachedProjects.countInstances());
+
+ QueryFilterParams params = new QueryFilterParams();
+ params.put("title", "First");
+
+ List filtered = reopened.listInstances(projectDefinition, params);
+
+ Assertions.assertEquals(1, filtered.size());
+ Assertions.assertEquals(1, cachedProjects.countInstances());
+
+ reopened.getInstanceData();
+
+ Assertions.assertEquals(2, cachedProjects.countInstances());
+ }
+ }
+
+ @Test
+ public void sqliteRelationshipReadsDoNotHydrateCompatibilitySnapshot() {
+ ERSchema schema = todoSchema();
+ EntityDefinition projectDefinition = schema.getEntityDefinitionNamed("project");
+ EntityDefinition taskDefinition = schema.getEntityDefinitionNamed("task");
+ Path databasePath = tempDir.resolve("relationship-lazy.sqlite");
+
+ try (ThingRepository repository =
+ SqliteThingRepository.fileBacked(EntityRelModel.DEFAULT_DATABASE_NAME, databasePath)) {
+ repository.initializeFrom(schema);
+
+ EntityInstance project = repository.addInstance(
+ new EntityInstance(projectDefinition).setValue("title", "SQLite project"));
+ EntityInstance firstTask = repository.addInstance(
+ new EntityInstance(taskDefinition).setValue("title", "Wire repository"));
+ EntityInstance secondTask = repository.addInstance(
+ new EntityInstance(taskDefinition).setValue("title", "Write tests"));
+
+ repository.connectRelationship(project, "tasks", firstTask);
+ repository.connectRelationship(project, "tasks", secondTask);
+ }
+
+ try (ThingRepository reopened =
+ SqliteThingRepository.fileBacked(EntityRelModel.DEFAULT_DATABASE_NAME, databasePath)) {
+ reopened.initializeFrom(schema);
+
+ Assertions.assertFalse(reopened.hasLoadedCompatibilitySnapshot());
+
+ EntityInstance project = reopened.findInstanceByPrimaryKey(projectDefinition, "1");
+
+ QueryFilterParams params = new QueryFilterParams();
+ params.put("title", "*=Wire*");
+
+ List filteredTasks =
+ reopened.listRelatedInstances(project, "tasks", params);
+
+ Assertions.assertEquals(1, filteredTasks.size());
+ Assertions.assertEquals("Wire repository",
+ filteredTasks.get(0).getFieldValue("title").asString());
+ Assertions.assertFalse(reopened.hasLoadedCompatibilitySnapshot());
+
+ QueryFilterParams regexParams = new QueryFilterParams();
+ regexParams.put("title", "~=Wire [Rr]epository");
+
+ List regexFilteredTasks =
+ reopened.listRelatedInstances(project, "tasks", regexParams);
+
+ Assertions.assertEquals(1, regexFilteredTasks.size());
+ Assertions.assertEquals("Wire repository",
+ regexFilteredTasks.get(0).getFieldValue("title").asString());
+ Assertions.assertFalse(reopened.hasLoadedCompatibilitySnapshot());
+
+ RepositoryBackedSimpleQuery query =
+ new RepositoryBackedSimpleQuery(schema, reopened, "project/1/tasks").
+ performQuery(new QueryFilterParams());
+
+ Assertions.assertTrue(query.isResultACollection());
+ Assertions.assertEquals(2, query.getListEntityInstances().size());
+ Assertions.assertFalse(reopened.hasLoadedCompatibilitySnapshot());
+ }
+ }
+
+ @Test
+ public void sqliteConvertibleRegexFilterDoesNotHydrateCompatibilitySnapshot() {
+ ERSchema schema = todoSchema();
+ EntityDefinition projectDefinition = schema.getEntityDefinitionNamed("project");
+ Path databasePath = tempDir.resolve("regex-like-lazy.sqlite");
+
+ try (ThingRepository repository =
+ SqliteThingRepository.fileBacked(EntityRelModel.DEFAULT_DATABASE_NAME, databasePath)) {
+ repository.initializeFrom(schema);
+
+ repository.addInstance(new EntityInstance(projectDefinition).
+ setValue("title", "Repository project"));
+ repository.addInstance(new EntityInstance(projectDefinition).
+ setValue("title", "repository project"));
+ repository.addInstance(new EntityInstance(projectDefinition).
+ setValue("title", "Another project"));
+ }
+
+ try (ThingRepository reopened =
+ SqliteThingRepository.fileBacked(EntityRelModel.DEFAULT_DATABASE_NAME, databasePath)) {
+ reopened.initializeFrom(schema);
+
+ QueryFilterParams regexParams = new QueryFilterParams();
+ regexParams.put("title", "~=Repository.*");
+
+ List filtered =
+ reopened.listInstances(projectDefinition, regexParams);
+
+ Assertions.assertEquals(1, filtered.size());
+ Assertions.assertEquals("Repository project",
+ filtered.get(0).getFieldValue("title").asString());
+ Assertions.assertFalse(reopened.hasLoadedCompatibilitySnapshot());
+ Assertions.assertEquals(1,
+ reopened.getInstanceCollectionForEntityNamed("project").countInstances());
+ }
+ }
+
+ @Test
+ public void sqliteRegexpFilterDoesNotHydrateCompatibilitySnapshot() {
+ ERSchema schema = todoSchema();
+ EntityDefinition projectDefinition = schema.getEntityDefinitionNamed("project");
+ Path databasePath = tempDir.resolve("regex-regexp-lazy.sqlite");
+
+ try (ThingRepository repository =
+ SqliteThingRepository.fileBacked(EntityRelModel.DEFAULT_DATABASE_NAME, databasePath)) {
+ repository.initializeFrom(schema);
+
+ repository.addInstance(new EntityInstance(projectDefinition).
+ setValue("title", "Repository project"));
+ repository.addInstance(new EntityInstance(projectDefinition).
+ setValue("title", "Another project"));
+ }
+
+ try (ThingRepository reopened =
+ SqliteThingRepository.fileBacked(EntityRelModel.DEFAULT_DATABASE_NAME, databasePath)) {
+ reopened.initializeFrom(schema);
+
+ QueryFilterParams regexParams = new QueryFilterParams();
+ regexParams.put("title", "~=Repository [Pp]roject");
+
+ List filtered =
+ reopened.listInstances(projectDefinition, regexParams);
+
+ Assertions.assertEquals(1, filtered.size());
+ Assertions.assertEquals("Repository project",
+ filtered.get(0).getFieldValue("title").asString());
+ Assertions.assertFalse(reopened.hasLoadedCompatibilitySnapshot());
+ Assertions.assertEquals(1,
+ reopened.getInstanceCollectionForEntityNamed("project").countInstances());
+ }
+ }
+
+ @Test
+ public void sqliteInMemoryRegexpFilterDoesNotHydrateCompatibilitySnapshot() {
+ ERSchema schema = todoSchema();
+ EntityDefinition projectDefinition = schema.getEntityDefinitionNamed("project");
+
+ try (ThingRepository repository =
+ SqliteThingRepository.inMemory(EntityRelModel.DEFAULT_DATABASE_NAME)) {
+ repository.initializeFrom(schema);
+
+ repository.addInstance(new EntityInstance(projectDefinition).
+ setValue("title", "Repository project"));
+ repository.addInstance(new EntityInstance(projectDefinition).
+ setValue("title", "Another project"));
+
+ QueryFilterParams regexParams = new QueryFilterParams();
+ regexParams.put("title", "~=Repository [Pp]roject");
+
+ List filtered =
+ repository.listInstances(projectDefinition, regexParams);
+
+ Assertions.assertEquals(1, filtered.size());
+ Assertions.assertEquals("Repository project",
+ filtered.get(0).getFieldValue("title").asString());
+ Assertions.assertFalse(repository.hasLoadedCompatibilitySnapshot());
+ }
+ }
+
+ @Test
+ public void sqliteInvalidRegexFilterFailsWithoutHydratingCompatibilitySnapshot() {
+ ERSchema schema = todoSchema();
+ EntityDefinition projectDefinition = schema.getEntityDefinitionNamed("project");
+
+ try (ThingRepository repository =
+ SqliteThingRepository.inMemory(EntityRelModel.DEFAULT_DATABASE_NAME)) {
+ repository.initializeFrom(schema);
+ repository.addInstance(new EntityInstance(projectDefinition).
+ setValue("title", "Repository project"));
+
+ QueryFilterParams regexParams = new QueryFilterParams();
+ regexParams.put("title", "~=[");
+
+ Assertions.assertThrows(
+ SqliteRegexFilterPolicy.UnsupportedRegexFilterException.class,
+ () -> repository.listInstances(projectDefinition, regexParams));
+ Assertions.assertFalse(repository.hasLoadedCompatibilitySnapshot());
+ }
+ }
+
+ @Test
+ public void sqliteTooComplexRegexFilterFailsWithoutHydratingCompatibilitySnapshot() {
+ ERSchema schema = todoSchema();
+ EntityDefinition projectDefinition = schema.getEntityDefinitionNamed("project");
+
+ try (ThingRepository repository =
+ SqliteThingRepository.inMemory(EntityRelModel.DEFAULT_DATABASE_NAME)) {
+ repository.initializeFrom(schema);
+ repository.addInstance(new EntityInstance(projectDefinition).
+ setValue("title", "Repository project"));
+
+ QueryFilterParams regexParams = new QueryFilterParams();
+ regexParams.put("title", "~=(.+)+");
+
+ Assertions.assertThrows(
+ SqliteRegexFilterPolicy.UnsupportedRegexFilterException.class,
+ () -> repository.listInstances(projectDefinition, regexParams));
+ Assertions.assertFalse(repository.hasLoadedCompatibilitySnapshot());
+ }
+ }
+
+ private void exerciseRepositoryContract(final ThingRepository repository) {
+ ERSchema schema = todoSchema();
+ repository.initializeFrom(schema);
+
+ EntityDefinition projectDefinition = schema.getEntityDefinitionNamed("project");
+ EntityDefinition taskDefinition = schema.getEntityDefinitionNamed("task");
+
+ EntityInstance project = new EntityInstance(projectDefinition);
+ project.setValue("title", "Repository project");
+
+ repository.addInstance(project);
+
+ Assertions.assertEquals("1", project.getPrimaryKeyValue());
+ Assertions.assertEquals(project, repository.findInstanceByPrimaryKey(projectDefinition, "1"));
+ Assertions.assertEquals(project, repository.findInstanceByQueryIdentifier(projectDefinition, "1"));
+ Assertions.assertEquals(project, repository.findInstanceByFieldNameAndValue(
+ projectDefinition, "title", "Repository project"));
+ Assertions.assertEquals(1, repository.listInstances(projectDefinition).size());
+
+ EntityInstance duplicate = new EntityInstance(projectDefinition);
+ duplicate.setValue("title", "Repository project");
+ Assertions.assertFalse(repository.checkFieldsForUniqueNess(duplicate, false).isValid());
+
+ EntityInstance secondProject = new EntityInstance(projectDefinition);
+ secondProject.setValue("title", "Another project");
+ repository.addInstance(secondProject);
+
+ QueryFilterParams filteredParams = new QueryFilterParams();
+ filteredParams.put("id", ">=2");
+ List filteredProjects =
+ repository.listInstances(projectDefinition, filteredParams);
+ Assertions.assertEquals(1, filteredProjects.size());
+ Assertions.assertEquals(secondProject, filteredProjects.get(0));
+
+ QueryFilterParams sortedParams = new QueryFilterParams();
+ sortedParams.put("sortBy", "-id");
+ List sortedProjects =
+ repository.listInstances(projectDefinition, sortedParams);
+ Assertions.assertEquals("2", sortedProjects.get(0).getPrimaryKeyValue());
+ Assertions.assertEquals("1", sortedProjects.get(1).getPrimaryKeyValue());
+
+ QueryFilterParams regexParams = new QueryFilterParams();
+ regexParams.put("title", "~=Repository.*");
+ List regexProjects =
+ repository.listInstances(projectDefinition, regexParams);
+ Assertions.assertEquals(1, regexProjects.size());
+ Assertions.assertEquals(project, regexProjects.get(0));
+
+ EntityInstance task = new EntityInstance(taskDefinition);
+ task.setValue("title", "Wire repository");
+ repository.addInstance(task);
+ repository.connectRelationship(project, "tasks", task);
+
+ EntityInstance secondTask = new EntityInstance(taskDefinition);
+ secondTask.setValue("title", "Write tests");
+ repository.addInstance(secondTask);
+ repository.connectRelationship(project, "tasks", secondTask);
+
+ Assertions.assertTrue(repository.getConnectedItems(task, "task-of").contains(project));
+ Assertions.assertTrue(repository.getConnectedItems(project, "tasks").contains(task));
+
+ Assertions.assertTrue(
+ repository.listRelatedInstances(task, "task-of").contains(project));
+ Assertions.assertTrue(
+ repository.listRelatedInstances(project, "tasks").contains(task));
+ Assertions.assertTrue(
+ repository.listRelatedInstances(project, "tasks").contains(secondTask));
+
+ QueryFilterParams relationshipFilterParams = new QueryFilterParams();
+ relationshipFilterParams.put("title", "*=Wire*");
+ List filteredTasks =
+ repository.listRelatedInstances(project, "tasks", relationshipFilterParams);
+ Assertions.assertEquals(1, filteredTasks.size());
+ Assertions.assertEquals(task, filteredTasks.get(0));
+
+ QueryFilterParams relationshipSortParams = new QueryFilterParams();
+ relationshipSortParams.put("sortBy", "-id");
+ List sortedTasks =
+ repository.listRelatedInstances(project, "tasks", relationshipSortParams);
+ Assertions.assertEquals("2", sortedTasks.get(0).getPrimaryKeyValue());
+ Assertions.assertEquals("1", sortedTasks.get(1).getPrimaryKeyValue());
+
+ repository.removeRelationshipsInvolving(project, task, "tasks");
+
+ Assertions.assertTrue(repository.getConnectedItems(task, "task-of").isEmpty());
+ Assertions.assertFalse(repository.getConnectedItems(project, "tasks").contains(task));
+ Assertions.assertTrue(repository.getConnectedItems(project, "tasks").contains(secondTask));
+
+ repository.removeRelationshipsInvolving(project, secondTask, "tasks");
+
+ Assertions.assertTrue(repository.getConnectedItems(project, "tasks").isEmpty());
+
+ repository.deleteEntityInstance(task);
+ repository.deleteEntityInstance(secondTask);
+
+ Assertions.assertEquals(0, repository.listInstances(taskDefinition).size());
+
+ repository.clearAllData();
+
+ Assertions.assertEquals(0, repository.listInstances(projectDefinition).size());
+ Assertions.assertEquals(0, repository.listInstances(taskDefinition).size());
+ }
+
+ private void exerciseProviderIsolation(final ThingRepositoryProvider provider) {
+ ERSchema schema = todoSchema();
+ ThingRepository defaultRepository = provider.getDefaultRepository();
+ defaultRepository.initializeFrom(schema);
+ provider.createRepository("session-one", schema);
+
+ EntityDefinition projectDefinition = schema.getEntityDefinitionNamed("project");
+ EntityInstance project = new EntityInstance(projectDefinition);
+ project.setValue("title", "Only default");
+
+ defaultRepository.addInstance(project);
+
+ Assertions.assertEquals(1, defaultRepository.listInstances(projectDefinition).size());
+ Assertions.assertEquals(
+ 0,
+ provider.getRepository("session-one").listInstances(projectDefinition).size());
+ }
+
+ private ERSchema todoSchema() {
+ ERSchema schema = new ERSchema();
+
+ EntityDefinition project = schema.defineEntity("project", "projects", -1);
+ project.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT));
+ project.addField(Field.is("title", FieldType.STRING).setMustBeUnique(true));
+
+ EntityDefinition task = schema.defineEntity("task", "tasks", -1);
+ task.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT));
+ task.addField(Field.is("title", FieldType.STRING));
+
+ schema.defineRelationship(task, project, "task-of", Cardinality.ONE_TO_ONE()).
+ whenReversed(Cardinality.ONE_TO_MANY(), "tasks");
+
+ return schema;
+ }
+}
diff --git a/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/repository/ThingRepositoryProviderConfigTest.java b/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/repository/ThingRepositoryProviderConfigTest.java
new file mode 100644
index 00000000..e4a33ed3
--- /dev/null
+++ b/ercoremodel/src/test/java/uk/co/compendiumdev/thingifier/core/repository/ThingRepositoryProviderConfigTest.java
@@ -0,0 +1,64 @@
+package uk.co.compendiumdev.thingifier.core.repository;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.file.Path;
+
+public class ThingRepositoryProviderConfigTest {
+
+ @TempDir
+ Path tempDir;
+
+ @Test
+ public void defaultsToCurrentInMemoryRepository() {
+ ThingRepositoryProviderConfig config = ThingRepositoryProviderConfig.fromArgs(new String[]{});
+
+ Assertions.assertEquals("memory", config.getRepositoryMode());
+ Assertions.assertTrue(config.createProvider() instanceof InMemoryThingRepositoryProvider);
+ }
+
+ @Test
+ public void canCreateSqliteInMemoryRepositoryFromArgs() {
+ ThingRepositoryProviderConfig config = ThingRepositoryProviderConfig.fromArgs(
+ new String[]{"-thingifier-repository=sqlite-memory"});
+
+ Assertions.assertEquals("sqlite-memory", config.getRepositoryMode());
+ Assertions.assertTrue(config.createProvider() instanceof SqliteThingRepositoryProvider);
+ }
+
+ @Test
+ public void canCreateSqliteInMemoryRepositoryFromChallengeMainShortcutArg() {
+ ThingRepositoryProviderConfig config = ThingRepositoryProviderConfig.fromArgs(
+ new String[]{"-sqlite-memory"});
+
+ Assertions.assertEquals("sqlite-memory", config.getRepositoryMode());
+ Assertions.assertTrue(config.createProvider() instanceof SqliteThingRepositoryProvider);
+ }
+
+ @Test
+ public void canCreateSqliteFileRepositoryFromArgs() {
+ ThingRepositoryProviderConfig config = ThingRepositoryProviderConfig.fromArgs(
+ new String[]{
+ "-thingifier-repository=sqlite-file",
+ "-thingifier-sqlite-directory=" + tempDir
+ });
+
+ Assertions.assertEquals("sqlite-file", config.getRepositoryMode());
+ Assertions.assertEquals(tempDir, config.getSqliteDirectory());
+ Assertions.assertTrue(config.createProvider() instanceof SqliteThingRepositoryProvider);
+ }
+
+ @Test
+ public void rejectsUnknownRepositoryModes() {
+ ThingRepositoryProviderConfig config = ThingRepositoryProviderConfig.fromArgs(
+ new String[]{"-thingifier-repository=unknown"});
+
+ IllegalArgumentException thrown = Assertions.assertThrows(
+ IllegalArgumentException.class,
+ config::createProvider);
+
+ Assertions.assertTrue(thrown.getMessage().contains("Unknown Thingifier repository mode"));
+ }
+}
diff --git a/examplemodels/src/main/java/uk/co/compendiumdev/thingifier/application/data/TodoAPIDataPopulator.java b/examplemodels/src/main/java/uk/co/compendiumdev/thingifier/application/data/TodoAPIDataPopulator.java
index a28df1ed..9f4e6ea4 100644
--- a/examplemodels/src/main/java/uk/co/compendiumdev/thingifier/application/data/TodoAPIDataPopulator.java
+++ b/examplemodels/src/main/java/uk/co/compendiumdev/thingifier/application/data/TodoAPIDataPopulator.java
@@ -1,31 +1,43 @@
package uk.co.compendiumdev.thingifier.application.data;
+import uk.co.compendiumdev.thingifier.core.domain.datapopulator.RepositoryDataPopulator;
import uk.co.compendiumdev.thingifier.core.domain.definitions.ERSchema;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
import uk.co.compendiumdev.thingifier.core.domain.instances.ERInstanceData;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceCollection;
-import uk.co.compendiumdev.thingifier.core.domain.datapopulator.DataPopulator;
+import uk.co.compendiumdev.thingifier.core.repository.ThingRepository;
-public class TodoAPIDataPopulator implements DataPopulator {
+public class TodoAPIDataPopulator implements RepositoryDataPopulator {
+
+ private static final String[] TODOS = {
+ "scan paperwork",
+ "file paperwork",
+ "process payments",
+ "escalate late payments",
+ "pay invoices",
+ "process payroll",
+ "train staff",
+ "schedule meeting"};
@Override
public void populate(final ERSchema schema, final ERInstanceData database) {
- String [] todos={
- "scan paperwork",
- "file paperwork",
- "process payments",
- "escalate late payments",
- "pay invoices",
- "process payroll",
- "train staff",
- "schedule meeting"};
-
EntityInstanceCollection todo = database.getInstanceCollectionForEntityNamed("todo");
- for(String todoItem : todos){
+ for(String todoItem : TODOS){
todo.addInstance(new EntityInstance(todo.definition())).
setValue("title", todoItem);
}
}
+
+ @Override
+ public void populate(final ERSchema schema, final ThingRepository repository) {
+ EntityDefinition todo = schema.getEntityDefinitionNamed("todo");
+
+ for (String todoItem : TODOS) {
+ repository.addInstance(new EntityInstance(todo).
+ setValue("title", todoItem));
+ }
+ }
}
diff --git a/examplemodels/src/main/java/uk/co/compendiumdev/thingifier/application/data/TodoManagerAPIDataPopulator.java b/examplemodels/src/main/java/uk/co/compendiumdev/thingifier/application/data/TodoManagerAPIDataPopulator.java
index a34aef69..d0146087 100644
--- a/examplemodels/src/main/java/uk/co/compendiumdev/thingifier/application/data/TodoManagerAPIDataPopulator.java
+++ b/examplemodels/src/main/java/uk/co/compendiumdev/thingifier/application/data/TodoManagerAPIDataPopulator.java
@@ -1,12 +1,14 @@
package uk.co.compendiumdev.thingifier.application.data;
+import uk.co.compendiumdev.thingifier.core.domain.datapopulator.RepositoryDataPopulator;
import uk.co.compendiumdev.thingifier.core.domain.definitions.ERSchema;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
import uk.co.compendiumdev.thingifier.core.domain.instances.ERInstanceData;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceCollection;
-import uk.co.compendiumdev.thingifier.core.domain.datapopulator.DataPopulator;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
+import uk.co.compendiumdev.thingifier.core.repository.ThingRepository;
-public class TodoManagerAPIDataPopulator implements DataPopulator {
+public class TodoManagerAPIDataPopulator implements RepositoryDataPopulator {
@Override
public void populate(final ERSchema schema, final ERInstanceData database) {
@@ -37,4 +39,31 @@ public void populate(final ERSchema schema, final ERInstanceData database) {
paperwork.getRelationships().connect("categories", officeCategory);
}
+
+ @Override
+ public void populate(final ERSchema schema, final ThingRepository repository) {
+ EntityDefinition todo = schema.getEntityDefinitionNamed("todo");
+ EntityDefinition category = schema.getEntityDefinitionNamed("category");
+ EntityDefinition project = schema.getEntityDefinitionNamed("project");
+
+ EntityInstance paperwork = repository.addInstance(new EntityInstance(todo).
+ setValue("title", "scan paperwork"));
+
+ EntityInstance filework = repository.addInstance(new EntityInstance(todo).
+ setValue("title", "file paperwork"));
+
+ EntityInstance officeCategory = repository.addInstance(new EntityInstance(category).
+ setValue("title", "Office"));
+
+ repository.addInstance(new EntityInstance(category).
+ setValue("title", "Home"));
+
+ EntityInstance officeWork = repository.addInstance(new EntityInstance(project).
+ setValue("title", "Office Work"));
+
+ repository.connectRelationship(officeWork, "tasks", paperwork);
+ repository.connectRelationship(officeWork, "tasks", filework);
+
+ repository.connectRelationship(paperwork, "categories", officeCategory);
+ }
}
diff --git a/examplemodels/src/main/java/uk/co/compendiumdev/thingifier/application/examples/TodoManagerThingifier.java b/examplemodels/src/main/java/uk/co/compendiumdev/thingifier/application/examples/TodoManagerThingifier.java
index 4558d572..a5f1d285 100644
--- a/examplemodels/src/main/java/uk/co/compendiumdev/thingifier/application/examples/TodoManagerThingifier.java
+++ b/examplemodels/src/main/java/uk/co/compendiumdev/thingifier/application/examples/TodoManagerThingifier.java
@@ -10,6 +10,7 @@
import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType;
import uk.co.compendiumdev.thingifier.core.domain.definitions.validation.VRule;
import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field;
+import uk.co.compendiumdev.thingifier.core.repository.ThingRepositoryProvider;
import static uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType.STRING;
@@ -19,6 +20,14 @@ public class TodoManagerThingifier {
REMEMBER - THIS IS THE PRODUCTION DEFINITION
*/
public Thingifier get() {
+ return get(new EntityRelModel());
+ }
+
+ public Thingifier get(final ThingRepositoryProvider repositoryProvider) {
+ return get(new EntityRelModel(repositoryProvider));
+ }
+
+ public Thingifier get(final EntityRelModel entityRelModel) {
// this is basically an Entity Relationship diagram as source
// TODO: should expand functionality based on E-R diagrams
@@ -40,7 +49,7 @@ public Thingifier get() {
// e.g. (.|\s)*\S(.|\s)* non empty
- Thingifier todoManager = new Thingifier();
+ Thingifier todoManager = new Thingifier(entityRelModel);
StringBuilder para = new StringBuilder();
diff --git a/pom.xml b/pom.xml
index 9641cdb1..26950ade 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,6 +16,7 @@
1.5.6-SNAPSHOT
2.2.27
2.8.6
+ 3.53.2.0
5.6.2
16
5.5.0
@@ -56,6 +57,7 @@
challengerAuto
standAloneTodoListRestApi
standAloneTodoListManagerRestApi
+ standAloneSqlLiteTodoListManagerRestApi
standAloneTodoListManagerRestApiAuto
thingifierapp
swaggerizer
@@ -157,4 +159,4 @@ Challenger
}
]
~~~~~~~~
- -->
\ No newline at end of file
+ -->
diff --git a/standAloneSqlLiteTodoListManagerRestApi/pom.xml b/standAloneSqlLiteTodoListManagerRestApi/pom.xml
new file mode 100644
index 00000000..f1393042
--- /dev/null
+++ b/standAloneSqlLiteTodoListManagerRestApi/pom.xml
@@ -0,0 +1,70 @@
+
+
+ 4.0.0
+
+
+ uk.co.compendiumdev.thingifier
+ thingifier-root
+ 1.5.6-SNAPSHOT
+
+
+ standAloneSqlLiteTodoListManagerRestApi
+
+
+
+ uk.co.compendiumdev
+ thingifier
+ ${thingifier.version}
+
+
+ uk.co.compendiumdev.thingifier
+ examplemodels
+ ${thingifier.version}
+
+
+ org.xerial
+ sqlite-jdbc
+ ${sqlite-jdbc-version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.3
+
+ ${java.version}
+ ${java.version}
+
+
+
+ maven-assembly-plugin
+ 3.3.0
+
+
+ package
+
+ single
+
+
+
+
+
+ jar-with-dependencies
+
+ runSqlLiteTodoManagerRestAPI-${project.version}
+ false
+
+
+ uk.co.compendiumdev.todolist.sqlite.application.Main
+
+
+
+
+
+
+
diff --git a/standAloneSqlLiteTodoListManagerRestApi/src/main/java/uk/co/compendiumdev/todolist/sqlite/application/Main.java b/standAloneSqlLiteTodoListManagerRestApi/src/main/java/uk/co/compendiumdev/todolist/sqlite/application/Main.java
new file mode 100644
index 00000000..094e1654
--- /dev/null
+++ b/standAloneSqlLiteTodoListManagerRestApi/src/main/java/uk/co/compendiumdev/todolist/sqlite/application/Main.java
@@ -0,0 +1,35 @@
+package uk.co.compendiumdev.todolist.sqlite.application;
+
+import uk.co.compendiumdev.thingifier.application.MainImplementation;
+import uk.co.compendiumdev.thingifier.application.examples.TodoManagerThingifier;
+import uk.co.compendiumdev.thingifier.core.repository.SqliteThingRepositoryProvider;
+
+import static spark.Spark.get;
+
+public class Main {
+
+ public static void main(String[] args) {
+
+ MainImplementation app = new MainImplementation();
+ app.registerModel("todoManager",
+ new TodoManagerThingifier().get(SqliteThingRepositoryProvider.inMemory()));
+
+ app.setDefaultsFromArgs(args);
+
+ app.configurePortAndDefaultRoutes();
+ app.setupBuiltInConfigurableRoutes();
+
+ app.chooseThingifier();
+ app.configureThingifierWithProfile();
+
+ app.setupDefaultGui();
+
+ get("/", (request, response) -> {
+ response.redirect("/gui/entities");
+ return "";
+ });
+
+ app.startRestServer();
+ app.addBuiltInArgConfiguredHooks();
+ }
+}
diff --git a/standAloneTodoListManagerRestApi/src/main/java/uk/co/compendiumdev/todolist/application/Main.java b/standAloneTodoListManagerRestApi/src/main/java/uk/co/compendiumdev/todolist/application/Main.java
index 656d6f3b..88476646 100644
--- a/standAloneTodoListManagerRestApi/src/main/java/uk/co/compendiumdev/todolist/application/Main.java
+++ b/standAloneTodoListManagerRestApi/src/main/java/uk/co/compendiumdev/todolist/application/Main.java
@@ -26,7 +26,7 @@ public static void main(String[] args) {
app.setupDefaultGui();
get("/", (request, response) -> {
- response.redirect("/gui");
+ response.redirect("/gui/entities");
return "";
});
diff --git a/standAloneTodoListManagerRestApiAuto/pom.xml b/standAloneTodoListManagerRestApiAuto/pom.xml
index 6714e449..d8579b61 100644
--- a/standAloneTodoListManagerRestApiAuto/pom.xml
+++ b/standAloneTodoListManagerRestApiAuto/pom.xml
@@ -20,6 +20,13 @@
test
+
+ uk.co.compendiumdev.thingifier
+ standAloneSqlLiteTodoListManagerRestApi
+ ${thingifier.version}
+ test
+
+
org.junit.jupiter
junit-jupiter-api
@@ -65,4 +72,4 @@
-
\ No newline at end of file
+
diff --git a/standAloneTodoListManagerRestApiAuto/src/test/java/uk/co/compendiumdev/sparkstart/Environment.java b/standAloneTodoListManagerRestApiAuto/src/test/java/uk/co/compendiumdev/sparkstart/Environment.java
index e1ae3505..5befd4b1 100644
--- a/standAloneTodoListManagerRestApiAuto/src/test/java/uk/co/compendiumdev/sparkstart/Environment.java
+++ b/standAloneTodoListManagerRestApiAuto/src/test/java/uk/co/compendiumdev/sparkstart/Environment.java
@@ -5,10 +5,13 @@
import io.restassured.filter.log.RequestLoggingFilter;
import io.restassured.filter.log.ResponseLoggingFilter;
import spark.Spark;
-import uk.co.compendiumdev.todolist.application.Main;
public class Environment {
+ private static final String APP_MODE_PROPERTY = "standalone.todo.manager.app";
+ private static final String APP_MODE_ENV = "STANDALONE_TODO_MANAGER_APP";
+ private static final String APP_ARGS_PROPERTY = "standalone.todo.manager.args";
+
public static String getEnv(String urlPath){
return getBaseUri() + urlPath;
}
@@ -30,12 +33,13 @@ public static String getBaseUri() {
}else{
//start it up
Spark.port(4567);
- String [] args;
-
- // todo support configuring to write tests against different versions
- args = "".split(",");
+ String[] args = configuredAppArgs();
- Main.main(args);
+ if (useSqliteMemoryApp()) {
+ uk.co.compendiumdev.todolist.sqlite.application.Main.main(args);
+ } else {
+ uk.co.compendiumdev.todolist.application.Main.main(args);
+ }
// wait till running
int maxtries=10;
@@ -54,4 +58,23 @@ public static String getBaseUri() {
// TODO: incorporate browsermob proxy and allow configuration of all
// requests through a proxy file to output a HAR file of all requests for later review
}
+
+ private static boolean useSqliteMemoryApp() {
+ String appMode = System.getProperty(APP_MODE_PROPERTY);
+ if (appMode == null || appMode.isBlank()) {
+ appMode = System.getenv(APP_MODE_ENV);
+ }
+
+ return "sqlite-memory".equalsIgnoreCase(appMode) ||
+ "sqlite".equalsIgnoreCase(appMode);
+ }
+
+ private static String[] configuredAppArgs() {
+ String configuredArgs = System.getProperty(APP_ARGS_PROPERTY, "");
+ if (configuredArgs.isBlank()) {
+ return new String[0];
+ }
+
+ return configuredArgs.split(",");
+ }
}
diff --git a/standAloneTodoListRestApi/src/main/java/uk/co/compendiumdev/todolist/application/Main.java b/standAloneTodoListRestApi/src/main/java/uk/co/compendiumdev/todolist/application/Main.java
index 51dcdce6..00b73ccf 100644
--- a/standAloneTodoListRestApi/src/main/java/uk/co/compendiumdev/todolist/application/Main.java
+++ b/standAloneTodoListRestApi/src/main/java/uk/co/compendiumdev/todolist/application/Main.java
@@ -27,7 +27,7 @@ public static void main(String[] args) {
app.setupDefaultGui();
get("/", (request, response) -> {
- response.redirect("/gui");
+ response.redirect("/gui/entities");
return "";
});
diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/Thingifier.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/Thingifier.java
index f6482ef9..86e1b7da 100644
--- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/Thingifier.java
+++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/Thingifier.java
@@ -7,11 +7,13 @@
import uk.co.compendiumdev.thingifier.apiconfig.ThingifierApiConfigProfiles;
import uk.co.compendiumdev.thingifier.core.EntityRelModel;
import uk.co.compendiumdev.thingifier.api.ermodelconversion.JsonPopulator;
+import uk.co.compendiumdev.thingifier.core.domain.datapopulator.RepositoryDataPopulator;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceCollection;
import uk.co.compendiumdev.thingifier.core.domain.datapopulator.DataPopulator;
import uk.co.compendiumdev.thingifier.core.domain.definitions.*;
import uk.co.compendiumdev.thingifier.core.domain.definitions.relationship.RelationshipDefinition;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
+import uk.co.compendiumdev.thingifier.core.repository.ThingRepository;
import uk.co.compendiumdev.thingifier.reporting.ThingReporter;
import java.util.*;
@@ -23,7 +25,7 @@ is the main class that allows access to:
- the API Definition and config
- TODO: why is the API documentation not in here?
*/
-public final class Thingifier {
+public final class Thingifier implements AutoCloseable {
private final EntityRelModel erm;
private ApiDocsConfig apiDocsConfig;
@@ -34,7 +36,11 @@ public final class Thingifier {
private final ThingifierApiConfigProfiles apiConfigProfiles;
public Thingifier(){
- erm = new EntityRelModel();
+ this(new EntityRelModel());
+ }
+
+ public Thingifier(final EntityRelModel erm) {
+ this.erm = erm;
title = "";
initialParagraph = "";
apiConfig = new ThingifierApiConfig("");
@@ -120,26 +126,47 @@ public boolean hasRelationshipNamed(final String relationshipName) {
// Instances
public List getThings(final String database) {
- return erm.getInstanceData(database).getAllInstanceCollections();
+ return erm.getRepository(database).getAllInstanceCollections();
}
public EntityInstance findThingInstanceByGuid(final String thingGUID, final String database) {
- return erm.getInstanceData(database).findEntityInstanceByGUID(thingGUID);
+ return erm.getRepository(database).findEntityInstanceByGUID(thingGUID);
}
-
+ @Deprecated
public EntityInstanceCollection getThingInstancesNamed(final String aName, final String database) {
- return erm.getInstanceData(database).getInstanceCollectionForEntityNamed(aName);
+ return erm.getRepository(database).getInstanceCollectionForEntityNamed(aName);
}
+ public List listThingInstancesNamed(final String aName, final String database) {
+ EntityDefinition definition = erm.getSchema().getDefinitionWithSingularOrPluralNamed(aName);
+ ThingRepository repository = erm.getRepository(database);
+ if (definition == null || repository == null) {
+ return Collections.emptyList();
+ }
+ return new ArrayList<>(repository.listInstances(definition));
+ }
+
+ public EntityInstance findThingInstanceByFieldNameAndValue(
+ final String entityName,
+ final String fieldName,
+ final String fieldValue,
+ final String database) {
+ EntityDefinition definition = erm.getSchema().getDefinitionWithSingularOrPluralNamed(entityName);
+ ThingRepository repository = erm.getRepository(database);
+ if (definition == null || repository == null) {
+ return null;
+ }
+ return repository.findInstanceByFieldNameAndValue(definition, fieldName, fieldValue);
+ }
-
+ @Deprecated
public EntityInstanceCollection getInstancesForSingularOrPluralNamedEntity(final String term, final String database) {
final EntityDefinition defn = erm.getSchema().getDefinitionWithSingularOrPluralNamed(term);
if(defn!=null){
final String entityName = defn.getName();
- return erm.getInstanceData(database).getInstanceCollectionForEntityNamed(entityName);
+ return erm.getRepository(database).getInstanceCollectionForEntityNamed(entityName);
}
return null;
@@ -157,18 +184,23 @@ public void clearAllData() {
}
public void clearAllData(final String database) {
- erm.getInstanceData(database).clearAllData();
+ erm.getRepository(database).clearAllData();
}
public void deleteThing(final EntityInstance aThingInstance, final String database) {
- erm.getInstanceData(database).deleteEntityInstance(aThingInstance);
+ erm.getRepository(database).deleteEntityInstance(aThingInstance);
}
// data generation
public void generateData(final String database) {
if(dataPopulator!=null){
- dataPopulator.populate(erm.getSchema(), erm.getInstanceData(database));
+ ThingRepository repository = erm.getRepository(database);
+ if (repository == null) {
+ return;
+ }
+ repository.refreshSchema(erm.getSchema());
+ populateRepository(repository);
}
}
@@ -217,6 +249,15 @@ public EntityRelModel getERmodel() {
return erm;
}
+ public ThingRepository getRepository(final String database) {
+ return erm.getRepository(database);
+ }
+
+ @Override
+ public void close() {
+ erm.close();
+ }
+
/*
TODO: these are documentation methods, why are they not in the
@@ -256,11 +297,9 @@ public void ensureCreatedAndPopulatedInstanceDatabaseNamed(String databaseName)
// if we created it then populate it
if(getDefaultDataPopulator()!=null){
// Use any default data populator to populate the new database
- getDefaultDataPopulator().
- populate(
- getERmodel().getSchema(),
- getERmodel().getInstanceData(databaseName)
- );
+ ThingRepository repository = getERmodel().getRepository(databaseName);
+ repository.refreshSchema(getERmodel().getSchema());
+ populateRepository(repository);
}
}
}
@@ -270,12 +309,23 @@ public void ensureCreatedAndPopulatedInstanceDatabaseFromJson(String databaseNam
new JsonPopulator(jsonDatabaseContents).populate(
getERmodel().getSchema(),
- getERmodel().getInstanceData(databaseName)
+ getERmodel().getRepository(databaseName).getInstanceData()
);
+ getERmodel().getRepository(databaseName).flush();
}
public ApiDocsConfig apidocsconfig() {
return apiDocsConfig;
}
+
+ private void populateRepository(final ThingRepository repository) {
+ if (dataPopulator instanceof RepositoryDataPopulator) {
+ ((RepositoryDataPopulator) dataPopulator).populate(erm.getSchema(), repository);
+ return;
+ }
+
+ dataPopulator.populate(erm.getSchema(), repository.getInstanceData());
+ repository.flush();
+ }
}
diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/BodyArgsProcessor.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/BodyArgsProcessor.java
index 79e2a2a8..9af76633 100644
--- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/BodyArgsProcessor.java
+++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/BodyArgsProcessor.java
@@ -73,9 +73,11 @@ public void identifyRelationships(List> fullargs,
instance.getEntity().related().getRelationships(relationshipName);
for(RelationshipVectorDefinition relate : relationshipsAre){
final EntityDefinition typeOfThing = relate.getTo();
- instanceToRelateTo = thingifier.getThingInstancesNamed(typeOfThing.getName(), database).
- findInstanceByFieldNameAndValue(relationshipFieldName,
- complexKeyValue.getValue());
+ instanceToRelateTo = thingifier.getRepository(database).
+ findInstanceByFieldNameAndValue(
+ typeOfThing,
+ relationshipFieldName,
+ complexKeyValue.getValue());
if(instanceToRelateTo!=null){
break;
}
diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/BodyCreationValidator.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/BodyCreationValidator.java
index d74a5f6e..43d1b17c 100644
--- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/BodyCreationValidator.java
+++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/BodyCreationValidator.java
@@ -7,6 +7,7 @@
import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType;
import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
+import uk.co.compendiumdev.thingifier.core.repository.ThingRepository;
import java.util.List;
import java.util.Map;
@@ -43,6 +44,36 @@ public ValidationReport validate(final BodyParser bodyargs, final EntityDefiniti
public ValidationReport areFieldsUnique(final BodyParser bodyargs, final EntityInstanceCollection thing,
List uniqueFields) {
+ final ValidationReport report = new ValidationReport();
+
+ for (Map.Entry entry : bodyargs.getFlattenedStringMap()) {
+
+ if (uniqueFields.contains(entry.getKey())) {
+ String existingValue = entry.getValue();
+
+ if (existingValue != null && existingValue.trim().length() > 0) {
+ final EntityInstance foundInstance = thing.findInstanceByFieldNameAndValue(
+ entry.getKey(),
+ entry.getValue()
+ );
+
+ if (foundInstance != null) {
+ report.setValid(false);
+ report.addErrorMessage(
+ String.format("Found Existing item with %s of %s",
+ entry.getKey(), entry.getValue()));
+ }
+ }
+ }
+ }
+ return report;
+ }
+
+ public ValidationReport areFieldsUnique(
+ final BodyParser bodyargs,
+ final EntityDefinition thingDefinition,
+ final ThingRepository repository,
+ final List uniqueFields) {
final ValidationReport report = new ValidationReport();
@@ -54,10 +85,13 @@ public ValidationReport areFieldsUnique(final BodyParser bodyargs, final EntityI
if (existingValue != null && existingValue.trim().length() > 0) {
// not unique if we can find something by that field value
- final EntityInstance foundInstance = thing.findInstanceByFieldNameAndValue(
- entry.getKey(),
- entry.getValue()
- );
+ final EntityInstance foundInstance;
+ if (repository == null) {
+ foundInstance = null;
+ } else {
+ foundInstance = repository.findInstanceByFieldNameAndValue(
+ thingDefinition, entry.getKey(), entry.getValue());
+ }
if (foundInstance!=null) {
report.setValid(false);
diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/BodyRelationshipValidator.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/BodyRelationshipValidator.java
index 25998f49..49ff580a 100644
--- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/BodyRelationshipValidator.java
+++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/BodyRelationshipValidator.java
@@ -86,8 +86,8 @@ private boolean validateCompressedRelationshipDefinition(
if(thingToRelateTo==null) {
final List relationshipsNamed = thingDefinition.related().getRelationships(relationShipName);
for(RelationshipVectorDefinition vector : relationshipsNamed){
- final EntityInstanceCollection thingToRelationship = thingifier.getThingInstancesNamed(vector.getTo().getName(), database);
- thingToRelateTo = thingToRelationship.findInstanceByPrimaryKey(guidValue);
+ thingToRelateTo = thingifier.getRepository(database).
+ findInstanceByQueryIdentifier(vector.getTo(), guidValue);
if(thingToRelateTo!=null){
break;
}
@@ -143,19 +143,22 @@ private boolean validateComplexFourPartRelationshipDefinition(
String uniqueId = complexKeyValue;
EntityInstance thingToRelateTo = null;
- EntityInstanceCollection things = thingifier.
- getInstancesForSingularOrPluralNamedEntity(relationshipToPart, database);
+ EntityDefinition things = thingifier.getERmodel().getSchema().
+ getDefinitionWithSingularOrPluralNamed(relationshipToPart);
if(things!=null){
- thingToRelateTo = things.findInstanceByPrimaryKey(uniqueId);
+ thingToRelateTo = thingifier.getRepository(database).
+ findInstanceByQueryIdentifier(things, uniqueId);
}
// haven't found it yet
if(thingToRelateTo==null){
- things = thingifier.getInstancesForSingularOrPluralNamedEntity(relationshipToPart, database);
+ things = thingifier.getERmodel().getSchema().
+ getDefinitionWithSingularOrPluralNamed(relationshipToPart);
if(things!=null) {
- thingToRelateTo = things.findInstanceByFieldNameAndValue(relationshipFieldPart, uniqueId);
+ thingToRelateTo = thingifier.getRepository(database).
+ findInstanceByFieldNameAndValue(things, relationshipFieldPart, uniqueId);
}
}
diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/EntityUrlMatcher.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/EntityUrlMatcher.java
new file mode 100644
index 00000000..f1e79243
--- /dev/null
+++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/EntityUrlMatcher.java
@@ -0,0 +1,74 @@
+package uk.co.compendiumdev.thingifier.api.restapihandlers;
+
+import uk.co.compendiumdev.thingifier.Thingifier;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
+import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
+
+final class EntityUrlMatcher {
+
+ private EntityUrlMatcher() {
+ }
+
+ static EntityDefinition entityFromCollectionUrl(final Thingifier thingifier, final String url) {
+ String[] parts = parts(url);
+ if (parts.length != 1) {
+ return null;
+ }
+ return thingifier.getERmodel().getSchema().
+ getDefinitionWithSingularOrPluralNamed(parts[0]);
+ }
+
+ static EntityDefinition entityFromInstanceUrl(final Thingifier thingifier, final String url) {
+ String[] parts = parts(url);
+ if (parts.length != 2) {
+ return null;
+ }
+ return thingifier.getERmodel().getSchema().
+ getDefinitionWithSingularOrPluralNamed(parts[0]);
+ }
+
+ static String identifierFromInstanceUrl(final String url) {
+ String[] parts = parts(url);
+ if (parts.length != 2) {
+ return null;
+ }
+ return parts[1];
+ }
+
+ static String entityTermFromUrl(final String url) {
+ String[] parts = parts(url);
+ if (parts.length == 0) {
+ return "";
+ }
+ return parts[0];
+ }
+
+ static boolean hasPartCount(final String url, final int count) {
+ return parts(url).length == count;
+ }
+
+ static EntityInstance findInstanceFromUrl(
+ final Thingifier thingifier, final String url, final String database) {
+ EntityDefinition entity = entityFromInstanceUrl(thingifier, url);
+ String identifier = identifierFromInstanceUrl(url);
+ if (entity == null || identifier == null) {
+ return null;
+ }
+ return thingifier.getRepository(database).
+ findInstanceByQueryIdentifier(entity, identifier);
+ }
+
+ private static String[] parts(final String url) {
+ String normalized = url == null ? "" : url.trim();
+ while (normalized.startsWith("/")) {
+ normalized = normalized.substring(1);
+ }
+ while (normalized.endsWith("/")) {
+ normalized = normalized.substring(0, normalized.length() - 1);
+ }
+ if (normalized.isEmpty()) {
+ return new String[0];
+ }
+ return normalized.split("/");
+ }
+}
diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipCreation.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipCreation.java
index 3faded3a..2d6584b9 100644
--- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipCreation.java
+++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipCreation.java
@@ -1,6 +1,5 @@
package uk.co.compendiumdev.thingifier.api.restapihandlers;
-import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceCollection;
import uk.co.compendiumdev.thingifier.Thingifier;
import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field;
import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType;
@@ -46,21 +45,22 @@ public ApiResponse create(final String url, final BodyParser bodyargs, final Sim
// if no way to narrow it down then use the first one TODO: potential bug if multiple named relationshps
relationshipToUse = possibleRelationships.get(0);
- EntityInstanceCollection thingTo = thingifier.getThingInstancesNamed(relationshipToUse.getTo().getName(), database);
+ EntityDefinition thingTo = relationshipToUse.getTo();
// if there is a guid in the body then use that to try and find a thing that matches it
// if there is a guid field or an id field then use whichever first matches a thing
boolean amExpectingARelatedItem = false;
String matchingFieldNames = "";
for(String fieldName : args.keySet()){
- final Field field = thingTo.definition().getField(fieldName);
+ final Field field = thingTo.getField(fieldName);
// in theory this is any 'key' unique field
if(field.getType()== FieldType.AUTO_GUID || field.getType() == FieldType.AUTO_INCREMENT){
amExpectingARelatedItem=true;
if(!matchingFieldNames.contains(fieldName+ " ")){
matchingFieldNames = matchingFieldNames + fieldName +" ";
}
- relatedItem = thingTo.findInstanceByFieldNameAndValue(fieldName, args.get(fieldName));
+ relatedItem = thingifier.getRepository(database).
+ findInstanceByFieldNameAndValue(thingTo, fieldName, args.get(fieldName));
if(relatedItem!=null){
// found something
break;
@@ -74,14 +74,14 @@ public ApiResponse create(final String url, final BodyParser bodyargs, final Sim
EntityInstance returnThing = null;
- EntityInstanceCollection thingToCreate = null;
+ EntityDefinition thingToCreate = null;
ApiResponse response = null;
// if we have a parent thing, but no GUID then can we create a Thing and connect it later?
if (relatedItem == null) {
EntityDefinition createThing = relationshipToUse.getTo();
- thingToCreate = thingifier.getThingInstancesNamed(createThing.getName(), database);
+ thingToCreate = createThing;
response = new ThingCreation(thingifier).with(bodyargs, thingToCreate, database);
if(response.isErrorResponse()){
@@ -131,7 +131,8 @@ public ApiResponse create(final String url, final BodyParser bodyargs, final Sim
return response;
}
- connectThis.getRelationships().connect(relationshipToUse.getName(), relatedItem);
+ thingifier.getRepository(database).connectRelationship(
+ connectThis, relationshipToUse.getName(), relatedItem);
// enforce cardinality on relationship
ValidationReport validNow = relatedItem.validateRelationships();
diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipCreator.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipCreator.java
index a7c8d4f6..cbafff1d 100644
--- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipCreator.java
+++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RelationshipCreator.java
@@ -3,6 +3,7 @@
import uk.co.compendiumdev.thingifier.Thingifier;
import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser;
import uk.co.compendiumdev.thingifier.api.response.ApiResponse;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
import java.util.ArrayList;
@@ -19,10 +20,14 @@ public ApiResponse createRelationships(final BodyParser bodyargs, final EntityIn
try {
List relationships = getRelationshipsFromArgs(bodyargs, instance, database);
for (RelationshipDetails relationship : relationships) {
- instance.getRelationships().connect(
+ EntityDefinition relatedEntity = thingifier.getERmodel().getSchema().
+ getDefinitionWithSingularOrPluralNamed(relationship.toType);
+ thingifier.getRepository(database).connectRelationship(
+ instance,
relationship.relationshipName,
- thingifier.getInstancesForSingularOrPluralNamedEntity(relationship.toType, database).
- findInstanceByFieldNameAndValue(relationship.guidName, relationship.guidValue));
+ thingifier.getRepository(database).
+ findInstanceByFieldNameAndValue(
+ relatedEntity, relationship.guidName, relationship.guidValue));
}
return ApiResponse.created(instance, thingifier.apiConfig());
diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiDeleteHandler.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiDeleteHandler.java
index b1cb0b80..780363f2 100644
--- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiDeleteHandler.java
+++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiDeleteHandler.java
@@ -1,9 +1,9 @@
package uk.co.compendiumdev.thingifier.api.restapihandlers;
import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock;
-import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceCollection;
import uk.co.compendiumdev.thingifier.Thingifier;
import uk.co.compendiumdev.thingifier.api.response.ApiResponse;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
import uk.co.compendiumdev.thingifier.core.query.SimpleQuery;
@@ -21,12 +21,23 @@ public ApiResponse handle(final String url, HttpHeadersBlock requestHeaders) {
String instanceDatabaseName = SessionHeaderParser.getDatabaseNameFromHeaderValue(requestHeaders);
// this should probably not delete root items
- EntityInstanceCollection thing = thingifier.getInstancesForSingularOrPluralNamedEntity(url, instanceDatabaseName);
+ EntityDefinition thing = EntityUrlMatcher.entityFromCollectionUrl(thingifier, url);
if (thing != null) {
// can't delete root level with a DELETE
return ApiResponse.error(405, "Cannot delete root level entity");
}
+ EntityDefinition entity = EntityUrlMatcher.entityFromInstanceUrl(thingifier, url);
+ if (entity != null) {
+ EntityInstance instance = EntityUrlMatcher.findInstanceFromUrl(
+ thingifier, url, instanceDatabaseName);
+ if (instance == null) {
+ return ApiResponse.error404(String.format("Could not find any instances with %s", url));
+ }
+ thingifier.deleteThing(instance, instanceDatabaseName);
+ return ApiResponse.success();
+ }
+
SimpleQuery queryResult = new SimpleQuery(thingifier.getERmodel().getSchema(), thingifier.getERmodel().getInstanceData(instanceDatabaseName), url).performQuery();
if (queryResult.wasItemFoundUnderARelationship()) {
@@ -35,7 +46,8 @@ public ApiResponse handle(final String url, HttpHeadersBlock requestHeaders) {
EntityInstance child = queryResult.getLastInstance();
// todo: this returns a list of 'items' to be removed based on
// removal of the relationships, these should really be deleted from thingifier now
- parent.getRelationships().removeRelationshipsInvolving(child, queryResult.getLastRelationshipName());
+ thingifier.getRepository(instanceDatabaseName).
+ removeRelationshipsInvolving(parent, child, queryResult.getLastRelationshipName());
} else {
List items = queryResult.getListEntityInstances();
if (items.isEmpty()) {
diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiGetHandler.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiGetHandler.java
index 730a6878..7c6e2d3b 100644
--- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiGetHandler.java
+++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiGetHandler.java
@@ -4,7 +4,9 @@
import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock;
import uk.co.compendiumdev.thingifier.api.response.ApiResponse;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
+import uk.co.compendiumdev.thingifier.core.query.QueryResult;
import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams;
+import uk.co.compendiumdev.thingifier.core.query.RepositoryBackedSimpleQuery;
import uk.co.compendiumdev.thingifier.core.query.SimpleQuery;
import java.util.List;
@@ -28,13 +30,28 @@ public ApiResponse handle(final String url, final QueryFilterParams queryParams,
String instanceDatabaseName = SessionHeaderParser.getDatabaseNameFromHeaderValue(requestHeaders);
- SimpleQuery queryResults;
-
- if(thingifier.apiConfig().forParams().willAllowFilteringThroughUrlParams()){
- queryResults = new SimpleQuery(thingifier.getERmodel().getSchema(), thingifier.getERmodel().getInstanceData(instanceDatabaseName), url).performQuery(
- queryParams);
- }else{
- queryResults = new SimpleQuery(thingifier.getERmodel().getSchema(), thingifier.getERmodel().getInstanceData(instanceDatabaseName), url).performQuery();
+ QueryResult queryResults;
+ boolean allowFiltering = thingifier.apiConfig().forParams().willAllowFilteringThroughUrlParams();
+ QueryFilterParams effectiveQueryParams = allowFiltering ? queryParams : new QueryFilterParams();
+
+ if (RepositoryBackedSimpleQuery.canHandle(thingifier.getERmodel().getSchema(), url)) {
+ queryResults = new RepositoryBackedSimpleQuery(
+ thingifier.getERmodel().getSchema(),
+ thingifier.getRepository(instanceDatabaseName),
+ url).
+ performQuery(effectiveQueryParams);
+ } else if(allowFiltering){
+ queryResults = new SimpleQuery(
+ thingifier.getERmodel().getSchema(),
+ thingifier.getERmodel().getInstanceData(instanceDatabaseName),
+ url).
+ performQuery(queryParams);
+ } else {
+ queryResults = new SimpleQuery(
+ thingifier.getERmodel().getSchema(),
+ thingifier.getERmodel().getInstanceData(instanceDatabaseName),
+ url).
+ performQuery();
}
// TODO: we should support pagination through query params
diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiPostHandler.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiPostHandler.java
index 821322d3..b5b28e90 100644
--- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiPostHandler.java
+++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiPostHandler.java
@@ -1,8 +1,8 @@
package uk.co.compendiumdev.thingifier.api.restapihandlers;
import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock;
-import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceCollection;
import uk.co.compendiumdev.thingifier.Thingifier;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
import uk.co.compendiumdev.thingifier.core.reporting.ValidationReport;
import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser;
import uk.co.compendiumdev.thingifier.api.response.ApiResponse;
@@ -30,11 +30,12 @@ public ApiResponse handle(final String url, final BodyParser args, final HttpHea
*/
// if queryis empty then need a way to check if the query matched
// create a thing
- EntityInstanceCollection instancesCollection = thingifier.getInstancesForSingularOrPluralNamedEntity(url, instanceDatabaseName);
- if (instancesCollection != null) {
+ EntityDefinition entityDefinition = EntityUrlMatcher.entityFromCollectionUrl(thingifier, url);
+ if (entityDefinition != null) {
// create a new thing does not enforce relationships
// TODO: validate before creation so as to only delete in an 'emergency' not as default
- final ApiResponse response = new ThingCreation(thingifier).with(args, instancesCollection, instanceDatabaseName);
+ final ApiResponse response = new ThingCreation(thingifier).with(
+ args, entityDefinition, instanceDatabaseName);
if (response.isErrorResponse()) {
return response;
}
@@ -47,7 +48,7 @@ public ApiResponse handle(final String url, final BodyParser args, final HttpHea
if (validity.isValid()) {
return response;
} else {
- instancesCollection.deleteInstance(response.getReturnedInstance());
+ thingifier.deleteThing(response.getReturnedInstance(), instanceDatabaseName);
return ApiResponse.error(400, validity.getErrorMessages()).addToErrorMessages("No new item created");
}
}
@@ -58,40 +59,36 @@ public ApiResponse handle(final String url, final BodyParser args, final HttpHea
*/
// amend a thing
// thing/guid
- String[] urlParts = url.split("/");
- if (urlParts.length == 2) {
+ entityDefinition = EntityUrlMatcher.entityFromInstanceUrl(thingifier, url);
+ if (entityDefinition != null) {
- String thingName = urlParts[0];
- instancesCollection = thingifier.getInstancesForSingularOrPluralNamedEntity(thingName, instanceDatabaseName);
+ String primaryKey = EntityUrlMatcher.identifierFromInstanceUrl(url);
- if (instancesCollection == null) {
- // this is not a URL for thing/guid
- // unknown thing
- return NoSuchEntity.response(thingName);
-
- }
-
- String primaryKey = urlParts[1];
-
- if(instancesCollection.definition().hasPrimaryKeyField()){
- EntityInstance instance = instancesCollection.findInstanceByPrimaryKey(primaryKey);
+ if(entityDefinition.hasPrimaryKeyField()){
+ EntityInstance instance = thingifier.getRepository(instanceDatabaseName).
+ findInstanceByQueryIdentifier(entityDefinition, primaryKey);
if (instance == null) {
// cannot amend something that does not exist
return ApiResponse.error404(String.format("No such %s entity instance with %s == %s found",
- instancesCollection.definition().getName(),
- instancesCollection.definition().getPrimaryKeyField().getName(),
+ entityDefinition.getName(),
+ entityDefinition.getPrimaryKeyField().getName(),
primaryKey)
);
}
return amendAThingWithPost(args, instance, instanceDatabaseName);
}else{
- return ApiResponse.error404(String.format("Entity %s does not have a primary key defined", instancesCollection.definition().getName()));
+ return ApiResponse.error404(String.format("Entity %s does not have a primary key defined", entityDefinition.getName()));
}
}
+ String thingName = EntityUrlMatcher.entityTermFromUrl(url);
+ if (!thingName.isEmpty() && EntityUrlMatcher.hasPartCount(url, 2)) {
+ return NoSuchEntity.response(thingName);
+ }
+
/*
Match a Relationship
diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiPutHandler.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiPutHandler.java
index 2553eeec..c73a16fe 100644
--- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiPutHandler.java
+++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/RestApiPutHandler.java
@@ -1,10 +1,10 @@
package uk.co.compendiumdev.thingifier.api.restapihandlers;
import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field;
import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType;
import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue;
-import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceCollection;
import uk.co.compendiumdev.thingifier.Thingifier;
import uk.co.compendiumdev.thingifier.api.http.bodyparser.BodyParser;
import uk.co.compendiumdev.thingifier.api.response.ApiResponse;
@@ -26,7 +26,7 @@ public ApiResponse handle(final String url, final BodyParser args, final HttpHea
// if queryis empty then need a way to check if the query matched
// create a thing
- EntityInstanceCollection thing = thingifier.getInstancesForSingularOrPluralNamedEntity(url, instanceDatabaseName);
+ EntityDefinition thing = EntityUrlMatcher.entityFromCollectionUrl(thingifier, url);
if (thing != null) {
// can't create a new thing at root level with PUT
return ApiResponse.error(405, "Cannot create root level entity with a PUT");
@@ -35,24 +35,22 @@ public ApiResponse handle(final String url, final BodyParser args, final HttpHea
// amend a thing
// thing/guid
- String[] urlParts = url.split("/");
- if (urlParts.length != 2) {
+ thing = EntityUrlMatcher.entityFromInstanceUrl(thingifier, url);
+ if (thing == null) {
// WHAT was that query?
+ if (EntityUrlMatcher.hasPartCount(url, 2)) {
+ String thingName = EntityUrlMatcher.entityTermFromUrl(url);
+ if (!thingName.isEmpty()) {
+ return NoSuchEntity.response(thingName);
+ }
+ }
return ApiResponse.error(400, "Your request was not understood");
}
- String thingName = urlParts[0];
- thing = thingifier.getInstancesForSingularOrPluralNamedEntity(thingName, instanceDatabaseName);
-
- if (thing == null) {
- // this is not a URL for thing/guid
- // unknown thing
- return NoSuchEntity.response(urlParts[0]);
- }
-
- String instanceGuid = urlParts[1];
+ String instanceGuid = EntityUrlMatcher.identifierFromInstanceUrl(url);
- EntityInstance instance = thing.findInstanceByPrimaryKey(instanceGuid);
+ EntityInstance instance = thingifier.getRepository(instanceDatabaseName).
+ findInstanceByQueryIdentifier(thing, instanceGuid);
if (instance == null) {
@@ -60,7 +58,7 @@ public ApiResponse handle(final String url, final BodyParser args, final HttpHea
// if the primary key is an AUTO i.e. AUTO_GUID or AUTO_INCREMENT then we should not be able to create it
// in fact if there are any fields at all which are AUTO then we should not be able to create it with PUT
- List forbiddenPutCreationFields = thing.definition().getFieldsOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID);
+ List forbiddenPutCreationFields = thing.getFieldsOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID);
if(forbiddenPutCreationFields.size()>0){
String names = "";
@@ -70,7 +68,7 @@ public ApiResponse handle(final String url, final BodyParser args, final HttpHea
}
names = names + field.getName();
}
- return ApiResponse.error(400, String.format("Cannot create %s with PUT due to Auto fields %s", thing.definition().getName(), names));
+ return ApiResponse.error(400, String.format("Cannot create %s with PUT due to Auto fields %s", thing.getName(), names));
}
@@ -78,10 +76,10 @@ public ApiResponse handle(final String url, final BodyParser args, final HttpHea
// any field in the body for the primary key must match the primarykey field
List fieldValues = FieldValues.fromListMapEntryStringString(args.getFlattenedStringMap());
for( NamedValue namedValue : fieldValues){
- if(namedValue.name.equals(thing.definition().getPrimaryKeyField().getName())){
+ if(namedValue.name.equals(thing.getPrimaryKeyField().getName())){
if(!namedValue.value.equals(instanceGuid)){
// primary key does not match the value in the message
- return ApiResponse.error(400, String.format("Cannot create %s with PUT as key does not match body value %s != %s", thing.definition().getName(), instanceGuid, namedValue.value));
+ return ApiResponse.error(400, String.format("Cannot create %s with PUT as key does not match body value %s != %s", thing.getName(), instanceGuid, namedValue.value));
}
}
}
diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingAmendment.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingAmendment.java
index 04f8ca49..7b323185 100644
--- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingAmendment.java
+++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingAmendment.java
@@ -66,8 +66,7 @@ public ApiResponse amendInstance(final BodyParser bodyargs, final EntityInstance
ValidationReport relationshipsValidation = new BodyRelationshipValidator(thingifier).validate(bodyargs, cloned.getEntity(), database);
validation.combine(relationshipsValidation);
- ValidationReport uniquenessCheck = thingifier.getERmodel().getInstanceData(database).
- getInstanceCollectionForEntityNamed(instance.getEntity().getName()).
+ ValidationReport uniquenessCheck = thingifier.getRepository(database).
checkFieldsForUniqueNess(cloned, true);
validation.combine(uniquenessCheck);
@@ -76,7 +75,7 @@ public ApiResponse amendInstance(final BodyParser bodyargs, final EntityInstance
instance.clearAllFields();
// delete all existing relationships for idempotent amend
// todo: this returns a list of 'items' to be removed based on relationship
- instance.getRelationships().removeAllRelationships();
+ thingifier.getRepository(database).removeAllRelationships(instance);
}
List fieldValues = FieldValues.
fromListMapEntryStringString(
@@ -85,6 +84,8 @@ public ApiResponse amendInstance(final BodyParser bodyargs, final EntityInstance
new EntityInstanceBulkUpdater(instance).setFieldValuesFrom(fieldValues);
+ thingifier.getRepository(database).updateInstance(instance);
+
// todo: should we check that this was actually a success?
final ApiResponse relresponse = new RelationshipCreator(thingifier).createRelationships(bodyargs, instance, database);
// todo: should check if any of the 'removed items due to relationship removal' need to be removed
diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingCreation.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingCreation.java
index 8aaa1c1b..4eb2d6b5 100644
--- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingCreation.java
+++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/api/restapihandlers/ThingCreation.java
@@ -1,6 +1,7 @@
package uk.co.compendiumdev.thingifier.api.restapihandlers;
import uk.co.compendiumdev.thingifier.core.domain.definitions.field.instance.NamedValue;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceCollection;
import uk.co.compendiumdev.thingifier.Thingifier;
import uk.co.compendiumdev.thingifier.core.reporting.ValidationReport;
@@ -21,6 +22,10 @@ public ThingCreation(final Thingifier thingifier) {
}
public ApiResponse with(final BodyParser bodyargs, final EntityInstanceCollection thing, final String database) {
+ return with(bodyargs, thing.definition(), database);
+ }
+
+ public ApiResponse with(final BodyParser bodyargs, final EntityDefinition thing, final String database) {
ValidationReport validated = new BodyRelationshipValidator(thingifier).validate(bodyargs, thing, database);
@@ -34,32 +39,42 @@ public ApiResponse with(final BodyParser bodyargs, final EntityInstanceCollectio
}
// todo: separate validation for creation of 'cannot' create with ID, or cannot create with GUID
- EntityInstance instance = new EntityInstance(thing.definition());
+ EntityInstance instance = new EntityInstance(thing);
instance.addAutoGUIDstoInstance();
- instance.addAutoIncrementIdsToInstance(thing.getCounters());
+ instance.addAutoIncrementIdsToInstance(
+ thingifier.getRepository(database).countersFor(thing));
return addNewThingWithFields(bodyargs, instance, thing, database);
}
// create with GUID and IDs is normally associated with PUT or 'insert'
public ApiResponse withPrimaryKey(final String primaryKey, final BodyParser bodyargs,
final EntityInstanceCollection thing, final String database) {
+ return withPrimaryKey(primaryKey, bodyargs, thing.definition(), database);
+ }
+
+ public ApiResponse withPrimaryKey(final String primaryKey, final BodyParser bodyargs,
+ final EntityDefinition thing, final String database) {
EntityInstance instance;
ValidationReport validated;
validated = new BodyCreationValidator(thingifier).
- areFieldsUnique(bodyargs, thing,
- thing.definition().getFieldNamesOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID));
+ areFieldsUnique(
+ bodyargs,
+ thing,
+ thingifier.getRepository(database),
+ thing.getFieldNamesOfType(FieldType.AUTO_INCREMENT, FieldType.AUTO_GUID));
if(!validated.isValid()){
return ApiResponse.error(409,"Cannot Create with duplicate values: "+
validated.getCombinedErrorMessages());
}
- instance = new EntityInstance(thing.definition());
- instance.overrideValue(thing.definition().getPrimaryKeyField().getName(), primaryKey);
+ instance = new EntityInstance(thing);
+ instance.overrideValue(thing.getPrimaryKeyField().getName(), primaryKey);
// TODO: should we really do this for a PUT when everything needs to exist, suspect this is a bug
- instance.addAutoIncrementIdsToInstance(thing.getCounters());
+ instance.addAutoIncrementIdsToInstance(
+ thingifier.getRepository(database).countersFor(thing));
validated = new BodyRelationshipValidator(thingifier).validate(bodyargs, thing, database);
@@ -76,14 +91,14 @@ public ApiResponse withPrimaryKey(final String primaryKey, final BodyParser body
bodyargs.getFlattenedStringMap());
- thing.setNextIdCountersToAccomodate(fieldValues);
+ thingifier.getRepository(database).setNextIdCountersToAccomodate(thing, fieldValues);
return insertNewThingWithFields(bodyargs, instance, thing, database);
}
private ApiResponse addNewThingWithFields(final BodyParser bodyargs, final EntityInstance instance,
- final EntityInstanceCollection thing, final String database) {
+ final EntityDefinition thing, final String database) {
if(thingifier.apiConfig().willApiEnforceDeclaredTypesInInput()) {
ValidationReport validatedTypes = bodyargs.validateAgainstType(instance.getEntity());
@@ -116,7 +131,7 @@ private ApiResponse addNewThingWithFields(final BodyParser bodyargs, final Entit
}
private ApiResponse insertNewThingWithFields(final BodyParser bodyargs, final EntityInstance instance,
- final EntityInstanceCollection thing, final String database) {
+ final EntityDefinition thing, final String database) {
if(thingifier.apiConfig().willApiEnforceDeclaredTypesInInput()) {
ValidationReport validatedTypes = bodyargs.validateAgainstType(instance.getEntity());
@@ -145,17 +160,16 @@ private ApiResponse insertNewThingWithFields(final BodyParser bodyargs, final En
return addValidatedInstance(bodyargs, instance, thing, database, validation);
}
- private ApiResponse addValidatedInstance(BodyParser bodyargs, EntityInstance instance, EntityInstanceCollection thing, String database, ValidationReport validation) {
+ private ApiResponse addValidatedInstance(BodyParser bodyargs, EntityInstance instance, EntityDefinition thing, String database, ValidationReport validation) {
// check for uniqueness prior to adding
- ValidationReport uniquenessCheck = thingifier.getERmodel().getInstanceData(database).
- getInstanceCollectionForEntityNamed(instance.getEntity().getName()).
+ ValidationReport uniquenessCheck = thingifier.getRepository(database).
checkFieldsForUniqueNess(instance, false);
validation.combine(uniquenessCheck);
if (validation.isValid()) {
try {
- thing.addInstance(instance);
+ thingifier.getRepository(database).addInstance(instance);
}catch(Exception e){
return ApiResponse.error(400, e.getMessage());
}
diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/MainImplementation.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/MainImplementation.java
index e1563d65..7f043e08 100644
--- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/MainImplementation.java
+++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/MainImplementation.java
@@ -28,7 +28,7 @@
This is a bit too tied down to the concept of one app, one thingifier at a time though.
We should be able to have multiple thingifiers at different endpoints e.g. /api1/, /api2/ etc.
*/
-public class MainImplementation {
+public class MainImplementation implements AutoCloseable {
Integer proxyport;
private Map thingifierModels;
@@ -253,7 +253,7 @@ public void configurePortAndDefaultRoutes() {
public void setupBuiltInConfigurableRoutes() {
if(allowShutdown) {
apiDefn.addRoutesToDocumentation(
- new ShutdownRouteHandler().
+ new ShutdownRouteHandler(this).
configureRoutes().
getRoutes());
}
@@ -391,4 +391,11 @@ public DefaultGUIHTML getGuiManagement() {
public ThingifierApiDocumentationDefn getApiDefn() {
return apiDefn;
}
+
+ @Override
+ public void close() {
+ for (Thingifier model : thingifierModels.values()) {
+ model.close();
+ }
+ }
}
diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/routehandlers/ShutdownRouteHandler.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/routehandlers/ShutdownRouteHandler.java
index 6db9ed56..c4d64cbd 100644
--- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/routehandlers/ShutdownRouteHandler.java
+++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/application/routehandlers/ShutdownRouteHandler.java
@@ -12,9 +12,15 @@
public class ShutdownRouteHandler {
List routes;
+ private final AutoCloseable closeable;
public ShutdownRouteHandler(){
+ this(null);
+ }
+
+ public ShutdownRouteHandler(final AutoCloseable closeable){
routes = new ArrayList();
+ this.closeable = closeable;
}
public List getRoutes(){
@@ -24,6 +30,9 @@ public List getRoutes(){
public ShutdownRouteHandler configureRoutes() {
get("/shutdown", (request, result) -> {
+ if (closeable != null) {
+ closeable.close();
+ }
System.exit(0);
return "";
});
diff --git a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/htmlgui/htmlgen/DefaultGuiHtmlPages.java b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/htmlgui/htmlgen/DefaultGuiHtmlPages.java
index b2aa114c..82c93496 100644
--- a/thingifier/src/main/java/uk/co/compendiumdev/thingifier/htmlgui/htmlgen/DefaultGuiHtmlPages.java
+++ b/thingifier/src/main/java/uk/co/compendiumdev/thingifier/htmlgui/htmlgen/DefaultGuiHtmlPages.java
@@ -12,9 +12,10 @@
import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType;
import uk.co.compendiumdev.thingifier.core.domain.definitions.relationship.RelationshipVectorDefinition;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
-import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceCollection;
+import uk.co.compendiumdev.thingifier.core.repository.ThingRepository;
-import java.util.Collection;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
public class DefaultGuiHtmlPages {
@@ -119,16 +120,20 @@ public String getInstancesListPage(String database, String entityName) {
htmlErrorMessage = htmlErrorMessage + "Database Named " + HtmlUtils.sanitise(database) + " not found. Have you made any API Calls?" + tryDefault + "
";
}
- EntityInstanceCollection thing = null;
+ EntityDefinition definition = thingifier.getERmodel().
+ getSchema().
+ getDefinitionWithSingularOrPluralNamed(entityName);
+ List instances = new ArrayList<>();
if (htmlErrorMessage.isEmpty()) {
try {
- thing = thingifier.getThingInstancesNamed(entityName, database);
+ ThingRepository repository = thingifier.getRepository(database);
+ instances = new ArrayList<>(repository.listInstances(definition));
} catch (Exception e) {
//htmlErrorMessage = htmlErrorMessage + "Database Access Error: " + e.getMessage() + ".
";
}
- if (thing == null) {
+ if (definition == null) {
htmlErrorMessage = htmlErrorMessage + "Entity instances not found in database, have you made any API calls?" + tryDefault + "
";
}
}
@@ -141,13 +146,11 @@ public String getInstancesListPage(String database, String entityName) {
try {
- final EntityDefinition definition = thing.definition();
-
html.append("" + definition.getPlural() + " ");
html.append(startHtmlTableFor(definition));
- for (EntityInstance instance : thing.getInstances()) {
+ for (EntityInstance instance : instances) {
html.append(htmlTableRowFor(instance, database));
}
@@ -276,16 +279,19 @@ public String getInstanceDetailsPage(String database, String entityName, MapDatabase Access Error: " + e.getMessage() + ".
";
}
- if(thing == null){
+ if(definition == null || repository == null){
htmlErrorMessage = htmlErrorMessage + "Entity instances not found in database, have you made any API calls?" + tryDefault + "
";
}
}
@@ -297,9 +303,11 @@ public String getInstanceDetailsPage(String database, String entityName, MapInstances not found in database, have you made any API calls?" + tryDefault + "";
}
@@ -324,7 +333,6 @@ public String getInstanceDetailsPage(String database, String entityName, Map");
- if (instance.getRelationships().hasAnyRelationshipInstances()) {
+ if (!definition.related().getRelationships().isEmpty()) {
html.append("Relationships ");
for (RelationshipVectorDefinition relationship : definition.related().getRelationships()) {
- final Collection relatedItems = instance.getRelationships().getConnectedItems(relationship.getName());
+ final List relatedItems =
+ repository.listRelatedInstances(instance, relationship.getName());
html.append("" + relationship.getName() + " ");
if (!relatedItems.isEmpty()) {
boolean header = true;
diff --git a/thingifier/src/test/java/uk/co/compendiumdev/casestudy/todomanager/api_non_http/RelationshipApiSqliteRepositoryTest.java b/thingifier/src/test/java/uk/co/compendiumdev/casestudy/todomanager/api_non_http/RelationshipApiSqliteRepositoryTest.java
new file mode 100644
index 00000000..60379606
--- /dev/null
+++ b/thingifier/src/test/java/uk/co/compendiumdev/casestudy/todomanager/api_non_http/RelationshipApiSqliteRepositoryTest.java
@@ -0,0 +1,70 @@
+package uk.co.compendiumdev.casestudy.todomanager.api_non_http;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import uk.co.compendiumdev.thingifier.Thingifier;
+import uk.co.compendiumdev.thingifier.api.http.headers.HttpHeadersBlock;
+import uk.co.compendiumdev.thingifier.api.response.ApiResponse;
+import uk.co.compendiumdev.thingifier.core.EntityRelModel;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.Cardinality;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType;
+import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
+import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams;
+import uk.co.compendiumdev.thingifier.core.repository.SqliteThingRepositoryProvider;
+import uk.co.compendiumdev.thingifier.core.repository.ThingRepository;
+
+public class RelationshipApiSqliteRepositoryTest {
+
+ @Test
+ public void getRelationshipPathsUseRepositoryWithoutLoadingCompatibilitySnapshot() {
+ try (Thingifier todoManager = new Thingifier(
+ new EntityRelModel(SqliteThingRepositoryProvider.inMemory()))) {
+ EntityDefinition todo = todoManager.defineThing("todo", "todos");
+ todo.addAsPrimaryKeyField(Field.is("guid", FieldType.AUTO_GUID));
+ todo.addField(Field.is("title", FieldType.STRING));
+
+ EntityDefinition project = todoManager.defineThing("project", "projects");
+ project.addAsPrimaryKeyField(Field.is("guid", FieldType.AUTO_GUID));
+ project.addField(Field.is("title", FieldType.STRING));
+
+ todoManager.defineRelationship(project, todo, "tasks", Cardinality.ONE_TO_MANY()).
+ whenReversed(Cardinality.ONE_TO_MANY(), "task-of");
+
+ ThingRepository repository =
+ todoManager.getRepository(EntityRelModel.DEFAULT_DATABASE_NAME);
+
+ EntityInstance task = repository.addInstance(new EntityInstance(todo).
+ setValue("title", "SQLite relationship task"));
+ EntityInstance projectInstance = repository.addInstance(new EntityInstance(project).
+ setValue("title", "SQLite relationship project"));
+
+ repository.connectRelationship(projectInstance, "tasks", task);
+
+ ApiResponse tasksResponse = todoManager.api().get(
+ String.format("project/%s/tasks", projectInstance.getPrimaryKeyValue()),
+ new QueryFilterParams(),
+ new HttpHeadersBlock());
+
+ Assertions.assertEquals(200, tasksResponse.getStatusCode());
+ Assertions.assertTrue(tasksResponse.isCollection());
+ Assertions.assertEquals(1, tasksResponse.getReturnedInstanceCollection().size());
+ Assertions.assertEquals(task.getPrimaryKeyValue(),
+ tasksResponse.getReturnedInstanceCollection().get(0).getPrimaryKeyValue());
+ Assertions.assertFalse(repository.hasLoadedCompatibilitySnapshot());
+
+ ApiResponse projectResponse = todoManager.api().get(
+ String.format("todo/%s/task-of", task.getPrimaryKeyValue()),
+ new QueryFilterParams(),
+ new HttpHeadersBlock());
+
+ Assertions.assertEquals(200, projectResponse.getStatusCode());
+ Assertions.assertTrue(projectResponse.isCollection());
+ Assertions.assertEquals(1, projectResponse.getReturnedInstanceCollection().size());
+ Assertions.assertEquals(projectInstance.getPrimaryKeyValue(),
+ projectResponse.getReturnedInstanceCollection().get(0).getPrimaryKeyValue());
+ Assertions.assertFalse(repository.hasLoadedCompatibilitySnapshot());
+ }
+ }
+}
diff --git a/thingifier/src/test/java/uk/co/compendiumdev/thingifier/htmlgui/htmlgen/DefaultGuiHtmlPagesRepositoryTest.java b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/htmlgui/htmlgen/DefaultGuiHtmlPagesRepositoryTest.java
new file mode 100644
index 00000000..6b16428f
--- /dev/null
+++ b/thingifier/src/test/java/uk/co/compendiumdev/thingifier/htmlgui/htmlgen/DefaultGuiHtmlPagesRepositoryTest.java
@@ -0,0 +1,63 @@
+package uk.co.compendiumdev.thingifier.htmlgui.htmlgen;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import uk.co.compendiumdev.thingifier.Thingifier;
+import uk.co.compendiumdev.thingifier.core.EntityRelModel;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.Cardinality;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.EntityDefinition;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.Field;
+import uk.co.compendiumdev.thingifier.core.domain.definitions.field.definition.FieldType;
+import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
+import uk.co.compendiumdev.thingifier.core.repository.SqliteThingRepositoryProvider;
+import uk.co.compendiumdev.thingifier.core.repository.ThingRepository;
+
+import java.util.Map;
+
+public class DefaultGuiHtmlPagesRepositoryTest {
+
+ @Test
+ public void guiInstancePagesReadFromRepositoryWithoutLoadingCompatibilitySnapshot() {
+ try (Thingifier thingifier = new Thingifier(
+ new EntityRelModel(SqliteThingRepositoryProvider.inMemory()))) {
+
+ EntityDefinition project = thingifier.defineThing("project", "projects");
+ project.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT));
+ project.addField(Field.is("title", FieldType.STRING));
+
+ EntityDefinition todo = thingifier.defineThing("todo", "todos");
+ todo.addAsPrimaryKeyField(Field.is("id", FieldType.AUTO_INCREMENT));
+ todo.addField(Field.is("title", FieldType.STRING));
+
+ thingifier.defineRelationship(project, todo, "tasks", Cardinality.ONE_TO_MANY()).
+ whenReversed(Cardinality.ONE_TO_MANY(), "tasksof");
+
+ ThingRepository repository =
+ thingifier.getRepository(EntityRelModel.DEFAULT_DATABASE_NAME);
+
+ EntityInstance projectInstance = repository.addInstance(new EntityInstance(project).
+ setValue("title", "Repository Project"));
+ EntityInstance todoInstance = repository.addInstance(new EntityInstance(todo).
+ setValue("title", "Repository Todo"));
+
+ repository.connectRelationship(projectInstance, "tasks", todoInstance);
+
+ DefaultGuiHtmlPages pages = new DefaultGuiHtmlPages(
+ new DefaultGUIHTML(), thingifier, "/gui");
+
+ String listHtml = pages.getInstancesListPage(
+ EntityRelModel.DEFAULT_DATABASE_NAME, "todo");
+ Assertions.assertTrue(listHtml.contains("Repository Todo"), listHtml);
+ Assertions.assertFalse(repository.hasLoadedCompatibilitySnapshot());
+
+ String detailHtml = pages.getInstanceDetailsPage(
+ EntityRelModel.DEFAULT_DATABASE_NAME,
+ "project",
+ Map.of("id", projectInstance.getPrimaryKeyValue()));
+
+ Assertions.assertTrue(detailHtml.contains("Repository Project"), detailHtml);
+ Assertions.assertTrue(detailHtml.contains("Repository Todo"), detailHtml);
+ Assertions.assertFalse(repository.hasLoadedCompatibilitySnapshot());
+ }
+ }
+}