Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.UUID;
import java.util.function.Function;

public class CustomTypeDeserializer<T extends CustomType<?>> extends JsonDeserializer<T> {
Expand Down Expand Up @@ -92,6 +93,9 @@ private static Function<JsonParser, Object> getTypeConverter(Class<?> wrappedTyp
if (ScaledBigDecimal.class.isAssignableFrom(wrappedType)) {
return getScaledBigDecimalConverter();
}
if (UUID.class.isAssignableFrom(wrappedType)) {
return getUUIDConverter();
}
throw new CustomTypeDeserializerException("Value type not supported: " + wrappedType.getName());
}

Expand Down Expand Up @@ -219,6 +223,20 @@ private static Function<JsonParser, Object> getCharConverter() {
};
}

private static Function<JsonParser, Object> getUUIDConverter() {
return jsonParser -> {
try {
var value = jsonParser.getValueAsString();
if (value == null || value.length() != 36) {
throw new IOException();
}
return UUID.fromString(value);
} catch (IOException e) {
throw new CustomTypeDeserializerException("Failed to read value as UUID.", e);
}
};
}

public static final class CustomTypeDeserializerException extends RuntimeException {
public CustomTypeDeserializerException(String message) {
super(message);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package it.aboutbits.springboot.toolbox.persistence.converter;

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;

import java.util.UUID;

@Converter
public class UUIDConverter implements AttributeConverter<UUID, String> {
@Override
public String convertToDatabaseColumn(UUID attribute) {
if (attribute == null) {
return null;
}

return attribute.toString();
}

@Override
public UUID convertToEntityAttribute(String dbData) {
if (dbData == null || dbData.isBlank()) {
return null;
}

return UUID.fromString(dbData);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package it.aboutbits.springboot.toolbox.persistence.javatype.base;

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.nio.charset.StandardCharsets;
import java.sql.Types;
import java.util.UUID;

public abstract class WrappedUUIDJavaType<T extends CustomType<UUID>> extends AbstractClassJavaType<T> {
private final transient Constructor<T> constructor;

protected WrappedUUIDJavaType(Class<T> type) {
super(type);

try {
this.constructor = type.getConstructor(UUID.class);
} catch (NoSuchMethodException e) {
throw new IllegalStateException("No method found for " + type.getName(), e);
}
}

@Override
public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) {
return indicators.getTypeConfiguration()
.getJdbcTypeRegistry()
.getDescriptor(Types.OTHER);
}

@SuppressWarnings("unchecked")
@Override
public <X> X unwrap(T id, Class<X> aClass, WrapperOptions wrapperOptions) {
var javaTypeClass = getJavaTypeClass();

if (id == null) {
return null;
}
if (javaTypeClass.isAssignableFrom(aClass)) {
return (X) id;
}
if (UUID.class.isAssignableFrom(aClass)) {
return (X) id.value();
}
if (String.class.isAssignableFrom(aClass)) {
return (X) id.value().toString();
}
if (byte[].class.isAssignableFrom(aClass)) {
return (X) id.value().toString().getBytes(StandardCharsets.UTF_8);
}

throw unknownUnwrap(aClass);
}

@SuppressWarnings("unchecked")
@SneakyThrows({InstantiationException.class, IllegalAccessException.class, InvocationTargetException.class})
@Override
public <X> T wrap(X value, WrapperOptions wrapperOptions) {
var clazz = getJavaTypeClass();

if (value == null) {
return null;
}
if (clazz.isInstance(value)) {
return (T) value;
}
if (value instanceof UUID uuidValue) {
return constructor.newInstance(uuidValue);
}

throw unknownWrap(value.getClass());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Iterator;
import java.util.UUID;

public class CustomTypeModelConverter implements ModelConverter {

Expand Down Expand Up @@ -71,6 +72,9 @@ public Schema<?> resolve(
if (Character.class.isAssignableFrom(wrappedType)) {
result = context.resolve(new AnnotatedType(Character.TYPE));
}
if (UUID.class.isAssignableFrom(wrappedType)) {
result = context.resolve(new AnnotatedType(UUID.class));
}

if (result != null) {
var isIdentity = EntityId.class.isAssignableFrom(clazz);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.UUID;

@Slf4j
public class CustomTypePropertyCustomizer implements PropertyCustomizer {
Expand Down Expand Up @@ -91,7 +92,7 @@ public Schema<?> customize(Schema property, AnnotatedType annotatedType) {
}
if (BigInteger.class.isAssignableFrom(wrappedType)) {
property.type("integer");
property.format("int64");
property.format("");
Comment thread
ThoSap marked this conversation as resolved.
Comment on lines -94 to +95

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A BigDecimal can be bigger than int64, therefore I removed the format here.
https://swagger.io/docs/specification/v3_0/data-models/data-types/#numbers

Comment thread
ThoSap marked this conversation as resolved.
property.setDescription(description);
property.setProperties(null);
property.set$ref(null);
Expand Down Expand Up @@ -147,6 +148,16 @@ public Schema<?> customize(Schema property, AnnotatedType annotatedType) {
property.set$ref(null);
return property;
}
if (UUID.class.isAssignableFrom(wrappedType)) {
property.type("string");
property.format("uuid");
Comment on lines +152 to +153

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OpenAPI specification has a built-in format for UUID, see https://swagger.io/docs/specification/v3_0/data-models/data-types/#strings

property.minLength(36);
property.maxLength(36);
property.setDescription(description);
property.setProperties(null);
property.set$ref(null);
return property;
}

log.warn("Property {} of type WrappedValue: Can not resolve parameter type!", property.getName());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.UUID;
import java.util.function.Function;

public final class CustomTypePropertyEditor<T extends CustomType<?>> extends PropertyEditorSupport {
Expand Down Expand Up @@ -100,6 +101,9 @@ private static Function<String, Object> getTextToTypeConverter(Class<?> wrappedT
if (ScaledBigDecimal.class.isAssignableFrom(wrappedType)) {
return ScaledBigDecimal::new;
}
if (UUID.class.isAssignableFrom(wrappedType)) {
return UUID::fromString;
}
throw new IllegalArgumentException("Unable to convert text to type: " + wrappedType.getName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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.autoconfiguration.mvc.body.BodyWithUUID;
import it.aboutbits.springboot.toolbox.support.HttpTest;
import it.aboutbits.springboot.toolbox.type.EmailAddress;
import it.aboutbits.springboot.toolbox.type.Iban;
Expand All @@ -18,6 +19,8 @@
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;

@HttpTest
Expand Down Expand Up @@ -166,6 +169,54 @@ void ScaledBigDecimalAsBody(double doubleValue) throws Exception {
}
}

@Nested
class UUIDType {
@ParameterizedTest
@ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"})
void UUIDAsPathVariable(String uuidStringValue) throws Exception {
var value = UUID.fromString(uuidStringValue);

var resultAsString = performGetAndReturnResult(
String.format("/test/type/UUID/as-path-variable/%s", value)
);

var actual = objectMapper.readValue(resultAsString, UUID.class);

assertThat(actual).isEqualTo(value);
}

@ParameterizedTest
@ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"})
void UUIDAsRequestParameter(String uuidStringValue) throws Exception {
var value = UUID.fromString(uuidStringValue);

var resultAsString = performGetAndReturnResult(
String.format("/test/type/UUID/as-request-parameter?value=%s", value)
);

var actual = objectMapper.readValue(resultAsString, UUID.class);

assertThat(actual).isEqualTo(value);
}

@ParameterizedTest
@ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"})
void UUIDAsBody(String uuidStringValue) throws Exception {
var value = new BodyWithUUID(
UUID.fromString(uuidStringValue)
);

var resultAsString = performPostAndReturnResult(
"/test/type/UUID/as-body",
value
);

var actual = objectMapper.readValue(resultAsString, BodyWithUUID.class);

assertThat(actual).isEqualTo(value);
}
}

private @NonNull String performGetAndReturnResult(@NonNull 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
@@ -0,0 +1,8 @@
package it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body;

import java.util.UUID;

public record BodyWithUUID(
UUID uuid
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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.autoconfiguration.mvc.body.BodyWithUUID;
import it.aboutbits.springboot.toolbox.type.EmailAddress;
import it.aboutbits.springboot.toolbox.type.Iban;
import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal;
Expand All @@ -14,6 +15,8 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RestController
@RequestMapping("/test/type")
public class CustomTypeTestController {
Expand Down Expand Up @@ -61,4 +64,19 @@ public ScaledBigDecimal scaledBigDecimalAsRequestParameter(@RequestParam ScaledB
public BodyWithScaledBigDecimal scaledBigDecimalAsBody(@RequestBody BodyWithScaledBigDecimal value) {
return value;
}

@GetMapping("/UUID/as-path-variable/{value}")
public UUID uuidAsPathVariable(@PathVariable UUID value) {
return value;
}

@GetMapping("/UUID/as-request-parameter")
public UUID uuidAsRequestParameter(@RequestParam UUID value) {
return value;
}

@PostMapping("/UUID/as-body")
public BodyWithUUID uuidAsBody(@RequestBody BodyWithUUID value) {
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;

@ApplicationTest
Expand Down Expand Up @@ -73,4 +75,39 @@ void inAndOut_shouldSucceed(double doubleValue) {
.isEqualTo(savedItem);
}
}

@Nested
class UUIDType {
@ParameterizedTest
@ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"})
void inAndOut_shouldSucceed(String uuidStringValue) {
var item = new CustomTypeTestModel();
item.setUuid(UUID.fromString(uuidStringValue));

var savedItem = repository.save(item);

var retrievedItem = repository.findByUuid(savedItem.getUuid());

assertThat(retrievedItem).isPresent()
.get()
.usingRecursiveComparison()
.isEqualTo(savedItem);
}

@ParameterizedTest
@ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"})
void inAndOutString_shouldSucceed(String uuidStringValue) {
var item = new CustomTypeTestModel();
item.setUuidAsString(UUID.fromString(uuidStringValue));

var savedItem = repository.save(item);

var retrievedItem = repository.findByUuidAsString(savedItem.getUuidAsString());

assertThat(retrievedItem).isPresent()
.get()
.usingRecursiveComparison()
.isEqualTo(savedItem);
}
}
}
Loading