Data Types

Each non-reference entity attribute is associated with an implementation of the Datatype interface. This interface defines methods for converting attribute values to and from strings (formatting and parsing) when displaying entities in User Interface and serializing in Generic REST.

The framework provides a set of Datatype implementations corresponding to standard data types of entity attributes.

We use term datatype in all lowercase to refer to implementations of the Datatype interface.

Localized Format Strings

Many standard datatypes use a set of format strings defined in the message bundle. It enables formatting and parsing dependent on the current user locale. The default set of format strings defined in the framework is the following:

# 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

To provide your own format strings, add the corresponding messages to the message bundle of your application. For example, to use the United States date format with the English locale, add the following lines to your messages_en.properties file:

messages_en.properties
dateFormat = MM/dd/yyyy
dateTimeFormat = MM/dd/yyyy HH:mm
offsetDateTimeFormat = MM/dd/yyyy HH:mm Z

Alternatively, define a separate en_US locale and set the data format strings in the messages_en_US.properties file.

You can configure data format strings using Studio: open the Locales tab of the Project Properties window and click Show data format strings checkbox.

Customized Formatting and Parsing

You can customize formatting and parsing of values for particular entity attributes by creating your own datatype and assigning it to the attributes.

Suppose that some entity attributes in your application store calendar years, represented by integer numbers. Users should be able to view and edit a year, and if a user enters just two digits, the application should transform it to a year between 2000 and 2100. Otherwise, the whole entered number should be accepted as a year.

First, create the Datatype implementation class and annotate it with @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 A unique identifier of the datatype.
2 Java class handled by the datatype.
3 Formatting without current user’s locale. This method is called for system-level conversion.
4 Formatting considering current user’s locale. This method is called in the UI.
5 Parsing without current user’s locale. This method is called for system-level conversion.
6 Parsing considering current user’s locale. This method is called in the UI.

After creating a Datatype implementation, you can specify it for an entity attribute using the @PropertyDatatype annotation:

@PropertyDatatype("year")
@Column(name = "YEAR_")
private Integer productionYear;

You cannot inject other beans like Messages directly into the datatype class using @Autowired, because datatypes are initialized early in the startup process and such injection will cause a circular dependency.

Instead, inject ApplicationContext and use its getBean() methods to locate required beans when needed.

Support for Custom Java Type

You can use a custom Java class as a type of entity attributes.

Suppose that you have created a Java class representing a geographical coordinate:

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);
    }
}

Now you want to use this class as a type of JPA entity attribute.

First, create a JPA converter for your class:

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 With autoApply = true you don’t need to specify the converter on each attribute. The converter will be applied for all attributes of the corresponding type.

Then create a Datatype implementation class for GeoPoint and annotate it with @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 A unique identifier of the datatype.
2 Java class handled by the datatype.
3 defaultForClass = true means that the datatype will be automatically applied to all entity attributes of GeoPoint type.
4 Using @Ddl annotation, you can specify what SQL type should be used for entity attributes. Studio considers this annotation when it generates database migration scripts.
5 Formatting without current user’s locale. This method is called for system-level conversion.
6 Formatting considering current user’s locale. This method is called in the UI.
7 Parsing without current user’s locale. This method is called for system-level conversion.
8 Parsing considering current user’s locale. This method is called in the UI.

After that, when you define an entity attribute of GeoPoint type, the framework will use your custom JPA converter and datatype:

@Column(name = "GEO_POINT")
private GeoPoint geoPoint;

Conversion Error Messages

When a datatype is used by a UI component to parse string input, it may produce parsing exceptions. The UI component handles the exception and shows a user-friendly message. These messages are located in the message bundle of the framework with the databinding.conversion.error.<datatype-id> keys. For example:

databinding.conversion.error.boolean=Must be Boolean

For the whole list of messages, see messages.properties for the branch corresponding to Jmix version used in your project.

If a message for a datatype does not exist, the following generic message is used:

databinding.conversion.error.defaultMessage=Wrong format

You can override the error messages in your project just by providing messages with the same keys. Also, provide error messages for your custom datatypes, for example:

databinding.conversion.error.year=Incorrect year format

Using Datatype Directly

Most of the time Datatype implementations are used internally by the framework to provide formatting and parsing of entity attributes. But sometimes you may need to use a datatype directly in your code.

Consider that you have a TextField component not bound to any entity attribute:

<textField id="amountField"/>

Then, if you want to enter decimal values in this component, you can assign a datatype to it in the screen controller by obtaining the datatype from the DatatypeRegistry bean:

@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);
}
In fact, you can assign a datatype to a text field more easily in XML, see datatype attribute of the component.

If you need to get a datatype of an entity attribute, you can do it through metadata. Below is a rather synthetic example of parsing a decimal value using a datatype corresponding to an entity property:

@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);
    }
}