Skip to content

Commit 3352b5e

Browse files
committed
Defaults for missing constructor params via annotation
We need both useDefault and defaultValue as otherwise there is no way to distinguish between the default value being the empty string and no default being set. If defaultValue is not set but useDefault is, we will use the Java default value, e.g., 0 or the empty string.
1 parent 59c7501 commit 3352b5e

File tree

5 files changed

+356
-8
lines changed

5 files changed

+356
-8
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,29 @@ public class Lookup {
137137
`@MaxMindDbParameter` annotation, the reader throws a
138138
`ParameterNotFoundException` with guidance.
139139

140+
Defaults for missing values
141+
142+
- Provide a default with
143+
`@MaxMindDbParameter(name = "...", useDefault = true, defaultValue = "...")`.
144+
- Supports primitives, boxed types, and `String`. If `defaultValue` is empty
145+
and `useDefault` is true, Java defaults are used (0, false, 0.0, empty
146+
string).
147+
- Example:
148+
149+
```java
150+
@MaxMindDbConstructor
151+
Example(
152+
@MaxMindDbParameter(name = "count", useDefault = true, defaultValue = "0")
153+
int count,
154+
@MaxMindDbParameter(
155+
name = "enabled",
156+
useDefault = true,
157+
defaultValue = "true"
158+
)
159+
boolean enabled
160+
) { }
161+
```
162+
140163
You can also use the reader object to iterate over the database.
141164
The `reader.networks()` and `reader.networksWithin()` methods can
142165
be used for this purpose.

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ record CachedConstructor<T>(
77
Constructor<T> constructor,
88
Class<?>[] parameterTypes,
99
java.lang.reflect.Type[] parameterGenericTypes,
10-
Map<String, Integer> parameterIndexes
11-
) {
12-
}
10+
Map<String, Integer> parameterIndexes,
11+
Object[] parameterDefaults
12+
) {}

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

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ private <T> Object decodeMapIntoObject(int size, Class<T> cls)
446446
Class<?>[] parameterTypes;
447447
java.lang.reflect.Type[] parameterGenericTypes;
448448
Map<String, Integer> parameterIndexes;
449+
Object[] parameterDefaults;
449450
if (cachedConstructor == null) {
450451
constructor = findConstructor(cls);
451452

@@ -454,9 +455,11 @@ private <T> Object decodeMapIntoObject(int size, Class<T> cls)
454455
parameterGenericTypes = constructor.getGenericParameterTypes();
455456

456457
parameterIndexes = new HashMap<>();
458+
parameterDefaults = new Object[constructor.getParameterCount()];
457459
var annotations = constructor.getParameterAnnotations();
458460
for (int i = 0; i < constructor.getParameterCount(); i++) {
459-
var name = getParameterName(annotations[i]);
461+
var ann = getParameterAnnotation(annotations[i]);
462+
var name = ann != null ? ann.name() : null;
460463
if (name == null) {
461464
// Fallbacks: record component name, then Java parameter name
462465
// (requires -parameters)
@@ -474,6 +477,10 @@ private <T> Object decodeMapIntoObject(int size, Class<T> cls)
474477
}
475478
}
476479
}
480+
// Prepare parsed defaults once and cache them
481+
if (ann != null && ann.useDefault()) {
482+
parameterDefaults[i] = parseDefault(ann.defaultValue(), parameterTypes[i]);
483+
}
477484
parameterIndexes.put(name, i);
478485
}
479486

@@ -483,14 +490,16 @@ private <T> Object decodeMapIntoObject(int size, Class<T> cls)
483490
constructor,
484491
parameterTypes,
485492
parameterGenericTypes,
486-
parameterIndexes
493+
parameterIndexes,
494+
parameterDefaults
487495
)
488496
);
489497
} else {
490498
constructor = cachedConstructor.constructor();
491499
parameterTypes = cachedConstructor.parameterTypes();
492500
parameterGenericTypes = cachedConstructor.parameterGenericTypes();
493501
parameterIndexes = cachedConstructor.parameterIndexes();
502+
parameterDefaults = cachedConstructor.parameterDefaults();
494503
}
495504

496505
var parameters = new Object[parameterTypes.length];
@@ -510,6 +519,13 @@ private <T> Object decodeMapIntoObject(int size, Class<T> cls)
510519
).value();
511520
}
512521

522+
// Apply cached defaults for missing parameters, if any
523+
for (int i = 0; i < parameters.length; i++) {
524+
if (parameters[i] == null && parameterDefaults[i] != null) {
525+
parameters[i] = parameterDefaults[i];
526+
}
527+
}
528+
513529
try {
514530
return constructor.newInstance(parameters);
515531
} catch (InstantiationException
@@ -583,17 +599,60 @@ private static <T> Constructor<T> findConstructor(Class<T> cls)
583599
+ "provide a record canonical constructor, or a single public constructor.");
584600
}
585601

586-
private static String getParameterName(Annotation[] annotations) {
602+
private static MaxMindDbParameter getParameterAnnotation(Annotation[] annotations) {
587603
for (var annotation : annotations) {
588604
if (!annotation.annotationType().equals(MaxMindDbParameter.class)) {
589605
continue;
590606
}
591-
var paramAnnotation = (MaxMindDbParameter) annotation;
592-
return paramAnnotation.name();
607+
return (MaxMindDbParameter) annotation;
593608
}
594609
return null;
595610
}
596611

612+
private static Object parseDefault(String value, Class<?> target) {
613+
try {
614+
if (target.equals(Boolean.TYPE) || target.equals(Boolean.class)) {
615+
return value.isEmpty() ? false : Boolean.parseBoolean(value);
616+
}
617+
if (target.equals(Byte.TYPE) || target.equals(Byte.class)) {
618+
var v = value.isEmpty() ? 0 : Integer.parseInt(value);
619+
if (v < Byte.MIN_VALUE || v > Byte.MAX_VALUE) {
620+
throw new DeserializationException(
621+
"Default value out of range for byte");
622+
}
623+
return (byte) v;
624+
}
625+
if (target.equals(Short.TYPE) || target.equals(Short.class)) {
626+
var v = value.isEmpty() ? 0 : Integer.parseInt(value);
627+
if (v < Short.MIN_VALUE || v > Short.MAX_VALUE) {
628+
throw new DeserializationException(
629+
"Default value out of range for short");
630+
}
631+
return (short) v;
632+
}
633+
if (target.equals(Integer.TYPE) || target.equals(Integer.class)) {
634+
return value.isEmpty() ? 0 : Integer.parseInt(value);
635+
}
636+
if (target.equals(Long.TYPE) || target.equals(Long.class)) {
637+
return value.isEmpty() ? 0L : Long.parseLong(value);
638+
}
639+
if (target.equals(Float.TYPE) || target.equals(Float.class)) {
640+
return value.isEmpty() ? 0.0f : Float.parseFloat(value);
641+
}
642+
if (target.equals(Double.TYPE) || target.equals(Double.class)) {
643+
return value.isEmpty() ? 0.0d : Double.parseDouble(value);
644+
}
645+
if (target.equals(String.class)) {
646+
return value;
647+
}
648+
} catch (NumberFormatException e) {
649+
throw new DeserializationException(
650+
"Invalid default '" + value + "' for type " + target.getSimpleName(), e);
651+
}
652+
throw new DeserializationException(
653+
"Defaults are only supported for primitives, boxed types, and String.");
654+
}
655+
597656
private long nextValueOffset(long offset, int numberToSkip)
598657
throws InvalidDatabaseException {
599658
if (numberToSkip == 0) {

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,16 @@
1414
* @return the name of the parameter in the MaxMind DB file
1515
*/
1616
String name();
17+
18+
/**
19+
* Whether to use a default when the value is missing in the database.
20+
*/
21+
boolean useDefault() default false;
22+
23+
/**
24+
* The default value as a string. Parsed according to the Java parameter
25+
* type (e.g., "0", "false"). If empty and {@code useDefault} is true,
26+
* the Java type's default is used (0, false, 0.0, and "" for String).
27+
*/
28+
String defaultValue() default "";
1729
}

0 commit comments

Comments
 (0)