Skip to content

Commit ab8b900

Browse files
feat: enhance code generator Javadoc and null-safety annotations
Improves the metaschema-maven-plugin code generator to produce binding classes with complete Javadoc and null-safety annotations. Code Generator Changes: - Fix Javadoc quote issue by using literal format instead of $S - Add Javadoc to constructor generation (no-arg and data constructors) - Add Javadoc to getter/setter generation with @param/@return tags - Add @NonNull/@nullable annotations based on required attribute - Add isRequired() and isCollectionType() methods to type info classes - Add lazy initialization for collection getters (LinkedList/LinkedHashMap) Regenerated Files: - metaschema-testing binding classes regenerated with improvements Closes #568, #571, #575
1 parent a6330f7 commit ab8b900

20 files changed

Lines changed: 1082 additions & 69 deletions

File tree

.claude/rules/unit-testing.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,21 @@ Every test class should prioritize edge case coverage over happy paths:
3939
3. Add tests before completing the work
4040

4141
Use the `unit-test-writing` skill for the detailed workflow.
42+
43+
## Legacy Code Coverage
44+
45+
**When improving or refactoring existing classes, add tests for legacy functionality.**
46+
47+
This ensures:
48+
- Existing behavior is documented through tests
49+
- Regressions are caught if refactoring breaks something
50+
- Test coverage improves incrementally over time
51+
52+
### Process
53+
54+
1. **Before changes**: Write tests capturing current behavior of code you're touching
55+
2. **Verify tests pass**: Confirms tests accurately reflect existing behavior
56+
3. **Make improvements**: Refactor or enhance the code
57+
4. **Verify tests still pass**: Confirms behavioral equivalence
58+
59+
This approach builds test coverage organically as the codebase evolves.

databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/AbstractModelInstanceTypeInfo.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@
2121
import gov.nist.secauto.metaschema.databind.codegen.typeinfo.def.IAssemblyDefinitionTypeInfo;
2222
import gov.nist.secauto.metaschema.databind.model.annotations.GroupAs;
2323

24+
import java.util.LinkedHashMap;
2425
import java.util.LinkedHashSet;
26+
import java.util.LinkedList;
2527
import java.util.List;
2628
import java.util.Map;
2729
import java.util.Set;
2830

2931
import edu.umd.cs.findbugs.annotations.NonNull;
32+
import edu.umd.cs.findbugs.annotations.Nullable;
3033

3134
abstract class AbstractModelInstanceTypeInfo<INSTANCE extends IModelInstanceAbsolute>
3235
extends AbstractInstanceTypeInfo<INSTANCE, IAssemblyDefinitionTypeInfo>
@@ -74,6 +77,29 @@ public TypeName getJavaFieldType() {
7477
return retval;
7578
}
7679

80+
@Override
81+
public boolean isCollectionType() {
82+
IModelInstanceAbsolute instance = getInstance();
83+
int maxOccurs = instance.getMaxOccurs();
84+
return maxOccurs == -1 || maxOccurs > 1;
85+
}
86+
87+
@Nullable
88+
@Override
89+
public Class<?> getCollectionImplementationClass() {
90+
IModelInstanceAbsolute instance = getInstance();
91+
int maxOccurs = instance.getMaxOccurs();
92+
if (maxOccurs == -1 || maxOccurs > 1) {
93+
// This is a collection - return the appropriate implementation class
94+
if (JsonGroupAsBehavior.KEYED.equals(instance.getJsonGroupAsBehavior())) {
95+
return LinkedHashMap.class;
96+
}
97+
return LinkedList.class;
98+
}
99+
// Not a collection
100+
return null;
101+
}
102+
77103
@NonNull
78104
protected abstract AnnotationSpec.Builder newBindingAnnotation();
79105

databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/AbstractNamedModelInstanceTypeInfo.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@ public AbstractNamedModelInstanceTypeInfo(
4242
super(instance, parentDefinition);
4343
}
4444

45+
@Override
46+
public boolean isRequired() {
47+
INSTANCE instance = getInstance();
48+
// A model instance is required if minOccurs >= 1 AND it's a single item (not a
49+
// collection)
50+
return instance.getMinOccurs() >= 1 && instance.getMaxOccurs() == 1;
51+
}
52+
53+
@Override
54+
public boolean isCollectionType() {
55+
INSTANCE instance = getInstance();
56+
// A collection has maxOccurs > 1 or unbounded (-1)
57+
return instance.getMaxOccurs() == -1 || instance.getMaxOccurs() > 1;
58+
}
59+
4560
@NonNull
4661
@Override
4762
public String getBaseName() {

databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/AbstractPropertyTypeInfo.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package gov.nist.secauto.metaschema.databind.codegen.typeinfo;
77

8+
import com.squareup.javapoet.AnnotationSpec;
89
import com.squareup.javapoet.FieldSpec;
910
import com.squareup.javapoet.MethodSpec;
1011
import com.squareup.javapoet.ParameterSpec;
@@ -21,6 +22,7 @@
2122
import javax.lang.model.element.Modifier;
2223

2324
import edu.umd.cs.findbugs.annotations.NonNull;
25+
import edu.umd.cs.findbugs.annotations.Nullable;
2426

2527
public abstract class AbstractPropertyTypeInfo<PARENT extends IDefinitionTypeInfo>
2628
extends AbstractTypeInfo<PARENT>
@@ -60,20 +62,39 @@ protected void buildExtraMethods(
6062
TypeName javaFieldType = getJavaFieldType();
6163
String propertyName = getPropertyName();
6264
{
65+
Class<?> collectionImplClass = getCollectionImplementationClass();
66+
// Collections are always @NonNull (lazy initialized), otherwise based on
67+
// isRequired()
68+
Class<?> nullAnnotation = collectionImplClass != null || isRequired() ? NonNull.class : Nullable.class;
6369
MethodSpec.Builder method = MethodSpec.methodBuilder("get" + propertyName)
6470
.returns(javaFieldType)
71+
.addAnnotation(AnnotationSpec.builder(nullAnnotation).build())
6572
.addModifiers(Modifier.PUBLIC);
6673
assert method != null;
74+
buildGetterJavadoc(method);
75+
76+
if (collectionImplClass != null) {
77+
// Use lazy initialization for collections
78+
method.beginControlFlow("if ($N == null)", fieldBuilder)
79+
.addStatement("$N = new $T<>()", fieldBuilder, collectionImplClass)
80+
.endControlFlow();
81+
}
6782
method.addStatement("return $N", fieldBuilder);
6883
typeBuilder.addMethod(method.build());
6984
}
7085

7186
{
72-
ParameterSpec valueParam = ParameterSpec.builder(javaFieldType, "value").build();
87+
// Add null-safety annotation to setter parameter
88+
// Required properties get @NonNull, optional properties get @Nullable
89+
ParameterSpec.Builder paramBuilder = ParameterSpec.builder(javaFieldType, "value");
90+
Class<?> paramAnnotation = isRequired() ? NonNull.class : Nullable.class;
91+
paramBuilder.addAnnotation(AnnotationSpec.builder(paramAnnotation).build());
92+
ParameterSpec valueParam = paramBuilder.build();
7393
MethodSpec.Builder method = MethodSpec.methodBuilder("set" + propertyName)
7494
.addModifiers(Modifier.PUBLIC)
7595
.addParameter(valueParam);
7696
assert method != null;
97+
buildSetterJavadoc(method, "value");
7798
method.addStatement("$N = $N", fieldBuilder, valueParam);
7899
typeBuilder.addMethod(method.build());
79100
}

databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/DefaultMetaschemaClassFactory.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,11 +386,16 @@ protected TypeSpec.Builder newClassBuilder(
386386

387387
builder.addMethod(MethodSpec.constructorBuilder()
388388
.addModifiers(Modifier.PUBLIC)
389+
.addJavadoc("Constructs a new {@code $L} instance with no metadata.\n", typeInfo.getClassName())
389390
.addStatement("this(null)")
390391
.build());
391392

392393
builder.addMethod(MethodSpec.constructorBuilder()
393394
.addModifiers(Modifier.PUBLIC)
395+
.addJavadoc("Constructs a new {@code $L} instance with the specified metadata.\n", typeInfo.getClassName())
396+
.addJavadoc("\n")
397+
.addJavadoc("@param data\n")
398+
.addJavadoc(" the metaschema data, or {@code null} if none\n")
394399
.addParameter(IMetaschemaData.class, "data")
395400
.addStatement("this.$N = $N", "__metaschemaData", "data")
396401
.build());

databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/FlagInstanceTypeInfoImpl.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ public String getBaseName() {
4242
return getInstance().getEffectiveName();
4343
}
4444

45+
@Override
46+
public boolean isRequired() {
47+
return getInstance().isRequired();
48+
}
49+
4550
@Override
4651
public TypeName getJavaFieldType() {
4752
return ObjectUtils.notNull(ClassName.get(getInstance().getDefinition().getJavaTypeAdapter().getJavaClass()));

databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/INamedInstanceTypeInfo.java

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66
package gov.nist.secauto.metaschema.databind.codegen.typeinfo;
77

88
import com.squareup.javapoet.FieldSpec;
9+
import com.squareup.javapoet.MethodSpec;
910

1011
import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine;
1112
import gov.nist.secauto.metaschema.core.model.INamedInstance;
1213

14+
import edu.umd.cs.findbugs.annotations.NonNull;
15+
1316
public interface INamedInstanceTypeInfo extends IInstanceTypeInfo {
1417
@Override
1518
INamedInstance getInstance();
@@ -18,7 +21,60 @@ public interface INamedInstanceTypeInfo extends IInstanceTypeInfo {
1821
default void buildFieldJavadoc(FieldSpec.Builder builder) {
1922
MarkupLine description = getInstance().getEffectiveDescription();
2023
if (description != null) {
21-
builder.addJavadoc("$S", description.toHtml());
24+
builder.addJavadoc("$L\n", description.toHtml());
25+
}
26+
}
27+
28+
@Override
29+
default void buildGetterJavadoc(@NonNull MethodSpec.Builder builder) {
30+
MarkupLine description = getInstance().getEffectiveDescription();
31+
String formalName = getInstance().getEffectiveFormalName();
32+
String propertyName = getInstance().getEffectiveName();
33+
34+
// Use formal name if available, otherwise property name
35+
if (formalName != null) {
36+
builder.addJavadoc("Get the $L.\n", TypeInfoUtils.toLowerFirstChar(formalName));
37+
} else {
38+
builder.addJavadoc("Get the {@code $L} property.\n", propertyName);
39+
}
40+
41+
// Add description as a second paragraph if available
42+
if (description != null) {
43+
builder.addJavadoc("\n");
44+
builder.addJavadoc("<p>\n");
45+
builder.addJavadoc("$L\n", description.toHtml());
46+
}
47+
48+
builder.addJavadoc("\n");
49+
if (isRequired()) {
50+
builder.addJavadoc("@return the $L value\n", propertyName);
51+
} else {
52+
builder.addJavadoc("@return the $L value, or {@code null} if not set\n", propertyName);
53+
}
54+
}
55+
56+
@Override
57+
default void buildSetterJavadoc(@NonNull MethodSpec.Builder builder, @NonNull String paramName) {
58+
MarkupLine description = getInstance().getEffectiveDescription();
59+
String formalName = getInstance().getEffectiveFormalName();
60+
String propertyName = getInstance().getEffectiveName();
61+
62+
// Use formal name if available, otherwise property name
63+
if (formalName != null) {
64+
builder.addJavadoc("Set the $L.\n", TypeInfoUtils.toLowerFirstChar(formalName));
65+
} else {
66+
builder.addJavadoc("Set the {@code $L} property.\n", propertyName);
67+
}
68+
69+
// Add description as a second paragraph if available
70+
if (description != null) {
71+
builder.addJavadoc("\n");
72+
builder.addJavadoc("<p>\n");
73+
builder.addJavadoc("$L\n", description.toHtml());
2274
}
75+
76+
builder.addJavadoc("\n");
77+
builder.addJavadoc("@param $L\n", paramName);
78+
builder.addJavadoc(" the $L value to set\n", propertyName);
2379
}
2480
}

databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/typeinfo/INamedModelInstanceTypeInfo.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
package gov.nist.secauto.metaschema.databind.codegen.typeinfo;
77

88
import com.squareup.javapoet.AnnotationSpec;
9+
import com.squareup.javapoet.FieldSpec;
10+
import com.squareup.javapoet.MethodSpec;
911

12+
import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine;
1013
import gov.nist.secauto.metaschema.core.model.INamedModelInstanceAbsolute;
1114

1215
import edu.umd.cs.findbugs.annotations.NonNull;
@@ -24,4 +27,67 @@ public interface INamedModelInstanceTypeInfo extends IModelInstanceTypeInfo {
2427
default void buildBindingAnnotationCommon(@NonNull AnnotationSpec.Builder annotation) {
2528
TypeInfoUtils.buildCommonBindingAnnotationValues(getInstance(), annotation);
2629
}
30+
31+
@Override
32+
default void buildFieldJavadoc(@NonNull FieldSpec.Builder builder) {
33+
MarkupLine description = getInstance().getEffectiveDescription();
34+
if (description != null) {
35+
builder.addJavadoc("$L\n", description.toHtml());
36+
}
37+
}
38+
39+
@Override
40+
default void buildGetterJavadoc(@NonNull MethodSpec.Builder builder) {
41+
MarkupLine description = getInstance().getEffectiveDescription();
42+
String formalName = getInstance().getEffectiveFormalName();
43+
String propertyName = getInstance().getEffectiveName();
44+
45+
// Use formal name if available, otherwise property name
46+
if (formalName != null) {
47+
builder.addJavadoc("Get the $L.\n", TypeInfoUtils.toLowerFirstChar(formalName));
48+
} else {
49+
builder.addJavadoc("Get the {@code $L} property.\n", propertyName);
50+
}
51+
52+
// Add description as a second paragraph if available
53+
if (description != null) {
54+
builder.addJavadoc("\n");
55+
builder.addJavadoc("<p>\n");
56+
builder.addJavadoc("$L\n", description.toHtml());
57+
}
58+
59+
builder.addJavadoc("\n");
60+
// Collections are always @NonNull (lazy initialized), required singles are
61+
// @NonNull
62+
if (isRequired() || isCollectionType()) {
63+
builder.addJavadoc("@return the $L value\n", propertyName);
64+
} else {
65+
builder.addJavadoc("@return the $L value, or {@code null} if not set\n", propertyName);
66+
}
67+
}
68+
69+
@Override
70+
default void buildSetterJavadoc(@NonNull MethodSpec.Builder builder, @NonNull String paramName) {
71+
MarkupLine description = getInstance().getEffectiveDescription();
72+
String formalName = getInstance().getEffectiveFormalName();
73+
String propertyName = getInstance().getEffectiveName();
74+
75+
// Use formal name if available, otherwise property name
76+
if (formalName != null) {
77+
builder.addJavadoc("Set the $L.\n", TypeInfoUtils.toLowerFirstChar(formalName));
78+
} else {
79+
builder.addJavadoc("Set the {@code $L} property.\n", propertyName);
80+
}
81+
82+
// Add description as a second paragraph if available
83+
if (description != null) {
84+
builder.addJavadoc("\n");
85+
builder.addJavadoc("<p>\n");
86+
builder.addJavadoc("$L\n", description.toHtml());
87+
}
88+
89+
builder.addJavadoc("\n");
90+
builder.addJavadoc("@param $L\n", paramName);
91+
builder.addJavadoc(" the $L value to set\n", propertyName);
92+
}
2793
}

0 commit comments

Comments
 (0)