Skip to content

Commit 1013dc6

Browse files
authored
Merge pull request #309 from maxmind/greg/eng-3230
Support fallback constructor/parameter selection
2 parents 27a39cf + 1d28478 commit 1013dc6

17 files changed

+1659
-114
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,19 @@ CHANGELOG
2222
accessor methods (e.g., `binaryFormatMajorVersion()`, `databaseType()`, etc.).
2323
* `Network.getNetworkAddress()` and `Network.getPrefixLength()` have been
2424
replaced with record accessor methods `networkAddress()` and `prefixLength()`.
25+
* Removed the legacy `DatabaseRecord(T, InetAddress, int)` constructor; pass a
26+
`Network` when constructing records manually.
27+
* Deserialization improvements:
28+
* If no constructor is annotated with `@MaxMindDbConstructor`, records now
29+
use their canonical constructor automatically. For non‑record classes with
30+
a single public constructor, that constructor is used by default.
31+
* `@MaxMindDbParameter` annotations are now optional when parameter names
32+
match field names in the database: for records, component names are used;
33+
for classes, Java parameter names are used (when compiled with
34+
`-parameters`). Annotations still take precedence when present.
35+
* Added `@MaxMindDbIpAddress` and `@MaxMindDbNetwork` annotations to inject
36+
the lookup IP address and resulting network into constructors. Annotation
37+
metadata is cached per type to avoid repeated reflection overhead.
2538

2639
3.2.0 (2025-05-28)
2740
------------------

README.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,88 @@ public class Lookup {
120120
}
121121
```
122122

123+
### Constructor and parameter selection
124+
125+
- Preferred: annotate a constructor with `@MaxMindDbConstructor` and its
126+
parameters with `@MaxMindDbParameter(name = "...")`.
127+
- Records: if no constructor is annotated, the canonical record constructor is
128+
used automatically. Record component names are used as field names.
129+
- Classes with a single public constructor: if no constructor is annotated,
130+
that constructor is used automatically.
131+
- Unannotated parameters: when a parameter is not annotated, the reader falls
132+
back to the parameter name. For records, this is the component name; for
133+
classes, this is the Java parameter name. To use Java parameter names at
134+
runtime, compile your model classes with the `-parameters` flag (Maven:
135+
`maven-compiler-plugin` with `<parameters>true</parameters>`).
136+
If Java parameter names are unavailable (no `-parameters`) and there is no
137+
`@MaxMindDbParameter` annotation, the reader throws a
138+
`ParameterNotFoundException` with guidance.
139+
140+
Defaults for missing values
141+
142+
- Provide a default with
143+
`@MaxMindDbParameter(name = "...", useDefault = true, defaultValue = "...")`.
144+
- Supports primitives, boxed types, and `String`. If `defaultValue` is empty
145+
and `useDefault` is true, Java defaults are used (0, false, 0.0, empty
146+
string).
147+
- Example:
148+
149+
```java
150+
@MaxMindDbConstructor
151+
Example(
152+
@MaxMindDbParameter(name = "count", useDefault = true, defaultValue = "0")
153+
int count,
154+
@MaxMindDbParameter(
155+
name = "enabled",
156+
useDefault = true,
157+
defaultValue = "true"
158+
)
159+
boolean enabled
160+
) { }
161+
```
162+
163+
Lookup context injection
164+
165+
- Use `@MaxMindDbIpAddress` to inject the IP address being decoded.
166+
Supported parameter types are `InetAddress` and `String`.
167+
- Use `@MaxMindDbNetwork` to inject the network of the resulting record.
168+
Supported parameter types are `Network` and `String`.
169+
- Context annotations cannot be combined with `@MaxMindDbParameter` on the same
170+
constructor argument. Values are populated for every lookup without being
171+
cached between different IPs.
172+
173+
Custom deserialization
174+
175+
- Use `@MaxMindDbCreator` to mark a static factory method or constructor that
176+
should be used for custom deserialization of a type from a MaxMind DB file.
177+
- This annotation is similar to Jackson's `@JsonCreator` and is useful for
178+
types that need custom deserialization logic, such as enums with non-standard
179+
string representations or types that require special initialization.
180+
- The annotation can be applied to both constructors and static factory methods.
181+
- Example with an enum:
182+
183+
```java
184+
public enum ConnectionType {
185+
DIALUP("Dialup"),
186+
CABLE_DSL("Cable/DSL");
187+
188+
private final String name;
189+
190+
ConnectionType(String name) {
191+
this.name = name;
192+
}
193+
194+
@MaxMindDbCreator
195+
public static ConnectionType fromString(String s) {
196+
return switch (s) {
197+
case "Dialup" -> DIALUP;
198+
case "Cable/DSL" -> CABLE_DSL;
199+
default -> null;
200+
};
201+
}
202+
}
203+
```
204+
123205
You can also use the reader object to iterate over the database.
124206
The `reader.networks()` and `reader.networksWithin()` methods can
125207
be used for this purpose.

pom.xml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<modelVersion>4.0.0</modelVersion>
44
<groupId>com.maxmind.db</groupId>
55
<artifactId>maxmind-db</artifactId>
6-
<version>3.2.0</version>
6+
<version>4.0.0-SNAPSHOT</version>
77
<packaging>jar</packaging>
88
<name>MaxMind DB Reader</name>
99
<description>Reader for MaxMind DB</description>
@@ -150,6 +150,7 @@
150150
<release>17</release>
151151
<source>17</source>
152152
<target>17</target>
153+
<parameters>true</parameters>
153154
</configuration>
154155
</plugin>
155156
<plugin>
@@ -218,10 +219,4 @@
218219
</build>
219220
</profile>
220221
</profiles>
221-
<distributionManagement>
222-
<repository>
223-
<id>sonatype-nexus-staging</id>
224-
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
225-
</repository>
226-
</distributionManagement>
227222
</project>

src/main/java/com/maxmind/db/BufferHolder.java

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.maxmind.db;
22

33
import com.maxmind.db.Reader.FileMode;
4+
import java.io.ByteArrayOutputStream;
45
import java.io.File;
56
import java.io.IOException;
67
import java.io.InputStream;
@@ -13,6 +14,10 @@ final class BufferHolder {
1314
// DO NOT PASS OUTSIDE THIS CLASS. Doing so will remove thread safety.
1415
private final Buffer buffer;
1516

17+
// Reasonable I/O buffer size for reading from InputStream.
18+
// This is separate from chunk size which determines MultiBuffer chunk allocation.
19+
private static final int IO_BUFFER_SIZE = 16 * 1024; // 16KB
20+
1621
BufferHolder(File database, FileMode mode) throws IOException {
1722
this(database, mode, MultiBuffer.DEFAULT_CHUNK_SIZE);
1823
}
@@ -78,29 +83,49 @@ final class BufferHolder {
7883
if (null == stream) {
7984
throw new NullPointerException("Unable to use a NULL InputStream");
8085
}
81-
var chunks = new ArrayList<ByteBuffer>();
82-
var total = 0L;
83-
var tmp = new byte[chunkSize];
86+
87+
// Read data from the stream in chunks to support databases >2GB.
88+
// Invariant: All chunks except the last are exactly chunkSize bytes.
89+
var chunks = new ArrayList<byte[]>();
90+
var currentChunkStream = new ByteArrayOutputStream();
91+
var tmp = new byte[IO_BUFFER_SIZE];
8492
int read;
8593

8694
while (-1 != (read = stream.read(tmp))) {
87-
var chunk = ByteBuffer.allocate(read);
88-
chunk.put(tmp, 0, read);
89-
chunk.flip();
90-
chunks.add(chunk);
91-
total += read;
92-
}
95+
var offset = 0;
96+
while (offset < read) {
97+
var spaceInCurrentChunk = chunkSize - currentChunkStream.size();
98+
var toWrite = Math.min(spaceInCurrentChunk, read - offset);
9399

94-
if (total <= chunkSize) {
95-
var data = new byte[(int) total];
96-
var pos = 0;
97-
for (var chunk : chunks) {
98-
System.arraycopy(chunk.array(), 0, data, pos, chunk.capacity());
99-
pos += chunk.capacity();
100+
currentChunkStream.write(tmp, offset, toWrite);
101+
offset += toWrite;
102+
103+
// When chunk is exactly full, save it and start a new one.
104+
// This guarantees all non-final chunks are exactly chunkSize.
105+
if (currentChunkStream.size() == chunkSize) {
106+
chunks.add(currentChunkStream.toByteArray());
107+
currentChunkStream = new ByteArrayOutputStream();
108+
}
100109
}
101-
this.buffer = SingleBuffer.wrap(data);
110+
}
111+
112+
// Handle last partial chunk (could be empty if total is multiple of chunkSize)
113+
if (currentChunkStream.size() > 0) {
114+
chunks.add(currentChunkStream.toByteArray());
115+
}
116+
117+
if (chunks.size() == 1) {
118+
// For databases that fit in a single chunk, use SingleBuffer
119+
this.buffer = SingleBuffer.wrap(chunks.get(0));
102120
} else {
103-
this.buffer = new MultiBuffer(chunks.toArray(new ByteBuffer[0]), chunkSize);
121+
// For large databases, wrap chunks in ByteBuffers and use MultiBuffer
122+
// Guaranteed: chunks[0..n-2] all have length == chunkSize
123+
// chunks[n-1] may have length < chunkSize
124+
var buffers = new ByteBuffer[chunks.size()];
125+
for (var i = 0; i < chunks.size(); i++) {
126+
buffers[i] = ByteBuffer.wrap(chunks.get(i));
127+
}
128+
this.buffer = new MultiBuffer(buffers, chunkSize);
104129
}
105130
}
106131

src/main/java/com/maxmind/db/CachedConstructor.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ record CachedConstructor<T>(
77
Constructor<T> constructor,
88
Class<?>[] parameterTypes,
99
java.lang.reflect.Type[] parameterGenericTypes,
10-
Map<String, Integer> parameterIndexes
11-
) {
12-
}
10+
Map<String, Integer> parameterIndexes,
11+
Object[] parameterDefaults,
12+
ParameterInjection[] parameterInjections,
13+
boolean requiresLookupContext
14+
) {}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.maxmind.db;
2+
3+
import java.lang.reflect.Method;
4+
5+
/**
6+
* Cached creator method information for efficient deserialization.
7+
* A creator method is a static factory method annotated with {@link MaxMindDbCreator}
8+
* that converts a decoded value to the target type.
9+
*
10+
* @param method the static factory method annotated with {@link MaxMindDbCreator}
11+
* @param parameterType the parameter type accepted by the creator method
12+
*/
13+
record CachedCreator(
14+
Method method,
15+
Class<?> parameterType
16+
) {}
Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package com.maxmind.db;
22

3-
import java.net.InetAddress;
4-
53
/**
64
* DatabaseRecord represents the data and metadata associated with a database
75
* lookup.
@@ -14,15 +12,4 @@
1412
* the largest network where all of the IPs in the network have the same
1513
* data.
1614
*/
17-
public record DatabaseRecord<T>(T data, Network network) {
18-
/**
19-
* Create a new record.
20-
*
21-
* @param data the data for the record in the database.
22-
* @param ipAddress the IP address used in the lookup.
23-
* @param prefixLength the network prefix length associated with the record in the database.
24-
*/
25-
public DatabaseRecord(T data, InetAddress ipAddress, int prefixLength) {
26-
this(data, new Network(ipAddress, prefixLength));
27-
}
28-
}
15+
public record DatabaseRecord<T>(T data, Network network) {}

0 commit comments

Comments
 (0)