Skip to content

Commit 5a20b0d

Browse files
agrgrCopilot
andcommitted
Refactor ClassCache, ClassCacheEntry, MappingConverter, ObjectReferenceMapper, ObjectEmbedMapper to use IObjectMapper/IRecordConverter
- Replace IBaseAeroMapper with IObjectMapper in ClassCache, ClassCacheEntry, MappingConverter, ObjectReferenceMapper, ObjectEmbedMapper - Remove policy storage (defaultPolicies, childrenPolicies, specificPolicies, determinePolicy, setDefaultPolicies, setReactiveDefaultPolicies) from ClassCache; moved to new PolicyCache singleton in legacy - Remove policy constructor params and getter methods (getReadPolicy, getWritePolicy, getBatchPolicy, getQueryPolicy, getScanPolicy) from ClassCacheEntry - Remove getBins, constructAndHydrate(Key,Record), hydrateFromRecord from ClassCacheEntry; add getKeyFieldName(), isKeyFieldStoredAsBin(), setGenerationValue() helpers - Replace IAerospikeClient with RecordLoader in MappingConverter constructor; new AerospikeRecordLoader implements RecordLoader via IAerospikeClient - Remove Key/Record convertToObject overloads from MappingConverter; keep map/list-based overloads and add resolveDependencies=false variant - ObjectReferenceMapper uses mapper.getRecordLoader() for record fetches - Add IRecordConverter core interface; IObjectMapper returns IRecordConverter (MappingConverter implements IRecordConverter) - IBaseAeroMapper extends IObjectMapper with covariant MappingConverter return - AeroMapper and ReactiveAeroMapper implement getRecordLoader(); use PolicyCache for policy lookups instead of ClassCacheEntry policy getters - Also update TypeUtils, ListMapper, MapMapper, PropertyDefinition, MapperUtils and virtuallist classes to use IObjectMapper; fix BaseVirtualList write policy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 312fb81 commit 5a20b0d

209 files changed

Lines changed: 1659 additions & 565 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/copilot-instructions.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Copilot Instructions for Aerospike Java Object Mapper
2+
3+
## Running Java commands
4+
JAVA_HOME=/usr/lib/jvm/java-17-openjdk-arm64
5+
PATH=/usr/lib/jvm/java-17-openjdk-arm64/bin:$PATH
6+
7+
## Build & Test
8+
9+
```bash
10+
# Compile (skip tests)
11+
mvn compile -B
12+
13+
# Run all tests (requires a running Aerospike server on localhost:3000)
14+
mvn clean test -B -U
15+
16+
# Run a single test class
17+
mvn test -Dtest=AeroMapperTest -B
18+
19+
# Run a single test method
20+
mvn test -Dtest=AeroMapperTest#testSimpleSave -B
21+
22+
# Run tests against a different Aerospike host
23+
mvn test -Dtest.host=myhost:3000 -B
24+
```
25+
26+
Java 8 source/target compatibility. No separate lint step.
27+
28+
## Architecture
29+
30+
This is an annotation-driven ORM that maps Java POJOs to Aerospike database records.
31+
It has two parallel APIs: synchronous (`AeroMapper`) and reactive (`ReactiveAeroMapper`),
32+
both built through the same `AbstractBuilder<T>` pattern.
33+
34+
### Core data flow (sync)
35+
36+
```
37+
Write path (Use-case → Aerospike):
38+
39+
save(obj) → ClassCacheEntry.getBins(obj) → IAerospikeClient.put(bins)
40+
41+
1. JavaMapperApplication → Entry point, calls mapper.save(customer)
42+
2. AeroMapper → API layer, creates WritePolicy, extracts key, converts to bins
43+
3. ClassCacheEntry<Customer> → Reflection engine, iterates fields, extracts values
44+
4. TypeMapper → Converts Java types (Date, List) to Aerospike format (Long, Map)
45+
5. AerospikeClient → SDK handles network I/O
46+
6. Aerospike Server → Persists record in namespace "test", set "customer"
47+
48+
Read path (Aerospike → Use-case):
49+
50+
read(Class<T>, key) → IAerospikeClient.get() → MappingConverter → ClassCacheEntry.hydrateFromRecord()
51+
52+
1. JavaMapperApplication → Calls mapper.read(Customer.class, "cust1")
53+
2. AeroMapper → Constructs Key, checks cache, calls client.get()
54+
3. AerospikeClient → Retrieves Record from server
55+
4. Aerospike Server → Returns Record with bin data
56+
5. MappingConverter → Orchestrates conversion
57+
6. ClassCacheEntry<Customer> → Constructs Customer, iterates bins, populates fields (hydrateFromRecord())
58+
7. TypeMapper → Converts Aerospike format back to Java types
59+
8. Customer Object → Fully hydrated and returned
60+
61+
VirtualList append path (e.g. appending an Item to a Container's embedded list):
62+
63+
mapper.asBackedList(container, "items", Item.class).append(new Item(500, new Date(), "Item5"))
64+
65+
1. AeroMapper.asBackedList(container, "items", Item.class) → Constructs VirtualList, resolves ClassCacheEntry for Container + Item, extracts ListMapper from the @AerospikeEmbed bin "items"
66+
2. VirtualList.append(item) → Delegates to VirtualListInteractors
67+
3. VirtualListInteractors.getAppendOperation() → ListMapper.toAerospikeInstanceFormat(item) converts Item to Aerospike-native format (Map/List depending on EmbedType)
68+
4. VirtualListInteractors → Builds CDT operation: MapOperation.put(binName, key, value) for MAP embed, or ListOperation.append(binName, value) for LIST embed
69+
5. AerospikeClient.operate(writePolicy, key, operation) → Sends CDT operation to server, atomically appends to the bin without reading the full list
70+
6. Aerospike Server → Appends element server-side, returns updated bin size
71+
7. VirtualList → Returns size (long); the in-memory container object is NOT updated
72+
73+
VirtualList query path (e.g. getByKeyRange):
74+
75+
list.getByKeyRange(100, 450)
76+
77+
1. VirtualList.getByKeyRange(start, end) → Sets return type, delegates to VirtualListInteractors
78+
2. VirtualListInteractors → Creates Interactor wrapping a DeferredOperation
79+
3. DeferredOperation.getOperation() → Translates keys via ClassCacheEntry.translateKeyToAerospikeKey(), builds MapOperation.getByKeyRange(binName, startValue, endValue, returnType)
80+
4. AerospikeClient.operate() → Sends CDT query to server
81+
5. Aerospike Server → Evaluates range server-side, returns matching entries
82+
6. Interactor.getResult() → Chains ResultsUnpackers (ArrayUnpacker iterates results, calls ListMapper.fromAerospikeInstanceFormat() per element)
83+
7. MappingConverter.resolveDependencies() → Resolves any nested @AerospikeReference objects
84+
8. VirtualList → Returns List<Item> of matched elements
85+
```
86+
87+
### Key classes and their roles
88+
89+
- **`AeroMapper` / `ReactiveAeroMapper`** — Public API entry points (sync returns objects, reactive returns `Mono`/`Flux`). Both are instantiated via their inner `Builder` class, never directly.
90+
- **`AbstractBuilder<T>`** — Shared builder logic: register custom type converters, preload classes, set policies, load YAML configuration.
91+
- **`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.
92+
- **`ClassCache`** — Singleton cache of `ClassCacheEntry` instances.
93+
- **`MappingConverter`** — Orchestrates type conversion during serialization/deserialization using registered `TypeMapper` instances.
94+
- **`TypeMapper`** — Abstract base for custom type converters. Override `toAerospikeFormat()` and `fromAerospikeFormat()`. Register via `builder.addConverter()`.
95+
96+
### Annotation system (`com.aerospike.mapper.annotations`)
97+
98+
- `@AerospikeRecord` — Marks a class as mappable; defines namespace, set, TTL.
99+
- `@AerospikeKey` — Designates the primary key field.
100+
- `@AerospikeBin` — Customizes bin (column) name for a field.
101+
- `@AerospikeEmbed` / `@AerospikeReference` — Relationship mapping (embedded vs. referenced sub-objects).
102+
- `@AerospikeExclude` — Excludes a field from mapping.
103+
- `@ToAerospike` / `@FromAerospike` — Custom per-field conversion methods.
104+
- `@AerospikeVersion` / `@AerospikeGeneration` — Optimistic concurrency control.
105+
- `@AerospikeConstructor` / `@ParamFrom` — Controls object construction from records.
106+
107+
### Type mappers (`tools/mappers/`)
108+
109+
Built-in mappers for Java primitives, `Date`, `Instant`, `LocalDate`, `LocalDateTime`, `BigDecimal`, `BigInteger`,
110+
enums, arrays, lists, and maps. Custom mappers extend `TypeMapper`.
111+
112+
### Virtual Lists (`tools/virtuallist/`)
113+
114+
Aerospike CDT-backed lists that support lazy loading and server-side operations without retrieving entire collections.
115+
Both sync (`VirtualList`) and reactive (`ReactiveVirtualList`) variants exist.
116+
117+
### Configuration (`tools/configuration/`)
118+
119+
Alternative to annotations: YAML-based configuration for class mappings via `builder.withConfiguration()`.
120+
121+
## Conventions
122+
123+
- **Package root**: `com.aerospike.mapper` — annotations, exceptions, and `tools` sub-packages.
124+
- **Lombok**: Used in production code (provided scope). Ensure IDE/build has Lombok support.
125+
- **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.
126+
- **Test setup**: Each test method gets a fresh `AeroMapper` via `Builder` and clears `ClassCache` in `@BeforeEach`. Tables are truncated before tests to ensure isolation.

core/pom.xml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0"
2+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>com.aerospike</groupId>
8+
<artifactId>java-object-mapper-parent</artifactId>
9+
<version>2.6.0</version>
10+
</parent>
11+
12+
<artifactId>java-object-mapper-core</artifactId>
13+
<packaging>jar</packaging>
14+
15+
<name>Aerospike Object Mapper Core</name>
16+
<description>Core annotation-processing and type-mapping engine with no Aerospike client dependency.</description>
17+
18+
<dependencies>
19+
<dependency>
20+
<groupId>javax.validation</groupId>
21+
<artifactId>validation-api</artifactId>
22+
</dependency>
23+
<dependency>
24+
<groupId>org.apache.commons</groupId>
25+
<artifactId>commons-lang3</artifactId>
26+
</dependency>
27+
<dependency>
28+
<groupId>com.fasterxml.jackson.dataformat</groupId>
29+
<artifactId>jackson-dataformat-yaml</artifactId>
30+
</dependency>
31+
<dependency>
32+
<groupId>org.projectlombok</groupId>
33+
<artifactId>lombok</artifactId>
34+
</dependency>
35+
<dependency>
36+
<groupId>org.junit.jupiter</groupId>
37+
<artifactId>junit-jupiter</artifactId>
38+
</dependency>
39+
<dependency>
40+
<groupId>org.mockito</groupId>
41+
<artifactId>mockito-core</artifactId>
42+
</dependency>
43+
</dependencies>
44+
45+
<build>
46+
<plugins>
47+
<plugin>
48+
<artifactId>maven-compiler-plugin</artifactId>
49+
</plugin>
50+
<plugin>
51+
<groupId>org.apache.maven.plugins</groupId>
52+
<artifactId>maven-dependency-plugin</artifactId>
53+
</plugin>
54+
<plugin>
55+
<groupId>org.apache.maven.plugins</groupId>
56+
<artifactId>maven-surefire-plugin</artifactId>
57+
</plugin>
58+
</plugins>
59+
<resources>
60+
<resource>
61+
<directory>${project.basedir}/src/main/java</directory>
62+
<includes>
63+
<include>**/*.properties</include>
64+
<include>**/*.xml</include>
65+
</includes>
66+
</resource>
67+
</resources>
68+
</build>
69+
</project>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.aerospike.mapper.tools;
2+
3+
/**
4+
* Minimal contract that core-bound classes use to interact with the enclosing mapper.
5+
* Implemented by both the legacy AeroMapper and the fluent FluentAeroMapper.
6+
*/
7+
public interface IObjectMapper {
8+
IRecordConverter getMappingConverter();
9+
10+
RecordLoader getRecordLoader();
11+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.aerospike.mapper.tools;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
6+
/**
7+
* Core-level abstraction for converting Aerospike bin data to Java objects.
8+
* Implemented by MappingConverter in the legacy module.
9+
*/
10+
public interface IRecordConverter {
11+
<T> T convertToObject(Class<T> clazz, Map<String, Object> record);
12+
<T> T convertToObject(Class<T> clazz, List<Object> record);
13+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.aerospike.mapper.tools;
2+
3+
/**
4+
* Client-agnostic identifier for an Aerospike record.
5+
* Used by RecordLoader for batch operations.
6+
*/
7+
public class RecordKey {
8+
public final String namespace;
9+
public final String setName;
10+
/** User-key value; null when the record is identified by digest only. */
11+
public final Object keyValue;
12+
/** Pre-computed digest; null when the record is identified by keyValue. */
13+
public final byte[] digest;
14+
15+
public RecordKey(String namespace, String setName, Object keyValue) {
16+
this.namespace = namespace;
17+
this.setName = setName;
18+
this.keyValue = keyValue;
19+
this.digest = null;
20+
}
21+
22+
public RecordKey(String namespace, String setName, byte[] digest) {
23+
this.namespace = namespace;
24+
this.setName = setName;
25+
this.keyValue = null;
26+
this.digest = digest;
27+
}
28+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.aerospike.mapper.tools;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
6+
/**
7+
* Abstraction over the Aerospike client's record-fetch operations.
8+
* Implemented by the legacy module (via IAerospikeClient) and the fluent module (via Session).
9+
* Used by MappingConverter to resolve @AerospikeReference objects without a direct client dependency.
10+
*/
11+
public interface RecordLoader {
12+
/**
13+
* Fetch a single record by its user-key value. Returns null if not found.
14+
*/
15+
Map<String, Object> getRecord(String namespace, String setName, Object keyValue);
16+
17+
/**
18+
* Fetch a single record by its pre-computed digest. Returns null if not found.
19+
*/
20+
Map<String, Object> getRecordByDigest(String namespace, String setName, byte[] digest);
21+
22+
/**
23+
* Batch-fetch records. Entries with no matching record are returned as null in the list.
24+
*/
25+
List<Map<String, Object>> getBatchRecords(List<RecordKey> keys);
26+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.aerospike.mapper.tools;
2+
3+
/**
4+
* Holds annotation-derived per-class Aerospike record settings.
5+
* Decouples ClassCacheEntry from legacy client policy types.
6+
*/
7+
public class RecordMetaConfig {
8+
private int ttl = -1;
9+
private Boolean sendKey = null;
10+
private Boolean durableDelete = null;
11+
private Boolean mapAll = null;
12+
13+
public int getTtl() { return ttl; }
14+
public void setTtl(int ttl) { this.ttl = ttl; }
15+
16+
public Boolean getSendKey() { return sendKey; }
17+
public void setSendKey(Boolean sendKey) { this.sendKey = sendKey; }
18+
19+
public Boolean getDurableDelete() { return durableDelete; }
20+
public void setDurableDelete(Boolean durableDelete) { this.durableDelete = durableDelete; }
21+
22+
public Boolean getMapAll() { return mapAll; }
23+
public void setMapAll(Boolean mapAll) { this.mapAll = mapAll; }
24+
}

fluent/pom.xml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0"
2+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>com.aerospike</groupId>
8+
<artifactId>java-object-mapper-parent</artifactId>
9+
<version>2.6.0</version>
10+
</parent>
11+
12+
<artifactId>java-object-mapper-fluent</artifactId>
13+
<packaging>jar</packaging>
14+
15+
<name>Aerospike Object Mapper Fluent</name>
16+
<description>Aerospike Object Mapper using the new fluent Aerospike Java client. Implements RecordMapper and RecordMappingFactory.</description>
17+
18+
<dependencies>
19+
<dependency>
20+
<groupId>com.aerospike</groupId>
21+
<artifactId>java-object-mapper-core</artifactId>
22+
</dependency>
23+
<dependency>
24+
<groupId>com.aerospike</groupId>
25+
<artifactId>aerospike-client-fluent</artifactId>
26+
</dependency>
27+
<dependency>
28+
<groupId>org.projectlombok</groupId>
29+
<artifactId>lombok</artifactId>
30+
</dependency>
31+
<dependency>
32+
<groupId>org.junit.jupiter</groupId>
33+
<artifactId>junit-jupiter</artifactId>
34+
</dependency>
35+
<dependency>
36+
<groupId>org.mockito</groupId>
37+
<artifactId>mockito-core</artifactId>
38+
</dependency>
39+
</dependencies>
40+
41+
<build>
42+
<plugins>
43+
<plugin>
44+
<artifactId>maven-compiler-plugin</artifactId>
45+
</plugin>
46+
<plugin>
47+
<groupId>org.apache.maven.plugins</groupId>
48+
<artifactId>maven-dependency-plugin</artifactId>
49+
</plugin>
50+
<plugin>
51+
<groupId>org.apache.maven.plugins</groupId>
52+
<artifactId>maven-surefire-plugin</artifactId>
53+
</plugin>
54+
</plugins>
55+
<resources>
56+
<resource>
57+
<directory>${project.basedir}/src/main/java</directory>
58+
<includes>
59+
<include>**/*.properties</include>
60+
<include>**/*.xml</include>
61+
</includes>
62+
</resource>
63+
</resources>
64+
</build>
65+
</project>

0 commit comments

Comments
 (0)