Skip to content

Commit 77eb68d

Browse files
authored
Adjust declarative config pojo toString, hashCode, equals to match autovalue (#8526)
1 parent 1704e22 commit 77eb68d

125 files changed

Lines changed: 3444 additions & 5473 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

buildSrc/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ dependencies {
4949
implementation("net.ltgt.gradle:gradle-errorprone-plugin:5.1.0")
5050
implementation("net.ltgt.gradle:gradle-nullaway-plugin:3.1.0")
5151
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.21")
52+
implementation("org.jsonschema2pojo:jsonschema2pojo-core:1.3.3")
5253
implementation("org.sonatype.gradle.plugins:scan-gradle-plugin:3.1.6")
5354
implementation("ru.vyarus:gradle-animalsniffer-plugin:2.0.1")
5455
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.gradle.js2p;
7+
8+
import com.fasterxml.jackson.databind.JsonNode;
9+
import com.sun.codemodel.JDefinedClass;
10+
import com.sun.codemodel.JFieldVar;
11+
import com.sun.codemodel.JMethod;
12+
import javax.annotation.Nullable;
13+
import org.jsonschema2pojo.AbstractAnnotator;
14+
15+
/**
16+
* Annotates every generated property field and getter with {@code @Nullable}.
17+
*
18+
* <p>jsonschema2pojo's built-in JSR-305 support ({@code includeJsr305Annotations}) annotates
19+
* required fields with {@code @Nonnull} and optional fields with {@code @Nullable}. The {@code
20+
* @Nonnull} fields are never initialized (Jackson populates them reflectively), which makes NullAway
21+
* flag them as uninitialized {@code @NonNull} fields. Since the generated getters are uniformly
22+
* {@code @Nullable} anyway and field presence is validated at runtime by the model factories, we
23+
* disable {@code includeJsr305Annotations} and instead annotate everything {@code @Nullable} here.
24+
*/
25+
public class NullableAnnotator extends AbstractAnnotator {
26+
27+
@Override
28+
public void propertyField(
29+
JFieldVar field, JDefinedClass clazz, String propertyName, JsonNode propertyNode) {
30+
field.annotate(Nullable.class);
31+
}
32+
33+
@Override
34+
public void propertyGetter(JMethod getter, JDefinedClass clazz, String propertyName) {
35+
getter.annotate(Nullable.class);
36+
}
37+
38+
@Override
39+
public boolean isPolymorphicDeserializationSupported(JsonNode node) {
40+
// Defer to the composed Jackson annotator rather than vetoing polymorphic deserialization.
41+
return true;
42+
}
43+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.gradle.js2p;
7+
8+
import com.fasterxml.jackson.databind.JsonNode;
9+
import com.sun.codemodel.ClassType;
10+
import com.sun.codemodel.JBlock;
11+
import com.sun.codemodel.JCodeModel;
12+
import com.sun.codemodel.JDefinedClass;
13+
import com.sun.codemodel.JExpr;
14+
import com.sun.codemodel.JFieldVar;
15+
import com.sun.codemodel.JMethod;
16+
import com.sun.codemodel.JMod;
17+
import com.sun.codemodel.JPackage;
18+
import com.sun.codemodel.JType;
19+
import com.sun.codemodel.JVar;
20+
import javax.annotation.Nullable;
21+
import org.jsonschema2pojo.Schema;
22+
import org.jsonschema2pojo.rules.ObjectRule;
23+
import org.jsonschema2pojo.rules.RuleFactory;
24+
import org.jsonschema2pojo.util.ParcelableHelper;
25+
import org.jsonschema2pojo.util.ReflectionHelper;
26+
27+
/**
28+
* An {@link ObjectRule} that replaces jsonschema2pojo's generated {@code toString}/{@code
29+
* equals}/{@code hashCode} with implementations that mirror AutoValue's style.
30+
*/
31+
public class OtelObjectRule extends ObjectRule {
32+
33+
public OtelObjectRule(
34+
RuleFactory ruleFactory,
35+
ParcelableHelper parcelableHelper,
36+
ReflectionHelper reflectionHelper) {
37+
super(ruleFactory, parcelableHelper, reflectionHelper);
38+
}
39+
40+
@Override
41+
public JType apply(
42+
String nodeName, JsonNode node, JsonNode parent, JPackage pkg, Schema schema) {
43+
JType type = super.apply(nodeName, node, parent, pkg, schema);
44+
if (type instanceof JDefinedClass
45+
&& ((JDefinedClass) type).getClassType() == ClassType.CLASS) {
46+
addValueMethods((JDefinedClass) type);
47+
}
48+
return type;
49+
}
50+
51+
private static void addValueMethods(JDefinedClass clazz) {
52+
JCodeModel model = clazz.owner();
53+
54+
addToString(clazz, model);
55+
addHashCode(clazz, model);
56+
addEquals(clazz, model);
57+
}
58+
59+
// toString: ClassName{field1=value1, field2=value2}
60+
private static void addToString(JDefinedClass clazz, JCodeModel model) {
61+
JMethod toString = clazz.method(JMod.PUBLIC, model.ref(String.class), "toString");
62+
toString.annotate(Override.class);
63+
64+
StringBuilder expr = new StringBuilder("return \"").append(clazz.name()).append("{\"");
65+
boolean first = true;
66+
for (JFieldVar field : clazz.fields().values()) {
67+
if (isStatic(field)) {
68+
continue;
69+
}
70+
expr.append(" + \"")
71+
.append(first ? "" : ", ")
72+
.append(field.name())
73+
.append("=\" + ")
74+
.append(field.name());
75+
first = false;
76+
}
77+
expr.append(" + \"}\";");
78+
toString.body().directStatement(expr.toString());
79+
}
80+
81+
// equals: instanceof + cast + (this.f == null ? that.f == null : this.f.equals(that.f)) && ...
82+
private static void addEquals(JDefinedClass clazz, JCodeModel model) {
83+
JMethod equals = clazz.method(JMod.PUBLIC, model.BOOLEAN, "equals");
84+
equals.annotate(Override.class);
85+
JVar other = equals.param(model.ref(Object.class), "o");
86+
other.annotate(Nullable.class);
87+
JBlock body = equals.body();
88+
89+
body._if(other.eq(JExpr._this()))._then()._return(JExpr.TRUE);
90+
91+
JBlock matched = body._if(other._instanceof(clazz))._then();
92+
matched.directStatement(clazz.name() + " that = (" + clazz.name() + ") o;");
93+
94+
StringBuilder comparison = new StringBuilder("return ");
95+
boolean first = true;
96+
for (JFieldVar field : clazz.fields().values()) {
97+
if (isStatic(field)) {
98+
continue;
99+
}
100+
String name = field.name();
101+
comparison
102+
.append(first ? "" : " && ")
103+
.append("(this.").append(name).append(" == null ? that.").append(name)
104+
.append(" == null : this.").append(name).append(".equals(that.").append(name)
105+
.append("))");
106+
first = false;
107+
}
108+
matched.directStatement(first ? "return true;" : comparison.append(";").toString());
109+
110+
body._return(JExpr.FALSE);
111+
}
112+
113+
// hashCode: h = 1; h *= 1000003; h ^= (f == null ? 0 : f.hashCode()); ...
114+
private static void addHashCode(JDefinedClass clazz, JCodeModel model) {
115+
JMethod hashCode = clazz.method(JMod.PUBLIC, model.INT, "hashCode");
116+
hashCode.annotate(Override.class);
117+
JBlock body = hashCode.body();
118+
JVar h = body.decl(model.INT, "h", JExpr.lit(1));
119+
120+
for (JFieldVar field : clazz.fields().values()) {
121+
if (isStatic(field)) {
122+
continue;
123+
}
124+
String name = field.name();
125+
body.directStatement("h *= 1000003;");
126+
body.directStatement("h ^= (this." + name + " == null) ? 0 : this." + name + ".hashCode();");
127+
}
128+
body._return(h);
129+
}
130+
131+
private static boolean isStatic(JFieldVar field) {
132+
return (field.mods().getValue() & JMod.STATIC) == JMod.STATIC;
133+
}
134+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.gradle.js2p;
7+
8+
import com.sun.codemodel.JPackage;
9+
import com.sun.codemodel.JType;
10+
import org.jsonschema2pojo.rules.ObjectRule;
11+
import org.jsonschema2pojo.rules.Rule;
12+
import org.jsonschema2pojo.rules.RuleFactory;
13+
import org.jsonschema2pojo.util.ParcelableHelper;
14+
15+
/**
16+
* Custom {@link RuleFactory} that swaps in {@link OtelObjectRule} so generated POJOs get
17+
* AutoValue-style {@code toString}/{@code equals}/{@code hashCode} implementations instead of
18+
* jsonschema2pojo's defaults.
19+
*
20+
* <p>Referenced from {@code sdk-extensions/declarative-config/build.gradle.kts} via {@code
21+
* jsonSchema2Pojo.customRuleFactory}.
22+
*/
23+
public class OtelRuleFactory extends RuleFactory {
24+
25+
@Override
26+
public Rule<JPackage, JType> getObjectRule() {
27+
return new OtelObjectRule(this, new ParcelableHelper(), getReflectionHelper());
28+
}
29+
}

sdk-extensions/declarative-config/build.gradle.kts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ dependencies {
4747
testImplementation("com.linecorp.armeria:armeria-junit5")
4848
//
4949
testImplementation("com.google.guava:guava-testlib")
50+
testImplementation("nl.jqno.equalsverifier:equalsverifier")
5051
}
5152

5253
// The following tasks download the JSON Schema files from open-telemetry/opentelemetry-configuration,
@@ -100,9 +101,20 @@ jsonSchema2Pojo {
100101
// Clear old source files to avoid contaminated source dir when updating
101102
removeOldOutput = true
102103

103-
// Include @Nullable annotation. Note: jsonSchema2Pojo will not add @Nullable annotations on getters
104-
// so we add these in syncPojoModelsToSrc.
105-
includeJsr305Annotations = true
104+
// Annotate fields/getters via NullableAnnotator instead of jsonschema2pojo's JSR-305 support. The
105+
// built-in support adds @Nonnull to required fields, which NullAway flags as uninitialized (Jackson
106+
// populates them reflectively). NullableAnnotator annotates everything @Nullable instead, matching
107+
// the getters and letting the model factories validate required-ness at runtime.
108+
includeJsr305Annotations = false
109+
setCustomAnnotator(io.opentelemetry.gradle.js2p.NullableAnnotator::class.java)
110+
111+
// Generate AutoValue-style toString/equals/hashCode via OtelObjectRule (wired through
112+
// OtelRuleFactory) rather than jsonschema2pojo's defaults. The defaults use a commons-style
113+
// toString (System.identityHashCode) and compare boxed fields with == (tripping ErrorProne's
114+
// BoxedPrimitiveEquality). Disable the built-in generation so the custom rule can supply its own.
115+
includeToString = false
116+
includeHashcodeAndEquals = false
117+
setCustomRuleFactory(io.opentelemetry.gradle.js2p.OtelRuleFactory::class.java)
106118

107119
// Prefer builders to setters
108120
includeSetters = false
@@ -146,22 +158,8 @@ val syncPojoModelsToSrc by tasks.registering(Copy::class) {
146158
it
147159
// Shorten FQCNs for same-package references generated by jsonschema2pojo
148160
.replace("io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.", "")
149-
// Remove @Nullable annotation so it can be deterministically added later
150-
.replace("import javax.annotation.Nullable;\n", "")
151161
// Replace java 9+ @Generated annotation with java 8 version, add @Nullable annotation
152-
.replace("import javax.annotation.processing.Generated;", "import javax.annotation.Nullable;\nimport javax.annotation.Generated;")
153-
// Add @SuppressWarnings annotations for issues inherent in jsonschema2pojo-generated code:
154-
// "rawtypes" - raw types used in builders
155-
// "NullAway" - uninitialized @NonNull fields on Jackson-deserialized POJOs
156-
// TODO(jack-berg): investigate jsonschema2pojo config to avoid @Nonnull on fields / generate initializing constructors
157-
// "BoxedPrimitiveEquality" - == comparison of boxed primitives in generated equals()
158-
// TODO(jack-berg): investigate jsonschema2pojo config for alternative equals implementation that avoids boxed primitives comparison
159-
.replace(
160-
"@Generated(\"jsonschema2pojo\")",
161-
"@Generated(\"jsonschema2pojo\")\n@SuppressWarnings({\"NullAway\", \"rawtypes\", \"BoxedPrimitiveEquality\"})"
162-
)
163-
// Add @Nullable annotations to all getters (except getAdditionalProperties which is non-null)
164-
.replace("( *)public (.+) get(?!AdditionalProperties)([a-zA-Z]*)".toRegex(), "$1@Nullable\n$1public $2 get$3")
162+
.replace("import javax.annotation.processing.Generated;", "import javax.annotation.Generated;")
165163
}
166164
}
167165

0 commit comments

Comments
 (0)