From 07a6e1bd13594a3e37911823e772518439238f85 Mon Sep 17 00:00:00 2001 From: "boris.sukhodoev" Date: Wed, 19 Nov 2025 14:15:13 +0200 Subject: [PATCH 01/15] feat: add extended property types --- cqp-api/build.gradle.kts | 4 +- .../com/quantori/cqp/api/model/Property.java | 43 ++++++++++++- .../quantori/cqp/api/model/PropertyValue.java | 53 ++++++++++++++++ .../cqp/api/model/PropertyTypeTest.java | 60 +++++++++++++++++++ cqp-core/build.gradle.kts | 2 +- cqp-storage-elasticsearch/build.gradle.kts | 2 +- 6 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 cqp-api/src/main/java/com/quantori/cqp/api/model/PropertyValue.java create mode 100644 cqp-api/src/test/java/com/quantori/cqp/api/model/PropertyTypeTest.java diff --git a/cqp-api/build.gradle.kts b/cqp-api/build.gradle.kts index 6e6c921..e6ae25e 100644 --- a/cqp-api/build.gradle.kts +++ b/cqp-api/build.gradle.kts @@ -6,7 +6,7 @@ plugins { group = "com.quantori" description = "Chem query platform. Storage API" -version = "0.0.16" +version = "0.0.17" dependencies { implementation("commons-codec:commons-codec:1.15") @@ -34,4 +34,4 @@ dependencies { testImplementation(libs.jackson) testImplementation(libs.lombok) testAnnotationProcessor(libs.lombok) -} \ No newline at end of file +} diff --git a/cqp-api/src/main/java/com/quantori/cqp/api/model/Property.java b/cqp-api/src/main/java/com/quantori/cqp/api/model/Property.java index 24ebd74..0f1314a 100644 --- a/cqp-api/src/main/java/com/quantori/cqp/api/model/Property.java +++ b/cqp-api/src/main/java/com/quantori/cqp/api/model/Property.java @@ -21,7 +21,48 @@ public Property(String name, PropertyType type) { this.type = type; } + /** + * Defines the supported property kinds in Chem Query Platform. Existing values must remain + * unchanged to keep backward compatibility with previously persisted data. + */ public enum PropertyType { - STRING, DECIMAL, DATE + STRING, + DECIMAL, + DATE, + /** + * Binary payload for storing images, PDFs or other small files. Intended size limit is 10 MB per + * value and the binary content is transferred as byte arrays. + */ + BINARY, + /** + * Timestamp value with timezone information preserved. Values are serialized as ISO-8601 + * instants (e.g. {@code 2025-01-15T10:30:00Z}). + */ + DATE_TIME, + /** + * Ordered collection of string values. This allows preserving the order in which the user + * provided the values (e.g. {@code ["first", "second"]}). + */ + LIST, + /** + * Hyperlink that stores HTTP/HTTPS (and similar) URL references. Validation is applied on + * backend layers while the enum only marks the data type. + */ + HYPERLINK, + /** + * Chemical structure serialized as a SMILES string. Consumers rely on Indigo toolkit for + * validation. + */ + CHEMICAL_STRUCTURE, + /** + * Three dimensional molecular structure stored as MOL text block. This is typically used for 3D + * renderings and cannot be exported to legacy SDF files. + */ + STRUCTURE_3D, + /** + * Sanitized HTML fragment used for rich text property rendering. Scripts and unsafe tags are + * expected to be removed before persisting. + */ + HTML } } diff --git a/cqp-api/src/main/java/com/quantori/cqp/api/model/PropertyValue.java b/cqp-api/src/main/java/com/quantori/cqp/api/model/PropertyValue.java new file mode 100644 index 0000000..5319d93 --- /dev/null +++ b/cqp-api/src/main/java/com/quantori/cqp/api/model/PropertyValue.java @@ -0,0 +1,53 @@ +package com.quantori.cqp.api.model; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Container for the value of a {@link Property}. Each concrete field maps to a {@link + * Property.PropertyType}. Only one field is expected to be non-null for a particular value. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PropertyValue { + + /** String payload for {@link Property.PropertyType#STRING} values. */ + private String stringValue; + + /** Decimal payload for {@link Property.PropertyType#DECIMAL} values. */ + private Double decimalValue; + + /** Date payload for {@link Property.PropertyType#DATE} values. */ + private LocalDate dateValue; + + /** + * Binary content for {@link Property.PropertyType#BINARY} values. Typical use cases include + * storing images or PDF attachments (up to 10 MB). + */ + private byte[] binaryValue; + + /** Timestamp payload for {@link Property.PropertyType#DATE_TIME} values. */ + private Instant dateTimeValue; + + /** Ordered collection of strings for {@link Property.PropertyType#LIST} values. */ + private List listValue; + + /** URL string for {@link Property.PropertyType#HYPERLINK} values. */ + private String hyperlinkValue; + + /** SMILES payload for {@link Property.PropertyType#CHEMICAL_STRUCTURE} values. */ + private String chemicalStructureValue; + + /** MOL block payload for {@link Property.PropertyType#STRUCTURE_3D} values. */ + private String structure3DValue; + + /** Sanitized HTML fragment for {@link Property.PropertyType#HTML} values. */ + private String htmlValue; +} diff --git a/cqp-api/src/test/java/com/quantori/cqp/api/model/PropertyTypeTest.java b/cqp-api/src/test/java/com/quantori/cqp/api/model/PropertyTypeTest.java new file mode 100644 index 0000000..fbced70 --- /dev/null +++ b/cqp-api/src/test/java/com/quantori/cqp/api/model/PropertyTypeTest.java @@ -0,0 +1,60 @@ +package com.quantori.cqp.api.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.EnumSet; +import java.util.List; +import org.junit.jupiter.api.Test; + +class PropertyTypeTest { + + @Test + void shouldContainExtendedTypes() { + EnumSet types = EnumSet.allOf(Property.PropertyType.class); + + assertThat(types) + .contains( + Property.PropertyType.BINARY, + Property.PropertyType.DATE_TIME, + Property.PropertyType.LIST, + Property.PropertyType.HYPERLINK, + Property.PropertyType.CHEMICAL_STRUCTURE, + Property.PropertyType.STRUCTURE_3D, + Property.PropertyType.HTML); + } + + @Test + void shouldAllowPropertyValueAccessors() { + byte[] binary = new byte[] {1, 2, 3}; + Instant timestamp = Instant.parse("2024-01-01T10:15:30Z"); + List orderedList = List.of("first", "second"); + LocalDate date = LocalDate.of(2024, 2, 29); + + PropertyValue value = + PropertyValue.builder() + .stringValue("test") + .decimalValue(12.34d) + .dateValue(date) + .binaryValue(binary) + .dateTimeValue(timestamp) + .listValue(orderedList) + .hyperlinkValue("https://example.com") + .chemicalStructureValue("CCO") + .structure3DValue("3D-MOL-DATA") + .htmlValue("

html

") + .build(); + + assertThat(value.getStringValue()).isEqualTo("test"); + assertThat(value.getDecimalValue()).isEqualTo(12.34d); + assertThat(value.getDateValue()).isEqualTo(date); + assertThat(value.getBinaryValue()).containsExactly(binary); + assertThat(value.getDateTimeValue()).isEqualTo(timestamp); + assertThat(value.getListValue()).containsExactlyElementsOf(orderedList); + assertThat(value.getHyperlinkValue()).isEqualTo("https://example.com"); + assertThat(value.getChemicalStructureValue()).isEqualTo("CCO"); + assertThat(value.getStructure3DValue()).isEqualTo("3D-MOL-DATA"); + assertThat(value.getHtmlValue()).isEqualTo("

html

"); + } +} diff --git a/cqp-core/build.gradle.kts b/cqp-core/build.gradle.kts index 053d408..5b6bf16 100644 --- a/cqp-core/build.gradle.kts +++ b/cqp-core/build.gradle.kts @@ -11,7 +11,7 @@ val akkaVersion: String = "2.9.0" val lightbendVersion: String = "1.5.0" dependencies { - implementation("com.quantori:cqp-api:0.0.16") + implementation("com.quantori:cqp-api:0.0.17") implementation("com.typesafe:config:1.4.2") diff --git a/cqp-storage-elasticsearch/build.gradle.kts b/cqp-storage-elasticsearch/build.gradle.kts index 7ef5cc2..4c2851c 100644 --- a/cqp-storage-elasticsearch/build.gradle.kts +++ b/cqp-storage-elasticsearch/build.gradle.kts @@ -16,7 +16,7 @@ tasks.named("javadoc") { } dependencies { - api("com.quantori:cqp-api:0.0.16") + api("com.quantori:cqp-api:0.0.17") implementation("co.elastic.clients:elasticsearch-java:8.6.2") implementation(libs.jackson) implementation(libs.jackson.jsr310) From 7885109c1b1cee54390e86d014f926918afcbfaa Mon Sep 17 00:00:00 2001 From: "boris.sukhodoev" Date: Wed, 19 Nov 2025 15:34:18 +0200 Subject: [PATCH 02/15] fix: make akka repo optional --- .../cqp/build/CqpJavaLibraryPlugin.kt | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt index dd6fbc7..d873d28 100644 --- a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt +++ b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt @@ -28,14 +28,30 @@ class CqpJavaLibraryPlugin : Plugin { project.repositories { mavenLocal() - mavenCentral () - maven { - name = "Akka" - url = project.uri("https://repo.akka.io/maven") - content { - includeGroup("com.typesafe.akka") - includeGroupByRegex("com\\.lightbend\\..*") + mavenCentral() + + val akkaUsername = (project.findProperty("akkaRepoUsername") as String?) + ?: System.getenv("AKKA_REPO_USERNAME") + val akkaPassword = (project.findProperty("akkaRepoPassword") as String?) + ?: System.getenv("AKKA_REPO_PASSWORD") + + if (!akkaUsername.isNullOrBlank() && !akkaPassword.isNullOrBlank()) { + maven { + name = "Akka" + url = project.uri("https://repo.akka.io/maven") + credentials { + username = akkaUsername + password = akkaPassword + } + content { + includeGroup("com.typesafe.akka") + includeGroupByRegex("com\\.lightbend\\..*") + } } + } else { + project.logger.lifecycle( + "Akka credentials not provided. Falling back to mavenCentral for com.typesafe.akka artifacts." + ) } } @@ -200,4 +216,4 @@ class CqpJavaLibraryPlugin : Plugin { } } } -} \ No newline at end of file +} From 3ab86f0d7532a2dee10f9426e556c86ec393b33d Mon Sep 17 00:00:00 2001 From: "boris.sukhodoev" Date: Wed, 19 Nov 2025 16:25:37 +0200 Subject: [PATCH 03/15] Add a key to download Akka libraries. --- cqp-core/build.gradle.kts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cqp-core/build.gradle.kts b/cqp-core/build.gradle.kts index 5b6bf16..70f7941 100644 --- a/cqp-core/build.gradle.kts +++ b/cqp-core/build.gradle.kts @@ -7,6 +7,19 @@ plugins { description = "Chem query platform. Compound quick search" version = "0.0.15" +repositories { + mavenLocal() + mavenCentral() + maven { + name = "Akka" + url = project.uri("https://repo.akka.io/PP6oS6TZpjJE2o7az2kZz-HjNl1wdyDdikSkrv9gNtumZcuQ/secure") + content { + includeGroup("com.typesafe.akka") + includeGroupByRegex("com\\.lightbend\\..*") + } + } +} + val akkaVersion: String = "2.9.0" val lightbendVersion: String = "1.5.0" From 10bd2fc4a4cfe8be2d414c366534a80edf706a3b Mon Sep 17 00:00:00 2001 From: "boris.sukhodoev" Date: Wed, 19 Nov 2025 16:57:18 +0200 Subject: [PATCH 04/15] fix: unblock CI builds --- .../cqp/build/CqpJavaLibraryPlugin.kt | 26 ++++++++++--------- cqp-core/build.gradle.kts | 4 ++- cqp-storage-elasticsearch/build.gradle.kts | 2 +- .../ElasticsearchStorageMolecules.java | 11 ++++++-- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt index d873d28..0fe4049 100644 --- a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt +++ b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt @@ -30,28 +30,30 @@ class CqpJavaLibraryPlugin : Plugin { mavenLocal() mavenCentral() + val akkaRepoUrl = + (project.findProperty("akkaRepoUrl") as String?) + ?: System.getenv("AKKA_REPO_URL") + ?: "https://repo.akka.io/PP6oS6TZpjJE2o7az2kZz-HjNl1wdyDdikSkrv9gNtumZcuQ/secure" val akkaUsername = (project.findProperty("akkaRepoUsername") as String?) ?: System.getenv("AKKA_REPO_USERNAME") val akkaPassword = (project.findProperty("akkaRepoPassword") as String?) ?: System.getenv("AKKA_REPO_PASSWORD") - if (!akkaUsername.isNullOrBlank() && !akkaPassword.isNullOrBlank()) { - maven { - name = "Akka" - url = project.uri("https://repo.akka.io/maven") + maven { + name = "Akka" + url = project.uri(akkaRepoUrl) + if (!akkaUsername.isNullOrBlank() && !akkaPassword.isNullOrBlank()) { credentials { username = akkaUsername password = akkaPassword } - content { - includeGroup("com.typesafe.akka") - includeGroupByRegex("com\\.lightbend\\..*") - } + } else { + project.logger.lifecycle("Using Akka repository without credentials (env/properties not provided)") + } + content { + includeGroup("com.typesafe.akka") + includeGroupByRegex("com\\.lightbend\\..*") } - } else { - project.logger.lifecycle( - "Akka credentials not provided. Falling back to mavenCentral for com.typesafe.akka artifacts." - ) } } diff --git a/cqp-core/build.gradle.kts b/cqp-core/build.gradle.kts index 70f7941..1c4d74a 100644 --- a/cqp-core/build.gradle.kts +++ b/cqp-core/build.gradle.kts @@ -1,3 +1,5 @@ +import java.net.URI + plugins { `java-library` id("com.diffplug.spotless") version "7.0.2" @@ -24,7 +26,7 @@ val akkaVersion: String = "2.9.0" val lightbendVersion: String = "1.5.0" dependencies { - implementation("com.quantori:cqp-api:0.0.17") + implementation(project(":cqp-api")) implementation("com.typesafe:config:1.4.2") diff --git a/cqp-storage-elasticsearch/build.gradle.kts b/cqp-storage-elasticsearch/build.gradle.kts index 4c2851c..038cccf 100644 --- a/cqp-storage-elasticsearch/build.gradle.kts +++ b/cqp-storage-elasticsearch/build.gradle.kts @@ -16,7 +16,7 @@ tasks.named("javadoc") { } dependencies { - api("com.quantori:cqp-api:0.0.17") + api(project(":cqp-api")) implementation("co.elastic.clients:elasticsearch-java:8.6.2") implementation(libs.jackson) implementation(libs.jackson.jsr310) diff --git a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStorageMolecules.java b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStorageMolecules.java index fc95cde..7b5344a 100644 --- a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStorageMolecules.java +++ b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStorageMolecules.java @@ -441,10 +441,11 @@ private String escape(String value) { private Object convert(String value, Property.PropertyType type) { return switch (type) { - case DATE -> { + case DATE, DATE_TIME -> { // TODO limited date support, needs to add a check for formats in future if (StringUtils.isBlank(value)) { - throw new ElasticsearchStorageException(String.format("A value \"%s\" cannot be cast to date", value)); + throw new ElasticsearchStorageException( + String.format("A value \"%s\" cannot be cast to %s", value, type.name().toLowerCase())); } else { yield value; } @@ -457,6 +458,12 @@ private Object convert(String value, Property.PropertyType type) { throw new ElasticsearchStorageException(String.format("A value \"%s\" cannot be cast to decimal", value)); } } + case BINARY, + LIST, + HYPERLINK, + CHEMICAL_STRUCTURE, + STRUCTURE_3D, + HTML -> value; }; } } From 3f9dec41cfc14b1a47bd5ec64c76f85908522495 Mon Sep 17 00:00:00 2001 From: "boris.sukhodoev" Date: Wed, 19 Nov 2025 17:15:54 +0200 Subject: [PATCH 05/15] chore: align Akka repo usage --- .../quantori/cqp/build/CqpJavaLibraryPlugin.kt | 12 ------------ cqp-core/build.gradle.kts | 17 +---------------- cqp-storage-elasticsearch/build.gradle.kts | 2 +- 3 files changed, 2 insertions(+), 29 deletions(-) diff --git a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt index 0fe4049..0091cc8 100644 --- a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt +++ b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt @@ -34,22 +34,10 @@ class CqpJavaLibraryPlugin : Plugin { (project.findProperty("akkaRepoUrl") as String?) ?: System.getenv("AKKA_REPO_URL") ?: "https://repo.akka.io/PP6oS6TZpjJE2o7az2kZz-HjNl1wdyDdikSkrv9gNtumZcuQ/secure" - val akkaUsername = (project.findProperty("akkaRepoUsername") as String?) - ?: System.getenv("AKKA_REPO_USERNAME") - val akkaPassword = (project.findProperty("akkaRepoPassword") as String?) - ?: System.getenv("AKKA_REPO_PASSWORD") maven { name = "Akka" url = project.uri(akkaRepoUrl) - if (!akkaUsername.isNullOrBlank() && !akkaPassword.isNullOrBlank()) { - credentials { - username = akkaUsername - password = akkaPassword - } - } else { - project.logger.lifecycle("Using Akka repository without credentials (env/properties not provided)") - } content { includeGroup("com.typesafe.akka") includeGroupByRegex("com\\.lightbend\\..*") diff --git a/cqp-core/build.gradle.kts b/cqp-core/build.gradle.kts index 1c4d74a..5b6bf16 100644 --- a/cqp-core/build.gradle.kts +++ b/cqp-core/build.gradle.kts @@ -1,5 +1,3 @@ -import java.net.URI - plugins { `java-library` id("com.diffplug.spotless") version "7.0.2" @@ -9,24 +7,11 @@ plugins { description = "Chem query platform. Compound quick search" version = "0.0.15" -repositories { - mavenLocal() - mavenCentral() - maven { - name = "Akka" - url = project.uri("https://repo.akka.io/PP6oS6TZpjJE2o7az2kZz-HjNl1wdyDdikSkrv9gNtumZcuQ/secure") - content { - includeGroup("com.typesafe.akka") - includeGroupByRegex("com\\.lightbend\\..*") - } - } -} - val akkaVersion: String = "2.9.0" val lightbendVersion: String = "1.5.0" dependencies { - implementation(project(":cqp-api")) + implementation("com.quantori:cqp-api:0.0.17") implementation("com.typesafe:config:1.4.2") diff --git a/cqp-storage-elasticsearch/build.gradle.kts b/cqp-storage-elasticsearch/build.gradle.kts index 038cccf..4c2851c 100644 --- a/cqp-storage-elasticsearch/build.gradle.kts +++ b/cqp-storage-elasticsearch/build.gradle.kts @@ -16,7 +16,7 @@ tasks.named("javadoc") { } dependencies { - api(project(":cqp-api")) + api("com.quantori:cqp-api:0.0.17") implementation("co.elastic.clients:elasticsearch-java:8.6.2") implementation(libs.jackson) implementation(libs.jackson.jsr310) From fc20593125077bac3354c122825eb91b1aa4f357 Mon Sep 17 00:00:00 2001 From: "boris.sukhodoev" Date: Wed, 19 Nov 2025 17:41:57 +0200 Subject: [PATCH 06/15] chore: align module versions to 0.0.17 --- ARCHITECTURE.md | 6 +++--- cqp-core/build.gradle.kts | 2 +- cqp-storage-elasticsearch/build.gradle.kts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 28fa5d6..7ecdc58 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -91,7 +91,7 @@ The Chemical Query Platform (CQP) is an open-source framework for indexing and s ### cqp-api (Core API Module) -**Version**: 0.0.16 +**Version**: 0.0.17 **Purpose**: Defines interfaces, models, and abstractions for chemical structure storage and search. @@ -132,7 +132,7 @@ com.quantori.cqp.api/ ### cqp-storage-elasticsearch (Elasticsearch Implementation) -**Version**: 0.0.14 +**Version**: 0.0.17 **Purpose**: Elasticsearch implementation of CQP storage interfaces. @@ -155,7 +155,7 @@ com.quantori.cqp.storage.elasticsearch/ └── ReactionMapper.java # Domain ↔ Elasticsearch ``` -**Dependencies**: cqp-api (0.0.16), Elasticsearch Java Client (8.6.2) +**Dependencies**: cqp-api (0.0.17), Elasticsearch Java Client (8.6.2) ### cqp-core (Akka Actors Module - Currently Missing) diff --git a/cqp-core/build.gradle.kts b/cqp-core/build.gradle.kts index 5b6bf16..08cb0a6 100644 --- a/cqp-core/build.gradle.kts +++ b/cqp-core/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } description = "Chem query platform. Compound quick search" -version = "0.0.15" +version = "0.0.17" val akkaVersion: String = "2.9.0" val lightbendVersion: String = "1.5.0" diff --git a/cqp-storage-elasticsearch/build.gradle.kts b/cqp-storage-elasticsearch/build.gradle.kts index 4c2851c..87d75b7 100644 --- a/cqp-storage-elasticsearch/build.gradle.kts +++ b/cqp-storage-elasticsearch/build.gradle.kts @@ -6,7 +6,7 @@ plugins { group = "com.quantori" description = "Chem query platform. Storage Elasticsearch" -version = "0.0.14" +version = "0.0.17" tasks.named("javadoc") { exclude( From 29fd639e46bb9cb54b1463c099892b9a39c4aa60 Mon Sep 17 00:00:00 2001 From: "boris.sukhodoev" Date: Wed, 19 Nov 2025 18:11:51 +0200 Subject: [PATCH 07/15] Fix build. --- .github/workflows/build-and-test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 6475f30..6291bac 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -53,5 +53,8 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 + - name: Build API with Gradle Wrapper + run: ./gradlew :cqp-api:build :cqp-api:publishToMavenLocal + - name: Build with Gradle Wrapper - run: ./gradlew build + run: ./gradlew build -x :cqp-api:build From fd04ee9780347536d4a94fe4b3573cc31cca0410 Mon Sep 17 00:00:00 2001 From: "boris.sukhodoev" Date: Wed, 19 Nov 2025 20:31:39 +0200 Subject: [PATCH 08/15] fix: add AKKA env + property mapping meta --- .../cqp/build/CqpJavaLibraryPlugin.kt | 19 +++--- .../ElasticIndexMappingsFactory.java | 68 ++++++++++++++----- ...asticsearchMoleculesResourceAllocator.java | 24 +++++-- ...ElasticsearchStoragePropertiesMapping.java | 22 +++++- 4 files changed, 101 insertions(+), 32 deletions(-) diff --git a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt index 0091cc8..bb95c3a 100644 --- a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt +++ b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt @@ -33,14 +33,17 @@ class CqpJavaLibraryPlugin : Plugin { val akkaRepoUrl = (project.findProperty("akkaRepoUrl") as String?) ?: System.getenv("AKKA_REPO_URL") - ?: "https://repo.akka.io/PP6oS6TZpjJE2o7az2kZz-HjNl1wdyDdikSkrv9gNtumZcuQ/secure" - - maven { - name = "Akka" - url = project.uri(akkaRepoUrl) - content { - includeGroup("com.typesafe.akka") - includeGroupByRegex("com\\.lightbend\\..*") + + if (akkaRepoUrl.isNullOrBlank()) { + project.logger.warn("AKKA_REPO_URL is not configured. Akka artifacts may fail to resolve.") + } else { + maven { + name = "Akka" + url = project.uri(akkaRepoUrl) + content { + includeGroup("com.typesafe.akka") + includeGroupByRegex("com\\.lightbend\\..*") + } } } } diff --git a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticIndexMappingsFactory.java b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticIndexMappingsFactory.java index bf8eb3d..5bdcd90 100644 --- a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticIndexMappingsFactory.java +++ b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticIndexMappingsFactory.java @@ -4,7 +4,12 @@ import com.quantori.cqp.storage.elasticsearch.model.MoleculeDocument; import lombok.experimental.UtilityClass; +import java.util.Collections; +import java.util.EnumMap; import java.util.Map; +import java.util.stream.Collectors; + +import static java.util.Map.entry; @UtilityClass class ElasticIndexMappingsFactory { @@ -19,19 +24,38 @@ class ElasticIndexMappingsFactory { public static final String ROLE = "role"; public static final String REACTION_ID = "reactionId"; - public static final Map TYPES_MAP = Map.of( - Property.PropertyType.STRING, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Wildcard.jsonValue(), - Property.PropertyType.DATE, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Date.jsonValue(), - Property.PropertyType.DECIMAL, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Double.jsonValue() - ); + public static final String PROPERTY_TYPE_META_KEY = "propertyType"; - public static final - Map KINDS_MAP = Map.of( - co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Wildcard, Property.PropertyType.STRING, - co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Date, Property.PropertyType.DATE, - co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Double, Property.PropertyType.DECIMAL + private static final Map KIND_BY_PROPERTY_TYPE = Map.ofEntries( + entry(Property.PropertyType.STRING, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Wildcard), + entry(Property.PropertyType.DATE, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Date), + entry(Property.PropertyType.DATE_TIME, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.DateNanos), + entry(Property.PropertyType.DECIMAL, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Double), + entry(Property.PropertyType.BINARY, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Binary), + entry(Property.PropertyType.LIST, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Keyword), + entry(Property.PropertyType.HYPERLINK, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.MatchOnlyText), + entry(Property.PropertyType.CHEMICAL_STRUCTURE, + co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Text), + entry(Property.PropertyType.STRUCTURE_3D, + co.elastic.clients.elasticsearch._types.mapping.Property.Kind.SearchAsYouType), + entry(Property.PropertyType.HTML, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Text) ); + public static final Map TYPES_MAP = + KIND_BY_PROPERTY_TYPE.entrySet().stream() + .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, entry -> entry.getValue().jsonValue())); + + public static final Map + KINDS_MAP; + + static { + EnumMap map = + new EnumMap<>(co.elastic.clients.elasticsearch._types.mapping.Property.Kind.class); + KIND_BY_PROPERTY_TYPE.forEach((propertyType, kind) -> map.putIfAbsent(kind, propertyType)); + KINDS_MAP = Collections.unmodifiableMap(map); + } + public static final String MOLECULES_MAPPING = String.format(""" { "settings": { @@ -228,20 +252,30 @@ class ElasticIndexMappingsFactory { "yyyy-MM-dd HH:mm:ss", "MM/dd/yyyy HH:mm:ss a", "dd.MM.yyyy", - "dd/MM-yyyy" + "dd/MM-yyyy", + "strict_date_optional_time", + "strict_date_optional_time_nanos" }; - public static final String DATE_PROPERTY_MAPPING = String.format(""" - "%%s": { - "type": "%%s", + public static final String DATE_FORMAT_PATTERN = String.join("||", DATE_FORMATS); + + public static final String DATE_PROPERTY_MAPPING = """ + "%s": { + "type": "%s", "ignore_malformed": true, - "format": "%s" + "format": "%s", + "meta": { + "%s": "%s" + } } - """, String.join("||", DATE_FORMATS)); + """; public static final String PROPERTY_MAPPING = """ "%s": { - "type": "%s" + "type": "%s", + "meta": { + "%s": "%s" + } } """; } diff --git a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchMoleculesResourceAllocator.java b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchMoleculesResourceAllocator.java index 07c62a7..02451e0 100644 --- a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchMoleculesResourceAllocator.java +++ b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchMoleculesResourceAllocator.java @@ -73,13 +73,25 @@ private String getPropertiesMapping(Map propertiesMapping) { String propertyKey = entry.getKey(); Property property = entry.getValue(); Property.PropertyType type = Objects.requireNonNullElse(property.getType(), Property.PropertyType.STRING); - if (Property.PropertyType.DATE == type) { - return String.format(ElasticIndexMappingsFactory.DATE_PROPERTY_MAPPING, propertyKey, - ElasticIndexMappingsFactory.TYPES_MAP.get(type)); - } else { - return String.format(ElasticIndexMappingsFactory.PROPERTY_MAPPING, propertyKey, - ElasticIndexMappingsFactory.TYPES_MAP.get(type)); + String elasticType = ElasticIndexMappingsFactory.TYPES_MAP.get(type); + String typeName = type.name(); + if (Property.PropertyType.DATE == type || Property.PropertyType.DATE_TIME == type) { + return String.format( + ElasticIndexMappingsFactory.DATE_PROPERTY_MAPPING, + propertyKey, + elasticType, + ElasticIndexMappingsFactory.DATE_FORMAT_PATTERN, + ElasticIndexMappingsFactory.PROPERTY_TYPE_META_KEY, + typeName + ); } + return String.format( + ElasticIndexMappingsFactory.PROPERTY_MAPPING, + propertyKey, + elasticType, + ElasticIndexMappingsFactory.PROPERTY_TYPE_META_KEY, + typeName + ); }) .collect(Collectors.joining(",\n", ",\"" + ElasticIndexMappingsFactory.PROPERTIES + "\": {\n", "}\n")); } diff --git a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStoragePropertiesMapping.java b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStoragePropertiesMapping.java index d057246..8535e5d 100644 --- a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStoragePropertiesMapping.java +++ b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStoragePropertiesMapping.java @@ -4,6 +4,7 @@ import co.elastic.clients.elasticsearch._types.ElasticsearchException; import co.elastic.clients.elasticsearch.indices.GetMappingResponse; import co.elastic.clients.transport.ElasticsearchTransport; +import co.elastic.clients.json.JsonData; import com.quantori.cqp.api.model.Library; import com.quantori.cqp.api.model.Property; import com.quantori.cqp.storage.elasticsearch.model.LibraryDocument; @@ -188,7 +189,7 @@ private Map convertElasticPropertiesToApplicationProperties( return indexedProperties.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, entry -> { Property property = libraryDocument.getPropertiesMapping().getOrDefault(entry.getKey(), null); - Property.PropertyType type = ElasticIndexMappingsFactory.KINDS_MAP.get(entry.getValue()._kind()); + Property.PropertyType type = resolvePropertyType(property, entry.getValue()); if (property == null) { return new Property(entry.getKey(), type); } else { @@ -225,6 +226,25 @@ private OperationStatus createAndUpdatePropertiesMapping(String libraryId, Libra } } + private Property.PropertyType resolvePropertyType( + Property storedProperty, + co.elastic.clients.elasticsearch._types.mapping.Property elasticProperty + ) { + if (storedProperty != null && storedProperty.getType() != null) { + return storedProperty.getType(); + } + return Optional.ofNullable(elasticProperty.meta()) + .map(meta -> meta.get(ElasticIndexMappingsFactory.PROPERTY_TYPE_META_KEY)) + .map(jsonData -> { + try { + return Property.PropertyType.valueOf(jsonData.to(String.class)); + } catch (IllegalArgumentException ignored) { + return null; + } + }) + .orElse(ElasticIndexMappingsFactory.KINDS_MAP.get(elasticProperty._kind())); + } + /** * Get a property name in an elastic index * From 20932a0c99c87b60bb3e0051b7931e81c0645704 Mon Sep 17 00:00:00 2001 From: "boris.sukhodoev" Date: Wed, 19 Nov 2025 20:45:47 +0200 Subject: [PATCH 09/15] Fix build. --- .github/workflows/build-and-test.yml | 2 ++ .../ElasticsearchStoragePropertiesMapping.java | 11 +++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 6291bac..018bbbe 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -39,6 +39,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + env: + AKKA_REPO_URL: ${{ secrets.AKKA_REPO_URL }} steps: - uses: actions/checkout@v4 diff --git a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStoragePropertiesMapping.java b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStoragePropertiesMapping.java index 8535e5d..57bad51 100644 --- a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStoragePropertiesMapping.java +++ b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStoragePropertiesMapping.java @@ -3,8 +3,8 @@ import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch._types.ElasticsearchException; import co.elastic.clients.elasticsearch.indices.GetMappingResponse; +import co.elastic.clients.elasticsearch._types.mapping.PropertyBase; import co.elastic.clients.transport.ElasticsearchTransport; -import co.elastic.clients.json.JsonData; import com.quantori.cqp.api.model.Library; import com.quantori.cqp.api.model.Property; import com.quantori.cqp.storage.elasticsearch.model.LibraryDocument; @@ -233,11 +233,14 @@ private Property.PropertyType resolvePropertyType( if (storedProperty != null && storedProperty.getType() != null) { return storedProperty.getType(); } - return Optional.ofNullable(elasticProperty.meta()) + return Optional.ofNullable(elasticProperty._get()) + .filter(PropertyBase.class::isInstance) + .map(PropertyBase.class::cast) + .map(PropertyBase::meta) .map(meta -> meta.get(ElasticIndexMappingsFactory.PROPERTY_TYPE_META_KEY)) - .map(jsonData -> { + .map(value -> { try { - return Property.PropertyType.valueOf(jsonData.to(String.class)); + return Property.PropertyType.valueOf(value); } catch (IllegalArgumentException ignored) { return null; } From 2ea35b5325ba9696c139a1cc8b57a04cc11caf64 Mon Sep 17 00:00:00 2001 From: "boris.sukhodoev" Date: Wed, 19 Nov 2025 21:41:44 +0200 Subject: [PATCH 10/15] chore: enforce Akka repo fail-fast --- Devnotes.md | 4 ++++ .../cqp/build/CqpJavaLibraryPlugin.kt | 21 +++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Devnotes.md b/Devnotes.md index 6fefa8b..8fef1c7 100644 --- a/Devnotes.md +++ b/Devnotes.md @@ -2,3 +2,7 @@ * Download the repository * Let your IDE import necessary dependencies automatically or fetch them manually * For local repository publication Run `gradle build` task and then run `gradle publishToMavenLocal` tasks + +## Repository configuration + +* Set the `AKKA_REPO_URL` Gradle property or environment variable before running builds. The shared plugin fails fast when the URL is absent to avoid silently compiling without the commercial Akka repository. diff --git a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt index bb95c3a..0195001 100644 --- a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt +++ b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt @@ -1,5 +1,6 @@ package com.quantori.cqp.build +import org.gradle.api.GradleException import org.gradle.api.JavaVersion import org.gradle.api.Plugin import org.gradle.api.Project @@ -33,17 +34,15 @@ class CqpJavaLibraryPlugin : Plugin { val akkaRepoUrl = (project.findProperty("akkaRepoUrl") as String?) ?: System.getenv("AKKA_REPO_URL") - - if (akkaRepoUrl.isNullOrBlank()) { - project.logger.warn("AKKA_REPO_URL is not configured. Akka artifacts may fail to resolve.") - } else { - maven { - name = "Akka" - url = project.uri(akkaRepoUrl) - content { - includeGroup("com.typesafe.akka") - includeGroupByRegex("com\\.lightbend\\..*") - } + ?: throw GradleException( + "AKKA_REPO_URL is not configured. Configure the secret/property to resolve Akka artifacts.") + + maven { + name = "Akka" + url = project.uri(akkaRepoUrl) + content { + includeGroup("com.typesafe.akka") + includeGroupByRegex("com\\.lightbend\\..*") } } } From 8e2d802071ecefef73ad7c75965634fadaf46c92 Mon Sep 17 00:00:00 2001 From: "boris.sukhodoev" Date: Wed, 19 Nov 2025 22:01:26 +0200 Subject: [PATCH 11/15] Fix build. --- Devnotes.md | 7 +++++-- .../kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Devnotes.md b/Devnotes.md index 8fef1c7..3c65fd8 100644 --- a/Devnotes.md +++ b/Devnotes.md @@ -1,8 +1,11 @@ ## Build instructions * Download the repository * Let your IDE import necessary dependencies automatically or fetch them manually -* For local repository publication Run `gradle build` task and then run `gradle publishToMavenLocal` tasks +* For local repository publication run the following tasks + * `gradlew :cqp-api:build :cqp-api:publishToMavenLocal` + * `gradle build -x :cqp-api:build` + * `gradle publishToMavenLocal -x :cqp-api:publishToMavenLocal` ## Repository configuration -* Set the `AKKA_REPO_URL` Gradle property or environment variable before running builds. The shared plugin fails fast when the URL is absent to avoid silently compiling without the commercial Akka repository. +* Set the `AKKA_REPO_URL` Gradle property or environment variable before running builds. The shared plugin fails fast when the URL is absent to avoid silently compiling without the commercial Akka repository. This URL should contain special security token which can be created here- https://account.akka.io/. diff --git a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt index 0195001..f5ed944 100644 --- a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt +++ b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt @@ -33,7 +33,7 @@ class CqpJavaLibraryPlugin : Plugin { val akkaRepoUrl = (project.findProperty("akkaRepoUrl") as String?) - ?: System.getenv("AKKA_REPO_URL") + ?: System.getenv("AKKA_REPO_URL").takeIf { !it.isNullOrBlank() } ?: throw GradleException( "AKKA_REPO_URL is not configured. Configure the secret/property to resolve Akka artifacts.") From 601fb924d6745fb013ab3052f0052056cef8c543 Mon Sep 17 00:00:00 2001 From: "boris.sukhodoev" Date: Wed, 19 Nov 2025 22:23:47 +0200 Subject: [PATCH 12/15] fix: read AKKA_REPO_URL property --- .../main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt index f5ed944..6e6cf99 100644 --- a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt +++ b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt @@ -32,7 +32,8 @@ class CqpJavaLibraryPlugin : Plugin { mavenCentral() val akkaRepoUrl = - (project.findProperty("akkaRepoUrl") as String?) + (project.findProperty("AKKA_REPO_URL") as String?) + ?: (project.findProperty("akkaRepoUrl") as String?) ?: System.getenv("AKKA_REPO_URL").takeIf { !it.isNullOrBlank() } ?: throw GradleException( "AKKA_REPO_URL is not configured. Configure the secret/property to resolve Akka artifacts.") From fef11555bbbc65a7cc65cf9bb72ebab69c85d8fd Mon Sep 17 00:00:00 2001 From: "boris.sukhodoev" Date: Wed, 19 Nov 2025 22:28:17 +0200 Subject: [PATCH 13/15] docs: clarify Akka repo resolution --- Devnotes.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Devnotes.md b/Devnotes.md index 3c65fd8..dfa7d0a 100644 --- a/Devnotes.md +++ b/Devnotes.md @@ -8,4 +8,8 @@ ## Repository configuration -* Set the `AKKA_REPO_URL` Gradle property or environment variable before running builds. The shared plugin fails fast when the URL is absent to avoid silently compiling without the commercial Akka repository. This URL should contain special security token which can be created here- https://account.akka.io/. +* The shared plugin reads the Akka repository URL in this order and stops at the first match: + 1. Gradle property `AKKA_REPO_URL` (set via `gradle.properties` or `-PAKKA_REPO_URL=...`) + 2. Gradle property `akkaRepoUrl` (legacy camelCase fallback) + 3. Environment variable `AKKA_REPO_URL` +* If none of the above is supplied, the build fails immediately. Obtain the secure URL/token from https://account.akka.io/ and set one of the properties/variables before running Gradle. From 0b3278da679a8da4699130e54c716e1233487103 Mon Sep 17 00:00:00 2001 From: "boris.sukhodoev" Date: Wed, 19 Nov 2025 23:13:13 +0200 Subject: [PATCH 14/15] Fix build. --- .github/workflows/build-and-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 018bbbe..ae2b1a7 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -17,6 +17,8 @@ jobs: format-check: name: Check Code Formatting runs-on: ubuntu-latest + env: + AKKA_REPO_URL: ${{ secrets.AKKA_REPO_URL }} steps: - uses: actions/checkout@v4 From 038bbd8e272448dbbf2732bc7f8813c42d8cf810 Mon Sep 17 00:00:00 2001 From: "boris.sukhodoev" Date: Thu, 20 Nov 2025 00:33:46 +0200 Subject: [PATCH 15/15] Add package naming strategy to CONTEXT.md. --- CONTEXT.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CONTEXT.md b/CONTEXT.md index 9de2653..2b4b326 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -6,6 +6,20 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co CQP is an open-source framework for indexing and searching within cheminformatics applications (molecules & reactions). It's built on the Akka Actors framework for scalability and supports multiple storage engines including PostgreSQL, Elasticsearch, and Apache Solr. +## Package Naming Strategy + +**CQP Internal Code**: All code in this repository uses the `com.quantori.cqp.*` package hierarchy: +- `com.quantori.cqp.api.model.*` - Core data models (Property, PropertyType, PropertyValue, etc.) +- `com.quantori.cqp.core.*` - Akka actors and core framework +- `com.quantori.cqp.storage.elasticsearch.*` - Elasticsearch storage implementation +- `com.quantori.cqp.build.*` - Gradle build plugins + +**External Indigo Library**: The external Indigo Toolkit library uses `com.epam.indigo.*`: +- `com.epam.indigo.Indigo` - Main Indigo class +- `com.epam.indigo.IndigoObject` - Indigo molecule/reaction objects +- `com.epam.indigo.IndigoException` - Indigo exceptions +- **Do not change** these package references - they belong to the external library + ## Project Structure This is a multi-module Gradle project with the following key modules: