diff --git a/pom.xml b/pom.xml
index e819c7c..e9ffd90 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
it.aboutbits
spring-boot-toolbox
- 2.5.0
+ 2.5.1-RC3
Utility library for Spring Boot projects.
jar
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
index 36242c9..51d4afe 100644
--- a/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/web/CustomTypeScanner.java
+++ b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/web/CustomTypeScanner.java
@@ -1,6 +1,7 @@
package it.aboutbits.springboot.toolbox.autoconfiguration.web;
import it.aboutbits.springboot.toolbox.reflection.util.ClassScannerUtil;
+import it.aboutbits.springboot.toolbox.reflection.util.CustomTypeReflectionUtil;
import it.aboutbits.springboot.toolbox.type.CustomType;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@@ -41,7 +42,7 @@ public void setAdditionalTypePackages(String[] additionalTypePackages) {
this.relevantTypes = findAllCustomTypes(classScanner);
}
- @SuppressWarnings("rawtypes")
+ @SuppressWarnings({"rawtypes", "unchecked"})
public static Set> findAllCustomTypes(ClassScannerUtil.ClassScanner classScanner) {
return classScanner.getSubTypesOf(CustomType.class).stream()
.filter(item ->
@@ -50,6 +51,27 @@ public static Set> findAllCustomTypes(ClassScannerUt
&& !Modifier.isAbstract(item.getModifiers())
&& !item.isAnnotationPresent(DisableCustomTypeConfiguration.class)
)
+ .filter(item -> {
+ if (item.isEnum()) {
+ return true;
+ }
+
+ try {
+ var constructor = CustomTypeReflectionUtil.getCustomTypeConstructor((Class extends CustomType>>) item);
+ var wrappedType = constructor.getParameterTypes()[0];
+
+ if (!CustomTypeReflectionUtil.isSupportedWrappedType(wrappedType)) {
+ log.debug("CustomType {} has an unsupported wrapped type {} and will be ignored.", item.getName(), wrappedType.getName());
+ return false;
+ }
+
+ return true;
+ } catch (NoSuchMethodException _) {
+ log.debug("CustomType {} is missing the required constructor and will be ignored.", item.getName());
+
+ return false;
+ }
+ })
.collect(Collectors.toSet());
}
diff --git a/src/main/java/it/aboutbits/springboot/toolbox/jackson/CustomTypeDeserializer.java b/src/main/java/it/aboutbits/springboot/toolbox/jackson/CustomTypeDeserializer.java
index 877447e..ccbc825 100644
--- a/src/main/java/it/aboutbits/springboot/toolbox/jackson/CustomTypeDeserializer.java
+++ b/src/main/java/it/aboutbits/springboot/toolbox/jackson/CustomTypeDeserializer.java
@@ -23,24 +23,29 @@
@NullMarked
public class CustomTypeDeserializer> extends ValueDeserializer {
private final Class customType;
- private final Constructor constructor;
+ private final @Nullable Constructor constructor;
private final Function typeConverter;
public CustomTypeDeserializer(Class customType) {
this.customType = customType;
- try {
- this.constructor = CustomTypeReflectionUtil.getCustomTypeConstructor(customType);
- } catch (NoSuchMethodException e) {
- throw new CustomTypeDeserializerException(
- "Unable to find constructor for type: " + customType.getName(),
- e
+ if (customType.isEnum()) {
+ this.constructor = null;
+ this.typeConverter = getEnumConverter(customType);
+ } else {
+ try {
+ this.constructor = CustomTypeReflectionUtil.getCustomTypeConstructor(customType);
+ } catch (NoSuchMethodException e) {
+ throw new CustomTypeDeserializerException(
+ "Unable to find constructor for type: " + customType.getName(),
+ e
+ );
+ }
+
+ this.typeConverter = getTypeConverter(
+ constructor.getParameterTypes()[0]
);
}
-
- this.typeConverter = getTypeConverter(
- constructor.getParameterTypes()[0]
- );
}
@Override
@@ -49,9 +54,18 @@ public Class handledType() {
}
@Override
- public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) {
+ @SuppressWarnings("unchecked")
+ public @Nullable T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) {
var value = typeConverter.apply(jsonParser);
+ if (value == null) {
+ return null;
+ }
+
+ if (constructor == null) {
+ return (T) value;
+ }
+
try {
return constructor.newInstance(value);
} catch (
diff --git a/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/CustomTypeReflectionUtil.java b/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/CustomTypeReflectionUtil.java
index 3d39ee0..9689c0d 100644
--- a/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/CustomTypeReflectionUtil.java
+++ b/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/CustomTypeReflectionUtil.java
@@ -1,11 +1,15 @@
package it.aboutbits.springboot.toolbox.reflection.util;
import it.aboutbits.springboot.toolbox.type.CustomType;
+import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal;
import org.jspecify.annotations.NullMarked;
import java.lang.reflect.Constructor;
import java.lang.reflect.ParameterizedType;
+import java.math.BigDecimal;
+import java.math.BigInteger;
import java.util.Arrays;
+import java.util.UUID;
@NullMarked
public final class CustomTypeReflectionUtil {
@@ -17,7 +21,7 @@ public static > Constructor getCustomTypeConstructor(
return customType.getConstructor(
getWrappedType(customType)
);
- } catch (NoSuchMethodException | SecurityException e) {
+ } catch (NoSuchMethodException | SecurityException _) {
throw new NoSuchMethodException();
}
}
@@ -43,4 +47,22 @@ public static Class> getWrappedType(Class extends CustomType>> customType)
throw new NoSuchMethodException();
}
+
+ public static boolean isSupportedWrappedType(Class> wrappedType) {
+ return Boolean.class.isAssignableFrom(wrappedType)
+ || String.class.isAssignableFrom(wrappedType)
+ || Character.class.isAssignableFrom(wrappedType)
+ || Byte.class.isAssignableFrom(wrappedType)
+ || Short.class.isAssignableFrom(wrappedType)
+ || Integer.class.isAssignableFrom(wrappedType)
+ || Long.class.isAssignableFrom(wrappedType)
+ || Float.class.isAssignableFrom(wrappedType)
+ || Double.class.isAssignableFrom(wrappedType)
+ || BigInteger.class.isAssignableFrom(wrappedType)
+ || BigDecimal.class.isAssignableFrom(wrappedType)
+ || ScaledBigDecimal.class.isAssignableFrom(wrappedType)
+ || UUID.class.isAssignableFrom(wrappedType)
+ || Enum.class.isAssignableFrom(wrappedType)
+ || wrappedType.isEnum();
+ }
}
diff --git a/src/main/java/it/aboutbits/springboot/toolbox/web/CustomTypePropertyEditor.java b/src/main/java/it/aboutbits/springboot/toolbox/web/CustomTypePropertyEditor.java
index 45d9f5d..8ab6afd 100644
--- a/src/main/java/it/aboutbits/springboot/toolbox/web/CustomTypePropertyEditor.java
+++ b/src/main/java/it/aboutbits/springboot/toolbox/web/CustomTypePropertyEditor.java
@@ -17,22 +17,27 @@
@NullMarked
public final class CustomTypePropertyEditor> extends PropertyEditorSupport {
- private final Constructor constructor;
+ private final @Nullable Constructor constructor;
private final Function<@Nullable String, @Nullable Object> typeConverter;
public CustomTypePropertyEditor(Class customType) {
- try {
- this.constructor = CustomTypeReflectionUtil.getCustomTypeConstructor(customType);
- } catch (NoSuchMethodException e) {
- throw new CustomTypeDeserializer.CustomTypeDeserializerException(
- "Unable to find constructor for type: " + customType.getName(),
- e
+ if (customType.isEnum()) {
+ this.constructor = null;
+ this.typeConverter = toEnumConverter(customType);
+ } else {
+ try {
+ this.constructor = CustomTypeReflectionUtil.getCustomTypeConstructor(customType);
+ } catch (NoSuchMethodException e) {
+ throw new CustomTypeDeserializer.CustomTypeDeserializerException(
+ "Unable to find constructor for type: " + customType.getName(),
+ e
+ );
+ }
+
+ this.typeConverter = getTextToTypeConverter(
+ constructor.getParameters()[0].getType()
);
}
-
- this.typeConverter = getTextToTypeConverter(
- constructor.getParameters()[0].getType()
- );
}
@SuppressWarnings("unchecked")
@@ -45,9 +50,14 @@ public String getAsText() {
}
@Override
- public void setAsText(String text) throws IllegalArgumentException {
+ public void setAsText(@Nullable String text) throws IllegalArgumentException {
var value = typeConverter.apply(text);
+ if (constructor == null) {
+ setValue(value);
+ return;
+ }
+
try {
setValue(
constructor.newInstance(value)
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
index 9af44d6..7e9f677 100644
--- a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/EntityIdBindingsForControllerTest.java
+++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/EntityIdBindingsForControllerTest.java
@@ -6,6 +6,7 @@
import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithEnumEntityId;
import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa.CustomTypeEnumTestModel;
import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa.CustomTypeTestModel;
+import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa.DirectEnumEntityId;
import org.jspecify.annotations.NullMarked;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@@ -117,6 +118,50 @@ void emailAddressAsBody() throws Exception {
}
}
+ @Nested
+ @ArchIgnoreNoProductionCounterpart
+ class DirectEnumEntityIdTest {
+ @Test
+ void valueAsPathVariable() throws Exception {
+ var value = DirectEnumEntityId.VAL1;
+
+ var resultAsString = performGetAndReturnResult(
+ String.format("/test/entity-id/DirectEnumEntityId/as-path-variable/%s", value)
+ );
+
+ var actual = jsonMapper.readValue(resultAsString, DirectEnumEntityId.class);
+
+ assertThat(actual).isEqualTo(value);
+ }
+
+ @Test
+ void valueAsRequestParameter() throws Exception {
+ var value = DirectEnumEntityId.VAL2;
+
+ var resultAsString = performGetAndReturnResult(
+ String.format("/test/entity-id/DirectEnumEntityId/as-request-parameter?value=%s", value)
+ );
+
+ var actual = jsonMapper.readValue(resultAsString, DirectEnumEntityId.class);
+
+ assertThat(actual).isEqualTo(value);
+ }
+
+ @Test
+ void valueAsBody() throws Exception {
+ var value = DirectEnumEntityId.VAL1;
+
+ var resultAsString = performPostAndReturnResult(
+ "/test/entity-id/DirectEnumEntityId/as-body",
+ value
+ );
+
+ var actual = jsonMapper.readValue(resultAsString, DirectEnumEntityId.class);
+
+ assertThat(actual).isEqualTo(value);
+ }
+ }
+
private String performGetAndReturnResult(String url) throws Exception {
var requestBuilder = MockMvcRequestBuilders.get(url)
.contentType(MediaType.APPLICATION_JSON);
diff --git a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/controller/EntityIdTestController.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/controller/EntityIdTestController.java
index 38d87de..a46c6e2 100644
--- 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
@@ -4,6 +4,7 @@
import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithEnumEntityId;
import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa.CustomTypeEnumTestModel;
import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa.CustomTypeTestModel;
+import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa.DirectEnumEntityId;
import org.jspecify.annotations.NullMarked;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -46,4 +47,19 @@ public CustomTypeEnumTestModel.ID customTypeEnumTestModelIdAsRequestParameter(@R
public BodyWithEnumEntityId customTypeEnumTestModelIdAsBody(@RequestBody BodyWithEnumEntityId value) {
return value;
}
+
+ @GetMapping("/DirectEnumEntityId/as-path-variable/{value}")
+ public DirectEnumEntityId directEnumEntityIdAsPathVariable(@PathVariable DirectEnumEntityId value) {
+ return value;
+ }
+
+ @GetMapping("/DirectEnumEntityId/as-request-parameter")
+ public DirectEnumEntityId directEnumEntityIdAsRequestParameter(@RequestParam DirectEnumEntityId value) {
+ return value;
+ }
+
+ @PostMapping("/DirectEnumEntityId/as-body")
+ public DirectEnumEntityId directEnumEntityIdAsBody(@RequestBody DirectEnumEntityId value) {
+ return value;
+ }
}
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
index cc51eaf..4b79dfb 100644
--- a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/EntityIdJpaTest.java
+++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/EntityIdJpaTest.java
@@ -7,6 +7,9 @@
import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa.CustomTypeEnumTestModelRepository;
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.DirectEnumEntityId;
+import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa.DirectEnumIdTestModel;
+import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa.DirectEnumIdTestModelRepository;
import it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa.ReferencedTestModel;
import org.jspecify.annotations.NullMarked;
import org.junit.jupiter.api.Nested;
@@ -28,10 +31,13 @@ class EntityIdJpaTest {
@Autowired
CustomTypeEnumTestModelRepository repositoryEnum;
+ @Autowired
+ DirectEnumIdTestModelRepository repositoryDirectEnum;
+
@Nested
class OwnId {
@Test
- void inAndOut_shouldSucceed() {
+ void ownId_inAndOut_shouldSucceed() {
var item = new CustomTypeTestModel();
var savedItem = repository.save(item);
@@ -48,7 +54,7 @@ void inAndOut_shouldSucceed() {
@Nested
class ReferencedId {
@Test
- void inAndOut_shouldSucceed() {
+ void referencedId_inAndOut_shouldSucceed() {
var item = new CustomTypeTestModel();
item.setReferencedId(new ReferencedTestModel.ID(1234L));
@@ -66,7 +72,7 @@ void inAndOut_shouldSucceed() {
@Nested
class EnumId {
@Test
- void inAndOut_shouldSucceed() {
+ void enumId_inAndOut_shouldSucceed() {
var values = CustomTypeEnumTestModel.CustomTypeEnum.values();
var item = new CustomTypeEnumTestModel();
@@ -84,4 +90,24 @@ values[new Random().nextInt(values.length)]
.isEqualTo(savedItem);
}
}
+
+ @Nested
+ class DirectEnumId {
+ @Test
+ void directEnumId_inAndOut_shouldSucceed() {
+ var values = DirectEnumEntityId.values();
+
+ var item = new DirectEnumIdTestModel();
+ item.setId(values[new Random().nextInt(values.length)]);
+
+ var savedItem = repositoryDirectEnum.save(item);
+
+ var retrievedItem = repositoryDirectEnum.findById(savedItem.getId());
+
+ assertThat(retrievedItem).isPresent()
+ .get()
+ .usingRecursiveComparison()
+ .isEqualTo(savedItem);
+ }
+ }
}
diff --git a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/DirectEnumEntityId.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/DirectEnumEntityId.java
new file mode 100644
index 0000000..8a9f7af
--- /dev/null
+++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/DirectEnumEntityId.java
@@ -0,0 +1,15 @@
+package it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa;
+
+import it.aboutbits.springboot.toolbox.type.identity.EntityId;
+import org.jspecify.annotations.NullMarked;
+
+@NullMarked
+public enum DirectEnumEntityId implements EntityId {
+ VAL1,
+ VAL2;
+
+ @Override
+ public DirectEnumEntityId value() {
+ return this;
+ }
+}
diff --git a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/DirectEnumIdTestModel.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/DirectEnumIdTestModel.java
new file mode 100644
index 0000000..ccf0abe
--- /dev/null
+++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/DirectEnumIdTestModel.java
@@ -0,0 +1,23 @@
+package it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa;
+
+import it.aboutbits.springboot.toolbox.type.identity.Identified;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.Getter;
+import lombok.Setter;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.NullUnmarked;
+
+@Entity
+@Getter
+@Setter
+@Table(name = "direct_enum_id_test_model")
+@NullUnmarked
+public class DirectEnumIdTestModel implements Identified<@NonNull DirectEnumEntityId> {
+ @Id
+ @Enumerated(EnumType.STRING)
+ private DirectEnumEntityId id;
+}
diff --git a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/DirectEnumIdTestModelRepository.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/DirectEnumIdTestModelRepository.java
new file mode 100644
index 0000000..79893a2
--- /dev/null
+++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/impl/jpa/DirectEnumIdTestModelRepository.java
@@ -0,0 +1,8 @@
+package it.aboutbits.springboot.toolbox.autoconfiguration.persistence.impl.jpa;
+
+import org.jspecify.annotations.NullMarked;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+@NullMarked
+public interface DirectEnumIdTestModelRepository extends JpaRepository {
+}
diff --git a/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/web/CustomTypeScannerTest.java b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/web/CustomTypeScannerTest.java
new file mode 100644
index 0000000..bfc0200
--- /dev/null
+++ b/src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/web/CustomTypeScannerTest.java
@@ -0,0 +1,67 @@
+package it.aboutbits.springboot.toolbox.autoconfiguration.web;
+
+import it.aboutbits.springboot.toolbox.reflection.util.ClassScannerUtil;
+import it.aboutbits.springboot.toolbox.type.CustomType;
+import org.jspecify.annotations.NullMarked;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@NullMarked
+class CustomTypeScannerTest {
+ @Test
+ @SuppressWarnings("unchecked")
+ void findAllCustomTypes_shouldFilterOutTypesMissingConstructor() {
+ // given
+ ClassScannerUtil.ClassScanner classScanner = mock(ClassScannerUtil.ClassScanner.class);
+ when(classScanner.getSubTypesOf(CustomType.class)).thenReturn(Set.of(
+ EnumCustomType.class,
+ InvalidCustomType.class,
+ UnsupportedWrappedTypeCustomType.class
+ ));
+
+ // when
+ Set> result = CustomTypeScanner.findAllCustomTypes(classScanner);
+
+ // then
+ assertThat(result)
+ .containsExactly(EnumCustomType.class)
+ .doesNotContain(InvalidCustomType.class)
+ .doesNotContain(UnsupportedWrappedTypeCustomType.class);
+ }
+
+ public enum EnumCustomType implements CustomType {
+ VALUE;
+
+ @Override
+ public EnumCustomType value() {
+ return this;
+ }
+ }
+
+ @SuppressWarnings("checkstyle:RedundantModifier")
+ public static class InvalidCustomType implements CustomType {
+ @Override
+ public String value() {
+ return "test";
+ }
+ }
+
+ @SuppressWarnings("checkstyle:RedundantModifier")
+ public static class UnsupportedWrappedTypeCustomType implements CustomType