Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: '${{ github.event.inputs.version }}',
name: 'Release ${{ github.event.inputs.version }}',
tag_name: 'v${{ github.event.inputs.version }}',
name: 'v${{ github.event.inputs.version }}',
prerelease: '${{ github.event.inputs.version }}'.includes('RC')
})
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ private static Function<JsonParser, Object> getTypeConverter(Class<?> wrappedTyp
if (UUID.class.isAssignableFrom(wrappedType)) {
return getUUIDConverter();
}
if (Enum.class.isAssignableFrom(wrappedType) || wrappedType.isEnum()) {
return getEnumConverter(wrappedType);
}
throw new CustomTypeDeserializerException("Value type not supported: " + wrappedType.getName());
}

Expand Down Expand Up @@ -237,6 +240,27 @@ private static Function<JsonParser, Object> getUUIDConverter() {
};
}

@SuppressWarnings("unchecked")
private static Function<JsonParser, Object> getEnumConverter(Class<?> wrappedType) {
var enumClass = (Class<? extends Enum<?>>) wrappedType.asSubclass(Enum.class);
return jsonParser -> {
try {
String value = jsonParser.getValueAsString();
if (value == null) {
return null;
}
return Enum.valueOf((Class<? extends Enum>) enumClass, value);
} catch (IllegalArgumentException e) {
throw new CustomTypeDeserializerException(
"Failed to read value as Enum: " + enumClass.getName(),
e
);
} catch (IOException e) {
throw new CustomTypeDeserializerException("Failed to read value as Enum.", 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
Expand Up @@ -26,6 +26,7 @@ public void serialize(
switch (value) {
case String stringValue -> jsonGenerator.writeString(stringValue);
case UUID uuidValue -> jsonGenerator.writeString(uuidValue.toString());
case Enum<?> enumValue -> jsonGenerator.writeString(enumValue.name());
case null -> jsonGenerator.writeNull();
default -> jsonGenerator.writeRawValue(String.valueOf(value));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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.sql.Types;

public abstract class WrappedEnumJavaType<T extends CustomType<? extends Enum<?>>> extends AbstractClassJavaType<T> {
private final transient Constructor<T> constructor;
private final Class<? extends Enum<?>> enumClass;

@SuppressWarnings("unchecked")
protected WrappedEnumJavaType(Class<T> type) {
super(type);

try {
this.enumClass = determineEnumClass(type);
this.constructor = type.getConstructor(enumClass);
} catch (NoSuchMethodException e) {
throw new IllegalStateException("No constructor found for " + type.getName() + " with enum parameter", e);
}
}

@SuppressWarnings("unchecked")
private Class<? extends Enum<?>> determineEnumClass(Class<T> type) {
// Try to extract from the generic superclass (if the CustomType exposes the enum via inheritance)
var genericSuperclass = type.getGenericSuperclass();
if (genericSuperclass instanceof java.lang.reflect.ParameterizedType parameterizedType) {
var actualTypeArguments = parameterizedType.getActualTypeArguments();
if (actualTypeArguments.length > 0 && actualTypeArguments[0] instanceof Class<?> enumType) {
if (enumType.isEnum()) {
return (Class<? extends Enum<?>>) enumType;
}
}
}
// Fallback: inspect constructors and use the single-arg enum constructor (works for records and classes)
for (var ctor : type.getDeclaredConstructors()) {
var params = ctor.getParameterTypes();
if (params.length == 1 && params[0].isEnum()) {
return (Class<? extends Enum<?>>) params[0];
}
}
throw new IllegalStateException("Could not determine enum type for " + type.getName());
}

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

@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 (String.class.isAssignableFrom(aClass)) {
return (X) id.value().name();
}
if (enumClass.isAssignableFrom(aClass)) {
return (X) id.value();
}

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 String stringValue) {
var enumValue = Enum.valueOf((Class<? extends Enum>) enumClass, stringValue);
return constructor.newInstance(enumValue);
}
if (enumClass.isInstance(value)) {
return constructor.newInstance(value);
}

throw unknownWrap(value.getClass());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,25 @@ private static Function<String, Object> getTextToTypeConverter(Class<?> wrappedT
if (UUID.class.isAssignableFrom(wrappedType)) {
return UUID::fromString;
}
if (Enum.class.isAssignableFrom(wrappedType)) {
return toEnumConverter(wrappedType);
}
throw new IllegalArgumentException("Unable to convert text to type: " + wrappedType.getName());
}

@SuppressWarnings("unchecked")
private static Function<String, Object> toEnumConverter(Class<?> wrappedType) {
var enumClass = (Class<? extends Enum<?>>) wrappedType.asSubclass(Enum.class);

return text -> {
if (text == null) {
return null;
}
try {
return Enum.valueOf((Class<? extends Enum>) enumClass, text);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Unable to convert text to enum: " + enumClass.getName(), e);
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import it.aboutbits.springboot.toolbox._support.ApplicationTest;
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.SampleEnum;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapBigDecimalClass;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapBigDecimalRecord;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapBigIntegerClass;
Expand All @@ -15,6 +16,8 @@
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapCharacterRecord;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapDoubleClass;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapDoubleRecord;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapEnumClass;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapEnumRecord;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapFloatClass;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapFloatRecord;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapIntegerClass;
Expand All @@ -33,6 +36,7 @@
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;

Expand Down Expand Up @@ -521,6 +525,40 @@ void givenStringValues_inAndOut_shouldSucceed(String uuidStringValue) {
.isEqualTo(savedItem);
}
}

@Nested
class WrapEnumRecordType {
@Test
void givenNull_inAndOut_shouldSucceed() {
var item = new WrapperTypesModel();
item.setEnumValue(null);

var savedItem = repository.save(item);

var retrievedItem = repository.findByEnumValue(null);

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

@ParameterizedTest
@EnumSource(SampleEnum.class)
void givenValues_inAndOut_shouldSucceed(SampleEnum enumValue) {
var item = new WrapperTypesModel();
item.setEnumValue(new WrapEnumRecord(enumValue));

var savedItem = repository.save(item);

var retrievedItem = repository.findByEnumValue(savedItem.getEnumValue());

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

@Nested
Expand Down Expand Up @@ -997,5 +1035,39 @@ void givenStringValues_inAndOut_shouldSucceed(String uuidStringValue) {
.isEqualTo(savedItem);
}
}

@Nested
class WrapEnumClassType {
@Test
void givenNull_inAndOut_shouldSucceed() {
var item = new WrapperTypesModel();
item.setEnumValueClass(null);

var savedItem = repository.save(item);

var retrievedItem = repository.findByEnumValueClass(null);

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

@ParameterizedTest
@EnumSource(SampleEnum.class)
void givenValues_inAndOut_shouldSucceed(SampleEnum enumValue) {
var item = new WrapperTypesModel();
item.setEnumValueClass(new WrapEnumClass(enumValue));

var savedItem = repository.save(item);

var retrievedItem = repository.findByEnumValueClass(savedItem.getEnumValueClass());

assertThat(retrievedItem).isPresent()
.get()
.usingRecursiveComparison()
.isEqualTo(savedItem);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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.WrappedEnumJavaType;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapEnumClass;

public final class WrapEnumClassJavaType extends WrappedEnumJavaType<WrapEnumClass> implements AutoRegisteredJavaType<WrapEnumClass> {
public WrapEnumClassJavaType() {
super(WrapEnumClass.class);
}
}
Original file line number Diff line number Diff line change
@@ -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.WrappedEnumJavaType;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapEnumRecord;

public final class WrapEnumRecordJavaType extends WrappedEnumJavaType<WrapEnumRecord> implements AutoRegisteredJavaType<WrapEnumRecord> {
public WrapEnumRecordJavaType() {
super(WrapEnumRecord.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapCharacterRecordJavaType;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapDoubleClassJavaType;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapDoubleRecordJavaType;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapEnumClassJavaType;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapEnumRecordJavaType;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapFloatClassJavaType;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapFloatRecordJavaType;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.javatype.WrapIntegerClassJavaType;
Expand All @@ -40,6 +42,8 @@
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapCharacterRecord;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapDoubleClass;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapDoubleRecord;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapEnumClass;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapEnumRecord;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapFloatClass;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapFloatRecord;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapIntegerClass;
Expand Down Expand Up @@ -136,6 +140,10 @@ public class WrapperTypesModel implements Identified<WrapperTypesModel.ID> {
@JdbcType(CharJdbcType.class)
private WrapUUIDRecord uuidValueAsString;

@SuppressWarnings("JpaAttributeTypeInspection")
@JavaType(WrapEnumRecordJavaType.class)
private WrapEnumRecord enumValue;

@SuppressWarnings("JpaAttributeTypeInspection")
@JavaType(WrapBigDecimalClassJavaType.class)
private WrapBigDecimalClass bigDecimalValueClass;
Expand Down Expand Up @@ -194,6 +202,10 @@ public class WrapperTypesModel implements Identified<WrapperTypesModel.ID> {
@JdbcType(CharJdbcType.class)
private WrapUUIDClass uuidValueClassAsString;

@SuppressWarnings("JpaAttributeTypeInspection")
@JavaType(WrapEnumClassJavaType.class)
private WrapEnumClass enumValueClass;

public record ID(
Long value
) implements EntityId<Long> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapCharacterRecord;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapDoubleClass;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapDoubleRecord;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapEnumClass;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapEnumRecord;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapFloatClass;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapFloatRecord;
import it.aboutbits.springboot.toolbox.persistence.javatype.impl.type.WrapIntegerClass;
Expand Down Expand Up @@ -86,4 +88,8 @@ public interface WrapperTypesModelRepository extends JpaRepository<WrapperTypesM
Optional<WrapperTypesModel> findByUuidValueClass(WrapUUIDClass value);

Optional<WrapperTypesModel> findByUuidValueClassAsString(WrapUUIDClass value);

Optional<WrapperTypesModel> findByEnumValue(WrapEnumRecord value);

Optional<WrapperTypesModel> findByEnumValueClass(WrapEnumClass value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package it.aboutbits.springboot.toolbox.persistence.javatype.impl.type;

public enum SampleEnum {
A, B, C
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package it.aboutbits.springboot.toolbox.persistence.javatype.impl.type;

import it.aboutbits.springboot.toolbox.type.CustomType;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;

@Getter
@Accessors(fluent = true)
@EqualsAndHashCode
@RequiredArgsConstructor
public class WrapEnumClass implements CustomType<SampleEnum> {
private final SampleEnum value;
}
Loading