Implement AttributeConverter for Generic Class with @Converter
Solution 1:
I used the following solution with Eclipselink and Java 8. One issue that wasn't immediately apparent is that the converter class must implement AttributeConverter directly (at least for it to work with Eclipselink)
Step 1. Define a generic interface to implement Object <-> String Json conversion. Because interfaces cannot contain properties I defined two methods getInstance()
and getObjectMapper()
to provide the conversion logic access to object instances that it requires at run time. Converter classes will need to provide implementations for these methods.
package au.com.sfamc.fusion.commons.jpa;
import java.io.IOException;
import javax.persistence.AttributeConverter;
import org.apache.commons.lang3.StringUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public interface GenericJsonAttributeConverter<X> extends AttributeConverter<X, String> {
X getInstance();
ObjectMapper getObjectMapper();
@Override
default String convertToDatabaseColumn(X attribute) {
String jsonString = "";
try {
// conversion of POJO to json
if(attribute != null) {
jsonString = getObjectMapper().writeValueAsString(attribute);
} else {
jsonString = "{}"; // empty object to protect against NullPointerExceptions
}
} catch (JsonProcessingException ex) {
}
return jsonString;
}
@Override
default X convertToEntityAttribute(String dbData) {
X attribute = null;
try {
if(StringUtils.isNoneBlank(dbData)) {
attribute = getObjectMapper().readValue(dbData, (Class<X>)getInstance().getClass());
} else {
attribute = getObjectMapper().readValue("{}", (Class<X>)getInstance().getClass());
}
} catch (IOException ex) {
}
return attribute;
}
}
Step 2. The getObjectMapper()
method implementation would be repeated every converter class so I introduced an abstract class that extends GenericJsonAttributeConverter
to save having to implement this method in every converter class.
package au.com.sfamc.fusion.commons.jpa;
import com.fasterxml.jackson.databind.ObjectMapper;
public abstract class AbstractGenericJsonAttributeConverter<X> implements GenericJsonAttributeConverter<X> {
private static final ObjectMapper objectmapper = new ObjectMapper();
@Override
public ObjectMapper getObjectMapper() {
return AbstractGenericJsonAttributeConverter.objectmapper;
}
}
Step 3. Create a concrete implementation of AbstractGenericJsonAttributeConverter
for each class you want to to convert to and from Json, even though each class you want to convert will need its own concrete converter class at least you aren't duplicating the conversion code...
package au.com.sfamc.fusion.main.client;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import au.com.sfamc.fusion.commons.jpa.AbstractGenericJsonAttributeConverter;
@Converter
public class ProjectMetricsReportJsonConverter extends AbstractGenericJsonAttributeConverter<ProjectMetricsReport> implements AttributeConverter<ProjectMetricsReport, String> {
private static final ProjectMetricsReport projectMetricsReport = new ProjectMetricsReport();
@Override
public ProjectMetricsReport getInstance() {
return ProjectMetricsReportJsonConverter.projectMetricsReport;
}
}
Note: Now the trick to get this to work with Eclipselink is subtle but required. Along with extending AbstractGenericJsonAttributeConverter
the concrete implementation must also make a direct implements
reference to the `AttributeConverter' interface (a small price to pay to get generic conversion working)