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 extends T>) 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