I tried to use a custom datatype implemented in an module/add-on (base) in another module/add-on (api) which provides a non-JPA read-only store consuming data from a REST API. This store defines DTO entities and one of these entities should have an entity property of the custom datatype provided by the base add-on.
Everything is compiling fine but the test loading the application context in the API module/add-on fails because the custom datatype cannot be loaded from the datatype registry. Is that because it’s in another add-on module? Do I have to publish or register them in some way? Do I follow some package conventions?
The test failure reads:
org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.jmix.core.impl.MetadataLoader]: Constructor threw exception; nested exception is java.lang.IllegalArgumentException:
…
java.lang.IllegalArgumentException: Datatype ‘instant’ is not found
at io.jmix.core.impl.DatatypeRegistryImpl.get(DatatypeRegistryImpl.java:64)
at io.jmix.core.impl.MetaModelLoader.getAdaptiveDatatype(MetaModelLoader.java:818)
at io.jmix.core.impl.MetaModelLoader.loadProperty(MetaModelLoader.java:378)
at io.jmix.core.impl.MetaModelLoader.initProperties(MetaModelLoader.java:282)
at io.jmix.core.impl.MetaModelLoader.loadClass(MetaModelLoader.java:173)
at io.jmix.core.impl.MetaModelLoader.loadModel(MetaModelLoader.java:135)
at io.jmix.core.impl.MetadataLoader.(MetadataLoader.java:61)
…
The DTO entity which uses the custom datatype within the api add-on:
@Store("transientapistore")
@JmixEntity("api_Announcement")
public class Announcement
{
@PropertyDatatype(InstantDatatype.ID)
private Instant time;
private String message;
public Instant getTime()
{
return time;
}
public void setTime(final Instant someTime)
{
time = someTime;
}
// ...
}
The attribute converter (if I may use the custom datatype within a JPA store as well) within the base add-on:
@Converter(autoApply = true)
public class InstantConverter implements AttributeConverter<Instant, Long>
{
@Override
public Long convertToDatabaseColumn(final Instant anAttribute)
{
return anAttribute == null ? null : anAttribute.toEpochMilli();
}
@Override
public Instant convertToEntityAttribute(final Long someDBData)
{
return someDBData == null ? null : Instant.ofEpochMilli(someDBData.longValue());
}
}
The custom datatype implementation within the base add-on:
@DatatypeDef(id = InstantDatatype.ID, javaClass = Instant.class, defaultForClass = true, value = InstantDatatype.VALUE)
@Ddl("TIMESTAMP")
public class InstantDatatype extends AbstractTemporalDatatype<Instant> implements TimeZoneAwareDatatype
{
/**
* Defines the datatype definition identifier.
*/
public static final String ID = "instant";
/**
* Defines the datatype definition value.
*/
public static final String VALUE = IBaseModule.PROJECT_PREFIX + "InstantDatatype";
/**
* Public default constructor.
*/
public InstantDatatype()
{
super(DateTimeFormatter.ISO_INSTANT);
}
@Override
public String format(@Nullable final Object aValue, final Locale aLocale, @Nullable final TimeZone aTimeZone)
{
final String result;
if (aValue instanceof Instant)
{
if (aTimeZone == null)
{
result = format(aValue, aLocale);
}
else
{
final LocalDateTime localDateTime = ((Instant) aValue).atZone(aTimeZone.toZoneId()).toLocalDateTime();
result = format(localDateTime, aLocale);
}
}
else
{
result = null;
}
return result;
}
@Override
protected DateTimeFormatter getDateTimeFormatter()
{
return DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).withZone(ZoneId.systemDefault());
}
@Override
protected DateTimeFormatter getDateTimeFormatter(final FormatStrings someFormatStrings, final Locale aLocale)
{
return DateTimeFormatter.ofPattern(someFormatStrings.getDateTimeFormat(), aLocale)
.withZone(ZoneId.systemDefault());
}
@Override
protected TemporalQuery<Instant> newInstance()
{
return Instant::from;
}
}