diff --git a/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-npe-fixes.json b/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-npe-fixes.json new file mode 100644 index 000000000000..42d1d2a109d0 --- /dev/null +++ b/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-npe-fixes.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "Amazon DynamoDB Enhanced Client", + "contributor": "", + "description": "Fix NullPointerException in `ConverterUtils.validateDouble` and `ConverterUtils.validateFloat` when input is null, and in `EnhancedType.hashCode()` when using wildcard types." +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedType.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedType.java index e220854247b7..c63fd935bbe1 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedType.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedType.java @@ -566,7 +566,7 @@ public boolean equals(Object o) { if (isWildcard != enhancedType.isWildcard) { return false; } - if (!rawClass.equals(enhancedType.rawClass)) { + if (rawClass != null ? !rawClass.equals(enhancedType.rawClass) : enhancedType.rawClass != null) { return false; } if (rawClassParameters != null ? !rawClassParameters.equals(enhancedType.rawClassParameters) : @@ -584,7 +584,7 @@ public boolean equals(Object o) { @Override public int hashCode() { int result = (isWildcard ? 1 : 0); - result = 31 * result + rawClass.hashCode(); + result = 31 * result + (rawClass != null ? rawClass.hashCode() : 0); result = 31 * result + (rawClassParameters != null ? rawClassParameters.hashCode() : 0); result = 31 * result + (tableSchema != null ? tableSchema.hashCode() : 0); result = 31 * result + (documentConfiguration != null ? documentConfiguration.hashCode() : 0); diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtils.java index 0f6f20583187..ee1f001641f5 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtils.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtils.java @@ -36,18 +36,24 @@ private ConverterUtils() { /** * Validates that a given Double input is a valid double supported by {@link DoubleAttributeConverter}. - * @param input + * @param input the Double value to validate, may be null */ public static void validateDouble(Double input) { + if (input == null) { + return; + } Validate.isTrue(!Double.isNaN(input), "NaN is not supported by the default converters."); Validate.isTrue(Double.isFinite(input), "Infinite numbers are not supported by the default converters."); } /** - * Validates that a given Float input is a valid double supported by {@link FloatAttributeConverter}. - * @param input + * Validates that a given Float input is a valid float supported by {@link FloatAttributeConverter}. + * @param input the Float value to validate, may be null */ public static void validateFloat(Float input) { + if (input == null) { + return; + } Validate.isTrue(!Float.isNaN(input), "NaN is not supported by the default converters."); Validate.isTrue(Float.isFinite(input), "Infinite numbers are not supported by the default converters."); } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java index a8732a15aba4..16018896d796 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java @@ -239,6 +239,31 @@ public void documentOf_withEnhancedTypeConfiguration() { assertThat(type.documentConfiguration().get().preserveEmptyObject()).isTrue(); } + @Test + public void wildcardType_hashCode_doesNotRaiseNPE() { + // When using a wildcard type like List, the type parameter is a wildcard + // with rawClass = null. hashCode() should handle this without NPE. + EnhancedType> listWithWildcard = new EnhancedType>(){}; + EnhancedType wildcardParam = listWithWildcard.rawClassParameters().get(0); + + // This should not throw NPE + assertThatCode(() -> wildcardParam.hashCode()).doesNotThrowAnyException(); + } + + @Test + public void wildcardType_equals_handlesNullRawClass() { + // Wildcard types should be comparable via equals without NPE + EnhancedType> listWithWildcard1 = new EnhancedType>(){}; + EnhancedType> listWithWildcard2 = new EnhancedType>(){}; + + EnhancedType wildcard1 = listWithWildcard1.rawClassParameters().get(0); + EnhancedType wildcard2 = listWithWildcard2.rawClassParameters().get(0); + + // Wildcards should be equal to each other + assertThat(wildcard1).isEqualTo(wildcard2); + assertThat(wildcard1.hashCode()).isEqualTo(wildcard2.hashCode()); + } + public class InnerType { } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtilsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtilsTest.java new file mode 100644 index 000000000000..c50f4877fbb7 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtilsTest.java @@ -0,0 +1,90 @@ +/* + * 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.internal.converter; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class ConverterUtilsTest { + + @Test + void validateDouble_withNull_doesNotThrowException() { + assertThatCode(() -> ConverterUtils.validateDouble(null)) + .doesNotThrowAnyException(); + } + + @Test + void validateDouble_withValidValue_doesNotThrowException() { + assertThatCode(() -> ConverterUtils.validateDouble(1.5)) + .doesNotThrowAnyException(); + } + + @Test + void validateDouble_withNaN_throwsException() { + assertThatThrownBy(() -> ConverterUtils.validateDouble(Double.NaN)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("NaN is not supported"); + } + + @Test + void validateDouble_withPositiveInfinity_throwsException() { + assertThatThrownBy(() -> ConverterUtils.validateDouble(Double.POSITIVE_INFINITY)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Infinite numbers are not supported"); + } + + @Test + void validateDouble_withNegativeInfinity_throwsException() { + assertThatThrownBy(() -> ConverterUtils.validateDouble(Double.NEGATIVE_INFINITY)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Infinite numbers are not supported"); + } + + @Test + void validateFloat_withNull_doesNotThrowException() { + assertThatCode(() -> ConverterUtils.validateFloat(null)) + .doesNotThrowAnyException(); + } + + @Test + void validateFloat_withValidValue_doesNotThrowException() { + assertThatCode(() -> ConverterUtils.validateFloat(1.5f)) + .doesNotThrowAnyException(); + } + + @Test + void validateFloat_withNaN_throwsException() { + assertThatThrownBy(() -> ConverterUtils.validateFloat(Float.NaN)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("NaN is not supported"); + } + + @Test + void validateFloat_withPositiveInfinity_throwsException() { + assertThatThrownBy(() -> ConverterUtils.validateFloat(Float.POSITIVE_INFINITY)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Infinite numbers are not supported"); + } + + @Test + void validateFloat_withNegativeInfinity_throwsException() { + assertThatThrownBy(() -> ConverterUtils.validateFloat(Float.NEGATIVE_INFINITY)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Infinite numbers are not supported"); + } +}