Интерфейс Datatype
Каждый атрибут сущности, не являющийся ссылкой, связан с реализацией интерфейса Datatype
. Этот интерфейс определяет методы конвертации значений атрибутов в строки и из строк (форматирование и парсинг) при отображении сущностей в пользовательском интерфейсе и сериализации в Универсальный REST.
Фреймворк предоставляет набор реализаций Datatype
, соответствующих стандартным типам данных атрибутов сущностей.
В данном руководстве термин datatype, написанный строчными буквами, используется для обозначения реализаций интерфейса Datatype .
|
Локализованные строки форматов
Многие стандартные datatype используют набор строк форматов, определяемых в пакете сообщений. Это дает возможность форматирования и парсинга, зависящих от текущего языка пользователя. Набор строк форматов по умолчанию, определяемый фреймворком, выглядит следующим образом:
# Date/time formats
dateFormat = dd/MM/yyyy
dateTimeFormat = dd/MM/yyyy HH:mm
offsetDateTimeFormat = dd/MM/yyyy HH:mm Z
timeFormat = HH:mm
offsetTimeFormat = HH:mm Z
# Number formats
integerFormat = #,##0
doubleFormat = #,##0.###
decimalFormat = #,##0.##
# Number separators
numberDecimalSeparator = .
numberGroupingSeparator = ,
# Booleans
trueString = True
falseString = False
Чтобы использовать собственные строки формата, добавьте соответствующие сообщения в пакет сообщений вашего приложения. Например, чтобы использовать формат даты Соединенных Штатов с английской локалью, добавьте следующие строки в файл messages_en.properties
:
dateFormat = MM/dd/yyyy
dateTimeFormat = MM/dd/yyyy HH:mm
offsetDateTimeFormat = MM/dd/yyyy HH:mm Z
Кроме того, вы можете определить отдельную локаль en_US
и задать строки формата данных в файле messages_en_us.properties
.
Вы можете настроить строки форматов данных с помощью Studio: откройте вкладку Locales в окне Project Properties и поставьте флажок Show data format strings. |
Специализированные форматирование и парсинг
Вы можете настроить форматирование и парсинг значений для определенных атрибутов сущности, создав свой собственный datatype и назначив его этим атрибутам.
В качестве примера представим, что в вашем приложении есть атрибуты сущностей, хранящие годы в виде целых чисел. Пользователи должны иметь возможность просматривать и редактировать годы, причем если пользователь вводит только две цифры, приложение должно преобразовать их в год между 2000 и 2100. В противном случае все введенное число считается годом.
Во-первых, создайте класс реализации Datatype
и аннотируйте его @DatatypeDef
:
import com.google.common.base.Strings;
import io.jmix.core.metamodel.annotation.DatatypeDef;
import io.jmix.core.metamodel.annotation.Ddl;
import io.jmix.core.metamodel.datatype.Datatype;
import javax.annotation.Nullable;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.Locale;
@DatatypeDef(
id = "year", (1)
javaClass = Integer.class (2)
)
@Ddl("int")
public class YearDatatype implements Datatype<Integer> {
private static final String PATTERN = "##00";
@Override
public String format(@Nullable Object value) { (3)
if (value == null)
return "";
DecimalFormat format = new DecimalFormat(PATTERN);
return format.format(value);
}
@Override
public String format(@Nullable Object value, Locale locale) { (4)
return format(value);
}
@Nullable
@Override
public Integer parse(@Nullable String value) throws ParseException { (5)
if (Strings.isNullOrEmpty(value))
return null;
DecimalFormat format = new DecimalFormat(PATTERN);
int year = format.parse(value).intValue();
if (year > 2100 || year < 0)
throw new ParseException("Invalid year", 0);
if (year < 100)
year += 2000;
return year;
}
@Nullable
@Override
public Integer parse(@Nullable String value, Locale locale) throws ParseException { (6)
return parse(value);
}
}
1 | Уникальный идентификатор datatype. |
2 | Класс Java, обрабатываемый данным datatype. |
3 | Форматирование без учета локали текущего пользователя. Этот метод вызывается для преобразования на системном уровне. |
4 | Форматирование с учетом локали текущего пользователя. Этот метод вызывается в UI. |
5 | Парсинг без учета локали текущего пользователя. Этот метод вызывается для преобразования на системном уровне. |
6 | Парсинг с учетом локали текущего пользователя. Этот метод вызывается в UI. |
После создания реализации Datatype
вы можете указать ее для атрибута сущности, используя аннотацию @PropertyDatatype:
@PropertyDatatype("year")
@Column(name = "YEAR_")
private Integer productionYear;
Бины, например Messages, нельзя напрямую инжектировать в классы datatype используя Вместо этого, инжектируйте |
Поддержка произвольных классов Java
Вы можете использовать произвольный класс Java в качестве типа атрибутов сущности.
Предположим, что вы создали класс Java, представляющий географическую координату:
import java.io.Serializable;
import java.util.Objects;
public class GeoPoint implements Serializable {
public final double latitude;
public final double longitude;
public GeoPoint(double latitude, double longitude) {
this.latitude = latitude;
this.longitude = longitude;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GeoPoint that = (GeoPoint) o;
return Double.compare(that.latitude, latitude) == 0 &&
Double.compare(that.longitude, longitude) == 0;
}
@Override
public int hashCode() {
return Objects.hash(latitude, longitude);
}
}
Теперь вы хотите использовать этот класс в качестве типа атрибута сущности JPA.
Во-первых, создайте конвертер JPA для этого класса:
import datamodel.ex1.entity.GeoPoint;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
@Converter(autoApply = true) (1)
public class GeoPointConverter implements AttributeConverter<GeoPoint, String> {
@Override
public String convertToDatabaseColumn(GeoPoint attribute) {
if (attribute == null)
return null;
return attribute.latitude + "|" + attribute.longitude;
}
@Override
public GeoPoint convertToEntityAttribute(String dbData) {
if (dbData == null)
return null;
String[] strings = dbData.split("\\|");
return new GeoPoint(Double.parseDouble(strings[0]), Double.parseDouble(strings[1]));
}
}
1 | С autoApply = true вам не нужно указывать конвертер для каждого атрибута. Конвертер будет применен ко всем атрибутам соответствующего типа. |
Затем создайте класс реализации Datatype
для GeoPoint
и аннотируйте его @DatatypeDef
:
import io.jmix.core.metamodel.annotation.DatatypeDef;
import io.jmix.core.metamodel.annotation.Ddl;
import io.jmix.core.metamodel.datatype.Datatype;
import datamodel.ex1.entity.GeoPoint;
import javax.annotation.Nullable;
import java.text.ParseException;
import java.util.Locale;
@DatatypeDef(
id = "geoPoint", (1)
javaClass = GeoPoint.class, (2)
defaultForClass = true (3)
)
@Ddl("varchar(255)") (4)
public class GeoPointDatatype implements Datatype<GeoPoint> {
@Override
public String format(@Nullable Object value) { (5)
if (value instanceof GeoPoint) {
return ((GeoPoint) value).latitude + "|" + ((GeoPoint) value).longitude;
}
return null;
}
@Override
public String format(@Nullable Object value, Locale locale) { (6)
return format(value);
}
@Nullable
@Override
public GeoPoint parse(@Nullable String value) throws ParseException { (7)
if (value == null)
return null;
String[] strings = value.split("\\|");
try {
return new GeoPoint(Double.parseDouble(strings[0]), Double.parseDouble(strings[1]));
} catch (Exception e) {
throw new ParseException(String.format("Cannot parse %s as GeoPoint: %s", value, e.toString()), 0);
}
}
@Nullable
@Override
public GeoPoint parse(@Nullable String value, Locale locale) throws ParseException { (8)
return parse(value);
}
}
1 | Уникальный идентификатор datatype. |
2 | Класс Java, обрабатываемый данным datatype. |
3 | defaultForClass = true означает, что datatype будет автоматически применен ко всем атрибутам сущности типа GeoPoint . |
4 | Используя аннотацию @Ddl , вы можете указать, какой тип SQL следует использовать для атрибутов сущности. Studio учитывает эту аннотацию при создании скриптов миграции базы данных. |
5 | Форматирование без учета локали текущего пользователя. Этот метод вызывается для преобразования на системном уровне. |
6 | Форматирование с учетом локали текущего пользователя. Этот метод вызывается в UI. |
7 | Парсинг без учета локали текущего пользователя. Этот метод вызывается для преобразования на системном уровне. |
8 | Парсинг с учетом локали текущего пользователя. Этот метод вызывается в UI. |
После этого, когда вы определите атрибут сущности типа GeoPoint
, фреймворк будет использовать созданные вами конвертер JPA и datatype:
@Column(name = "GEO_POINT")
private GeoPoint geoPoint;
Сообщения об ошибках преобразования
Когда datatype используется компонентом UI для разбора строкового ввода, это может привести к исключениям парсинга. Компонент UI обрабатывает исключение и отображает удобное для пользователя сообщение. Эти сообщения находятся в пакете сообщений фреймворка с ключами databinding.conversion.error.<datatype-id>
. Например:
databinding.conversion.error.boolean=Must be Boolean
Полный список сообщений см. в разделе messages.properties ветки, соответствующей версии Jmix, используемой в вашем проекте.
Если сообщение для datatype не существует, используется следующее общее сообщение:
databinding.conversion.error.defaultMessage=Wrong format
Вы можете переопределить сообщения об ошибках в своем проекте, просто предоставив сообщения с теми же ключами. Кроме того, необходимо предоставить сообщения об ошибках для настроенных вами datatype, например:
databinding.conversion.error.year=Incorrect year format
Использование datatype напрямую
Большую часть времени реализации Datatype
используются внутри фреймворка для форматирования и парсинга атрибутов сущностей. Но иногда может появиться необходимость использовать datatype непосредственно в коде.
Представим, что у вас есть компонент TextField
, не привязанный ни к какому атрибуту сущности:
<textField id="amountField"/>
Теперь если в этом компоненте нужно ввести десятичные значения, вы можете назначить ему datatype в контроллере экрана, получив datatype из компонента DatatypeRegistry
:
@Autowired
private TextField<BigDecimal> amountField;
@Autowired
private DatatypeRegistry datatypeRegistry;
@Subscribe
public void onInit(InitEvent event) {
Datatype<BigDecimal> datatype = datatypeRegistry.get(BigDecimal.class);
amountField.setDatatype(datatype);
}
В действительности назначить datatype текстовому полю легче в XML, см. его атрибут datatype. |
Если вам нужно получить datatype атрибута сущности, это можно сделать с помощью метаданных. Ниже приведен синтетический пример парсинга десятичного значения с использованием datatype, соответствующего свойству сущности:
@Autowired
private Metadata metadata;
private BigDecimal parseAmountValue(String stringValue) {
MetaClass metaClass = metadata.getClass(Order.class);
Datatype<BigDecimal> amountDatatype = metaClass.getProperty("amount")
.getRange().asDatatype();
assert amountDatatype instanceof BigDecimalDatatype;
try {
return amountDatatype.parse(stringValue);
} catch (ParseException e) {
throw new RuntimeException("Cannot parse amount", e);
}
}