Skip to content

Commit fe4ed61

Browse files
committed
Add MaxMindDbCreator annotation
Similar to JsonCreator
1 parent 3352b5e commit fe4ed61

File tree

2 files changed

+169
-5
lines changed

2 files changed

+169
-5
lines changed

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

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.lang.annotation.Annotation;
55
import java.lang.reflect.Constructor;
66
import java.lang.reflect.InvocationTargetException;
7+
import java.lang.reflect.Method;
78
import java.lang.reflect.ParameterizedType;
89
import java.math.BigInteger;
910
import java.nio.charset.CharacterCodingException;
@@ -161,13 +162,54 @@ private <T> Object decodeByType(
161162
case BOOLEAN:
162163
return Decoder.decodeBoolean(size);
163164
case UTF8_STRING:
164-
return this.decodeString(size);
165+
var s = this.decodeString(size);
166+
var created = tryCreateFromScalar(cls, s);
167+
if (created != null) {
168+
return created;
169+
}
170+
if (cls.isEnum()) {
171+
@SuppressWarnings({"rawtypes", "unchecked"})
172+
var enumClass = (Class<? extends Enum>) cls;
173+
try {
174+
// Attempt a forgiving mapping commonly needed for ConnectionType
175+
var candidate = s.trim()
176+
.replace(' ', '_')
177+
.replace('-', '_')
178+
.replace('/', '_')
179+
.toUpperCase();
180+
return Enum.valueOf(enumClass, candidate);
181+
} catch (IllegalArgumentException ignored) {
182+
// fall through to return the raw string
183+
}
184+
}
185+
return s;
165186
case DOUBLE:
166-
return this.decodeDouble(size);
187+
var d = this.decodeDouble(size);
188+
{
189+
var created2 = tryCreateFromScalar(cls, d);
190+
if (created2 != null) {
191+
return created2;
192+
}
193+
}
194+
return d;
167195
case FLOAT:
168-
return this.decodeFloat(size);
196+
var f = this.decodeFloat(size);
197+
{
198+
var created3 = tryCreateFromScalar(cls, f);
199+
if (created3 != null) {
200+
return created3;
201+
}
202+
}
203+
return f;
169204
case BYTES:
170-
return this.getByteArray(size);
205+
var bytes = this.getByteArray(size);
206+
{
207+
var created4 = tryCreateFromScalar(cls, bytes);
208+
if (created4 != null) {
209+
return created4;
210+
}
211+
}
212+
return bytes;
171213
case UINT16:
172214
return coerceFromInt(this.decodeUint16(size), cls);
173215
case UINT32:
@@ -176,14 +218,26 @@ private <T> Object decodeByType(
176218
return coerceFromInt(this.decodeInt32(size), cls);
177219
case UINT64:
178220
case UINT128:
179-
return this.decodeBigInteger(size);
221+
var bi = this.decodeBigInteger(size);
222+
{
223+
var created5 = tryCreateFromScalar(cls, bi);
224+
if (created5 != null) {
225+
return created5;
226+
}
227+
}
228+
return bi;
180229
default:
181230
throw new InvalidDatabaseException(
182231
"Unknown or unexpected type: " + type.name());
183232
}
184233
}
185234

186235
private static Object coerceFromInt(int value, Class<?> target) {
236+
// If a creator exists that accepts an Integer-compatible value, use it
237+
var created = tryCreateFromScalar(target, Integer.valueOf(value));
238+
if (created != null) {
239+
return created;
240+
}
187241
if (target.equals(Object.class)
188242
|| target.equals(Integer.TYPE)
189243
|| target.equals(Integer.class)) {
@@ -218,6 +272,10 @@ private static Object coerceFromInt(int value, Class<?> target) {
218272
}
219273

220274
private static Object coerceFromLong(long value, Class<?> target) {
275+
var created = tryCreateFromScalar(target, Long.valueOf(value));
276+
if (created != null) {
277+
return created;
278+
}
221279
if (target.equals(Object.class) || target.equals(Long.TYPE) || target.equals(Long.class)) {
222280
return value;
223281
}
@@ -251,6 +309,58 @@ private static Object coerceFromLong(long value, Class<?> target) {
251309
return value;
252310
}
253311

312+
private static Object tryCreateFromScalar(Class<?> target, Object value) {
313+
if (target.equals(Object.class)) {
314+
return null;
315+
}
316+
if (value != null && target.isAssignableFrom(value.getClass())) {
317+
return null;
318+
}
319+
Method creator = findSingleArgCreator(target);
320+
if (creator == null) {
321+
return null;
322+
}
323+
var paramType = creator.getParameterTypes()[0];
324+
Object argument = value;
325+
if (value != null && !paramType.isAssignableFrom(value.getClass())) {
326+
// Minimal adaptation: allow converting to String for String parameters
327+
if (paramType.equals(String.class)) {
328+
argument = String.valueOf(value);
329+
} else {
330+
return null;
331+
}
332+
}
333+
try {
334+
return creator.invoke(null, argument);
335+
} catch (IllegalAccessException | InvocationTargetException e) {
336+
throw new DeserializationException("Error invoking @MaxMindDbCreator on "
337+
+ target.getName() + ": " + e.getMessage(), e);
338+
}
339+
}
340+
341+
private static Method findSingleArgCreator(Class<?> target) {
342+
for (var m : target.getDeclaredMethods()) {
343+
if (m.getAnnotation(MaxMindDbCreator.class) == null) {
344+
continue;
345+
}
346+
if (!java.lang.reflect.Modifier.isStatic(m.getModifiers())) {
347+
continue;
348+
}
349+
if (!java.lang.reflect.Modifier.isPublic(m.getModifiers())) {
350+
// To avoid module access issues, only consider public methods
351+
continue;
352+
}
353+
if (!target.isAssignableFrom(m.getReturnType())) {
354+
continue;
355+
}
356+
if (m.getParameterCount() != 1) {
357+
continue;
358+
}
359+
return m;
360+
}
361+
return null;
362+
}
363+
254364
private String decodeString(long size) throws CharacterCodingException {
255365
var oldLimit = buffer.limit();
256366
buffer.limit(buffer.position() + size);

src/test/java/com/maxmind/db/ReaderTest.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,60 @@ private void testDecodingTypesIntoModelObject(Reader reader, boolean booleanValu
615615
model.uint128Field);
616616
}
617617

618+
// Validate @MaxMindDbCreator for enum conversion from a string field
619+
enum Greeting {
620+
HELLO,
621+
OTHER;
622+
623+
@MaxMindDbCreator
624+
public static Greeting from(String s) {
625+
return "hello".equalsIgnoreCase(s) ? HELLO : OTHER;
626+
}
627+
}
628+
629+
static class MapXModelEnum {
630+
List<Long> arrayXField;
631+
Greeting greeting;
632+
633+
@MaxMindDbConstructor
634+
public MapXModelEnum(
635+
@MaxMindDbParameter(name = "arrayX") List<Long> arrayXField,
636+
@MaxMindDbParameter(name = "utf8_stringX") Greeting greeting
637+
) {
638+
this.arrayXField = arrayXField;
639+
this.greeting = greeting;
640+
}
641+
}
642+
643+
static class MapModelEnum {
644+
MapXModelEnum mapXField;
645+
646+
@MaxMindDbConstructor
647+
public MapModelEnum(@MaxMindDbParameter(name = "mapX") MapXModelEnum mapXField) {
648+
this.mapXField = mapXField;
649+
}
650+
}
651+
652+
static class TestModelEnumCreator {
653+
MapModelEnum mapField;
654+
655+
@MaxMindDbConstructor
656+
public TestModelEnumCreator(@MaxMindDbParameter(name = "map") MapModelEnum mapField) {
657+
this.mapField = mapField;
658+
}
659+
}
660+
661+
@ParameterizedTest
662+
@MethodSource("chunkSizes")
663+
public void testEnumCreatorFromString(int chunkSize) throws IOException {
664+
this.testReader = new Reader(getFile("MaxMind-DB-test-decoder.mmdb"), chunkSize);
665+
var model = this.testReader.get(
666+
InetAddress.getByName("::1.1.1.0"),
667+
TestModelEnumCreator.class
668+
);
669+
assertEquals(Greeting.HELLO, model.mapField.mapXField.greeting);
670+
}
671+
618672
static class TestModel {
619673
boolean booleanField;
620674
byte[] bytesField;

0 commit comments

Comments
 (0)