diff --git a/changelog/@unreleased/pr-2386.v2.yml b/changelog/@unreleased/pr-2386.v2.yml new file mode 100644 index 000000000..0361d6e35 --- /dev/null +++ b/changelog/@unreleased/pr-2386.v2.yml @@ -0,0 +1,5 @@ +type: improvement +improvement: + description: '[FR] Add Serialization Optimization for Primitive Collection Types' + links: + - https://github.com/palantir/conjure-java/pull/2386 diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/CollectionsTestObject.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/CollectionsTestObject.java index a2b70e293..4f65c071d 100644 --- a/conjure-java-core/src/integrationInput/java/com/palantir/product/CollectionsTestObject.java +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/CollectionsTestObject.java @@ -50,7 +50,7 @@ private CollectionsTestObject( CollectionsTestAliasSet aset, CollectionsTestAliasMap amap) { validateFields(items, itemsMap, optionalItem, itemsSet, alist, aset, amap); - this.items = Collections.unmodifiableList(items); + this.items = ConjureCollections.unmodifiableList(items); this.itemsMap = Collections.unmodifiableMap(itemsMap); this.optionalItem = optionalItem; this.itemsSet = Collections.unmodifiableSet(itemsSet); diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/CovariantListExample.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/CovariantListExample.java index d0037dfe5..60ac3e9a0 100644 --- a/conjure-java-core/src/integrationInput/java/com/palantir/product/CovariantListExample.java +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/CovariantListExample.java @@ -10,7 +10,6 @@ import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -28,8 +27,8 @@ public final class CovariantListExample { private CovariantListExample(List items, List externalItems) { validateFields(items, externalItems); - this.items = Collections.unmodifiableList(items); - this.externalItems = Collections.unmodifiableList(externalItems); + this.items = ConjureCollections.unmodifiableList(items); + this.externalItems = ConjureCollections.unmodifiableList(externalItems); } @JsonProperty("items") diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/ExternalLongExample.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/ExternalLongExample.java index 6a219fd2d..561025f71 100644 --- a/conjure-java-core/src/integrationInput/java/com/palantir/product/ExternalLongExample.java +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/ExternalLongExample.java @@ -11,7 +11,6 @@ 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 java.util.function.Function; @@ -34,7 +33,7 @@ private ExternalLongExample(long externalLong, Optional optionalExternalLo validateFields(optionalExternalLong, listExternalLong); this.externalLong = externalLong; this.optionalExternalLong = optionalExternalLong; - this.listExternalLong = Collections.unmodifiableList(listExternalLong); + this.listExternalLong = ConjureCollections.unmodifiableList(listExternalLong); } @JsonProperty("externalLong") diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/ListExample.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/ListExample.java index 885402021..8e739e799 100644 --- a/conjure-java-core/src/integrationInput/java/com/palantir/product/ListExample.java +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/ListExample.java @@ -11,7 +11,6 @@ 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; @@ -47,13 +46,13 @@ private ListExample( List> nestedItems) { validateFields( items, primitiveItems, doubleItems, booleanItems, optionalItems, aliasOptionalItems, nestedItems); - this.items = Collections.unmodifiableList(items); - this.primitiveItems = Collections.unmodifiableList(primitiveItems); - this.doubleItems = Collections.unmodifiableList(doubleItems); - this.booleanItems = Collections.unmodifiableList(booleanItems); - this.optionalItems = Collections.unmodifiableList(optionalItems); - this.aliasOptionalItems = Collections.unmodifiableList(aliasOptionalItems); - this.nestedItems = Collections.unmodifiableList(nestedItems); + this.items = ConjureCollections.unmodifiableList(items); + this.primitiveItems = ConjureCollections.unmodifiableList(primitiveItems); + this.doubleItems = ConjureCollections.unmodifiableList(doubleItems); + this.booleanItems = ConjureCollections.unmodifiableList(booleanItems); + this.optionalItems = ConjureCollections.unmodifiableList(optionalItems); + this.aliasOptionalItems = ConjureCollections.unmodifiableList(aliasOptionalItems); + this.nestedItems = ConjureCollections.unmodifiableList(nestedItems); } @JsonProperty("items") diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/ManyFieldExample.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/ManyFieldExample.java index 7386d9a38..860ef06f9 100644 --- a/conjure-java-core/src/integrationInput/java/com/palantir/product/ManyFieldExample.java +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/ManyFieldExample.java @@ -56,7 +56,7 @@ private ManyFieldExample( this.integer = integer; this.doubleValue = doubleValue; this.optionalItem = optionalItem; - this.items = Collections.unmodifiableList(items); + this.items = ConjureCollections.unmodifiableList(items); this.set = Collections.unmodifiableSet(set); this.map = Collections.unmodifiableMap(map); this.alias = alias; diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/MultipleFieldsOnlyFinalStage.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/MultipleFieldsOnlyFinalStage.java index 8b6984d98..44217c2bb 100644 --- a/conjure-java-core/src/integrationInput/java/com/palantir/product/MultipleFieldsOnlyFinalStage.java +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/MultipleFieldsOnlyFinalStage.java @@ -53,11 +53,11 @@ private MultipleFieldsOnlyFinalStage( Optional optionalItemOld, Set itemsSetOld) { validateFields(items, itemsMap, optionalItem, itemsSet, itemsOld, itemsMapOld, optionalItemOld, itemsSetOld); - this.items = Collections.unmodifiableList(items); + this.items = ConjureCollections.unmodifiableList(items); this.itemsMap = Collections.unmodifiableMap(itemsMap); this.optionalItem = optionalItem; this.itemsSet = Collections.unmodifiableSet(itemsSet); - this.itemsOld = Collections.unmodifiableList(itemsOld); + this.itemsOld = ConjureCollections.unmodifiableList(itemsOld); this.itemsMapOld = Collections.unmodifiableMap(itemsMapOld); this.optionalItemOld = optionalItemOld; this.itemsSetOld = Collections.unmodifiableSet(itemsSetOld); diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/PrimitiveExample.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/PrimitiveExample.java new file mode 100644 index 000000000..a1ab65037 --- /dev/null +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/PrimitiveExample.java @@ -0,0 +1,218 @@ +package com.palantir.product; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +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.google.errorprone.annotations.CheckReturnValue; +import com.palantir.conjure.java.lib.SafeLong; +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.List; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.processing.Generated; + +@JsonDeserialize(builder = PrimitiveExample.Builder.class) +@Generated("com.palantir.conjure.java.types.BeanGenerator") +public final class PrimitiveExample { + private final List ints; + + private final List doubles; + + private final List longs; + + private int memoizedHashCode; + + private PrimitiveExample(List ints, List doubles, List longs) { + validateFields(ints, doubles, longs); + this.ints = ConjureCollections.unmodifiableList(ints); + this.doubles = ConjureCollections.unmodifiableList(doubles); + this.longs = ConjureCollections.unmodifiableList(longs); + } + + @JsonProperty("ints") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public List getInts() { + return this.ints; + } + + @JsonProperty("doubles") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public List getDoubles() { + return this.doubles; + } + + @JsonProperty("longs") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public List getLongs() { + return this.longs; + } + + @Override + public boolean equals(@Nullable Object other) { + return this == other || (other instanceof PrimitiveExample && equalTo((PrimitiveExample) other)); + } + + private boolean equalTo(PrimitiveExample other) { + if (this.memoizedHashCode != 0 + && other.memoizedHashCode != 0 + && this.memoizedHashCode != other.memoizedHashCode) { + return false; + } + return this.ints.equals(other.ints) && this.doubles.equals(other.doubles) && this.longs.equals(other.longs); + } + + @Override + public int hashCode() { + int result = memoizedHashCode; + if (result == 0) { + int hash = 1; + hash = 31 * hash + this.ints.hashCode(); + hash = 31 * hash + this.doubles.hashCode(); + hash = 31 * hash + this.longs.hashCode(); + result = hash; + memoizedHashCode = result; + } + return result; + } + + @Override + public String toString() { + return "PrimitiveExample{ints: " + ints + ", doubles: " + doubles + ", longs: " + longs + '}'; + } + + public static PrimitiveExample of(List ints, List doubles, List longs) { + return builder().ints(ints).doubles(doubles).longs(longs).build(); + } + + private static void validateFields(List ints, List doubles, List longs) { + List missingFields = null; + missingFields = addFieldIfMissing(missingFields, ints, "ints"); + missingFields = addFieldIfMissing(missingFields, doubles, "doubles"); + missingFields = addFieldIfMissing(missingFields, longs, "longs"); + 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 ints = ConjureCollections.newNonNullIntegerList(); + + private List doubles = ConjureCollections.newNonNullDoubleList(); + + private List longs = ConjureCollections.newNonNullSafeLongList(); + + private Builder() {} + + public Builder from(PrimitiveExample other) { + checkNotBuilt(); + ints(other.getInts()); + doubles(other.getDoubles()); + longs(other.getLongs()); + return this; + } + + @JsonSetter(value = "ints", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) + public Builder ints(@Nonnull Iterable ints) { + checkNotBuilt(); + this.ints = + ConjureCollections.newNonNullIntegerList(Preconditions.checkNotNull(ints, "ints cannot be null")); + return this; + } + + public Builder addAllInts(@Nonnull Iterable ints) { + checkNotBuilt(); + ConjureCollections.addAllAndCheckNonNull( + this.ints, Preconditions.checkNotNull(ints, "ints cannot be null")); + return this; + } + + public Builder ints(int ints) { + checkNotBuilt(); + Preconditions.checkNotNull(ints, "ints cannot be null"); + this.ints.add(ints); + return this; + } + + @JsonSetter(value = "doubles", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) + public Builder doubles(@Nonnull Iterable doubles) { + checkNotBuilt(); + this.doubles = ConjureCollections.newNonNullDoubleList( + Preconditions.checkNotNull(doubles, "doubles cannot be null")); + return this; + } + + public Builder addAllDoubles(@Nonnull Iterable doubles) { + checkNotBuilt(); + ConjureCollections.addAllAndCheckNonNull( + this.doubles, Preconditions.checkNotNull(doubles, "doubles cannot be null")); + return this; + } + + public Builder doubles(double doubles) { + checkNotBuilt(); + Preconditions.checkNotNull(doubles, "doubles cannot be null"); + this.doubles.add(doubles); + return this; + } + + @JsonSetter(value = "longs", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) + public Builder longs(@Nonnull Iterable longs) { + checkNotBuilt(); + this.longs = ConjureCollections.newNonNullSafeLongList( + Preconditions.checkNotNull(longs, "longs cannot be null")); + return this; + } + + public Builder addAllLongs(@Nonnull Iterable longs) { + checkNotBuilt(); + ConjureCollections.addAllAndCheckNonNull( + this.longs, Preconditions.checkNotNull(longs, "longs cannot be null")); + return this; + } + + public Builder longs(SafeLong longs) { + checkNotBuilt(); + Preconditions.checkNotNull(longs, "longs cannot be null"); + this.longs.add(longs); + return this; + } + + @CheckReturnValue + public PrimitiveExample build() { + checkNotBuilt(); + this._buildInvoked = true; + return new PrimitiveExample(ints, doubles, longs); + } + + private void checkNotBuilt() { + Preconditions.checkState(!_buildInvoked, "Build has already been called"); + } + } +} diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/SafeExternalLongExample.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/SafeExternalLongExample.java index ee64acbd5..ea5ec2019 100644 --- a/conjure-java-core/src/integrationInput/java/com/palantir/product/SafeExternalLongExample.java +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/SafeExternalLongExample.java @@ -43,7 +43,7 @@ private SafeExternalLongExample( validateFields(optionalSafeExternalLong, safeExternalLongList, safeExternalLongSet); this.safeExternalLongValue = safeExternalLongValue; this.optionalSafeExternalLong = optionalSafeExternalLong; - this.safeExternalLongList = Collections.unmodifiableList(safeExternalLongList); + this.safeExternalLongList = ConjureCollections.unmodifiableList(safeExternalLongList); this.safeExternalLongSet = Collections.unmodifiableSet(safeExternalLongSet); } diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/SafeLongExample.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/SafeLongExample.java index ba755c61a..227e74a05 100644 --- a/conjure-java-core/src/integrationInput/java/com/palantir/product/SafeLongExample.java +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/SafeLongExample.java @@ -11,7 +11,6 @@ import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -29,7 +28,7 @@ public final class SafeLongExample { private SafeLongExample(SafeLong safeLongValue, List safeLongList) { validateFields(safeLongValue, safeLongList); this.safeLongValue = safeLongValue; - this.safeLongList = Collections.unmodifiableList(safeLongList); + this.safeLongList = ConjureCollections.unmodifiableList(safeLongList); } @JsonProperty("safeLongValue") diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/StrictFourFields.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/StrictFourFields.java index 8a20c77a3..609669d9b 100644 --- a/conjure-java-core/src/integrationInput/java/com/palantir/product/StrictFourFields.java +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/StrictFourFields.java @@ -44,7 +44,7 @@ private StrictFourFields( Optional optionalItem, Map mappedRids) { validateFields(myList, bearerTokenValue, optionalItem, mappedRids); - this.myList = Collections.unmodifiableList(myList); + this.myList = ConjureCollections.unmodifiableList(myList); this.bearerTokenValue = bearerTokenValue; this.optionalItem = optionalItem; this.mappedRids = Collections.unmodifiableMap(mappedRids); diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/StrictMultipleDeprecatedAndUnsafeFields.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/StrictMultipleDeprecatedAndUnsafeFields.java index 1035a2096..842d79234 100644 --- a/conjure-java-core/src/integrationInput/java/com/palantir/product/StrictMultipleDeprecatedAndUnsafeFields.java +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/StrictMultipleDeprecatedAndUnsafeFields.java @@ -55,7 +55,7 @@ private StrictMultipleDeprecatedAndUnsafeFields( Map mappedRids, StrictFourFields strictFourFieldsObject) { validateFields(myList, bearerTokenValue, safeLongValue, optionalItem, mappedRids, strictFourFieldsObject); - this.myList = Collections.unmodifiableList(myList); + this.myList = ConjureCollections.unmodifiableList(myList); this.bearerTokenValue = bearerTokenValue; this.safeLongValue = safeLongValue; this.myInteger = myInteger; diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/StrictOneCollectionField.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/StrictOneCollectionField.java index 49ff3d3da..dd74d7b72 100644 --- a/conjure-java-core/src/integrationInput/java/com/palantir/product/StrictOneCollectionField.java +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/StrictOneCollectionField.java @@ -11,7 +11,6 @@ import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -26,7 +25,7 @@ public final class StrictOneCollectionField { private StrictOneCollectionField(List myList) { validateFields(myList); - this.myList = Collections.unmodifiableList(myList); + this.myList = ConjureCollections.unmodifiableList(myList); } @JsonProperty("myList") diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/StrictThreeFields.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/StrictThreeFields.java index 4ae6c4551..80e89cfe0 100644 --- a/conjure-java-core/src/integrationInput/java/com/palantir/product/StrictThreeFields.java +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/StrictThreeFields.java @@ -14,7 +14,6 @@ import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; import com.palantir.tokens.auth.BearerToken; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Optional; import javax.annotation.Nonnull; @@ -35,7 +34,7 @@ public final class StrictThreeFields { private StrictThreeFields(List myList, BearerToken bearerTokenValue, Optional optionalItem) { validateFields(myList, bearerTokenValue, optionalItem); - this.myList = Collections.unmodifiableList(myList); + this.myList = ConjureCollections.unmodifiableList(myList); this.bearerTokenValue = bearerTokenValue; this.optionalItem = optionalItem; } diff --git a/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/CovariantListExample.java b/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/CovariantListExample.java index 8eff4d61e..e59c78b13 100644 --- a/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/CovariantListExample.java +++ b/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/CovariantListExample.java @@ -11,7 +11,6 @@ import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -29,8 +28,8 @@ public final class CovariantListExample { private CovariantListExample(List items, List externalItems) { validateFields(items, externalItems); - this.items = Collections.unmodifiableList(items); - this.externalItems = Collections.unmodifiableList(externalItems); + this.items = ConjureCollections.unmodifiableList(items); + this.externalItems = ConjureCollections.unmodifiableList(externalItems); } @JsonProperty("items") diff --git a/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/ExternalLongExample.java b/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/ExternalLongExample.java index d5e45b8f5..0cb67d326 100644 --- a/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/ExternalLongExample.java +++ b/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/ExternalLongExample.java @@ -12,7 +12,6 @@ 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 java.util.function.Function; @@ -35,7 +34,7 @@ private ExternalLongExample(long externalLong, Optional optionalExternalLo validateFields(optionalExternalLong, listExternalLong); this.externalLong = externalLong; this.optionalExternalLong = optionalExternalLong; - this.listExternalLong = Collections.unmodifiableList(listExternalLong); + this.listExternalLong = ConjureCollections.unmodifiableList(listExternalLong); } @JsonProperty("externalLong") diff --git a/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/ListExample.java b/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/ListExample.java index 21a972e86..4bf20286d 100644 --- a/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/ListExample.java +++ b/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/ListExample.java @@ -12,7 +12,6 @@ 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; @@ -48,13 +47,13 @@ private ListExample( List> nestedItems) { validateFields( items, primitiveItems, doubleItems, booleanItems, optionalItems, aliasOptionalItems, nestedItems); - this.items = Collections.unmodifiableList(items); - this.primitiveItems = Collections.unmodifiableList(primitiveItems); - this.doubleItems = Collections.unmodifiableList(doubleItems); - this.booleanItems = Collections.unmodifiableList(booleanItems); - this.optionalItems = Collections.unmodifiableList(optionalItems); - this.aliasOptionalItems = Collections.unmodifiableList(aliasOptionalItems); - this.nestedItems = Collections.unmodifiableList(nestedItems); + this.items = ConjureCollections.unmodifiableList(items); + this.primitiveItems = ConjureCollections.unmodifiableList(primitiveItems); + this.doubleItems = ConjureCollections.unmodifiableList(doubleItems); + this.booleanItems = ConjureCollections.unmodifiableList(booleanItems); + this.optionalItems = ConjureCollections.unmodifiableList(optionalItems); + this.aliasOptionalItems = ConjureCollections.unmodifiableList(aliasOptionalItems); + this.nestedItems = ConjureCollections.unmodifiableList(nestedItems); } @JsonProperty("items") diff --git a/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/ManyFieldExample.java b/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/ManyFieldExample.java index 0f421ecbf..dafbd08c8 100644 --- a/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/ManyFieldExample.java +++ b/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/ManyFieldExample.java @@ -57,7 +57,7 @@ private ManyFieldExample( this.integer = integer; this.doubleValue = doubleValue; this.optionalItem = optionalItem; - this.items = Collections.unmodifiableList(items); + this.items = ConjureCollections.unmodifiableList(items); this.set = Collections.unmodifiableSet(set); this.map = Collections.unmodifiableMap(map); this.alias = alias; diff --git a/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/SafeExternalLongExample.java b/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/SafeExternalLongExample.java index e03834858..81ae838be 100644 --- a/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/SafeExternalLongExample.java +++ b/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/SafeExternalLongExample.java @@ -44,7 +44,7 @@ private SafeExternalLongExample( validateFields(optionalSafeExternalLong, safeExternalLongList, safeExternalLongSet); this.safeExternalLongValue = safeExternalLongValue; this.optionalSafeExternalLong = optionalSafeExternalLong; - this.safeExternalLongList = Collections.unmodifiableList(safeExternalLongList); + this.safeExternalLongList = ConjureCollections.unmodifiableList(safeExternalLongList); this.safeExternalLongSet = Collections.unmodifiableSet(safeExternalLongSet); } diff --git a/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/SafeLongExample.java b/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/SafeLongExample.java index f444d7d4c..9868e5ff1 100644 --- a/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/SafeLongExample.java +++ b/conjure-java-core/src/integrationInput/java/test/prefix/com/palantir/product/SafeLongExample.java @@ -12,7 +12,6 @@ import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -30,7 +29,7 @@ public final class SafeLongExample { private SafeLongExample(SafeLong safeLongValue, List safeLongList) { validateFields(safeLongValue, safeLongList); this.safeLongValue = safeLongValue; - this.safeLongList = Collections.unmodifiableList(safeLongList); + this.safeLongList = ConjureCollections.unmodifiableList(safeLongList); } @JsonProperty("safeLongValue") 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 ea7caa700..8267d8ef9 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 @@ -26,6 +26,7 @@ import com.palantir.conjure.CaseConverter; import com.palantir.conjure.java.ConjureAnnotations; import com.palantir.conjure.java.Options; +import com.palantir.conjure.java.lib.internal.ConjureCollections; import com.palantir.conjure.java.util.JavaNameSanitizer; import com.palantir.conjure.java.util.Javadoc; import com.palantir.conjure.java.util.Packages; @@ -259,7 +260,7 @@ private static MethodSpec createConstructor(Collection fields, Co // is private and necessarily called from the builder, which does its own defensive copying. if (field.conjureDef().getType().accept(TypeVisitor.IS_LIST)) { // TODO(melliot): contribute a fix to JavaPoet that parses $T correctly for a JavaPoet FieldSpec - body.addStatement("this.$1N = $2T.unmodifiableList($1N)", spec, Collections.class); + body.addStatement("this.$1N = $2T.unmodifiableList($1N)", spec, ConjureCollections.class); } else if (field.conjureDef().getType().accept(TypeVisitor.IS_SET)) { body.addStatement("this.$1N = $2T.unmodifiableSet($1N)", spec, Collections.class); } else if (field.conjureDef().getType().accept(TypeVisitor.IS_MAP)) { diff --git a/conjure-java-core/src/test/java/com/palantir/conjure/java/NonNullCollectionsTest.java b/conjure-java-core/src/test/java/com/palantir/conjure/java/NonNullCollectionsTest.java index e6e8ed6b8..7ccd2b51d 100644 --- a/conjure-java-core/src/test/java/com/palantir/conjure/java/NonNullCollectionsTest.java +++ b/conjure-java-core/src/test/java/com/palantir/conjure/java/NonNullCollectionsTest.java @@ -22,15 +22,19 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.InvalidNullException; +import com.palantir.conjure.java.lib.SafeLong; import com.palantir.conjure.java.serialization.ObjectMappers; import com.palantir.product.CovariantListExample; import com.palantir.product.ListExample; +import com.palantir.product.PrimitiveExample; import java.util.Collections; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.Test; public class NonNullCollectionsTest { - private static final ObjectMapper objectMapper = ObjectMappers.newClientObjectMapper(); + private static final ObjectMapper clientMapper = ObjectMappers.newClientObjectMapper(); + private static final ObjectMapper serverMapper = ObjectMappers.newServerJsonMapper(); @Test public void throwsNpe() { @@ -55,7 +59,7 @@ public void testOptionalSerialization() throws JsonProcessingException { .optionalItems(Collections.singleton(Optional.empty())) .build(); - assertThat(objectMapper.readValue(objectMapper.writeValueAsString(listExample), ListExample.class)) + assertThat(clientMapper.readValue(clientMapper.writeValueAsString(listExample), ListExample.class)) .isEqualTo(listExample); // non-null collections will add "contentNulls = Nulls.FAIL" to the JsonSetter annotation. This will cause deser @@ -64,12 +68,31 @@ public void testOptionalSerialization() throws JsonProcessingException { .addAllItems(Collections.singleton(Optional.empty())) .build(); assertThatExceptionOfType(InvalidNullException.class) - .isThrownBy(() -> objectMapper.readValue( - objectMapper.writeValueAsString(covariantListExample), CovariantListExample.class)); + .isThrownBy(() -> clientMapper.readValue( + clientMapper.writeValueAsString(covariantListExample), CovariantListExample.class)); // Similarly, setting a null in the builder also breaks assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> CovariantListExample.builder() .addAllItems(Collections.singleton(null)) .build()); } + + @Test + public void testSerDeOptimizationRespectsConjureEmptyCollections() throws JsonProcessingException { + PrimitiveExample expected = PrimitiveExample.builder().build(); + assertThat(clientMapper.writeValueAsString(expected)) + .describedAs("Does not serialize any empty collections, even when optimizing for primitives") + .isEqualTo("{}"); + } + + @Test + public void testSerializationRoundtrip() throws JsonProcessingException { + PrimitiveExample expected = PrimitiveExample.builder() + .ints(List.of(1, 2, 3)) + .doubles(List.of(1.1, 2.2, 3.3)) + .longs(List.of(SafeLong.of(1L), SafeLong.of(2L))) + .build(); + String serialized = serverMapper.writeValueAsString(expected); + assertThat(expected).isEqualTo(clientMapper.readValue(serialized, PrimitiveExample.class)); + } } 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..92d4b3a7e 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 @@ -168,6 +168,21 @@ public void testObjectGenerator_excludeEmptyCollections() throws IOException { assertThatFilesAreTheSame(files, REFERENCE_FILES_FOLDER); } + @Test + public void testObjectGenerator_primitiveCollections() throws IOException { + ConjureDefinition def = + Conjure.parse(ImmutableList.of(new File("src/test/resources/primitive-collections.yml"))); + List files = new GenerationCoordinator( + MoreExecutors.directExecutor(), + ImmutableSet.of(new ObjectGenerator(Options.builder() + .excludeEmptyCollections(true) + .nonNullCollections(true) + .build()))) + .emit(def, tempDir); + + assertThatFilesAreTheSame(files, REFERENCE_FILES_FOLDER); + } + @Test public void testConjureImports() throws IOException { ConjureDefinition conjure = Conjure.parse(ImmutableList.of( diff --git a/conjure-java-core/src/test/resources/primitive-collections.yml b/conjure-java-core/src/test/resources/primitive-collections.yml new file mode 100644 index 000000000..adc68a33e --- /dev/null +++ b/conjure-java-core/src/test/resources/primitive-collections.yml @@ -0,0 +1,9 @@ +types: + definitions: + default-package: com.palantir.product + objects: + PrimitiveExample: + fields: + ints: list + doubles: list + longs: list diff --git a/conjure-lib/src/main/java/com/palantir/conjure/java/lib/internal/ConjureCollections.java b/conjure-lib/src/main/java/com/palantir/conjure/java/lib/internal/ConjureCollections.java index 579aaf9b5..ff1b15bd8 100644 --- a/conjure-lib/src/main/java/com/palantir/conjure/java/lib/internal/ConjureCollections.java +++ b/conjure-lib/src/main/java/com/palantir/conjure/java/lib/internal/ConjureCollections.java @@ -20,6 +20,7 @@ import com.palantir.logsafe.Preconditions; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -37,6 +38,35 @@ private ConjureCollections() { // cannot instantiate } + /* + * This is bizarre. Allow me to explain... + * + * We do _not_ want to expose the Conjure*List types externally + * but we also want the optimizations they provide to make it thru + * to jackson for serialization. So the runtime type needs to be + * preserved while also not exposing the type :phew:. + * + * To achieve this we have to do some gymnastics surrounding the type + * system. We need this to return the type of the list given, but also + * return specific Conjure types when detected. This requires that we + * erase the type info, but we know this is safe because we are directly + * returning the same type which is by definition the identity function. + * Therefore the input List is the same types as the output List. + */ + public static List unmodifiableList(List list) { + // Return the unmodifiable version of the Eclipse types + if (list instanceof ConjureIntegerList) { + return (List) ((ConjureIntegerList) list).asUnmodifiable(); + } else if (list instanceof ConjureDoubleList) { + return (List) ((ConjureDoubleList) list).asUnmodifiable(); + } else if (list instanceof ConjureSafeLongList) { + return (List) ((ConjureSafeLongList) list).asUnmodifiable(); + } else { + // Otherwise use the JDK types + return Collections.unmodifiableList(list); + } + } + @SuppressWarnings("unchecked") public static void addAll(Collection addTo, Iterable elementsToAdd) { Preconditions.checkNotNull(elementsToAdd, "elementsToAdd cannot be null"); @@ -170,7 +200,7 @@ public static List newNonNullIntegerList() { // This method returns a list that can't handle nulls. Do not use this unless the nonNullCollections flag is set public static List newNonNullIntegerList(Iterable iterable) { - List integerList; + ConjureIntegerList integerList; if (iterable instanceof Collection) { integerList = new ConjureIntegerList(new IntArrayList(((Collection) iterable).size())); } else { diff --git a/conjure-lib/src/main/java/com/palantir/conjure/java/lib/internal/ConjureDoubleList.java b/conjure-lib/src/main/java/com/palantir/conjure/java/lib/internal/ConjureDoubleList.java index 58d63bbaa..d20e69e94 100644 --- a/conjure-lib/src/main/java/com/palantir/conjure/java/lib/internal/ConjureDoubleList.java +++ b/conjure-lib/src/main/java/com/palantir/conjure/java/lib/internal/ConjureDoubleList.java @@ -16,10 +16,11 @@ package com.palantir.conjure.java.lib.internal; +import com.fasterxml.jackson.annotation.JsonValue; import java.util.AbstractList; import java.util.Collection; import java.util.RandomAccess; -import org.eclipse.collections.impl.list.mutable.primitive.DoubleArrayList; +import org.eclipse.collections.api.list.primitive.MutableDoubleList; import org.eclipse.collections.impl.utility.Iterate; /** @@ -27,9 +28,9 @@ * a BoxedMutableDoubleList will be released. Once available, ConjureDoubleList should be replaced with that. */ final class ConjureDoubleList extends AbstractList implements RandomAccess { - private final DoubleArrayList delegate; + private final MutableDoubleList delegate; - ConjureDoubleList(DoubleArrayList delegate) { + ConjureDoubleList(MutableDoubleList delegate) { this.delegate = delegate; } @@ -69,4 +70,15 @@ public void clear() { public Double set(int index, Double element) { return delegate.set(index, element); } + + public ConjureDoubleList asUnmodifiable() { + return new ConjureDoubleList(delegate.asUnmodifiable()); + } + + // Cannot be named 'toArray' as that conflicts with the #toArray in AbstractList + // This is a serialization optimization that avoids boxing, but does copy + @JsonValue + double[] jacksonSerialize() { + return delegate.toArray(); + } } diff --git a/conjure-lib/src/main/java/com/palantir/conjure/java/lib/internal/ConjureIntegerList.java b/conjure-lib/src/main/java/com/palantir/conjure/java/lib/internal/ConjureIntegerList.java index 79ecd32b6..8046d9e2b 100644 --- a/conjure-lib/src/main/java/com/palantir/conjure/java/lib/internal/ConjureIntegerList.java +++ b/conjure-lib/src/main/java/com/palantir/conjure/java/lib/internal/ConjureIntegerList.java @@ -16,10 +16,11 @@ package com.palantir.conjure.java.lib.internal; +import com.fasterxml.jackson.annotation.JsonValue; import java.util.AbstractList; import java.util.Collection; import java.util.RandomAccess; -import org.eclipse.collections.impl.list.mutable.primitive.IntArrayList; +import org.eclipse.collections.api.list.primitive.MutableIntList; import org.eclipse.collections.impl.utility.Iterate; /** @@ -27,9 +28,9 @@ * a BoxedMutableIntList will be released. Once available, ConjureIntegerList should be replaced with that. */ final class ConjureIntegerList extends AbstractList implements RandomAccess { - private final IntArrayList delegate; + private final MutableIntList delegate; - ConjureIntegerList(IntArrayList delegate) { + ConjureIntegerList(MutableIntList delegate) { this.delegate = delegate; } @@ -69,4 +70,15 @@ public void clear() { public Integer set(int index, Integer element) { return delegate.set(index, element); } + + public ConjureIntegerList asUnmodifiable() { + return new ConjureIntegerList(delegate.asUnmodifiable()); + } + + // Cannot be named 'toArray' as that conflicts with the #toArray in AbstractList + // This is a serialization optimization that avoids boxing, but does copy + @JsonValue + int[] jacksonSerialize() { + return delegate.toArray(); + } } diff --git a/conjure-lib/src/main/java/com/palantir/conjure/java/lib/internal/ConjureSafeLongList.java b/conjure-lib/src/main/java/com/palantir/conjure/java/lib/internal/ConjureSafeLongList.java index 7995d8285..19f5ca17f 100644 --- a/conjure-lib/src/main/java/com/palantir/conjure/java/lib/internal/ConjureSafeLongList.java +++ b/conjure-lib/src/main/java/com/palantir/conjure/java/lib/internal/ConjureSafeLongList.java @@ -16,11 +16,12 @@ package com.palantir.conjure.java.lib.internal; +import com.fasterxml.jackson.annotation.JsonValue; import com.palantir.conjure.java.lib.SafeLong; import java.util.AbstractList; import java.util.Collection; import java.util.RandomAccess; -import org.eclipse.collections.impl.list.mutable.primitive.LongArrayList; +import org.eclipse.collections.api.list.primitive.MutableLongList; import org.eclipse.collections.impl.utility.Iterate; /** @@ -28,9 +29,9 @@ * with SafeLongs. */ final class ConjureSafeLongList extends AbstractList implements RandomAccess { - private final LongArrayList delegate; + private final MutableLongList delegate; - ConjureSafeLongList(LongArrayList delegate) { + ConjureSafeLongList(MutableLongList delegate) { this.delegate = delegate; } @@ -70,4 +71,15 @@ public void clear() { public SafeLong set(int index, SafeLong element) { return SafeLong.of(delegate.set(index, element.longValue())); } + + public ConjureSafeLongList asUnmodifiable() { + return new ConjureSafeLongList(delegate.asUnmodifiable()); + } + + // Cannot be named 'toArray' as that conflicts with the #toArray in AbstractList + // This is a serialization optimization that avoids boxing, but does copy + @JsonValue + long[] jacksonSerialize() { + return delegate.toArray(); + } }