Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ CHANGELOG
accessor methods (e.g., `binaryFormatMajorVersion()`, `databaseType()`, etc.).
* `Network.getNetworkAddress()` and `Network.getPrefixLength()` have been
replaced with record accessor methods `networkAddress()` and `prefixLength()`.
* Removed the legacy `DatabaseRecord(T, InetAddress, int)` constructor; pass a
`Network` when constructing records manually.
* Deserialization improvements:
* If no constructor is annotated with `@MaxMindDbConstructor`, records now
use their canonical constructor automatically. For non‑record classes with
a single public constructor, that constructor is used by default.
* `@MaxMindDbParameter` annotations are now optional when parameter names
match field names in the database: for records, component names are used;
for classes, Java parameter names are used (when compiled with
`-parameters`). Annotations still take precedence when present.
* Added `@MaxMindDbIpAddress` and `@MaxMindDbNetwork` annotations to inject
the lookup IP address and resulting network into constructors. Annotation
metadata is cached per type to avoid repeated reflection overhead.

3.2.0 (2025-05-28)
------------------
Expand Down
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,88 @@ public class Lookup {
}
```

### Constructor and parameter selection

- Preferred: annotate a constructor with `@MaxMindDbConstructor` and its
parameters with `@MaxMindDbParameter(name = "...")`.
- Records: if no constructor is annotated, the canonical record constructor is
used automatically. Record component names are used as field names.
- Classes with a single public constructor: if no constructor is annotated,
that constructor is used automatically.
- Unannotated parameters: when a parameter is not annotated, the reader falls
back to the parameter name. For records, this is the component name; for
classes, this is the Java parameter name. To use Java parameter names at
runtime, compile your model classes with the `-parameters` flag (Maven:
`maven-compiler-plugin` with `<parameters>true</parameters>`).
If Java parameter names are unavailable (no `-parameters`) and there is no
`@MaxMindDbParameter` annotation, the reader throws a
`ParameterNotFoundException` with guidance.

Defaults for missing values

- Provide a default with
`@MaxMindDbParameter(name = "...", useDefault = true, defaultValue = "...")`.
- Supports primitives, boxed types, and `String`. If `defaultValue` is empty
and `useDefault` is true, Java defaults are used (0, false, 0.0, empty
string).
- Example:

```java
@MaxMindDbConstructor
Example(
@MaxMindDbParameter(name = "count", useDefault = true, defaultValue = "0")
int count,
@MaxMindDbParameter(
name = "enabled",
useDefault = true,
defaultValue = "true"
)
boolean enabled
) { }
```

Lookup context injection

- Use `@MaxMindDbIpAddress` to inject the IP address being decoded.
Supported parameter types are `InetAddress` and `String`.
- Use `@MaxMindDbNetwork` to inject the network of the resulting record.
Supported parameter types are `Network` and `String`.
- Context annotations cannot be combined with `@MaxMindDbParameter` on the same
constructor argument. Values are populated for every lookup without being
cached between different IPs.

Custom deserialization

- Use `@MaxMindDbCreator` to mark a static factory method or constructor that
should be used for custom deserialization of a type from a MaxMind DB file.
- This annotation is similar to Jackson's `@JsonCreator` and is useful for
types that need custom deserialization logic, such as enums with non-standard
string representations or types that require special initialization.
- The annotation can be applied to both constructors and static factory methods.
- Example with an enum:

```java
public enum ConnectionType {
DIALUP("Dialup"),
CABLE_DSL("Cable/DSL");

private final String name;

ConnectionType(String name) {
this.name = name;
}

@MaxMindDbCreator
public static ConnectionType fromString(String s) {
return switch (s) {
case "Dialup" -> DIALUP;
case "Cable/DSL" -> CABLE_DSL;
default -> null;
};
}
}
```

You can also use the reader object to iterate over the database.
The `reader.networks()` and `reader.networksWithin()` methods can
be used for this purpose.
Expand Down
9 changes: 2 additions & 7 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.maxmind.db</groupId>
<artifactId>maxmind-db</artifactId>
<version>3.2.0</version>
<version>4.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>MaxMind DB Reader</name>
<description>Reader for MaxMind DB</description>
Expand Down Expand Up @@ -150,6 +150,7 @@
<release>17</release>
<source>17</source>
<target>17</target>
<parameters>true</parameters>
</configuration>
</plugin>
<plugin>
Expand Down Expand Up @@ -218,10 +219,4 @@
</build>
</profile>
</profiles>
<distributionManagement>
<repository>
<id>sonatype-nexus-staging</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
</project>
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The distributionManagement section was removed. If this repository still needs to publish artifacts to Maven Central or other repositories, this removal could break the release process. Verify that artifact distribution is handled elsewhere or restore this configuration if needed.

Copilot uses AI. Check for mistakes.
59 changes: 42 additions & 17 deletions src/main/java/com/maxmind/db/BufferHolder.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.maxmind.db;

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

// Reasonable I/O buffer size for reading from InputStream.
// This is separate from chunk size which determines MultiBuffer chunk allocation.
private static final int IO_BUFFER_SIZE = 16 * 1024; // 16KB
Comment on lines +17 to +19
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The comment states '16KB' but should specify that this is for stream I/O operations. Consider rephrasing to: 'Reasonable I/O buffer size (16KB) for reading from InputStream. This is separate from chunkSize which determines MultiBuffer chunk allocation.'

Suggested change
// Reasonable I/O buffer size for reading from InputStream.
// This is separate from chunk size which determines MultiBuffer chunk allocation.
private static final int IO_BUFFER_SIZE = 16 * 1024; // 16KB
// Reasonable I/O buffer size (16KB) for reading from InputStream.
// This is separate from chunkSize which determines MultiBuffer chunk allocation.
private static final int IO_BUFFER_SIZE = 16 * 1024;

Copilot uses AI. Check for mistakes.

BufferHolder(File database, FileMode mode) throws IOException {
this(database, mode, MultiBuffer.DEFAULT_CHUNK_SIZE);
}
Expand Down Expand Up @@ -78,29 +83,49 @@ final class BufferHolder {
if (null == stream) {
throw new NullPointerException("Unable to use a NULL InputStream");
}
var chunks = new ArrayList<ByteBuffer>();
var total = 0L;
var tmp = new byte[chunkSize];

// Read data from the stream in chunks to support databases >2GB.
// Invariant: All chunks except the last are exactly chunkSize bytes.
var chunks = new ArrayList<byte[]>();
var currentChunkStream = new ByteArrayOutputStream();
var tmp = new byte[IO_BUFFER_SIZE];
int read;

while (-1 != (read = stream.read(tmp))) {
var chunk = ByteBuffer.allocate(read);
chunk.put(tmp, 0, read);
chunk.flip();
chunks.add(chunk);
total += read;
}
var offset = 0;
while (offset < read) {
var spaceInCurrentChunk = chunkSize - currentChunkStream.size();
var toWrite = Math.min(spaceInCurrentChunk, read - offset);

if (total <= chunkSize) {
var data = new byte[(int) total];
var pos = 0;
for (var chunk : chunks) {
System.arraycopy(chunk.array(), 0, data, pos, chunk.capacity());
pos += chunk.capacity();
currentChunkStream.write(tmp, offset, toWrite);
offset += toWrite;

// When chunk is exactly full, save it and start a new one.
// This guarantees all non-final chunks are exactly chunkSize.
if (currentChunkStream.size() == chunkSize) {
chunks.add(currentChunkStream.toByteArray());
currentChunkStream = new ByteArrayOutputStream();
}
}
this.buffer = SingleBuffer.wrap(data);
}

// Handle last partial chunk (could be empty if total is multiple of chunkSize)
if (currentChunkStream.size() > 0) {
chunks.add(currentChunkStream.toByteArray());
}

if (chunks.size() == 1) {
// For databases that fit in a single chunk, use SingleBuffer
this.buffer = SingleBuffer.wrap(chunks.get(0));
} else {
this.buffer = new MultiBuffer(chunks.toArray(new ByteBuffer[0]), chunkSize);
// For large databases, wrap chunks in ByteBuffers and use MultiBuffer
// Guaranteed: chunks[0..n-2] all have length == chunkSize
// chunks[n-1] may have length < chunkSize
var buffers = new ByteBuffer[chunks.size()];
for (var i = 0; i < chunks.size(); i++) {
buffers[i] = ByteBuffer.wrap(chunks.get(i));
}
this.buffer = new MultiBuffer(buffers, chunkSize);
}
}

Expand Down
8 changes: 5 additions & 3 deletions src/main/java/com/maxmind/db/CachedConstructor.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ record CachedConstructor<T>(
Constructor<T> constructor,
Class<?>[] parameterTypes,
java.lang.reflect.Type[] parameterGenericTypes,
Map<String, Integer> parameterIndexes
) {
}
Map<String, Integer> parameterIndexes,
Object[] parameterDefaults,
ParameterInjection[] parameterInjections,
boolean requiresLookupContext
) {}
16 changes: 16 additions & 0 deletions src/main/java/com/maxmind/db/CachedCreator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.maxmind.db;

import java.lang.reflect.Method;

/**
* Cached creator method information for efficient deserialization.
* A creator method is a static factory method annotated with {@link MaxMindDbCreator}
* that converts a decoded value to the target type.
*
* @param method the static factory method annotated with {@link MaxMindDbCreator}
* @param parameterType the parameter type accepted by the creator method
*/
record CachedCreator(
Method method,
Class<?> parameterType
) {}
15 changes: 1 addition & 14 deletions src/main/java/com/maxmind/db/DatabaseRecord.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.maxmind.db;

import java.net.InetAddress;

/**
* DatabaseRecord represents the data and metadata associated with a database
* lookup.
Expand All @@ -14,15 +12,4 @@
* the largest network where all of the IPs in the network have the same
* data.
*/
public record DatabaseRecord<T>(T data, Network network) {
/**
* Create a new record.
*
* @param data the data for the record in the database.
* @param ipAddress the IP address used in the lookup.
* @param prefixLength the network prefix length associated with the record in the database.
*/
public DatabaseRecord(T data, InetAddress ipAddress, int prefixLength) {
this(data, new Network(ipAddress, prefixLength));
}
}
public record DatabaseRecord<T>(T data, Network network) {}
Loading
Loading