diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b80d986..89c3df84 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,23 @@ CHANGELOG
------------------
* Java 17 or greater is now required.
+* Added support for MaxMind DB files larger than 2GB. The library now uses
+ an internal Buffer abstraction that can handle databases exceeding the
+ 2GB ByteBuffer limit. Files under 2GB continue to use a single ByteBuffer
+ for optimal performance. Requested by nonetallt. GitHub #154. Fixed by
+ Silvano Cerza. GitHub #289.
+* `Metadata.getBuildDate()` has been replaced with `buildTime()`, which returns
+ `java.time.Instant` instead of `java.util.Date`. The instant represents the
+ database build time in UTC.
+* `DatabaseRecord`, `Metadata`, `Network`, and internal `DecodedValue` classes
+ have been converted to records. The following API changes were made:
+ * `DatabaseRecord.getData()` and `DatabaseRecord.getNetwork()` have been
+ replaced with record accessor methods `data()` and `network()`.
+ * Simple getter methods on `Metadata` (e.g., `getBinaryFormatMajorVersion()`,
+ `getDatabaseType()`, etc.) have been replaced with their corresponding record
+ accessor methods (e.g., `binaryFormatMajorVersion()`, `databaseType()`, etc.).
+ * `Network.getNetworkAddress()` and `Network.getPrefixLength()` have been
+ replaced with record accessor methods `networkAddress()` and `prefixLength()`.
3.2.0 (2025-05-28)
------------------
diff --git a/src/main/java/com/maxmind/db/Buffer.java b/src/main/java/com/maxmind/db/Buffer.java
index eef66f73..b12dbda7 100644
--- a/src/main/java/com/maxmind/db/Buffer.java
+++ b/src/main/java/com/maxmind/db/Buffer.java
@@ -1,7 +1,5 @@
package com.maxmind.db;
-import java.io.IOException;
-import java.nio.channels.FileChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetDecoder;
@@ -12,8 +10,11 @@
*
*
This interface is designed to provide a long-based API while
* remaining compatible with the limitations of underlying storage.
+ *
+ *
All underlying {@link java.nio.ByteBuffer}s are read-only to prevent
+ * accidental modification of shared data.
*/
-interface Buffer {
+sealed interface Buffer permits SingleBuffer, MultiBuffer {
/**
* Returns the total capacity of this buffer in bytes.
*
@@ -96,16 +97,6 @@ interface Buffer {
*/
Buffer duplicate();
- /**
- * Reads data from the given channel into this buffer starting at the
- * current position.
- *
- * @param channel the file channel
- * @return the number of bytes read
- * @throws IOException if an I/O error occurs
- */
- long readFrom(FileChannel channel) throws IOException;
-
/**
* Decodes the buffer's content into a string using the given decoder.
*
diff --git a/src/main/java/com/maxmind/db/BufferHolder.java b/src/main/java/com/maxmind/db/BufferHolder.java
index c0ad56cf..cd60a915 100644
--- a/src/main/java/com/maxmind/db/BufferHolder.java
+++ b/src/main/java/com/maxmind/db/BufferHolder.java
@@ -8,7 +8,6 @@
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
-import java.util.List;
final class BufferHolder {
// DO NOT PASS OUTSIDE THIS CLASS. Doing so will remove thread safety.
@@ -23,18 +22,48 @@ final class BufferHolder {
FileChannel channel = file.getChannel()) {
long size = channel.size();
if (mode == FileMode.MEMORY) {
- Buffer buf;
if (size <= chunkSize) {
- buf = new SingleBuffer(size);
+ // Allocate, read, and make read-only
+ ByteBuffer buffer = ByteBuffer.allocate((int) size);
+ if (channel.read(buffer) != size) {
+ throw new IOException("Unable to read "
+ + database.getName()
+ + " into memory. Unexpected end of stream.");
+ }
+ buffer.flip();
+ this.buffer = new SingleBuffer(buffer);
} else {
- buf = new MultiBuffer(size);
- }
- if (buf.readFrom(channel) != buf.capacity()) {
- throw new IOException("Unable to read "
- + database.getName()
- + " into memory. Unexpected end of stream.");
+ // Allocate chunks, read, and make read-only
+ var fullChunks = (int) (size / chunkSize);
+ var remainder = (int) (size % chunkSize);
+ var totalChunks = fullChunks + (remainder > 0 ? 1 : 0);
+ var buffers = new ByteBuffer[totalChunks];
+
+ for (int i = 0; i < fullChunks; i++) {
+ buffers[i] = ByteBuffer.allocate(chunkSize);
+ }
+ if (remainder > 0) {
+ buffers[totalChunks - 1] = ByteBuffer.allocate(remainder);
+ }
+
+ var totalRead = 0L;
+ for (var buffer : buffers) {
+ var read = channel.read(buffer);
+ if (read == -1) {
+ break;
+ }
+ totalRead += read;
+ buffer.flip();
+ }
+
+ if (totalRead != size) {
+ throw new IOException("Unable to read "
+ + database.getName()
+ + " into memory. Unexpected end of stream.");
+ }
+
+ this.buffer = new MultiBuffer(buffers, chunkSize);
}
- this.buffer = buf;
} else {
if (size <= chunkSize) {
this.buffer = SingleBuffer.mapFromChannel(channel);
@@ -45,28 +74,17 @@ final class BufferHolder {
}
}
- /**
- * Construct a ThreadBuffer from the provided URL.
- *
- * @param stream the source of my bytes.
- * @throws IOException if unable to read from your source.
- * @throws NullPointerException if you provide a NULL InputStream
- */
- BufferHolder(InputStream stream) throws IOException {
- this(stream, MultiBuffer.DEFAULT_CHUNK_SIZE);
- }
-
BufferHolder(InputStream stream, int chunkSize) throws IOException {
if (null == stream) {
throw new NullPointerException("Unable to use a NULL InputStream");
}
- List chunks = new ArrayList<>();
- long total = 0;
- byte[] tmp = new byte[chunkSize];
+ var chunks = new ArrayList();
+ var total = 0L;
+ var tmp = new byte[chunkSize];
int read;
while (-1 != (read = stream.read(tmp))) {
- ByteBuffer chunk = ByteBuffer.allocate(read);
+ var chunk = ByteBuffer.allocate(read);
chunk.put(tmp, 0, read);
chunk.flip();
chunks.add(chunk);
@@ -74,9 +92,9 @@ final class BufferHolder {
}
if (total <= chunkSize) {
- byte[] data = new byte[(int) total];
- int pos = 0;
- for (ByteBuffer chunk : chunks) {
+ 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();
}
diff --git a/src/main/java/com/maxmind/db/CHMCache.java b/src/main/java/com/maxmind/db/CHMCache.java
index 0b22d4cc..250b45b1 100644
--- a/src/main/java/com/maxmind/db/CHMCache.java
+++ b/src/main/java/com/maxmind/db/CHMCache.java
@@ -37,7 +37,7 @@ public CHMCache(int capacity) {
@Override
public DecodedValue get(CacheKey> key, Loader loader) throws IOException {
- DecodedValue value = cache.get(key);
+ var value = cache.get(key);
if (value == null) {
value = loader.load(key);
if (!cacheFull) {
diff --git a/src/main/java/com/maxmind/db/ClosedDatabaseException.java b/src/main/java/com/maxmind/db/ClosedDatabaseException.java
index 6a02c28a..8d3169fa 100644
--- a/src/main/java/com/maxmind/db/ClosedDatabaseException.java
+++ b/src/main/java/com/maxmind/db/ClosedDatabaseException.java
@@ -6,9 +6,6 @@
* Signals that the underlying database has been closed.
*/
public class ClosedDatabaseException extends IOException {
-
- private static final long serialVersionUID = 1L;
-
ClosedDatabaseException() {
super("The MaxMind DB has been closed.");
}
diff --git a/src/main/java/com/maxmind/db/ConstructorNotFoundException.java b/src/main/java/com/maxmind/db/ConstructorNotFoundException.java
index 2f803a87..781afca1 100644
--- a/src/main/java/com/maxmind/db/ConstructorNotFoundException.java
+++ b/src/main/java/com/maxmind/db/ConstructorNotFoundException.java
@@ -5,8 +5,6 @@
* constructor in the class with the MaxMindDbConstructor annotation.
*/
public class ConstructorNotFoundException extends RuntimeException {
- private static final long serialVersionUID = 1L;
-
ConstructorNotFoundException(String message) {
super(message);
}
diff --git a/src/main/java/com/maxmind/db/DatabaseRecord.java b/src/main/java/com/maxmind/db/DatabaseRecord.java
index 6779408b..9ff1c7ac 100644
--- a/src/main/java/com/maxmind/db/DatabaseRecord.java
+++ b/src/main/java/com/maxmind/db/DatabaseRecord.java
@@ -7,11 +7,14 @@
* lookup.
*
* @param the type to deserialize the returned value to
+ * @param data the data for the record in the database. The record will be
+ * {@code null} if there was no data for the address in the
+ * database.
+ * @param network the network associated with the record in the database. This is
+ * the largest network where all of the IPs in the network have the same
+ * data.
*/
-public final class DatabaseRecord {
- private final T data;
- private final Network network;
-
+public record DatabaseRecord(T data, Network network) {
/**
* Create a new record.
*
@@ -20,25 +23,6 @@ public final class DatabaseRecord {
* @param prefixLength the network prefix length associated with the record in the database.
*/
public DatabaseRecord(T data, InetAddress ipAddress, int prefixLength) {
- this.data = data;
- this.network = new Network(ipAddress, prefixLength);
- }
-
- /**
- * @return the data for the record in the database. The record will be
- * null if there was no data for the address in the
- * database.
- */
- public T getData() {
- return data;
- }
-
- /**
- * @return the network associated with the record in the database. This is
- * the largest network where all of the IPs in the network have the same
- * data.
- */
- public Network getNetwork() {
- return network;
+ this(data, new Network(ipAddress, prefixLength));
}
}
diff --git a/src/main/java/com/maxmind/db/DecodedValue.java b/src/main/java/com/maxmind/db/DecodedValue.java
index 8a6f4454..c3174f69 100644
--- a/src/main/java/com/maxmind/db/DecodedValue.java
+++ b/src/main/java/com/maxmind/db/DecodedValue.java
@@ -3,15 +3,7 @@
/**
* {@code DecodedValue} is a wrapper for the decoded value and the number of bytes used
* to decode it.
+ *
+ * @param value the decoded value
*/
-public final class DecodedValue {
- final Object value;
-
- DecodedValue(Object value) {
- this.value = value;
- }
-
- Object getValue() {
- return value;
- }
-}
+record DecodedValue(Object value) {}
diff --git a/src/main/java/com/maxmind/db/Decoder.java b/src/main/java/com/maxmind/db/Decoder.java
index 48dc24ce..ee6b9930 100644
--- a/src/main/java/com/maxmind/db/Decoder.java
+++ b/src/main/java/com/maxmind/db/Decoder.java
@@ -6,7 +6,6 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.math.BigInteger;
-import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
@@ -69,7 +68,7 @@ T decode(long offset, Class cls) throws IOException {
}
this.buffer.position(offset);
- return cls.cast(decode(cls, null).getValue());
+ return cls.cast(decode(cls, null).value());
}
private DecodedValue decode(CacheKey key) throws IOException {
@@ -87,26 +86,26 @@ private DecodedValue decode(CacheKey key) throws IOException {
private DecodedValue decode(Class cls, java.lang.reflect.Type genericType)
throws IOException {
- int ctrlByte = 0xFF & this.buffer.get();
+ var ctrlByte = 0xFF & this.buffer.get();
- Type type = Type.fromControlByte(ctrlByte);
+ var type = Type.fromControlByte(ctrlByte);
// Pointers are a special case, we don't read the next 'size' bytes, we
// use the size to determine the length of the pointer and then follow
// it.
if (type.equals(Type.POINTER)) {
- int pointerSize = ((ctrlByte >>> 3) & 0x3) + 1;
- int base = pointerSize == 4 ? (byte) 0 : (byte) (ctrlByte & 0x7);
- int packed = this.decodeInteger(base, pointerSize);
- long pointer = packed + this.pointerBase + POINTER_VALUE_OFFSETS[pointerSize];
+ var pointerSize = ((ctrlByte >>> 3) & 0x3) + 1;
+ var base = pointerSize == 4 ? (byte) 0 : (byte) (ctrlByte & 0x7);
+ var packed = this.decodeInteger(base, pointerSize);
+ var pointer = packed + this.pointerBase + POINTER_VALUE_OFFSETS[pointerSize];
return decodePointer(pointer, cls, genericType);
}
if (type.equals(Type.EXTENDED)) {
- int nextByte = this.buffer.get();
+ var nextByte = this.buffer.get();
- int typeNum = nextByte + 7;
+ var typeNum = nextByte + 7;
if (typeNum < 8) {
throw new InvalidDatabaseException(
@@ -132,11 +131,10 @@ private DecodedValue decode(Class cls, java.lang.reflect.Type genericType
DecodedValue decodePointer(long pointer, Class> cls, java.lang.reflect.Type genericType)
throws IOException {
- long targetOffset = pointer;
- long position = buffer.position();
+ var position = buffer.position();
- CacheKey> key = new CacheKey<>(targetOffset, cls, genericType);
- DecodedValue o = cache.get(key, cacheLoader);
+ var key = new CacheKey<>(pointer, cls, genericType);
+ var o = cache.get(key, cacheLoader);
buffer.position(position);
return o;
@@ -154,7 +152,7 @@ private Object decodeByType(
case ARRAY:
Class> elementClass = Object.class;
if (genericType instanceof ParameterizedType ptype) {
- java.lang.reflect.Type[] actualTypes = ptype.getActualTypeArguments();
+ var actualTypes = ptype.getActualTypeArguments();
if (actualTypes.length == 1) {
elementClass = (Class>) actualTypes[0];
}
@@ -186,9 +184,9 @@ private Object decodeByType(
}
private String decodeString(long size) throws CharacterCodingException {
- long oldLimit = buffer.limit();
+ var oldLimit = buffer.limit();
buffer.limit(buffer.position() + size);
- String s = buffer.decode(utfDecoder);
+ var s = buffer.decode(utfDecoder);
buffer.limit(oldLimit);
return s;
}
@@ -234,7 +232,7 @@ static int decodeInteger(Buffer buffer, int base, int size) {
}
private BigInteger decodeBigInteger(int size) {
- byte[] bytes = this.getByteArray(size);
+ var bytes = this.getByteArray(size);
return new BigInteger(1, bytes);
}
@@ -287,10 +285,10 @@ private List decodeArray(
throw new DeserializationException(
"No constructor found for the List: " + e.getMessage(), e);
}
- Object[] parameters = {size};
+ var parameters = new Object[]{size};
try {
@SuppressWarnings("unchecked")
- List array2 = (List) constructor.newInstance(parameters);
+ var array2 = (List) constructor.newInstance(parameters);
array = array2;
} catch (InstantiationException
| IllegalAccessException
@@ -300,7 +298,7 @@ private List decodeArray(
}
for (int i = 0; i < size; i++) {
- Object e = this.decode(elementClass, null).getValue();
+ var e = this.decode(elementClass, null).value();
array.add(elementClass.cast(e));
}
@@ -315,9 +313,9 @@ private Object decodeMap(
if (Map.class.isAssignableFrom(cls) || cls.equals(Object.class)) {
Class> valueClass = Object.class;
if (genericType instanceof ParameterizedType ptype) {
- java.lang.reflect.Type[] actualTypes = ptype.getActualTypeArguments();
+ var actualTypes = ptype.getActualTypeArguments();
if (actualTypes.length == 2) {
- Class> keyClass = (Class>) actualTypes[0];
+ var keyClass = (Class>) actualTypes[0];
if (!keyClass.equals(String.class)) {
throw new DeserializationException("Map keys must be strings.");
}
@@ -347,10 +345,10 @@ private Map decodeMapIntoMap(
throw new DeserializationException(
"No constructor found for the Map: " + e.getMessage(), e);
}
- Object[] parameters = {size};
+ var parameters = new Object[]{size};
try {
@SuppressWarnings("unchecked")
- Map map2 = (Map) constructor.newInstance(parameters);
+ var map2 = (Map) constructor.newInstance(parameters);
map = map2;
} catch (InstantiationException
| IllegalAccessException
@@ -360,8 +358,8 @@ private Map decodeMapIntoMap(
}
for (int i = 0; i < size; i++) {
- String key = (String) this.decode(String.class, null).getValue();
- Object value = this.decode(valueClass, null).getValue();
+ var key = (String) this.decode(String.class, null).value();
+ var value = this.decode(valueClass, null).value();
try {
map.put(key, valueClass.cast(value));
} catch (ClassCastException e) {
@@ -375,7 +373,7 @@ private Map decodeMapIntoMap(
private Object decodeMapIntoObject(int size, Class cls)
throws IOException {
- CachedConstructor cachedConstructor = getCachedConstructor(cls);
+ var cachedConstructor = getCachedConstructor(cls);
Constructor constructor;
Class>[] parameterTypes;
java.lang.reflect.Type[] parameterGenericTypes;
@@ -388,9 +386,9 @@ private Object decodeMapIntoObject(int size, Class cls)
parameterGenericTypes = constructor.getGenericParameterTypes();
parameterIndexes = new HashMap<>();
- Annotation[][] annotations = constructor.getParameterAnnotations();
+ var annotations = constructor.getParameterAnnotations();
for (int i = 0; i < constructor.getParameterCount(); i++) {
- String parameterName = getParameterName(cls, i, annotations[i]);
+ var parameterName = getParameterName(cls, i, annotations[i]);
parameterIndexes.put(parameterName, i);
}
@@ -410,13 +408,13 @@ private Object decodeMapIntoObject(int size, Class cls)
parameterIndexes = cachedConstructor.parameterIndexes();
}
- Object[] parameters = new Object[parameterTypes.length];
+ var parameters = new Object[parameterTypes.length];
for (int i = 0; i < size; i++) {
- String key = (String) this.decode(String.class, null).getValue();
+ var key = (String) this.decode(String.class, null).value();
- Integer parameterIndex = parameterIndexes.get(key);
+ var parameterIndex = parameterIndexes.get(key);
if (parameterIndex == null) {
- long offset = this.nextValueOffset(this.buffer.position(), 1);
+ var offset = this.nextValueOffset(this.buffer.position(), 1);
this.buffer.position(offset);
continue;
}
@@ -424,7 +422,7 @@ private Object decodeMapIntoObject(int size, Class cls)
parameters[parameterIndex] = this.decode(
parameterTypes[parameterIndex],
parameterGenericTypes[parameterIndex]
- ).getValue();
+ ).value();
}
try {
@@ -434,9 +432,9 @@ private Object decodeMapIntoObject(int size, Class cls)
| InvocationTargetException e) {
throw new DeserializationException("Error creating object: " + e.getMessage(), e);
} catch (IllegalArgumentException e) {
- StringBuilder sbErrors = new StringBuilder();
- for (String key : parameterIndexes.keySet()) {
- int index = parameterIndexes.get(key);
+ var sbErrors = new StringBuilder();
+ for (var key : parameterIndexes.keySet()) {
+ var index = parameterIndexes.get(key);
if (parameters[index] != null
&& !parameters[index].getClass().isAssignableFrom(parameterTypes[index])) {
sbErrors.append(" argument type mismatch in " + key + " MMDB Type: "
@@ -458,8 +456,8 @@ private CachedConstructor getCachedConstructor(Class cls) {
private static Constructor findConstructor(Class cls)
throws ConstructorNotFoundException {
- Constructor>[] constructors = cls.getConstructors();
- for (Constructor> constructor : constructors) {
+ var constructors = cls.getConstructors();
+ for (var constructor : constructors) {
if (constructor.getAnnotation(MaxMindDbConstructor.class) == null) {
continue;
}
@@ -477,11 +475,11 @@ private static String getParameterName(
int index,
Annotation[] annotations
) throws ParameterNotFoundException {
- for (Annotation annotation : annotations) {
+ for (var annotation : annotations) {
if (!annotation.annotationType().equals(MaxMindDbParameter.class)) {
continue;
}
- MaxMindDbParameter paramAnnotation = (MaxMindDbParameter) annotation;
+ var paramAnnotation = (MaxMindDbParameter) annotation;
return paramAnnotation.name();
}
throw new ParameterNotFoundException(
@@ -495,15 +493,15 @@ private long nextValueOffset(long offset, int numberToSkip)
return offset;
}
- CtrlData ctrlData = this.getCtrlData(offset);
- int ctrlByte = ctrlData.ctrlByte();
- int size = ctrlData.size();
+ var ctrlData = this.getCtrlData(offset);
+ var ctrlByte = ctrlData.ctrlByte();
+ var size = ctrlData.size();
offset = ctrlData.offset();
- Type type = ctrlData.type();
+ var type = ctrlData.type();
switch (type) {
case POINTER:
- int pointerSize = ((ctrlByte >>> 3) & 0x3) + 1;
+ var pointerSize = ((ctrlByte >>> 3) & 0x3) + 1;
offset += pointerSize;
break;
case MAP:
@@ -531,15 +529,15 @@ private CtrlData getCtrlData(long offset)
}
this.buffer.position(offset);
- int ctrlByte = 0xFF & this.buffer.get();
+ var ctrlByte = 0xFF & this.buffer.get();
offset++;
- Type type = Type.fromControlByte(ctrlByte);
+ var type = Type.fromControlByte(ctrlByte);
if (type.equals(Type.EXTENDED)) {
- int nextByte = this.buffer.get();
+ var nextByte = this.buffer.get();
- int typeNum = nextByte + 7;
+ var typeNum = nextByte + 7;
if (typeNum < 8) {
throw new InvalidDatabaseException(
@@ -552,9 +550,9 @@ private CtrlData getCtrlData(long offset)
offset++;
}
- int size = ctrlByte & 0x1f;
+ var size = ctrlByte & 0x1f;
if (size >= 29) {
- int bytesToRead = size - 28;
+ var bytesToRead = size - 28;
offset += bytesToRead;
size = switch (size) {
case 29 -> 29 + (0xFF & buffer.get());
@@ -571,7 +569,7 @@ private byte[] getByteArray(int length) {
}
private static byte[] getByteArray(Buffer buffer, int length) {
- byte[] bytes = new byte[length];
+ var bytes = new byte[length];
buffer.get(bytes);
return bytes;
}
diff --git a/src/main/java/com/maxmind/db/DeserializationException.java b/src/main/java/com/maxmind/db/DeserializationException.java
index c18b24ba..e7e3db5a 100644
--- a/src/main/java/com/maxmind/db/DeserializationException.java
+++ b/src/main/java/com/maxmind/db/DeserializationException.java
@@ -4,8 +4,6 @@
* Signals that the value could not be deserialized into the type.
*/
public class DeserializationException extends RuntimeException {
- private static final long serialVersionUID = 1L;
-
DeserializationException() {
super("Database value cannot be deserialized into the type.");
}
diff --git a/src/main/java/com/maxmind/db/InvalidDatabaseException.java b/src/main/java/com/maxmind/db/InvalidDatabaseException.java
index 776ea4b0..0e1c94ea 100644
--- a/src/main/java/com/maxmind/db/InvalidDatabaseException.java
+++ b/src/main/java/com/maxmind/db/InvalidDatabaseException.java
@@ -8,9 +8,6 @@
* corrupt or otherwise not in a format supported by the reader.
*/
public class InvalidDatabaseException extends IOException {
-
- private static final long serialVersionUID = 6161763462364823003L;
-
/**
* @param message A message describing the reason why the exception was thrown.
*/
diff --git a/src/main/java/com/maxmind/db/Metadata.java b/src/main/java/com/maxmind/db/Metadata.java
index aa2d733d..ddeb8b01 100644
--- a/src/main/java/com/maxmind/db/Metadata.java
+++ b/src/main/java/com/maxmind/db/Metadata.java
@@ -1,177 +1,54 @@
package com.maxmind.db;
import java.math.BigInteger;
-import java.util.Date;
+import java.time.Instant;
import java.util.List;
import java.util.Map;
/**
* {@code Metadata} holds data associated with the database itself.
+ *
+ * @param binaryFormatMajorVersion The major version number for the database's
+ * binary format.
+ * @param binaryFormatMinorVersion The minor version number for the database's
+ * binary format.
+ * @param buildEpoch The date of the database build.
+ * @param databaseType A string that indicates the structure of each
+ * data record associated with an IP address.
+ * The actual definition of these structures is
+ * left up to the database creator.
+ * @param languages List of languages supported by the database.
+ * @param description Map from language code to description in that
+ * language.
+ * @param ipVersion Whether the database contains IPv4 or IPv6
+ * address data. The only possible values are 4
+ * and 6.
+ * @param nodeCount The number of nodes in the search tree.
+ * @param recordSize The number of bits in a record in the search
+ * tree. Note that each node consists of two
+ * records.
*/
-public final class Metadata {
- private final int binaryFormatMajorVersion;
- private final int binaryFormatMinorVersion;
-
- private final BigInteger buildEpoch;
-
- private final String databaseType;
-
- private final Map description;
-
- private final int ipVersion;
-
- private final List languages;
-
- private final int nodeByteSize;
-
- private final long nodeCount;
-
- private final int recordSize;
-
- private final long searchTreeSize;
-
- /**
- * Constructs a {@code Metadata} object.
- *
- * @param binaryFormatMajorVersion The major version number for the database's
- * binary format.
- * @param binaryFormatMinorVersion The minor version number for the database's
- * binary format.
- * @param buildEpoch The date of the database build.
- * @param databaseType A string that indicates the structure of each
- * data record associated with an IP address.
- * The actual definition of these structures is
- * left up to the database creator.
- * @param languages List of languages supported by the database.
- * @param description Map from language code to description in that
- * language.
- * @param ipVersion Whether the database contains IPv4 or IPv6
- * address data. The only possible values are 4
- * and 6.
- * @param nodeCount The number of nodes in the search tree.
- * @param recordSize The number of bits in a record in the search
- * tree. Note that each node consists of two
- * records.
+public record Metadata(
+ @MaxMindDbParameter(name = "binary_format_major_version") int binaryFormatMajorVersion,
+ @MaxMindDbParameter(name = "binary_format_minor_version") int binaryFormatMinorVersion,
+ @MaxMindDbParameter(name = "build_epoch") BigInteger buildEpoch,
+ @MaxMindDbParameter(name = "database_type") String databaseType,
+ @MaxMindDbParameter(name = "languages") List languages,
+ @MaxMindDbParameter(name = "description") Map description,
+ @MaxMindDbParameter(name = "ip_version") int ipVersion,
+ @MaxMindDbParameter(name = "node_count") long nodeCount,
+ @MaxMindDbParameter(name = "record_size") int recordSize
+) {
+ /**
+ * Compact constructor for the Metadata record.
*/
@MaxMindDbConstructor
- public Metadata(
- @MaxMindDbParameter(name = "binary_format_major_version") int binaryFormatMajorVersion,
- @MaxMindDbParameter(name = "binary_format_minor_version") int binaryFormatMinorVersion,
- @MaxMindDbParameter(name = "build_epoch") BigInteger buildEpoch,
- @MaxMindDbParameter(name = "database_type") String databaseType,
- @MaxMindDbParameter(name = "languages") List languages,
- @MaxMindDbParameter(name = "description") Map description,
- @MaxMindDbParameter(name = "ip_version") int ipVersion,
- @MaxMindDbParameter(name = "node_count") long nodeCount,
- @MaxMindDbParameter(name = "record_size") int recordSize) {
- this.binaryFormatMajorVersion = binaryFormatMajorVersion;
- this.binaryFormatMinorVersion = binaryFormatMinorVersion;
- this.buildEpoch = buildEpoch;
- this.databaseType = databaseType;
- this.languages = languages;
- this.description = description;
- this.ipVersion = ipVersion;
- this.nodeCount = nodeCount;
- this.recordSize = recordSize;
-
- this.nodeByteSize = this.recordSize / 4;
- this.searchTreeSize = this.nodeCount * this.nodeByteSize;
- }
-
- /**
- * @return the major version number for the database's binary format.
- */
- public int getBinaryFormatMajorVersion() {
- return this.binaryFormatMajorVersion;
- }
-
- /**
- * @return the minor version number for the database's binary format.
- */
- public int getBinaryFormatMinorVersion() {
- return this.binaryFormatMinorVersion;
- }
-
- /**
- * @return the date of the database build.
- */
- public Date getBuildDate() {
- return new Date(this.buildEpoch.longValue() * 1000);
- }
-
- /**
- * @return a string that indicates the structure of each data record
- * associated with an IP address. The actual definition of these
- * structures is left up to the database creator.
- */
- public String getDatabaseType() {
- return this.databaseType;
- }
-
- /**
- * @return map from language code to description in that language.
- */
- public Map getDescription() {
- return this.description;
- }
-
- /**
- * @return whether the database contains IPv4 or IPv6 address data. The only
- * possible values are 4 and 6.
- */
- public int getIpVersion() {
- return this.ipVersion;
- }
-
- /**
- * @return list of languages supported by the database.
- */
- public List getLanguages() {
- return this.languages;
- }
-
- /**
- * @return the nodeByteSize
- */
- int getNodeByteSize() {
- return this.nodeByteSize;
- }
-
- /**
- * @return the number of nodes in the search tree.
- */
- long getNodeCount() {
- return this.nodeCount;
- }
+ public Metadata {}
/**
- * @return the number of bits in a record in the search tree. Note that each
- * node consists of two records.
- */
- int getRecordSize() {
- return this.recordSize;
- }
-
- /**
- * @return the searchTreeSize
- */
- long getSearchTreeSize() {
- return this.searchTreeSize;
- }
-
- /*
- * (non-Javadoc)
- *
- * @see java.lang.Object#toString()
+ * @return the instant of the database build in UTC.
*/
- @Override
- public String toString() {
- return "Metadata [binaryFormatMajorVersion="
- + this.binaryFormatMajorVersion + ", binaryFormatMinorVersion="
- + this.binaryFormatMinorVersion + ", buildEpoch="
- + this.buildEpoch + ", databaseType=" + this.databaseType
- + ", description=" + this.description + ", ipVersion="
- + this.ipVersion + ", nodeCount=" + this.nodeCount
- + ", recordSize=" + this.recordSize + "]";
+ public Instant buildTime() {
+ return Instant.ofEpochSecond(buildEpoch.longValue());
}
}
diff --git a/src/main/java/com/maxmind/db/MultiBuffer.java b/src/main/java/com/maxmind/db/MultiBuffer.java
index 9113a7d5..16a02014 100644
--- a/src/main/java/com/maxmind/db/MultiBuffer.java
+++ b/src/main/java/com/maxmind/db/MultiBuffer.java
@@ -6,7 +6,6 @@
import java.nio.channels.FileChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetDecoder;
-import java.nio.charset.CoderResult;
/**
* A {@link Buffer} implementation backed by multiple {@link ByteBuffer}s,
@@ -17,8 +16,11 @@
* a single logical position and limit across them.
*
*
Use this when working with databases/files that may exceed 2GB.
+ *
+ *
All underlying {@link ByteBuffer}s are read-only to prevent accidental
+ * modification of shared data.
*/
-class MultiBuffer implements Buffer {
+final class MultiBuffer implements Buffer {
/** Default maximum size per underlying chunk. */
static final int DEFAULT_CHUNK_SIZE = Integer.MAX_VALUE - 8;
@@ -30,16 +32,6 @@ class MultiBuffer implements Buffer {
private long position = 0;
private long limit;
- /**
- * Creates a new {@code MultiBuffer} with the given capacity, backed by
- * heap-allocated {@link ByteBuffer}s.
- *
- * @param capacity the total capacity in bytes
- */
- public MultiBuffer(long capacity) {
- this(capacity, DEFAULT_CHUNK_SIZE);
- }
-
/**
* Creates a new {@code MultiBuffer} backed by the given
* {@link ByteBuffer}s.
@@ -52,7 +44,7 @@ public MultiBuffer(long capacity) {
*/
MultiBuffer(ByteBuffer[] buffers, int chunkSize) {
for (int i = 0; i < buffers.length; i++) {
- ByteBuffer chunk = buffers[i];
+ var chunk = buffers[i];
if (chunk.capacity() == chunkSize) {
continue;
}
@@ -64,43 +56,19 @@ public MultiBuffer(long capacity) {
+ " is smaller than expected chunk size");
}
- this.buffers = buffers.clone();
- long capacity = 0;
- for (ByteBuffer buffer : buffers) {
- capacity += buffer.capacity();
+ // Make all buffers read-only
+ this.buffers = new ByteBuffer[buffers.length];
+ for (int i = 0; i < buffers.length; i++) {
+ this.buffers[i] = buffers[i].asReadOnlyBuffer();
}
- this.capacity = capacity;
- this.limit = capacity;
- this.chunkSize = chunkSize;
- }
- /**
- * Creates a new {@code MultiBuffer} with the given capacity, backed by
- * heap-allocated {@link ByteBuffer}s with the given chunk size.
- *
- * @param capacity the total capacity in bytes
- * @param chunkSize the size of each buffer chunk
- */
- MultiBuffer(long capacity, int chunkSize) {
- if (capacity <= 0) {
- throw new IllegalArgumentException("Capacity must be positive");
+ var capacity = 0L;
+ for (var buffer : this.buffers) {
+ capacity += buffer.capacity();
}
this.capacity = capacity;
this.limit = capacity;
this.chunkSize = chunkSize;
-
- int fullChunks = (int) (capacity / chunkSize);
- int remainder = (int) (capacity % chunkSize);
- int totalChunks = fullChunks + (remainder > 0 ? 1 : 0);
-
- this.buffers = new ByteBuffer[totalChunks];
-
- for (int i = 0; i < fullChunks; i++) {
- buffers[i] = ByteBuffer.allocate(chunkSize);
- }
- if (remainder > 0) {
- buffers[totalChunks - 1] = ByteBuffer.allocate(remainder);
- }
}
/** {@inheritDoc} */
@@ -147,7 +115,7 @@ public Buffer limit(long newLimit) {
/** {@inheritDoc} */
@Override
public byte get() {
- byte value = get(position);
+ var value = get(position);
position++;
return value;
}
@@ -155,22 +123,22 @@ public byte get() {
/** {@inheritDoc} */
@Override
public Buffer get(byte[] dst) {
- if (position + dst.length > limit) {
+ if (position > limit - dst.length) {
throw new IndexOutOfBoundsException(
"Read exceeds limit: position=" + position
+ ", length=" + dst.length
+ ", limit=" + limit
);
}
- long pos = position;
- int offset = 0;
- int length = dst.length;
+ var pos = position;
+ var offset = 0;
+ var length = dst.length;
while (length > 0) {
- int bufIndex = (int) (pos / this.chunkSize);
- int bufOffset = (int) (pos % this.chunkSize);
- ByteBuffer buf = buffers[bufIndex];
+ var bufIndex = (int) (pos / this.chunkSize);
+ var bufOffset = (int) (pos % this.chunkSize);
+ var buf = buffers[bufIndex];
buf.position(bufOffset);
- int toRead = Math.min(buf.remaining(), length);
+ var toRead = Math.min(buf.remaining(), length);
buf.get(dst, offset, toRead);
pos += toRead;
offset += toRead;
@@ -186,24 +154,24 @@ public byte get(long index) {
if (index < 0 || index >= limit) {
throw new IndexOutOfBoundsException("Index: " + index);
}
- int bufIndex = (int) (index / this.chunkSize);
- int offset = (int) (index % this.chunkSize);
+ var bufIndex = (int) (index / this.chunkSize);
+ var offset = (int) (index % this.chunkSize);
return buffers[bufIndex].get(offset);
}
/** {@inheritDoc} */
@Override
public double getDouble() {
- int bufIndex = (int) (position / this.chunkSize);
- int off = (int) (position % this.chunkSize);
- ByteBuffer buf = buffers[bufIndex];
+ var bufIndex = (int) (position / this.chunkSize);
+ var off = (int) (position % this.chunkSize);
+ var buf = buffers[bufIndex];
buf.position(off);
if (buf.remaining() >= 8) {
- double value = buf.getDouble();
+ var value = buf.getDouble();
position += 8;
return value;
} else {
- byte[] eight = new byte[8];
+ var eight = new byte[8];
get(eight);
return ByteBuffer.wrap(eight).getDouble();
}
@@ -212,16 +180,16 @@ public double getDouble() {
/** {@inheritDoc} */
@Override
public float getFloat() {
- int bufIndex = (int) (position / this.chunkSize);
- int off = (int) (position % this.chunkSize);
- ByteBuffer buf = buffers[bufIndex];
+ var bufIndex = (int) (position / this.chunkSize);
+ var off = (int) (position % this.chunkSize);
+ var buf = buffers[bufIndex];
buf.position(off);
if (buf.remaining() >= 4) {
- float value = buf.getFloat();
+ var value = buf.getFloat();
position += 4;
return value;
} else {
- byte[] four = new byte[4];
+ var four = new byte[4];
get(four);
return ByteBuffer.wrap(four).getFloat();
}
@@ -230,51 +198,16 @@ public float getFloat() {
/** {@inheritDoc} */
@Override
public Buffer duplicate() {
- ByteBuffer[] duplicatedBuffers = new ByteBuffer[buffers.length];
+ var duplicatedBuffers = new ByteBuffer[buffers.length];
for (int i = 0; i < buffers.length; i++) {
duplicatedBuffers[i] = buffers[i].duplicate();
}
- MultiBuffer copy = new MultiBuffer(duplicatedBuffers, chunkSize);
+ var copy = new MultiBuffer(duplicatedBuffers, chunkSize);
copy.position = this.position;
copy.limit = this.limit;
return copy;
}
- /** {@inheritDoc} */
- @Override
- public long readFrom(FileChannel channel) throws IOException {
- return this.readFrom(channel, DEFAULT_CHUNK_SIZE);
- }
-
- /**
- * Reads data from the given channel into this buffer starting at the
- * current position.
- *
- * @param channel the file channel
- * @param chunkSize the chunk size to use for positioning reads
- * @return the number of bytes read
- * @throws IOException if an I/O error occurs
- */
- long readFrom(FileChannel channel, int chunkSize) throws IOException {
- long totalRead = 0;
- long pos = position;
- for (int i = (int) (pos / chunkSize); i < buffers.length; i++) {
- ByteBuffer buf = buffers[i];
- buf.position((int) (pos % chunkSize));
- int read = channel.read(buf);
- if (read == -1) {
- break;
- }
- totalRead += read;
- pos += read;
- if (pos >= limit) {
- break;
- }
- }
- position = pos;
- return totalRead;
- }
-
/** {@inheritDoc} */
@Override
public String decode(CharsetDecoder decoder)
@@ -284,7 +217,7 @@ public String decode(CharsetDecoder decoder)
String decode(CharsetDecoder decoder, int maxCharBufferSize)
throws CharacterCodingException {
- long remainingBytes = limit - position;
+ var remainingBytes = limit - position;
// Cannot allocate more than maxCharBufferSize for CharBuffer
if (remainingBytes > maxCharBufferSize) {
@@ -293,21 +226,24 @@ String decode(CharsetDecoder decoder, int maxCharBufferSize)
);
}
- CharBuffer out = CharBuffer.allocate((int) remainingBytes);
- long pos = position;
+ var out = CharBuffer.allocate((int) remainingBytes);
+ var pos = position;
while (remainingBytes > 0) {
// Locate which underlying buffer we are in
- int bufIndex = (int) (pos / this.chunkSize);
- int bufOffset = (int) (pos % this.chunkSize);
+ var bufIndex = (int) (pos / this.chunkSize);
+ var bufOffset = (int) (pos % this.chunkSize);
- ByteBuffer srcView = buffers[bufIndex].duplicate();
+ var srcView = buffers[bufIndex];
+ var savedLimit = srcView.limit();
srcView.position(bufOffset);
- int toRead = (int) Math.min(srcView.remaining(), remainingBytes);
+ var toRead = (int) Math.min(srcView.remaining(), remainingBytes);
srcView.limit(bufOffset + toRead);
- CoderResult result = decoder.decode(srcView, out, false);
+ var result = decoder.decode(srcView, out, false);
+ srcView.limit(savedLimit);
+
if (result.isError()) {
result.throwException();
}
@@ -332,21 +268,21 @@ String decode(CharsetDecoder decoder, int maxCharBufferSize)
* @throws IOException if an I/O error occurs
*/
public static MultiBuffer mapFromChannel(FileChannel channel) throws IOException {
- long size = channel.size();
+ var size = channel.size();
if (size <= 0) {
throw new IllegalArgumentException("File channel has no data");
}
- int fullChunks = (int) (size / DEFAULT_CHUNK_SIZE);
- int remainder = (int) (size % DEFAULT_CHUNK_SIZE);
- int totalChunks = fullChunks + (remainder > 0 ? 1 : 0);
+ var fullChunks = (int) (size / DEFAULT_CHUNK_SIZE);
+ var remainder = (int) (size % DEFAULT_CHUNK_SIZE);
+ var totalChunks = fullChunks + (remainder > 0 ? 1 : 0);
- ByteBuffer[] buffers = new ByteBuffer[totalChunks];
- long remaining = size;
+ var buffers = new ByteBuffer[totalChunks];
+ var remaining = size;
for (int i = 0; i < totalChunks; i++) {
- long chunkPos = (long) i * DEFAULT_CHUNK_SIZE;
- long chunkSize = Math.min(DEFAULT_CHUNK_SIZE, remaining);
+ var chunkPos = (long) i * DEFAULT_CHUNK_SIZE;
+ var chunkSize = Math.min(DEFAULT_CHUNK_SIZE, remaining);
buffers[i] = channel.map(
FileChannel.MapMode.READ_ONLY,
chunkPos,
@@ -356,4 +292,4 @@ public static MultiBuffer mapFromChannel(FileChannel channel) throws IOException
}
return new MultiBuffer(buffers, DEFAULT_CHUNK_SIZE);
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/maxmind/db/Network.java b/src/main/java/com/maxmind/db/Network.java
index 32033e0a..2ba3ea2b 100644
--- a/src/main/java/com/maxmind/db/Network.java
+++ b/src/main/java/com/maxmind/db/Network.java
@@ -4,32 +4,19 @@
import java.net.UnknownHostException;
/**
- * Network represents an IP network.
+ * {@code Network} represents an IP network.
+ *
+ * @param ipAddress An IP address in the network. This does not have to be
+ * the first address in the network.
+ * @param prefixLength The prefix length for the network. This is the number of
+ * leading 1 bits in the subnet mask, sometimes also known as
+ * netmask length.
*/
-public final class Network {
- private final InetAddress ipAddress;
- private final int prefixLength;
- private InetAddress networkAddress = null;
-
- /**
- * Construct a Network
- *
- * @param ipAddress An IP address in the network. This does not have to be
- * the first address in the network.
- * @param prefixLength The prefix length for the network.
- */
- public Network(InetAddress ipAddress, int prefixLength) {
- this.ipAddress = ipAddress;
- this.prefixLength = prefixLength;
- }
-
+public record Network(InetAddress ipAddress, int prefixLength) {
/**
* @return The first address in the network.
*/
- public InetAddress getNetworkAddress() {
- if (networkAddress != null) {
- return networkAddress;
- }
+ public InetAddress networkAddress() {
byte[] ipBytes = ipAddress.getAddress();
byte[] networkBytes = new byte[ipBytes.length];
int curPrefix = prefixLength;
@@ -44,27 +31,19 @@ public InetAddress getNetworkAddress() {
}
try {
- networkAddress = InetAddress.getByAddress(networkBytes);
+ return InetAddress.getByAddress(networkBytes);
} catch (UnknownHostException e) {
throw new RuntimeException(
"Illegal network address byte length of " + networkBytes.length);
}
- return networkAddress;
}
- /**
- * @return The prefix length is the number of leading 1 bits in the subnet
- * mask. Sometimes also known as netmask length.
- */
- public int getPrefixLength() {
- return prefixLength;
- }
/**
* @return A string representation of the network in CIDR notation, e.g.,
- * 1.2.3.0/24 or 2001::/8.
+ * {@code 1.2.3.0/24} or {@code 2001::/8}.
*/
public String toString() {
- return getNetworkAddress().getHostAddress() + "/" + prefixLength;
+ return networkAddress().getHostAddress() + "/" + prefixLength;
}
}
diff --git a/src/main/java/com/maxmind/db/Networks.java b/src/main/java/com/maxmind/db/Networks.java
index cc77af25..00ed79cf 100644
--- a/src/main/java/com/maxmind/db/Networks.java
+++ b/src/main/java/com/maxmind/db/Networks.java
@@ -3,7 +3,6 @@
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
-import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Stack;
@@ -22,19 +21,6 @@ public final class Networks implements Iterator> {
private final Buffer buffer; /* Stores the buffer for Next() calls */
private final Class typeParameterClass;
- /**
- * Constructs a Networks instance.
- *
- * @param reader The reader object.
- * @param includeAliasedNetworks The boolean to include aliased networks.
- * @param typeParameterClass The type of data returned by the iterator.
- * @throws ClosedDatabaseException Exception for a closed database.
- */
- Networks(Reader reader, boolean includeAliasedNetworks, Class typeParameterClass)
- throws ClosedDatabaseException {
- this(reader, includeAliasedNetworks, new NetworkNode[0], typeParameterClass);
- }
-
/**
* Constructs a Networks instance.
*
@@ -60,16 +46,6 @@ public final class Networks implements Iterator> {
}
}
- /**
- * Constructs a Networks instance with includeAliasedNetworks set to false by default.
- *
- * @param reader The reader object.
- * @param typeParameterClass The type of data returned by the iterator.
- */
- Networks(Reader reader, Class typeParameterClass) throws ClosedDatabaseException {
- this(reader, false, typeParameterClass);
- }
-
/**
* Returns the next DataRecord.
*
@@ -79,11 +55,11 @@ public final class Networks implements Iterator> {
@Override
public DatabaseRecord next() {
try {
- T data = this.reader.resolveDataPointer(
+ var data = this.reader.resolveDataPointer(
this.buffer, this.lastNode.pointer, this.typeParameterClass);
- byte[] ip = this.lastNode.ip;
- int prefixLength = this.lastNode.prefix;
+ var ip = this.lastNode.ip;
+ var prefixLength = this.lastNode.prefix;
// We do this because uses of includeAliasedNetworks will get IPv4 networks
// from the ::FFFF:0:0/96. We want to return the IPv4 form of the address
@@ -95,7 +71,7 @@ public DatabaseRecord next() {
// If the ip is in ipv6 form, drop the prefix manually
// as InetAddress converts it to ipv4.
- InetAddress ipAddr = InetAddress.getByAddress(ip);
+ var ipAddr = InetAddress.getByAddress(ip);
if (ipAddr instanceof Inet4Address && ip.length > 4 && prefixLength > 96) {
prefixLength -= 96;
}
@@ -129,10 +105,10 @@ private boolean isInIpv4Subtree(byte[] ip) {
@Override
public boolean hasNext() {
while (!this.nodes.isEmpty()) {
- NetworkNode node = this.nodes.pop();
+ var node = this.nodes.pop();
// Next until we don't have data.
- while (node.pointer != this.reader.getMetadata().getNodeCount()) {
+ while (node.pointer != this.reader.getMetadata().nodeCount()) {
// This skips IPv4 aliases without hardcoding the networks that the writer
// currently aliases.
if (!this.includeAliasedNetworks && this.reader.getIpv4Start() != 0
@@ -141,12 +117,12 @@ public boolean hasNext() {
break;
}
- if (node.pointer > this.reader.getMetadata().getNodeCount()) {
+ if (node.pointer > this.reader.getMetadata().nodeCount()) {
this.lastNode = node;
return true;
}
- byte[] ipRight = Arrays.copyOf(node.ip, node.ip.length);
+ var ipRight = Arrays.copyOf(node.ip, node.ip.length);
if (ipRight.length <= (node.prefix >> 3)) {
throw new NetworksIterationException("Invalid search tree");
}
@@ -154,7 +130,7 @@ public boolean hasNext() {
ipRight[node.prefix >> 3] |= 1 << (7 - (node.prefix % 8));
try {
- long rightPointer = this.reader.readNode(this.buffer, node.pointer, 1);
+ var rightPointer = this.reader.readNode(this.buffer, node.pointer, 1);
node.prefix++;
this.nodes.push(new NetworkNode(ipRight, node.prefix, rightPointer));
@@ -169,7 +145,7 @@ public boolean hasNext() {
static class NetworkNode {
/** The IP address of the node. */
- public byte[] ip;
+ public final byte[] ip;
/** The prefix of the node. */
public int prefix;
/** The node number. */
diff --git a/src/main/java/com/maxmind/db/ParameterNotFoundException.java b/src/main/java/com/maxmind/db/ParameterNotFoundException.java
index bf67f7c1..e0d21339 100644
--- a/src/main/java/com/maxmind/db/ParameterNotFoundException.java
+++ b/src/main/java/com/maxmind/db/ParameterNotFoundException.java
@@ -5,8 +5,6 @@
* parameters of the constructor class with the MaxMindDbParameter annotation.
*/
public class ParameterNotFoundException extends RuntimeException {
- private static final long serialVersionUID = 1L;
-
ParameterNotFoundException(String message) {
super(message);
}
diff --git a/src/main/java/com/maxmind/db/Reader.java b/src/main/java/com/maxmind/db/Reader.java
index 2e6b1007..3af489d3 100644
--- a/src/main/java/com/maxmind/db/Reader.java
+++ b/src/main/java/com/maxmind/db/Reader.java
@@ -7,7 +7,6 @@
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
-import java.nio.ByteBuffer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
@@ -24,6 +23,8 @@ public final class Reader implements Closeable {
private final long ipV4Start;
private final Metadata metadata;
+ private final int nodeByteSize;
+ private final long searchTreeSize;
private final AtomicReference bufferHolderReference;
private final NodeCache cache;
private final ConcurrentHashMap, CachedConstructor>> constructors;
@@ -152,12 +153,16 @@ private Reader(BufferHolder bufferHolder, String name, NodeCache cache) throws I
}
this.cache = cache;
- Buffer buffer = bufferHolder.get();
+ var buffer = bufferHolder.get();
long start = this.findMetadataStart(buffer, name);
- Decoder metadataDecoder = new Decoder(this.cache, buffer, start);
+ var metadataDecoder = new Decoder(this.cache, buffer, start);
this.metadata = metadataDecoder.decode(start, Metadata.class);
+ // Calculate and cache these values as they are used in hot paths
+ this.nodeByteSize = this.metadata.recordSize() / 4;
+ this.searchTreeSize = this.metadata.nodeCount() * this.nodeByteSize;
+
this.ipV4Start = this.findIpV4StartNode(buffer);
this.constructors = new ConcurrentHashMap<>();
@@ -173,7 +178,7 @@ private Reader(BufferHolder bufferHolder, String name, NodeCache cache) throws I
* @throws IOException if a file I/O error occurs.
*/
public T get(InetAddress ipAddress, Class cls) throws IOException {
- return getRecord(ipAddress, cls).getData();
+ return getRecord(ipAddress, cls).data();
}
long getIpv4Start() {
@@ -193,15 +198,15 @@ long getIpv4Start() {
public DatabaseRecord getRecord(InetAddress ipAddress, Class cls)
throws IOException {
- byte[] rawAddress = ipAddress.getAddress();
+ var rawAddress = ipAddress.getAddress();
- long[] traverseResult = traverseTree(rawAddress, rawAddress.length * 8);
+ var traverseResult = traverseTree(rawAddress, rawAddress.length * 8);
long record = traverseResult[0];
int pl = (int) traverseResult[1];
- long nodeCount = this.metadata.getNodeCount();
- Buffer buffer = this.getBufferHolder().get();
+ long nodeCount = this.metadata.nodeCount();
+ var buffer = this.getBufferHolder().get();
T dataRecord = null;
if (record > nodeCount) {
// record is a data pointer
@@ -253,14 +258,14 @@ public Networks networks(
Class typeParameterClass) throws
InvalidNetworkException, ClosedDatabaseException, InvalidDatabaseException {
try {
- if (this.getMetadata().getIpVersion() == 6) {
- InetAddress ipv6 = InetAddress.getByAddress(new byte[16]);
- Network ipAllV6 = new Network(ipv6, 0); // Mask 128.
+ if (this.getMetadata().ipVersion() == 6) {
+ var ipv6 = InetAddress.getByAddress(new byte[16]);
+ var ipAllV6 = new Network(ipv6, 0); // Mask 128.
return this.networksWithin(ipAllV6, includeAliasedNetworks, typeParameterClass);
}
- InetAddress ipv4 = InetAddress.getByAddress(new byte[4]);
- Network ipAllV4 = new Network(ipv4, 0); // Mask 32.
+ var ipv4 = InetAddress.getByAddress(new byte[4]);
+ var ipAllV4 = new Network(ipv4, 0); // Mask 32.
return this.networksWithin(ipAllV4, includeAliasedNetworks, typeParameterClass);
} catch (UnknownHostException e) {
/* This is returned by getByAddress. This should never happen
@@ -270,7 +275,7 @@ public Networks networks(
}
BufferHolder getBufferHolder() throws ClosedDatabaseException {
- BufferHolder bufferHolder = this.bufferHolderReference.get();
+ var bufferHolder = this.bufferHolderReference.get();
if (bufferHolder == null) {
throw new ClosedDatabaseException();
}
@@ -280,7 +285,7 @@ BufferHolder getBufferHolder() throws ClosedDatabaseException {
private long startNode(int bitLength) {
// Check if we are looking up an IPv4 address in an IPv6 tree. If this
// is the case, we can skip over the first 96 nodes.
- if (this.metadata.getIpVersion() == 6 && bitLength == 32) {
+ if (this.metadata.ipVersion() == 6 && bitLength == 32) {
return this.ipV4Start;
}
// The first node of the tree is always node 0, at the beginning of the
@@ -290,12 +295,12 @@ private long startNode(int bitLength) {
private long findIpV4StartNode(Buffer buffer)
throws InvalidDatabaseException {
- if (this.metadata.getIpVersion() == 4) {
+ if (this.metadata.ipVersion() == 4) {
return 0;
}
long node = 0;
- for (int i = 0; i < 96 && node < this.metadata.getNodeCount(); i++) {
+ for (int i = 0; i < 96 && node < this.metadata.nodeCount(); i++) {
node = this.readNode(buffer, node, 0);
}
return node;
@@ -322,15 +327,15 @@ public Networks networksWithin(
boolean includeAliasedNetworks,
Class typeParameterClass)
throws InvalidNetworkException, ClosedDatabaseException, InvalidDatabaseException {
- InetAddress networkAddress = network.getNetworkAddress();
- if (this.metadata.getIpVersion() == 4 && networkAddress instanceof Inet6Address) {
+ var networkAddress = network.networkAddress();
+ if (this.metadata.ipVersion() == 4 && networkAddress instanceof Inet6Address) {
throw new InvalidNetworkException(networkAddress);
}
- byte[] ipBytes = networkAddress.getAddress();
- int prefixLength = network.getPrefixLength();
+ var ipBytes = networkAddress.getAddress();
+ int prefixLength = network.prefixLength();
- if (this.metadata.getIpVersion() == 6 && ipBytes.length == IPV4_LEN) {
+ if (this.metadata.ipVersion() == 6 && ipBytes.length == IPV4_LEN) {
if (includeAliasedNetworks) {
// Convert it to the IP address (in 16-byte from) of the IPv4 address.
ipBytes = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
@@ -343,7 +348,7 @@ public Networks networksWithin(
prefixLength += 96;
}
- long[] traverseResult = this.traverseTree(ipBytes, prefixLength);
+ var traverseResult = this.traverseTree(ipBytes, prefixLength);
long node = traverseResult[0];
int prefix = (int) traverseResult[1];
@@ -361,10 +366,10 @@ public Networks networksWithin(
*/
private long[] traverseTree(byte[] ip, int bitCount)
throws ClosedDatabaseException, InvalidDatabaseException {
- Buffer buffer = this.getBufferHolder().get();
+ var buffer = this.getBufferHolder().get();
int bitLength = ip.length * 8;
long record = this.startNode(bitLength);
- long nodeCount = this.metadata.getNodeCount();
+ long nodeCount = this.metadata.nodeCount();
int i = 0;
for (; i < bitCount && record < nodeCount; i++) {
@@ -383,33 +388,29 @@ long readNode(Buffer buffer, long nodeNumber, int index)
throws InvalidDatabaseException {
// index is the index of the record within the node, which
// can either be 0 or 1.
- long baseOffset = nodeNumber * this.metadata.getNodeByteSize();
+ var baseOffset = nodeNumber * this.nodeByteSize;
- switch (this.metadata.getRecordSize()) {
- case 24:
+ var recordSize = this.metadata.recordSize();
+ return switch (recordSize) {
+ case 24 -> {
// For a 24 bit record, each record is 3 bytes.
buffer.position(baseOffset + (long) index * 3);
- return Decoder.decodeLong(buffer, 0, 3);
- case 28:
+ yield Decoder.decodeLong(buffer, 0, 3);
+ }
+ case 28 -> {
int middle = buffer.get(baseOffset + 3);
-
- if (index == 0) {
- // We get the most significant from the first half
- // of the byte. It belongs to the first record.
- middle = (0xF0 & middle) >>> 4;
- } else {
- // We get the most significant byte of the second record.
- middle = 0x0F & middle;
- }
+ // We get the most significant bits from the appropriate half
+ // of the byte based on the index.
+ middle = index == 0 ? (0xF0 & middle) >>> 4 : 0x0F & middle;
buffer.position(baseOffset + (long) index * 4);
- return Decoder.decodeLong(buffer, middle, 3);
- case 32:
+ yield Decoder.decodeLong(buffer, middle, 3);
+ }
+ case 32 -> {
buffer.position(baseOffset + (long) index * 4);
- return Decoder.decodeLong(buffer, 0, 4);
- default:
- throw new InvalidDatabaseException("Unknown record size: "
- + this.metadata.getRecordSize());
- }
+ yield Decoder.decodeLong(buffer, 0, 4);
+ }
+ default -> throw new InvalidDatabaseException("Unknown record size: " + recordSize);
+ };
}
T resolveDataPointer(
@@ -417,8 +418,8 @@ T resolveDataPointer(
long pointer,
Class cls
) throws IOException {
- long resolved = (pointer - this.metadata.getNodeCount())
- + this.metadata.getSearchTreeSize();
+ long resolved = (pointer - this.metadata.nodeCount())
+ + this.searchTreeSize;
if (resolved >= buffer.capacity()) {
throw new InvalidDatabaseException(
@@ -428,10 +429,10 @@ T resolveDataPointer(
// We only want the data from the decoder, not the offset where it was
// found.
- Decoder decoder = new Decoder(
+ var decoder = new Decoder(
this.cache,
buffer,
- this.metadata.getSearchTreeSize() + DATA_SECTION_SEPARATOR_SIZE,
+ this.searchTreeSize + DATA_SECTION_SEPARATOR_SIZE,
this.constructors
);
return decoder.decode(resolved, cls);
@@ -447,7 +448,7 @@ T resolveDataPointer(
*/
private long findMetadataStart(Buffer buffer, String databaseName)
throws InvalidDatabaseException {
- long fileSize = buffer.capacity();
+ var fileSize = buffer.capacity();
FILE:
for (long i = 0; i < fileSize - METADATA_START_MARKER.length + 1; i++) {
diff --git a/src/main/java/com/maxmind/db/SingleBuffer.java b/src/main/java/com/maxmind/db/SingleBuffer.java
index 89c95980..eca1629a 100644
--- a/src/main/java/com/maxmind/db/SingleBuffer.java
+++ b/src/main/java/com/maxmind/db/SingleBuffer.java
@@ -12,34 +12,22 @@
*
*
This implementation is limited to capacities up to
* {@link Integer#MAX_VALUE}, as {@link ByteBuffer} cannot exceed that size.
+ *
+ *
The underlying {@link ByteBuffer} is read-only to prevent accidental
+ * modification of shared data.
*/
-class SingleBuffer implements Buffer {
+final class SingleBuffer implements Buffer {
private final ByteBuffer buffer;
- /**
- * Creates a new {@code SingleBuffer} with the given capacity.
- *
- * @param capacity the capacity in bytes (must be <= Integer.MAX_VALUE)
- * @throws IllegalArgumentException if the capacity exceeds
- * {@link Integer#MAX_VALUE}
- */
- public SingleBuffer(long capacity) {
- if (capacity > Integer.MAX_VALUE) {
- throw new IllegalArgumentException(
- "SingleBuffer cannot exceed Integer.MAX_VALUE capacity"
- );
- }
- this.buffer = ByteBuffer.allocate((int) capacity);
- }
-
/**
* Creates a new {@code SingleBuffer} wrapping the given {@link ByteBuffer}.
+ * The buffer is made read-only.
*
* @param buffer the underlying buffer
*/
- private SingleBuffer(ByteBuffer buffer) {
- this.buffer = buffer;
+ SingleBuffer(ByteBuffer buffer) {
+ this.buffer = buffer.asReadOnlyBuffer();
}
/** {@inheritDoc} */
@@ -111,12 +99,6 @@ public SingleBuffer duplicate() {
return new SingleBuffer(this.buffer.duplicate());
}
- /** {@inheritDoc} */
- @Override
- public long readFrom(FileChannel channel) throws IOException {
- return channel.read(buffer);
- }
-
/** {@inheritDoc} */
@Override
public String decode(CharsetDecoder decoder)
@@ -144,7 +126,7 @@ public static SingleBuffer wrap(byte[] array) {
*/
public static SingleBuffer mapFromChannel(FileChannel channel)
throws IOException {
- ByteBuffer buffer = channel.map(MapMode.READ_ONLY, 0, channel.size());
- return new SingleBuffer(buffer.asReadOnlyBuffer());
+ var buffer = channel.map(MapMode.READ_ONLY, 0, channel.size());
+ return new SingleBuffer(buffer);
}
}
diff --git a/src/test/java/com/maxmind/db/DecoderTest.java b/src/test/java/com/maxmind/db/DecoderTest.java
index 5e02c347..07d00d01 100644
--- a/src/test/java/com/maxmind/db/DecoderTest.java
+++ b/src/test/java/com/maxmind/db/DecoderTest.java
@@ -20,7 +20,7 @@ public class DecoderTest {
private static Map int32() {
int max = (2 << 30) - 1;
- HashMap int32 = new HashMap<>();
+ var int32 = new HashMap();
int32.put(0, new byte[] {0x0, 0x1});
int32.put(-1, new byte[] {0x4, 0x1, (byte) 0xff, (byte) 0xff,
@@ -49,7 +49,7 @@ private static Map int32() {
private static Map uint32() {
long max = (((long) 1) << 32) - 1;
- HashMap uint32s = new HashMap<>();
+ var uint32s = new HashMap();
uint32s.put((long) 0, new byte[] {(byte) 0xc0});
uint32s.put((long) ((1 << 8) - 1), new byte[] {(byte) 0xc1,
@@ -69,7 +69,7 @@ private static Map uint32() {
private static Map uint16() {
int max = (1 << 16) - 1;
- Map uint16s = new HashMap<>();
+ var uint16s = new HashMap();
uint16s.put(0, new byte[] {(byte) 0xa0});
uint16s.put((1 << 8) - 1, new byte[] {(byte) 0xa1, (byte) 0xff});
@@ -80,7 +80,7 @@ private static Map uint16() {
}
private static Map largeUint(int bits) {
- Map uints = new HashMap<>();
+ var uints = new HashMap();
byte ctrlByte = (byte) (bits == 64 ? 0x2 : 0x3);
@@ -92,10 +92,10 @@ private static Map largeUint(int bits) {
for (int power = 1; power <= bits / 8; power++) {
- BigInteger key = BigInteger.valueOf(2).pow(8 * power)
+ var key = BigInteger.valueOf(2).pow(8 * power)
.subtract(BigInteger.valueOf(1));
- byte[] value = new byte[2 + power];
+ var value = new byte[2 + power];
value[0] = (byte) power;
value[1] = ctrlByte;
for (int i = 2; i < value.length; i++) {
@@ -108,7 +108,7 @@ private static Map largeUint(int bits) {
}
private static Map pointers() {
- Map pointers = new HashMap<>();
+ var pointers = new HashMap();
pointers.put((long) 0, new byte[] {0x20, 0x0});
pointers.put((long) 5, new byte[] {0x20, 0x5});
@@ -131,7 +131,7 @@ private static Map pointers() {
}
private static Map strings() {
- Map strings = new HashMap<>();
+ var strings = new HashMap();
DecoderTest.addTestString(strings, (byte) 0x40, "");
DecoderTest.addTestString(strings, (byte) 0x41, "1");
@@ -167,12 +167,12 @@ private static Map strings() {
}
private static Map bytes() {
- Map bytes = new HashMap<>();
+ var bytes = new HashMap();
- Map strings = DecoderTest.strings();
+ var strings = DecoderTest.strings();
for (String s : strings.keySet()) {
- byte[] ba = strings.get(s);
+ var ba = strings.get(s);
ba[0] ^= 0xc0;
bytes.put(s.getBytes(StandardCharsets.UTF_8), ba);
@@ -189,8 +189,8 @@ private static void addTestString(Map tests, byte ctrl,
private static void addTestString(Map tests, byte[] ctrl,
String str) {
- byte[] sb = str.getBytes(StandardCharsets.UTF_8);
- byte[] bytes = new byte[ctrl.length + sb.length];
+ var sb = str.getBytes(StandardCharsets.UTF_8);
+ var bytes = new byte[ctrl.length + sb.length];
System.arraycopy(ctrl, 0, bytes, 0, ctrl.length);
System.arraycopy(sb, 0, bytes, ctrl.length, sb.length);
@@ -198,7 +198,7 @@ private static void addTestString(Map tests, byte[] ctrl,
}
private static Map doubles() {
- Map doubles = new HashMap<>();
+ var doubles = new HashMap();
doubles.put(0.0, new byte[] {0x68, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0});
doubles.put(0.5, new byte[] {0x68, 0x3F, (byte) 0xE0, 0x0, 0x0, 0x0,
@@ -220,7 +220,7 @@ private static Map doubles() {
}
private static Map floats() {
- Map floats = new HashMap<>();
+ var floats = new HashMap();
floats.put((float) 0.0, new byte[] {0x4, 0x8, 0x0, 0x0, 0x0, 0x0});
floats.put((float) 1.0, new byte[] {0x4, 0x8, 0x3F, (byte) 0x80, 0x0,
0x0});
@@ -243,7 +243,7 @@ private static Map floats() {
}
private static Map booleans() {
- Map booleans = new HashMap<>();
+ var booleans = new HashMap();
booleans.put(Boolean.FALSE, new byte[] {0x0, 0x7});
booleans.put(Boolean.TRUE, new byte[] {0x1, 0x7});
@@ -251,17 +251,17 @@ private static Map booleans() {
}
private static Map