Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AttributeConverter generic types are not determined correctly when converter inheritance is used #2060

Open
grimsa opened this issue Feb 8, 2024 · 0 comments

Comments

@grimsa
Copy link

grimsa commented Feb 8, 2024

Problem

Converting between types fails when an AttributeConverter class has a generic type parameter, but implements AttributeConverter<..., ...> is specified on a superclass.

For example, a converter static class PowerControlSystemJpaConverter extends JsonSerializableStringConverter<PowerControlSystem>, where base is abstract class JsonSerializableStringConverter<T> implements AttributeConverter<T, String>.

Some debugging seems to suggest that the problem is in org.eclipse.persistence.internal.jpa.metadata.accessors.classes.ConverterAccessor#initClassificationClasses where in such case genericTypes are resolved as:

genericTypes = {ArrayList@7678}  size = 2
 0 = "com.company.infrastructure.persistence.jpa.JsonSerializableStringConverter"
 1 = "com.company.core.domain.PowerControlSystem"

while if we do not use a base class (i.e., flatten the hierarchy), it looks like this:

genericTypes = {ArrayList@7747}  size = 4
 0 = "java.lang.Object"
 1 = "jakarta.persistence.AttributeConverter"
 2 = "com.company.core.domain.PowerControlSystem"
 3 = "java.lang.String"

Because of this fieldClassification is determined incorrectly (as it is assumed to be the last generic type).

Context and partial example

We have many different types of objects that know how to serialize themselves to and from Jackson's JsonNode, and we wanted to create a generic attribute converter that would handle the generic JsonNode <-> String conversion, yet would allow pluggable Object <--> JsonNode conversion.

The generic converter looked like this:

package com.company.infrastructure.persistence.jpa;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.persistence.AttributeConverter;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.function.Function;

public abstract class JsonSerializableStringConverter<T> implements AttributeConverter<T, String> {
    private static final JsonMapper mapper = JsonMapper.builder().build();
    private final Function<T, JsonNode> serializationFunction;
    private final Function<JsonNode, T> deserializationFunction;

    protected JsonSerializableStringConverter(Function<T, JsonNode> serializationFunction, Function<JsonNode, T> deserializationFunction) {
        this.serializationFunction = serializationFunction;
        this.deserializationFunction = deserializationFunction;
    }

    @Override
    public final String convertToDatabaseColumn(T attribute) {
        JsonNode json = serializationFunction.apply(attribute);
        try {
            return mapper.writeValueAsString(json);
        } catch (JsonProcessingException e) {
            throw new UncheckedIOException("Can't convert JsonNode to String. Reason: " + e.getMessage(), e);
        }
    }

    @Override
    public final T convertToEntityAttribute(String dbData) {
        try {
            JsonNode json = mapper.readTree(dbData);
            return deserializationFunction.apply(json);
        } catch (IOException e) {
            throw new UncheckedIOException("Failed to convert String to JsonNode. Reason: " + e.getMessage(), e);
        }
    }
}

and then the converter for specific type of objects can be as simple as:

@Converter
public static class PowerControlSystemJpaConverter extends JsonSerializableStringConverter<PowerControlSystem> {
    public JpaConverter() {
        super(
             object -> object.asJsonNode(),                     // serialization function
             json -> new PowerControlSystem(json)        // deserialization function
        );
    }
}

Converter definition then looks like this then:

@Convert(converter = PowerControlSystemJpaConverter.class)
@Column(columnDefinition = "TEXT")
@Mutable
private PowerControlSystem powerControlSystem;

The exception we get is e.g.:

org.eclipse.persistence.exceptions.ConversionException: 
Exception Description: The object [{"definitionId":"b1cc196a-e01f-4063-8110-396fbe5d1202"}], of class [class java.lang.String], from mapping [org.eclipse.persistence.mappings.DirectToFieldMapping[powerControlSystem-->pvsystem.POWERCONTROLSYSTEM]] with descriptor [RelationalDescriptor(com.company.core.domain.AbstractPhotovoltaicGridTieSystem --> [DatabaseTable(pvsystem)])], could not be converted to [class com.company.core.domain.PowerControlSystem].
	at org.eclipse.persistence.exceptions.ConversionException.couldNotBeConverted(ConversionException.java:81) ~[org.eclipse.persistence.core-4.0.2.jar:na]
	at org.eclipse.persistence.internal.helper.ConversionManager.convertObject(ConversionManager.java:268) ~[org.eclipse.persistence.core-4.0.2.jar:na]
	at org.eclipse.persistence.internal.databaseaccess.DatasourcePlatform.convertObject(DatasourcePlatform.java:251) ~[org.eclipse.persistence.core-4.0.2.jar:na]
	at org.eclipse.persistence.mappings.foundation.AbstractDirectMapping.getFieldValue(AbstractDirectMapping.java:801) ~[org.eclipse.persistence.core-4.0.2.jar:na]
	at org.eclipse.persistence.mappings.foundation.AbstractDirectMapping.buildCloneValue(AbstractDirectMapping.java:261) ~[org.eclipse.persistence.core-4.0.2.jar:na]
	at org.eclipse.persistence.mappings.foundation.AbstractDirectMapping.mergeIntoObject(AbstractDirectMapping.java:1061) ~[org.eclipse.persistence.core-4.0.2.jar:na]
	at org.eclipse.persistence.internal.descriptors.ObjectBuilder.mergeIntoObject(ObjectBuilder.java:4281) ~[org.eclipse.persistence.core-4.0.2.jar:na]
	at org.eclipse.persistence.internal.sessions.MergeManager.mergeChangesOfCloneIntoWorkingCopy(MergeManager.java:612) ~[org.eclipse.persistence.core-4.0.2.jar:na]
	at org.eclipse.persistence.internal.sessions.MergeManager.mergeChanges(MergeManager.java:324) ~[org.eclipse.persistence.core-4.0.2.jar:na]
       ...
  • EclipseLink version: 4.0.2
  • Java/JDK version: 21

Expected behavior

I'd hope the AttributeConverter would work regardless of whether implements AttributeConverter<T1, T2> is specified on the class itself, or on a superclass.

Workaround

Specify implements AttributeConverter again on the child class:

@Converter
public static class PowerControlSystemJpaConverter extends JsonSerializableStringConverter<PowerControlSystem>
   implements AttributeConverter<PowerControlSystem, String> // <-- add this
{
    public JpaConverter() {
        super(
             object -> object.asJsonNode(),                     // serialization function
             json -> new PowerControlSystem(json)        // deserialization function
        );
    }
}

then genericTypes are resolved correctly:

genericTypes = {ArrayList@7749}  size = 5
 0 = "com.company.infrastructure.persistence.jpa.JsonSerializableStringConverter"
 1 = "com.company.core.domain.PowerControlSystem"
 2 = "jakarta.persistence.AttributeConverter"
 3 = "com.company.core.domain.PowerControlSystem"
 4 = "java.lang.String"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant