diff --git a/src/main/java/it/aboutbits/springboot/toolbox/jackson/CustomTypeDeserializer.java b/src/main/java/it/aboutbits/springboot/toolbox/jackson/CustomTypeDeserializer.java index a21106a..41f7fae 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/jackson/CustomTypeDeserializer.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/jackson/CustomTypeDeserializer.java @@ -12,6 +12,7 @@ import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; import java.math.BigInteger; +import java.util.UUID; import java.util.function.Function; public class CustomTypeDeserializer> extends JsonDeserializer { @@ -92,6 +93,9 @@ private static Function getTypeConverter(Class wrappedTyp if (ScaledBigDecimal.class.isAssignableFrom(wrappedType)) { return getScaledBigDecimalConverter(); } + if (UUID.class.isAssignableFrom(wrappedType)) { + return getUUIDConverter(); + } throw new CustomTypeDeserializerException("Value type not supported: " + wrappedType.getName()); } @@ -219,6 +223,20 @@ private static Function getCharConverter() { }; } + private static Function getUUIDConverter() { + return jsonParser -> { + try { + var value = jsonParser.getValueAsString(); + if (value == null || value.length() != 36) { + throw new IOException(); + } + return UUID.fromString(value); + } catch (IOException e) { + throw new CustomTypeDeserializerException("Failed to read value as UUID.", e); + } + }; + } + public static final class CustomTypeDeserializerException extends RuntimeException { public CustomTypeDeserializerException(String message) { super(message); diff --git a/src/main/java/it/aboutbits/springboot/toolbox/persistence/converter/UUIDConverter.java b/src/main/java/it/aboutbits/springboot/toolbox/persistence/converter/UUIDConverter.java new file mode 100644 index 0000000..1ccbdd1 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/converter/UUIDConverter.java @@ -0,0 +1,27 @@ +package it.aboutbits.springboot.toolbox.persistence.converter; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.UUID; + +@Converter +public class UUIDConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(UUID attribute) { + if (attribute == null) { + return null; + } + + return attribute.toString(); + } + + @Override + public UUID convertToEntityAttribute(String dbData) { + if (dbData == null || dbData.isBlank()) { + return null; + } + + return UUID.fromString(dbData); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedUUIDJavaType.java b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedUUIDJavaType.java new file mode 100644 index 0000000..af9eddc --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedUUIDJavaType.java @@ -0,0 +1,83 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.base; + +import it.aboutbits.springboot.toolbox.type.CustomType; +import lombok.SneakyThrows; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.AbstractClassJavaType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.nio.charset.StandardCharsets; +import java.sql.Types; +import java.util.UUID; + +public abstract class WrappedUUIDJavaType> extends AbstractClassJavaType { + private final transient Constructor constructor; + + protected WrappedUUIDJavaType(Class type) { + super(type); + + try { + this.constructor = type.getConstructor(UUID.class); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("No method found for " + type.getName(), e); + } + } + + @Override + public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { + return indicators.getTypeConfiguration() + .getJdbcTypeRegistry() + .getDescriptor(Types.OTHER); + } + + @SuppressWarnings("unchecked") + @Override + public X unwrap(T id, Class aClass, WrapperOptions wrapperOptions) { + var javaTypeClass = getJavaTypeClass(); + + if (id == null) { + return null; + } + if (javaTypeClass.isAssignableFrom(aClass)) { + return (X) id; + } + if (UUID.class.isAssignableFrom(aClass)) { + return (X) id.value(); + } + if (String.class.isAssignableFrom(aClass)) { + return (X) id.value().toString(); + } + if (byte[].class.isAssignableFrom(aClass)) { + return (X) id.value().toString().getBytes(StandardCharsets.UTF_8); + } + + throw unknownUnwrap(aClass); + } + + @SuppressWarnings("unchecked") + @SneakyThrows({InstantiationException.class, IllegalAccessException.class, InvocationTargetException.class}) + @Override + public T wrap(X value, WrapperOptions wrapperOptions) { + var clazz = getJavaTypeClass(); + + if (value == null) { + return null; + } + if (clazz.isInstance(value)) { + return (T) value; + } + + return switch (value) { + case UUID uuidValue -> constructor.newInstance(uuidValue); + case String uuidStringValue -> constructor.newInstance(UUID.fromString(uuidStringValue)); + case byte[] uuidBytesValue -> constructor.newInstance(UUID.fromString(new String( + uuidBytesValue, + StandardCharsets.UTF_8 + ))); + default -> throw unknownWrap(value.getClass()); + }; + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypeModelConverter.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypeModelConverter.java index 66e1e77..c8b8539 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypeModelConverter.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypeModelConverter.java @@ -14,6 +14,7 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.util.Iterator; +import java.util.UUID; public class CustomTypeModelConverter implements ModelConverter { @@ -71,6 +72,9 @@ public Schema resolve( if (Character.class.isAssignableFrom(wrappedType)) { result = context.resolve(new AnnotatedType(Character.TYPE)); } + if (UUID.class.isAssignableFrom(wrappedType)) { + result = context.resolve(new AnnotatedType(UUID.class)); + } if (result != null) { var isIdentity = EntityId.class.isAssignableFrom(clazz); diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypePropertyCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypePropertyCustomizer.java index b0a492b..85bcadc 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypePropertyCustomizer.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypePropertyCustomizer.java @@ -14,6 +14,7 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.util.UUID; @Slf4j public class CustomTypePropertyCustomizer implements PropertyCustomizer { @@ -91,7 +92,7 @@ public Schema customize(Schema property, AnnotatedType annotatedType) { } if (BigInteger.class.isAssignableFrom(wrappedType)) { property.type("integer"); - property.format("int64"); + property.format(""); property.setDescription(description); property.setProperties(null); property.set$ref(null); @@ -147,6 +148,16 @@ public Schema customize(Schema property, AnnotatedType annotatedType) { property.set$ref(null); return property; } + if (UUID.class.isAssignableFrom(wrappedType)) { + property.type("string"); + property.format("uuid"); + property.minLength(36); + property.maxLength(36); + property.setDescription(description); + property.setProperties(null); + property.set$ref(null); + return property; + } log.warn("Property {} of type WrappedValue: Can not resolve parameter type!", property.getName()); } diff --git a/src/main/java/it/aboutbits/springboot/toolbox/web/CustomTypePropertyEditor.java b/src/main/java/it/aboutbits/springboot/toolbox/web/CustomTypePropertyEditor.java index e76a007..c8c3237 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/web/CustomTypePropertyEditor.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/web/CustomTypePropertyEditor.java @@ -12,6 +12,7 @@ import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; import java.math.BigInteger; +import java.util.UUID; import java.util.function.Function; public final class CustomTypePropertyEditor> extends PropertyEditorSupport { @@ -100,6 +101,9 @@ private static Function getTextToTypeConverter(Class wrappedT if (ScaledBigDecimal.class.isAssignableFrom(wrappedType)) { return ScaledBigDecimal::new; } + if (UUID.class.isAssignableFrom(wrappedType)) { + return UUID::fromString; + } throw new IllegalArgumentException("Unable to convert text to type: " + wrappedType.getName()); } } diff --git a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/CustomTypeBindingsForControllerTest.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/CustomTypeBindingsForControllerTest.java index 0017b0a..78e0e92 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/CustomTypeBindingsForControllerTest.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/CustomTypeBindingsForControllerTest.java @@ -4,6 +4,7 @@ import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithEmailAddress; import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithIban; import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithScaledBigDecimal; +import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithUUID; import it.aboutbits.springboot.toolbox.support.HttpTest; import it.aboutbits.springboot.toolbox.type.EmailAddress; import it.aboutbits.springboot.toolbox.type.Iban; @@ -18,6 +19,8 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import java.util.UUID; + import static org.assertj.core.api.Assertions.assertThat; @HttpTest @@ -166,6 +169,54 @@ void ScaledBigDecimalAsBody(double doubleValue) throws Exception { } } + @Nested + class UUIDType { + @ParameterizedTest + @ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"}) + void UUIDAsPathVariable(String uuidStringValue) throws Exception { + var value = UUID.fromString(uuidStringValue); + + var resultAsString = performGetAndReturnResult( + String.format("/test/type/UUID/as-path-variable/%s", value) + ); + + var actual = objectMapper.readValue(resultAsString, UUID.class); + + assertThat(actual).isEqualTo(value); + } + + @ParameterizedTest + @ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"}) + void UUIDAsRequestParameter(String uuidStringValue) throws Exception { + var value = UUID.fromString(uuidStringValue); + + var resultAsString = performGetAndReturnResult( + String.format("/test/type/UUID/as-request-parameter?value=%s", value) + ); + + var actual = objectMapper.readValue(resultAsString, UUID.class); + + assertThat(actual).isEqualTo(value); + } + + @ParameterizedTest + @ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"}) + void UUIDAsBody(String uuidStringValue) throws Exception { + var value = new BodyWithUUID( + UUID.fromString(uuidStringValue) + ); + + var resultAsString = performPostAndReturnResult( + "/test/type/UUID/as-body", + value + ); + + var actual = objectMapper.readValue(resultAsString, BodyWithUUID.class); + + assertThat(actual).isEqualTo(value); + } + } + private @NonNull String performGetAndReturnResult(@NonNull String url) throws Exception { var requestBuilder = MockMvcRequestBuilders.get(url) .contentType(MediaType.APPLICATION_JSON); diff --git a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/body/BodyWithUUID.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/body/BodyWithUUID.java new file mode 100644 index 0000000..d6f2a1d --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/body/BodyWithUUID.java @@ -0,0 +1,8 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body; + +import java.util.UUID; + +public record BodyWithUUID( + UUID uuid +) { +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/controller/CustomTypeTestController.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/controller/CustomTypeTestController.java index 7c4d827..4551cff 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/controller/CustomTypeTestController.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/controller/CustomTypeTestController.java @@ -3,6 +3,7 @@ import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithEmailAddress; import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithIban; import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithScaledBigDecimal; +import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithUUID; import it.aboutbits.springboot.toolbox.type.EmailAddress; import it.aboutbits.springboot.toolbox.type.Iban; import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; @@ -14,6 +15,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.UUID; + @RestController @RequestMapping("/test/type") public class CustomTypeTestController { @@ -61,4 +64,19 @@ public ScaledBigDecimal scaledBigDecimalAsRequestParameter(@RequestParam ScaledB public BodyWithScaledBigDecimal scaledBigDecimalAsBody(@RequestBody BodyWithScaledBigDecimal value) { return value; } + + @GetMapping("/UUID/as-path-variable/{value}") + public UUID uuidAsPathVariable(@PathVariable UUID value) { + return value; + } + + @GetMapping("/UUID/as-request-parameter") + public UUID uuidAsRequestParameter(@RequestParam UUID value) { + return value; + } + + @PostMapping("/UUID/as-body") + public BodyWithUUID uuidAsBody(@RequestBody BodyWithUUID value) { + return value; + } } diff --git a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/CustomTypeJpaTest.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/CustomTypeJpaTest.java index 0cca224..a704e5a 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/CustomTypeJpaTest.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/CustomTypeJpaTest.java @@ -12,6 +12,8 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; +import java.util.UUID; + import static org.assertj.core.api.Assertions.assertThat; @ApplicationTest @@ -73,4 +75,39 @@ void inAndOut_shouldSucceed(double doubleValue) { .isEqualTo(savedItem); } } + + @Nested + class UUIDType { + @ParameterizedTest + @ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"}) + void inAndOut_shouldSucceed(String uuidStringValue) { + var item = new CustomTypeTestModel(); + item.setUuid(UUID.fromString(uuidStringValue)); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByUuid(savedItem.getUuid()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + + @ParameterizedTest + @ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"}) + void inAndOutString_shouldSucceed(String uuidStringValue) { + var item = new CustomTypeTestModel(); + item.setUuidAsString(UUID.fromString(uuidStringValue)); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByUuidAsString(savedItem.getUuidAsString()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + } } diff --git a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/CustomTypeTestModel.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/CustomTypeTestModel.java index 08db44d..49b80b1 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/CustomTypeTestModel.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/CustomTypeTestModel.java @@ -1,6 +1,7 @@ package it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa; import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.converter.UUIDConverter; import it.aboutbits.springboot.toolbox.persistence.javatype.EmailAddressJavaType; import it.aboutbits.springboot.toolbox.persistence.javatype.IbanJavaType; import it.aboutbits.springboot.toolbox.persistence.javatype.ScaledBigDecimalJavaType; @@ -10,6 +11,7 @@ import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import it.aboutbits.springboot.toolbox.type.identity.EntityId; import it.aboutbits.springboot.toolbox.type.identity.Identified; +import jakarta.persistence.Convert; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -19,6 +21,8 @@ import lombok.Setter; import org.hibernate.annotations.JavaType; +import java.util.UUID; + @Entity @Getter @Setter @@ -41,6 +45,11 @@ public class CustomTypeTestModel implements Identified { @JavaType(ScaledBigDecimalJavaType.class) private ScaledBigDecimal accountBalance; + private UUID uuid; + + @Convert(converter = UUIDConverter.class) + private UUID uuidAsString; + @JavaType(ReferencedTestModel.ID.JavaType.class) private ReferencedTestModel.ID referencedId; diff --git a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/CustomTypeTestModelRepository.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/CustomTypeTestModelRepository.java index 6616575..cd89b75 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/CustomTypeTestModelRepository.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/CustomTypeTestModelRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; +import java.util.UUID; public interface CustomTypeTestModelRepository extends JpaRepository { Optional findByEmail(EmailAddress emailAddress); @@ -14,5 +15,8 @@ public interface CustomTypeTestModelRepository extends JpaRepository findByAccountBalance(ScaledBigDecimal accountBalance); + Optional findByUuid(UUID uuid); + Optional findByUuidAsString(UUID uuidAsString); + Optional findByReferencedId(ReferencedTestModel.ID otherId); } diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/WrapperTypesJpaTest.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/WrapperTypesJpaTest.java index 54a8779..3c1559d 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/WrapperTypesJpaTest.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/WrapperTypesJpaTest.java @@ -26,6 +26,8 @@ import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapShortRecord; import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapStringClass; import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapStringRecord; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapUUIDClass; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapUUIDRecord; import it.aboutbits.springboot.toolbox.support.ApplicationTest; import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import org.junit.jupiter.api.Nested; @@ -36,6 +38,7 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -453,6 +456,71 @@ void givenValues_inAndOut_shouldSucceed(Boolean boolValue) { .isEqualTo(savedItem); } } + + @Nested + class WrapUUIDRecordType { + @Test + void givenNull_inAndOut_shouldSucceed() { + var item = new WrapperTypesModel(); + item.setUuidValueClass(null); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByUuidValue(null); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + + @ParameterizedTest + @ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"}) + void givenValues_inAndOut_shouldSucceed(String uuidStringValue) { + var item = new WrapperTypesModel(); + item.setUuidValue(new WrapUUIDRecord(UUID.fromString(uuidStringValue))); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByUuidValue(savedItem.getUuidValue()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + + @Test + void givenNullString_inAndOut_shouldSucceed() { + var item = new WrapperTypesModel(); + item.setUuidValueAsString(null); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByUuidValueAsString(null); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + + @ParameterizedTest + @ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"}) + void givenStringValues_inAndOut_shouldSucceed(String uuidStringValue) { + var item = new WrapperTypesModel(); + item.setUuidValueAsString(new WrapUUIDRecord(UUID.fromString(uuidStringValue))); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByUuidValueAsString(savedItem.getUuidValueAsString()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + } } @Nested @@ -864,5 +932,70 @@ void givenValues_inAndOut_shouldSucceed(Boolean boolValue) { .isEqualTo(savedItem); } } + + @Nested + class WrapUUIDClassType { + @Test + void givenNull_inAndOut_shouldSucceed() { + var item = new WrapperTypesModel(); + item.setUuidValueClass(null); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByUuidValueClass(null); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + + @ParameterizedTest + @ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"}) + void givenValues_inAndOut_shouldSucceed(String uuidStringValue) { + var item = new WrapperTypesModel(); + item.setUuidValueClass(new WrapUUIDClass(UUID.fromString(uuidStringValue))); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByUuidValueClass(savedItem.getUuidValueClass()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + + @Test + void givenNullString_inAndOut_shouldSucceed() { + var item = new WrapperTypesModel(); + item.setUuidValueClassAsString(null); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByUuidValueClassAsString(null); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + + @ParameterizedTest + @ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"}) + void givenStringValues_inAndOut_shouldSucceed(String uuidStringValue) { + var item = new WrapperTypesModel(); + item.setUuidValueClassAsString(new WrapUUIDClass(UUID.fromString(uuidStringValue))); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByUuidValueClassAsString(savedItem.getUuidValueClassAsString()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + } } } diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapUUIDClassJavaType.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapUUIDClassJavaType.java new file mode 100644 index 0000000..e5f0e75 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapUUIDClassJavaType.java @@ -0,0 +1,11 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype; + +import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.base.WrappedUUIDJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapUUIDClass; + +public final class WrapUUIDClassJavaType extends WrappedUUIDJavaType implements AutoRegisteredJavaType { + public WrapUUIDClassJavaType() { + super(WrapUUIDClass.class); + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapUUIDRecordJavaType.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapUUIDRecordJavaType.java new file mode 100644 index 0000000..23ed29a --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapUUIDRecordJavaType.java @@ -0,0 +1,11 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype; + +import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.base.WrappedUUIDJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapUUIDRecord; + +public final class WrapUUIDRecordJavaType extends WrappedUUIDJavaType implements AutoRegisteredJavaType { + public WrapUUIDRecordJavaType() { + super(WrapUUIDRecord.class); + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/jpa/WrapperTypesModel.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/jpa/WrapperTypesModel.java index 6be216d..8d5e81a 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/jpa/WrapperTypesModel.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/jpa/WrapperTypesModel.java @@ -26,6 +26,8 @@ import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapShortRecordJavaType; import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapStringClassJavaType; import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapStringRecordJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapUUIDClassJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapUUIDRecordJavaType; import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapBigDecimalClass; import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapBigDecimalRecord; import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapBigIntegerClass; @@ -50,6 +52,8 @@ import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapShortRecord; import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapStringClass; import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapStringRecord; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapUUIDClass; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapUUIDRecord; import it.aboutbits.springboot.toolbox.type.identity.EntityId; import it.aboutbits.springboot.toolbox.type.identity.Identified; import jakarta.persistence.Entity; @@ -60,6 +64,9 @@ import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.JavaType; +import org.hibernate.annotations.JdbcType; +import org.hibernate.type.descriptor.jdbc.CharJdbcType; +import org.hibernate.type.descriptor.jdbc.UUIDJdbcType; @Entity @Getter @@ -119,6 +126,16 @@ public class WrapperTypesModel implements Identified { @JavaType(WrapBooleanRecordJavaType.class) private WrapBooleanRecord boolValue; + @SuppressWarnings("JpaAttributeTypeInspection") + @JavaType(WrapUUIDRecordJavaType.class) + @JdbcType(UUIDJdbcType.class) + private WrapUUIDRecord uuidValue; + + @SuppressWarnings("JpaAttributeTypeInspection") + @JavaType(WrapUUIDRecordJavaType.class) + @JdbcType(CharJdbcType.class) + private WrapUUIDRecord uuidValueAsString; + @SuppressWarnings("JpaAttributeTypeInspection") @JavaType(WrapBigDecimalClassJavaType.class) private WrapBigDecimalClass bigDecimalValueClass; @@ -167,6 +184,15 @@ public class WrapperTypesModel implements Identified { @JavaType(WrapBooleanClassJavaType.class) private WrapBooleanClass boolValueClass; + @SuppressWarnings("JpaAttributeTypeInspection") + @JavaType(WrapUUIDClassJavaType.class) + @JdbcType(UUIDJdbcType.class) + private WrapUUIDClass uuidValueClass; + + @SuppressWarnings("JpaAttributeTypeInspection") + @JavaType(WrapUUIDClassJavaType.class) + @JdbcType(CharJdbcType.class) + private WrapUUIDClass uuidValueClassAsString; public record ID( Long value diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/jpa/WrapperTypesModelRepository.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/jpa/WrapperTypesModelRepository.java index 10b271f..69e7532 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/jpa/WrapperTypesModelRepository.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/jpa/WrapperTypesModelRepository.java @@ -24,6 +24,8 @@ import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapShortRecord; import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapStringClass; import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapStringRecord; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapUUIDClass; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapUUIDRecord; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; @@ -53,6 +55,10 @@ public interface WrapperTypesModelRepository extends JpaRepository findByCharValue(WrapCharacterRecord value); + Optional findByUuidValue(WrapUUIDRecord value); + + Optional findByUuidValueAsString(WrapUUIDRecord value); + Optional findByBigDecimalValueClass(WrapBigDecimalClass value); Optional findByBigIntegerValueClass(WrapBigIntegerClass value); @@ -76,4 +82,8 @@ public interface WrapperTypesModelRepository extends JpaRepository findByByteValueClass(WrapByteClass value); Optional findByCharValueClass(WrapCharacterClass value); + + Optional findByUuidValueClass(WrapUUIDClass value); + + Optional findByUuidValueClassAsString(WrapUUIDClass value); } diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapUUIDClass.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapUUIDClass.java new file mode 100644 index 0000000..3087dae --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapUUIDClass.java @@ -0,0 +1,17 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.type; + +import it.aboutbits.springboot.toolbox.type.CustomType; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.UUID; + +@Getter +@Accessors(fluent = true) +@EqualsAndHashCode +@RequiredArgsConstructor +public class WrapUUIDClass implements CustomType { + private final UUID value; +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapUUIDRecord.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapUUIDRecord.java new file mode 100644 index 0000000..0c93286 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapUUIDRecord.java @@ -0,0 +1,9 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.type; + +import it.aboutbits.springboot.toolbox.type.CustomType; + +import java.util.UUID; + +public record WrapUUIDRecord(UUID value) implements CustomType { + +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/transformer/TupleTransformerTest.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/transformer/TupleTransformerTest.java index 2965c33..7a7c54d 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/persistence/transformer/TupleTransformerTest.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/transformer/TupleTransformerTest.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -72,7 +73,9 @@ void createRecord_givenMixedObjects_shouldPass() { var tupleTransformer = new TupleTransformer<>(DataRecord.class); var l = Long.valueOf(7); var b = Boolean.valueOf(true); - var objectsUnderTest = new Object[]{l, b, "String123", false, SomeEnum.ENUM_1}; + var objectsUnderTest = new Object[]{ + l, b, "String123", false, SomeEnum.ENUM_1, UUID.fromString("f255a993-0086-4b4d-a62f-3aa174501f88") + }; // when var result = tupleTransformer.transform(objectsUnderTest); @@ -83,6 +86,7 @@ void createRecord_givenMixedObjects_shouldPass() { assertThat(result.string).isEqualTo("String123"); assertThat(result.booleanPrimitive).isFalse(); assertThat(result.anEnum).isEqualTo(SomeEnum.ENUM_1); + assertThat(result.uuid).isEqualTo(UUID.fromString("f255a993-0086-4b4d-a62f-3aa174501f88")); } @@ -92,7 +96,9 @@ void createRecord_givenMixedObjects_enumValueAsString_shouldPass() { var tupleTransformer = new TupleTransformer<>(DataRecord.class); var l = Long.valueOf(7); var b = Boolean.valueOf(true); - var objectsUnderTest = new Object[]{l, b, "String123", false, "ENUM_1"}; + var objectsUnderTest = new Object[]{ + l, b, "String123", false, "ENUM_1", UUID.fromString("f255a993-0086-4b4d-a62f-3aa174501f88") + }; // when var result = tupleTransformer.transform(objectsUnderTest); @@ -103,6 +109,7 @@ void createRecord_givenMixedObjects_enumValueAsString_shouldPass() { assertThat(result.string).isEqualTo("String123"); assertThat(result.booleanPrimitive).isFalse(); assertThat(result.anEnum).isEqualTo(SomeEnum.ENUM_1); + assertThat(result.uuid).isEqualTo(UUID.fromString("f255a993-0086-4b4d-a62f-3aa174501f88")); } @@ -110,7 +117,14 @@ void createRecord_givenMixedObjects_enumValueAsString_shouldPass() { void createRecordInsideAClass_givenMixedObjects_shouldPass() { // given var tupleTransformer = new TupleTransformer<>(DataRecordParent.class); - var rec = new DataRecord(7L, true, "String123", false, SomeEnum.ENUM_1); + var rec = new DataRecord( + 7L, + true, + "String123", + false, + SomeEnum.ENUM_1, + UUID.fromString("f255a993-0086-4b4d-a62f-3aa174501f88") + ); var objectsUnderTest = new Object[]{rec, 33}; // when @@ -122,6 +136,7 @@ void createRecordInsideAClass_givenMixedObjects_shouldPass() { assertThat(result.dataRecord.string).isEqualTo("String123"); assertThat(result.dataRecord.booleanPrimitive).isFalse(); assertThat(result.dataRecord.anEnum).isEqualTo(SomeEnum.ENUM_1); + assertThat(result.dataRecord.uuid).isEqualTo(UUID.fromString("f255a993-0086-4b4d-a62f-3aa174501f88")); assertThat(result.someOtherField).isEqualTo(33); } @@ -130,7 +145,9 @@ void createRecordInsideAClass_givenMixedObjects_shouldPass() { void createRecord_givenMixedObjects_someNullValues_shouldPass() { // given var tupleTransformer = new TupleTransformer<>(DataRecord.class); - var objectsUnderTest = new Object[]{null, null, "String123", false, null}; + var objectsUnderTest = new Object[]{ + null, null, "String123", false, null, UUID.fromString("f255a993-0086-4b4d-a62f-3aa174501f88") + }; // when var result = tupleTransformer.transform(objectsUnderTest); @@ -141,6 +158,7 @@ void createRecord_givenMixedObjects_someNullValues_shouldPass() { assertThat(result.string).isEqualTo("String123"); assertThat(result.booleanPrimitive).isFalse(); assertThat(result.anEnum).isNull(); + assertThat(result.uuid).isEqualTo(UUID.fromString("f255a993-0086-4b4d-a62f-3aa174501f88")); } @@ -148,8 +166,22 @@ void createRecord_givenMixedObjects_someNullValues_shouldPass() { void createRecordInsideAClass_givenMixedObjectsAsList_shouldPass() { // given var tupleTransformer = new TupleTransformer<>(DataRecordParentWithList.class); - var rec1 = new DataRecord(7L, true, "String111", false, SomeEnum.ENUM_1); - var rec2 = new DataRecord(0L, null, "String222", false, SomeEnum.ENUM_2); + var rec1 = new DataRecord( + 7L, + true, + "String111", + false, + SomeEnum.ENUM_1, + UUID.fromString("c3221989-b494-4227-b7e1-2f84fdc048f7") + ); + var rec2 = new DataRecord( + 0L, + null, + "String222", + false, + SomeEnum.ENUM_2, + UUID.fromString("25f6e0c2-4628-43a1-a703-e80926496fd1") + ); var list = new ArrayList<>(); list.add(rec1); list.add(rec2); @@ -159,19 +191,21 @@ void createRecordInsideAClass_givenMixedObjectsAsList_shouldPass() { var result = tupleTransformer.transform(objectsUnderTest); // then - var firstRecord = result.dataRecords.get(0); + var firstRecord = result.dataRecords.getFirst(); assertThat(firstRecord.longField).isEqualTo(7L); assertThat(firstRecord.booleanBoxed).isTrue(); assertThat(firstRecord.string).isEqualTo("String111"); assertThat(firstRecord.booleanPrimitive).isFalse(); assertThat(firstRecord.anEnum).isEqualTo(SomeEnum.ENUM_1); + assertThat(firstRecord.uuid).isEqualTo(UUID.fromString("c3221989-b494-4227-b7e1-2f84fdc048f7")); var secondRecord = result.dataRecords.get(1); - assertThat(secondRecord.longField).isEqualTo(0L); + assertThat(secondRecord.longField).isZero(); assertThat(secondRecord.booleanBoxed).isNull(); assertThat(secondRecord.string).isEqualTo("String222"); assertThat(secondRecord.booleanPrimitive).isFalse(); assertThat(secondRecord.anEnum).isEqualTo(SomeEnum.ENUM_2); + assertThat(secondRecord.uuid).isEqualTo(UUID.fromString("25f6e0c2-4628-43a1-a703-e80926496fd1")); assertThat(result.someOtherField).isEqualTo(33); } @@ -189,7 +223,8 @@ protected record DataRecord( Boolean booleanBoxed, String string, boolean booleanPrimitive, - SomeEnum anEnum + SomeEnum anEnum, + UUID uuid ) { } diff --git a/src/test/resources/db/changelog/2024-09-06-create-custom-type-testing-table.yml b/src/test/resources/db/changelog/2024-09-06-create-custom-type-testing-table.yml index 9ee001a..245f6c7 100644 --- a/src/test/resources/db/changelog/2024-09-06-create-custom-type-testing-table.yml +++ b/src/test/resources/db/changelog/2024-09-06-create-custom-type-testing-table.yml @@ -25,3 +25,9 @@ databaseChangeLog: - column: name: referenced_id type: bigint + - column: + name: uuid + type: uuid + - column: + name: uuid_as_string + type: char(36) diff --git a/src/test/resources/db/changelog/2024-09-06-create-wrapper-type-testing-table.yml b/src/test/resources/db/changelog/2024-09-06-create-wrapper-type-testing-table.yml index fd963ca..d4a6270 100644 --- a/src/test/resources/db/changelog/2024-09-06-create-wrapper-type-testing-table.yml +++ b/src/test/resources/db/changelog/2024-09-06-create-wrapper-type-testing-table.yml @@ -49,6 +49,12 @@ databaseChangeLog: - column: name: bool_value type: boolean + - column: + name: uuid_value + type: uuid + - column: + name: uuid_value_as_string + type: char(36) - column: name: big_decimal_value_class type: double @@ -85,3 +91,9 @@ databaseChangeLog: - column: name: bool_value_class type: boolean + - column: + name: uuid_value_class + type: uuid + - column: + name: uuid_value_class_as_string + type: char(36)