diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-38866e6.json b/.changes/next-release/bugfix-AWSSDKforJavav2-38866e6.json new file mode 100644 index 000000000000..f6df85f9d6a3 --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-38866e6.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Fix DynamoDB Enhanced Client immutable class introspection fails for \"is\" prefix fields. Fixes [#4446](https://github.com/aws/aws-sdk-java-v2/issues/4446)." +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutableIntrospector.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutableIntrospector.java index c7095dd38b05..ca2cc9005fd9 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutableIntrospector.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/immutable/ImmutableIntrospector.java @@ -180,6 +180,14 @@ private String normalizeSetterName(Method setter) { return Character.toLowerCase(setterName.charAt(3)) + setterName.substring(4); } + if (setterName.length() > 2 + && Character.isUpperCase(setterName.charAt(2)) + && setterName.startsWith(IS_PREFIX) + && isSetterMethodBoolean(setter)) { + + return Character.toLowerCase(setterName.charAt(2)) + setterName.substring(3); + } + return setterName; } @@ -208,6 +216,11 @@ private boolean isMethodBoolean(Method method) { return method.getReturnType() == boolean.class || method.getReturnType() == Boolean.class; } + private boolean isSetterMethodBoolean(Method setter) { + return setter.getParameterCount() == 1 && + (setter.getParameterTypes()[0] == boolean.class || setter.getParameterTypes()[0] == Boolean.class); + } + private Optional extractBuildMethod(Map indexedBuilderMethods, Class immutableClass) { Method buildMethod = indexedBuilderMethods.get(BUILD_METHOD); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableSchemaImmutableIsPrefixTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableSchemaImmutableIsPrefixTest.java new file mode 100644 index 000000000000..229511d7b164 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableSchemaImmutableIsPrefixTest.java @@ -0,0 +1,162 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +/** + * Tests that TableSchema.fromImmutableClass() works correctly with immutable classes + * that have fields using "is" prefix. + */ +public class TableSchemaImmutableIsPrefixTest { + + // Test class for boolean fields with "is" prefix + @DynamoDbImmutable(builder = Car.Builder.class) + public static final class Car { + private final String licensePlate; + private final boolean isRusty; + private final boolean isImpounded; + + private Car(Builder b) { + this.licensePlate = b.licensePlate; + this.isRusty = b.isRusty; + this.isImpounded = b.isImpounded; + } + + @DynamoDbPartitionKey + public String licensePlate() { + return this.licensePlate; + } + + public boolean isRusty() { + return this.isRusty; + } + + public boolean isImpounded() { + return this.isImpounded; + } + + public static final class Builder { + private String licensePlate; + private boolean isRusty; + private boolean isImpounded; + + public Builder licensePlate(String licensePlate) { + this.licensePlate = licensePlate; + return this; + } + + public Builder isRusty(boolean isRusty) { + this.isRusty = isRusty; + return this; + } + + public Builder isImpounded(boolean isImpounded) { + this.isImpounded = isImpounded; + return this; + } + + public Car build() { + return new Car(this); + } + } + } + + @Test + public void fromImmutableClass_withIsPrefixBooleanSetters_shouldCreateSchemaSuccessfully() { + // This should work without exception + TableSchema schema = TableSchema.fromImmutableClass(Car.class); + + // Verify the schema was created successfully without exception + assertThat(schema).isNotNull(); + assertThat(schema.itemType().rawClass()).isEqualTo(Car.class); + + // Verify all attributes are mapped correctly + assertThat(schema.attributeNames()).containsExactlyInAnyOrder( + "licensePlate", "rusty", "impounded" + ); + } + + // Test class for non-boolean fields with "is" prefix + @DynamoDbImmutable(builder = Vehicle.Builder.class) + public static final class Vehicle { + private final String licensePlate; + private final String isModel; + private final Integer isYear; + + private Vehicle(Builder b) { + this.licensePlate = b.licensePlate; + this.isModel = b.isModel; + this.isYear = b.isYear; + } + + @DynamoDbPartitionKey + public String licensePlate() { + return this.licensePlate; + } + + public String isModel() { + return this.isModel; + } + + public Integer isYear() { + return this.isYear; + } + + public static final class Builder { + private String licensePlate; + private String isModel; + private Integer isYear; + + public Builder licensePlate(String licensePlate) { + this.licensePlate = licensePlate; + return this; + } + + public Builder isModel(String isModel) { + this.isModel = isModel; + return this; + } + + public Builder isYear(Integer isYear) { + this.isYear = isYear; + return this; + } + + public Vehicle build() { + return new Vehicle(this); + } + } + } + + @Test + public void fromImmutableClass_withIsPrefixNonBooleanFields_shouldNotNormalizeIsPrefix() { + TableSchema schema = TableSchema.fromImmutableClass(Vehicle.class); + + // Verify the schema was created successfully + assertThat(schema).isNotNull(); + assertThat(schema.itemType().rawClass()).isEqualTo(Vehicle.class); + + // Verify non-boolean "is" prefix fields are not normalized + assertThat(schema.attributeNames()).containsExactlyInAnyOrder( + "licensePlate", "isModel", "isYear" + ); + } +}