Skip to content

Commit 7b532ea

Browse files
authored
Merge pull request #23 from aboutbits/support-uuid
Add support for the UUID type
2 parents c654edb + df0cb25 commit 7b532ea

22 files changed

Lines changed: 554 additions & 10 deletions

src/main/java/it/aboutbits/springboot/toolbox/jackson/CustomTypeDeserializer.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.lang.reflect.InvocationTargetException;
1313
import java.math.BigDecimal;
1414
import java.math.BigInteger;
15+
import java.util.UUID;
1516
import java.util.function.Function;
1617

1718
public class CustomTypeDeserializer<T extends CustomType<?>> extends JsonDeserializer<T> {
@@ -92,6 +93,9 @@ private static Function<JsonParser, Object> getTypeConverter(Class<?> wrappedTyp
9293
if (ScaledBigDecimal.class.isAssignableFrom(wrappedType)) {
9394
return getScaledBigDecimalConverter();
9495
}
96+
if (UUID.class.isAssignableFrom(wrappedType)) {
97+
return getUUIDConverter();
98+
}
9599
throw new CustomTypeDeserializerException("Value type not supported: " + wrappedType.getName());
96100
}
97101

@@ -219,6 +223,20 @@ private static Function<JsonParser, Object> getCharConverter() {
219223
};
220224
}
221225

226+
private static Function<JsonParser, Object> getUUIDConverter() {
227+
return jsonParser -> {
228+
try {
229+
var value = jsonParser.getValueAsString();
230+
if (value == null || value.length() != 36) {
231+
throw new IOException();
232+
}
233+
return UUID.fromString(value);
234+
} catch (IOException e) {
235+
throw new CustomTypeDeserializerException("Failed to read value as UUID.", e);
236+
}
237+
};
238+
}
239+
222240
public static final class CustomTypeDeserializerException extends RuntimeException {
223241
public CustomTypeDeserializerException(String message) {
224242
super(message);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package it.aboutbits.springboot.toolbox.persistence.converter;
2+
3+
import jakarta.persistence.AttributeConverter;
4+
import jakarta.persistence.Converter;
5+
6+
import java.util.UUID;
7+
8+
@Converter
9+
public class UUIDConverter implements AttributeConverter<UUID, String> {
10+
@Override
11+
public String convertToDatabaseColumn(UUID attribute) {
12+
if (attribute == null) {
13+
return null;
14+
}
15+
16+
return attribute.toString();
17+
}
18+
19+
@Override
20+
public UUID convertToEntityAttribute(String dbData) {
21+
if (dbData == null || dbData.isBlank()) {
22+
return null;
23+
}
24+
25+
return UUID.fromString(dbData);
26+
}
27+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package it.aboutbits.springboot.toolbox.persistence.javatype.base;
2+
3+
import it.aboutbits.springboot.toolbox.type.CustomType;
4+
import lombok.SneakyThrows;
5+
import org.hibernate.type.descriptor.WrapperOptions;
6+
import org.hibernate.type.descriptor.java.AbstractClassJavaType;
7+
import org.hibernate.type.descriptor.jdbc.JdbcType;
8+
import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
9+
10+
import java.lang.reflect.Constructor;
11+
import java.lang.reflect.InvocationTargetException;
12+
import java.nio.charset.StandardCharsets;
13+
import java.sql.Types;
14+
import java.util.UUID;
15+
16+
public abstract class WrappedUUIDJavaType<T extends CustomType<UUID>> extends AbstractClassJavaType<T> {
17+
private final transient Constructor<T> constructor;
18+
19+
protected WrappedUUIDJavaType(Class<T> type) {
20+
super(type);
21+
22+
try {
23+
this.constructor = type.getConstructor(UUID.class);
24+
} catch (NoSuchMethodException e) {
25+
throw new IllegalStateException("No method found for " + type.getName(), e);
26+
}
27+
}
28+
29+
@Override
30+
public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) {
31+
return indicators.getTypeConfiguration()
32+
.getJdbcTypeRegistry()
33+
.getDescriptor(Types.OTHER);
34+
}
35+
36+
@SuppressWarnings("unchecked")
37+
@Override
38+
public <X> X unwrap(T id, Class<X> aClass, WrapperOptions wrapperOptions) {
39+
var javaTypeClass = getJavaTypeClass();
40+
41+
if (id == null) {
42+
return null;
43+
}
44+
if (javaTypeClass.isAssignableFrom(aClass)) {
45+
return (X) id;
46+
}
47+
if (UUID.class.isAssignableFrom(aClass)) {
48+
return (X) id.value();
49+
}
50+
if (String.class.isAssignableFrom(aClass)) {
51+
return (X) id.value().toString();
52+
}
53+
if (byte[].class.isAssignableFrom(aClass)) {
54+
return (X) id.value().toString().getBytes(StandardCharsets.UTF_8);
55+
}
56+
57+
throw unknownUnwrap(aClass);
58+
}
59+
60+
@SuppressWarnings("unchecked")
61+
@SneakyThrows({InstantiationException.class, IllegalAccessException.class, InvocationTargetException.class})
62+
@Override
63+
public <X> T wrap(X value, WrapperOptions wrapperOptions) {
64+
var clazz = getJavaTypeClass();
65+
66+
if (value == null) {
67+
return null;
68+
}
69+
if (clazz.isInstance(value)) {
70+
return (T) value;
71+
}
72+
73+
return switch (value) {
74+
case UUID uuidValue -> constructor.newInstance(uuidValue);
75+
case String uuidStringValue -> constructor.newInstance(UUID.fromString(uuidStringValue));
76+
case byte[] uuidBytesValue -> constructor.newInstance(UUID.fromString(new String(
77+
uuidBytesValue,
78+
StandardCharsets.UTF_8
79+
)));
80+
default -> throw unknownWrap(value.getClass());
81+
};
82+
}
83+
}

src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypeModelConverter.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.math.BigDecimal;
1515
import java.math.BigInteger;
1616
import java.util.Iterator;
17+
import java.util.UUID;
1718

1819
public class CustomTypeModelConverter implements ModelConverter {
1920

@@ -71,6 +72,9 @@ public Schema<?> resolve(
7172
if (Character.class.isAssignableFrom(wrappedType)) {
7273
result = context.resolve(new AnnotatedType(Character.TYPE));
7374
}
75+
if (UUID.class.isAssignableFrom(wrappedType)) {
76+
result = context.resolve(new AnnotatedType(UUID.class));
77+
}
7478

7579
if (result != null) {
7680
var isIdentity = EntityId.class.isAssignableFrom(clazz);

src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypePropertyCustomizer.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import java.math.BigDecimal;
1616
import java.math.BigInteger;
17+
import java.util.UUID;
1718

1819
@Slf4j
1920
public class CustomTypePropertyCustomizer implements PropertyCustomizer {
@@ -91,7 +92,7 @@ public Schema<?> customize(Schema property, AnnotatedType annotatedType) {
9192
}
9293
if (BigInteger.class.isAssignableFrom(wrappedType)) {
9394
property.type("integer");
94-
property.format("int64");
95+
property.format("");
9596
property.setDescription(description);
9697
property.setProperties(null);
9798
property.set$ref(null);
@@ -147,6 +148,16 @@ public Schema<?> customize(Schema property, AnnotatedType annotatedType) {
147148
property.set$ref(null);
148149
return property;
149150
}
151+
if (UUID.class.isAssignableFrom(wrappedType)) {
152+
property.type("string");
153+
property.format("uuid");
154+
property.minLength(36);
155+
property.maxLength(36);
156+
property.setDescription(description);
157+
property.setProperties(null);
158+
property.set$ref(null);
159+
return property;
160+
}
150161

151162
log.warn("Property {} of type WrappedValue: Can not resolve parameter type!", property.getName());
152163
}

src/main/java/it/aboutbits/springboot/toolbox/web/CustomTypePropertyEditor.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.lang.reflect.InvocationTargetException;
1313
import java.math.BigDecimal;
1414
import java.math.BigInteger;
15+
import java.util.UUID;
1516
import java.util.function.Function;
1617

1718
public final class CustomTypePropertyEditor<T extends CustomType<?>> extends PropertyEditorSupport {
@@ -100,6 +101,9 @@ private static Function<String, Object> getTextToTypeConverter(Class<?> wrappedT
100101
if (ScaledBigDecimal.class.isAssignableFrom(wrappedType)) {
101102
return ScaledBigDecimal::new;
102103
}
104+
if (UUID.class.isAssignableFrom(wrappedType)) {
105+
return UUID::fromString;
106+
}
103107
throw new IllegalArgumentException("Unable to convert text to type: " + wrappedType.getName());
104108
}
105109
}

src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/CustomTypeBindingsForControllerTest.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithEmailAddress;
55
import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithIban;
66
import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithScaledBigDecimal;
7+
import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithUUID;
78
import it.aboutbits.springboot.toolbox.support.HttpTest;
89
import it.aboutbits.springboot.toolbox.type.EmailAddress;
910
import it.aboutbits.springboot.toolbox.type.Iban;
@@ -18,6 +19,8 @@
1819
import org.springframework.test.web.servlet.MockMvc;
1920
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
2021

22+
import java.util.UUID;
23+
2124
import static org.assertj.core.api.Assertions.assertThat;
2225

2326
@HttpTest
@@ -166,6 +169,54 @@ void ScaledBigDecimalAsBody(double doubleValue) throws Exception {
166169
}
167170
}
168171

172+
@Nested
173+
class UUIDType {
174+
@ParameterizedTest
175+
@ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"})
176+
void UUIDAsPathVariable(String uuidStringValue) throws Exception {
177+
var value = UUID.fromString(uuidStringValue);
178+
179+
var resultAsString = performGetAndReturnResult(
180+
String.format("/test/type/UUID/as-path-variable/%s", value)
181+
);
182+
183+
var actual = objectMapper.readValue(resultAsString, UUID.class);
184+
185+
assertThat(actual).isEqualTo(value);
186+
}
187+
188+
@ParameterizedTest
189+
@ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"})
190+
void UUIDAsRequestParameter(String uuidStringValue) throws Exception {
191+
var value = UUID.fromString(uuidStringValue);
192+
193+
var resultAsString = performGetAndReturnResult(
194+
String.format("/test/type/UUID/as-request-parameter?value=%s", value)
195+
);
196+
197+
var actual = objectMapper.readValue(resultAsString, UUID.class);
198+
199+
assertThat(actual).isEqualTo(value);
200+
}
201+
202+
@ParameterizedTest
203+
@ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"})
204+
void UUIDAsBody(String uuidStringValue) throws Exception {
205+
var value = new BodyWithUUID(
206+
UUID.fromString(uuidStringValue)
207+
);
208+
209+
var resultAsString = performPostAndReturnResult(
210+
"/test/type/UUID/as-body",
211+
value
212+
);
213+
214+
var actual = objectMapper.readValue(resultAsString, BodyWithUUID.class);
215+
216+
assertThat(actual).isEqualTo(value);
217+
}
218+
}
219+
169220
private @NonNull String performGetAndReturnResult(@NonNull String url) throws Exception {
170221
var requestBuilder = MockMvcRequestBuilders.get(url)
171222
.contentType(MediaType.APPLICATION_JSON);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body;
2+
3+
import java.util.UUID;
4+
5+
public record BodyWithUUID(
6+
UUID uuid
7+
) {
8+
}

src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/mvc/controller/CustomTypeTestController.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithEmailAddress;
44
import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithIban;
55
import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithScaledBigDecimal;
6+
import it.aboutbits.springboot.toolbox.autoconfiguration.mvc.body.BodyWithUUID;
67
import it.aboutbits.springboot.toolbox.type.EmailAddress;
78
import it.aboutbits.springboot.toolbox.type.Iban;
89
import it.aboutbits.springboot.toolbox.type.ScaledBigDecimal;
@@ -14,6 +15,8 @@
1415
import org.springframework.web.bind.annotation.RequestParam;
1516
import org.springframework.web.bind.annotation.RestController;
1617

18+
import java.util.UUID;
19+
1720
@RestController
1821
@RequestMapping("/test/type")
1922
public class CustomTypeTestController {
@@ -61,4 +64,19 @@ public ScaledBigDecimal scaledBigDecimalAsRequestParameter(@RequestParam ScaledB
6164
public BodyWithScaledBigDecimal scaledBigDecimalAsBody(@RequestBody BodyWithScaledBigDecimal value) {
6265
return value;
6366
}
67+
68+
@GetMapping("/UUID/as-path-variable/{value}")
69+
public UUID uuidAsPathVariable(@PathVariable UUID value) {
70+
return value;
71+
}
72+
73+
@GetMapping("/UUID/as-request-parameter")
74+
public UUID uuidAsRequestParameter(@RequestParam UUID value) {
75+
return value;
76+
}
77+
78+
@PostMapping("/UUID/as-body")
79+
public BodyWithUUID uuidAsBody(@RequestBody BodyWithUUID value) {
80+
return value;
81+
}
6482
}

src/test/java/it/aboutbits/springboot/toolbox/autoconfiguration/persistence/CustomTypeJpaTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import org.junit.jupiter.params.provider.ValueSource;
1313
import org.springframework.beans.factory.annotation.Autowired;
1414

15+
import java.util.UUID;
16+
1517
import static org.assertj.core.api.Assertions.assertThat;
1618

1719
@ApplicationTest
@@ -73,4 +75,39 @@ void inAndOut_shouldSucceed(double doubleValue) {
7375
.isEqualTo(savedItem);
7476
}
7577
}
78+
79+
@Nested
80+
class UUIDType {
81+
@ParameterizedTest
82+
@ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"})
83+
void inAndOut_shouldSucceed(String uuidStringValue) {
84+
var item = new CustomTypeTestModel();
85+
item.setUuid(UUID.fromString(uuidStringValue));
86+
87+
var savedItem = repository.save(item);
88+
89+
var retrievedItem = repository.findByUuid(savedItem.getUuid());
90+
91+
assertThat(retrievedItem).isPresent()
92+
.get()
93+
.usingRecursiveComparison()
94+
.isEqualTo(savedItem);
95+
}
96+
97+
@ParameterizedTest
98+
@ValueSource(strings = {"d414ed05-c370-445a-8430-1fd1021c9856", "0682a03d-3618-470f-a8f9-78e4a52f1a2c"})
99+
void inAndOutString_shouldSucceed(String uuidStringValue) {
100+
var item = new CustomTypeTestModel();
101+
item.setUuidAsString(UUID.fromString(uuidStringValue));
102+
103+
var savedItem = repository.save(item);
104+
105+
var retrievedItem = repository.findByUuidAsString(savedItem.getUuidAsString());
106+
107+
assertThat(retrievedItem).isPresent()
108+
.get()
109+
.usingRecursiveComparison()
110+
.isEqualTo(savedItem);
111+
}
112+
}
76113
}

0 commit comments

Comments
 (0)