diff --git a/pom.xml b/pom.xml index 01001da..4d78bfe 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.3.2 + 3.3.3 @@ -21,11 +21,21 @@ + + org.springframework.boot + spring-boot-starter-data-jpa + + org.springframework.boot spring-boot-starter-validation + + org.springframework.boot + spring-boot-starter-json + + org.projectlombok @@ -33,18 +43,99 @@ true + + + + io.github.classgraph + classgraph + 4.8.175 + + + commons-validator commons-validator 1.9.0 + + + commons-collections + commons-collections + + + commons-logging + commons-logging + + + + + + + org.apache.commons + commons-collections4 + 4.4 + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.6.0 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 @@ -53,7 +144,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.13.0 ${java.version} ${java.version} @@ -62,7 +153,7 @@ org.apache.maven.plugins maven-checkstyle-plugin - 3.2.1 + 3.5.0 checkstyle.xml @@ -86,7 +177,7 @@ com.puppycrawl.tools checkstyle - 10.3.4 + 10.18.1 diff --git a/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/AbstractCustomTypeContributor.java b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/AbstractCustomTypeContributor.java new file mode 100644 index 0000000..3f0f02e --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/AbstractCustomTypeContributor.java @@ -0,0 +1,37 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.persistence; + +import it.aboutbits.springboot.toolbox.reflection.util.ClassScannerUtil; +import lombok.SneakyThrows; +import org.hibernate.boot.model.TypeContributions; +import org.hibernate.boot.model.TypeContributor; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.type.descriptor.java.JavaType; + +import java.lang.reflect.InvocationTargetException; +import java.util.Set; + +public abstract class AbstractCustomTypeContributor implements TypeContributor { + @SuppressWarnings("rawtypes") + private final Set> relevantTypes; + + protected AbstractCustomTypeContributor(String... packageNames) { + var classScanner = ClassScannerUtil.getScannerForPackages(packageNames); + + this.relevantTypes = findAllRelevantTypes(classScanner); + } + + @SneakyThrows({InstantiationException.class, IllegalAccessException.class, InvocationTargetException.class}) + @Override + public void contribute(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + for (var type : relevantTypes) { + typeContributions.contributeJavaType( + (JavaType) type.getConstructors()[0].newInstance() + ); + } + } + + @SuppressWarnings("rawtypes") + private static Set> findAllRelevantTypes(ClassScannerUtil.ClassScanner classScanner) { + return classScanner.getSubTypesOf(AutoRegisteredJavaType.class); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/AutoRegisteredJavaType.java b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/AutoRegisteredJavaType.java new file mode 100644 index 0000000..881a05b --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/AutoRegisteredJavaType.java @@ -0,0 +1,6 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.persistence; + +import org.hibernate.type.descriptor.java.JavaType; + +public interface AutoRegisteredJavaType extends JavaType { +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/CustomTypeContributor.java b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/CustomTypeContributor.java new file mode 100644 index 0000000..8bd8407 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/CustomTypeContributor.java @@ -0,0 +1,7 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.persistence; + +public final class CustomTypeContributor extends AbstractCustomTypeContributor { + public CustomTypeContributor() { + super("it.aboutbits.springboot.toolbox"); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/swagger/RegisterCustomTypesWithSwagger.java b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/swagger/RegisterCustomTypesWithSwagger.java new file mode 100644 index 0000000..ed99c70 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/swagger/RegisterCustomTypesWithSwagger.java @@ -0,0 +1,19 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.swagger; + +import it.aboutbits.springboot.toolbox.swagger.CustomTypeModelConverter; +import it.aboutbits.springboot.toolbox.swagger.CustomTypePropertyCustomizer; +import org.springframework.context.annotation.Import; + +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) +@Import({ + CustomTypeModelConverter.class, + CustomTypePropertyCustomizer.class +}) +public @interface RegisterCustomTypesWithSwagger { +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/web/CustomTypeConfiguration.java b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/web/CustomTypeConfiguration.java new file mode 100644 index 0000000..231739a --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/web/CustomTypeConfiguration.java @@ -0,0 +1,50 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.web; + +import it.aboutbits.springboot.toolbox.jackson.CustomTypeDeserializer; +import it.aboutbits.springboot.toolbox.jackson.CustomTypeSerializer; +import it.aboutbits.springboot.toolbox.mvc.CustomTypePropertyEditor; +import it.aboutbits.springboot.toolbox.type.CustomType; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.InitBinder; + +import java.util.Set; + +@Slf4j +@Configuration +public class CustomTypeConfiguration { + @ControllerAdvice + public static class CustomTypePropertyBinder { + @SuppressWarnings("rawtypes") + private final Set> types; + + public CustomTypePropertyBinder(CustomTypeScanner configuration) { + this.types = configuration.getRelevantTypes(); + } + + @InitBinder + public void initBinder(WebDataBinder binder) { + for (var clazz : types) { + binder.registerCustomEditor(clazz, new CustomTypePropertyEditor<>(clazz)); + } + } + } + + @Bean + public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer(CustomTypeScanner configuration) { + var types = configuration.getRelevantTypes(); + + var deserializers = types.stream() + .map(CustomTypeDeserializer::new) + .toList() + .toArray(new CustomTypeDeserializer[types.size()]); + + return builder -> builder + .serializers(new CustomTypeSerializer()) + .deserializers(deserializers); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/web/CustomTypeScanner.java b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/web/CustomTypeScanner.java new file mode 100644 index 0000000..d677d4b --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/web/CustomTypeScanner.java @@ -0,0 +1,43 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.web; + +import it.aboutbits.springboot.toolbox.reflection.util.ClassScannerUtil; +import it.aboutbits.springboot.toolbox.type.CustomType; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +@Getter +public class CustomTypeScanner { + private static final String LIBRARY_BASE_PACKAGE_NAME = "it.aboutbits.springboot.toolbox"; + + @SuppressWarnings("rawtypes") + private Set> relevantTypes = new HashSet<>(); + + 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())); + + var packageNamesToScan = tmp.toArray(new String[0]); + + log.info("CustomTypeConfiguration enabled. Scanning: {}", Arrays.toString(packageNamesToScan)); + var classScanner = ClassScannerUtil.getScannerForPackages(packageNamesToScan); + + this.relevantTypes = findAllCustomTypeRecords(classScanner); + } + + @SuppressWarnings("rawtypes") + public static Set> findAllCustomTypeRecords(ClassScannerUtil.ClassScanner classScanner) { + return classScanner.getSubTypesOf(CustomType.class).stream() + .filter(Record.class::isAssignableFrom) + .collect(Collectors.toSet()); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/web/CustomTypeScannerRegistrar.java b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/web/CustomTypeScannerRegistrar.java new file mode 100644 index 0000000..a77f0b7 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/web/CustomTypeScannerRegistrar.java @@ -0,0 +1,33 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.web; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; + +import java.util.Objects; + +public class CustomTypeScannerRegistrar implements ImportBeanDefinitionRegistrar { + + /** + * We use this to register a new bean with actual configuration parameters coming from an annotation. + * That way we can use @RegisterCustomTypesWithJacksonAndMvc and take the parameter "additionalTypePackages" to + * scan packages. + */ + @Override + public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { + var attributes = new AnnotationAttributes( + Objects.requireNonNull( + metadata.getAnnotationAttributes( + RegisterCustomTypesWithJacksonAndMvc.class.getName() + ) + ) + ); + var value = attributes.getStringArray("additionalTypePackages"); + + var builder = BeanDefinitionBuilder.genericBeanDefinition(CustomTypeScanner.class); + builder.addPropertyValue("additionalTypePackages", value); + registry.registerBeanDefinition("CustomTypeScanner", builder.getBeanDefinition()); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/web/RegisterCustomTypesWithJacksonAndMvc.java b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/web/RegisterCustomTypesWithJacksonAndMvc.java new file mode 100644 index 0000000..c4d6ffe --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/web/RegisterCustomTypesWithJacksonAndMvc.java @@ -0,0 +1,15 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.web; + +import org.springframework.context.annotation.Import; + +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) +@Import({CustomTypeScannerRegistrar.class, CustomTypeConfiguration.class}) +public @interface RegisterCustomTypesWithJacksonAndMvc { + String[] additionalTypePackages() default ""; +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/jackson/CustomTypeDeserializer.java b/src/main/java/it/aboutbits/springboot/toolbox/jackson/CustomTypeDeserializer.java new file mode 100644 index 0000000..7d39bc0 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/jackson/CustomTypeDeserializer.java @@ -0,0 +1,181 @@ +package it.aboutbits.springboot.toolbox.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; +import it.aboutbits.springboot.toolbox.type.CustomType; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; + +import java.io.IOException; +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 { + private final Class customType; + private final Constructor constructor; + private final Function typeConverter; + + public CustomTypeDeserializer(Class customType) { + this.customType = customType; + + this.constructor = RecordReflectionUtil.getCanonicalConstructor(customType); + + this.typeConverter = getTypeConverter( + constructor.getParameterTypes()[0] + ); + } + + @Override + public Class handledType() { + return customType; + } + + @Override + public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + var value = typeConverter.apply(jsonParser); + + try { + return constructor.newInstance(value); + } catch ( + IllegalAccessException + | InvocationTargetException + | InstantiationException e) { + throw new IOException(e); + } + } + + private static Function getTypeConverter(Class wrappedType) { + if (String.class.isAssignableFrom(wrappedType)) { + return getStringConverter(); + } + if (Short.class.isAssignableFrom(wrappedType)) { + return getShortConverter(); + } + if (Integer.class.isAssignableFrom(wrappedType)) { + return getIntegerConverter(); + } + if (Long.class.isAssignableFrom(wrappedType)) { + return getLongConverter(); + } + if (BigInteger.class.isAssignableFrom(wrappedType)) { + return getBigIntegerConverter(); + } + if (Float.class.isAssignableFrom(wrappedType)) { + return getFloatConverter(); + } + if (Double.class.isAssignableFrom(wrappedType)) { + return getDoubleConverter(); + } + if (BigDecimal.class.isAssignableFrom(wrappedType)) { + return getBigDecimalConverter(); + } + if (ScaledBigDecimal.class.isAssignableFrom(wrappedType)) { + return getScaledBigDecimalConverter(); + } + throw new CustomTypeDeserializerException("Value type not supported: " + wrappedType.getName()); + } + + private static Function getScaledBigDecimalConverter() { + return jsonParser -> { + try { + return new ScaledBigDecimal(jsonParser.getDecimalValue()); + } catch (IOException e) { + throw new CustomTypeDeserializerException("Failed to read value as ScaledBigDecimal.", e); + } + }; + } + + private static Function getBigDecimalConverter() { + return jsonParser -> { + try { + return jsonParser.getDecimalValue(); + } catch (IOException e) { + throw new CustomTypeDeserializerException("Failed to read value as BigDecimal.", e); + } + }; + } + + private static Function getDoubleConverter() { + return jsonParser -> { + try { + return jsonParser.getDoubleValue(); + } catch (IOException e) { + throw new CustomTypeDeserializerException("Failed to read value as Double.", e); + } + }; + } + + private static Function getFloatConverter() { + return jsonParser -> { + try { + return jsonParser.getFloatValue(); + } catch (IOException e) { + throw new CustomTypeDeserializerException("Failed to read value as Float.", e); + } + }; + } + + 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 { + return jsonParser.getLongValue(); + } catch (IOException e) { + throw new CustomTypeDeserializerException("Failed to read value as Long.", e); + } + }; + } + + private static Function getIntegerConverter() { + return jsonParser -> { + try { + return jsonParser.getIntValue(); + } catch (IOException e) { + throw new CustomTypeDeserializerException("Failed to read value as Integer.", e); + } + }; + } + + private static Function getShortConverter() { + return jsonParser -> { + try { + return jsonParser.getShortValue(); + } catch (IOException e) { + throw new CustomTypeDeserializerException("Failed to read value as Short.", e); + } + }; + } + + private static Function getStringConverter() { + return jsonParser -> { + try { + return jsonParser.getValueAsString(); + } catch (IOException e) { + throw new CustomTypeDeserializerException("Failed to read value as String.", e); + } + }; + } + + public static final class CustomTypeDeserializerException extends RuntimeException { + public CustomTypeDeserializerException(String message) { + super(message); + } + + public CustomTypeDeserializerException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/jackson/CustomTypeSerializer.java b/src/main/java/it/aboutbits/springboot/toolbox/jackson/CustomTypeSerializer.java new file mode 100644 index 0000000..1b8909d --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/jackson/CustomTypeSerializer.java @@ -0,0 +1,38 @@ +package it.aboutbits.springboot.toolbox.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import it.aboutbits.springboot.toolbox.type.CustomType; + +import java.io.IOException; + +public class CustomTypeSerializer extends JsonSerializer> { + @SuppressWarnings("unchecked") + @Override + public Class> handledType() { + return (Class>) (Class) CustomType.class; + } + + @Override + public void serialize( + CustomType customType, + JsonGenerator jsonGenerator, + SerializerProvider serializerProvider + ) throws IOException { + var value = customType.value(); + + if (value instanceof String stringValue) { + jsonGenerator.writeString( + stringValue + ); + return; + } + + jsonGenerator.writeRawValue( + String.valueOf( + value + ) + ); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/mvc/CustomTypePropertyEditor.java b/src/main/java/it/aboutbits/springboot/toolbox/mvc/CustomTypePropertyEditor.java new file mode 100644 index 0000000..2b06cfd --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/mvc/CustomTypePropertyEditor.java @@ -0,0 +1,84 @@ +package it.aboutbits.springboot.toolbox.mvc; + +import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; +import it.aboutbits.springboot.toolbox.type.CustomType; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; +import lombok.NonNull; +import lombok.SneakyThrows; +import org.springframework.lang.Nullable; + +import java.beans.PropertyEditorSupport; +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 { + private final Constructor constructor; + private final Function typeConverter; + + @SneakyThrows + public CustomTypePropertyEditor(@NonNull Class customType) { + this.constructor = RecordReflectionUtil.getCanonicalConstructor(customType); + this.typeConverter = getTextToTypeConverter( + constructor.getParameters()[0].getType() + ); + } + + @SuppressWarnings("unchecked") + @Override + @Nullable + public String getAsText() { + var value = (T) getValue(); + + return value == null ? null : String.valueOf(value.value()); + } + + @Override + public void setAsText(String text) throws IllegalArgumentException { + var value = typeConverter.apply(text); + + try { + setValue( + constructor.newInstance(value) + ); + } catch ( + IllegalAccessException + | InvocationTargetException + | InstantiationException e) { + throw new IllegalArgumentException(e); + } + } + + private static Function getTextToTypeConverter(Class wrappedType) { + if (String.class.isAssignableFrom(wrappedType)) { + return text -> text; + } + if (Short.class.isAssignableFrom(wrappedType)) { + return Short::parseShort; + } + if (Integer.class.isAssignableFrom(wrappedType)) { + return Integer::parseInt; + } + if (Long.class.isAssignableFrom(wrappedType)) { + return Long::parseLong; + } + if (BigInteger.class.isAssignableFrom(wrappedType)) { + return BigInteger::new; + } + if (Float.class.isAssignableFrom(wrappedType)) { + return Float::parseFloat; + } + if (Double.class.isAssignableFrom(wrappedType)) { + return Double::parseDouble; + } + if (BigDecimal.class.isAssignableFrom(wrappedType)) { + return BigDecimal::new; + } + if (ScaledBigDecimal.class.isAssignableFrom(wrappedType)) { + return ScaledBigDecimal::new; + } + throw new IllegalArgumentException("Unable to convert text to type: " + wrappedType.getName()); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/EmailAddressJavaType.java b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/EmailAddressJavaType.java new file mode 100644 index 0000000..4ee5961 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/EmailAddressJavaType.java @@ -0,0 +1,11 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype; + +import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.base.WrappedStringJavaType; +import it.aboutbits.springboot.toolbox.type.EmailAddress; + +public final class EmailAddressJavaType extends WrappedStringJavaType implements AutoRegisteredJavaType { + public EmailAddressJavaType() { + super(EmailAddress.class); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/IbanJavaType.java b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/IbanJavaType.java new file mode 100644 index 0000000..020c787 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/IbanJavaType.java @@ -0,0 +1,11 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype; + +import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.base.WrappedStringJavaType; +import it.aboutbits.springboot.toolbox.type.Iban; + +public final class IbanJavaType extends WrappedStringJavaType implements AutoRegisteredJavaType { + public IbanJavaType() { + super(Iban.class); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/ScaledBigDecimalJavaType.java b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/ScaledBigDecimalJavaType.java new file mode 100644 index 0000000..7b659db --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/ScaledBigDecimalJavaType.java @@ -0,0 +1,11 @@ +package it.aboutbits.springboot.toolbox.persistence.javatype; + +import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.persistence.javatype.base.WrappedBigDecimalJavaType; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; + +public final class ScaledBigDecimalJavaType extends WrappedBigDecimalJavaType implements AutoRegisteredJavaType { + public ScaledBigDecimalJavaType() { + super(ScaledBigDecimal.class); + } +} 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 new file mode 100644 index 0000000..d9b2382 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedBigDecimalJavaType.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.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 + public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { + return indicators.getTypeConfiguration() + .getJdbcTypeRegistry() + .getDescriptor(Types.DOUBLE); + } + + @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 (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) { + var clazz = getJavaTypeClass(); + + if (value == null) { + return null; + } + if (clazz.isInstance(value)) { + return (T) value; + } + 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 new file mode 100644 index 0000000..37a6b36 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedDoubleJavaType.java @@ -0,0 +1,67 @@ +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.sql.Types; + +public abstract class WrappedDoubleJavaType> extends AbstractClassJavaType { + private final transient Constructor canonicalConstructor; + + protected WrappedDoubleJavaType(Class type) { + super(type); + + this.canonicalConstructor = RecordReflectionUtil.getCanonicalConstructor(type); + } + + @Override + public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { + return indicators.getTypeConfiguration() + .getJdbcTypeRegistry() + .getDescriptor(Types.DOUBLE); + } + + @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 (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) { + var clazz = getJavaTypeClass(); + + if (value == null) { + return null; + } + if (clazz.isInstance(value)) { + return (T) value; + } + if (value instanceof Double doubleValue) { + 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 new file mode 100644 index 0000000..c862e39 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedFloatJavaType.java @@ -0,0 +1,67 @@ +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.sql.Types; + +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.FLOAT); + } + + @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 (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) { + var clazz = getJavaTypeClass(); + + if (value == null) { + return null; + } + if (clazz.isInstance(value)) { + return (T) value; + } + if (value instanceof Float floatValue) { + 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 new file mode 100644 index 0000000..8c4d933 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedIntegerJavaType.java @@ -0,0 +1,67 @@ +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.sql.Types; + +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.INTEGER); + } + + @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 (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) { + var clazz = getJavaTypeClass(); + + if (value == null) { + return null; + } + if (clazz.isInstance(value)) { + return (T) value; + } + if (value instanceof Integer integerValue) { + 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 new file mode 100644 index 0000000..4949394 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedLongJavaType.java @@ -0,0 +1,67 @@ +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.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 + 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) id.value(); + } + + 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(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 new file mode 100644 index 0000000..3450c14 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedScaledBigDecimalJavaType.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 it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; +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.sql.Types; + +public abstract class WrappedScaledBigDecimalJavaType> extends AbstractClassJavaType { + private final transient Constructor canonicalConstructor; + + protected WrappedScaledBigDecimalJavaType(Class type) { + super(type); + + this.canonicalConstructor = RecordReflectionUtil.getCanonicalConstructor(type); + } + + @Override + public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { + return indicators.getTypeConfiguration() + .getJdbcTypeRegistry() + .getDescriptor(Types.DOUBLE); + } + + @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 (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) { + var clazz = getJavaTypeClass(); + + if (value == null) { + return null; + } + if (clazz.isInstance(value)) { + return (T) value; + } + 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 new file mode 100644 index 0000000..b5782fa --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedShortJavaType.java @@ -0,0 +1,67 @@ +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.sql.Types; + +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.SMALLINT); + } + + @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 (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) { + var clazz = getJavaTypeClass(); + + if (value == null) { + return null; + } + if (clazz.isInstance(value)) { + return (T) value; + } + if (value instanceof Short shortValue) { + 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 new file mode 100644 index 0000000..35b9fd6 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/persistence/javatype/base/WrappedStringJavaType.java @@ -0,0 +1,67 @@ +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.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 + public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { + return indicators.getTypeConfiguration() + .getJdbcTypeRegistry() + .getDescriptor(Types.VARCHAR); + } + + @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 (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) { + var clazz = getJavaTypeClass(); + + if (value == null) { + return null; + } + if (clazz.isInstance(value)) { + return (T) value; + } + if (value instanceof String 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 new file mode 100644 index 0000000..653dd52 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/ClassScannerUtil.java @@ -0,0 +1,42 @@ +package it.aboutbits.springboot.toolbox.reflection.util; + +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; +import lombok.NonNull; + +import java.util.Set; +import java.util.stream.Collectors; + +public final class ClassScannerUtil { + private ClassScannerUtil() { + } + + public static ClassScanner getScannerForPackages(String... packages) { + return new ClassScanner(packages); + } + + public static final class ClassScanner implements AutoCloseable { + private final ScanResult scanResult; + + private ClassScanner(String... packages) { + var result = new ClassGraph() + .enableAllInfo() + .acceptPackages(packages) + .scan(); + this.scanResult = result; + } + + @SuppressWarnings("unchecked") + public Set> getSubTypesOf(@NonNull Class clazz) { + return scanResult.getClassesImplementing(clazz).loadClasses() + .stream() + .map(item -> (Class) item) + .collect(Collectors.toSet()); + } + + @Override + public void close() { + scanResult.close(); + } + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/RecordReflectionUtil.java b/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/RecordReflectionUtil.java new file mode 100644 index 0000000..38e721a --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/RecordReflectionUtil.java @@ -0,0 +1,61 @@ +package it.aboutbits.springboot.toolbox.reflection.util; + +import java.lang.reflect.Constructor; + +public final class RecordReflectionUtil { + private RecordReflectionUtil() { + } + + @SuppressWarnings("unchecked") + public static Constructor getCanonicalConstructor(Class recordClass) { + if (!recordClass.isRecord()) { + throw new IllegalArgumentException("Class must be a record: " + recordClass.getName()); + } + + var constructors = recordClass.getDeclaredConstructors(); + + for (var constructor : constructors) { + if (parametersAreEqual(recordClass, constructor)) { + return (Constructor) constructor; + } + } + + throw new IllegalStateException("Canonical constructor not found for the record: " + recordClass.getName()); + } + + @SuppressWarnings("unchecked") + public static Constructor getConstructorForType( + Class recordClass, + Class type + ) { + if (!recordClass.isRecord()) { + throw new IllegalArgumentException("Class must be a record: " + recordClass.getName()); + } + + var constructors = recordClass.getDeclaredConstructors(); + + for (var constructor : constructors) { + if (constructor.getParameterCount() == 1 && constructor.getParameterTypes()[0].equals(type)) { + return (Constructor) constructor; + } + } + + throw new IllegalStateException("No constructor for type (" + type.getName() + ") found for the record: " + recordClass.getName()); + } + + private static boolean parametersAreEqual(Class recordClass, Constructor constructor) { + var recordComponents = recordClass.getRecordComponents(); + var parameterTypes = constructor.getParameterTypes(); + + if (recordComponents.length != parameterTypes.length) { + return false; + } + + for (int i = 0; i < recordComponents.length; i++) { + if (!parameterTypes[i].equals(recordComponents[i].getType())) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypeModelConverter.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypeModelConverter.java new file mode 100644 index 0000000..803937f --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypeModelConverter.java @@ -0,0 +1,61 @@ +package it.aboutbits.springboot.toolbox.swagger; + +import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.core.converter.ModelConverter; +import io.swagger.v3.core.converter.ModelConverterContext; +import io.swagger.v3.oas.models.media.Schema; +import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; +import it.aboutbits.springboot.toolbox.type.CustomType; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Iterator; + +public class CustomTypeModelConverter implements ModelConverter { + + @Override + public Schema resolve( + AnnotatedType annotatedType, + ModelConverterContext context, + Iterator chain + ) { + + var type = annotatedType.getType(); + + if (type instanceof Class clazz && CustomType.class.isAssignableFrom(clazz)) { + var constructor = RecordReflectionUtil.getCanonicalConstructor(clazz); + var wrappedType = constructor.getParameters()[0].getType(); + + if (Short.class.isAssignableFrom(wrappedType)) { + return context.resolve(new AnnotatedType(Short.TYPE)); + } + if (Integer.class.isAssignableFrom(wrappedType)) { + return context.resolve(new AnnotatedType(Integer.TYPE)); + } + 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)); + } + if (Double.class.isAssignableFrom(wrappedType)) { + return context.resolve(new AnnotatedType(Double.TYPE)); + } + 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)); + } + } + + return chain.hasNext() ? chain.next().resolve(annotatedType, context, chain) : null; + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypePropertyCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypePropertyCustomizer.java new file mode 100644 index 0000000..fd3f81e --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypePropertyCustomizer.java @@ -0,0 +1,123 @@ +package it.aboutbits.springboot.toolbox.swagger; + +import com.fasterxml.jackson.databind.type.SimpleType; +import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.oas.models.media.Schema; +import it.aboutbits.springboot.toolbox.reflection.util.RecordReflectionUtil; +import it.aboutbits.springboot.toolbox.type.CustomType; +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; +import it.aboutbits.springboot.toolbox.type.identity.EntityId; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.PropertyCustomizer; + +import java.math.BigDecimal; +import java.math.BigInteger; + +@Slf4j +public class CustomTypePropertyCustomizer implements PropertyCustomizer { + @Override + public Schema customize(Schema property, AnnotatedType annotatedType) { + var type = annotatedType.getType(); + + if (type instanceof SimpleType simpleType && CustomType.class.isAssignableFrom(simpleType.getRawClass())) { + var rawClass = simpleType.getRawClass(); + + var displayName = rawClass.getSimpleName(); + + if (EntityId.class.isAssignableFrom(rawClass)) { + displayName = resolveEntityIdDisplayName(rawClass); + } + + var constructor = RecordReflectionUtil.getCanonicalConstructor(rawClass); + var wrappedType = constructor.getParameters()[0].getType(); + + + if (Short.class.isAssignableFrom(wrappedType)) { + property.type("integer"); + property.format(""); + property.setDescription(displayName); + property.setProperties(null); + property.set$ref(null); + return property; + } + if (Integer.class.isAssignableFrom(wrappedType)) { + property.type("integer"); + property.format("int32"); + property.setDescription(displayName); + property.setProperties(null); + property.set$ref(null); + return property; + } + if (Long.class.isAssignableFrom(wrappedType)) { + property.type("integer"); + property.format("int64"); + property.setDescription(displayName); + property.setProperties(null); + 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"); + property.setDescription(displayName); + property.setProperties(null); + property.set$ref(null); + return property; + } + if (Double.class.isAssignableFrom(wrappedType)) { + property.type("number"); + property.format("double"); + property.setDescription(displayName); + property.setProperties(null); + property.set$ref(null); + return property; + } + if (BigDecimal.class.isAssignableFrom(wrappedType)) { + property.type("number"); + property.format(""); + property.setDescription(displayName); + property.setProperties(null); + 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); + property.setDescription(displayName); + property.setProperties(null); + property.set$ref(null); + return property; + } + log.warn("Property {} of type WrappedValue: Can not resolve parameter type!", property.getName()); + } + + return property; + } + + @NonNull + private static String resolveEntityIdDisplayName(Class rawClass) { + var parent = rawClass.getEnclosingClass(); + if (parent != null) { + return parent.getSimpleName() + "." + rawClass.getSimpleName(); + } + return rawClass.getSimpleName(); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/type/CustomType.java b/src/main/java/it/aboutbits/springboot/toolbox/type/CustomType.java new file mode 100644 index 0000000..fd9e12e --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/type/CustomType.java @@ -0,0 +1,5 @@ +package it.aboutbits.springboot.toolbox.type; + +public interface CustomType { + T value(); +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/type/EmailAddress.java b/src/main/java/it/aboutbits/springboot/toolbox/type/EmailAddress.java index 4c2869b..78245aa 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/type/EmailAddress.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/type/EmailAddress.java @@ -15,7 +15,7 @@ * @param value the email address string * @throws IllegalArgumentException if the provided email address is not in a valid format */ -public record EmailAddress(String value) implements Comparable { +public record EmailAddress(String value) implements CustomType, Comparable { public EmailAddress(String value) { if (value == null || EmailAddressValidator.isNotValid(value)) { throw new IllegalArgumentException("Value is not a valid email address: " + value); diff --git a/src/main/java/it/aboutbits/springboot/toolbox/type/Iban.java b/src/main/java/it/aboutbits/springboot/toolbox/type/Iban.java index 0098da5..a180661 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/type/Iban.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/type/Iban.java @@ -11,7 +11,7 @@ * * @param value the IBAN value, which must be a valid and non-null string */ -public record Iban(String value) implements Comparable { +public record Iban(String value) implements CustomType, Comparable { public Iban(String value) { if (value == null || IbanValidator.isNotValid(value.toUpperCase())) { throw new IllegalArgumentException("Value is not a valid IBAN: " + value); 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 3ffc5ec..5a4dda6 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/type/ScaledBigDecimal.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/type/ScaledBigDecimal.java @@ -14,7 +14,7 @@ */ public record ScaledBigDecimal( @NonNull BigDecimal value -) implements Comparable { +) implements CustomType, Comparable { private static final MathContext MATH_CONTEXT = new MathContext(15, RoundingMode.HALF_UP); public static final ScaledBigDecimal ZERO = new ScaledBigDecimal(0); @@ -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/identity/EntityId.java b/src/main/java/it/aboutbits/springboot/toolbox/type/identity/EntityId.java new file mode 100644 index 0000000..4f8ad59 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/type/identity/EntityId.java @@ -0,0 +1,14 @@ +package it.aboutbits.springboot.toolbox.type.identity; + +import it.aboutbits.springboot.toolbox.type.CustomType; + +import java.io.Serializable; + +/** + * The Identity interface represents a serializable entity that wraps a value. + * A class implementing Identity is considered to be the primary key of an entity type. + * + * @param the type of the value being wrapped + */ +public interface EntityId extends CustomType, Serializable { +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/type/identity/Identified.java b/src/main/java/it/aboutbits/springboot/toolbox/type/identity/Identified.java new file mode 100644 index 0000000..6d0613b --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/type/identity/Identified.java @@ -0,0 +1,5 @@ +package it.aboutbits.springboot.toolbox.type.identity; + +public interface Identified> { + ID getId(); +} 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..51be45a --- /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.autoconfiguration.swagger.RegisterCustomTypesWithSwagger; +import it.aboutbits.springboot.toolbox.autoconfiguration.web.RegisterCustomTypesWithJacksonAndMvc; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SuppressWarnings("checkstyle:HideUtilityClassConstructor") +@SpringBootApplication +@RegisterCustomTypesWithJacksonAndMvc +@RegisterCustomTypesWithSwagger +public class TestApp { + + public static void main(String[] args) { + SpringApplication.run(TestApp.class, args); + } + +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/CustomTypeBindingsForControllerTest.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/CustomTypeBindingsForControllerTest.java new file mode 100644 index 0000000..0017b0a --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/CustomTypeBindingsForControllerTest.java @@ -0,0 +1,190 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.mvc; + +import com.fasterxml.jackson.databind.ObjectMapper; +import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithEmailAddress; +import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithIban; +import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithScaledBigDecimal; +import it.aboutbits.springboot.toolbox.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/autoconfiguration/mvc/EntityIdBindingsForControllerTest.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/EntityIdBindingsForControllerTest.java new file mode 100644 index 0000000..b6c2a6c --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/EntityIdBindingsForControllerTest.java @@ -0,0 +1,91 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.mvc; + +import com.fasterxml.jackson.databind.ObjectMapper; +import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithEntityId; +import it.aboutbits.springboot.toolbox.autoconfiguration.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/autoconfiguration/mvc/body/BodyWithEmailAddress.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/body/BodyWithEmailAddress.java new file mode 100644 index 0000000..6feb6ff --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/body/BodyWithEmailAddress.java @@ -0,0 +1,8 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body; + +import it.aboutbits.springboot.toolbox.type.EmailAddress; + +public record BodyWithEmailAddress( + EmailAddress emailAddress +) { +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/body/BodyWithEntityId.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/body/BodyWithEntityId.java new file mode 100644 index 0000000..5a75d33 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/body/BodyWithEntityId.java @@ -0,0 +1,8 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body; + +import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa.CustomTypeTestModel; + +public record BodyWithEntityId( + CustomTypeTestModel.ID entityId +) { +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/body/BodyWithIban.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/body/BodyWithIban.java new file mode 100644 index 0000000..607e29f --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/body/BodyWithIban.java @@ -0,0 +1,8 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body; + +import it.aboutbits.springboot.toolbox.type.Iban; + +public record BodyWithIban( + Iban iban +) { +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/body/BodyWithScaledBigDecimal.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/body/BodyWithScaledBigDecimal.java new file mode 100644 index 0000000..e2c9cea --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/body/BodyWithScaledBigDecimal.java @@ -0,0 +1,8 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body; + +import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal; + +public record BodyWithScaledBigDecimal( + ScaledBigDecimal scaledBigDecimal +) { +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/controller/CustomTypeTestController.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/controller/CustomTypeTestController.java new file mode 100644 index 0000000..7c4d827 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/controller/CustomTypeTestController.java @@ -0,0 +1,64 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.mvc.controller; + +import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithEmailAddress; +import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithIban; +import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithScaledBigDecimal; +import it.aboutbits.springboot.toolbox.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/autoconfiguration/mvc/controller/EntityIdTestController.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/controller/EntityIdTestController.java new file mode 100644 index 0000000..abd6ccc --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/controller/EntityIdTestController.java @@ -0,0 +1,30 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.mvc.controller; + +import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithEntityId; +import it.aboutbits.springboot.toolbox.autoconfiguration.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/autoconfiguration/persistence/CustomTypeJpaTest.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/CustomTypeJpaTest.java new file mode 100644 index 0000000..0cca224 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/CustomTypeJpaTest.java @@ -0,0 +1,76 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.persistence; + +import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa.CustomTypeTestModel; +import it.aboutbits.springboot.toolbox.autoconfiguration.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/autoconfiguration/persistence/EntityIdJpaTest.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/EntityIdJpaTest.java new file mode 100644 index 0000000..eafb95d --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/EntityIdJpaTest.java @@ -0,0 +1,54 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.persistence; + +import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa.CustomTypeTestModel; +import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa.CustomTypeTestModelRepository; +import it.aboutbits.springboot.toolbox.autoconfiguration.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/autoconfiguration/persistence/impl/jpa/CustomTypeTestModel.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/CustomTypeTestModel.java new file mode 100644 index 0000000..73e2d15 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/CustomTypeTestModel.java @@ -0,0 +1,62 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa; + +import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.type.identity.EntityId; +import it.aboutbits.springboot.toolbox.type.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/autoconfiguration/persistence/impl/jpa/CustomTypeTestModelRepository.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/CustomTypeTestModelRepository.java new file mode 100644 index 0000000..6616575 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/CustomTypeTestModelRepository.java @@ -0,0 +1,18 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.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/autoconfiguration/persistence/impl/jpa/ReferencedTestModel.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/ReferencedTestModel.java new file mode 100644 index 0000000..38ca9a7 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/ReferencedTestModel.java @@ -0,0 +1,25 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa; + +import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.type.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/autoconfiguration/swagger/SwaggerTest.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/swagger/SwaggerTest.java new file mode 100644 index 0000000..0e57c3e --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/swagger/SwaggerTest.java @@ -0,0 +1,36 @@ +package it.aboutbits.springboot.toolbox.autoconfiguration.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..f754b8c --- /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.autoconfiguration.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..71d2a5d --- /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.autoconfiguration.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..2e69f31 --- /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.autoconfiguration.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..81a1815 --- /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.autoconfiguration.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..d20de96 --- /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.autoconfiguration.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..9375633 --- /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.autoconfiguration.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..0959545 --- /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.autoconfiguration.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..78d3a36 --- /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.autoconfiguration.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..43466e3 --- /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.autoconfiguration.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..f2b5d84 --- /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.autoconfiguration.persistence.AutoRegisteredJavaType; +import it.aboutbits.springboot.toolbox.type.identity.EntityId; +import it.aboutbits.springboot.toolbox.type.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