Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dependencies {
implementation("net.ltgt.gradle:gradle-errorprone-plugin:5.1.0")
implementation("net.ltgt.gradle:gradle-nullaway-plugin:3.1.0")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.21")
implementation("org.jsonschema2pojo:jsonschema2pojo-core:1.3.3")
implementation("org.sonatype.gradle.plugins:scan-gradle-plugin:3.1.6")
implementation("ru.vyarus:gradle-animalsniffer-plugin:2.0.1")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.gradle.js2p;

import com.fasterxml.jackson.databind.JsonNode;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JMethod;
import javax.annotation.Nullable;
import org.jsonschema2pojo.AbstractAnnotator;

/**
* Annotates every generated property field and getter with {@code @Nullable}.
*
* <p>jsonschema2pojo's built-in JSR-305 support ({@code includeJsr305Annotations}) annotates
* required fields with {@code @Nonnull} and optional fields with {@code @Nullable}. The {@code
* @Nonnull} fields are never initialized (Jackson populates them reflectively), which makes NullAway
* flag them as uninitialized {@code @NonNull} fields. Since the generated getters are uniformly
* {@code @Nullable} anyway and field presence is validated at runtime by the model factories, we
* disable {@code includeJsr305Annotations} and instead annotate everything {@code @Nullable} here.
*/
public class NullableAnnotator extends AbstractAnnotator {

@Override
public void propertyField(
JFieldVar field, JDefinedClass clazz, String propertyName, JsonNode propertyNode) {
field.annotate(Nullable.class);
}

@Override
public void propertyGetter(JMethod getter, JDefinedClass clazz, String propertyName) {
getter.annotate(Nullable.class);
}

@Override
public boolean isPolymorphicDeserializationSupported(JsonNode node) {
// Defer to the composed Jackson annotator rather than vetoing polymorphic deserialization.
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.gradle.js2p;

import com.fasterxml.jackson.databind.JsonNode;
import com.sun.codemodel.ClassType;
import com.sun.codemodel.JBlock;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JPackage;
import com.sun.codemodel.JType;
import com.sun.codemodel.JVar;
import javax.annotation.Nullable;
import org.jsonschema2pojo.Schema;
import org.jsonschema2pojo.rules.ObjectRule;
import org.jsonschema2pojo.rules.RuleFactory;
import org.jsonschema2pojo.util.ParcelableHelper;
import org.jsonschema2pojo.util.ReflectionHelper;

/**
* An {@link ObjectRule} that replaces jsonschema2pojo's generated {@code toString}/{@code
* equals}/{@code hashCode} with implementations that mirror AutoValue's style.
*/
public class OtelObjectRule extends ObjectRule {

public OtelObjectRule(
RuleFactory ruleFactory,
ParcelableHelper parcelableHelper,
ReflectionHelper reflectionHelper) {
super(ruleFactory, parcelableHelper, reflectionHelper);
}

@Override
public JType apply(
String nodeName, JsonNode node, JsonNode parent, JPackage pkg, Schema schema) {
JType type = super.apply(nodeName, node, parent, pkg, schema);
if (type instanceof JDefinedClass
&& ((JDefinedClass) type).getClassType() == ClassType.CLASS) {
addValueMethods((JDefinedClass) type);
}
return type;
}

private static void addValueMethods(JDefinedClass clazz) {
JCodeModel model = clazz.owner();

addToString(clazz, model);
addHashCode(clazz, model);
addEquals(clazz, model);
}

// toString: ClassName{field1=value1, field2=value2}
private static void addToString(JDefinedClass clazz, JCodeModel model) {
JMethod toString = clazz.method(JMod.PUBLIC, model.ref(String.class), "toString");
toString.annotate(Override.class);

StringBuilder expr = new StringBuilder("return \"").append(clazz.name()).append("{\"");
boolean first = true;
for (JFieldVar field : clazz.fields().values()) {
if (isStatic(field)) {
continue;
}
expr.append(" + \"")
.append(first ? "" : ", ")
.append(field.name())
.append("=\" + ")
.append(field.name());
first = false;
}
expr.append(" + \"}\";");
toString.body().directStatement(expr.toString());
}

// equals: instanceof + cast + (this.f == null ? that.f == null : this.f.equals(that.f)) && ...
private static void addEquals(JDefinedClass clazz, JCodeModel model) {
JMethod equals = clazz.method(JMod.PUBLIC, model.BOOLEAN, "equals");
equals.annotate(Override.class);
JVar other = equals.param(model.ref(Object.class), "o");
other.annotate(Nullable.class);
JBlock body = equals.body();

body._if(other.eq(JExpr._this()))._then()._return(JExpr.TRUE);

JBlock matched = body._if(other._instanceof(clazz))._then();
matched.directStatement(clazz.name() + " that = (" + clazz.name() + ") o;");

StringBuilder comparison = new StringBuilder("return ");
boolean first = true;
for (JFieldVar field : clazz.fields().values()) {
if (isStatic(field)) {
continue;
}
String name = field.name();
comparison
.append(first ? "" : " && ")
.append("(this.").append(name).append(" == null ? that.").append(name)
.append(" == null : this.").append(name).append(".equals(that.").append(name)
.append("))");
first = false;
}
matched.directStatement(first ? "return true;" : comparison.append(";").toString());

body._return(JExpr.FALSE);
}

// hashCode: h = 1; h *= 1000003; h ^= (f == null ? 0 : f.hashCode()); ...
private static void addHashCode(JDefinedClass clazz, JCodeModel model) {
JMethod hashCode = clazz.method(JMod.PUBLIC, model.INT, "hashCode");
hashCode.annotate(Override.class);
JBlock body = hashCode.body();
JVar h = body.decl(model.INT, "h", JExpr.lit(1));

for (JFieldVar field : clazz.fields().values()) {
if (isStatic(field)) {
continue;
}
String name = field.name();
body.directStatement("h *= 1000003;");
body.directStatement("h ^= (this." + name + " == null) ? 0 : this." + name + ".hashCode();");
}
body._return(h);
}

private static boolean isStatic(JFieldVar field) {
return (field.mods().getValue() & JMod.STATIC) == JMod.STATIC;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.gradle.js2p;

import com.sun.codemodel.JPackage;
import com.sun.codemodel.JType;
import org.jsonschema2pojo.rules.ObjectRule;
import org.jsonschema2pojo.rules.Rule;
import org.jsonschema2pojo.rules.RuleFactory;
import org.jsonschema2pojo.util.ParcelableHelper;

/**
* Custom {@link RuleFactory} that swaps in {@link OtelObjectRule} so generated POJOs get
* AutoValue-style {@code toString}/{@code equals}/{@code hashCode} implementations instead of
* jsonschema2pojo's defaults.
*
* <p>Referenced from {@code sdk-extensions/declarative-config/build.gradle.kts} via {@code
* jsonSchema2Pojo.customRuleFactory}.
*/
public class OtelRuleFactory extends RuleFactory {

@Override
public Rule<JPackage, JType> getObjectRule() {
return new OtelObjectRule(this, new ParcelableHelper(), getReflectionHelper());
}
}
34 changes: 16 additions & 18 deletions sdk-extensions/declarative-config/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ dependencies {
testImplementation("com.linecorp.armeria:armeria-junit5")
//
testImplementation("com.google.guava:guava-testlib")
testImplementation("nl.jqno.equalsverifier:equalsverifier")
}

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

// Include @Nullable annotation. Note: jsonSchema2Pojo will not add @Nullable annotations on getters
// so we add these in syncPojoModelsToSrc.
includeJsr305Annotations = true
// Annotate fields/getters via NullableAnnotator instead of jsonschema2pojo's JSR-305 support. The
// built-in support adds @Nonnull to required fields, which NullAway flags as uninitialized (Jackson
// populates them reflectively). NullableAnnotator annotates everything @Nullable instead, matching
// the getters and letting the model factories validate required-ness at runtime.
includeJsr305Annotations = false
setCustomAnnotator(io.opentelemetry.gradle.js2p.NullableAnnotator::class.java)

// Generate AutoValue-style toString/equals/hashCode via OtelObjectRule (wired through
// OtelRuleFactory) rather than jsonschema2pojo's defaults. The defaults use a commons-style
// toString (System.identityHashCode) and compare boxed fields with == (tripping ErrorProne's
// BoxedPrimitiveEquality). Disable the built-in generation so the custom rule can supply its own.
includeToString = false
includeHashcodeAndEquals = false
setCustomRuleFactory(io.opentelemetry.gradle.js2p.OtelRuleFactory::class.java)

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

Expand Down
Loading
Loading