Data Objects
This document is referring to a past Scout release. Please click here for the recent version. |
Data objects are Scout beans, which are used as data transfer objects for synchronous REST and asynchronous MOM interfaces. Furthermore, they can be used as domain objects within business logic.
Data Object Definition
A data object extends the DoEntity
base class and declares each attribute as a single accessor method.
Attributes of two kinds are available:
-
Value attribute of type
T
-
List attribute of type
List<T>
The name of the accessor method defines the attribute name. The return value of the accessor method defines the attribute type.
@TypeName("lorem.ExampleEntity")
@TypeVersion(Lorem_1_2_0.class)
public class ExampleEntityDo extends DoEntity {
public DoValue<String> name() { (1)
return doValue("name");
}
public DoList<Integer> values() { (2)
return doList("values");
}
/* **************************************************************************
* GENERATED CONVENIENCE METHODS
* *************************************************************************/
@Generated("DoConvenienceMethodsGenerator")
public ExampleEntityDo withName(String name) {
name().set(name);
return this;
}
@Generated("DoConvenienceMethodsGenerator")
public String getName() {
return name().get();
}
@Generated("DoConvenienceMethodsGenerator")
public ExampleEntityDo withValues(Collection<? extends Integer> values) {
values().updateAll(values);
return this;
}
@Generated("DoConvenienceMethodsGenerator")
public ExampleEntityDo withValues(Integer... values) {
values().updateAll(values);
return this;
}
@Generated("DoConvenienceMethodsGenerator")
public List<Integer> getValues() {
return values().get();
}
}
1 | Example attribute of type String |
2 | Example attribute of type List<Integer> |
For convenience reasons when working with the data objects it is recommended to add a getter and a with (e.g. setter) method. Using the convenience with methods, new data objects can be created with fluent-style API:
ExampleEntityDo entity = BEANS.get(ExampleEntityDo.class)
.withName("Example")
.withValues(1, 2, 3, 4, 5);
Marshalling
Using the IDataObjectMapper
interface a data object can be converted from and to its string representation.
The marshalling strategy is generic and replaceable.
The Scout platform defines the IDataObjectMapper
interface, at runtime a Scout bean implementing the interface must be available.
The Scout module org.eclipse.scout.rt.jackson
provides a default implementation serializing data objects from and to JSON using the Jackson library.
String string = BEANS.get(IDataObjectMapper.class).writeValue(entity);
The data object ExampleEntityDo
serialized to JSON:
{
"_type" : "lorem.ExampleEntity",
"_typeVersion": "lorem-1.2.0",
"name" : "example",
"values" : [1,2,3,4,5]
}
ExampleEntityDo marhalled = BEANS.get(IDataObjectMapper.class)
.readValue(string, ExampleEntityDo.class);
Type Name
A data object is annotated with a logical type name using the @TypeName
annotation.
Declaring a logical type name using the @TypeName annotation for each data object is mandatory.
|
The annotation value is added to the serialized JSON object as top-level _type
property.
Using the type property the data object marshaller is able to find and instantiate the matching data object class, without having to rely on a fully classified class name.
It avoids a 1:1 dependency between the serialized JSON String and the fully classified class name.
A stable type name is required in order to be able to change the data object structure without breaking the API.
Type Version
A data object may be annotated with a type version using the @TypeVersion
annotation.
The type version represents the version of the structure of the data object and not the version of the data within the data object.
The type version value should be incremented, each time, the data object class is modified (add/remove/rename attributes).
If a version is required for versioning the values of a data object, consider add a version
attribute, incrementing its value, every time a value of the data object is modified.
The annotation value is added to the serialized JSON object as top-level _typeVersion
property.
The serialized _typeVersion
value is not deserialized into an attribute, since the deserializer creates a concrete data object class at runtime, having the @TypeVersion
annotation providing the type version value.
Declaring a logical type version using the `@TypeVersion`annotation is highly recommended if a data object is persisted as JSON document to a file or database. |
Namespace and ITypeVersion
A namespace (implementation of INamespace
) represents a container for data objects.
Each data object must have a unique type name within a namespace.
Scout has its own namespace (with ID scout
), your project should use an own one.
public final class LoremNamespace implements INamespace {
public static final String ID = "lorem";
public static final double ORDER = 9000;
@Override
public String getId() {
return ID;
}
@Override
public double getOrder() {
return ORDER;
}
}
A class implementing ITypeVersion
is used within the @TypeVersion
annotation.
Several type versions for one namespace may be bundled in a container class.
There are a few different constructors provided by AbstractTypeVersion
that simplify the definition of such a type version.
The default constructor extracts the namespace and version based on the class name.
public final class LoremTypeVersions {
private LoremTypeVersions() {
}
public static final class Lorem_1_0_0 extends AbstractTypeVersion {
}
public static final class Lorem_1_2_0 extends AbstractTypeVersion {
}
}
Signature Test
AbstractDataObjectSignatureTest
provides an abstract implementation of a test that creates a signature of all data object annotated with a type version
including additional signatures (e.g. referenced IEnum
with their values).
A signature test enables to detect changes in data object that might need a migration.
Each module containing data objects with type version annotation should implement a data object signature test.
public class DocsSnippetsDataObjectSignatureTest extends AbstractDataObjectSignatureTest {
@Override
protected String getFilenamePrefix() {
return "docs-snippets";
}
@Override
protected String getPackageNamePrefix() {
return "org.eclipse.scout.docs.snippets";
}
}
Data Object Naming Convention
Scout objects use the following naming conventions:
-
A data object class should use the `Do' suffix.
-
The value of the
@TypeName
annotation corresponds to the simple class name withoutDo
suffix-
A namespace prefix (separated by a dot) is recommended in order to avoid duplicated type names across different modules (e.g.
scout.Bookmark
,helloworld.MyDataObject
)
-
Attribute Name
The default attribute name within the serialized string corresponds to the name of the attribute accessor method defined in the data object.
To use a custom attribute name within the serialized string, the attribute accessor method can be annotated by @AttributeName
providing the custom attribute name.
@AttributeName("myCustomName")
public DoValue<String> name() {
return doValue("myCustomName"); (1)
}
1 | Important: The annotation value must be equals to the string constant used for the doValue() or doList() attribute declaration. |
{
"_type" : "CustomAttributeNameEntity",
"myCustomName" : "example"
}
Attribute Format
Using the ValueFormat
annotation a data type dependent format string may be provided, which is used for the marshalling.
@ValueFormat(pattern = IValueFormatConstants.DATE_PATTERN)
public DoValue<Date> date() {
return doValue("date");
}
The IValueFormatConstants
interface declares a set of default format pattern constants.
Attributes with type java.util.Date
accept the format pattern specified by SimpleDateFormat
class (see https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html)
Ignoring an Attribute
The @JsonIgnore
annotation included in the Jackson library is currently not supported for data objects.
To ignore an attribute when serializing a data object, the attribute must be removed from the data object by either not setting a value for the desired attribute
or by explicitly removing the attribute before a data object is serialized:
ExampleEntityDo entity = BEANS.get(ExampleEntityDo.class)
.withName("Example")
.withValues(1, 2, 3, 4, 5);
// remove by attribute accessor method reference
entity.remove(entity::name);
// remove by attribute node
entity.remove(entity.name());
// remove by attribute name
entity.remove(entity.name().getAttributeName());
// remove by attribute name raw
entity.remove("name");
Handling of DoEntity Attributes
Instead of data objects, a REST or MOM interface could be built using simple plain old Java objects (POJOs). Compared to POJOs a Scout data object offers additional support and convenience when working with attributes.
A JSON attribute may have three different states:
-
Attribute available with a value
-
Attribute available with value
null
-
Attribute not available
These three states cannot be represented with a POJO object which is based on a single variable with a pair of getter/setter.
In order to differ between value not available and value is null, a wrapper type is required, which beside the value stores the information, if the attribute is available.
Scout data objects solve this issue: Data objects internally use a Map<String, DoNode<?>>
where the abstract DoNode
at runtime is represented by a DoValue<T>
or a DoList<T>
object instance wrapping the value.
Access Data Object Attributes
-
Value:
DoNode.get()
returns the (wrapped) value of the attribute
ExampleEntityDo entity = BEANS.get(ExampleEntityDo.class)
.withName("Example")
.withValues(1, 2, 3, 4, 5);
// access using attribute accessor
String name1 = entity.name().get();
List<Integer> values1 = entity.values().get();
// access using generated attribute getter
String name2 = entity.getName();
List<Integer> values2 = entity.getValues();
-
Existence: Using the
DoNode.exists()
method, each attribute may be checked for existence
// check existence of attribute
boolean hasName = entity.name().exists();
Abstract Data Objects & Polymorphism
A simple data objects is implemented by subclassing the DoEntity
class.
For a complex hierarchy of data objects the base class may be abstract and extend the DoEntity
class, further subclasses extend the abstract base class.
The abstract base data object class does not need to specify a @TypeName
annotation since there are no instances of the abstract class which are serialized or deserialized directly.
Each non-abstract subclass must specify a unique @TypeName
annotation value.
public abstract class AbstractExampleEntityDo extends DoEntity {
public DoValue<String> name() {
return doValue("name");
}
@TypeName("ExampleEntity1")
public class ExampleEntity1Do extends AbstractExampleEntityDo {
public DoValue<String> name1Ex() {
return doValue("name1Ex");
}
/* **************************************************************************
* GENERATED CONVENIENCE METHODS
* *************************************************************************/
@Generated("DoConvenienceMethodsGenerator")
public ExampleEntity1Do withName1Ex(String name1Ex) {
name1Ex().set(name1Ex);
return this;
}
@Generated("DoConvenienceMethodsGenerator")
public String getName1Ex() {
return name1Ex().get();
}
@Override
@Generated("DoConvenienceMethodsGenerator")
public ExampleEntity1Do withName(String name) {
name().set(name);
return this;
}
}
@TypeName("ExampleEntity2")
public class ExampleEntity2Do extends AbstractExampleEntityDo {
public DoValue<String> name2Ex() {
return doValue("name2Ex");
}
/* **************************************************************************
* GENERATED CONVENIENCE METHODS
* *************************************************************************/
@Generated("DoConvenienceMethodsGenerator")
public ExampleEntity2Do withName2Ex(String name2Ex) {
name2Ex().set(name2Ex);
return this;
}
@Generated("DoConvenienceMethodsGenerator")
public String getName2Ex() {
return name2Ex().get();
}
@Override
@Generated("DoConvenienceMethodsGenerator")
public ExampleEntity2Do withName(String name) {
name().set(name);
return this;
}
}
public class ExampleDoEntityListDo extends DoEntity {
public DoList<AbstractExampleEntityDo> listAttribute() {
return doList("listAttribute");
}
public DoValue<AbstractExampleEntityDo> singleAttribute() {
return doValue("singleAttribute");
}
/* **************************************************************************
* GENERATED CONVENIENCE METHODS
* *************************************************************************/
@Generated("DoConvenienceMethodsGenerator")
public ExampleDoEntityListDo withListAttribute(Collection<? extends AbstractExampleEntityDo> listAttribute) {
listAttribute().updateAll(listAttribute);
return this;
}
@Generated("DoConvenienceMethodsGenerator")
public ExampleDoEntityListDo withListAttribute(AbstractExampleEntityDo... listAttribute) {
listAttribute().updateAll(listAttribute);
return this;
}
@Generated("DoConvenienceMethodsGenerator")
public List<AbstractExampleEntityDo> getListAttribute() {
return listAttribute().get();
}
@Generated("DoConvenienceMethodsGenerator")
public ExampleDoEntityListDo withSingleAttribute(AbstractExampleEntityDo singleAttribute) {
singleAttribute().set(singleAttribute);
return this;
}
@Generated("DoConvenienceMethodsGenerator")
public AbstractExampleEntityDo getSingleAttribute() {
return singleAttribute().get();
}
}
ExampleDoEntityListDo entity = BEANS.get(ExampleDoEntityListDo.class);
entity.withListAttribute(
BEANS.get(ExampleEntity1Do.class)
.withName1Ex("one-ex")
.withName("one"),
BEANS.get(ExampleEntity2Do.class)
.withName2Ex("two-ex")
.withName("two"));
entity.withSingleAttribute(
BEANS.get(ExampleEntity1Do.class)
.withName1Ex("single-one-ex")
.withName("single-one"));
If an instance of ExampleDoEntityListDo
is serialized, each attribute is serialized using its runtime data type, adding an appropriate _type
attribute to each serialized object.
Therefore, the deserializer knows which concrete class to instantiate while deserializing the JSON document.
This mechanism is used for simple value properties and list value properties.
To each object which is part of a list value property the _type
property is added to support polymorphism within single elements of a list.
{
"_type" : "ExampleDoEntityListDo",
"listAttribute" : [ {
"_type" : "ExampleEntity1",
"name" : "one",
"name1Ex" : "one-ex"
}, {
"_type" : "ExampleEntity2",
"name" : "two",
"name2Ex" : "two-ex"
} ],
"singleAttribute" : {
"_type" : "ExampleEntity1",
"name" : "single-one",
"name1Ex" : "single-one-ex"
}
}
Rename an attribute of a data object in a subclass
To rename a data object attribute in a subclass, override the attribute accessor method and annotate it with @AttributeName
using the new attribute name as value.
Additionally the overridden method must call the doValue()
method providing the new attribute name as argument.
@TypeName("ExampleEntityEx")
public class ExampleEntityExDo extends ExampleEntityDo {
@Override
@AttributeName("nameEx")
public DoValue<String> name() { (1)
return doValue("nameEx");
}
/* **************************************************************************
* GENERATED CONVENIENCE METHODS
* *************************************************************************/
@Override
@Generated("DoConvenienceMethodsGenerator")
public ExampleEntityExDo withName(String name) {
name().set(name);
return this;
}
@Override
@Generated("DoConvenienceMethodsGenerator")
public ExampleEntityExDo withValues(Collection<? extends Integer> values) {
values().updateAll(values);
return this;
}
@Override
@Generated("DoConvenienceMethodsGenerator")
public ExampleEntityExDo withValues(Integer... values) {
values().updateAll(values);
return this;
}
}
1 | Rename name attribute of superclass to nameEx |
Interfaces to Data Objects
Use the basic data object interface IDoEntity
to model a data object hierarchy with own base interfaces and a set of implementing classes.
Interfaces extending IDataObject
do not need a @TypeName
annotation, since they are never directly serialized or deserialized.
The interfaces may be used as types for attributes within a data object. At runtime the concrete classes implementing the interfaces are serialized and their @TypeName
annotation value is used.
Equals and Hashcode
The Data Object base class DoEntity
defines a generic equals()
and hashCode()
implementation considering all attributes of a data object for equality.
A data object is equals to another data object, if the Java class of both data objects is identical and the attribute maps (including their nested values) of both data objects are equals.
For futher details see:
-
org.eclipse.scout.rt.dataobject.DoEntity.equals(Object)
-
org.eclipse.scout.rt.dataobject.DoNode.equals(Object)
Generic DoEntity
An instance of the DoEntity
class can represent any kind of JSON document.
If the JSON document contains no type attributes or no matching data object class exists at runtime, the JSON document is deserialized into a raw DoEntity
instance holding all attributes.
To access the attributes of the data object a set of generic getter methods may be used by specifying the attribute name.
A generic JSON document is deserialized into a generic tree-like structure of nested DoEntity
instances.
If the serialized JSON document contains a _type
and/or _typeVersion
attribute, the attribute and its value is added as attribute to the generic raw DoEntity
instance.
ExampleEntityDo entity = BEANS.get(ExampleEntityDo.class)
.withName("Example")
.withValues(1, 2, 3, 4, 5);
// access name attribute by its attribute name
Object name1 = entity.get("name"); (1)
String name2 = entity.get("name", String.class); (2)
String name3 = entity.getString("name"); (3)
// access values attribute by its attribute name
List<Object> values1 = entity.getList("values"); (4)
List<String> values2 = entity.getList("values", String.class); (5)
List<String> values3 = entity.getStringList("values"); (6)
// optional list attribute access by its attribute name
Optional<List<Object>> values4 = entity.optList("values"); (7)
Optional<List<String>> values5 = entity.optList("values", String.class); (8)
1 | Accessing value attribute, default type is Object |
2 | Accessing value attribute, specify the type as class object if known |
3 | Accessing value attribute, convenience method for a set of common types |
4 | Accessing list attribute, default type is Object |
5 | Accessing list attribute, specify the type as class object if known |
6 | Accessing list attribute, convenience method for a set of common types |
7 | Accessing optional list attribute, default type is Object |
8 | Accessing optional list attribute, specify the type as class object if known |
If a list attribute is not available, using one of the getList(…) getters adds an empty list as attribute value into the entity and returns the list.
Use optList(…) in order to get an optionally available list without adding a new empty list as attribute.
|
Apart of the convenience methods available directly within the DoEntity
class, the DataObjectHelper
class contains a set of further convenience methods to access raw values of a data object.
Accessing number values
If a generic JSON document is deserialized to a DoEntity
class without using a subclass specifying the attribute types, all attributes of type JSON number are deserialized into the smallest possible Java type.
For instance the number value 42 is deserialized into an Integer value, a large number may be deserialized into a BigInteger
or BigDecimal
if it is a floating point value.
Using the convenience method DoEntity.getDecimal(…)
each number attribute is converted automatically into a BigDecimal
instance on access.
If a generic JSON document is deserialized, only a set of basic Java types like String, Number, Double are supported. Every JSON object is deserialized into a (nested) DoEntity structure, which internally is represented by a nested structure of Map<String, Object> .
|
Map of objects
To build map-like a data object (corresponds to Map<String, T>
), the DoMapEntity<T>
base class may be used.
@TypeName("ExampleMapEntity")
public class ExampleMapEntityDo extends DoMapEntity<ExampleEntityDo> {
}
The example JSON document of ExampleMapEntityDo instance with two elements:
{
"_type" : "ExampleMapEntity",
"mapAttribute1" : {
"_type" : "ExampleEntity",
"name" : "example-1",
"values" : [1,2,3,4,5]
},
"mapAttribute2" : {
"_type" : "ExampleEntity",
"name" : "example-2",
"values" : [6,7,8,9]
}
}
ExampleMapEntityDo mapEntity = BEANS.get(ExampleMapEntityDo.class);
mapEntity.put("mapAttribute1",
BEANS.get(ExampleEntityDo.class)
.withName("Example")
.withValues(1, 2, 3, 4, 5));
mapEntity.put("mapAttribute2",
BEANS.get(ExampleEntityDo.class)
.withName("Example")
.withValues(6, 7, 8, 9));
ExampleEntityDo attr1 = mapEntity.get("mapAttribute1"); (1)
Map<String, ExampleEntityDo> allAttributes = mapEntity.all(); (2)
1 | Accessing attribute using get method returns the attribute of declared type T |
2 | Accessing all attributes using all method returns a map with all attributes of type T |
A DoMapEntity<T> subclass may declare custom attributes of another type than T (e.g. an integer size attribute). If attributes of other types are used, using the all method results in a ClassCastException since not all attributes are of the same type any longer.
|
IDataObject Interface - Data Objects with unknown structure
According to the JSON specification a JSON document at top level may contain a object or an array. If a JSON string of unknown structure is deserialized, the common super interface IDataObject
may be used as target type for the call to the deserializer:
String json = "<any JSON content>";
IDataObjectMapper mapper = BEANS.get(IDataObjectMapper.class);
IDataObject dataObject = mapper.readValue(json, IDataObject.class);
if (dataObject instanceof IDoEntity) {
// handle object content
}
else if (dataObject instanceof DoList) {
// handle array content
}
Ad-Hoc Data Objects
The DoEntityBuilder
may be used to build ad-hoc data objects without a concrete Java class defining its attributes.
IDoEntity entity = BEANS.get(DoEntityBuilder.class)
.put("attr1", "foo")
.put("attr2", "bar")
.putList("listAttr", 1, 2, 3)
.build(); (1)
String entityString = BEANS.get(DoEntityBuilder.class)
.put("attr1", "foo")
.put("attr2", "bar")
.putList("listAttr", 1, 2, 3)
.buildString(); (2)
1 | Builder for a DoEntity object |
2 | Builder for the string representation of a DoEntity objects |
Maven Dependencies
The Scout data object implementation does not reference any specific Java serialization library or framework.
The basic building blocs of data objects are part of the Scout platform and to not reference any thirdparty libraries.
At runtime an implementation of the IDataObjectMapper
interface must be provided.
The Scout default implementation based on the JSON library Jackson is provided by adding a maven dependency to the module org.eclipse.scout.rt.jackson
.
The dependency to this module must be added in the top-level .dev/.app module.
A dependency within the program code is not necessaray as long as no specific Jackson features should be used within the application code.
Data Object Inventory
The class org.eclipse.scout.rt.dataobject.DataObjectInventory
provides access to all available data objects at runtime.
For each data object all available attributes and their properties (name, type, accessor method and format pattern) are available:
Map<String, DataObjectAttributeDescriptor> attributes =
BEANS.get(DataObjectInventory.class).getAttributesDescription(ExampleEntityDo.class);
attributes.forEach(
(key, value) -> System.out.println("Attribute " + key + " type " + value.getType()));
Apart from attribute descriptions, the inventory provides access to type name and type version of each data object class.
Extending with custom serializer and deserializer
The application scoped beans DataObjectSerializers
resp. DataObjectDeserializers
define the available serializer and deserializer classes used to marshal the data objects.
Own custom serializer and deserializer implementations can be added by replacing the corresponding base class and register its own custom serializer or deserializer.
Enumerations within Data Objects
Implementations of org.eclipse.scout.rt.dataobject.enumeration.IEnum
add a stringValue()
method to each enumeration value, guaranteeing a constant, fixed string value for each enumeration value.
An arbitrary Java enum may be used within a data object, but does not guarantee a stable serialized value, if an enumeration value is changed in future.
Additionally implementations of IEnum
can be annotated with @EnumName
to support being referenced in a data object signature test.
All instances of IEnum
may be used within data objects and are automatically serialized to their JSON string value representation and deserialized back to the correct Java class instance.
The default resolver mechanism for IEnum
(see org.eclipse.scout.rt.dataobject.enumeration.EnumResolver
) matches the given string with the available string values in the current enumeration implementation to look up the matching enumeration value.
An optional static resolve()
method handles the resolve of a given string value into the correct enumeration value allowing to support even string values, whose enumeration values where changed or deleted.
@EnumName("scout.ExampleEnum")
public enum ExampleEnum implements IEnum {
ONE("one"),
TWO("two"),
THREE("three");
private final String m_stringValue;
ExampleEnum(String stringValue) {
m_stringValue = stringValue;
}
@Override
public String stringValue() {
return m_stringValue;
}
public static ExampleEnum resolve(String value) { (1)
// custom null handling
if (value == null) {
return null;
}
switch (value) {
// custom handling of old values (assuming 'old' was used in earlier revisions)
case "one":
return ONE;
case "two":
return TWO;
case "three":
return THREE;
case "four":
return THREE;
default:
// custom handling of unknown values
throw new AssertionException("unsupported status value '{}'", value);
}
}
}
1 | Optional resolve method |
Typed IDs within Data Objects
Implementations of org.eclipse.scout.rt.dataobject.id.IId<WRAPPED_TYPE>
interface wrap an arbitrary value adding a concrete Java type to a scalar value.
E.g. the key of an example entity which technically is a UUID becomes an instance of the ExampleId
class.
All instances of IId
may be used within data objects and are automatically serialized to their JSON string representation of the wrapped value and deserialized back to the correct Java class instance.
An exampleId instance may then be used as type-safe parameter for further referencing a given example entity record, for instance as attribute value within a data object.
@IdTypeName("scout.ExampleId")
public static final class ExampleId extends AbstractUuId {
private static final long serialVersionUID = 1L;
public static ExampleId create() {
return new ExampleId(UUID.randomUUID());
}
public static ExampleId of(UUID id) {
if (id == null) {
return null;
}
return new ExampleId(id);
}
public static ExampleId of(String id) {
if (id == null) {
return null;
}
return new ExampleId(UUID.fromString(id));
}
private ExampleId(UUID id) {
super(id);
}
}