-
Notifications
You must be signed in to change notification settings - Fork 11
CLIENT-4143 Restructure to support the new Java client #204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
agrgr
wants to merge
8
commits into
main
Choose a base branch
from
CLIENT-4143-to-multi-module
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
5a20b0d
Refactor ClassCache, ClassCacheEntry, MappingConverter, ObjectReferen…
agrgr e030fc4
Reorganize into three submodules
agrgr c588fa3
Rename directories of clients APIs submodules
agrgr a60dd85
Add durable delete in tests
agrgr 92303e0
Fix bugs, improve safety, further decouple modules, add tests
agrgr 1974629
Downgrade Mockito to Java 8-compatible version
agrgr 4d6a1bc
Remove debug durableDelete
agrgr 8ac9a4a
Set durableDelete explicitly in tests
agrgr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # top-most EditorConfig file | ||
| root = true | ||
|
|
||
| # Unix-style newlines with a newline ending every file | ||
| [*] | ||
| charset = utf-8 | ||
| end_of_line = lf | ||
|
|
||
| [*.java] | ||
| indent_style = space | ||
| indent_size = 4 | ||
| insert_final_newline = true | ||
| max_line_length = 120 | ||
| ij_java_wrap_long_lines = true | ||
| ij_java_wrap_comments = true | ||
| ij_java_method_call_chain_wrap = normal | ||
| ij_java_blank_lines_after_class_header = 1 | ||
| ij_java_class_count_to_use_import_on_demand = 10 | ||
| ij_java_names_count_to_use_import_on_demand = 10 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| # Copilot Instructions for Aerospike Java Object Mapper | ||
|
|
||
| ## Running Java commands | ||
| JAVA_HOME=/usr/lib/jvm/java-1.21.0-openjdk-arm64/ | ||
| PATH=/usr/lib/jvm/java-1.21.0-openjdk-arm64//bin:$PATH | ||
|
|
||
| ## Starting Aerospike Server in Docker for testing | ||
|
|
||
| Command to run Aerospike Server 8.1.0.1: aerospike_server_8101 and wait for its finishing | ||
| Command to run aql utility to connect to the DB if needed: | ||
| docker run --rm -it aerospike/aerospike-tools:8.1.0 aql -h 172.17.0.2 -U tester -P psw | ||
|
|
||
| ## Build & Test | ||
|
|
||
| ```bash | ||
| # Compile (skip tests) | ||
| mvn compile -B | ||
|
|
||
| # Run all tests (requires a running Aerospike server on localhost:3000) | ||
| mvn clean test -B -U | ||
|
|
||
| # Run a single test class | ||
| mvn test -Dtest=AeroMapperTest -B | ||
|
|
||
| # Run a single test method | ||
| mvn test -Dtest=AeroMapperTest#testSimpleSave -B | ||
|
|
||
| # Run tests against a different Aerospike host | ||
| mvn test -Dtest.host=myhost:3000 -B | ||
| ``` | ||
|
|
||
| Java 8 source/target compatibility. No separate lint step. | ||
|
|
||
| ## Architecture | ||
|
|
||
| This is an annotation-driven ORM that maps Java POJOs to Aerospike database records. | ||
| It has two parallel APIs: synchronous (`AeroMapper`) and reactive (`ReactiveAeroMapper`), | ||
| both built through the same `AbstractBuilder<T>` pattern. | ||
|
|
||
| ### Core data flow (sync) | ||
|
|
||
| ``` | ||
| Write path (Use-case → Aerospike): | ||
|
|
||
| save(obj) → ClassCacheEntry.getBins(obj) → IAerospikeClient.put(bins) | ||
|
|
||
| 1. JavaMapperApplication → Entry point, calls mapper.save(customer) | ||
| 2. AeroMapper → API layer, creates WritePolicy, extracts key, converts to bins | ||
| 3. ClassCacheEntry<Customer> → Reflection engine, iterates fields, extracts values | ||
| 4. TypeMapper → Converts Java types (Date, List) to Aerospike format (Long, Map) | ||
| 5. AerospikeClient → SDK handles network I/O | ||
| 6. Aerospike Server → Persists record in namespace "test", set "customer" | ||
|
|
||
| Read path (Aerospike → Use-case): | ||
|
|
||
| read(Class<T>, key) → IAerospikeClient.get() → MappingConverter → ClassCacheEntry.hydrateFromRecord() | ||
|
|
||
| 1. JavaMapperApplication → Calls mapper.read(Customer.class, "cust1") | ||
| 2. AeroMapper → Constructs Key, checks cache, calls client.get() | ||
| 3. AerospikeClient → Retrieves Record from server | ||
| 4. Aerospike Server → Returns Record with bin data | ||
| 5. MappingConverter → Orchestrates conversion | ||
| 6. ClassCacheEntry<Customer> → Constructs Customer, iterates bins, populates fields (hydrateFromRecord()) | ||
| 7. TypeMapper → Converts Aerospike format back to Java types | ||
| 8. Customer Object → Fully hydrated and returned | ||
|
|
||
| VirtualList append path (e.g. appending an Item to a Container's embedded list): | ||
|
|
||
| mapper.asBackedList(container, "items", Item.class).append(new Item(500, new Date(), "Item5")) | ||
|
|
||
| 1. AeroMapper.asBackedList(container, "items", Item.class) → Constructs VirtualList, resolves ClassCacheEntry for Container + Item, extracts ListMapper from the @AerospikeEmbed bin "items" | ||
| 2. VirtualList.append(item) → Delegates to VirtualListInteractors | ||
| 3. VirtualListInteractors.getAppendOperation() → ListMapper.toAerospikeInstanceFormat(item) converts Item to Aerospike-native format (Map/List depending on EmbedType) | ||
| 4. VirtualListInteractors → Builds CDT operation: MapOperation.put(binName, key, value) for MAP embed, or ListOperation.append(binName, value) for LIST embed | ||
| 5. AerospikeClient.operate(writePolicy, key, operation) → Sends CDT operation to server, atomically appends to the bin without reading the full list | ||
| 6. Aerospike Server → Appends element server-side, returns updated bin size | ||
| 7. VirtualList → Returns size (long); the in-memory container object is NOT updated | ||
|
|
||
| VirtualList query path (e.g. getByKeyRange): | ||
|
|
||
| list.getByKeyRange(100, 450) | ||
|
|
||
| 1. VirtualList.getByKeyRange(start, end) → Sets return type, delegates to VirtualListInteractors | ||
| 2. VirtualListInteractors → Creates Interactor wrapping a DeferredOperation | ||
| 3. DeferredOperation.getOperation() → Translates keys via ClassCacheEntry.translateKeyToAerospikeKey(), builds MapOperation.getByKeyRange(binName, startValue, endValue, returnType) | ||
| 4. AerospikeClient.operate() → Sends CDT query to server | ||
| 5. Aerospike Server → Evaluates range server-side, returns matching entries | ||
| 6. Interactor.getResult() → Chains ResultsUnpackers (ArrayUnpacker iterates results, calls ListMapper.fromAerospikeInstanceFormat() per element) | ||
| 7. MappingConverter.resolveDependencies() → Resolves any nested @AerospikeReference objects | ||
| 8. VirtualList → Returns List<Item> of matched elements | ||
| ``` | ||
|
|
||
| ### Key classes and their roles | ||
|
|
||
| - **`AeroMapper` / `ReactiveAeroMapper`** — Public API entry points (sync returns objects, reactive returns `Mono`/`Flux`). Both are instantiated via their inner `Builder` class, never directly. | ||
| - **`AbstractBuilder<T>`** — Shared builder logic: register custom type converters, preload classes, set policies, load YAML configuration. | ||
| - **`ClassCacheEntry<T>`** — Parses annotations on a mapped class and caches the metadata (namespace, set, key field, bin names, policies). Lazily constructed. Used by the mapper to serialize/deserialize objects. | ||
| - **`ClassCache`** — Singleton cache of `ClassCacheEntry` instances. | ||
| - **`MappingConverter`** — Orchestrates type conversion during serialization/deserialization using registered `TypeMapper` instances. | ||
| - **`TypeMapper`** — Abstract base for custom type converters. Override `toAerospikeFormat()` and `fromAerospikeFormat()`. Register via `builder.addConverter()`. | ||
|
|
||
| ### Annotation system (`com.aerospike.mapper.annotations`) | ||
|
|
||
| - `@AerospikeRecord` — Marks a class as mappable; defines namespace, set, TTL. | ||
| - `@AerospikeKey` — Designates the primary key field. | ||
| - `@AerospikeBin` — Customizes bin (column) name for a field. | ||
| - `@AerospikeEmbed` / `@AerospikeReference` — Relationship mapping (embedded vs. referenced sub-objects). | ||
| - `@AerospikeExclude` — Excludes a field from mapping. | ||
| - `@ToAerospike` / `@FromAerospike` — Custom per-field conversion methods. | ||
| - `@AerospikeVersion` / `@AerospikeGeneration` — Optimistic concurrency control. | ||
| - `@AerospikeConstructor` / `@ParamFrom` — Controls object construction from records. | ||
|
|
||
| ### Type mappers (`tools/mappers/`) | ||
|
|
||
| Built-in mappers for Java primitives, `Date`, `Instant`, `LocalDate`, `LocalDateTime`, `BigDecimal`, `BigInteger`, | ||
| enums, arrays, lists, and maps. Custom mappers extend `TypeMapper`. | ||
|
|
||
| ### Virtual Lists (`tools/virtuallist/`) | ||
|
|
||
| Aerospike CDT-backed lists that support lazy loading and server-side operations without retrieving entire collections. | ||
| Both sync (`VirtualList`) and reactive (`ReactiveVirtualList`) variants exist. | ||
|
|
||
| ### Configuration (`tools/configuration/`) | ||
|
|
||
| Alternative to annotations: YAML-based configuration for class mappings via `builder.withConfiguration()`. | ||
|
|
||
| ## Conventions | ||
|
|
||
| - **Package root**: `com.aerospike.mapper` — annotations, exceptions, and `tools` sub-packages. | ||
| - **Lombok**: Used in production code (provided scope). Ensure IDE/build has Lombok support. | ||
| - **Test base classes**: Sync tests extend `AeroMapperBaseTest`; reactive tests extend `ReactiveAeroMapperBaseTest`. Both handle client lifecycle and provide a `compare()` helper that uses Jackson for JSON-based object comparison. | ||
| - **Test setup**: Each test method gets a fresh `AeroMapper` via `Builder` and clears `ClassCache` in `@BeforeEach`. Tables are truncated before tests to ensure isolation. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| <project xmlns="http://maven.apache.org/POM/4.0.0" | ||
| xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
| <modelVersion>4.0.0</modelVersion> | ||
|
|
||
| <parent> | ||
| <groupId>com.aerospike</groupId> | ||
| <artifactId>java-object-mapper-parent</artifactId> | ||
| <version>2.6.0</version> | ||
| </parent> | ||
|
|
||
| <artifactId>java-object-mapper-core</artifactId> | ||
| <packaging>jar</packaging> | ||
|
|
||
| <name>Aerospike Object Mapper Core</name> | ||
| <description>Core annotation-processing and type-mapping engine with no Aerospike client dependency.</description> | ||
|
|
||
| <dependencies> | ||
| <dependency> | ||
| <groupId>javax.validation</groupId> | ||
| <artifactId>validation-api</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.apache.commons</groupId> | ||
| <artifactId>commons-lang3</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>com.fasterxml.jackson.dataformat</groupId> | ||
| <artifactId>jackson-dataformat-yaml</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.projectlombok</groupId> | ||
| <artifactId>lombok</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.junit.jupiter</groupId> | ||
| <artifactId>junit-jupiter</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.mockito</groupId> | ||
| <artifactId>mockito-core</artifactId> | ||
| </dependency> | ||
| </dependencies> | ||
|
|
||
| <build> | ||
| <plugins> | ||
| <plugin> | ||
| <artifactId>maven-compiler-plugin</artifactId> | ||
| </plugin> | ||
| <plugin> | ||
| <groupId>org.apache.maven.plugins</groupId> | ||
| <artifactId>maven-dependency-plugin</artifactId> | ||
| </plugin> | ||
| <plugin> | ||
| <groupId>org.apache.maven.plugins</groupId> | ||
| <artifactId>maven-surefire-plugin</artifactId> | ||
| </plugin> | ||
| </plugins> | ||
| <resources> | ||
| <resource> | ||
| <directory>${project.basedir}/src/main/java</directory> | ||
| <includes> | ||
| <include>**/*.properties</include> | ||
| <include>**/*.xml</include> | ||
| </includes> | ||
| </resource> | ||
| </resources> | ||
| </build> | ||
| </project> | ||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
21 changes: 21 additions & 0 deletions
21
core/src/main/java/com/aerospike/mapper/exceptions/AerospikeMapperException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package com.aerospike.mapper.exceptions; | ||
|
|
||
| /** | ||
| * Base unchecked exception for Aerospike Object Mapper errors. | ||
| */ | ||
| public class AerospikeMapperException extends RuntimeException { | ||
|
|
||
| private static final long serialVersionUID = 1L; | ||
|
|
||
| public AerospikeMapperException(String message) { | ||
| super(message); | ||
| } | ||
|
|
||
| public AerospikeMapperException(String message, Throwable cause) { | ||
| super(message, cause); | ||
| } | ||
|
|
||
| public AerospikeMapperException(Throwable cause) { | ||
| super(cause); | ||
| } | ||
| } |
6 changes: 2 additions & 4 deletions
6
.../mapper/exceptions/NotAnnotatedClass.java → .../mapper/exceptions/NotAnnotatedClass.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,11 @@ | ||
| package com.aerospike.mapper.exceptions; | ||
|
|
||
| import com.aerospike.client.AerospikeException; | ||
|
|
||
| public class NotAnnotatedClass extends AerospikeException { | ||
| public class NotAnnotatedClass extends AerospikeMapperException { | ||
|
|
||
| private static final long serialVersionUID = -4781097961894057707L; | ||
| public static final int REASON_CODE = -109; | ||
|
|
||
| public NotAnnotatedClass(String description) { | ||
| super(REASON_CODE, description); | ||
| super(description); | ||
| } | ||
| } |
113 changes: 113 additions & 0 deletions
113
core/src/main/java/com/aerospike/mapper/tools/ClassCache.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| package com.aerospike.mapper.tools; | ||
|
|
||
| import com.aerospike.mapper.exceptions.AerospikeMapperException; | ||
| import com.aerospike.mapper.exceptions.NotAnnotatedClass; | ||
| import com.aerospike.mapper.tools.configuration.ClassConfig; | ||
| import com.aerospike.mapper.tools.configuration.Configuration; | ||
| import com.aerospike.mapper.tools.utils.TypeUtils; | ||
| import lombok.Getter; | ||
|
|
||
| import javax.validation.constraints.NotNull; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
|
|
||
| public class ClassCache { | ||
|
|
||
| @Getter | ||
| private static final ClassCache instance = new ClassCache(); | ||
| private final ConcurrentHashMap<Class<?>, ClassCacheEntry<?>> cacheMap = new ConcurrentHashMap<>(); | ||
| private final ConcurrentHashMap<String, ClassConfig> classesConfig = new ConcurrentHashMap<>(); | ||
| private final ConcurrentHashMap<String, ClassCacheEntry<?>> storedNameToCacheEntry = new ConcurrentHashMap<>(); | ||
| private final Object lock = new Object(); | ||
|
|
||
| private ClassCache() { | ||
| } | ||
|
|
||
| public <T> ClassCacheEntry<T> loadClass(@NotNull Class<T> clazz, IObjectMapper mapper) { | ||
| return loadClass(clazz, mapper, true); | ||
| } | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
| public <T> ClassCacheEntry<T> loadClass(@NotNull Class<T> clazz, IObjectMapper mapper, boolean requireRecord) { | ||
| if (clazz == null || clazz.isPrimitive() || clazz.equals(Object.class) || clazz.equals(String.class) | ||
| || clazz.equals(Character.class) || Number.class.isAssignableFrom(clazz)) { | ||
| return null; | ||
| } | ||
|
|
||
| ClassCacheEntry<T> entry = (ClassCacheEntry<T>) cacheMap.get(clazz); | ||
| if (entry == null || entry.isNotConstructed()) { | ||
| synchronized (lock) { | ||
| entry = (ClassCacheEntry<T>) cacheMap.get(clazz); | ||
| if (entry == null) { | ||
| try { | ||
| entry = new ClassCacheEntry<>(clazz, mapper, getClassConfig(clazz), requireRecord); | ||
| } catch (NotAnnotatedClass nae) { | ||
| return null; | ||
| } | ||
| cacheMap.put(clazz, entry); | ||
| try { | ||
| entry.construct(); | ||
| } catch (IllegalArgumentException iae) { | ||
| cacheMap.remove(clazz); | ||
| return null; | ||
| } catch (Exception e) { | ||
| cacheMap.remove(clazz); | ||
| throw e; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return entry; | ||
| } | ||
|
|
||
| void setStoredName(@NotNull ClassCacheEntry<?> entry, @NotNull String name) { | ||
| ClassCacheEntry<?> existingEntry = storedNameToCacheEntry.get(name); | ||
| if (existingEntry != null && !(existingEntry.equals(entry))) { | ||
| String errorMessage = String.format("Stored name of \"%s\" is used for both %s and %s", | ||
| name, existingEntry.getUnderlyingClass().getName(), entry.getUnderlyingClass().getName()); | ||
| throw new AerospikeMapperException(errorMessage); | ||
| } else { | ||
| storedNameToCacheEntry.put(name, entry); | ||
| } | ||
| } | ||
|
|
||
| public ClassCacheEntry<?> getCacheEntryFromStoredName(@NotNull String name) { | ||
| return storedNameToCacheEntry.get(name); | ||
| } | ||
|
|
||
| public boolean hasClass(Class<?> clazz) { | ||
| return cacheMap.containsKey(clazz); | ||
| } | ||
|
|
||
| public void clear() { | ||
| synchronized (lock) { | ||
| this.cacheMap.clear(); | ||
| this.classesConfig.clear(); | ||
| TypeUtils.clear(); | ||
| this.storedNameToCacheEntry.clear(); | ||
| } | ||
| } | ||
|
|
||
| public void addConfiguration(@NotNull Configuration configuration) { | ||
| for (ClassConfig thisConfig : configuration.getClasses()) { | ||
| classesConfig.put(thisConfig.getClassName(), thisConfig); | ||
| } | ||
| } | ||
|
|
||
| @SuppressWarnings("unused") | ||
| public ClassConfig getClassConfig(String className) { | ||
| return classesConfig.get(className); | ||
| } | ||
|
|
||
| public ClassConfig getClassConfig(Class<?> clazz) { | ||
| return classesConfig.get(clazz.getName()); | ||
| } | ||
|
|
||
| @SuppressWarnings("unused") | ||
| public boolean hasClassConfig(String className) { | ||
| return classesConfig.containsKey(className); | ||
| } | ||
|
|
||
| public boolean hasClassConfig(Class<?> clazz) { | ||
| return classesConfig.containsKey(clazz.getName()); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.