Интерфейс 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:

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 используя @Autowired, так как экземпляры datatype инициализируются в самом начале процесса запуска приложения и такая инжекция может вызвать циклическую зависимость.

Вместо этого, инжектируйте ApplicationContext и используйте его методы getBean() для получения требуемых бинов.

Поддержка произвольных классов 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);
    }
}