diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/CovariantListExampleNoStaticFactory.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/CovariantListExampleNoStaticFactory.java new file mode 100644 index 000000000..cd81024da --- /dev/null +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/CovariantListExampleNoStaticFactory.java @@ -0,0 +1,204 @@ +package com.palantir.product; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.palantir.conjure.java.lib.internal.ConjureCollections; +import com.palantir.logsafe.Preconditions; +import com.palantir.logsafe.SafeArg; +import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.processing.Generated; +import test.api.ExampleExternalReference; + +@JsonDeserialize(builder = CovariantListExampleNoStaticFactory.Builder.class) +@Generated("com.palantir.conjure.java.types.BeanGenerator") +public final class CovariantListExampleNoStaticFactory { + private final List items; + + private final List externalItems; + + private final Optional optionalField; + + private int memoizedHashCode; + + private CovariantListExampleNoStaticFactory( + List items, List externalItems, Optional optionalField) { + validateFields(items, externalItems, optionalField); + this.items = Collections.unmodifiableList(items); + this.externalItems = Collections.unmodifiableList(externalItems); + this.optionalField = optionalField; + } + + @JsonProperty("items") + public List getItems() { + return this.items; + } + + @JsonProperty("externalItems") + public List getExternalItems() { + return this.externalItems; + } + + @JsonProperty("optionalField") + public Optional getOptionalField() { + return this.optionalField; + } + + @Override + public boolean equals(@Nullable Object other) { + return this == other + || (other instanceof CovariantListExampleNoStaticFactory + && equalTo((CovariantListExampleNoStaticFactory) other)); + } + + private boolean equalTo(CovariantListExampleNoStaticFactory other) { + if (this.memoizedHashCode != 0 + && other.memoizedHashCode != 0 + && this.memoizedHashCode != other.memoizedHashCode) { + return false; + } + return this.items.equals(other.items) + && this.externalItems.equals(other.externalItems) + && this.optionalField.equals(other.optionalField); + } + + @Override + public int hashCode() { + int result = memoizedHashCode; + if (result == 0) { + int hash = 1; + hash = 31 * hash + this.items.hashCode(); + hash = 31 * hash + this.externalItems.hashCode(); + hash = 31 * hash + this.optionalField.hashCode(); + result = hash; + memoizedHashCode = result; + } + return result; + } + + @Override + public String toString() { + return "CovariantListExampleNoStaticFactory{items: " + items + ", externalItems: " + externalItems + + ", optionalField: " + optionalField + '}'; + } + + private static void validateFields( + List items, List externalItems, Optional optionalField) { + List missingFields = null; + missingFields = addFieldIfMissing(missingFields, items, "items"); + missingFields = addFieldIfMissing(missingFields, externalItems, "externalItems"); + missingFields = addFieldIfMissing(missingFields, optionalField, "optionalField"); + if (missingFields != null) { + throw new SafeIllegalArgumentException( + "Some required fields have not been set", SafeArg.of("missingFields", missingFields)); + } + } + + private static List addFieldIfMissing(List prev, Object fieldValue, String fieldName) { + List missingFields = prev; + if (fieldValue == null) { + if (missingFields == null) { + missingFields = new ArrayList<>(3); + } + missingFields.add(fieldName); + } + return missingFields; + } + + public static Builder builder() { + return new Builder(); + } + + @Generated("com.palantir.conjure.java.types.BeanBuilderGenerator") + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + boolean _buildInvoked; + + private List items = new ArrayList<>(); + + private List externalItems = new ArrayList<>(); + + private Optional optionalField = Optional.empty(); + + private Builder() {} + + public Builder from(CovariantListExampleNoStaticFactory other) { + checkNotBuilt(); + items(other.getItems()); + externalItems(other.getExternalItems()); + optionalField(other.getOptionalField()); + return this; + } + + @JsonSetter(value = "items", nulls = Nulls.SKIP) + public Builder items(@Nonnull Iterable items) { + checkNotBuilt(); + this.items = ConjureCollections.newArrayList(Preconditions.checkNotNull(items, "items cannot be null")); + return this; + } + + public Builder addAllItems(@Nonnull Iterable items) { + checkNotBuilt(); + ConjureCollections.addAll(this.items, Preconditions.checkNotNull(items, "items cannot be null")); + return this; + } + + public Builder items(Object items) { + checkNotBuilt(); + this.items.add(items); + return this; + } + + @JsonSetter(value = "externalItems", nulls = Nulls.SKIP) + public Builder externalItems(@Nonnull Iterable externalItems) { + checkNotBuilt(); + this.externalItems = ConjureCollections.newArrayList( + Preconditions.checkNotNull(externalItems, "externalItems cannot be null")); + return this; + } + + public Builder addAllExternalItems(@Nonnull Iterable externalItems) { + checkNotBuilt(); + ConjureCollections.addAll( + this.externalItems, Preconditions.checkNotNull(externalItems, "externalItems cannot be null")); + return this; + } + + public Builder externalItems(ExampleExternalReference externalItems) { + checkNotBuilt(); + this.externalItems.add(externalItems); + return this; + } + + @JsonSetter(value = "optionalField", nulls = Nulls.SKIP) + public Builder optionalField(@Nonnull Optional optionalField) { + checkNotBuilt(); + this.optionalField = Preconditions.checkNotNull(optionalField, "optionalField cannot be null"); + return this; + } + + public Builder optionalField(@Nonnull String optionalField) { + checkNotBuilt(); + this.optionalField = Optional.of(Preconditions.checkNotNull(optionalField, "optionalField cannot be null")); + return this; + } + + public CovariantListExampleNoStaticFactory build() { + checkNotBuilt(); + this._buildInvoked = true; + return new CovariantListExampleNoStaticFactory(items, externalItems, optionalField); + } + + private void checkNotBuilt() { + Preconditions.checkState(!_buildInvoked, "Build has already been called"); + } + } +} diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/EmptyExampleNoStaticFactory.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/EmptyExampleNoStaticFactory.java new file mode 100644 index 000000000..e4968cf9a --- /dev/null +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/EmptyExampleNoStaticFactory.java @@ -0,0 +1,31 @@ +package com.palantir.product; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.palantir.logsafe.Safe; +import javax.annotation.processing.Generated; + +/** + * There are no fields in this type. A static factory method (of) should be generated. + */ +@Safe +@JsonSerialize +@JsonIgnoreProperties(ignoreUnknown = true) +@Generated("com.palantir.conjure.java.types.BeanGenerator") +public final class EmptyExampleNoStaticFactory { + private static final EmptyExampleNoStaticFactory INSTANCE = new EmptyExampleNoStaticFactory(); + + private EmptyExampleNoStaticFactory() {} + + @Override + @Safe + public String toString() { + return "EmptyExampleNoStaticFactory{}"; + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static EmptyExampleNoStaticFactory of() { + return INSTANCE; + } +} diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/StringExampleNoStaticFactory.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/StringExampleNoStaticFactory.java new file mode 100644 index 000000000..1ea632b10 --- /dev/null +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/StringExampleNoStaticFactory.java @@ -0,0 +1,107 @@ +package com.palantir.product; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.palantir.logsafe.Preconditions; +import com.palantir.logsafe.SafeArg; +import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.processing.Generated; + +@JsonDeserialize(builder = StringExampleNoStaticFactory.Builder.class) +@Generated("com.palantir.conjure.java.types.BeanGenerator") +public final class StringExampleNoStaticFactory { + private final String string; + + private StringExampleNoStaticFactory(String string) { + validateFields(string); + this.string = string; + } + + @JsonProperty("string") + public String getString() { + return this.string; + } + + @Override + public boolean equals(@Nullable Object other) { + return this == other + || (other instanceof StringExampleNoStaticFactory && equalTo((StringExampleNoStaticFactory) other)); + } + + private boolean equalTo(StringExampleNoStaticFactory other) { + return this.string.equals(other.string); + } + + @Override + public int hashCode() { + return this.string.hashCode(); + } + + @Override + public String toString() { + return "StringExampleNoStaticFactory{string: " + string + '}'; + } + + private static void validateFields(String string) { + List missingFields = null; + missingFields = addFieldIfMissing(missingFields, string, "string"); + if (missingFields != null) { + throw new SafeIllegalArgumentException( + "Some required fields have not been set", SafeArg.of("missingFields", missingFields)); + } + } + + private static List addFieldIfMissing(List prev, Object fieldValue, String fieldName) { + List missingFields = prev; + if (fieldValue == null) { + if (missingFields == null) { + missingFields = new ArrayList<>(1); + } + missingFields.add(fieldName); + } + return missingFields; + } + + public static Builder builder() { + return new Builder(); + } + + @Generated("com.palantir.conjure.java.types.BeanBuilderGenerator") + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + boolean _buildInvoked; + + private String string; + + private Builder() {} + + public Builder from(StringExampleNoStaticFactory other) { + checkNotBuilt(); + string(other.getString()); + return this; + } + + @JsonSetter("string") + public Builder string(@Nonnull String string) { + checkNotBuilt(); + this.string = Preconditions.checkNotNull(string, "string cannot be null"); + return this; + } + + public StringExampleNoStaticFactory build() { + checkNotBuilt(); + this._buildInvoked = true; + return new StringExampleNoStaticFactory(string); + } + + private void checkNotBuilt() { + Preconditions.checkState(!_buildInvoked, "Build has already been called"); + } + } +} diff --git a/conjure-java-core/src/main/java/com/palantir/conjure/java/Options.java b/conjure-java-core/src/main/java/com/palantir/conjure/java/Options.java index 398aa0e8a..0fa11db1a 100644 --- a/conjure-java-core/src/main/java/com/palantir/conjure/java/Options.java +++ b/conjure-java-core/src/main/java/com/palantir/conjure/java/Options.java @@ -163,6 +163,15 @@ default boolean jetbrainsContractAnnotations() { return false; } + /** + * If set to true, static factory methods ('of') will be excluded from generated objects with one or more fields. + * Note that for objects without any fields, static factory methods will still be generated. + */ + @Value.Default + default boolean excludeStaticFactoryMethodsForObjectsWithAtLeastOneField() { + return false; + } + Optional packagePrefix(); Optional apiVersion(); diff --git a/conjure-java-core/src/main/java/com/palantir/conjure/java/types/BeanGenerator.java b/conjure-java-core/src/main/java/com/palantir/conjure/java/types/BeanGenerator.java index 2b64a351b..54a1d5ee3 100644 --- a/conjure-java-core/src/main/java/com/palantir/conjure/java/types/BeanGenerator.java +++ b/conjure-java-core/src/main/java/com/palantir/conjure/java/types/BeanGenerator.java @@ -102,12 +102,16 @@ public static JavaFile generateBeanType( TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(prefixedName.getName()) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) - .addAnnotations(safety) - .addFields(poetFields) - .addMethod(createConstructor(fields, poetFields)) - .addMethods(createGetters(fields, typesMap, options, safetyEvaluator)); + .addAnnotations(safety); + + if (poetFields.isEmpty()) { + addEmptyBean(typeBuilder, prefixedName, safety, objectClass, options); + } else { + typeBuilder + .addFields(poetFields) + .addMethod(createConstructor(fields, poetFields)) + .addMethods(createGetters(fields, typesMap, options, safetyEvaluator)); - if (!poetFields.isEmpty()) { boolean useCachedHashCode = useCachedHashCode(fields); typeBuilder .addMethod(MethodSpecs.createEquals(objectClass)) @@ -117,51 +121,65 @@ public static JavaFile generateBeanType( } else { typeBuilder.addMethod(MethodSpecs.createHashCode(poetFields)); } - } - typeBuilder.addMethod(MethodSpecs.createToString( - prefixedName.getName(), - fields.stream().map(EnrichedField::fieldName).collect(Collectors.toList())) - .toBuilder() - .addAnnotations(safety) - .build()); - - if (poetFields.size() <= MAX_NUM_PARAMS_FOR_FACTORY) { - typeBuilder.addMethod(createStaticFactoryMethod( - fields, - objectClass, - safetyEvaluator, - options.useStagedBuilders() && !options.useStrictStagedBuilders())); - } + typeBuilder.addMethod(MethodSpecs.createToString( + prefixedName.getName(), + fields.stream().map(EnrichedField::fieldName).collect(Collectors.toList())) + .toBuilder() + .addAnnotations(safety) + .build()); + + // If the `excludeStaticFactoryMethods` is set, do not create a static factory method, unless the object + // does not have any fields. + if (!options.excludeStaticFactoryMethodsForObjectsWithAtLeastOneField() + && poetFields.size() <= MAX_NUM_PARAMS_FOR_FACTORY) { + typeBuilder.addMethod(createStaticFactoryMethod( + fields, + objectClass, + safetyEvaluator, + options.useStagedBuilders() && !options.useStrictStagedBuilders())); + } - if (!nonPrimitiveEnrichedFields.isEmpty()) { - typeBuilder - .addMethod(createValidateFields(nonPrimitiveEnrichedFields)) - .addMethod(createAddFieldIfMissing(nonPrimitiveEnrichedFields.size())); - } + if (!nonPrimitiveEnrichedFields.isEmpty()) { + typeBuilder + .addMethod(createValidateFields(nonPrimitiveEnrichedFields)) + .addMethod(createAddFieldIfMissing(nonPrimitiveEnrichedFields.size())); + } - if (poetFields.isEmpty()) { - // Need to add JsonSerialize annotation which indicates that the empty bean serializer should be used to - // serialize this class. Without this annotation no serializer will be set for this class, thus preventing - // serialization. - typeBuilder.addAnnotation(JsonSerialize.class).addField(createSingletonField(objectClass)); - if (!options.strictObjects()) { - typeBuilder.addAnnotation(AnnotationSpec.builder(JsonIgnoreProperties.class) - .addMember("ignoreUnknown", "$L", true) - .build()); + if (options.useStrictStagedBuilders()) { + BeanBuilderGenerator.addStrictStagedBuilder( + typeBuilder, + typeMapper, + safetyEvaluator, + objectClass, + builderClass, + typeDef, + typesMap, + options); + } else if (options.useStagedBuilders()) { + BeanBuilderGenerator.addStagedBuilder( + typeBuilder, + typeMapper, + safetyEvaluator, + objectClass, + builderClass, + typeDef, + typesMap, + options); + } else { + BeanBuilderGenerator.addBuilder( + typeBuilder, + typeMapper, + safetyEvaluator, + objectClass, + builderClass, + typeDef, + typesMap, + options); } - } else if (options.useStrictStagedBuilders()) { - BeanBuilderGenerator.addStrictStagedBuilder( - typeBuilder, typeMapper, safetyEvaluator, objectClass, builderClass, typeDef, typesMap, options); - } else if (options.useStagedBuilders()) { - BeanBuilderGenerator.addStagedBuilder( - typeBuilder, typeMapper, safetyEvaluator, objectClass, builderClass, typeDef, typesMap, options); - } else { - BeanBuilderGenerator.addBuilder( - typeBuilder, typeMapper, safetyEvaluator, objectClass, builderClass, typeDef, typesMap, options); } - typeBuilder.addAnnotation(ConjureAnnotations.getConjureGeneratedAnnotation(BeanGenerator.class)); + typeBuilder.addAnnotation(ConjureAnnotations.getConjureGeneratedAnnotation(BeanGenerator.class)); typeDef.getDocs().ifPresent(docs -> typeBuilder.addJavadoc("$L", Javadoc.render(docs))); return JavaFile.builder(prefixedName.getPackage(), typeBuilder.build()) @@ -170,6 +188,33 @@ public static JavaFile generateBeanType( .build(); } + private static void addEmptyBean( + TypeSpec.Builder typeBuilder, + com.palantir.conjure.spec.TypeName prefixedName, + ImmutableList safety, + ClassName objectClass, + Options options) { + // Add ctor + typeBuilder.addMethod(createConstructor(ImmutableList.of(), ImmutableList.of())); + + // Add toString + typeBuilder.addMethod(MethodSpecs.createToString(prefixedName.getName(), Collections.emptyList()).toBuilder() + .addAnnotations(safety) + .build()); + + typeBuilder.addMethod(createStaticFactoryMethodForEmptyBean(objectClass)); + + // Need to add JsonSerialize annotation which indicates that the empty bean serializer should be used to + // serialize this class. Without this annotation no serializer will be set for this class, thus preventing + // serialization. + typeBuilder.addAnnotation(JsonSerialize.class).addField(createSingletonField(objectClass)); + if (!options.strictObjects()) { + typeBuilder.addAnnotation(AnnotationSpec.builder(JsonIgnoreProperties.class) + .addMember("ignoreUnknown", "$L", true) + .build()); + } + } + private static boolean useCachedHashCode(Collection fields) { if (fields.size() == 1) { EnrichedField field = Iterables.getOnlyElement(fields); @@ -331,37 +376,44 @@ private static MethodSpec createStaticFactoryMethod( ClassName objectClass, SafetyEvaluator safetyEvaluator, boolean useNonStrictStagedBuilders) { + if (fields.isEmpty()) { + return createStaticFactoryMethodForEmptyBean(objectClass); + } + MethodSpec.Builder builder = MethodSpec.methodBuilder("of") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(objectClass); - - if (fields.isEmpty()) { - builder.addAnnotation(ConjureAnnotations.delegatingJsonCreator()) - .addCode("return $L;", SINGLETON_INSTANCE_NAME); - } else { - builder.addCode("return builder()"); - fields.forEach(field -> builder.addParameter(ParameterSpec.builder( - getTypeNameWithoutOptional(field.poetSpec()), field.poetSpec().name) - .addAnnotations(ConjureAnnotations.safety(safetyEvaluator.getUsageTimeSafety(field.conjureDef()))) - .build())); - - Stream methodArgs = useNonStrictStagedBuilders - ? fields.stream() - .sorted(Comparator.comparing(BeanBuilderGenerator::stagedBuilderFieldShouldBeInFinalStage)) - : fields.stream(); - methodArgs.map(EnrichedField::poetSpec).forEach(spec -> { - if (isOptional(spec)) { - builder.addCode("\n .$L(Optional.of($L))", spec.name, spec.name); - } else { - builder.addCode("\n .$L($L)", spec.name, spec.name); - } - }); - builder.addCode("\n .build();\n"); - } + builder.addCode("return builder()"); + fields.forEach(field -> builder.addParameter(ParameterSpec.builder( + getTypeNameWithoutOptional(field.poetSpec()), field.poetSpec().name) + .addAnnotations(ConjureAnnotations.safety(safetyEvaluator.getUsageTimeSafety(field.conjureDef()))) + .build())); + + Stream methodArgs = useNonStrictStagedBuilders + ? fields.stream() + .sorted(Comparator.comparing(BeanBuilderGenerator::stagedBuilderFieldShouldBeInFinalStage)) + : fields.stream(); + methodArgs.map(EnrichedField::poetSpec).forEach(spec -> { + if (isOptional(spec)) { + builder.addCode("\n .$L(Optional.of($L))", spec.name, spec.name); + } else { + builder.addCode("\n .$L($L)", spec.name, spec.name); + } + }); + builder.addCode("\n .build();\n"); return builder.build(); } + private static MethodSpec createStaticFactoryMethodForEmptyBean(ClassName objectClass) { + return MethodSpec.methodBuilder("of") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(objectClass) + .addAnnotation(ConjureAnnotations.delegatingJsonCreator()) + .addCode("return $L;", SINGLETON_INSTANCE_NAME) + .build(); + } + private static MethodSpec createAddFieldIfMissing(int fieldCount) { ParameterizedTypeName listOfStringType = ParameterizedTypeName.get(List.class, String.class); ParameterSpec listParam = diff --git a/conjure-java-core/src/test/java/com/palantir/conjure/java/types/ObjectGeneratorTests.java b/conjure-java-core/src/test/java/com/palantir/conjure/java/types/ObjectGeneratorTests.java index f7a67c38e..8d1df1cb5 100644 --- a/conjure-java-core/src/test/java/com/palantir/conjure/java/types/ObjectGeneratorTests.java +++ b/conjure-java-core/src/test/java/com/palantir/conjure/java/types/ObjectGeneratorTests.java @@ -134,6 +134,20 @@ public void testObjectGenerator_strictStagedBuilder() throws IOException { assertThatFilesAreTheSame(files, REFERENCE_FILES_FOLDER); } + @Test + public void testObjectGenerator_noStaticFactory() throws IOException { + ConjureDefinition def = + Conjure.parse(ImmutableList.of(new File("src/test/resources/example-types-no-static-factory.yml"))); + List files = new GenerationCoordinator( + MoreExecutors.directExecutor(), + ImmutableSet.of(new ObjectGenerator(Options.builder() + .excludeStaticFactoryMethodsForObjectsWithAtLeastOneField(true) + .build()))) + .emit(def, tempDir); + + assertThatFilesAreTheSame(files, REFERENCE_FILES_FOLDER); + } + @Test public void testObjectGenerator_stagedBuilderAndStrictStagedBuilder() throws IOException { // Check that setting enabling staged and strict staged builders is equivalent to only enabling strict staged diff --git a/conjure-java-core/src/test/resources/example-types-no-static-factory.yml b/conjure-java-core/src/test/resources/example-types-no-static-factory.yml new file mode 100644 index 000000000..33668943e --- /dev/null +++ b/conjure-java-core/src/test/resources/example-types-no-static-factory.yml @@ -0,0 +1,20 @@ +types: + imports: + ExampleExternalReference: + base-type: string + external: + java: test.api.ExampleExternalReference + definitions: + default-package: com.palantir.product + objects: + EmptyExampleNoStaticFactory: + fields: { } + docs: There are no fields in this type. A static factory method (`of`) should be generated. + StringExampleNoStaticFactory: + fields: + string: string + CovariantListExampleNoStaticFactory: + fields: + items: list + externalItems: list + optionalField: optional \ No newline at end of file diff --git a/readme.md b/readme.md index b615cc79f..dc296b8c4 100644 --- a/readme.md +++ b/readme.md @@ -49,6 +49,9 @@ The recommended way to use conjure-java is via a build tool like [gradle-conjure --jakartaPackages Generates jax-rs annotated interfaces which use the newer 'jakarta` packages instead of the legacy 'javax' packages. + --excludeStaticFactoryMethods + Exclude static factory methods from generated objects with one or more fields. Note that for + objects without any fields, this will still generate the static factory method. ### Known Tag Values