Skip to content

Commit d746dad

Browse files
committed
Handle nested context-only constructors
1 parent 90d18b4 commit d746dad

File tree

2 files changed

+162
-1
lines changed

2 files changed

+162
-1
lines changed

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

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,20 @@ private <T> CachedConstructor<T> loadConstructorMetadata(Class<T> cls) {
555555
parameterIndexes.put(name, i);
556556
}
557557

558+
// Check for transitive context requirements: if any non-injection parameter type
559+
// itself requires context (e.g., nested objects with @MaxMindDbIpAddress annotations),
560+
// then this parent class also requires context to avoid incorrect caching.
561+
if (!requiresContext) {
562+
for (int i = 0; i < parameterTypes.length; i++) {
563+
if (parameterInjections[i] == ParameterInjection.NONE) {
564+
if (shouldInstantiateFromContext(parameterTypes[i])) {
565+
requiresContext = true;
566+
break;
567+
}
568+
}
569+
}
570+
}
571+
558572
var cachedConstructor = new CachedConstructor<>(
559573
constructor,
560574
parameterTypes,
@@ -602,8 +616,15 @@ private <T> Object decodeMapIntoObject(int size, Class<T> cls)
602616
parameters[i] = injectParameter(parameterInjections[i], parameterTypes[i]);
603617
continue;
604618
}
605-
if (parameters[i] == null && parameterDefaults[i] != null) {
619+
if (parameters[i] != null) {
620+
continue;
621+
}
622+
if (parameterDefaults[i] != null) {
606623
parameters[i] = parameterDefaults[i];
624+
continue;
625+
}
626+
if (shouldInstantiateFromContext(parameterTypes[i])) {
627+
parameters[i] = instantiateWithLookupContext(parameterTypes[i]);
607628
}
608629
}
609630

@@ -629,6 +650,77 @@ private <T> Object decodeMapIntoObject(int size, Class<T> cls)
629650
}
630651
}
631652

653+
private boolean shouldInstantiateFromContext(Class<?> parameterType) {
654+
if (parameterType == null
655+
|| parameterType.isPrimitive()
656+
|| isSimpleType(parameterType)
657+
|| Map.class.isAssignableFrom(parameterType)
658+
|| List.class.isAssignableFrom(parameterType)) {
659+
return false;
660+
}
661+
return requiresLookupContext(parameterType);
662+
}
663+
664+
private Object instantiateWithLookupContext(Class<?> parameterType) {
665+
var metadata = loadConstructorMetadata(parameterType);
666+
if (metadata == null || !metadata.requiresLookupContext()) {
667+
return null;
668+
}
669+
670+
var ctor = metadata.constructor();
671+
var types = metadata.parameterTypes();
672+
var defaults = metadata.parameterDefaults();
673+
var injections = metadata.parameterInjections();
674+
var args = new Object[types.length];
675+
676+
for (int i = 0; i < args.length; i++) {
677+
if (injections[i] != ParameterInjection.NONE) {
678+
args[i] = injectParameter(injections[i], types[i]);
679+
} else if (defaults[i] != null) {
680+
args[i] = defaults[i];
681+
} else if (types[i].isPrimitive()) {
682+
args[i] = primitiveDefault(types[i]);
683+
} else {
684+
args[i] = null;
685+
}
686+
}
687+
688+
try {
689+
return ctor.newInstance(args);
690+
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
691+
throw new DeserializationException(
692+
"Error creating object of type: " + parameterType.getName(), e);
693+
}
694+
}
695+
696+
private static Object primitiveDefault(Class<?> type) {
697+
if (type.equals(Boolean.TYPE)) {
698+
return false;
699+
}
700+
if (type.equals(Byte.TYPE)) {
701+
return (byte) 0;
702+
}
703+
if (type.equals(Short.TYPE)) {
704+
return (short) 0;
705+
}
706+
if (type.equals(Integer.TYPE)) {
707+
return 0;
708+
}
709+
if (type.equals(Long.TYPE)) {
710+
return 0L;
711+
}
712+
if (type.equals(Float.TYPE)) {
713+
return 0.0f;
714+
}
715+
if (type.equals(Double.TYPE)) {
716+
return 0.0d;
717+
}
718+
if (type.equals(Character.TYPE)) {
719+
return '\0';
720+
}
721+
return null;
722+
}
723+
632724
private Object injectParameter(ParameterInjection injection, Class<?> parameterType) {
633725
return switch (injection) {
634726
case IP_ADDRESS -> getLookupIpValue(parameterType);

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,49 @@ public void testContextAnnotations(int chunkSize) throws IOException {
552552
}
553553
}
554554

555+
@ParameterizedTest
556+
@MethodSource("chunkSizes")
557+
public void testNestedContextAnnotations(int chunkSize) throws IOException {
558+
try (var reader = new Reader(getFile("MaxMind-DB-test-decoder.mmdb"), chunkSize)) {
559+
var firstIp = InetAddress.getByName("1.1.1.1");
560+
var secondIp = InetAddress.getByName("1.1.1.3");
561+
var expectedNetwork = reader.getRecord(firstIp, Map.class).network().toString();
562+
563+
var first = reader.get(firstIp, WrapperContextOnlyModel.class);
564+
var second = reader.get(secondIp, WrapperContextOnlyModel.class);
565+
566+
assertNotNull(first.context);
567+
assertEquals(firstIp, first.context.lookupIp);
568+
assertEquals(expectedNetwork, first.context.lookupNetwork.toString());
569+
570+
assertNotNull(second.context);
571+
assertEquals(secondIp, second.context.lookupIp);
572+
assertEquals(expectedNetwork, second.context.lookupNetwork.toString());
573+
}
574+
}
575+
576+
@ParameterizedTest
577+
@MethodSource("chunkSizes")
578+
public void testNestedContextAnnotationsWithCache(int chunkSize) throws IOException {
579+
var cache = new CHMCache();
580+
try (var reader = new Reader(getFile("MaxMind-DB-test-decoder.mmdb"), cache, chunkSize)) {
581+
var firstIp = InetAddress.getByName("1.1.1.1");
582+
var secondIp = InetAddress.getByName("1.1.1.3");
583+
var expectedNetwork = reader.getRecord(firstIp, Map.class).network().toString();
584+
585+
var first = reader.get(firstIp, WrapperContextOnlyModel.class);
586+
var second = reader.get(secondIp, WrapperContextOnlyModel.class);
587+
588+
assertNotNull(first.context);
589+
assertEquals(firstIp, first.context.lookupIp);
590+
assertEquals(expectedNetwork, first.context.lookupNetwork.toString());
591+
592+
assertNotNull(second.context);
593+
assertEquals(secondIp, second.context.lookupIp);
594+
assertEquals(expectedNetwork, second.context.lookupNetwork.toString());
595+
}
596+
}
597+
555598
@ParameterizedTest
556599
@MethodSource("chunkSizes")
557600
public void testDecodingTypesPointerDecoderFile(int chunkSize) throws IOException {
@@ -842,6 +885,32 @@ public TestModelBoxed(
842885
}
843886
}
844887

888+
static class ContextOnlyModel {
889+
InetAddress lookupIp;
890+
Network lookupNetwork;
891+
892+
@MaxMindDbConstructor
893+
public ContextOnlyModel(
894+
@MaxMindDbIpAddress InetAddress lookupIp,
895+
@MaxMindDbNetwork Network lookupNetwork
896+
) {
897+
this.lookupIp = lookupIp;
898+
this.lookupNetwork = lookupNetwork;
899+
}
900+
}
901+
902+
static class WrapperContextOnlyModel {
903+
ContextOnlyModel context;
904+
905+
@MaxMindDbConstructor
906+
public WrapperContextOnlyModel(
907+
@MaxMindDbParameter(name = "missing_context")
908+
ContextOnlyModel context
909+
) {
910+
this.context = context;
911+
}
912+
}
913+
845914
static class MapModelBoxed {
846915
MapXModelBoxed mapXField;
847916

0 commit comments

Comments
 (0)