diff --git a/.gitmodules b/.gitmodules index 6660f3f7..2fb5f51a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "src/test/resources/maxmind-db"] - path = src/test/resources/maxmind-db + path = reader/src/test/resources/maxmind-db url = https://github.com/maxmind/MaxMind-DB diff --git a/README.md b/README.md index 8e8c4532..8008aa8a 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,18 @@ Maven, you must Failure to do so will result in `InvalidDatabaseException` exceptions being thrown when querying the database. +## Benchmarking ## + +Set an environment variable `GEO_LITE` with the path to `GeoLite2-City.mmdb`. + +```shell +mvn -Dcheckstyle.skip -DskipTests clean package +GEO_LITE=/.../GeoLite2-City.mmdb java -jar benchmarks/target/microbenchmarks.jar +``` + +For more, see [https://github.com/openjdk/jmh](https://github.com/openjdk/jmh) or +`java -jar benchmarks/target/microbenchmarks.jar -h`. + ## Format ## The MaxMind DB format is an open format for quickly mapping IP addresses to diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml new file mode 100644 index 00000000..5941ee97 --- /dev/null +++ b/benchmarks/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + com.maxmind.db + benchmarks + 3.1.2-SNAPSHOT + jar + + 1.37 + + + + com.maxmind.db + maxmind-db + 3.1.2-SNAPSHOT + + + org.openjdk.jmh + jmh-core + ${jmhVersion} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 11 + 11 + 11 + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmhVersion} + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + microbenchmarks + + + org.openjdk.jmh.Main + + + + + *:* + + META-INF/services/javax.annotation.processing.Processor + + + + + + + + + + diff --git a/benchmarks/src/main/java/com/maxmind/db/BenchmarkGet.java b/benchmarks/src/main/java/com/maxmind/db/BenchmarkGet.java new file mode 100644 index 00000000..23db99d8 --- /dev/null +++ b/benchmarks/src/main/java/com/maxmind/db/BenchmarkGet.java @@ -0,0 +1,71 @@ +package com.maxmind.db; + +import com.maxmind.db.Reader.FileMode; +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OperationsPerInvocation; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@State(Scope.Benchmark) +@Warmup(iterations = 3, time = 5) +@Measurement(iterations = 10, time = 5) +@Fork(1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class BenchmarkGet { + private static final int COUNT = 1_000_000; + + private static InetAddress[] getInetAddresses(final int seed) throws UnknownHostException { + final InetAddress[] addresses = new InetAddress[COUNT]; + final Random random = new Random(seed); + final byte[] address = new byte[4]; + for (int addressIx = 0; addressIx < COUNT; addressIx++) { + random.nextBytes(address); + addresses[addressIx] = InetAddress.getByAddress(address); + } + return addresses; + } + + InetAddress[] addresses; + Reader reader; + Reader cachedReader; + + @Setup + public void setup() throws IOException { + addresses = getInetAddresses(0); + final File database = new File(System.getenv("GEO_LITE")); + reader = new Reader(database, FileMode.MEMORY_MAPPED, NoCache.getInstance()); + cachedReader = new Reader(database, FileMode.MEMORY_MAPPED, new CHMCache()); + } + + @Benchmark + @OperationsPerInvocation(COUNT) + public void withoutCaching(Blackhole bh) throws IOException { + for (InetAddress address: addresses) { + bh.consume(reader.get(address, Map.class)); + } + } + + @Benchmark + @OperationsPerInvocation(COUNT) + public void withCaching(Blackhole bh) throws IOException { + for (InetAddress address: addresses) { + bh.consume(cachedReader.get(address, Map.class)); + } + } +} diff --git a/dev-bin/release.sh b/dev-bin/release.sh index 497c346e..38ccd69f 100755 --- a/dev-bin/release.sh +++ b/dev-bin/release.sh @@ -97,11 +97,11 @@ fi # could be combined with the primary build -mvn release:clean -mvn release:prepare -DreleaseVersion="$version" -Dtag="$tag" -mvn release:perform +mvn -pl reader release:clean +mvn -pl reader release:prepare -DreleaseVersion="$version" -Dtag="$tag" +mvn -pl reader release:perform rm -fr ".gh-pages/doc/$tag" -cp -r target/checkout/target/reports/apidocs ".gh-pages/doc/$tag" +cp -r reader/target/checkout/target/reports/apidocs ".gh-pages/doc/$tag" rm .gh-pages/doc/latest ln -fs "$tag" .gh-pages/doc/latest diff --git a/pom.xml b/pom.xml index f71271c2..8b7d95bc 100644 --- a/pom.xml +++ b/pom.xml @@ -1,237 +1,18 @@ - 4.0.0 - com.maxmind.db - maxmind-db - 3.1.2-SNAPSHOT - jar - MaxMind DB Reader - Reader for MaxMind DB - http://dev.maxmind.com/ - - - Apache License 2.0 - http://www.apache.org/licenses/LICENSE-2.0.html - repo - - - - MaxMind, Inc. - http://www.maxmind.com/ - - - https://github.com/maxmind/MaxMind-DB-Reader-java - scm:git:git://github.com:maxmind/MaxMind-DB-Reader-java.git - scm:git:git@github.com:maxmind/MaxMind-DB-Reader-java.git - HEAD + 4.0.0 + pom + com.maxmind.db + maxmind-parent + 3.1.2-SNAPSHOT + + https://github.com/maxmind/MaxMind-DB-Reader-java + scm:git:git://github.com:maxmind/MaxMind-DB-Reader-java.git + scm:git:git@github.com:maxmind/MaxMind-DB-Reader-java.git + HEAD - - https://github.com/maxmind/MaxMind-DB-Reader-java/issues - GitHub - - - - oschwald - Gregory J. Oschwald - goschwald@maxmind.com - - - - - org.junit.jupiter - junit-jupiter - 5.11.4 - test - - - org.hamcrest - java-hamcrest - 2.0.0.0 - test - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - 3.5.0 - - - enforce-maven - - enforce - - - - - 3.6.3 - - - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.6.0 - - true - checkstyle.xml - checkstyle-suppressions.xml - warning - - - - com.puppycrawl.tools - checkstyle - 10.21.2 - - - - - test - test - - check - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 3.2.7 - - - sign-artifacts - verify - - sign - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.3.1 - - - attach-sources - package - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.11.2 - - -missing - - - - - jar - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.13.0 - - 11 - 11 - 11 - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.5.2 - - - org.codehaus.mojo - versions-maven-plugin - 2.18.0 - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.7.0 - true - - sonatype-nexus-staging - https://oss.sonatype.org/ - true - - - - default-deploy - deploy - - deploy - - - - - - - - UTF-8 - - - - not-windows - - - !Windows - - - - - - org.codehaus.mojo - exec-maven-plugin - 3.5.0 - - - initialize - invoke build - - exec - - - - - git - submodule update --init --recursive - - - - - - - - - sonatype-nexus-staging - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - + + reader + benchmarks + diff --git a/checkstyle-suppressions.xml b/reader/checkstyle-suppressions.xml similarity index 100% rename from checkstyle-suppressions.xml rename to reader/checkstyle-suppressions.xml diff --git a/reader/pom.xml b/reader/pom.xml new file mode 100644 index 00000000..c05bcb75 --- /dev/null +++ b/reader/pom.xml @@ -0,0 +1,237 @@ + + + 4.0.0 + jar + com.maxmind.db + maxmind-db + 3.1.2-SNAPSHOT + MaxMind DB Reader + Reader for MaxMind DB + http://dev.maxmind.com/ + + + Apache License 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + MaxMind, Inc. + http://www.maxmind.com/ + + + https://github.com/maxmind/MaxMind-DB-Reader-java + scm:git:git://github.com:maxmind/MaxMind-DB-Reader-java.git + scm:git:git@github.com:maxmind/MaxMind-DB-Reader-java.git + HEAD + + + https://github.com/maxmind/MaxMind-DB-Reader-java/issues + GitHub + + + + oschwald + Gregory J. Oschwald + goschwald@maxmind.com + + + + + org.junit.jupiter + junit-jupiter + 5.11.4 + test + + + org.hamcrest + java-hamcrest + 2.0.0.0 + test + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.5.0 + + + enforce-maven + + enforce + + + + + 3.6.3 + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + true + ../checkstyle.xml + checkstyle-suppressions.xml + warning + + + + com.puppycrawl.tools + checkstyle + 10.21.2 + + + + + test + test + + check + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.7 + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + attach-sources + package + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.11.2 + + -missing + + + + + jar + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 11 + 11 + 11 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + + org.codehaus.mojo + versions-maven-plugin + 2.18.0 + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.7.0 + true + + sonatype-nexus-staging + https://oss.sonatype.org/ + true + + + + default-deploy + deploy + + deploy + + + + + + + + UTF-8 + + + + not-windows + + + !Windows + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.5.0 + + + initialize + invoke build + + exec + + + + + git + submodule update --init --recursive + + + + + + + + + sonatype-nexus-staging + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + diff --git a/reader/src/main/java/com/maxmind/db/ByteBufferByteReader.java b/reader/src/main/java/com/maxmind/db/ByteBufferByteReader.java new file mode 100644 index 00000000..4d451bdc --- /dev/null +++ b/reader/src/main/java/com/maxmind/db/ByteBufferByteReader.java @@ -0,0 +1,60 @@ +package com.maxmind.db; + +import java.nio.ByteBuffer; + +/** + * A {@link ByteReader} implemented by a {@link ByteBuffer}. + */ +public class ByteBufferByteReader extends IntLimitedByteReader { + protected final ByteBuffer bytes; + + /** + * Create a {@link ByteReader} from a {@link ByteBuffer}. + */ + public ByteBufferByteReader(final ByteBuffer bytes) { + this.bytes = bytes; + } + + @Override + public byte[] getBytes(final int length) { + final byte[] dst = new byte[length]; + bytes.get(dst); + return dst; + } + + @Override + public byte get() { + return bytes.get(); + } + + @Override + public byte get(long index) { + return super.get(index); + } + + @Override + protected byte get(final int index) { + return bytes.get(index); + } + + @Override + public long capacity() { + return bytes.capacity(); + } + + @Override + protected ByteReader position(final int newPosition) { + bytes.position(newPosition); + return this; + } + + @Override + public long position() { + return bytes.position(); + } + + @Override + public ByteReader duplicate() { + return new ByteBufferByteReader(bytes.duplicate()); + } +} diff --git a/reader/src/main/java/com/maxmind/db/ByteReader.java b/reader/src/main/java/com/maxmind/db/ByteReader.java new file mode 100644 index 00000000..74984a0a --- /dev/null +++ b/reader/src/main/java/com/maxmind/db/ByteReader.java @@ -0,0 +1,57 @@ +package com.maxmind.db; + +import java.nio.ByteBuffer; + +/** + *

This provides functionality from {@link ByteBuffer} that is required for this library. + * The default implementation is {@link ByteBufferByteReader}, which just redirects method calls + * to a {@link ByteBuffer}.Another implementation might be preferable. In that case, you can pass + * it to {@link Reader#Reader(ByteReader, String, NodeCache)}.

+ * + *

Methods in implementations should throw the same exceptions as the methods with the same name + * in {@link ByteBuffer}.

+ * + *

If your implementation can only handle positions of up to Integer.MAX_VALUE, + * implement {@link IntLimitedByteReader}.

+ */ +public interface ByteReader { + + /** + * See {@link ByteBuffer#position()} + */ + long position(); + + /** + * See {@link ByteBuffer#position(int)} + */ + ByteReader position(final long newPosition); + + /** + * See {@link ByteBuffer#capacity()}. + */ + long capacity(); + + /** + * Get the byte at {@link ByteReader#position()}. See {@link ByteBuffer#get()} + */ + byte get(); + + /** + * Get the byte at the given index. See {@link ByteBuffer#get(int)} + */ + byte get(final long index); + + /** + * Read the requested number of bytes starting at {@link ByteReader#position()}. + * It moves {@link ByteReader#position()} forward by the same amount. + */ + byte[] getBytes(final int length); + + /** + * This is intended for creating a copy of this collection that is safe + * for use by a new thread.

+ * + * See {@link java.nio.ByteBuffer#duplicate()} + */ + ByteReader duplicate(); +} diff --git a/src/main/java/com/maxmind/db/BufferHolder.java b/reader/src/main/java/com/maxmind/db/ByteReaderHolder.java similarity index 74% rename from src/main/java/com/maxmind/db/BufferHolder.java rename to reader/src/main/java/com/maxmind/db/ByteReaderHolder.java index dbbe8aa5..1b461ba7 100644 --- a/src/main/java/com/maxmind/db/BufferHolder.java +++ b/reader/src/main/java/com/maxmind/db/ByteReaderHolder.java @@ -10,11 +10,15 @@ import java.nio.channels.FileChannel; import java.nio.channels.FileChannel.MapMode; -final class BufferHolder { +final class ByteReaderHolder { // DO NOT PASS OUTSIDE THIS CLASS. Doing so will remove thread safety. - private final ByteBuffer buffer; + private final ByteReader reader; - BufferHolder(File database, FileMode mode) throws IOException { + ByteReaderHolder(ByteReader reader) { + this.reader = reader; + } + + ByteReaderHolder(File database, FileMode mode) throws IOException { try ( final RandomAccessFile file = new RandomAccessFile(database, "r"); final FileChannel channel = file.getChannel() @@ -26,21 +30,23 @@ final class BufferHolder { + database.getName() + " into memory. Unexpected end of stream."); } - this.buffer = buf.asReadOnlyBuffer(); + this.reader = new ByteBufferByteReader(buf.asReadOnlyBuffer()); } else { - this.buffer = channel.map(MapMode.READ_ONLY, 0, channel.size()).asReadOnlyBuffer(); + final ByteBuffer mappedBuffer = + channel.map(MapMode.READ_ONLY, 0, channel.size()).asReadOnlyBuffer(); + this.reader = new ByteBufferByteReader(mappedBuffer); } } } /** - * Construct a ThreadBuffer from the provided URL. + * Construct a {@link ByteReaderHolder} from the provided {@link InputStream}. * * @param stream the source of my bytes. * @throws IOException if unable to read from your source. - * @throws NullPointerException if you provide a NULL InputStream + * @throws NullPointerException if you provide a {@code null} InputStream */ - BufferHolder(InputStream stream) throws IOException { + ByteReaderHolder(InputStream stream) throws IOException { if (null == stream) { throw new NullPointerException("Unable to use a NULL InputStream"); } @@ -50,14 +56,15 @@ final class BufferHolder { while (-1 != (br = stream.read(bytes))) { baos.write(bytes, 0, br); } - this.buffer = ByteBuffer.wrap(baos.toByteArray()).asReadOnlyBuffer(); + final ByteBuffer readOnlyBuffer = ByteBuffer.wrap(baos.toByteArray()).asReadOnlyBuffer(); + this.reader = new ByteBufferByteReader(readOnlyBuffer); } /* * Returns a duplicate of the underlying ByteBuffer. The returned ByteBuffer * should not be shared between threads. */ - ByteBuffer get() { + public ByteReader get() { // The Java API docs for buffer state: // // Buffers are not safe for use by multiple concurrent threads. If a buffer is to be @@ -75,6 +82,6 @@ ByteBuffer get() { // operations on the original buffer object, the risk of not synchronizing this call seems // relatively low and worth taking for the performance benefit when lookups are being done // from many threads. - return this.buffer.duplicate(); + return reader.duplicate(); } } diff --git a/src/main/java/com/maxmind/db/CHMCache.java b/reader/src/main/java/com/maxmind/db/CHMCache.java similarity index 100% rename from src/main/java/com/maxmind/db/CHMCache.java rename to reader/src/main/java/com/maxmind/db/CHMCache.java diff --git a/src/main/java/com/maxmind/db/CacheKey.java b/reader/src/main/java/com/maxmind/db/CacheKey.java similarity index 89% rename from src/main/java/com/maxmind/db/CacheKey.java rename to reader/src/main/java/com/maxmind/db/CacheKey.java index d62c0843..81f79514 100644 --- a/src/main/java/com/maxmind/db/CacheKey.java +++ b/reader/src/main/java/com/maxmind/db/CacheKey.java @@ -8,17 +8,17 @@ * @param the type of value */ public final class CacheKey { - private final int offset; + private final long offset; private final Class cls; private final java.lang.reflect.Type type; - CacheKey(int offset, Class cls, java.lang.reflect.Type type) { + CacheKey(long offset, Class cls, java.lang.reflect.Type type) { this.offset = offset; this.cls = cls; this.type = type; } - int getOffset() { + long getOffset() { return this.offset; } @@ -58,7 +58,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - int result = offset; + int result = Long.hashCode(offset); result = 31 * result + (cls == null ? 0 : cls.hashCode()); result = 31 * result + (type == null ? 0 : type.hashCode()); return result; diff --git a/src/main/java/com/maxmind/db/CachedConstructor.java b/reader/src/main/java/com/maxmind/db/CachedConstructor.java similarity index 100% rename from src/main/java/com/maxmind/db/CachedConstructor.java rename to reader/src/main/java/com/maxmind/db/CachedConstructor.java diff --git a/src/main/java/com/maxmind/db/ClosedDatabaseException.java b/reader/src/main/java/com/maxmind/db/ClosedDatabaseException.java similarity index 100% rename from src/main/java/com/maxmind/db/ClosedDatabaseException.java rename to reader/src/main/java/com/maxmind/db/ClosedDatabaseException.java diff --git a/src/main/java/com/maxmind/db/ConstructorNotFoundException.java b/reader/src/main/java/com/maxmind/db/ConstructorNotFoundException.java similarity index 100% rename from src/main/java/com/maxmind/db/ConstructorNotFoundException.java rename to reader/src/main/java/com/maxmind/db/ConstructorNotFoundException.java diff --git a/src/main/java/com/maxmind/db/CtrlData.java b/reader/src/main/java/com/maxmind/db/CtrlData.java similarity index 70% rename from src/main/java/com/maxmind/db/CtrlData.java rename to reader/src/main/java/com/maxmind/db/CtrlData.java index c9bf03f1..af7062a2 100644 --- a/src/main/java/com/maxmind/db/CtrlData.java +++ b/reader/src/main/java/com/maxmind/db/CtrlData.java @@ -3,10 +3,10 @@ final class CtrlData { private final Type type; private final int ctrlByte; - private final int offset; - private final int size; + private final long offset; + private final long size; - CtrlData(Type type, int ctrlByte, int offset, int size) { + CtrlData(Type type, int ctrlByte, long offset, long size) { this.type = type; this.ctrlByte = ctrlByte; this.offset = offset; @@ -21,11 +21,11 @@ public int getCtrlByte() { return this.ctrlByte; } - public int getOffset() { + public long getOffset() { return this.offset; } - public int getSize() { + public long getSize() { return this.size; } } diff --git a/src/main/java/com/maxmind/db/DatabaseRecord.java b/reader/src/main/java/com/maxmind/db/DatabaseRecord.java similarity index 100% rename from src/main/java/com/maxmind/db/DatabaseRecord.java rename to reader/src/main/java/com/maxmind/db/DatabaseRecord.java diff --git a/src/main/java/com/maxmind/db/DecodedValue.java b/reader/src/main/java/com/maxmind/db/DecodedValue.java similarity index 100% rename from src/main/java/com/maxmind/db/DecodedValue.java rename to reader/src/main/java/com/maxmind/db/DecodedValue.java diff --git a/src/main/java/com/maxmind/db/Decoder.java b/reader/src/main/java/com/maxmind/db/Decoder.java similarity index 92% rename from src/main/java/com/maxmind/db/Decoder.java rename to reader/src/main/java/com/maxmind/db/Decoder.java index ab1bc5e2..83a3a35c 100644 --- a/src/main/java/com/maxmind/db/Decoder.java +++ b/reader/src/main/java/com/maxmind/db/Decoder.java @@ -6,10 +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; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; @@ -24,8 +20,6 @@ */ final class Decoder { - private static final Charset UTF_8 = StandardCharsets.UTF_8; - private static final int[] POINTER_VALUE_OFFSETS = {0, 0, 1 << 11, (1 << 19) + (1 << 11), 0}; // XXX - This is only for unit testings. We should possibly make a @@ -36,13 +30,11 @@ final class Decoder { private final long pointerBase; - private final CharsetDecoder utfDecoder = UTF_8.newDecoder(); - - private final ByteBuffer buffer; + private final ByteReader buffer; private final ConcurrentHashMap constructors; - Decoder(NodeCache cache, ByteBuffer buffer, long pointerBase) { + Decoder(NodeCache cache, ByteReader buffer, long pointerBase) { this( cache, buffer, @@ -53,7 +45,7 @@ final class Decoder { Decoder( NodeCache cache, - ByteBuffer buffer, + ByteReader buffer, long pointerBase, ConcurrentHashMap constructors ) { @@ -65,7 +57,7 @@ final class Decoder { private final NodeCache.Loader cacheLoader = this::decode; - public T decode(int offset, Class cls) throws IOException { + public T decode(long offset, Class cls) throws IOException { if (offset >= this.buffer.capacity()) { throw new InvalidDatabaseException( "The MaxMind DB file's data section contains bad data: " @@ -77,7 +69,7 @@ public T decode(int offset, Class cls) throws IOException { } private DecodedValue decode(CacheKey key) throws IOException { - int offset = key.getOffset(); + long offset = key.getOffset(); if (offset >= this.buffer.capacity()) { throw new InvalidDatabaseException( "The MaxMind DB file's data section contains bad data: " @@ -110,7 +102,7 @@ private DecodedValue decode(Class cls, java.lang.reflect.Type genericType } int targetOffset = (int) pointer; - int position = buffer.position(); + long position = buffer.position(); CacheKey key = new CacheKey(targetOffset, cls, genericType); DecodedValue o = cache.get(key, cacheLoader); @@ -195,12 +187,8 @@ private Object decodeByType( } } - private String decodeString(int size) throws CharacterCodingException { - int oldLimit = buffer.limit(); - buffer.limit(buffer.position() + size); - String s = utfDecoder.decode(buffer).toString(); - buffer.limit(oldLimit); - return s; + private String decodeString(int size) { + return new String(buffer.getBytes(size), StandardCharsets.UTF_8); } private int decodeUint16(int size) { @@ -231,7 +219,7 @@ private int decodeInteger(int base, int size) { return Decoder.decodeInteger(this.buffer, base, size); } - static int decodeInteger(ByteBuffer buffer, int base, int size) { + static int decodeInteger(ByteReader buffer, int base, int size) { int integer = base; for (int i = 0; i < size; i++) { integer = (integer << 8) | (buffer.get() & 0xFF); @@ -250,7 +238,7 @@ private double decodeDouble(int size) throws InvalidDatabaseException { "The MaxMind DB file's data section contains bad data: " + "invalid size of double."); } - return this.buffer.getDouble(); + return Double.longBitsToDouble(decodeLong(size)); } private float decodeFloat(int size) throws InvalidDatabaseException { @@ -259,7 +247,7 @@ private float decodeFloat(int size) throws InvalidDatabaseException { "The MaxMind DB file's data section contains bad data: " + "invalid size of float."); } - return this.buffer.getFloat(); + return Float.intBitsToFloat(decodeInteger(size)); } private static boolean decodeBoolean(int size) @@ -376,7 +364,7 @@ private Map decodeMapIntoMap( map.put(key, valueClass.cast(value)); } catch (ClassCastException e) { throw new DeserializationException( - "Error creating map entry for '" + key + "': " + e.getMessage(), e); + "Error creating map entry for '" + key + "': " + e.getMessage(), e); } } @@ -426,7 +414,7 @@ private Object decodeMapIntoObject(int size, Class cls) Integer parameterIndex = parameterIndexes.get(key); if (parameterIndex == null) { - int offset = this.nextValueOffset(this.buffer.position(), 1); + long offset = this.nextValueOffset(this.buffer.position(), 1); this.buffer.position(offset); continue; } @@ -492,7 +480,7 @@ private static String getParameterName( + " is not annotated with MaxMindDbParameter."); } - private int nextValueOffset(int offset, int numberToSkip) + private long nextValueOffset(long offset, long numberToSkip) throws InvalidDatabaseException { if (numberToSkip == 0) { return offset; @@ -500,7 +488,7 @@ private int nextValueOffset(int offset, int numberToSkip) CtrlData ctrlData = this.getCtrlData(offset); int ctrlByte = ctrlData.getCtrlByte(); - int size = ctrlData.getSize(); + long size = ctrlData.getSize(); offset = ctrlData.getOffset(); Type type = ctrlData.getType(); @@ -525,7 +513,7 @@ private int nextValueOffset(int offset, int numberToSkip) return nextValueOffset(offset, numberToSkip - 1); } - private CtrlData getCtrlData(int offset) + private CtrlData getCtrlData(long offset) throws InvalidDatabaseException { if (offset >= this.buffer.capacity()) { throw new InvalidDatabaseException( @@ -575,12 +563,6 @@ private CtrlData getCtrlData(int offset) } private byte[] getByteArray(int length) { - return Decoder.getByteArray(this.buffer, length); - } - - private static byte[] getByteArray(ByteBuffer buffer, int length) { - byte[] bytes = new byte[length]; - buffer.get(bytes); - return bytes; + return buffer.getBytes(length); } } diff --git a/src/main/java/com/maxmind/db/DeserializationException.java b/reader/src/main/java/com/maxmind/db/DeserializationException.java similarity index 100% rename from src/main/java/com/maxmind/db/DeserializationException.java rename to reader/src/main/java/com/maxmind/db/DeserializationException.java diff --git a/reader/src/main/java/com/maxmind/db/IntLimitedByteReader.java b/reader/src/main/java/com/maxmind/db/IntLimitedByteReader.java new file mode 100644 index 00000000..c910cda2 --- /dev/null +++ b/reader/src/main/java/com/maxmind/db/IntLimitedByteReader.java @@ -0,0 +1,34 @@ +package com.maxmind.db; + +/** + * This provides safe functionality for {@link ByteReader}s that only can + * handle up to Integer.MAX_VALUE positions. + */ +public abstract class IntLimitedByteReader implements ByteReader { + protected abstract ByteReader position(final int index); + + @Override + public ByteReader position(long newPosition) { + final boolean isLarge = newPosition > Integer.MAX_VALUE; + if (isLarge || newPosition < Integer.MIN_VALUE) { + throw PositionedByteReader.createPositionException(capacity(), newPosition, isLarge); + } + return position((int) newPosition); + } + + protected abstract byte get(final int index); + + @Override + public byte get(final long index) { + /* + bytes.get(int) will do a range check for [0,limit), + but before that, we need to make sure that casting won't + do anything unexpected, like making a negative long + cast to a positive int. + */ + if (index > Integer.MAX_VALUE || index < Integer.MIN_VALUE) { + throw new IndexOutOfBoundsException(); + } + return get((int) index); + } +} diff --git a/src/main/java/com/maxmind/db/InvalidDatabaseException.java b/reader/src/main/java/com/maxmind/db/InvalidDatabaseException.java similarity index 100% rename from src/main/java/com/maxmind/db/InvalidDatabaseException.java rename to reader/src/main/java/com/maxmind/db/InvalidDatabaseException.java diff --git a/src/main/java/com/maxmind/db/InvalidNetworkException.java b/reader/src/main/java/com/maxmind/db/InvalidNetworkException.java similarity index 100% rename from src/main/java/com/maxmind/db/InvalidNetworkException.java rename to reader/src/main/java/com/maxmind/db/InvalidNetworkException.java diff --git a/src/main/java/com/maxmind/db/MaxMindDbConstructor.java b/reader/src/main/java/com/maxmind/db/MaxMindDbConstructor.java similarity index 100% rename from src/main/java/com/maxmind/db/MaxMindDbConstructor.java rename to reader/src/main/java/com/maxmind/db/MaxMindDbConstructor.java diff --git a/src/main/java/com/maxmind/db/MaxMindDbParameter.java b/reader/src/main/java/com/maxmind/db/MaxMindDbParameter.java similarity index 100% rename from src/main/java/com/maxmind/db/MaxMindDbParameter.java rename to reader/src/main/java/com/maxmind/db/MaxMindDbParameter.java diff --git a/src/main/java/com/maxmind/db/Metadata.java b/reader/src/main/java/com/maxmind/db/Metadata.java similarity index 100% rename from src/main/java/com/maxmind/db/Metadata.java rename to reader/src/main/java/com/maxmind/db/Metadata.java diff --git a/src/main/java/com/maxmind/db/Network.java b/reader/src/main/java/com/maxmind/db/Network.java similarity index 100% rename from src/main/java/com/maxmind/db/Network.java rename to reader/src/main/java/com/maxmind/db/Network.java diff --git a/src/main/java/com/maxmind/db/Networks.java b/reader/src/main/java/com/maxmind/db/Networks.java similarity index 98% rename from src/main/java/com/maxmind/db/Networks.java rename to reader/src/main/java/com/maxmind/db/Networks.java index b06dfffb..0439f2af 100644 --- a/src/main/java/com/maxmind/db/Networks.java +++ b/reader/src/main/java/com/maxmind/db/Networks.java @@ -19,9 +19,9 @@ public final class Networks implements Iterator> { private final Stack nodes; private NetworkNode lastNode; private final boolean includeAliasedNetworks; - private final ByteBuffer buffer; /* Stores the buffer for Next() calls */ + private final ByteReader buffer; /* Stores the buffer for Next() calls */ private final Class typeParameterClass; - + /** * Constructs a Networks instance. * diff --git a/src/main/java/com/maxmind/db/NetworksIterationException.java b/reader/src/main/java/com/maxmind/db/NetworksIterationException.java similarity index 100% rename from src/main/java/com/maxmind/db/NetworksIterationException.java rename to reader/src/main/java/com/maxmind/db/NetworksIterationException.java diff --git a/src/main/java/com/maxmind/db/NoCache.java b/reader/src/main/java/com/maxmind/db/NoCache.java similarity index 100% rename from src/main/java/com/maxmind/db/NoCache.java rename to reader/src/main/java/com/maxmind/db/NoCache.java diff --git a/src/main/java/com/maxmind/db/NodeCache.java b/reader/src/main/java/com/maxmind/db/NodeCache.java similarity index 100% rename from src/main/java/com/maxmind/db/NodeCache.java rename to reader/src/main/java/com/maxmind/db/NodeCache.java diff --git a/src/main/java/com/maxmind/db/ParameterNotFoundException.java b/reader/src/main/java/com/maxmind/db/ParameterNotFoundException.java similarity index 100% rename from src/main/java/com/maxmind/db/ParameterNotFoundException.java rename to reader/src/main/java/com/maxmind/db/ParameterNotFoundException.java diff --git a/reader/src/main/java/com/maxmind/db/PositionedByteReader.java b/reader/src/main/java/com/maxmind/db/PositionedByteReader.java new file mode 100644 index 00000000..27f1fb87 --- /dev/null +++ b/reader/src/main/java/com/maxmind/db/PositionedByteReader.java @@ -0,0 +1,145 @@ +package com.maxmind.db; + +import java.nio.BufferUnderflowException; + +/** + * This is a {@link ByteReader} that tracks its position. For instance, + * {@link java.nio.ByteBuffer}s track their position, so when using that, this class isn't + * necessary. If implemented with something like an array, which doesn't have its own pointer, + * this class is helpful. + */ +public abstract class PositionedByteReader implements ByteReader { + // Copied from java.nio.Buffer + /** + * {@link java.nio.ByteBuffer#createCapacityException(int)} + */ + static IllegalArgumentException createCapacityException(long capacity) { + assert capacity < 0 : "capacity expected to be negative"; + return new IllegalArgumentException("capacity < 0: (" + + capacity + " < 0)"); + } + + // copied from Buffer.java + /** + * {@link java.nio.Buffer#createPositionException(int)} + */ + static IllegalArgumentException createPositionException( + final long capacity, + final long newPosition, + final boolean isLarge + ) { + String msg = null; + + if (isLarge) { + msg = "newPosition > limit: (" + newPosition + " > " + capacity + ")"; + } else { // assume negative + assert newPosition < 0 : "newPosition expected to be negative"; + msg = "newPosition < 0: (" + newPosition + " < 0)"; + } + + return new IllegalArgumentException(msg); + } + + protected long position; + protected final long capacity; + + protected PositionedByteReader( + final long position, + final long capacity + ) { + if (capacity < 0) { + throw createCapacityException(capacity); + } + this.position = position; + this.capacity = capacity; + } + + protected PositionedByteReader(final long capacity) { + this(0, capacity); + } + + // copied from Buffer.java + /** + * {@link java.nio.ByteBuffer#nextGetIndex(int)} + */ + protected long nextGetIndex(long nb) { // package-private + long p = position(); + if (capacity() - p < nb) { + throw new BufferUnderflowException(); + } + position = p + nb; + return p; + } + + // copied from Buffer.java + /** + * {@link java.nio.Buffer#checkIndex(int)} + */ + protected long checkIndex(long i) { // package-private + if ((i < 0) || (i >= capacity())) { + throw new IndexOutOfBoundsException(); + } + return i; + } + + /** + * See {@link java.nio.ByteBuffer#position(int)} + */ + @Override + public ByteReader position(final long newPosition) { + final boolean isLarge = newPosition >= capacity(); + if (isLarge || newPosition < 0) { + throw createPositionException(capacity, newPosition, isLarge); + } + this.position = newPosition; + return this; + } + + @Override + public long position() { + return position; + } + + @Override + public long capacity() { + return capacity; + } + + /** + * Get the byte with the assumption that the index is valid. + */ + protected abstract byte getUnsafe(long index); + + /** + * Get the byte at {@link ByteReader#position()}. See {@link java.nio.ByteBuffer#get()} + */ + @Override + public byte get() { + return getUnsafe(nextGetIndex(1)); + } + + /** + * Get the byte at the given index. See {@link java.nio.ByteBuffer#get(int)} + */ + @Override + public byte get(long index) { + return getUnsafe(checkIndex(index)); + } + + /** + * Get the bytes with the assumption that the bounds checks already passed. + */ + protected abstract byte[] getBytesUnsafe( + final long position, + final int length + ); + + /** + * Read the requested number of bytes starting at {@link ByteReader#position()}. + * It moves {@link ByteReader#position()} forward by the same amount. + */ + @Override + public byte[] getBytes(final int length) { + return getBytesUnsafe(nextGetIndex(length), length); + } +} diff --git a/reader/src/main/java/com/maxmind/db/PositionedIntLimitedByteReader.java b/reader/src/main/java/com/maxmind/db/PositionedIntLimitedByteReader.java new file mode 100644 index 00000000..b61ca90a --- /dev/null +++ b/reader/src/main/java/com/maxmind/db/PositionedIntLimitedByteReader.java @@ -0,0 +1,109 @@ +package com.maxmind.db; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +/** + * This is a combination of {@link PositionedByteReader} and {@link IntLimitedByteReader}. + */ +public abstract class PositionedIntLimitedByteReader extends IntLimitedByteReader { + protected int position; + protected final int capacity; + + protected PositionedIntLimitedByteReader( + final int position, + final int capacity + ) { + if (capacity < 0) { + throw PositionedByteReader.createCapacityException(capacity); + } + this.position = position; + this.capacity = capacity; + } + + protected PositionedIntLimitedByteReader(final int capacity) { + this(0, capacity); + } + + // copied from Buffer.java + /** + * {@link ByteBuffer#nextGetIndex(int)} + */ + protected int nextGetIndex(long nb) { // package-private + int p = position; + if (capacity - p < nb) { + throw new BufferUnderflowException(); + } + position = p + (int) nb; + return p; + } + + // copied from Buffer.java + /** + * {@link java.nio.Buffer#checkIndex(int)} + */ + protected int checkIndex(long i) { // package-private + if ((i < 0) || (i >= capacity)) { + throw new IndexOutOfBoundsException(); + } + return (int) i; + } + + /** + * See {@link ByteBuffer#position(int)} + */ + @Override + protected ByteReader position(final int newPosition) { + final boolean isLarge = newPosition >= capacity; + if (isLarge || newPosition < 0) { + throw PositionedByteReader.createPositionException(capacity, newPosition, isLarge); + } + this.position = newPosition; + return this; + } + + @Override + public long position() { + return position; + } + + @Override + public long capacity() { + return capacity; + } + + /** + * Get the byte with the assumption that the index is valid. + */ + protected abstract byte getUnsafe(int index); + + /** + * Get the byte at {@link ByteReader#position()}. See {@link ByteBuffer#get()}. + */ + public byte get() { + return getUnsafe(nextGetIndex(1)); + } + + /** + * Get the byte at the given index. See {@link ByteBuffer#get(int)} + */ + public byte get(long index) { + return getUnsafe(checkIndex(index)); + } + + /** + * Get the bytes with the assumption that the bounds checks already passed. + */ + protected abstract byte[] getBytesUnsafe( + final int position, + final int length + ); + + /** + * Read the requested number of bytes starting at {@link ByteReader#position()}. + * It moves {@link ByteReader#position()} forward by the same amount. + */ + public byte[] getBytes(final int length) { + return getBytesUnsafe(nextGetIndex(length), length); + } +} diff --git a/src/main/java/com/maxmind/db/Reader.java b/reader/src/main/java/com/maxmind/db/Reader.java similarity index 89% rename from src/main/java/com/maxmind/db/Reader.java rename to reader/src/main/java/com/maxmind/db/Reader.java index 1b8b84b2..e0e71405 100644 --- a/src/main/java/com/maxmind/db/Reader.java +++ b/reader/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,7 +23,7 @@ public final class Reader implements Closeable { private final int ipV4Start; private final Metadata metadata; - private final AtomicReference bufferHolderReference; + private volatile ByteReaderHolder bufferHolderReference; private final NodeCache cache; private final ConcurrentHashMap constructors; @@ -44,6 +43,13 @@ public enum FileMode { MEMORY } + /** + * Create a Reader using your own implementation of {@link ByteReader}. + */ + public Reader(final ByteReader reader, String name, NodeCache cache) throws IOException { + this(new ByteReaderHolder(reader), name, cache); + } + /** * Constructs a Reader for the MaxMind DB format, with no caching. The file * passed to it must be a valid MaxMind DB file such as a GeoIP2 database @@ -89,7 +95,7 @@ public Reader(InputStream source) throws IOException { * @throws IOException if there is an error reading from the Stream. */ public Reader(InputStream source, NodeCache cache) throws IOException { - this(new BufferHolder(source), "", cache); + this(new ByteReaderHolder(source), "", cache); } /** @@ -116,20 +122,23 @@ public Reader(File database, FileMode fileMode) throws IOException { * @throws IOException if there is an error opening or reading from the file. */ public Reader(File database, FileMode fileMode, NodeCache cache) throws IOException { - this(new BufferHolder(database, fileMode), database.getName(), cache); + this(new ByteReaderHolder(database, fileMode), database.getName(), cache); } - private Reader(BufferHolder bufferHolder, String name, NodeCache cache) throws IOException { - this.bufferHolderReference = new AtomicReference<>( - bufferHolder); + private Reader( + ByteReaderHolder byteReaderHolder, + String name, + NodeCache cache + ) throws IOException { + this.bufferHolderReference = byteReaderHolder; if (cache == null) { throw new NullPointerException("Cache cannot be null"); } this.cache = cache; - ByteBuffer buffer = bufferHolder.get(); - int start = this.findMetadataStart(buffer, name); + ByteReader buffer = byteReaderHolder.get(); + long start = this.findMetadataStart(buffer, name); Decoder metadataDecoder = new Decoder(this.cache, buffer, start); this.metadata = metadataDecoder.decode(start, Metadata.class); @@ -176,8 +185,8 @@ public DatabaseRecord getRecord(InetAddress ipAddress, Class cls) int pl = traverseResult[1]; int record = traverseResult[0]; - int nodeCount = this.metadata.getNodeCount(); - ByteBuffer buffer = this.getBufferHolder().get(); + long nodeCount = this.metadata.getNodeCount(); + ByteReader buffer = this.getBufferHolder().get(); T dataRecord = null; if (record > nodeCount) { // record is a data pointer @@ -205,7 +214,7 @@ int record = traverseResult[0]; * @throws ClosedDatabaseException Exception for a closed databased. * @throws InvalidDatabaseException Exception for an invalid database. */ - public Networks networks(Class typeParameterClass) throws + public Networks networks(Class typeParameterClass) throws InvalidNetworkException, ClosedDatabaseException, InvalidDatabaseException { return this.networks(false, typeParameterClass); } @@ -225,8 +234,8 @@ public Networks networks(Class typeParameterClass) throws * @throws InvalidDatabaseException Exception for an invalid database. */ public Networks networks( - boolean includeAliasedNetworks, - Class typeParameterClass) throws + boolean includeAliasedNetworks, + Class typeParameterClass) throws InvalidNetworkException, ClosedDatabaseException, InvalidDatabaseException { try { if (this.getMetadata().getIpVersion() == 6) { @@ -245,12 +254,12 @@ public Networks networks( } } - BufferHolder getBufferHolder() throws ClosedDatabaseException { - BufferHolder bufferHolder = this.bufferHolderReference.get(); - if (bufferHolder == null) { + ByteReaderHolder getBufferHolder() throws ClosedDatabaseException { + ByteReaderHolder byteReaderHolder = this.bufferHolderReference; + if (byteReaderHolder == null) { throw new ClosedDatabaseException(); } - return bufferHolder; + return byteReaderHolder; } private int startNode(int bitLength) { @@ -264,7 +273,7 @@ private int startNode(int bitLength) { return 0; } - private int findIpV4StartNode(ByteBuffer buffer) + private int findIpV4StartNode(ByteReader buffer) throws InvalidDatabaseException { if (this.metadata.getIpVersion() == 4) { return 0; @@ -294,9 +303,9 @@ private int findIpV4StartNode(ByteBuffer buffer) * @throws InvalidDatabaseException Exception for an invalid database. */ public Networks networksWithin( - Network network, - boolean includeAliasedNetworks, - Class typeParameterClass) + Network network, + boolean includeAliasedNetworks, + Class typeParameterClass) throws InvalidNetworkException, ClosedDatabaseException, InvalidDatabaseException { InetAddress networkAddress = network.getNetworkAddress(); if (this.metadata.getIpVersion() == 4 && networkAddress instanceof Inet6Address) { @@ -337,10 +346,10 @@ public Networks networksWithin( */ private int[] traverseTree(byte[] ip, int bitCount) throws ClosedDatabaseException, InvalidDatabaseException { - ByteBuffer buffer = this.getBufferHolder().get(); + ByteReader buffer = this.getBufferHolder().get(); int bitLength = ip.length * 8; int record = this.startNode(bitLength); - int nodeCount = this.metadata.getNodeCount(); + long nodeCount = this.metadata.getNodeCount(); int i = 0; for (; i < bitCount && record < nodeCount; i++) { @@ -355,11 +364,11 @@ record = this.readNode(buffer, record, bit); return new int[]{record, i}; } - int readNode(ByteBuffer buffer, int nodeNumber, int index) - throws InvalidDatabaseException { + int readNode(ByteReader buffer, int nodeNumber, int index) + throws InvalidDatabaseException { // index is the index of the record within the node, which // can either be 0 or 1. - int baseOffset = nodeNumber * this.metadata.getNodeByteSize(); + long baseOffset = ((long) nodeNumber) * this.metadata.getNodeByteSize(); switch (this.metadata.getRecordSize()) { case 24: @@ -389,11 +398,11 @@ int readNode(ByteBuffer buffer, int nodeNumber, int index) } T resolveDataPointer( - ByteBuffer buffer, + ByteReader buffer, int pointer, Class cls ) throws IOException { - int resolved = (pointer - this.metadata.getNodeCount()) + long resolved = (pointer - this.metadata.getNodeCount()) + this.metadata.getSearchTreeSize(); if (resolved >= buffer.capacity()) { @@ -421,9 +430,9 @@ T resolveDataPointer( * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever * an issue, but I suspect it won't be. */ - private int findMetadataStart(ByteBuffer buffer, String databaseName) + private long findMetadataStart(ByteReader buffer, String databaseName) throws InvalidDatabaseException { - int fileSize = buffer.capacity(); + long fileSize = buffer.capacity(); FILE: for (int i = 0; i < fileSize - METADATA_START_MARKER.length + 1; i++) { @@ -455,7 +464,7 @@ public Metadata getMetadata() { *

* If you are using FileMode.MEMORY_MAPPED, this will * not unmap the underlying file due to a limitation in Java's - * MappedByteBuffer. It will however set the reference to + * MappedBigByteBuffer. It will however set the reference to * the buffer to null, allowing the garbage collector to * collect it. *

@@ -464,6 +473,6 @@ public Metadata getMetadata() { */ @Override public void close() throws IOException { - this.bufferHolderReference.set(null); + this.bufferHolderReference = null; } } diff --git a/src/main/java/com/maxmind/db/Type.java b/reader/src/main/java/com/maxmind/db/Type.java similarity index 100% rename from src/main/java/com/maxmind/db/Type.java rename to reader/src/main/java/com/maxmind/db/Type.java diff --git a/src/main/java/com/maxmind/db/package-info.java b/reader/src/main/java/com/maxmind/db/package-info.java similarity index 100% rename from src/main/java/com/maxmind/db/package-info.java rename to reader/src/main/java/com/maxmind/db/package-info.java diff --git a/src/main/java/module-info.java b/reader/src/main/java/module-info.java similarity index 100% rename from src/main/java/module-info.java rename to reader/src/main/java/module-info.java diff --git a/reader/src/test/java/com/maxmind/db/ByteReaderHolderTest.java b/reader/src/test/java/com/maxmind/db/ByteReaderHolderTest.java new file mode 100644 index 00000000..b46b1a67 --- /dev/null +++ b/reader/src/test/java/com/maxmind/db/ByteReaderHolderTest.java @@ -0,0 +1,22 @@ +package com.maxmind.db; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ByteReaderHolderTest { + + @Test + public void testThreadSafe() throws IOException { + final ByteReaderHolder holder = + new ByteReaderHolder( + new ByteArrayInputStream(new byte[] {0, 1}) + ); + final ByteReader byteReader0 = holder.get(); + byteReader0.position(1); + final ByteReader byteReader1 = holder.get(); + Assertions.assertNotEquals(byteReader0.position(), byteReader1.position()); + } + +} diff --git a/src/test/java/com/maxmind/db/DecoderTest.java b/reader/src/test/java/com/maxmind/db/DecoderTest.java similarity index 98% rename from src/test/java/com/maxmind/db/DecoderTest.java rename to reader/src/test/java/com/maxmind/db/DecoderTest.java index 2668bd58..6a3f9181 100644 --- a/src/test/java/com/maxmind/db/DecoderTest.java +++ b/reader/src/test/java/com/maxmind/db/DecoderTest.java @@ -407,8 +407,9 @@ public void testArrays() throws IOException { public void testInvalidControlByte() throws IOException { try (FileChannel fc = DecoderTest.getFileChannel(new byte[] {0x0, 0xF})) { MappedByteBuffer mmap = fc.map(MapMode.READ_ONLY, 0, fc.size()); + ByteReader byteReader = new ByteBufferByteReader(mmap); - Decoder decoder = new Decoder(new CHMCache(), mmap, 0); + Decoder decoder = new Decoder(new CHMCache(), byteReader, 0); InvalidDatabaseException ex = assertThrows( InvalidDatabaseException.class, () -> decoder.decode(0, String.class)); @@ -428,8 +429,9 @@ private static void testTypeDecoding(Type type, Map tests) String desc = "decoded " + type.name() + " - " + expect; try (FileChannel fc = DecoderTest.getFileChannel(input)) { MappedByteBuffer mmap = fc.map(MapMode.READ_ONLY, 0, fc.size()); + ByteReader byteReader = new ByteBufferByteReader(mmap); - Decoder decoder = new Decoder(cache, mmap, 0); + Decoder decoder = new Decoder(cache, byteReader, 0); decoder.pointerTestHack = true; // XXX - this could be streamlined diff --git a/src/test/java/com/maxmind/db/MultiThreadedTest.java b/reader/src/test/java/com/maxmind/db/MultiThreadedTest.java similarity index 100% rename from src/test/java/com/maxmind/db/MultiThreadedTest.java rename to reader/src/test/java/com/maxmind/db/MultiThreadedTest.java diff --git a/src/test/java/com/maxmind/db/NetworkTest.java b/reader/src/test/java/com/maxmind/db/NetworkTest.java similarity index 100% rename from src/test/java/com/maxmind/db/NetworkTest.java rename to reader/src/test/java/com/maxmind/db/NetworkTest.java diff --git a/src/test/java/com/maxmind/db/PointerTest.java b/reader/src/test/java/com/maxmind/db/PointerTest.java similarity index 94% rename from src/test/java/com/maxmind/db/PointerTest.java rename to reader/src/test/java/com/maxmind/db/PointerTest.java index 1aa755f6..3e50901e 100644 --- a/src/test/java/com/maxmind/db/PointerTest.java +++ b/reader/src/test/java/com/maxmind/db/PointerTest.java @@ -14,7 +14,7 @@ public class PointerTest { @Test public void testWithPointers() throws IOException { File file = ReaderTest.getFile("maps-with-pointers.raw"); - BufferHolder ptf = new BufferHolder(file, FileMode.MEMORY); + ByteReaderHolder ptf = new ByteReaderHolder(file, FileMode.MEMORY); Decoder decoder = new Decoder(NoCache.getInstance(), ptf.get(), 0); Map map = new HashMap<>(); diff --git a/reader/src/test/java/com/maxmind/db/PositionByteReader.java b/reader/src/test/java/com/maxmind/db/PositionByteReader.java new file mode 100644 index 00000000..3a92f38f --- /dev/null +++ b/reader/src/test/java/com/maxmind/db/PositionByteReader.java @@ -0,0 +1,33 @@ +package com.maxmind.db; + +import java.util.Arrays; + +/** + * This is just for testing. It returns the position as the byte value. + */ +public class PositionByteReader extends PositionedByteReader { + public PositionByteReader(long position, long capacity) { + super(position, capacity); + } + + public PositionByteReader(long capacity) { + super(capacity); + } + + @Override + protected byte getUnsafe(long index) { + return (byte) index; + } + + @Override + protected byte[] getBytesUnsafe(final long position, final int length) { + final byte[] bytes = new byte[length]; + Arrays.fill(bytes, (byte) position); + return bytes; + } + + @Override + public ByteReader duplicate() { + return new PositionByteReader(position, capacity); + } +} diff --git a/reader/src/test/java/com/maxmind/db/PositionedByteReaderTest.java b/reader/src/test/java/com/maxmind/db/PositionedByteReaderTest.java new file mode 100644 index 00000000..488b4dca --- /dev/null +++ b/reader/src/test/java/com/maxmind/db/PositionedByteReaderTest.java @@ -0,0 +1,33 @@ +package com.maxmind.db; + +import java.util.Arrays; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class PositionedByteReaderTest { + + @Test + public void position() { + final PositionedByteReader r = new PositionByteReader(10); + Assertions.assertEquals(0L, r.position()); + Assertions.assertSame(r, r.position(1)); + Assertions.assertEquals(1L, r.position()); + Assertions.assertThrows(IllegalArgumentException.class, () -> r.position(r.capacity())); + Assertions.assertThrows(IllegalArgumentException.class, () -> r.position(-1)); + final byte[] expectedPositionBytes = new byte[3]; + Arrays.fill(expectedPositionBytes, (byte) 1); + Assertions.assertArrayEquals( + expectedPositionBytes, + r.getBytes(expectedPositionBytes.length) + ); + } + + @Test + public void capacity() { + final long capacity = 0; + final PositionedByteReader r = new PositionByteReader(capacity); + Assertions.assertEquals(capacity, r.capacity()); + Assertions.assertThrows(IllegalArgumentException.class, () -> new PositionByteReader(-1)); + } + +} diff --git a/src/test/java/com/maxmind/db/ReaderTest.java b/reader/src/test/java/com/maxmind/db/ReaderTest.java similarity index 99% rename from src/test/java/com/maxmind/db/ReaderTest.java rename to reader/src/test/java/com/maxmind/db/ReaderTest.java index 004ecb24..5f1642ba 100644 --- a/src/test/java/com/maxmind/db/ReaderTest.java +++ b/reader/src/test/java/com/maxmind/db/ReaderTest.java @@ -94,7 +94,7 @@ public void testNetworks() throws IOException, InvalidDatabaseException, Invalid InetAddress actualIPInData = InetAddress.getByName(data.get("ip")); assertEquals( - iteration.getNetwork().getNetworkAddress(), + iteration.getNetwork().getNetworkAddress(), actualIPInData, "expected ip address" ); @@ -172,7 +172,7 @@ public networkTest(String network, int prefix,String database, String[] expecte } ), new networkTest( - "255.255.255.0", + "255.255.255.0", 24, "ipv4", new String[]{} @@ -238,7 +238,7 @@ public networkTest(String network, int prefix,String database, String[] expecte "1.1.1.4/30", "1.1.1.8/29", "1.1.1.16/28", - "1.1.1.32/32", + "1.1.1.32/32", } ), new networkTest( @@ -251,7 +251,7 @@ public networkTest(String network, int prefix,String database, String[] expecte "1.1.1.4/30", "1.1.1.8/29", "1.1.1.16/28", - "1.1.1.32/32", + "1.1.1.32/32", }, true ), @@ -937,8 +937,8 @@ public void testDecodeWrongTypeWithWrongArguments() throws IOException { public void testDecodeWithDataTypeMismatchInModel() throws IOException { this.testReader = new Reader(getFile("GeoIP2-City-Test.mmdb")); DeserializationException ex = assertThrows(DeserializationException.class, - () -> this.testReader.get(InetAddress.getByName("2.125.160.216"), - TestDataTypeMismatchInModel.class)); + () -> this.testReader.get(InetAddress.getByName("2.125.160.216"), + TestDataTypeMismatchInModel.class)); assertThat(ex.getMessage(), containsString("Error getting record for IP")); assertThat(ex.getMessage(), containsString("Error creating map entry for")); assertThat(ex.getCause().getCause().getClass(), equalTo(ClassCastException.class)); @@ -998,8 +998,8 @@ static class TestDataTypeMismatchInModel { @MaxMindDbConstructor public TestDataTypeMismatchInModel( - @MaxMindDbParameter(name = "location") - Map location + @MaxMindDbParameter(name = "location") + Map location ) { this.location = location; } diff --git a/src/test/resources/maxmind-db b/reader/src/test/resources/maxmind-db similarity index 100% rename from src/test/resources/maxmind-db rename to reader/src/test/resources/maxmind-db diff --git a/sample/Benchmark.java b/sample/Benchmark.java deleted file mode 100644 index 3d73011b..00000000 --- a/sample/Benchmark.java +++ /dev/null @@ -1,62 +0,0 @@ -import java.io.File; -import java.io.IOException; -import java.net.InetAddress; -import java.util.Map; -import java.util.Random; - -import com.maxmind.db.CHMCache; -import com.maxmind.db.InvalidDatabaseException; -import com.maxmind.db.NoCache; -import com.maxmind.db.NodeCache; -import com.maxmind.db.Reader; -import com.maxmind.db.Reader.FileMode; - -public class Benchmark { - - private final static int COUNT = 1000000; - private final static int WARMUPS = 3; - private final static int BENCHMARKS = 5; - private final static boolean TRACE = false; - - public static void main(String[] args) throws IOException, InvalidDatabaseException { - File file = new File(args.length > 0 ? args[0] : "GeoLite2-City.mmdb"); - System.out.println("No caching"); - loop("Warming up", file, WARMUPS, NoCache.getInstance()); - loop("Benchmarking", file, BENCHMARKS, NoCache.getInstance()); - - System.out.println("With caching"); - loop("Warming up", file, WARMUPS, new CHMCache()); - loop("Benchmarking", file, BENCHMARKS, new CHMCache()); - } - - private static void loop(String msg, File file, int loops, NodeCache cache) throws IOException { - System.out.println(msg); - for (int i = 0; i < loops; i++) { - Reader r = new Reader(file, FileMode.MEMORY_MAPPED, cache); - bench(r, COUNT, i); - } - System.out.println(); - } - - private static void bench(Reader r, int count, int seed) throws IOException { - Random random = new Random(seed); - long startTime = System.nanoTime(); - byte[] address = new byte[4]; - for (int i = 0; i < count; i++) { - random.nextBytes(address); - InetAddress ip = InetAddress.getByAddress(address); - Map t = r.get(ip, Map.class); - if (TRACE) { - if (i % 50000 == 0) { - System.out.println(i + " " + ip); - System.out.println(t); - } - } - } - long endTime = System.nanoTime(); - - long duration = endTime - startTime; - long qps = count * 1000000000L / duration; - System.out.println("Requests per second: " + qps); - } -}