In certain scenarios, a model for your data already exists in some form and you are faced with the task of creating a different view on that model. Consider the case of building an editor for XML files of a certain schema.
Say you want to give the user ability to switch from editing the XML source directly and using a rich form-based UI. WTP has an XML source editor that you want to re-use in this project. It turns out that WTP XML editor maintains an in-memory model of the document in a form of a DOM tree. This DOM is tied directly to the editor buffer. When one changes, the other is updated automatically. Of course, building your form UI by binding directly to the DOM tree is a tedious and error-prone process. The DOM has very little in a way of semantic information about the data that it contains. This means that you would be embedding semantics of the data that you are modeling in the UI code. This is generally not a good idea.
Ok, so you need to build a semantic model for your data and somehow link it to the DOM instance exposed by the XML editor so that you can be connected to the editor's text buffer. Unfortunately, most existing modeling frameworks don't have a good answer for this scenario. You can build a semantic model using JAXB or EMF, but then you have a completely separate model in memory that you have to synchronize with the DOM tree. Not only is it not an efficient use of system resources (two full models loaded in memory), but writing by-directional synchronization logic between two different models is an extremely complex task. What you really want is a light-weight wrapper around DOM that will give you a rich semantic view on your data without the memory overhead and synchronization issues of two separate models.
Sapphire Modeling Framework is a solution specifically intended to help you create layered models that lightly wrap around existing data. Layering on top of DOM is one specific scenario that the framework was built to support, but in principle it can support other underlying models, including other Sapphire models.
The Sapphire Modeling Framework is located in the org.eclipse.sapphire.modeling plugin. You will need to add a dependency on this plugin. You will also need this plugin present in your development Eclipse install. The framework uses Java annotation processing for code generation and the necessary processor is located in this plugin. Finally, you need to make sure that annotation processing is enabled on the plugin project that will contain your model. This setting is located under Project Properties -> Java Compiler -> Annotation Processing.
A model is a tree. Rather than building from the root, I find it easier to work on the leaves first. Once you have finished the leaves, it is fairly easy to attach them to branches and wrap it up by connecting everything to the model root.
Sapphire model elements are specified as Java interfaces that extend either IModelElement or IRemovableModelElement. Most of the model elements that you are going to write are going to be removable. Removable model elements are those elements that are held in lists or need to be explicitly created before use. Non-removable elements always exist if the parent element exists. These are useful for cases where you just want to organize related properties, but don't want to manage their container's existence. As an example of this, consider a person's name. Say you are creating a contacts management application and you are working on the IContact element. You could put all the individual properties that correspond to name parts (first, middle, last, etc.) directly under IContact or you could create a non-removable IContactName element to hold these properties.
Let's now take a look at an example of a very simple model element that has three value properties. Do not worry about the details at this point. We will cover those details later in this article. For now, just note the type declaration and property declarations. These static objects collectively represent the model metadata and reflective access API for the model. Every model element must have a static member called TYPE and one or more static members called PROP_* for each of the properties. Properties are further annotated with semantics and have a series of accessor methods that vary depending on the type of the property.
import org.eclipse.sapphire.modeling.*;
import org.eclipse.sapphire.modeling.annotations.*;
import org.eclipse.sapphire.modeling.values.*;
public interface IContactName extends IModelElement
{
ModelElementType TYPE = new ModelElementType( IContactName.class );
// *** FirstName ***
@Label( standard = "first name" )
@NonNullValue
ValueProperty PROP_FIRST_NAME = new ValueProperty( TYPE, "FirstName" );
StringValue getFirstName();
void setFirstName( String firstName );
// *** MiddleName ***
@Label( standard = "middle name" )
ValueProperty PROP_MIDDLE_NAME = new ValueProperty( TYPE, "MiddleName" );
StringValue getMiddleName();
void setMiddleName( String middleName );
// *** LastName ***
@Label( standard = "last name" )
@NonNullValue
ValueProperty PROP_LAST_NAME = new ValueProperty( TYPE, "LastName" );
StringValue getLastName();
void setLastName( String lastName );
}
The root of your model must extend IModel (which itself extends IModelElement). IModel interface includes API for managing the lifecycle of the model, such as saving and disposing. It also gives you a way to access the model store, the object which abstracts the details of how the model is stored and loaded. Other than a extending a different interface, the model does not differ from other model elements.
Once you have defined all of your model elements, it is time to create the class that will be responsible for creating model instances. The Sapphire Modeling framework does not place any constraints on how you choose to implement the model factory, but certain patterns have emerged. The following is an example of a very simple model factory.
import java.io.*;
import org.eclipse.sapphire.modeling.*;
import org.eclipse.core.resources.*;
public class ContactsDatabaseFactory
{
// This is the most important method in any factory. It allows the client to create
// a model instance using any model store implementation.
public static IContactsDatabase load( final ModelStore modelStore )
{
return new ContactsDatabase( modelStore );
}
// You may also want to add a few convenience methods so that the clients don't
// have to know how to instantiate some of the more common model stores.
public static IContactsDatabase load( final File file )
{
final FileModelStore modelStore = new FileModelStore( file );
return load( modelStore );
}
public static IContactsDatabase load( final IFile file )
{
final EclipseFileModelStore modelStore = new EclipseFileModelStore( file );
return load( modelStore );
}
}
At this point, an astute reader would ask where the model implementation came from. So far, we only talked about creating interfaces and you cannot instantiate those. At it's core, Sapphire Modeling Framework just specifies how model interface should look like so that semantics are exposed and easier reflective access is possible. The actual model element implementations vary significantly depending on the type of data that is being wrapped. Of course, if your scenario involves wrapping XML DOM, rest assured that you will not be writing you model implementation by hand. At the end of this article (in the XML Binding section), we will cover how to annotate your model with XML bindings and have Sapphire generate the implementation classes for you.
Value properties are the smallest building blocks of the model. A value is an atomic piece of data that does not break up further into model elements. Let's take a look at an example of a value property definition in detail.
// Annotations on the property define semantics and behaviors. Take a look at
// the annotations section in this article for a complete documentation of all
// available annotations.
@Label( standard = "age" )
@NonNullValue
// The property itself. Note that this is not a simple string as you would have
// with a Java Bean. This is an instance of ValueProperty. The name of the property
// is passed into the ValueProperty constructor.
ValueProperty PROP_AGE = new ValueProperty( TYPE, "Age" );
// Various property accessor methods. The naming rules are the same as the convention
// used for Java Beans. The getter is going to be called get[PropertyName] or
// is[PropertyName]. The setter will be called set[PropertyName].
// The type returned by the property getter method is the type of the value property.
// Note that this type is not identical to any of the types taken by the setter
// methods. This is an important detail. The type returned by a value property getter
// is a special value object that has many property-specific capabilities that will be
// covered in detail later in this section. As for setters, there is always a setter
// that takes a string and optionally a setter that takes a typed object. Further,
// the setters always take objects rather than a primitive type like int or boolean.
// Even when semantics dictate that this would be an error condition, it should be
// possible to set a property to null.
IntegerValue getAge();
void setAge( String age );
void setAge( Integer age );
So what are value objects? Consider that even when semantics specify that a particular value is typed, the backing store (such as XML DOM) may allow untyped data to be entered. If the semantic model only provides access to strictly-typed data, then the only options you have when faced with invalid content is to throw an exception or return some default value. Neither option is particularly satisfactory. You want the semantic model to give you access to both typed and untyped data. This is the most important function of the value object. It is initialized with untyped data (in string form) and is able to parse this data on demand. In addition to this, the value object also provides access to the default value and is able to validate the data that it holds.
It is important to note that a value property getter never returns null. It always returns a value object, which may in turn hold null data.
All value classes extend abstract class Value. The Sapphire Modeling Framework has a collection of standard value class implementations (such as IntegerValue and BooleanValue), but those writing models using the framework may need to create a custom value implementation. Let's take a look at what the interface for the Value class looks like.
public abstract class Value<T>
{
public Object getContext()
public String getString()
public String getString( boolean useDefaultValue )
public T getParsedValue()
public T getParsedValue( boolean useDefaultValue )
public T getDefaultValue()
public Status validate()
// This is the method that you have to implement in order to create
// a custom value implementation.
protected abstract T parse( String str )
}
An important variant of a value property is an enum property. Like for other value properties, you need the base type and the wrapper value type. For enums, the base type is the enum itself while the value type is generated by the annotation processor.
Let's start with the enum definition.
package org.eclipse.sapphire.samples.contacts;
import org.eclipse.sapphire.modeling.annotations.*;
// The GenerateEnumValue annotation will be picked up by the annotation processor
// and cause the wrapper value type to be generated. The generated wrapper will
// be called the same as the enum type, but with "Value" prefixed.
// By default, the enum value type is generated into the same package as the base
// enum type. This is appropriate for most situations, but can be overridden by
// using the packageName attribute of this annotation.
@GenerateEnumValue
// The Label annotation is used on the enum type in order to provide a more
// user-friendly term to use when referring to the enumeration. This is used
// when generating validation messages and in other contexts.
//
// Example: "foobar" is not a valid contact type.
//
// In the above example "contact type" is the string specified by this label annotation.
@Label( standard = "contact type" )
public enum ContactType
{
// By default, the label used for the annotation item is exactly as returned by
// the Enum.name() method. You can override this by using the Label annotation.
@Label( standard = "personal" )
PERSONAL,
@Label( standard = "business" )
BUSINESS;
// Override the toString() method if you want to control the string representation
// of enum values. This will be used both during reading and writing these values.
public String toString()
{
if( this == PERSONAL )
{
return "p";
}
else if( this == BUSINESS )
{
return "b";
}
else
{
throw new IllegalStateException( name() );
}
}
}
The corresponding ContactType.properties file would look like this:
enumTypeLabel = contact type
personalLabel = personal
businessLabel = business
Now we are ready to use this enum for a value property. This looks very similar to how other types of value properties are defined.
ValueProperty PROP_CONTACT_TYPE = new ValueProperty( TYPE, "ContactType" );
...
@DefaultStringValue( "p" ) // use the string representation of the enum item
...
ContactTypeValue getContactTypeType();
void setContactType( String contactType );
void setContactType( ContactType contactType );
An element property provides access to a child model element when child element cardinality is at most one. Two important variants are supported. The child model element can be removable or non-removable. The difference here is that for removable elements, the property needs to give client a way to create the element, while for non-removable elements, the element always exists.
Removable element properties are defined as follows:
static final ClassProperty PROP_ADDRESS = new ClassProperty( TYPE, "Address", IAddress.class );
IAddress getAddress(); // defaults createIfNecessary to false
IAddress getAddress( boolean createIfNecessary );
Now suppose, you changed your mind and made IAddress non-removable. The address property definition would then look as follows:
static final ClassProperty PROP_ADDRESS = new ClassProperty( TYPE, "Address", IAddress.class );
IAddress getAddress(); // never returns null
Note that for legacy reasons the property class is called "ClassProperty". It will likely be renamed to something like "ElementProperty" to better reflect it's function.
Model properties that contain an ordered sequence of elements are known as list properties. List properties can be homogeneous (only holding one type of model elements) or heterogeneous (holding several different types of model elements). Note that for heterogeneous list properties, you have to be able to enumerate at model design time all of the possible types that the list will hold. You specify the model element types that the list property is capable of containing using the ListMemberTypes annotation. This annotation is required for all list properties.
In the first example, we will define a homogeneous list property.
@ListMemberTypes( base = IBike.class )
ListProperty PROP_BIKES = new ListProperty( TYPE, "Bikes" );
Now suppose you wanted to create specialized interfaces IMountainBike and IRoadBike that both extend IBike and you want to be able to add elements representing both types of bikes to the list. The syntax looks similar to the original example, but now you have to enumerate all of the possible types in addition to the base type.
@ListMemberTypes( base = IBike.class,
possible = { IMountainBike.class, IRoadBike.class } )
ListProperty PROP_BIKES = new ListProperty( TYPE, "Bikes" );
So what about accessor methods? For list properties, only a getter method is needed as all of the changes are performed using methods on the returned list. Note that you must use ModelElementList class in your getter declaration rather than the raw List class.
ModelElementList getBikes();
The Sapphire Modeling Framework relies on Java annotations to attach additional information and behaviors to model elements and properties. The annotations are used in two ways. Some are read at runtime when the model is loaded, while others are processed by the annotation processor that is part of the framework and are used for code generation at build time.
This section is a reference to all of the framework's annotations.
Applies to value properties that use RelativePathValue class. In situations where the path held in a property is relative, but you know all of the possible roots or at least can calculate them at runtime, using this annotation will provide the model consumer with a richer experience. For instance, when combined with the ResourceMustExist annotation, the model will be able to verify validity of the provided relative path. In UI, this annotation can also be used by the property editor renderer to provide browsing and content help behaviors.
Example:
Consider the case where you have a "LibraryPath" property which holds a relative path to a library that can be found in one of several designated directories. This example has been simplified to return absolute paths. Base paths are typically computed at runtime by taking into account the surrounding context.
import java.util.Collections;
import java.util.*;
import org.eclipse.sapphire.modeling.annotations.BasePathsProviderImpl;
import org.eclipse.sapphire.modeling.util.Path;
public class LibraryLocationsBasePathProviderImpl extends BasePathsProviderImpl
{
public List getBasePaths( Object modelElement )
{
final List result = new ArrayList();
result.add( new Path( "c:/private" ) );
result.add( new Path( "c:/shared" ) );
return result;
}
}
You would then attach it to the "LibraryPath" property by using the following declaration.
@BasePathsProvider( LibraryLocationsBasePathProviderImpl.class )
Applies to list properties. Declares that the number of items contained in the list must fall within the specified range. You can specify either both min and max parameters or just one of them. If not specified, min defaults to 0 and max defaults to Integer.MAX_VALUE.
Example A:
The list must contain at least one item.
@CountContraint( min = 1 )
Example B:
The list must contain at least two, but no more than eight items.
@CountContraint( min = 2, max = 8 )
Applies to value properties. Declares the default value that should be used if property value has not been set. The DefaultStringValue annotation can be used not just for string properties, but also for any other data type that doesn't have a more specific annotation.
Example:
@DefaultIntegerValue( 200 )
Applies to value properties. Provides a way to attach custom logic for computing the default value of a property at runtime. Your default value provider implementation will have access to the model element and the property in question. From there, you can compute the default value based on the state of other model properties or even external system state.
Make sure to consider how the annotated property will get notified of changes to entities that are used for computing the default value. If the default value computation involves other model properties, you will likely want to use the DependsOn annotation to declare the relationship.
Example:
Consider a situation where you have a target operating system property. You want the default value to be the running OS where the model is loaded, but at the same time let the user set the desired target OS manually.
import org.eclipse.sapphire.modeling.IModelElement;
import org.eclipse.sapphire.modeling.ValueProperty;
import org.eclipse.sapphire.modeling.annotations.DefaultValueProviderImpl;
public class OperatingSystemDefaultValueProvider extends DefaultValueProviderImpl
{
@Override
public String getDefaultValue( IModelElement modelElement, ValueProperty property )
{
return new System.getProperty( "os.name" );
}
}
You would then attach it to the property by using the following declaration.
@DefaultValueProvider( OperatingSystemDefaultValueProvider.class )
Applies to methods when code generation is used for creating model element implementation. This annotation enables additional methods that are not property getters or setters to be added to a model element by telling the code generator that a static method in another class will handle the method implementation. The method in question must be static, public and have the same name and return type as the method that is being delegated. The method parameters must match as well, however the first parameter will always be the model element. The type of the first parameter can be kept generic by using IModelElement, but most of the time it is more convenient to use the more concrete model element type.
Example:
Consider a situation where you have an IContact model element in a contacts database model, which has a list of phone numbers. Suppose you wanted to add a convenience method to easily remove all phone numbers that have a specific area code. You would add the following method to the IContact interface. Note the use of the DelegateImplementation annotation.
@DelegateImplementation( ContactMethods.class )
void removePhoneNumbersByAreaCode( String areaCode );
The ContactMethods class would look something like the following listing.
public final class ContactMethods
{
public static void removePhoneNumbersByAreaCode( IContact contact, String areaCode )
{
for( IPhoneNumber pn : contact.getPhoneNumbers() )
{
if( areaCode.equals( pn.getAreaCode().getString() ) )
{
pn.remove();
}
}
}
}
Applies to properties. Declares that the property in question depends on one or more properties. This is used in cases where the value, the enablement or the validation state of a property can change as the result of changes to another property. The properties are specified by name. Note that this is the property name, not the field name that holds the property object instance. You can only declare a dependency on other properties within the same model element.
If you register a property change listener on property A which depends on property B, when property B changes the listener registered on property A will be notified. The events are cascaded if a multi-level property dependency exists. If A depends on B which depends on C, the registered listeners on A and B will hear about changes to C. It is important to note that the event will be generated regardless of whether the property actually changed in any way as the result of the dependency change. The framework does not track the state of the property, so it has no way of knowing if the property actually changed.
Circular dependency relationships between properties are supported. This allows one to create a group of properties that are inter-dependent. A good example of this can be found by looking at addresses. The relationship between city, state and zip is such that a change in one affects validity of the other two.
Example:
Consider the case where you have a computed property "FullName". You can use the DependsOn annotation in the following way to declare dependency on "FirstName" and "LastName" properties.
@DependsOn( "FirstName", "LastName" )
Applies to properties. Declares that the annotated property should be enabled when the value of the property specified in this annotation is true. The property is specified by name. Note that this is the property name, not the field name that holds the property object instance. You can only reference another property within the same model element.
Example:
Consider the case where you are modeling travel arrangements. If someone reserved a rental car, then the name of the rental car company must be specified. Otherwise, it is not relevant. You would add the following annotation to the "RentalCarCompany" property.
@EnabledByBooleanProperty( "RentalCarReserved" )
Applies to properties. Declares that the annotated property should be enabled when the value of the specified enumeration property is one of the provided values. The property is specified by name. Note that this is the property name, not the field name that holds the property object instance. You can only reference another property within the same model element.
Example:
Consider the case where you are modeling connection parameters. One of the properties is an enumeration of different connection types. The rest of the properties are various parameters that may or may not be applicable for a given connection type. You might might add the following annotation to the "Port" property.
@EnabledByEnumProperty( property = "ConnectionType", values = { "HTTP", "HTTPS" } )
Applies to properties. Used for attaching custom logic for calculating whether a given property should be enabled. The enablement logic is implemented by creating a class that extends EnablerImpl class. This annotation is typically used in conjunction with the DependsOn annotation if the enablement logic takes into account values of other properties.
Example:
import org.eclipse.sapphire.modeling.ModelProperty;
import org.eclipse.sapphire.modeling.annotations.EnablerImpl;
public class AuthorizingManagerEnabler extends EnablerImpl
{
public boolean isEnabled( ModelProperty property, Object element )
{
final ITravelPlan tp = (ITravelPlan) element;
return ( tp.getAirfareCost().getParsedValue() + tp.getLodgingCost().getParsedValue ) >= 1000;
}
}
Once you've implemented the enabler, you would attach it to the "AuthorizingManager" property using the following annotations. Notice the use of the DependsOn annotation to setup a dependency on the "AirfareCost" and "LodgingCost" properties. This ensures that the "AuthorizingManager" property enablement will be re-evaluated when either one of those properties changes.
@Enabler( AuthorizingManagerEnabler.class )
@DependsOn( "AirfareCost", "LodgingCost" )
Applies to list properties. Declares that the items in the list cannot be re-arranged. This is typically used when the underlying model does not allow arbitrary positions for list items. This is less restrictive than using the ReadOnly annotation since items can still be added and removed.
Example:
@FixedOrderList
Applies to enum types. Used for flagging the enum to the Sapphire annotation processor for generation of the value wrapper class. See Enum Properties section for more information.
Applies to model elements. It is often desirable to associate images with model elements or model element types. Even though images are not interesting in non-UI contexts, having ability to attach them directly to the model simplifies their management and makes it easier to write UI. This annotation is designed to allow future extensibility, but currently it only allows a "small" image to be attached. A small image should be 16x16 pixels.
You can choose to attach either a static image or an image provider. The image provider is particularly useful when the state of the model element instance should influence the image.
Example A:
This example attaches a static image. The first segment in the path must be a bundle id.
@Image( small = "org.eclipse.sapphire.samples/images/person.png" )
Example B:
This example attaches an image provider that varies the image based on the value of one of the element's properties. Note that just like when images are attached statically, the image provider returns paths that must begin with the bundle id.
Full working listing of this example can be found in the Calendar Sample.
@Image( provider = AttendeeImageProvider.class )
import org.eclipse.sapphire.modeling.*;
import org.eclipse.sapphire.modeling.annotations.*;
public class AttendeeImageProvider extends ImageProvider
{
private static final String IMG_PERSON = SapphireSamplesPlugin.PLUGIN_ID + "/images/person.png";
private static final String IMG_PERSON_FADED = SapphireSamplesPlugin.PLUGIN_ID + "/images/person-faded.png";
public String getSmallImagePath( IModelElement element )
{
if( ( (IAttendee) element ).isInContactsDatabase().getParsedValue() )
{
return IMG_PERSON;
}
else
{
return IMG_PERSON_FADED;
}
}
public String getSmallImagePath( ModelElementType type )
{
return IMG_PERSON;
}
}
Applies to value properties that use JavaClassNameValue type. Declares that the class name held by the property must implement the specified interface.
Example:
@Implements( "org.w3c.dom.events.EventListener" )
Applies to integer value properties. Declares that the value of the property must be within the range specified by this annotation. You can specify both min and max or just one of them. If not specified, min defaults to Integer.MIN_VALUE/Long.MIN_VALUE and max defaults to Integer.MAX_VALUE/Long.MAX_VALUE.
Example A:
The value must be greater or equal to one.
@IntegerRangeConstraint( min = 1 )
Example B:
The value must be between two and eight (inclusive).
@LongRangeConstraint( min = 2, max = 8 )
Applies to properties, model element types, enum types, enum values, etc. Associates a human-friendly label or term to go with whatever is being labeled. Labels are very important in Sapphire models. They are used in everything from validation messages to widget labels in the UI. The specified label strings are extracted by the Sapphire annotation processor into property files for localization. At runtime, the label text is drawn from the locale-appropriate resource file rather than the annotation itself.
The labels are expected to be in a specific form. Use all lower case letters unless the word is an acronym or must be capitalized. Sapphire will capitalize the label as appropriate depending on the context that it will be embedded in. Avoid punctuation, especially trailing dots, colons, etc. Visualize your label at the start of a sentence, in a middle of a sentence or as a label in front of a widget.
In some contexts, it is desirable to specify a short label and a fully-qualified long label. For instance, "last name" vs. "employee last name". In these cases, you can use the optional "full" attribute of the Label annotation to specify the longer version of the label. The framework will look for the standard label or the full label depending on context. If the full label is not specified, the standard label will be used instead
Example:
Consider the last name property on IContact element. You could annotate it as follows:
@Label( standard = "last name", full = "contact last name" )
Applies to list properties. Declares the type of elements held by the list. See List Properties section for more information.
Applies to value properties. Declares that the annotated property typically holds strings that are longer than average. This provides a hint to whoever is processing the model. For instance, when rendering a property editor in UI, a multi-line text box would be used instead of a single-line text box.
Example:
@LongString
Applies to value properties. Declares that the property must have a value. If the property value is not set, a validation error will be generated.
Example:
@NonNullValue
Applies to properties. Declares that the value of the annotated property cannot be changed. This is commonly used when a property represents computed information. For value properties, the use of this annotation allows the setter method to be omitted. For list properties, this annotation only applies to the list itself. The properties of model elements held in the list can still be changed unless they also carry this annotation.
Example:
@ReadOnly
Applies to value properties. Used for properties that reference external resources such as DirectoryPathValue or RelativePathValue. Declares that the referenced resource must exist. If it doesn't exist, a validation error message will be generated.
Example:
@ResourceMustExist
Applies to properties. Used for attaching custom validation logic by implementing IValidator interface.
Example:
Consider the case where you want to check that a value has no whitespace characters. You would do that with a validator similar to the one presented here. The error message in this example has been simplified for example purposes. You would typically want to provide more contextual information.
import org.eclipse.sapphire.modeling.annotations.IValidator;
import org.eclipse.sapphire.modeling.util.Status;
import org.eclipse.sapphire.modeling.values.StringValue;
public class NoWhitespaceValidator implements IValidator
{
public Status validate( final Object value )
{
final String str = ( (StringValue) value ).getString( true );
if( str != null )
{
for( int i = 0, n = str.length(); i < n; i++ )
{
if( Character.isWhitespace( str.charAt( i ) ) )
{
return new Status( Status.ERROR, "sample", "Whitespace is not allowed." );
}
}
}
return Status.OK_STATUS;
}
}
Once you have implemented a custom validator, you would attach it to a property using the following annotation.
@Validator( NoWhitespaceValidator.class )
Applies to value properties that hold file path values such as RelativePathValue. Used for specifying allowed file extensions.
Example:
@ValidFileExtensions( { "jar", "zip" } )
Applies to value properties that hold file path values such as RelativePathValue. Used for specifying if the path must be a file or if it must be a directory. If it can be either, you do not need to use this annotation.
Example:
@ValidFileSystemResourceType( FileSystemResourceType.FILE )
Applies to value properties. Used in situations where the possible values are not static, but can be enumerated at runtime. A values provider is created by extending the ValuesProviderImpl class.
Example:
Consider the case where you have a property that is supposed to hold a character set name. The name must be valid on a target computer system. You don't have a way of computing the list of character sets available on the target system, but you can reference the system that the model is loaded on as a decent approximation. Since you can provide a list of values, but aren't certain that the list is 100% correct for the situation, you would override the getInvalidValueSeverity() method to specify a warning severity. The default behavior is to use error severity.
import java.nio.charset.Charset;
import java.util.List;
import java.util.ArrayList;
import org.eclipse.sapphire.modeling.util.Status;
import org.eclipse.sapphire.modeling.annotations.ValuesProviderImpl;
public class CharsetNamesValuesProvider extends ValuesProviderImpl
{
public List getValues( Object modelElement )
{
final List list = new ArrayList();
for( String charset : Charset.availableCharsets().keySet() )
{
list.add( charset );
}
return list;
}
public String getInvalidValueMessage( Object modelElement, String invalidValue )
{
return "\"" + invalidValue + "\" is not a valid character set name.";
}
@Override
public int getInvalidValueSeverity( Object modelElement, String invalidValue )
{
return Status.WARNING;
}
}
Once you have implemented a values provider, you would attach it to a property using the following annotation.
@ValuesProvider( CharsetNamesValuesProvider.class )
Binding Sapphire models to XML DOM is one of the primary usecases for the framework. As such, the framework includes a series of annotations to declare how the model elements and properties map to XML elements. These annotations are read by the framework's annotation processor at build time, which generates implementation classes for the annotated model elements.
Various XML binding annotations require a relative path through the XML document to be specified. The syntax that is used is similar in form to XPath, but is tuned to the needs of XML binding.
The syntax can be briefly described as follows:
The handling of namespaces deserves particular attention. Each element segment in a path can be assigned to a particular namespace depending on the specified prefix. If no prefix is specified, the element will have the namespace of the parent element in whose context the path is used in. If prefix is specified, it is resolved to a namespace URI by consulting the XmlNamespace, XmlNamespaces and RootXmlBinding annotations on the model element type in whose context the path is used in.
Example A:
address
address/city
address/@city
address/%city
Example B:
Suppose you have an address element, whose child elements belong to a different namespace. In your IAddress model element, you would first declare the namespace like follows:
@XmlNamespace( uri = "http://www.eclipse.org/sapphire/samples/address", prefix = "a" )
Once the namespace has been declared, you can write paths like the following in the XML binding annotations.
a:city
a:zip
Applies to boolean value properties. This annotation is a specialized form of the more general XmlBinding annotation. It used in situations where existence of a particular element should be associated with a boolean value in the semantic model.
In addition to the "path" attribute, this annotation also has "treatExistenceAsValue" parameter (which defaults to false) and "valueWhenPresent" parameter which (defaults to true).
Example A:
Consider the case where the XML schema for address in a contacts database has a "use-for-mailing" element which doesn't have any content, but is used to flag if the address is a mailing address. You could use the following annotation to establish this binding to a boolean "PreferedForMailing" property.
@BooleanPropertyXmlBinding( path = "use-for-mailing", treatExistenceAsValue = true )
Example B:
Now consider a variant of the scenario presented in Example A. You want to expose the same "PreferedForMailing" property in the semantic model, but your schema has a "not-for-mailing" element that flags the opposite value. The "valueWhenPresent" parameter helps you in this case.
@BooleanPropertyXmlBinding( path = "not-for-mailing", treatExistenceAsValue = true, valueWhenPresent = false )
Applies to interfaces that extend IModelElement or IRemovableModelElement. The main purpose of this annotation is to indicate to the Sapphire annotation processor that you wish to generate an implementation class for this interface that binds to XML DOM.
The optional "packageName" parameter can be used to specify the package that will be assigned to the generated implementation class for this interface. By default, the package name will "[interface-package].internal".
If the XML binding for all of the properties in the annotated model element will shared the same path prefix, that prefix path can be specified using the "elementPath" parameter. This is equivalent to including the prefix in all the individual bindings, but may result in slightly more optimal implementation.
If the annotated model element is non-removable, the "singleton" parameter must be used and set to true. This parameter may eventually be removed.
The optional "baseClass" parameter can be used to provide an alternate base class for the implementation to extend. By default, the implementation class will extend AbstractModelElementForXml class. This facility is useful if your model element interface includes additional methods besides property accessors that you need to provide implementations for.
The annotation processor will look for XML binding annotations on every property of the marked interface. Make sure that you have annotated them all using annotations described here.
Example A:
@GenerateXmlBindingModelElementImpl
Example B:
@GenerateXmlBindingModelElementImpl( elementPath = "address", singleton = true )
Applies to interfaces that extend IModel. The main purpose of this annotation is to indicate to the Sapphire annotation processor that you wish to generate an implementation class for this interface that binds to XML DOM.
The optional "packageName" parameter can be used to specify the package that will be assigned to the generated implementation class for this interface. By default, the package name will "[interface-package].internal".
The annotation processor will look for XML binding annotations on every property of the marked interface. Make sure that you have annotated them all using annotations described here.
Example A:
@GenerateXmlBindingModelImpl
Example B:
@GenerateXmlBindingModelImpl( packageName = "sample.model.impl" )
Applies to list properties. This annotation is used for cases that are not possible to express using ListPropertyXmlBinding annotation. This typically happens when list member's type cannot be definitively determined by looking at just the XML element name. In those cases, you can implement a custom binding by writing a class that extends ModelElementListControllerForXml abstract class.
Example:
@ListPropertyCustomXmlBinding( MyCustomModelElementListController.class )
Applies to list properties. This annotation can be used to specify XML binding for both homogeneous and heterogeneous lists as long as there is a direct mapping from XML element name to model element type. If determination of model element type is more involved, then ListPropertyCustomXmlBinding annotation must be used instead.
This annotation has two attributes. The "path" attribute describes how to get to the place in the DOM tree where the XML elements that correspond to list elements are located. It can be set to "" if the list elements are direct descendents of model element's context DOM element. The second attribute is "mappings". It is used to map XML element names to model element types.
Example:
Suppose you have a property that holds a list of addresses that are associated with a contact in a contacts database. Addresses come in different forms depending on the country in question, so you have different model element types defined to handle them. You binding declaration might look something like the following in this situation.
@ListPropertyXmlBinding( path = "addresses",
mappings =
{
@ListPropertyXmlBindingMapping( element = "usa-address", type = IUsaAddress.class ),
@ListPropertyXmlBindingMapping( element = "canadian-address", type = ICanadianAddress.class )
} )
Applies to value properties. This annotation is used in cases where XML binding is so specialized that the provided declarative annotations do not have sufficient expressive power. This annotation allows you to specify a custom implementation of ValuePropertyCustomXmlBindingImpl class that will handle reading and writing of the property value.
Example:
Consider a situation where you have a "phone-number" XML element that holds strings of "(###) ###-####" format and you want to have an area code property in the model. Your custom binding for the area code property might look like the following implementation.
import org.eclipse.sapphire.modeling.xml.annotations.ValuePropertyCustomXmlBindingImpl;
import org.w3c.dom.Element;
public class AreaCodeBinding extends ValuePropertyCustomXmlBindingImpl
{
public String read( final Object /* IModelElement */ element,
final Object /* ValueProperty */ property )
{
final PhoneNumber pnModelElement = (PhoneNumber) element;
final Element el = pnModelElement.getXmlElement( false );
final String pnStr = pnModelElement.getChildElementText( el, "number" );
final ParsedPhoneNumber pn = new ParsedPhoneNumber( pnStr );
return pn.areaCode;
}
public void write( final Object /* IModelElement */ element,
final Object /* ValuePropertry */ property,
final String value )
{
final PhoneNumber pnModelElement = (PhoneNumber) element;
final Element el = pnModelElement.getXmlElement( false );
final String pnStr = pnModelElement.getChildElementText( el, "number" );
final ParsedPhoneNumber pn = new ParsedPhoneNumber( pnStr );
pn.areaCode = value;
pnModelElement.setChildElementText( el, "number", pn.toString(), true );
}
private static class ParsedPhoneNumber
{
public String areaCode;
public String localNumber;
public ParsedPhoneNumber( final String phoneNumber )
{
this.areaCode = null;
this.localNumber = null;
if( phoneNumber != null && phoneNumber.startsWith( "(" ) )
{
final int closingParen = phoneNumber.indexOf( ')' );
if( closingParen != -1 )
{
this.areaCode = phoneNumber.substring( 1, closingParen );
if( closingParen + 1 < phoneNumber.length() )
{
this.localNumber = phoneNumber.substring( closingParen + 1 ).trim();
}
}
}
if( this.areaCode == null )
{
this.localNumber = phoneNumber;
}
}
public String toString()
{
if( this.areaCode == null && this.localNumber == null )
{
return null;
}
else
{
final StringBuilder buf = new StringBuilder();
if( this.areaCode != null && this.areaCode.length() > 0 )
{
buf.append( '(' );
buf.append( this.areaCode );
buf.append( ')' );
}
if( this.localNumber != null )
{
if( buf.length() > 0 )
{
buf.append( ' ' );
}
buf.append( this.localNumber );
}
return buf.toString();
}
}
}
}
Once you have implement a custom binding, you can attach it to the property with the following annotation. The code generator will take care of the rest.
@ValuePropertyCustomXmlBinding( AreaCodeBinding.class )
Applies to value properties and element properties. Associates the property with the XML element specified by the path parameter. For value properties, the text of the specified element is used for value data. For element properties, the XML element itself is used to establish a new binding context for a child model element.
Example A:
@XmlBinding( path = "first-name" )
Example B:
@XmlBinding( path = "name/first" )
Applies to model elements. Whenever XML Paths use namespace prefixes, the utilized namespaces must be declared. This allows the system to resolve the prefix that is used in a path to the corresponding namespace URI. A namespace declaration is only active within the scope of a given model element. If multiple namespaces need to be declared, you can use XmlNamespaces annotation.
Example:
@XmlNamespace( uri = "http://www.eclipse.org/sapphire/samples/address", prefix = "a" )
Applies to model elements. Whenever XML Paths use namespace prefixes, the utilized namespaces must be declared. This allows the system to resolve the prefix that is used in a path to the corresponding namespace URI. A namespace declaration is only active within the scope of a given model element. If only one namespace needs to be declared for a given model element, you can use the XmlNamespace annotation instead for more compact syntax.
Example:
@XmlNamespaces
(
{
@XmlNamespace( uri = "http://www.eclipse.org/sapphire/samples/contacts", prefix = "c" ),
@XmlNamespace( uri = "http://www.eclipse.org/sapphire/samples/address", prefix = "a" )
}
)