LuciadLightspeed provides a framework to access domain objects in a unified way. With this framework it is possible to access all domain objects of a LuciadLightspeed model, independent of their specific domain. Additionally, the framework provides information about the static structure of the domain objects as is typically provided by a UML diagram.
This article uses the term domain model to refer to the set of domain objects and their relationship as used in a specific domain. A LuciadLightspeed model refers to an |
Introducing the framework
LuciadLightspeed supports a multitude of domain models, for example AIXM51, SHP, DAFIF, and VPF. All these domain models have their own types of domain objects that represent the different concepts in their domains. LuciadLightspeed provides a framework for these specific domain objects which makes it easy to interact with the model and allows for example to retrieve the name of an airport, to create a new airspace, or to change the designator of a certain procedure.
In some cases, you might need to access the different domain models in a unified way. Consider for example the case of a label painter that is responsible for drawing a label containing the name of a domain object on the map. This label painter should work both for an AIXM51 airspace and for a DAFIF airport. For these use cases, LuciadLightspeed provides a framework that defines unified access to domain objects.
The com.luciad.datamodel
package defines a common meta model that describes the static structure of the different domain models. The meta model structures domain objects using types,
each with a different set of properties. TLcdDataType
and TLcdDataProperty
explicitly represent types and properties as objects at runtime, which makes the static structure of a domain model visible.
The package also defines the ILcdDataObject
interface that enables unified access to domain objects.
The java.lang.reflect
package provides similar introspection capabilities for Java classes and Java objects. One advantage of the com.luciad.datamodel
package is that you can use it in situations where new data models are created at run-time. In such cases, different types
often share the same instance class (for example TLcdDataObject
). You will not be able to extract all necessary type information using Java reflection. Also, the com.luciad.datamodel
package provides these capabilities at a higher level of abstraction, much closer to the actual data model.
A model is an abstraction of objects in the real world; a meta model is yet another abstraction, highlighting properties of the model itself. A well-known meta model is the Unified Modeling Language (UML) that is used to specify, visualize, modify, construct, and document the artifacts of an object-oriented software system. |
Getting started
Handling models
The elements contained in an ILcdModel
are domain objects. In case these domain objects implement ILcdDataObject
, the model should have an ILcdModelDescriptor
that implements ILcdDataModelDescriptor
. This interface gives access to the data model on which the model’s elements are based.
A data model, represented using the TLcdDataModel
class, is defined as a collection of types. A type is always declared by exactly one data model. Data models allow you to
group types in logical units, much like Java classes are grouped in packages or XML schema types are grouped in XML schema
documents.
Program: Displaying all element types that are defined for a certain model shows how you can use an ILcdDataModelDescriptor
to print out all data types that are defined for elements within a certain model. Note that this does not require iteration
over the elements of the model.
public void print(ILcdModel model) { if (model.getModelDescriptor() instanceof ILcdDataModelDescriptor) { ILcdDataModelDescriptor desc = (ILcdDataModelDescriptor) model.getModelDescriptor(); for (TLcdDataType t : desc.getModelElementTypes()) { System.out.println(t.getDataModel() + ":" + t.getName()); } } }
For a more elaborate code sample refer to samples\util\dataModelDisplayTree
. The sample shows how to display the information of a data model in a tree model.
Handling domain objects
The interface ILcdDataObject
is the unified representation of a domain object. A data object holds a set of named properties. Each property contains either
a simple primitive-type value or a reference to another data object. The interface ILcdDataObject
provides methods to manipulate these properties. A data object also provides access to its type. The type defines which properties
an object can have. Figure 1, “ILcdDataObject” shows ILcdDataObject
in a diagram.
Using ILcdDataObject
allows you to access the data stored inside a domain object. For example, Program: Printing all properties of an object shows how you can print the values of all properties of an object on the console.
public void print(ILcdDataObject dataObject) { for (TLcdDataProperty p : dataObject.getDataType().getProperties()) { System.out.println(p.getName() + " => " + dataObject.getValue(p)); } }
For a more elaborate code sample refer to the samples.common.dataObjectDisplayTree.DataObjectDisplayTree
class. This sample class shows how to display the properties of a data object in a tree model.
To change an object, you can use ILcdDataObject
as shown in Program: Translating the data inside an object. This code snippet shows how to translate all string typed properties for a given data object.
public void translateDataObject(ILcdDataObject dataObject) { for (TLcdDataProperty p : dataObject.getDataType().getProperties()) { if (p.getType() == TLcdCoreDataTypes.STRING_TYPE) { dataObject.setValue(p, translate((String) dataObject.getValue(p))); } } } public String translate(String aString) { //return a translated version of the string }
Finally, you can create new instances with a TLcdDataType
. Program: Creating instances with TLcdDataType
shows how to create an instance for a given type. The code also sets the value of the name
property.
TLcdDataType
public ILcdDataObject createObject(TLcdDataType type, String name) { ILcdDataObject result = type.newInstance(); TLcdDataProperty p = type.getProperty("name"); if (p != null) { result.setValue(p, name); } return result; }
Browsing type information
Given a data model, it is possible to browse for all kinds of type information. Program: Finding child types shows for example how to find all types that are declared in a given data model and that are a subtype of a given type.
public List<TLcdDataType> findSubTypes(TLcdDataModel dataModel, TLcdDataType type) { List<TLcdDataType> result = new ArrayList<TLcdDataType>(); for (TLcdDataType t : dataModel.getDeclaredTypes()) { if (type.isAssignableFrom(t)) { result.add(t); } } return result; }
Defining a data model
Suppose that we have a simple model defining a flight plan with a name. Figure 2, “Extending the Flight Plan model” shows how to extend this flight plan with an association to a way point.
You can create a data model for this extension using a TLcdDataModelBuilder
. With such a builder, you create the WayPointBasedFlightPlan
type and the WayPoint
type as shown in Program: Creating the data model.
public static TLcdDataModel createWayPointBasedFlightPlanModel() { TLcdDataModelBuilder builder = new TLcdDataModelBuilder("http://www.mydomain.com/datamodel/WayPointBasedFlightPlanModel"); TLcdDataTypeBuilder typeBuilder = builder.typeBuilder("WayPointBasedFlightPlanType"); typeBuilder.superType(FLIGHT_PLAN_DATA_TYPE); typeBuilder.addProperty("wayPoints", "WayPointType").collectionType(TLcdDataProperty.CollectionType.LIST); typeBuilder = builder.typeBuilder("WayPointType"); typeBuilder.addProperty("name", TLcdCoreDataTypes.STRING_TYPE); typeBuilder.addProperty("identifier", TLcdCoreDataTypes.STRING_TYPE); return builder.createDataModel(); }
Note how the wayPoints
property on WayPointBasedFlightPlanType
is created. Unlike the other properties, the type of this property has not yet been created. Therefore, you cannot use a
type object to set the property’s type. Instead, you need to refer to that type using its name (WayPointType
). Note that it does not matter if WayPointType
is defined before or after the WayPointBasedFlightPlanType
. The CollectionType.LIST
marks the property as a list, this is an ordered collection implemented at runtime by instances of java.util.List
.
The super type of WayPointBasedFlightPlanType
is FlightPlanType
. As such, WayPointBasedFlightPlanType
inherits the name
and geometry properties of FlightPlanType
. Also, because of this relation, the WayPointBasedFlightPlanModel
data model depends on the original FlightPlanModel
data model.
Program: Creating the data model does not assign instance classes for the two new types. As a result, new instances will be instances of TLcdDataObject
. You can change this by setting appropriate instance classes.
Explaining the meta model
Figure 3, “The meta model” shows a UML diagram of the LuciadLightspeed meta model. The three main classes defined by the meta model are TLcdDataModel
, TLcdDataType
, and TLcdDataProperty
. The following sections provide more information on each of the main classes.
Describing the static model
TLcdDataModel
A TLcdDataModel
is a collection of types that forms a logical entity. As such you can compare it to a Java or UML package or an XML schema.
A data model is identified by a unique name and can have a number of dependencies. A data model depends on another data model
when domain model objects of the data model need to refer to domain model objects of another data model. This is for example
the case when a type of the data model declares a property of a type of another data model. This is actually the most common
case of dependency. As such, these dependencies are automatically defined by the framework. Dependencies can be cyclic.
A special kind of data model is an anonymous data model. By definition, this is a data model that has no name and declares no types. It only has dependencies. These data models are typically used as a simple way to represent a group of data models as a single data model.
TLcdDataType
A TLcdDataType
represents the type of a data object. A data type describes the structure of a data object as a list of properties. Each
of these properties themselves are of a certain type. A type always has a super type. The only exception to this rule is the
Object
type, which has no super type. A type (recursively) inherits all properties from its super type. Note that a type cannot
redefine these inherited properties, they are always inherited as is.
A type is either a primitive type or a data object type. Primitive types are types which have no internal structure (no properties) and typically represent simple objects such
as strings, numbers, dates, and so on. All primitive types either extend from another primitive type or directly from the
Object
type. A data object type is structured as a list of properties. All data object types either extend from another data object
type or from the DataObject
type. Instances of data object types implement ILcdDataObject
.
Types are always defined in the context of a TLcdDataModel
. Within a certain data model, types are uniquely defined by their name. As such, two types are equal if their data models
are equal and they have the same name.
Types are mapped on Java classes. This is a many-to-one mapping; each type maps on one Java class, but different types can map on the same Java class. This class is called the type’s instance class. Instances of a type are always instances of the type’s instance class.
A type can be defined as an enumeration. This means that there are only a fixed set of possible instances. This set is directly
available from the TLcdDataType
interface. A prime example of such a type is a type of which the instance class is a Java enumeration.
You can create an instance of a data object type using the newInstance
method. Note that primitive types do not support this method.
TLcdDataProperty
A TLcdDataProperty
represents a property of a type. A property always belongs to a certain type (called
its declaring type) and also has a certain type (called its type).
A property either has a single value or multiple values. In case of a single value, the value of the property for a certain
data object is an instance of the type of the property. In case of multiple values, the property has a CollectionType
that defines if property values are represented as a set, a list, or a map. Values of the property will be instances of which
the class implements one of the Java Set
, List
or Map
interfaces. The elements of these Set
and List
objects and the values of these Map
objects are instances of the property’s type. The keys in the Map
objects are instances of the property’s map key type.
By default, a property is defined as contained. This means that, by default, the values for properties are not shared among data objects. In other words, the graph of data objects of which the types only have contained properties is a tree. Cycles are only possible if a property is not contained.
Properties which are not nullable should always have a value that is different from null
.
Creating data models
You create a data model using a TLcdDataModelBuilder
. With such a builder, you create a TLcdDataTypeBuilder
for each type you need to create. With the TLcdDataTypeBuilder
you can build a data type. When you add a property to a data type builder, a TLcdDataPropertyBuilder
is returned. That allows you to configure the properties.
Once all types and properties are built, you can ask the TLcdDataModelBuilder
to create a data model. At the same time, this creates all types and properties. From that point on, the data model builder
and all type and property builders depending on it will no longer accept any interaction and throw exceptions to indicate
that the data model has been built. This ensures that you cannot modify a data model once it has been created.
The TLcdDataModelBuilder
allows the creation of other TLcdDataModelBuilder
instances. This is required to create data models that have a cyclic dependency.
Advanced topics
Getting started gives a quick introduction to the meta model and describes a simple use case. This section covers some more advanced topics.
Designing a data model
The following subsections describe the considerations to take when modeling domain objects.
Single or multiple data models
You have to decide whether you want to model your domain with one or more data models. Using a single model is the simplest.
Using multiple data models allows partitioning of types into different groups, which can benefit larger models. For example,
suppose that the sample described in Getting started is extended so that it includes multiple different types of way points to cater for nation-specific differences. In that
case, you might decide to use two data models: one for the WayPointsBasedFlightPlanType
, and one for the different way point types.
Choosing the right super type
The meta model supports single inheritance. Each type has exactly one super type. The only exception is the predefined TLcdCoreDataTypes.OBJECT_TYPE
that serves as the root of all types. This type has no super type. Another essential core type is TLcdCoreDataTypes.DATA_OBJECT_TYPE
. This type is the root of all data object types. By definition, only data object types can have properties defined on them. Types that do not extend from DATA_OBJECT_TYPE
are called primitive types. These types are typically used for data of which the internal structure is a black box. Typical examples are simple types
like String
, Number
, and Date
of which the values are immutable. But that is not a hard constraint. In the end, it is up to you to decide which types are
data object types and which are primitives. For example, it is a perfectly good practice to define a primitive type with instance
class ILcd2DEditablePoint
(which obviously is not immutable) if you do not want to expose the internal structure of the point (its x and y coordinate)
using the data model API.
Many-valued properties
The meta model also supports many-valued properties. These properties are typically represented in the domain class as some
form of collection. For example, in the Program: Creating the data model the wayPoints
property is represented by a List
. Note that if you provide an instance class for the WayPointsBasedFlightPlanType
, then that class does not have to provide an accessor to set the value of the wayPoints
property. Instead, modifications are done by changing the list object returned by the getter. Note that ILcdDataObject
requires that implementations provide an initial (empty) list object.
((List<ILcdDataObject>) flightPlan.getValue("wayPoints")).add(wayPoint);
Data model dependencies
Data models can depend on other data models. You can define dependencies either implicitly or explicitly.
An implicit dependency is created when one of the types of a data model has a property of a type from
another data model. These dependencies are automatically detected and added by the framework. This is
the typical use case. However, the TLcdDataModelBuilder
also allows to explicitly add
dependencies. This is, for example, used to create an anonymous data model that merely serves as a
container of other data models. Note that graphs with cyclic dependencies are supported.
Traversing an object graph
An often occurring pattern when dealing with data objects is traversal through the data contained in
a graph of data objects. The ILcdDataObject
interface allows you to traverse the object graph.
Program: Traversing a data object
traverses an ILcdDataObject
by looking at the values of all its properties. If the value is
not null, then depending on whether the property is a map, a collection, or just a single value,
traversal continues on the value.
samples/common/dataObjectTraversal/DataObjectTraversalUtil
)
public static void traverseDataObject(ILcdDataObject aObject) { for (TLcdDataProperty property : aObject.getDataType().getProperties()) { Object value = aObject.getValue(property); if (value != null) { if (property.getCollectionType() == null) { // Single-valued property traverseChild(property.getType(), value); } else { switch (property.getCollectionType()) { case MAP: Map<Object, Object> map = (Map<Object, Object>) value; for (Map.Entry<Object, Object> entry : map.entrySet()) { traverseChild(property.getMapKeyType(), entry.getKey()); traverseChild(property.getType(), entry.getValue()); } break; case LIST: case SET: for (Object element : (Collection<?>) value) { traverseChild(property.getType(), element); } break; } } } } }
Program: Traversing an object traverses a single object. The type of the object is passed as argument. Note that this is merely the declared type. The object itself may be of a specialized type. This is in accordance with the standard rules on polymorphism.
-
If the type is primitive, then no further (generic) introspection is possible.
-
If the type is a data object type, then the object has to be an
ILcdDataObject
and traversal can continue as shown in Program: Traversing a data object. -
Otherwise the type is
TLcdCoreDataTypes.OBJECT_TYPE
. This is an exceptional case where you do not have enough type information available. You have to examine the object that is passed as parameter. The methodtraverseObject
uses the Javainstanceof
operator to determine how to continue the traversal.
samples/common/dataObjectTraversal/DataObjectTraversalUtil
)
public static void traverseChild(TLcdDataType aType, Object aObject) { if (aType.isPrimitive()) { traversePrimitive(aType, aObject); } else if (aType.isDataObjectType()) { traverseDataObject((ILcdDataObject) aObject); } else { traverseObject(aObject); } } public static void traverseObject(Object aObject) { if (aObject instanceof Map<?, ?>) { for (Map.Entry entry : ((Map<?, ?>) aObject).entrySet()) { // ... } } else if (aObject instanceof Collection<?>) { for (Object element : (Collection<?>) aObject) { traverseObject(element); } } else if (aObject instanceof ILcdDataObject) { traverseDataObject((ILcdDataObject) aObject); } else { traversePrimitive(null, aObject); } } public static void traversePrimitive(TLcdDataType aType, Object aValue) { // ... }
A practical example with properties of type TLcdCoreDataTypes.OBJECT_TYPE
is the modeling of
a choice from XML schema or a union from C. Both a choice and a union property can have values of
different types. You can model this by representing the choice or union by a single property. The type
of that property is the common super type of the choice or union types. If the choice or union
types contain both data object types and primitive types, this common super type is
TLcdCoreDataTypes.OBJECT_TYPE
.
Traversal as shown in the programs above works fine for graphs of objects in which there are no cycles. This is the case for most of the supported domain models in LuciadMap. In case of cycles, you need to add a check when a data object is traversed to see if the object has not already been traversed before.
Implementing ILcdDataObject
In some cases, you might not want your domain classes to extend from TLcdDataObject
. In that
case you have to implement ILcdDataObject
yourself. Probably the easiest way to do this is
to use delegation. There are cases, however, where you
may not want to keep the state in a delegate TLcdDataObject
. Program: Implementing ILcdDataObject
shows an alternative implementation for the simple WayPoint
type with two properties we used above. The
getValue
and setValue
operations can be implemented easily based on a switch on
the index of the property. This index is the position of this property in its declaring type’s
properties. Because this index is fixed by the data model, you can use it as an efficient way of
resolving properties. The getDataType
operation can easily be implemented using a static
reference to the type. In cases where more than one type uses the same domain class, you may need
to add the data type as an instance variable.
ILcdDataObject
public final class WayPoint implements ILcdDataObject { private final TLcdDataType wayPointDataType = FLIGHT_PLAN_DATA_MODEL.getDeclaredType("WayPointType"); private String name; private String identifier; @Override public TLcdDataType getDataType() { return wayPointDataType; } @Override public boolean hasValue(String propertyName) { return true; } @Override public boolean hasValue(TLcdDataProperty aProperty) { return true; } @Override public Object getValue(String propertyName) { TLcdDataProperty property = getDataType().getDeclaredProperty(propertyName); if (property == null) { throw new IllegalArgumentException("Property " + propertyName + " does not exist in data type " + getDataType()); } return getValue(property); } @Override public Object getValue(TLcdDataProperty property) { if (property.getDeclaringType() != wayPointDataType) { throw new IllegalArgumentException("Invalid property " + property); } switch (property.getIndex()) { case 0: return name; case 1: return identifier; } throw new IllegalStateException("Unknown property " + property); } @Override public void setValue(String propertyName, Object value) { TLcdDataProperty property = getDataType().getDeclaredProperty(propertyName); if (property == null) { throw new IllegalArgumentException("Property " + propertyName + " does not exist in data type " + getDataType()); } setValue(property, value); } @Override public void setValue(TLcdDataProperty property, Object value) { if (property.getDeclaringType() != wayPointDataType) { throw new IllegalArgumentException("Invalid property " + property); } switch (property.getIndex()) { case 0: name = (String) value; break; case 1: identifier = (String) value; break; } throw new IllegalStateException("Unknown property " + property); } }
Adding custom metadata
In some cases, you might want to attach additional information to a TLcdDataModel
. The meta model supports this through the use of ILcdAnnotation
. You can attach implementations of this tag to a data model. As data models are immutable, all annotations that are or will
be attached to them should be immutable as well.
Program: Annotating a data model shows how to do this for a simple example. Suppose you have a data model that is derived from an XML schema. You want to
add the location of this schema to the data model. First you define the SchemaLocation
class that implements ILcdAnnotation
and that keeps track of the location. Then, when your data model is being built, you can annotate the data model with an
appropriate SchemaLocation
instance.
First you create the custom annotation:
public class SchemaLocation implements ILcdAnnotation { private final String location; public String getLocation() { return location; } public SchemaLocation(String location) { this.location = location; } }
and use it to annotate the data model:
dataModelBuilder.annotate(new SchemaLocation("http://www.luciad.com/samples/model/1.1"));
TLcdDataModel
provides methods to retrieve all annotations that are attached to it. Program: Accessing annotations shows how to retrieve the schema location annotation from the data model.
SchemaLocation location = dataModel.getAnnotation(SchemaLocation.class); if (location != null) { System.out.println("Data model " + dataModel.getName() + " is derived from " + location.getLocation()); }
Note that exactly the same annotation functionality is also defined on TLcdDataType
and TLcdDataProperty
.
Representing geometries
Some data types are directly linked to a geometry. For example, a FlightPlanType
may be represented by a polyline. In most cases, you should not add the information about the geometry to the data type. Instead, the data type should be given an appropriate instance class
that implements an appropriate ILcdShape
interface. This keeps the data type focused on the data while the implementation of the interface ensures integration with
LuciadLightspeed.
There are two main exceptions to this general rule:
-
Sometimes you may want to use a certain instance class for different geometries. The common practice in LuciadLightspeed for such cases is to have the instance class implement
ILcdShapeList
and put the geometry that needs to be supported inside the shape list. Most parts of LuciadLightspeed can handle such composite shapes. However, some information is lost. Given a data type with such an instance class, it is no longer possible to determine which type of shape it represents. You should use theTLcdShapeListAnnotation
class to annotate the data type and prevent this loss of information. Another, less common, option is to use anILcdShape
withTLcdShapeAnnotation
. -
In some cases, it does make sense to represent geometry information also in the data type through properties. An example of such a data model is GML. This data model closely resembles the GML XML schema. As such, all data (whether it corresponds to a geometry or not) is modeled using properties. In such cases, you should annotate these data types with the
TLcdHasAShapeAnnotation
. This asserts that the state exposed by theILcdShape
interface is also exposed by one or more of these types' properties.
You can consult the API reference for a more detailed explanation on these annotation classes.