diff --git a/pom.xml b/pom.xml index 68764cd..4d78bfe 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ lombok true - + @@ -88,6 +88,54 @@ org.springframework.boot spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-web + test + + + + + org.liquibase + liquibase-core + test + + + org.postgresql + postgresql + test + + + + + org.testcontainers + testcontainers + 1.20.1 + test + + + org.hamcrest + hamcrest-core + + + org.hamcrest + hamcrest-library + + + + + org.testcontainers + junit-jupiter + 1.20.1 + test + + + org.testcontainers + postgresql + 1.20.1 + test diff --git a/src/main/java/it/aboutbits/springboot/toolbox/boot/type/CustomTypeConfiguration.java b/src/main/java/it/aboutbits/springboot/toolbox/boot/type/CustomTypeConfiguration.java index 83e5550..09d4f48 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/boot/type/CustomTypeConfiguration.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/boot/type/CustomTypeConfiguration.java @@ -1,11 +1,9 @@ package it.aboutbits.springboot.toolbox.boot.type; -import it.aboutbits.springboot.toolbox.reflection.util.ClassScannerUtil; import it.aboutbits.springboot.toolbox.type.CustomType; import it.aboutbits.springboot.toolbox.type.jackson.CustomTypeDeserializer; import it.aboutbits.springboot.toolbox.type.jackson.CustomTypeSerializer; import it.aboutbits.springboot.toolbox.type.mvc.CustomTypePropertyEditor; -import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.context.annotation.Bean; @@ -14,39 +12,22 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.InitBinder; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Set; -import java.util.stream.Collectors; @Slf4j @Configuration public class CustomTypeConfiguration { - public static final String LIBRARY_BASE_PACKAGE_NAME = "it.aboutbits.springboot.toolbox"; - - private String[] packageNamesToScan; - private ClassScannerUtil.ClassScanner classScanner; - - public void setAdditionalTypePackages(String[] additionalTypePackages) { - var tmp = new ArrayList(); - tmp.add(LIBRARY_BASE_PACKAGE_NAME); - tmp.addAll(Arrays.asList(additionalTypePackages)); - - this.packageNamesToScan = tmp.toArray(new String[0]); - - classScanner = ClassScannerUtil.getScannerForPackages(packageNamesToScan); - } + @ControllerAdvice + public static class CustomTypePropertyBinder { + @SuppressWarnings("rawtypes") + private final Set> types; - @PostConstruct - public void init() { - log.info("CustomTypeConfiguration enabled. Scanning: {}", Arrays.toString(packageNamesToScan)); - } + public CustomTypePropertyBinder(CustomTypeScanner configuration) { + this.types = configuration.findAllCustomTypeRecords(); + } - @ControllerAdvice - public class CustomTypePropertyBinder { @InitBinder public void initBinder(WebDataBinder binder) { - var types = findAllCustomTypeRecords(); for (var clazz : types) { binder.registerCustomEditor(clazz, new CustomTypePropertyEditor<>(clazz)); } @@ -54,25 +35,16 @@ public void initBinder(WebDataBinder binder) { } @Bean - public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { - var types = findAllCustomTypeRecords(); + public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer(CustomTypeScanner configuration) { + var types = configuration.findAllCustomTypeRecords(); var deserializers = types.stream() .map(CustomTypeDeserializer::new) .toList() .toArray(new CustomTypeDeserializer[types.size()]); - classScanner.close(); - return builder -> builder .serializers(new CustomTypeSerializer()) .deserializers(deserializers); } - - @SuppressWarnings("rawtypes") - private Set> findAllCustomTypeRecords() { - return classScanner.getSubTypesOf(CustomType.class).stream() - .filter(Record.class::isAssignableFrom) - .collect(Collectors.toSet()); - } } diff --git a/src/main/java/it/aboutbits/springboot/toolbox/boot/type/CustomTypeConfigurationRegistrar.java b/src/main/java/it/aboutbits/springboot/toolbox/boot/type/CustomTypeConfigurationRegistrar.java index 33b3fe0..b2750bb 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/boot/type/CustomTypeConfigurationRegistrar.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/boot/type/CustomTypeConfigurationRegistrar.java @@ -21,8 +21,8 @@ public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionR ); var value = attributes.getStringArray("additionalTypePackages"); - var builder = BeanDefinitionBuilder.genericBeanDefinition(CustomTypeConfiguration.class); + var builder = BeanDefinitionBuilder.genericBeanDefinition(CustomTypeScanner.class); builder.addPropertyValue("additionalTypePackages", value); - registry.registerBeanDefinition("CustomTypeConfiguration", builder.getBeanDefinition()); + registry.registerBeanDefinition("CustomTypeScanner", builder.getBeanDefinition()); } } diff --git a/src/main/java/it/aboutbits/springboot/toolbox/boot/type/CustomTypeScanner.java b/src/main/java/it/aboutbits/springboot/toolbox/boot/type/CustomTypeScanner.java new file mode 100644 index 0000000..9d3327f --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/boot/type/CustomTypeScanner.java @@ -0,0 +1,43 @@ +package it.aboutbits.springboot.toolbox.boot.type; + +import it.aboutbits.springboot.toolbox.reflection.util.ClassScannerUtil; +import it.aboutbits.springboot.toolbox.type.CustomType; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +public class CustomTypeScanner { + public static final String LIBRARY_BASE_PACKAGE_NAME = "it.aboutbits.springboot.toolbox"; + + private String[] packageNamesToScan; + private ClassScannerUtil.ClassScanner classScanner; + + public void setAdditionalTypePackages(String[] additionalTypePackages) { + var tmp = new ArrayList(); + tmp.add(LIBRARY_BASE_PACKAGE_NAME); + tmp.addAll(Arrays.stream(additionalTypePackages) + .filter(item -> !item.isBlank()) + .collect(Collectors.toSet())); + + this.packageNamesToScan = tmp.toArray(new String[0]); + + classScanner = ClassScannerUtil.getScannerForPackages(packageNamesToScan); + } + + @PostConstruct + public void init() { + log.info("CustomTypeConfiguration enabled. Scanning: {}", Arrays.toString(packageNamesToScan)); + } + + @SuppressWarnings("rawtypes") + public Set> findAllCustomTypeRecords() { + return classScanner.getSubTypesOf(CustomType.class).stream() + .filter(Record.class::isAssignableFrom) + .collect(Collectors.toSet()); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/boot/type/RegisterCustomTypes.java b/src/main/java/it/aboutbits/springboot/toolbox/boot/type/RegisterCustomTypes.java index 5e4643e..4a6d307 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/boot/type/RegisterCustomTypes.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/boot/type/RegisterCustomTypes.java @@ -9,7 +9,7 @@ @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) -@Import(CustomTypeConfigurationRegistrar.class) +@Import({CustomTypeConfigurationRegistrar.class, CustomTypeConfiguration.class}) public @interface RegisterCustomTypes { String[] additionalTypePackages() default ""; } diff --git a/src/main/java/it/aboutbits/springboot/toolbox/boot/type/swagger/CustomTypeModelConverter.java b/src/main/java/it/aboutbits/springboot/toolbox/boot/type/swagger/CustomTypeModelConverter.java index 655b6e9..f99009a 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/boot/type/swagger/CustomTypeModelConverter.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/boot/type/swagger/CustomTypeModelConverter.java @@ -7,6 +7,7 @@ import it.aboutbits.springboot.toolbox.persistence.identity.EntityId; import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; import it.aboutbits.springboot.toolbox.type.CustomType; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import org.springframework.stereotype.Component; import java.math.BigDecimal; @@ -38,6 +39,9 @@ public Schema resolve( if (Long.class.isAssignableFrom(wrappedType)) { return context.resolve(new AnnotatedType(Long.TYPE)); } + if (BigDecimal.class.isAssignableFrom(wrappedType)) { + return context.resolve(new AnnotatedType(Long.TYPE)); + } if (Float.class.isAssignableFrom(wrappedType)) { return context.resolve(new AnnotatedType(Float.TYPE)); } @@ -47,6 +51,9 @@ public Schema resolve( if (BigDecimal.class.isAssignableFrom(wrappedType)) { return context.resolve(new AnnotatedType(Double.TYPE)); } + if (ScaledBigDecimal.class.isAssignableFrom(wrappedType)) { + return context.resolve(new AnnotatedType(Double.TYPE)); + } if (String.class.isAssignableFrom(wrappedType)) { return context.resolve(new AnnotatedType(String.class)); } diff --git a/src/main/java/it/aboutbits/springboot/toolbox/boot/type/swagger/CustomTypePropertyCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/boot/type/swagger/CustomTypePropertyCustomizer.java index 0e5f194..b2bd284 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/boot/type/swagger/CustomTypePropertyCustomizer.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/boot/type/swagger/CustomTypePropertyCustomizer.java @@ -6,11 +6,13 @@ import it.aboutbits.springboot.toolbox.persistence.identity.EntityId; import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; import it.aboutbits.springboot.toolbox.type.CustomType; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import lombok.extern.slf4j.Slf4j; import org.springdoc.core.customizers.PropertyCustomizer; import org.springframework.stereotype.Component; import java.math.BigDecimal; +import java.math.BigInteger; @Slf4j @Component @@ -53,6 +55,14 @@ public Schema customize(Schema property, AnnotatedType annotatedType) { property.set$ref(null); return property; } + if (BigInteger.class.isAssignableFrom(wrappedType)) { + property.type("integer"); + property.format("int64"); + property.setDescription(displayName); + property.setProperties(null); + property.set$ref(null); + return property; + } if (Float.class.isAssignableFrom(wrappedType)) { property.type("number"); property.format("float"); @@ -77,6 +87,15 @@ public Schema customize(Schema property, AnnotatedType annotatedType) { property.set$ref(null); return property; } + + if (ScaledBigDecimal.class.isAssignableFrom(wrappedType)) { + property.type("number"); + property.format(""); + property.setDescription(displayName); + property.setProperties(null); + property.set$ref(null); + return property; + } if (String.class.isAssignableFrom(wrappedType)) { property.type("string"); property.format(null); diff --git a/src/main/java/it/aboutbits/springboot/toolbox/boot/type/swagger/EntityIdModelConverter.java b/src/main/java/it/aboutbits/springboot/toolbox/boot/type/swagger/EntityIdModelConverter.java index 9863063..8a0cfc5 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/boot/type/swagger/EntityIdModelConverter.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/boot/type/swagger/EntityIdModelConverter.java @@ -6,9 +6,11 @@ import io.swagger.v3.oas.models.media.Schema; import it.aboutbits.springboot.toolbox.persistence.identity.EntityId; import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import org.springframework.stereotype.Component; import java.math.BigDecimal; +import java.math.BigInteger; import java.util.Iterator; @Component @@ -37,6 +39,9 @@ public Schema resolve( if (Long.class.isAssignableFrom(wrappedType)) { return context.resolve(new AnnotatedType(Long.TYPE)); } + if (BigInteger.class.isAssignableFrom(wrappedType)) { + return context.resolve(new AnnotatedType(Long.TYPE)); + } if (Float.class.isAssignableFrom(wrappedType)) { return context.resolve(new AnnotatedType(Float.TYPE)); } @@ -46,6 +51,9 @@ public Schema resolve( if (BigDecimal.class.isAssignableFrom(wrappedType)) { return context.resolve(new AnnotatedType(Double.TYPE)); } + if (ScaledBigDecimal.class.isAssignableFrom(wrappedType)) { + return context.resolve(new AnnotatedType(Double.TYPE)); + } if (String.class.isAssignableFrom(wrappedType)) { return context.resolve(new AnnotatedType(String.class)); } diff --git a/src/main/java/it/aboutbits/springboot/toolbox/boot/type/swagger/EntityIdPropertyCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/boot/type/swagger/EntityIdPropertyCustomizer.java index 12367b3..968ee9f 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/boot/type/swagger/EntityIdPropertyCustomizer.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/boot/type/swagger/EntityIdPropertyCustomizer.java @@ -5,12 +5,14 @@ import io.swagger.v3.oas.models.media.Schema; import it.aboutbits.springboot.toolbox.persistence.identity.EntityId; import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.springdoc.core.customizers.PropertyCustomizer; import org.springframework.stereotype.Component; import java.math.BigDecimal; +import java.math.BigInteger; @Slf4j @Component @@ -59,6 +61,14 @@ public Schema customize(Schema property, AnnotatedType annotatedType) { property.set$ref(null); return property; } + if (BigInteger.class.isAssignableFrom(wrappedType)) { + property.type("integer"); + property.format("int64"); + property.setDescription(displayName); + property.setProperties(null); + property.set$ref(null); + return property; + } if (Float.class.isAssignableFrom(wrappedType)) { property.type("number"); property.format("float"); @@ -83,6 +93,14 @@ public Schema customize(Schema property, AnnotatedType annotatedType) { property.set$ref(null); return property; } + if (ScaledBigDecimal.class.isAssignableFrom(wrappedType)) { + property.type("number"); + property.format(""); + property.setDescription(displayName); + property.setProperties(null); + property.set$ref(null); + return property; + } if (String.class.isAssignableFrom(wrappedType)) { property.type("string"); property.format(null); diff --git a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedBigDecimalJavaType.java b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedBigDecimalJavaType.java index 166d3e9..d9b2382 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedBigDecimalJavaType.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedBigDecimalJavaType.java @@ -1,5 +1,6 @@ package it.aboutbits.springboot.toolbox.persistence.javatype.base; +import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; import it.aboutbits.springboot.toolbox.type.CustomType; import lombok.SneakyThrows; import org.hibernate.type.descriptor.WrapperOptions; @@ -7,13 +8,18 @@ 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.math.BigDecimal; import java.sql.Types; public abstract class WrappedBigDecimalJavaType> extends AbstractClassJavaType { + private final transient Constructor canonicalConstructor; + protected WrappedBigDecimalJavaType(Class type) { super(type); + + this.canonicalConstructor = RecordReflectionUtil.getCanonicalConstructor(type); } @Override @@ -23,6 +29,7 @@ public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { .getDescriptor(Types.DOUBLE); } + @SuppressWarnings("unchecked") @Override public X unwrap(T id, Class aClass, WrapperOptions wrapperOptions) { var javaTypeClass = getJavaTypeClass(); @@ -33,12 +40,14 @@ public X unwrap(T id, Class aClass, WrapperOptions wrapperOptions) { if (javaTypeClass.isAssignableFrom(aClass)) { return (X) id; } - if (BigDecimal.class.isAssignableFrom(aClass)) { - return (X) id.value(); + if (Double.class.isAssignableFrom(aClass)) { + return (X) Double.valueOf(id.value().doubleValue()); } + throw unknownUnwrap(aClass); } + @SuppressWarnings("unchecked") @SneakyThrows({InstantiationException.class, IllegalAccessException.class, InvocationTargetException.class}) @Override public T wrap(X value, WrapperOptions wrapperOptions) { @@ -50,12 +59,10 @@ public T wrap(X value, WrapperOptions wrapperOptions) { if (clazz.isInstance(value)) { return (T) value; } - if (value instanceof BigDecimal bigDecimal) { - return (T) clazz.getConstructors()[0].newInstance(bigDecimal); - } - if (value instanceof String stringValue) { - return (T) clazz.getConstructors()[0].newInstance(new BigDecimal(stringValue)); + if (value instanceof Double doubleValue) { + return canonicalConstructor.newInstance(BigDecimal.valueOf(doubleValue)); } + throw unknownWrap(value.getClass()); } } diff --git a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedBigIntegerJavaType.java b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedBigIntegerJavaType.java new file mode 100644 index 0000000..ef2094c --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedBigIntegerJavaType.java @@ -0,0 +1,68 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.base; + +import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; +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.math.BigInteger; +import java.sql.Types; + +public abstract class WrappedBigIntegerJavaType> extends AbstractClassJavaType { + private final transient Constructor canonicalConstructor; + + protected WrappedBigIntegerJavaType(Class type) { + super(type); + + this.canonicalConstructor = RecordReflectionUtil.getCanonicalConstructor(type); + } + + @Override + public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { + return indicators.getTypeConfiguration() + .getJdbcTypeRegistry() + .getDescriptor(Types.BIGINT); + } + + @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 (Long.class.isAssignableFrom(aClass)) { + return (X) Long.valueOf(id.value().longValue()); + } + + 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; + } + if (value instanceof Long longValue) { + return canonicalConstructor.newInstance(BigInteger.valueOf(longValue)); + } + + throw unknownWrap(value.getClass()); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedDoubleJavaType.java b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedDoubleJavaType.java index 2f88ae6..37a6b36 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedDoubleJavaType.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedDoubleJavaType.java @@ -1,5 +1,6 @@ package it.aboutbits.springboot.toolbox.persistence.javatype.base; +import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; import it.aboutbits.springboot.toolbox.type.CustomType; import lombok.SneakyThrows; import org.hibernate.type.descriptor.WrapperOptions; @@ -7,12 +8,17 @@ 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.sql.Types; -public abstract class WrappedDoubleJavaType> extends AbstractClassJavaType { +public abstract class WrappedDoubleJavaType> extends AbstractClassJavaType { + private final transient Constructor canonicalConstructor; + protected WrappedDoubleJavaType(Class type) { super(type); + + this.canonicalConstructor = RecordReflectionUtil.getCanonicalConstructor(type); } @Override @@ -22,6 +28,7 @@ public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { .getDescriptor(Types.DOUBLE); } + @SuppressWarnings("unchecked") @Override public X unwrap(T id, Class aClass, WrapperOptions wrapperOptions) { var javaTypeClass = getJavaTypeClass(); @@ -35,9 +42,11 @@ public X unwrap(T id, Class aClass, WrapperOptions wrapperOptions) { if (Double.class.isAssignableFrom(aClass)) { return (X) id.value(); } + throw unknownUnwrap(aClass); } + @SuppressWarnings("unchecked") @SneakyThrows({InstantiationException.class, IllegalAccessException.class, InvocationTargetException.class}) @Override public T wrap(X value, WrapperOptions wrapperOptions) { @@ -50,11 +59,9 @@ public T wrap(X value, WrapperOptions wrapperOptions) { return (T) value; } if (value instanceof Double doubleValue) { - return (T) clazz.getConstructors()[0].newInstance(doubleValue); - } - if (value instanceof String stringValue) { - return (T) clazz.getConstructors()[0].newInstance(Double.parseDouble(stringValue)); + return canonicalConstructor.newInstance(doubleValue); } + throw unknownWrap(value.getClass()); } } diff --git a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedFloatJavaType.java b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedFloatJavaType.java index 513079b..c862e39 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedFloatJavaType.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedFloatJavaType.java @@ -1,5 +1,6 @@ package it.aboutbits.springboot.toolbox.persistence.javatype.base; +import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; import it.aboutbits.springboot.toolbox.type.CustomType; import lombok.SneakyThrows; import org.hibernate.type.descriptor.WrapperOptions; @@ -7,21 +8,27 @@ 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.sql.Types; -public abstract class WrappedFloatJavaType> extends AbstractClassJavaType { +public abstract class WrappedFloatJavaType> extends AbstractClassJavaType { + private final transient Constructor canonicalConstructor; + protected WrappedFloatJavaType(Class type) { super(type); + + this.canonicalConstructor = RecordReflectionUtil.getCanonicalConstructor(type); } @Override public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { return indicators.getTypeConfiguration() .getJdbcTypeRegistry() - .getDescriptor(Types.DOUBLE); + .getDescriptor(Types.FLOAT); } + @SuppressWarnings("unchecked") @Override public X unwrap(T id, Class aClass, WrapperOptions wrapperOptions) { var javaTypeClass = getJavaTypeClass(); @@ -35,9 +42,11 @@ public X unwrap(T id, Class aClass, WrapperOptions wrapperOptions) { if (Float.class.isAssignableFrom(aClass)) { return (X) id.value(); } + throw unknownUnwrap(aClass); } + @SuppressWarnings("unchecked") @SneakyThrows({InstantiationException.class, IllegalAccessException.class, InvocationTargetException.class}) @Override public T wrap(X value, WrapperOptions wrapperOptions) { @@ -50,11 +59,9 @@ public T wrap(X value, WrapperOptions wrapperOptions) { return (T) value; } if (value instanceof Float floatValue) { - return (T) clazz.getConstructors()[0].newInstance(floatValue); - } - if (value instanceof String stringValue) { - return (T) clazz.getConstructors()[0].newInstance(Float.parseFloat(stringValue)); + return canonicalConstructor.newInstance(floatValue); } + throw unknownWrap(value.getClass()); } } diff --git a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedIntegerJavaType.java b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedIntegerJavaType.java index 3b53e55..8c4d933 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedIntegerJavaType.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedIntegerJavaType.java @@ -1,5 +1,6 @@ package it.aboutbits.springboot.toolbox.persistence.javatype.base; +import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; import it.aboutbits.springboot.toolbox.type.CustomType; import lombok.SneakyThrows; import org.hibernate.type.descriptor.WrapperOptions; @@ -7,21 +8,27 @@ 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.sql.Types; -public abstract class WrappedIntegerJavaType> extends AbstractClassJavaType { +public abstract class WrappedIntegerJavaType> extends AbstractClassJavaType { + private final transient Constructor canonicalConstructor; + protected WrappedIntegerJavaType(Class type) { super(type); + + this.canonicalConstructor = RecordReflectionUtil.getCanonicalConstructor(type); } @Override public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { return indicators.getTypeConfiguration() .getJdbcTypeRegistry() - .getDescriptor(Types.BIGINT); + .getDescriptor(Types.INTEGER); } + @SuppressWarnings("unchecked") @Override public X unwrap(T id, Class aClass, WrapperOptions wrapperOptions) { var javaTypeClass = getJavaTypeClass(); @@ -35,9 +42,11 @@ public X unwrap(T id, Class aClass, WrapperOptions wrapperOptions) { if (Integer.class.isAssignableFrom(aClass)) { return (X) id.value(); } + throw unknownUnwrap(aClass); } + @SuppressWarnings("unchecked") @SneakyThrows({InstantiationException.class, IllegalAccessException.class, InvocationTargetException.class}) @Override public T wrap(X value, WrapperOptions wrapperOptions) { @@ -50,11 +59,9 @@ public T wrap(X value, WrapperOptions wrapperOptions) { return (T) value; } if (value instanceof Integer integerValue) { - return (T) clazz.getConstructors()[0].newInstance(integerValue); - } - if (value instanceof String stringValue) { - return (T) clazz.getConstructors()[0].newInstance(Integer.parseInt(stringValue)); + return canonicalConstructor.newInstance(integerValue); } + throw unknownWrap(value.getClass()); } } diff --git a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedLongJavaType.java b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedLongJavaType.java index efc5288..4949394 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedLongJavaType.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedLongJavaType.java @@ -1,5 +1,6 @@ package it.aboutbits.springboot.toolbox.persistence.javatype.base; +import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; import it.aboutbits.springboot.toolbox.type.CustomType; import lombok.SneakyThrows; import org.hibernate.type.descriptor.WrapperOptions; @@ -7,12 +8,17 @@ 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.sql.Types; public abstract class WrappedLongJavaType> extends AbstractClassJavaType { + private final transient Constructor canonicalConstructor; + protected WrappedLongJavaType(Class type) { super(type); + + this.canonicalConstructor = RecordReflectionUtil.getCanonicalConstructor(type); } @Override @@ -22,6 +28,7 @@ public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { .getDescriptor(Types.BIGINT); } + @SuppressWarnings("unchecked") @Override public X unwrap(T id, Class aClass, WrapperOptions wrapperOptions) { var javaTypeClass = getJavaTypeClass(); @@ -35,9 +42,11 @@ public X unwrap(T id, Class aClass, WrapperOptions wrapperOptions) { if (Long.class.isAssignableFrom(aClass)) { return (X) id.value(); } + throw unknownUnwrap(aClass); } + @SuppressWarnings("unchecked") @SneakyThrows({InstantiationException.class, IllegalAccessException.class, InvocationTargetException.class}) @Override public T wrap(X value, WrapperOptions wrapperOptions) { @@ -50,11 +59,9 @@ public T wrap(X value, WrapperOptions wrapperOptions) { return (T) value; } if (value instanceof Long longValue) { - return (T) clazz.getConstructors()[0].newInstance(longValue); - } - if (value instanceof String stringValue) { - return (T) clazz.getConstructors()[0].newInstance(Long.parseLong(stringValue)); + return canonicalConstructor.newInstance(longValue); } + throw unknownWrap(value.getClass()); } } diff --git a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedScaledBigDecimalJavaType.java b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedScaledBigDecimalJavaType.java index 9472cb0..3450c14 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedScaledBigDecimalJavaType.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedScaledBigDecimalJavaType.java @@ -1,5 +1,6 @@ package it.aboutbits.springboot.toolbox.persistence.javatype.base; +import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; import it.aboutbits.springboot.toolbox.type.CustomType; import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; import lombok.SneakyThrows; @@ -8,13 +9,17 @@ 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.math.BigDecimal; import java.sql.Types; -public abstract class WrappedScaledBigDecimalJavaType> extends AbstractClassJavaType { +public abstract class WrappedScaledBigDecimalJavaType> extends AbstractClassJavaType { + private final transient Constructor canonicalConstructor; + protected WrappedScaledBigDecimalJavaType(Class type) { super(type); + + this.canonicalConstructor = RecordReflectionUtil.getCanonicalConstructor(type); } @Override @@ -24,6 +29,7 @@ public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { .getDescriptor(Types.DOUBLE); } + @SuppressWarnings("unchecked") @Override public X unwrap(T id, Class aClass, WrapperOptions wrapperOptions) { var javaTypeClass = getJavaTypeClass(); @@ -34,12 +40,14 @@ public X unwrap(T id, Class aClass, WrapperOptions wrapperOptions) { if (javaTypeClass.isAssignableFrom(aClass)) { return (X) id; } - if (ScaledBigDecimal.class.isAssignableFrom(aClass)) { - return (X) id.value(); + if (Double.class.isAssignableFrom(aClass)) { + return (X) Double.valueOf(id.value().value().doubleValue()); } + throw unknownUnwrap(aClass); } + @SuppressWarnings("unchecked") @SneakyThrows({InstantiationException.class, IllegalAccessException.class, InvocationTargetException.class}) @Override public T wrap(X value, WrapperOptions wrapperOptions) { @@ -51,12 +59,10 @@ public T wrap(X value, WrapperOptions wrapperOptions) { if (clazz.isInstance(value)) { return (T) value; } - if (value instanceof ScaledBigDecimal scaledBigDecimalValue) { - return (T) clazz.getConstructors()[0].newInstance(scaledBigDecimalValue); - } - if (value instanceof String stringValue) { - return (T) clazz.getConstructors()[0].newInstance(new ScaledBigDecimal(stringValue)); + if (value instanceof Double doubleValue) { + return canonicalConstructor.newInstance(new ScaledBigDecimal(doubleValue)); } + throw unknownWrap(value.getClass()); } } diff --git a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedShortJavaType.java b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedShortJavaType.java index 757bc3e..b5782fa 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedShortJavaType.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedShortJavaType.java @@ -1,5 +1,6 @@ package it.aboutbits.springboot.toolbox.persistence.javatype.base; +import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; import it.aboutbits.springboot.toolbox.type.CustomType; import lombok.SneakyThrows; import org.hibernate.type.descriptor.WrapperOptions; @@ -7,21 +8,27 @@ 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.sql.Types; -public abstract class WrappedShortJavaType> extends AbstractClassJavaType { +public abstract class WrappedShortJavaType> extends AbstractClassJavaType { + private final transient Constructor canonicalConstructor; + protected WrappedShortJavaType(Class type) { super(type); + + this.canonicalConstructor = RecordReflectionUtil.getCanonicalConstructor(type); } @Override public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { return indicators.getTypeConfiguration() .getJdbcTypeRegistry() - .getDescriptor(Types.BIGINT); + .getDescriptor(Types.SMALLINT); } + @SuppressWarnings("unchecked") @Override public X unwrap(T id, Class aClass, WrapperOptions wrapperOptions) { var javaTypeClass = getJavaTypeClass(); @@ -35,9 +42,11 @@ public X unwrap(T id, Class aClass, WrapperOptions wrapperOptions) { if (Short.class.isAssignableFrom(aClass)) { return (X) id.value(); } + throw unknownUnwrap(aClass); } + @SuppressWarnings("unchecked") @SneakyThrows({InstantiationException.class, IllegalAccessException.class, InvocationTargetException.class}) @Override public T wrap(X value, WrapperOptions wrapperOptions) { @@ -50,11 +59,9 @@ public T wrap(X value, WrapperOptions wrapperOptions) { return (T) value; } if (value instanceof Short shortValue) { - return (T) clazz.getConstructors()[0].newInstance(shortValue); - } - if (value instanceof String stringValue) { - return (T) clazz.getConstructors()[0].newInstance(Short.parseShort(stringValue)); + return canonicalConstructor.newInstance(shortValue); } + throw unknownWrap(value.getClass()); } } diff --git a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedStringJavaType.java b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedStringJavaType.java index 32eea0f..35b9fd6 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedStringJavaType.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedStringJavaType.java @@ -1,5 +1,6 @@ package it.aboutbits.springboot.toolbox.persistence.javatype.base; +import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; import it.aboutbits.springboot.toolbox.type.CustomType; import lombok.SneakyThrows; import org.hibernate.type.descriptor.WrapperOptions; @@ -7,12 +8,17 @@ 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.sql.Types; public abstract class WrappedStringJavaType> extends AbstractClassJavaType { + private final transient Constructor canonicalConstructor; + protected WrappedStringJavaType(Class type) { super(type); + + this.canonicalConstructor = RecordReflectionUtil.getCanonicalConstructor(type); } @Override @@ -22,6 +28,7 @@ public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { .getDescriptor(Types.VARCHAR); } + @SuppressWarnings("unchecked") @Override public X unwrap(T id, Class aClass, WrapperOptions wrapperOptions) { var javaTypeClass = getJavaTypeClass(); @@ -35,9 +42,11 @@ public X unwrap(T id, Class aClass, WrapperOptions wrapperOptions) { if (String.class.isAssignableFrom(aClass)) { return (X) id.value(); } + throw unknownUnwrap(aClass); } + @SuppressWarnings("unchecked") @SneakyThrows({InstantiationException.class, IllegalAccessException.class, InvocationTargetException.class}) @Override public T wrap(X value, WrapperOptions wrapperOptions) { @@ -50,8 +59,9 @@ public T wrap(X value, WrapperOptions wrapperOptions) { return (T) value; } if (value instanceof String stringValue) { - return (T) clazz.getConstructors()[0].newInstance(stringValue); + return canonicalConstructor.newInstance(stringValue); } + throw unknownWrap(value.getClass()); } } diff --git a/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/ClassScannerUtil.java b/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/ClassScannerUtil.java index bc67ac7..13016ca 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/ClassScannerUtil.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/ClassScannerUtil.java @@ -19,17 +19,16 @@ public static final class ClassScanner { private final ScanResult scanResult; private ClassScanner(String... packages) { - try (var result = new ClassGraph() + var result = new ClassGraph() .enableAllInfo() .acceptPackages(packages) - .scan()) { - this.scanResult = result; - } + .scan(); + this.scanResult = result; } @SuppressWarnings("unchecked") public Set> getSubTypesOf(@NonNull Class clazz) { - return scanResult.getSubclasses(clazz).loadClasses() + return scanResult.getClassesImplementing(clazz).loadClasses() .stream() .map(item -> (Class) item) .collect(Collectors.toSet()); diff --git a/src/main/java/it/aboutbits/springboot/toolbox/type/ScaledBigDecimal.java b/src/main/java/it/aboutbits/springboot/toolbox/type/ScaledBigDecimal.java index 8256bee..5a4dda6 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/type/ScaledBigDecimal.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/type/ScaledBigDecimal.java @@ -206,6 +206,11 @@ public ScaledBigDecimal roundToScale(int scale) { return new ScaledBigDecimal(this.value().setScale(scale, RoundingMode.HALF_UP)); } + @Override + public String toString() { + return this.value().toString(); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/src/main/java/it/aboutbits/springboot/toolbox/type/jackson/CustomTypeDeserializer.java b/src/main/java/it/aboutbits/springboot/toolbox/type/jackson/CustomTypeDeserializer.java index b405351..1137753 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/type/jackson/CustomTypeDeserializer.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/type/jackson/CustomTypeDeserializer.java @@ -11,6 +11,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; +import java.math.BigInteger; import java.util.function.Function; public class CustomTypeDeserializer> extends JsonDeserializer { @@ -60,6 +61,9 @@ private static Function getTypeConverter(Class wrappedTyp if (Long.class.isAssignableFrom(wrappedType)) { return getLongConverter(); } + if (BigInteger.class.isAssignableFrom(wrappedType)) { + return getBigIntegerConverter(); + } if (Float.class.isAssignableFrom(wrappedType)) { return getFloatConverter(); } @@ -115,6 +119,16 @@ private static Function getFloatConverter() { }; } + private static Function getBigIntegerConverter() { + return jsonParser -> { + try { + return jsonParser.getBigIntegerValue(); + } catch (IOException e) { + throw new CustomTypeDeserializerException("Failed to read value as BigInteger.", e); + } + }; + } + private static Function getLongConverter() { return jsonParser -> { try { diff --git a/src/main/java/it/aboutbits/springboot/toolbox/type/mvc/CustomTypePropertyEditor.java b/src/main/java/it/aboutbits/springboot/toolbox/type/mvc/CustomTypePropertyEditor.java index 6310057..1b9af95 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/type/mvc/CustomTypePropertyEditor.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/type/mvc/CustomTypePropertyEditor.java @@ -11,6 +11,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; +import java.math.BigInteger; import java.util.function.Function; public final class CustomTypePropertyEditor> extends PropertyEditorSupport { @@ -63,6 +64,9 @@ private static Function getTextToTypeConverter(Class wrappedT if (Long.class.isAssignableFrom(wrappedType)) { return Long::parseLong; } + if (BigInteger.class.isAssignableFrom(wrappedType)) { + return BigInteger::new; + } if (Float.class.isAssignableFrom(wrappedType)) { return Float::parseFloat; } diff --git a/src/test/java/it/aboutbits/springboot/toolbox/TestApp.java b/src/test/java/it/aboutbits/springboot/toolbox/TestApp.java new file mode 100644 index 0000000..68f1947 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/TestApp.java @@ -0,0 +1,18 @@ +package it.aboutbits.springboot.toolbox; + +import it.aboutbits.springboot.toolbox.boot.type.RegisterCustomTypes; +import it.aboutbits.springboot.toolbox.boot.type.swagger.RegisterCustomSwaggerTypes; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SuppressWarnings("checkstyle:HideUtilityClassConstructor") +@SpringBootApplication +@RegisterCustomTypes +@RegisterCustomSwaggerTypes +public class TestApp { + + public static void main(String[] args) { + SpringApplication.run(TestApp.class, args); + } + +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/CustomTypeBindingsForControllerTest.java b/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/CustomTypeBindingsForControllerTest.java new file mode 100644 index 0000000..36fcf00 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/CustomTypeBindingsForControllerTest.java @@ -0,0 +1,190 @@ +package it.aboutbits.springboot.toolbox.boot.mvc; + +import com.fasterxml.jackson.databind.ObjectMapper; +import it.aboutbits.springboot.toolbox.boot.mvc.body.BodyWithEmailAddress; +import it.aboutbits.springboot.toolbox.boot.mvc.body.BodyWithIban; +import it.aboutbits.springboot.toolbox.boot.mvc.body.BodyWithScaledBigDecimal; +import it.aboutbits.springboot.toolbox.support.HttpTest; +import it.aboutbits.springboot.toolbox.type.EmailAddress; +import it.aboutbits.springboot.toolbox.type.Iban; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; +import lombok.NonNull; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.assertj.core.api.Assertions.assertThat; + +@HttpTest +public class CustomTypeBindingsForControllerTest { + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected ObjectMapper objectMapper; + + @Nested + class EmailAddressType { + @Test + void emailAddressAsPathVariable() throws Exception { + var value = new EmailAddress("herbert@aboutbits.it"); + + var resultAsString = performGetAndReturnResult( + String.format("/test/type/EmailAddress/as-path-variable/%s", value) + ); + + var actual = objectMapper.readValue(resultAsString, EmailAddress.class); + + assertThat(actual).isEqualTo(value); + } + + @Test + void emailAddressAsRequestParameter() throws Exception { + var value = new EmailAddress("herbert@aboutbits.it"); + + var resultAsString = performGetAndReturnResult( + String.format("/test/type/EmailAddress/as-request-parameter?value=%s", value) + ); + + var actual = objectMapper.readValue(resultAsString, EmailAddress.class); + + assertThat(actual).isEqualTo(value); + } + + @Test + void emailAddressAsBody() throws Exception { + var value = new BodyWithEmailAddress( + new EmailAddress("herbert@aboutbits.it") + ); + + var resultAsString = performPostAndReturnResult( + "/test/type/EmailAddress/as-body", + value + ); + + var actual = objectMapper.readValue(resultAsString, BodyWithEmailAddress.class); + + assertThat(actual).isEqualTo(value); + } + } + + @Nested + class IbanType { + @Test + void IbanAsPathVariable() throws Exception { + var value = new Iban("NL63ABNA7864733042"); + + var resultAsString = performGetAndReturnResult( + String.format("/test/type/Iban/as-path-variable/%s", value) + ); + + var actual = objectMapper.readValue(resultAsString, Iban.class); + + assertThat(actual).isEqualTo(value); + } + + @Test + void IbanAsRequestParameter() throws Exception { + var value = new Iban("NL63ABNA7864733042"); + + var resultAsString = performGetAndReturnResult( + String.format("/test/type/Iban/as-request-parameter?value=%s", value) + ); + + var actual = objectMapper.readValue(resultAsString, Iban.class); + + assertThat(actual).isEqualTo(value); + } + + @Test + void IbanAsBody() throws Exception { + var value = new BodyWithIban( + new Iban("NL63ABNA7864733042") + ); + + var resultAsString = performPostAndReturnResult( + "/test/type/Iban/as-body", + value + ); + + var actual = objectMapper.readValue(resultAsString, BodyWithIban.class); + + assertThat(actual).isEqualTo(value); + } + } + + @Nested + class ScaledBigDecimalType { + @ParameterizedTest + @ValueSource(doubles = {-1, 0, 1, -0.001, 0.001, -100_000_000, 100_000_000}) + void ScaledBigDecimalAsPathVariable(double doubleValue) throws Exception { + var value = new ScaledBigDecimal(doubleValue); + + var resultAsString = performGetAndReturnResult( + String.format("/test/type/ScaledBigDecimal/as-path-variable/%s", value) + ); + + var actual = objectMapper.readValue(resultAsString, ScaledBigDecimal.class); + + assertThat(actual).isEqualTo(value); + } + + @ParameterizedTest + @ValueSource(doubles = {-1, 0, 1, -0.001, 0.001, -100_000_000, 100_000_000}) + void ScaledBigDecimalAsRequestParameter(double doubleValue) throws Exception { + var value = new ScaledBigDecimal(doubleValue); + + var resultAsString = performGetAndReturnResult( + String.format("/test/type/ScaledBigDecimal/as-request-parameter?value=%s", value) + ); + + var actual = objectMapper.readValue(resultAsString, ScaledBigDecimal.class); + + assertThat(actual).isEqualTo(value); + } + + @ParameterizedTest + @ValueSource(doubles = {-1, 0, 1, -0.001, 0.001, -100_000_000, 100_000_000}) + void ScaledBigDecimalAsBody(double doubleValue) throws Exception { + var value = new BodyWithScaledBigDecimal( + new ScaledBigDecimal(doubleValue) + ); + + var resultAsString = performPostAndReturnResult( + "/test/type/ScaledBigDecimal/as-body", + value + ); + + var actual = objectMapper.readValue(resultAsString, BodyWithScaledBigDecimal.class); + + assertThat(actual).isEqualTo(value); + } + } + + private @NonNull String performGetAndReturnResult(@NonNull String url) throws Exception { + var requestBuilder = MockMvcRequestBuilders.get(url) + .contentType(MediaType.APPLICATION_JSON); + + return mockMvc.perform(requestBuilder) + .andReturn() + .getResponse() + .getContentAsString(); + } + + private @NonNull String performPostAndReturnResult(@NonNull String url, @NonNull Object body) throws Exception { + var requestBuilder = MockMvcRequestBuilders.post(url) + .content(objectMapper.writeValueAsString(body)) + .contentType(MediaType.APPLICATION_JSON); + + return mockMvc.perform(requestBuilder) + .andReturn() + .getResponse() + .getContentAsString(); + } + +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/EntityIdBindingsForControllerTest.java b/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/EntityIdBindingsForControllerTest.java new file mode 100644 index 0000000..65b414d --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/EntityIdBindingsForControllerTest.java @@ -0,0 +1,91 @@ +package it.aboutbits.springboot.toolbox.boot.mvc; + +import com.fasterxml.jackson.databind.ObjectMapper; +import it.aboutbits.springboot.toolbox.boot.mvc.body.BodyWithEntityId; +import it.aboutbits.springboot.toolbox.boot.persistence.impl.jpa.CustomTypeTestModel; +import it.aboutbits.springboot.toolbox.support.HttpTest; +import lombok.NonNull; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.assertj.core.api.Assertions.assertThat; + +@HttpTest +public class EntityIdBindingsForControllerTest { + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected ObjectMapper objectMapper; + + @Nested + class EntityId { + @Test + void emailAddressAsPathVariable() throws Exception { + var value = new CustomTypeTestModel.ID(512L); + + var resultAsString = performGetAndReturnResult( + String.format("/test/entity-id/CustomTypeTestModel.ID/as-path-variable/%s", value) + ); + + var actual = objectMapper.readValue(resultAsString, CustomTypeTestModel.ID.class); + + assertThat(actual).isEqualTo(value); + } + + @Test + void emailAddressAsRequestParameter() throws Exception { + var value = new CustomTypeTestModel.ID(512L); + + var resultAsString = performGetAndReturnResult( + String.format("/test/entity-id/CustomTypeTestModel.ID/as-request-parameter?value=%s", value) + ); + + var actual = objectMapper.readValue(resultAsString, CustomTypeTestModel.ID.class); + + assertThat(actual).isEqualTo(value); + } + + @Test + void emailAddressAsBody() throws Exception { + var value = new BodyWithEntityId( + new CustomTypeTestModel.ID(512L) + ); + + var resultAsString = performPostAndReturnResult( + "/test/entity-id/CustomTypeTestModel.ID/as-body", + value + ); + + var actual = objectMapper.readValue(resultAsString, BodyWithEntityId.class); + + assertThat(actual).isEqualTo(value); + } + } + + private @NonNull String performGetAndReturnResult(@NonNull String url) throws Exception { + var requestBuilder = MockMvcRequestBuilders.get(url) + .contentType(MediaType.APPLICATION_JSON); + + return mockMvc.perform(requestBuilder) + .andReturn() + .getResponse() + .getContentAsString(); + } + + private @NonNull String performPostAndReturnResult(@NonNull String url, @NonNull Object body) throws Exception { + var requestBuilder = MockMvcRequestBuilders.post(url) + .content(objectMapper.writeValueAsString(body)) + .contentType(MediaType.APPLICATION_JSON); + + return mockMvc.perform(requestBuilder) + .andReturn() + .getResponse() + .getContentAsString(); + } + +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/body/BodyWithEmailAddress.java b/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/body/BodyWithEmailAddress.java new file mode 100644 index 0000000..49d7c44 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/body/BodyWithEmailAddress.java @@ -0,0 +1,8 @@ +package it.aboutbits.springboot.toolbox.boot.mvc.body; + +import it.aboutbits.springboot.toolbox.type.EmailAddress; + +public record BodyWithEmailAddress( + EmailAddress emailAddress +) { +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/body/BodyWithEntityId.java b/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/body/BodyWithEntityId.java new file mode 100644 index 0000000..06c4a13 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/body/BodyWithEntityId.java @@ -0,0 +1,8 @@ +package it.aboutbits.springboot.toolbox.boot.mvc.body; + +import it.aboutbits.springboot.toolbox.boot.persistence.impl.jpa.CustomTypeTestModel; + +public record BodyWithEntityId( + CustomTypeTestModel.ID entityId +) { +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/body/BodyWithIban.java b/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/body/BodyWithIban.java new file mode 100644 index 0000000..2d24b5d --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/body/BodyWithIban.java @@ -0,0 +1,8 @@ +package it.aboutbits.springboot.toolbox.boot.mvc.body; + +import it.aboutbits.springboot.toolbox.type.Iban; + +public record BodyWithIban( + Iban iban +) { +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/body/BodyWithScaledBigDecimal.java b/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/body/BodyWithScaledBigDecimal.java new file mode 100644 index 0000000..ea2d5e7 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/body/BodyWithScaledBigDecimal.java @@ -0,0 +1,8 @@ +package it.aboutbits.springboot.toolbox.boot.mvc.body; + +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; + +public record BodyWithScaledBigDecimal( + ScaledBigDecimal scaledBigDecimal +) { +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/controller/CustomTypeTestController.java b/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/controller/CustomTypeTestController.java new file mode 100644 index 0000000..dd98121 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/controller/CustomTypeTestController.java @@ -0,0 +1,64 @@ +package it.aboutbits.springboot.toolbox.boot.mvc.controller; + +import it.aboutbits.springboot.toolbox.boot.mvc.body.BodyWithEmailAddress; +import it.aboutbits.springboot.toolbox.boot.mvc.body.BodyWithIban; +import it.aboutbits.springboot.toolbox.boot.mvc.body.BodyWithScaledBigDecimal; +import it.aboutbits.springboot.toolbox.type.EmailAddress; +import it.aboutbits.springboot.toolbox.type.Iban; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/test/type") +public class CustomTypeTestController { + @GetMapping("/EmailAddress/as-path-variable/{value}") + public EmailAddress emailAddressAsPathVariable(@PathVariable EmailAddress value) { + return value; + } + + @GetMapping("/EmailAddress/as-request-parameter") + public EmailAddress emailAddressAsRequestParameter(@RequestParam EmailAddress value) { + return value; + } + + @PostMapping("/EmailAddress/as-body") + public BodyWithEmailAddress emailAddressAsBody(@RequestBody BodyWithEmailAddress value) { + return value; + } + + @GetMapping("/Iban/as-path-variable/{value}") + public Iban ibanAsPathVariable(@PathVariable Iban value) { + return value; + } + + @GetMapping("/Iban/as-request-parameter") + public Iban ibanAsRequestParameter(@RequestParam Iban value) { + return value; + } + + @PostMapping("/Iban/as-body") + public BodyWithIban ibanAsBody(@RequestBody BodyWithIban value) { + return value; + } + + @GetMapping("/ScaledBigDecimal/as-path-variable/{value}") + public ScaledBigDecimal scaledBigDecimalAsPathVariable(@PathVariable ScaledBigDecimal value) { + return value; + } + + @GetMapping("/ScaledBigDecimal/as-request-parameter") + public ScaledBigDecimal scaledBigDecimalAsRequestParameter(@RequestParam ScaledBigDecimal value) { + return value; + } + + @PostMapping("/ScaledBigDecimal/as-body") + public BodyWithScaledBigDecimal scaledBigDecimalAsBody(@RequestBody BodyWithScaledBigDecimal value) { + return value; + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/controller/EntityIdTestController.java b/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/controller/EntityIdTestController.java new file mode 100644 index 0000000..6cf40bc --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/boot/mvc/controller/EntityIdTestController.java @@ -0,0 +1,30 @@ +package it.aboutbits.springboot.toolbox.boot.mvc.controller; + +import it.aboutbits.springboot.toolbox.boot.mvc.body.BodyWithEntityId; +import it.aboutbits.springboot.toolbox.boot.persistence.impl.jpa.CustomTypeTestModel; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/test/entity-id") +public class EntityIdTestController { + @GetMapping("/CustomTypeTestModel.ID/as-path-variable/{value}") + public CustomTypeTestModel.ID customTypeTestModelIdAsPathVariable(@PathVariable CustomTypeTestModel.ID value) { + return value; + } + + @GetMapping("/CustomTypeTestModel.ID/as-request-parameter") + public CustomTypeTestModel.ID customTypeTestModelIdAsRequestParameter(@RequestParam CustomTypeTestModel.ID value) { + return value; + } + + @PostMapping("/CustomTypeTestModel.ID/as-body") + public BodyWithEntityId customTypeTestModelIdAsBody(@RequestBody BodyWithEntityId value) { + return value; + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/boot/persistence/CustomTypeJpaTest.java b/src/test/java/it/aboutbits/springboot/toolbox/boot/persistence/CustomTypeJpaTest.java new file mode 100644 index 0000000..bd045bf --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/boot/persistence/CustomTypeJpaTest.java @@ -0,0 +1,76 @@ +package it.aboutbits.springboot.toolbox.boot.persistence; + +import it.aboutbits.springboot.toolbox.boot.persistence.impl.jpa.CustomTypeTestModel; +import it.aboutbits.springboot.toolbox.boot.persistence.impl.jpa.CustomTypeTestModelRepository; +import it.aboutbits.springboot.toolbox.support.ApplicationTest; +import it.aboutbits.springboot.toolbox.type.EmailAddress; +import it.aboutbits.springboot.toolbox.type.Iban; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.assertj.core.api.Assertions.assertThat; + +@ApplicationTest +public class CustomTypeJpaTest { + @Autowired + CustomTypeTestModelRepository repository; + + @Nested + class EmailAddressType { + @Test + void inAndOut_shouldSucceed() { + var item = new CustomTypeTestModel(); + item.setEmail(new EmailAddress("sepp@aboutbits.it")); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByEmail(savedItem.getEmail()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + } + + @Nested + class IbanType { + @Test + void inAndOut_shouldSucceed() { + var item = new CustomTypeTestModel(); + item.setIban(new Iban("NL63ABNA7864733042")); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByIban(savedItem.getIban()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + } + + @Nested + class ScaledBigDecimalType { + @ParameterizedTest + @ValueSource(doubles = {-1, 0, 1, -0.001, 0.001, -100_000_000, 100_000_000}) + void inAndOut_shouldSucceed(double doubleValue) { + var item = new CustomTypeTestModel(); + item.setAccountBalance(new ScaledBigDecimal(doubleValue)); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByAccountBalance(savedItem.getAccountBalance()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/boot/persistence/EntityIdJpaTest.java b/src/test/java/it/aboutbits/springboot/toolbox/boot/persistence/EntityIdJpaTest.java new file mode 100644 index 0000000..59110c1 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/boot/persistence/EntityIdJpaTest.java @@ -0,0 +1,54 @@ +package it.aboutbits.springboot.toolbox.boot.persistence; + +import it.aboutbits.springboot.toolbox.boot.persistence.impl.jpa.CustomTypeTestModel; +import it.aboutbits.springboot.toolbox.boot.persistence.impl.jpa.CustomTypeTestModelRepository; +import it.aboutbits.springboot.toolbox.boot.persistence.impl.jpa.ReferencedTestModel; +import it.aboutbits.springboot.toolbox.support.ApplicationTest; +import it.aboutbits.springboot.toolbox.support.WithPersistence; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.assertj.core.api.Assertions.assertThat; + +@ApplicationTest +@WithPersistence +public class EntityIdJpaTest { + @Autowired + CustomTypeTestModelRepository repository; + + @Nested + class OwnId { + @Test + void inAndOut_shouldSucceed() { + var item = new CustomTypeTestModel(); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findById(savedItem.getId()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + } + + @Nested + class ReferencedId { + @Test + void inAndOut_shouldSucceed() { + var item = new CustomTypeTestModel(); + item.setReferencedId(new ReferencedTestModel.ID(1234L)); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByReferencedId(savedItem.getReferencedId()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/boot/persistence/impl/jpa/CustomTypeTestModel.java b/src/test/java/it/aboutbits/springboot/toolbox/boot/persistence/impl/jpa/CustomTypeTestModel.java new file mode 100644 index 0000000..ed6075c --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/boot/persistence/impl/jpa/CustomTypeTestModel.java @@ -0,0 +1,62 @@ +package it.aboutbits.springboot.toolbox.boot.persistence.impl.jpa; + +import it.aboutbits.springboot.toolbox.boot.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.identity.EntityId; +import it.aboutbits.springboot.toolbox.persistence.identity.Identified; +import it.aboutbits.springboot.toolbox.persistence.javatype.EmailAddressJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.IbanJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.ScaledBigDecimalJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.base.WrappedLongJavaType; +import it.aboutbits.springboot.toolbox.type.EmailAddress; +import it.aboutbits.springboot.toolbox.type.Iban; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.JavaType; + +@Entity +@Getter +@Setter +@Table(name = "custom_type_test_model") +public class CustomTypeTestModel implements Identified { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @JavaType(CustomTypeTestModel.ID.JavaType.class) + private ID id; + + @SuppressWarnings("JpaAttributeTypeInspection") + @JavaType(EmailAddressJavaType.class) + private EmailAddress email; + + @SuppressWarnings("JpaAttributeTypeInspection") + @JavaType(IbanJavaType.class) + private Iban iban; + + @SuppressWarnings("JpaAttributeTypeInspection") + @JavaType(ScaledBigDecimalJavaType.class) + private ScaledBigDecimal accountBalance; + + @JavaType(ReferencedTestModel.ID.JavaType.class) + private ReferencedTestModel.ID referencedId; + + public record ID( + Long value + ) implements EntityId { + + @Override + public String toString() { + return String.valueOf(value()); + } + + public static class JavaType extends WrappedLongJavaType implements AutoRegisteredJavaType { + public JavaType() { + super(ID.class); + } + } + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/boot/persistence/impl/jpa/CustomTypeTestModelRepository.java b/src/test/java/it/aboutbits/springboot/toolbox/boot/persistence/impl/jpa/CustomTypeTestModelRepository.java new file mode 100644 index 0000000..647ea69 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/boot/persistence/impl/jpa/CustomTypeTestModelRepository.java @@ -0,0 +1,18 @@ +package it.aboutbits.springboot.toolbox.boot.persistence.impl.jpa; + +import it.aboutbits.springboot.toolbox.type.EmailAddress; +import it.aboutbits.springboot.toolbox.type.Iban; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface CustomTypeTestModelRepository extends JpaRepository { + Optional findByEmail(EmailAddress emailAddress); + + Optional findByIban(Iban iban); + + Optional findByAccountBalance(ScaledBigDecimal accountBalance); + + Optional findByReferencedId(ReferencedTestModel.ID otherId); +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/boot/persistence/impl/jpa/ReferencedTestModel.java b/src/test/java/it/aboutbits/springboot/toolbox/boot/persistence/impl/jpa/ReferencedTestModel.java new file mode 100644 index 0000000..349bac8 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/boot/persistence/impl/jpa/ReferencedTestModel.java @@ -0,0 +1,25 @@ +package it.aboutbits.springboot.toolbox.boot.persistence.impl.jpa; + +import it.aboutbits.springboot.toolbox.boot.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.identity.EntityId; +import it.aboutbits.springboot.toolbox.persistence.javatype.base.WrappedLongJavaType; + +public class ReferencedTestModel { + // we just use this to have and ID we can actually reference + + public record ID( + Long value + ) implements EntityId { + + @Override + public String toString() { + return String.valueOf(value()); + } + + public static class JavaType extends WrappedLongJavaType implements AutoRegisteredJavaType { + public JavaType() { + super(ID.class); + } + } + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/boot/swagger/SwaggerTest.java b/src/test/java/it/aboutbits/springboot/toolbox/boot/swagger/SwaggerTest.java new file mode 100644 index 0000000..7e1e12f --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/boot/swagger/SwaggerTest.java @@ -0,0 +1,36 @@ +package it.aboutbits.springboot.toolbox.boot.swagger; + +import it.aboutbits.springboot.toolbox.support.HttpTest; +import lombok.NonNull; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.assertj.core.api.Assertions.assertThat; + +@HttpTest +public class SwaggerTest { + @Autowired + MockMvc mockMvc; + + @Test + void shouldBeAccessible() throws Exception { + var swaggerUiString = performGetAndReturnResult("/docs/swagger-ui/index.html"); + var swaggerSchemaString = performGetAndReturnResult("/docs/api"); + + assertThat(swaggerUiString).isNotBlank(); + assertThat(swaggerSchemaString).isNotBlank(); + } + + private @NonNull String performGetAndReturnResult(@NonNull String url) throws Exception { + var requestBuilder = MockMvcRequestBuilders.get(url) + .contentType(MediaType.APPLICATION_JSON); + + return mockMvc.perform(requestBuilder) + .andReturn() + .getResponse() + .getContentAsString(); + } +} 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 new file mode 100644 index 0000000..c54b8b3 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/WrapperTypesJpaTest.java @@ -0,0 +1,337 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype; + +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.jpa.WrapperTypesModel; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.jpa.WrapperTypesModelRepository; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapBigDecimal; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapBigInteger; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapDouble; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapFloat; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapInteger; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapLong; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapScaledBigDecimal; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapShort; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapString; +import it.aboutbits.springboot.toolbox.support.ApplicationTest; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +@ApplicationTest +public class WrapperTypesJpaTest { + @Autowired + WrapperTypesModelRepository repository; + + @Nested + class WrapBigDecimalType { + @Test + void givenNull_inAndOut_shouldSucceed() { + var item = new WrapperTypesModel(); + item.setBigDecimalValue(null); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByBigDecimalValue(null); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + + @ParameterizedTest + @ValueSource(doubles = {-1, 0, 1, -0.001, 0.001, -100_000_000, 100_000_000}) + void givenValues_inAndOut_shouldSucceed(double doubleValue) { + var item = new WrapperTypesModel(); + item.setBigDecimalValue(new WrapBigDecimal(BigDecimal.valueOf(doubleValue))); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByBigDecimalValue(savedItem.getBigDecimalValue()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + } + + @Nested + class WrapBigIntegerType { + @Test + void givenNull_inAndOut_shouldSucceed() { + var item = new WrapperTypesModel(); + item.setBigIntegerValue(null); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByBigIntegerValue(null); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + + @ParameterizedTest + @ValueSource(ints = {-1, 0, 1, -100_000_000, 100_000_000}) + void givenValues_inAndOut_shouldSucceed(int intValue) { + var item = new WrapperTypesModel(); + item.setBigIntegerValue(new WrapBigInteger(BigInteger.valueOf(intValue))); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByBigIntegerValue(savedItem.getBigIntegerValue()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + } + + @Nested + class WrapDoubleType { + @Test + void givenNull_inAndOut_shouldSucceed() { + var item = new WrapperTypesModel(); + item.setDoubleValue(null); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByDoubleValue(null); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + + @ParameterizedTest + @ValueSource(doubles = {-1, 0, 1, -0.001, 0.001, -100_000_000, 100_000_000}) + void givenValues_inAndOut_shouldSucceed(double doubleValue) { + var item = new WrapperTypesModel(); + item.setDoubleValue(new WrapDouble(doubleValue)); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByDoubleValue(savedItem.getDoubleValue()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + } + + @Nested + class WrapFloatType { + @Test + void givenNull_inAndOut_shouldSucceed() { + var item = new WrapperTypesModel(); + item.setFloatValue(null); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByFloatValue(null); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + + @ParameterizedTest + @ValueSource(floats = {-1, 0, 1, -0.001f, 0.001f, -100_000_000, 100_000_000}) + void givenValues_inAndOut_shouldSucceed(float floatValue) { + var item = new WrapperTypesModel(); + item.setFloatValue(new WrapFloat(floatValue)); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByFloatValue(savedItem.getFloatValue()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + } + + @Nested + class WrapIntegerType { + @Test + void givenNull_inAndOut_shouldSucceed() { + var item = new WrapperTypesModel(); + item.setIntegerValue(null); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByIntegerValue(null); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + + @ParameterizedTest + @ValueSource(ints = {-1, 0, 1, -100_000_000, 100_000_000}) + void givenValues_inAndOut_shouldSucceed(int intValue) { + var item = new WrapperTypesModel(); + item.setIntegerValue(new WrapInteger(intValue)); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByIntegerValue(savedItem.getIntegerValue()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + } + + @Nested + class WrapLongType { + @Test + void givenNull_inAndOut_shouldSucceed() { + var item = new WrapperTypesModel(); + item.setLongValue(null); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByLongValue(null); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + + @ParameterizedTest + @ValueSource(longs = {-1, 0, 1, -100_000_000, 100_000_000}) + void givenValues_inAndOut_shouldSucceed(long longValue) { + var item = new WrapperTypesModel(); + item.setLongValue(new WrapLong(longValue)); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByLongValue(savedItem.getLongValue()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + } + + @Nested + class WrapScaledBigDecimalType { + @Test + void givenNull_inAndOut_shouldSucceed() { + var item = new WrapperTypesModel(); + item.setScaledBigDecimalValue(null); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByScaledBigDecimalValue(null); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + + @ParameterizedTest + @ValueSource(doubles = {-1, 0, 1, -0.001, 0.001, -100_000_000, 100_000_000}) + void givenValues_inAndOut_shouldSucceed(double doubleValue) { + var item = new WrapperTypesModel(); + item.setScaledBigDecimalValue(new WrapScaledBigDecimal(new ScaledBigDecimal(doubleValue))); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByScaledBigDecimalValue(savedItem.getScaledBigDecimalValue()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + } + + @Nested + class WrapShortType { + @Test + void givenNull_inAndOut_shouldSucceed() { + var item = new WrapperTypesModel(); + item.setShortValue(null); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByShortValue(null); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + + @ParameterizedTest + @ValueSource(shorts = {-1, 0, 1, -10_000, 10_000}) + void givenValues_inAndOut_shouldSucceed(short shortValue) { + var item = new WrapperTypesModel(); + item.setShortValue(new WrapShort(shortValue)); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByShortValue(savedItem.getShortValue()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + } + + @Nested + class WrapStringType { + @Test + void givenNull_inAndOut_shouldSucceed() { + var item = new WrapperTypesModel(); + item.setStringValue(null); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByStringValue(null); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", "test", "some longer test", "\r", "\n"}) + void givenValues_inAndOut_shouldSucceed(String stringValue) { + var item = new WrapperTypesModel(); + item.setStringValue(new WrapString(stringValue)); + + var savedItem = repository.save(item); + + var retrievedItem = repository.findByStringValue(savedItem.getStringValue()); + + assertThat(retrievedItem).isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(savedItem); + } + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapBigDecimalJavaType.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapBigDecimalJavaType.java new file mode 100644 index 0000000..5374d1a --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapBigDecimalJavaType.java @@ -0,0 +1,11 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype; + +import it.aboutbits.springboot.toolbox.boot.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.base.WrappedBigDecimalJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapBigDecimal; + +public final class WrapBigDecimalJavaType extends WrappedBigDecimalJavaType implements AutoRegisteredJavaType { + public WrapBigDecimalJavaType() { + super(WrapBigDecimal.class); + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapBigIntegerJavaType.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapBigIntegerJavaType.java new file mode 100644 index 0000000..99b796c --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapBigIntegerJavaType.java @@ -0,0 +1,11 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype; + +import it.aboutbits.springboot.toolbox.boot.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.base.WrappedBigIntegerJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapBigInteger; + +public final class WrapBigIntegerJavaType extends WrappedBigIntegerJavaType implements AutoRegisteredJavaType { + public WrapBigIntegerJavaType() { + super(WrapBigInteger.class); + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapDoubleJavaType.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapDoubleJavaType.java new file mode 100644 index 0000000..932d68d --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapDoubleJavaType.java @@ -0,0 +1,11 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype; + +import it.aboutbits.springboot.toolbox.boot.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.base.WrappedDoubleJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapDouble; + +public final class WrapDoubleJavaType extends WrappedDoubleJavaType implements AutoRegisteredJavaType { + public WrapDoubleJavaType() { + super(WrapDouble.class); + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapFloatJavaType.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapFloatJavaType.java new file mode 100644 index 0000000..908aab2 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapFloatJavaType.java @@ -0,0 +1,11 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype; + +import it.aboutbits.springboot.toolbox.boot.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.base.WrappedFloatJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapFloat; + +public final class WrapFloatJavaType extends WrappedFloatJavaType implements AutoRegisteredJavaType { + public WrapFloatJavaType() { + super(WrapFloat.class); + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapIntegerJavaType.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapIntegerJavaType.java new file mode 100644 index 0000000..6d5e350 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapIntegerJavaType.java @@ -0,0 +1,11 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype; + +import it.aboutbits.springboot.toolbox.boot.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.base.WrappedIntegerJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapInteger; + +public final class WrapIntegerJavaType extends WrappedIntegerJavaType implements AutoRegisteredJavaType { + public WrapIntegerJavaType() { + super(WrapInteger.class); + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapLongJavaType.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapLongJavaType.java new file mode 100644 index 0000000..c014d7e --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapLongJavaType.java @@ -0,0 +1,11 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype; + +import it.aboutbits.springboot.toolbox.boot.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.base.WrappedLongJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapLong; + +public final class WrapLongJavaType extends WrappedLongJavaType implements AutoRegisteredJavaType { + public WrapLongJavaType() { + super(WrapLong.class); + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapScaledBigDecimalJavaType.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapScaledBigDecimalJavaType.java new file mode 100644 index 0000000..f508865 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapScaledBigDecimalJavaType.java @@ -0,0 +1,11 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype; + +import it.aboutbits.springboot.toolbox.boot.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.base.WrappedScaledBigDecimalJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapScaledBigDecimal; + +public final class WrapScaledBigDecimalJavaType extends WrappedScaledBigDecimalJavaType implements AutoRegisteredJavaType { + public WrapScaledBigDecimalJavaType() { + super(WrapScaledBigDecimal.class); + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapShortJavaType.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapShortJavaType.java new file mode 100644 index 0000000..8a3440e --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapShortJavaType.java @@ -0,0 +1,11 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype; + +import it.aboutbits.springboot.toolbox.boot.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.base.WrappedShortJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapShort; + +public final class WrapShortJavaType extends WrappedShortJavaType implements AutoRegisteredJavaType { + public WrapShortJavaType() { + super(WrapShort.class); + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapStringJavaType.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapStringJavaType.java new file mode 100644 index 0000000..fb9e910 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/javatype/WrapStringJavaType.java @@ -0,0 +1,11 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype; + +import it.aboutbits.springboot.toolbox.boot.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.base.WrappedStringJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapString; + +public final class WrapStringJavaType extends WrappedStringJavaType implements AutoRegisteredJavaType { + public WrapStringJavaType() { + super(WrapString.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 new file mode 100644 index 0000000..b778cf9 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/jpa/WrapperTypesModel.java @@ -0,0 +1,95 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.jpa; + +import it.aboutbits.springboot.toolbox.boot.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.identity.EntityId; +import it.aboutbits.springboot.toolbox.persistence.identity.Identified; +import it.aboutbits.springboot.toolbox.persistence.javatype.base.WrappedLongJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapBigDecimalJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapBigIntegerJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapDoubleJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapFloatJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapIntegerJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapLongJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapScaledBigDecimalJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapShortJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapStringJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapBigDecimal; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapBigInteger; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapDouble; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapFloat; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapInteger; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapLong; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapScaledBigDecimal; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapShort; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapString; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.JavaType; + +@Entity +@Getter +@Setter +@Table(name = "wrapper_type_test_model") +public class WrapperTypesModel implements Identified { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @JavaType(ID.JavaType.class) + private ID id; + + @SuppressWarnings("JpaAttributeTypeInspection") + @JavaType(WrapBigDecimalJavaType.class) + private WrapBigDecimal bigDecimalValue; + + @SuppressWarnings("JpaAttributeTypeInspection") + @JavaType(WrapBigIntegerJavaType.class) + private WrapBigInteger bigIntegerValue; + + @SuppressWarnings("JpaAttributeTypeInspection") + @JavaType(WrapDoubleJavaType.class) + private WrapDouble doubleValue; + + @SuppressWarnings("JpaAttributeTypeInspection") + @JavaType(WrapFloatJavaType.class) + private WrapFloat floatValue; + + @SuppressWarnings("JpaAttributeTypeInspection") + @JavaType(WrapIntegerJavaType.class) + private WrapInteger integerValue; + + @SuppressWarnings("JpaAttributeTypeInspection") + @JavaType(WrapLongJavaType.class) + private WrapLong longValue; + + @SuppressWarnings("JpaAttributeTypeInspection") + @JavaType(WrapScaledBigDecimalJavaType.class) + private WrapScaledBigDecimal scaledBigDecimalValue; + + @SuppressWarnings("JpaAttributeTypeInspection") + @JavaType(WrapShortJavaType.class) + private WrapShort shortValue; + + @SuppressWarnings("JpaAttributeTypeInspection") + @JavaType(WrapStringJavaType.class) + private WrapString stringValue; + + public record ID( + Long value + ) implements EntityId { + + @Override + public String toString() { + return String.valueOf(value()); + } + + public static class JavaType extends WrappedLongJavaType implements AutoRegisteredJavaType { + public JavaType() { + super(ID.class); + } + } + } +} 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 new file mode 100644 index 0000000..e7c2459 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/jpa/WrapperTypesModelRepository.java @@ -0,0 +1,34 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.jpa; + +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapBigDecimal; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapBigInteger; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapDouble; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapFloat; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapInteger; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapLong; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapScaledBigDecimal; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapShort; +import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapString; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface WrapperTypesModelRepository extends JpaRepository { + Optional findByBigDecimalValue(WrapBigDecimal value); + + Optional findByBigIntegerValue(WrapBigInteger value); + + Optional findByDoubleValue(WrapDouble value); + + Optional findByFloatValue(WrapFloat value); + + Optional findByIntegerValue(WrapInteger value); + + Optional findByLongValue(WrapLong value); + + Optional findByScaledBigDecimalValue(WrapScaledBigDecimal value); + + Optional findByShortValue(WrapShort value); + + Optional findByStringValue(WrapString value); +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapBigDecimal.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapBigDecimal.java new file mode 100644 index 0000000..5b3b2b8 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapBigDecimal.java @@ -0,0 +1,9 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.type; + +import it.aboutbits.springboot.toolbox.type.CustomType; + +import java.math.BigDecimal; + +public record WrapBigDecimal(BigDecimal value) implements CustomType { + +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapBigInteger.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapBigInteger.java new file mode 100644 index 0000000..f97a743 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapBigInteger.java @@ -0,0 +1,9 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.type; + +import it.aboutbits.springboot.toolbox.type.CustomType; + +import java.math.BigInteger; + +public record WrapBigInteger(BigInteger value) implements CustomType { + +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapDouble.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapDouble.java new file mode 100644 index 0000000..467527e --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapDouble.java @@ -0,0 +1,7 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.type; + +import it.aboutbits.springboot.toolbox.type.CustomType; + +public record WrapDouble(Double value) implements CustomType { + +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapFloat.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapFloat.java new file mode 100644 index 0000000..357b8aa --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapFloat.java @@ -0,0 +1,7 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.type; + +import it.aboutbits.springboot.toolbox.type.CustomType; + +public record WrapFloat(Float value) implements CustomType { + +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapInteger.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapInteger.java new file mode 100644 index 0000000..73a08af --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapInteger.java @@ -0,0 +1,7 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.type; + +import it.aboutbits.springboot.toolbox.type.CustomType; + +public record WrapInteger(Integer value) implements CustomType { + +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapLong.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapLong.java new file mode 100644 index 0000000..fc54ee0 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapLong.java @@ -0,0 +1,7 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.type; + +import it.aboutbits.springboot.toolbox.type.CustomType; + +public record WrapLong(Long value) implements CustomType { + +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapScaledBigDecimal.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapScaledBigDecimal.java new file mode 100644 index 0000000..87746eb --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapScaledBigDecimal.java @@ -0,0 +1,8 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.type; + +import it.aboutbits.springboot.toolbox.type.CustomType; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; + +public record WrapScaledBigDecimal(ScaledBigDecimal value) implements CustomType { + +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapShort.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapShort.java new file mode 100644 index 0000000..5933d1d --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapShort.java @@ -0,0 +1,7 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.type; + +import it.aboutbits.springboot.toolbox.type.CustomType; + +public record WrapShort(Short value) implements CustomType { + +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapString.java b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapString.java new file mode 100644 index 0000000..af28f4d --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/persistence/javatype/impl/type/WrapString.java @@ -0,0 +1,7 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype.impl.type; + +import it.aboutbits.springboot.toolbox.type.CustomType; + +public record WrapString(String value) implements CustomType { + +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/support/ApplicationTest.java b/src/test/java/it/aboutbits/springboot/toolbox/support/ApplicationTest.java new file mode 100644 index 0000000..6763f38 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/support/ApplicationTest.java @@ -0,0 +1,17 @@ +package it.aboutbits.springboot.toolbox.support; + +import it.aboutbits.springboot.toolbox.support.persistence.WithPostgres; +import org.springframework.boot.test.context.SpringBootTest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@SpringBootTest +@WithPostgres +public @interface ApplicationTest { + +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/support/HttpTest.java b/src/test/java/it/aboutbits/springboot/toolbox/support/HttpTest.java new file mode 100644 index 0000000..723081e --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/support/HttpTest.java @@ -0,0 +1,13 @@ +package it.aboutbits.springboot.toolbox.support; + +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ApplicationTest +@AutoConfigureMockMvc +public @interface HttpTest { + +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/support/WithPersistence.java b/src/test/java/it/aboutbits/springboot/toolbox/support/WithPersistence.java new file mode 100644 index 0000000..e42ca53 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/support/WithPersistence.java @@ -0,0 +1,12 @@ +package it.aboutbits.springboot.toolbox.support; + +import it.aboutbits.springboot.toolbox.support.persistence.WithPostgres; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@WithPostgres +public @interface WithPersistence { + +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/support/persistence/PostgresTestcontainer.java b/src/test/java/it/aboutbits/springboot/toolbox/support/persistence/PostgresTestcontainer.java new file mode 100644 index 0000000..8a21f17 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/support/persistence/PostgresTestcontainer.java @@ -0,0 +1,110 @@ +package it.aboutbits.springboot.toolbox.support.persistence; + +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.utility.DockerImageName; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; + +@Log4j2 +public class PostgresTestcontainer implements BeforeAllCallback, AfterEachCallback { + public static final PostgreSQLContainer POSTGRES_CONTAINER; + private static final Set TABLES_TO_IGNORE = Set.of( + "databasechangelog", + "databasechangeloglock" + ); + + // See https://www.postgresql.org/docs/current/non-durability.html for details about PostgreSQL CLI parameters + static { + POSTGRES_CONTAINER = new PostgreSQLContainer<>( + DockerImageName.parse("postgres:16").asCompatibleSubstituteFor("postgres")) + .withDatabaseName("app") + .withCommand( + "postgres -c max_connections=500 -c fsync=off -c synchronous_commit=off -c full_page_writes=off -c max_wal_size=2GB -c checkpoint_timeout=20min" + ) + .withUsername("admin") + .withPassword("password") + .withReuse(true); + + POSTGRES_CONTAINER.start(); + } + + @Override + public void beforeAll(ExtensionContext extensionContext) { + System.setProperty("spring.datasource.url", POSTGRES_CONTAINER.getJdbcUrl()); + System.setProperty("spring.datasource.username", POSTGRES_CONTAINER.getUsername()); + System.setProperty("spring.datasource.password", POSTGRES_CONTAINER.getPassword()); + System.setProperty("spring.liquibase.url", POSTGRES_CONTAINER.getJdbcUrl()); + System.setProperty("spring.liquibase.user", POSTGRES_CONTAINER.getUsername()); + System.setProperty("spring.liquibase.password", POSTGRES_CONTAINER.getPassword()); + } + + @Override + public void afterEach(ExtensionContext extensionContext) throws Exception { + var connection = DriverManager.getConnection( + POSTGRES_CONTAINER.getJdbcUrl(), + POSTGRES_CONTAINER.getUsername(), + POSTGRES_CONTAINER.getPassword() + ); + + try { + connection.setAutoCommit(false); + var tablesToClean = loadTablesToClean(connection); + cleanTablesData(tablesToClean, connection); + connection.commit(); + } catch (SQLException exception) { + exception.printStackTrace(); + } + } + + private List loadTablesToClean(Connection connection) throws SQLException { + var databaseMetaData = connection.getMetaData(); + var resultSet = databaseMetaData.getTables( + connection.getCatalog(), null, null, new String[]{"TABLE"}); + + var tablesToClean = new ArrayList(); + while (resultSet.next()) { + var table = new TableData( + resultSet.getString("TABLE_SCHEM"), + resultSet.getString("TABLE_NAME") + ); + + if (!TABLES_TO_IGNORE.contains(table.name())) { + tablesToClean.add(table); + } + } + + return tablesToClean; + } + + private void cleanTablesData(List tablesToClean, Connection connection) throws SQLException { + if (tablesToClean.isEmpty()) { + return; + } + + log.debug("Cleaning Database tables {}", tablesToClean); + + var allTables = new StringJoiner(", "); + for (var table : tablesToClean) { + allTables.add(table.getFullyQualifiedName()); + } + + var statement = String.format("TRUNCATE %s RESTART IDENTITY CASCADE", allTables); + connection.prepareStatement(statement).execute(); + } + + private record TableData(String schema, String name) { + public String getFullyQualifiedName() { + return schema + "." + name; + } + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/support/persistence/WithPostgres.java b/src/test/java/it/aboutbits/springboot/toolbox/support/persistence/WithPostgres.java new file mode 100644 index 0000000..9f51713 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/support/persistence/WithPostgres.java @@ -0,0 +1,14 @@ +package it.aboutbits.springboot.toolbox.support.persistence; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith({PostgresTestcontainer.class}) +@ResourceLock(value = "Database", mode = ResourceAccessMode.READ_WRITE) +public @interface WithPostgres { +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 0000000..6a029c0 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,62 @@ +server: + shutdown: graceful + +spring: + lifecycle: + timeout-per-shutdown-phase: 30s + threads: + virtual: + enabled: true + mvc: + format: + date: yyyy-MM-dd + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://localhost:5432/app?applicationName=ToolboxTestApp + username: root + password: password + hikari: + maximum-pool-size: 10 + schema: public + jpa: + open-in-view: false + properties: + hibernate: + jdbc: + lob: + non-contextual-creation: true + default-schema: ${spring.datasource.hikari.schema} + liquibase: + enabled: true + change-log: "classpath:db/changelog/master.yml" + user: ${spring.datasource.username} + password: ${spring.datasource.password} + default-schema: ${spring.datasource.hikari.schema} + test: + database: + replace: none + sql: + init: + continue-on-error: true + +springdoc: + api-docs: + path: /docs/api + groups: + enabled: true + enabled: true + swagger-ui: + path: /docs/swagger + csrf: + enabled: true + enabled: ${springdoc.api-docs.enabled} + tags-sorter: alpha + operations-sorter: alpha + doc-expansion: none + default-model-expand-depth: 99 + default-models-expand-depth: 99 + +logging: + level: + it.aboutbits: ${LOG_LEVEL:INFO} + root: ${LOG_LEVEL:WARN} 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 new file mode 100644 index 0000000..9ee001a --- /dev/null +++ b/src/test/resources/db/changelog/2024-09-06-create-custom-type-testing-table.yml @@ -0,0 +1,27 @@ +databaseChangeLog: + - changeSet: + author: Andreas Hufler + id: 2024-09-06-create-custom-type-testing-table + changes: + - createTable: + tableName: custom_type_test_model + columns: + - column: + name: id + autoIncrement: true + type: bigserial + constraints: + nullable: false + primaryKey: true + - column: + name: email + type: text + - column: + name: iban + type: text + - column: + name: account_balance + type: double + - column: + name: referenced_id + type: bigint 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 new file mode 100644 index 0000000..1ae8cb5 --- /dev/null +++ b/src/test/resources/db/changelog/2024-09-06-create-wrapper-type-testing-table.yml @@ -0,0 +1,42 @@ +databaseChangeLog: + - changeSet: + author: Andreas Hufler + id: 2024-09-06-create-wrapper-type-testing-table + changes: + - createTable: + tableName: wrapper_type_test_model + columns: + - column: + name: id + autoIncrement: true + type: bigserial + constraints: + nullable: false + primaryKey: true + - column: + name: big_decimal_value + type: double + - column: + name: big_integer_value + type: bigint + - column: + name: double_value + type: double + - column: + name: float_value + type: double + - column: + name: integer_value + type: bigint + - column: + name: long_value + type: bigint + - column: + name: scaled_big_decimal_value + type: double + - column: + name: short_value + type: bigint + - column: + name: string_value + type: text diff --git a/src/test/resources/db/changelog/master.yml b/src/test/resources/db/changelog/master.yml new file mode 100644 index 0000000..ae4ea78 --- /dev/null +++ b/src/test/resources/db/changelog/master.yml @@ -0,0 +1,7 @@ +databaseChangeLog: + - include: + file: 2024-09-06-create-custom-type-testing-table.yml + relativeToChangelogFile: true + - include: + file: 2024-09-06-create-wrapper-type-testing-table.yml + relativeToChangelogFile: true