Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

<groupId>it.aboutbits</groupId>
<artifactId>spring-boot-toolbox</artifactId>
<version>2.5.0</version>
<version>2.5.1-RC3</version>
<description>Utility library for Spring Boot projects.</description>
<packaging>jar</packaging>

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -41,7 +42,7 @@ public void setAdditionalTypePackages(String[] additionalTypePackages) {
this.relevantTypes = findAllCustomTypes(classScanner);
}

@SuppressWarnings("rawtypes")
@SuppressWarnings({"rawtypes", "unchecked"})
public static Set<Class<? extends CustomType>> findAllCustomTypes(ClassScannerUtil.ClassScanner classScanner) {
return classScanner.getSubTypesOf(CustomType.class).stream()
.filter(item ->
Expand All @@ -50,6 +51,27 @@ public static Set<Class<? extends CustomType>> 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());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,29 @@
@NullMarked
public class CustomTypeDeserializer<T extends CustomType<?>> extends ValueDeserializer<T> {
private final Class<T> customType;
private final Constructor<T> constructor;
private final @Nullable Constructor<T> constructor;
private final Function<JsonParser, @Nullable Object> typeConverter;

public CustomTypeDeserializer(Class<T> 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
Expand All @@ -49,9 +54,18 @@ public Class<T> 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 (
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -17,7 +21,7 @@ public static <T extends CustomType<?>> Constructor<T> getCustomTypeConstructor(
return customType.getConstructor(
getWrappedType(customType)
);
} catch (NoSuchMethodException | SecurityException e) {
} catch (NoSuchMethodException | SecurityException _) {
throw new NoSuchMethodException();
}
}
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,27 @@

@NullMarked
public final class CustomTypePropertyEditor<T extends CustomType<?>> extends PropertyEditorSupport {
private final Constructor<T> constructor;
private final @Nullable Constructor<T> constructor;
private final Function<@Nullable String, @Nullable Object> typeConverter;

public CustomTypePropertyEditor(Class<T> 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")
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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));

Expand All @@ -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();
Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<DirectEnumEntityId> {
VAL1,
VAL2;

@Override
public DirectEnumEntityId value() {
return this;
}
}
Loading