Develop complex diagrams as easily as forms with a new diagram node shape language.
The language is composed of six primitives.
In past releases, Sapphire used a Java annotation processor linked to @GenerateImpl annotation to produce implementation classes for model element interfaces at build time. This system has been replaced by on-demand compilation straight to Java bytecode. When application code instantiates an element for the first time, Sapphire will automatically compile it and use the compiled result for the duration of the JVM instance.
This approach has several advantages.
ValueSerializationService, AdapterService and EL's TypeCast API have been consolidated with ConversionService. The result is a single API for implementing conversions. Many of the basic conversions are now available in the root service context, opening the door for novel uses.
Example
Integer number = Sapphire.service( MasterConversionService.class ).convert( "123", Integer.class );
Example
In this example, the generics are used in conjunction with Sapphire's library of conversions to implement the pattern where the type of the default value controls the return type. This pattern is typically implemented as a less flexible series of overloaded methods for various types.
public class AttributesContainer
{
private final Map<String,String> attributes = new HashMap<String,String>();
public <T> T getAttribute( final String name, final T def )
{
if( name == null )
{
throw new IllegalArgumentException();
}
Object value = this.attributes.get( name );
if( value != null && def != null && def != String.class )
{
value = Sapphire.service( MasterConversionService.class ).convert( value, def.getClass() );
}
if( value == null )
{
value = def;
}
return (T) value;
}
public String getAttribute( final String name )
{
return (String) getAttribute( name, null );
}
public void setAttribute( final String name, final Object value )
{
if( name == null )
{
throw new IllegalArgumentException();
}
if( value == null )
{
this.attributes.remove( name );
}
else
{
final String string = Sapphire.service( MasterConversionService.class ).convert( value, String.class );
this.attributes.put( name, string );
}
}
}
Editor pages are able to persist user interface state between sessions independent of the data that is being edited. What state is persisted is dependent on editor page type. Two common examples of persistent state are sizing of resizable elements and selection. The persistent state is now extensible, allowing adopters to persist custom data.
The recommended approach is to extend the page's persistent state element type to add custom properties. The custom element type for persistent state is specified in sdef.
Example
In the catalog sample, a toggle action controls whether the manufacturer name is shown in the catalog item label. The state of this toggle is persisted by extending the state of the editor page. The persistent state is also used for communication between the toggle and the item label.
public interface CatalogEditorPageState extends MasterDetailsEditorPageState
{
ElementType TYPE = new ElementType( CatalogEditorPageState.class );
// *** ShowManufacturer ***
@Type( base = Boolean.class )
@DefaultValue( text = "false" )
ValueProperty PROP_SHOW_MANUFACTURER = new ValueProperty( TYPE, "ShowManufacturer" );
Value<Boolean> getShowManufacturer();
void setShowManufacturer( String value );
void setShowManufacturer( Boolean value );
}
The custom state element type is attached to the editor page in sdef.
<editor-page>
<persistent-state-element-type>org.eclipse.sapphire.samples.catalog.CatalogEditorPageState</persistent-state-element-type>
</editor-page>
The toggle action handler only interacts with the editor page state.
public final class ShowManufacturerActionHandler extends SapphireActionHandler
{
private CatalogEditorPageState state;
@Override
public void init( final SapphireAction action, final ActionHandlerDef def )
{
super.init( action, def );
this.state = (CatalogEditorPageState) getPart().nearest( SapphireEditorPagePart.class ).state();
final Listener listener = new FilteredListener<PropertyContentEvent>()
{
@Override
protected void handleTypedEvent( final PropertyContentEvent event )
{
setChecked( ShowManufacturerActionHandler.this.state.getShowManufacturer().getContent() );
}
};
this.state.attach( listener, CatalogEditorPageState.PROP_SHOW_MANUFACTURER );
setChecked( this.state.getShowManufacturer().getContent() );
attach
(
new FilteredListener<DisposeEvent>()
{
@Override
protected void handleTypedEvent( final DisposeEvent event )
{
ShowManufacturerActionHandler.this.state.detach( listener, CatalogEditorPageState.PROP_SHOW_MANUFACTURER );
}
}
);
}
@Override
protected Object run( final SapphireRenderingContext context )
{
this.state.setShowManufacturer( ! this.state.getShowManufacturer().getContent() );
return null;
}
}
The toggle action and its handler are defined in sdef.
<editor-page>
<action>
<id>Sample.ShowManufacturer</id>
<label>Show Manufacturer</label>
<image>ShowManufacturer.png</image>
<type>TOGGLE</type>
<context>Sapphire.EditorPage</context>
<location>before:Sapphire.Outline.Hide</location>
</action>
<action-handler>
<action>Sample.ShowManufacturer</action>
<id>Sample.ShowManufacturer</id>
<impl>ShowManufacturerActionHandler</impl>
</action-handler>
</editor-page>
Finally, the content outline node label for a catalog item is defined using an expression that reads the editor page state to determine whether to include the manufacturer in the label. The label automatically updates when any of the properties utilized in the expression are changed.
<node-factory>
<property>Items</property>
<case>
<label>${ Name == null ? "<item>" : ( State().ShowManufacturer && Manufacturer != null ? Concat( Manufacturer, " ", Name ) : Name ) }</label>
</case>
</node-factory>
Alternatively, custom state can be stored as arbitrary key-value pairs without extending the persistent state element. All of the system-provided state element types include an Attributes property for this purpose. To make it easier to work with the Attributes property, methods are provided to read and write attributes by name. These methods leverage all conversions known to Sapphire, so it is typically not necessary to manually convert the values to and from a string.
This approach should only be used in situations when extending the persistent state element is not practical or possible. State stored as attributes is harder to access. For instance, unlike actual properties, attributes cannot be directly accessed from EL.
Example
In the catalog sample, a toggle action controls whether the catalog items are color-coded by manufacturer. The state of this toggle is persisted as an attribute.
public final class ShowManufacturerColorActionHandler extends SapphireActionHandler
{
public static final String ATTRIBUTE = "ColorCode";
private CatalogEditorPageState state;
@Override
public void init( final SapphireAction action, final ActionHandlerDef def )
{
super.init( action, def );
this.state = (CatalogEditorPageState) getPart().nearest( SapphireEditorPagePart.class ).state();
final Listener listener = new FilteredListener<PropertyContentEvent>()
{
@Override
protected void handleTypedEvent( final PropertyContentEvent event )
{
setChecked( ShowManufacturerColorActionHandler.this.state.getAttribute( ATTRIBUTE, false ) );
}
};
this.state.attach( listener, CatalogEditorPageState.PROP_ATTRIBUTES.getName() + "/*" );
setChecked( this.state.getAttribute( ATTRIBUTE, false ) );
attach
(
new FilteredListener<DisposeEvent>()
{
@Override
protected void handleTypedEvent( final DisposeEvent event )
{
ShowManufacturerColorActionHandler.this.state.detach( listener, CatalogEditorPageState.PROP_ATTRIBUTES.getName() + "/*" );
}
}
);
}
@Override
protected Object run( final SapphireRenderingContext context )
{
this.state.setAttribute( ATTRIBUTE, ! this.state.getAttribute( ATTRIBUTE, false ) );
return null;
}
}
The toggle action and its handler are defined in sdef.
<editor-page>
<action>
<id>Sample.ShowManufacturerColor</id>
<label>Color Code Manufacturers</label>
<image>ItemPurple.png</image>
<type>TOGGLE</type>
<context>Sapphire.EditorPage</context>
<location>after:Sample.ShowManufacturer</location>
<location>before:Sapphire.Outline.Hide</location>
</action>
<action-handler>
<action>Sample.ShowManufacturerColor</action>
<id>Sample.ShowManufacturerColor</id>
<impl>ShowManufacturerColorActionHandler</impl>
</action-handler>
</editor-page>
A custom EL function is used to read the state attribute and to derive a color code item image based on the manufacturer.
public final class ItemImageFunction extends Function
{
private final ImageData IMAGE_GENERIC = ImageData.createFromClassLoader( ItemImageFunction.class, "Item.png" );
private final ImageData[] IMAGES =
{
ImageData.createFromClassLoader( ItemImageFunction.class, "ItemBlue.png" ),
ImageData.createFromClassLoader( ItemImageFunction.class, "ItemGreen.png" ),
ImageData.createFromClassLoader( ItemImageFunction.class, "ItemOrange.png" ),
ImageData.createFromClassLoader( ItemImageFunction.class, "ItemPurple.png" ),
ImageData.createFromClassLoader( ItemImageFunction.class, "ItemRed.png" ),
ImageData.createFromClassLoader( ItemImageFunction.class, "ItemTurquoise.png" ),
ImageData.createFromClassLoader( ItemImageFunction.class, "ItemYellow.png" )
};
@Override
public String name()
{
return "CatalogItemImage";
}
@Override
public FunctionResult evaluate( final FunctionContext context )
{
if( context instanceof PartFunctionContext )
{
final SapphirePart part = ( (PartFunctionContext) context ).part();
final MasterDetailsEditorPagePart page = part.nearest( MasterDetailsEditorPagePart.class );
if( page != null )
{
final Element element = part.getLocalModelElement();
if( element instanceof Item )
{
final Item item = (Item) element;
final MasterDetailsEditorPageState state = page.state();
return new FunctionResult( this, context )
{
private Listener listener;
@Override
protected void init()
{
this.listener = new FilteredListener<PropertyContentEvent>()
{
@Override
protected void handleTypedEvent( final PropertyContentEvent event )
{
refresh();
}
};
state.attach( this.listener, MasterDetailsEditorPageState.PROP_ATTRIBUTES.getName() + "/*" );
element.attach( this.listener, Item.PROP_MANUFACTURER );
}
@Override
protected Object evaluate()
{
final boolean color = state.getAttribute( ShowManufacturerColorActionHandler.ATTRIBUTE, false );
if( color )
{
final String manufacturer = item.getManufacturer().getContent();
final int hashCode = ( manufacturer == null ? 0 : manufacturer.hashCode() );
final int index = abs( hashCode ) % IMAGES.length;
return IMAGES[ index ];
}
else
{
return IMAGE_GENERIC;
}
}
@Override
public void dispose()
{
super.dispose();
if( this.listener != null )
{
state.detach( this.listener, MasterDetailsEditorPageState.PROP_ATTRIBUTES.getName() + "/*" );
element.detach( this.listener, Item.PROP_MANUFACTURER );
this.listener = null;
}
}
};
}
}
}
throw new FunctionException( "CatalogItemImage() function cannot be used in this context.");
}
}
The CatalogItemImage() function is registered as a Sapphire extension.
<extension>
<function>
<name>CatalogItemImage</name>
<impl>org.eclipse.sapphire.samples.catalog.ItemImageFunction</impl>
</function>
</extension>
Finally, the content outline node image for a catalog item is defined using a simple expression that references the CatalogItemImage() function.
<node-factory>
<property>Items</property>
<case>
<image>${ CatalogItemImage() }</image>
</case>
</node-factory>
Within Java code, such as an implementation of a service, Sapphire has previously relied on the NLS class copied from Eclipse platform. The developer experience has been improved.
The static fields holding localizable text are able to exist in any class, not just a dedicated resource bundle class.
Formatting messages using localizable text as a template is more fluid.
Resources.message.format( x, y )
vs NLS.bind( Resources.message, x, y )
The original text can be specified using an annotation, concentrating text closer to the point of use and avoiding numerous problems associated with maintaining text in discrete resource files. When time comes to do the translation, an annotation processor or a similar build system can be used to extract text into resource files. Only the translated versions of the resource files need to be distributed. The original text will be read from the annotations at runtime.
Before | After |
---|---|
|
|
The scalability and performance of some features can benefit from constant time lookup of list entries based on the value of a member property.
A list can have one or more indexes that are created on first request. Once created, an index is shared by all consumers of the list and updates itself automatically. The index can also notify listeners when it changes.
Index<T extends Element>
{
ElementList<T> list()
ValueProperty property()
T element( String key )
Set<T> elements( String key )
attach( Listener listener )
detach( Listener listener )
}
ElementList<T extends Element>
{
Index<T> index( ValueProperty property )
Index<T> index( String property )
}
A quick lookup is easy to write.
Task task = repository.getTasks().index( "Id" ).element( "1234" );
Multiple elements that share the same key value can be retrieved as a group.
List<Task> tasks = repository.getTasks().index( "Component" ).elements( "SDK" );
Listening for changes to the index as opposed to the whole list can help reduce the number of times an expensive operation is performed.
Index<Task> index = repository.getTasks().index( "Component" );
Set<Task> tasks = index.elements( "SDK" );
Listener listener = new Listener()
{
@Override
public void handle( Event event )
{
// Do something when the index has changed.
}
}
index.attach( listener );
...
index.detach( listener );
Each property instance is now exposed as an object. This makes it easier to pass them around. Previously, an element instance and a property name was necessary to refer to a property instance.
Example
In this example, all integer value properties within an element are incremented.
for( Property property : element.properties( "*" ) )
{
if( property instanceof Value )
{
Object content = value.content();
if( content instanceof Integer )
{
value.write( ( (Integer) content ).intValue() + 1 );
}
}
}
Clear all properties in an element using the new method.
Element
{
void clear()
}
A property can now be looked up by a path. Previously, only lookup by name was available. In addition to this, the result of the lookup is a Property object that represents an instance of a property within an element.
Element
{
SortedSet<Property> properties()
Property property( String path )
Property property( ModelPath path )
Property property( ModelProperty property )
}
Among objects returned by property getters (Value, Transient, ElementHandle and ElementList), ElementHandle was the only one that had no API to retrieve the parent property.
ElementHandle
{
ElementProperty property()
}
For convenience, type can be specified using the type's class to avoid having to cast the result.
ElementHandle
{
T content()
T content( boolean force )
T content( boolean force, ElementType type )
<C extends Element> C content( boolean force, Class<C> cl )
}
In certain situations, it is useful to be able to reference a fully typed ElementHandle class object. One such scenario occurs when using the context lookup method as part of a service implementation. If context lookup is performed using ElementHandle.class, the resulting object reference is a raw ElementHandle. Trying to assign it to a typed ElementHandle results in an unchecked cast warning. A method has been added to make it easy to reference a fully typed ElementHandle class object.
ElementHandle
{
static <TX> Class<ElementHandle<TX>> of( Class<TX> type )
}
Example
public class ExampleValidationService extends ValidationService
{
@Override
public Status validate()
{
ElementHandle<Item> item = context( ElementHandle.of( Item.class ) );
...
}
}
In certain situations, it is useful to be able to reference a fully typed ElementList class object. One such scenario occurs when using the context lookup method as part of a service implementation. If context lookup is performed using ElementList.class, the resulting object reference is a raw ElementList. Trying to assign it to a typed ElementList results in an unchecked cast warning. A method has been added to make it easy to reference a fully typed ElementList class object.
ElementList
{
static <TX> Class<ElementList<TX>> of( Class<TX> type )
}
Example
public class ExampleValidationService extends ValidationService
{
@Override
public Status validate()
{
ElementList<Item> items = context( ElementList.of( Item.class ) );
...
}
}
A property can now be looked up by a path. Previously, only lookup by name was available.
ElementType
{
<T extends ModelProperty> T property( String path )
<T extends ModelProperty> T property( ModelPath path )
}
The new Property class represents an instance of a property within an element. Many of the operations performed on an element that require a property to be supplied can now be performed on the property instance. A property instance can found using methods on Element.
Property
{
Element root()
Element element()
PropertyDef definition()
String name()
<T> T nearest( Class<T> type )
void clear()
void copy( Element source )
boolean empty()
boolean enabled()
Status validation()
void refresh()
<S extends Service> S service( Class<S> type )
<S extends Service> List<S> services( Class<S> type )
void attach( Listener listener )
void attach( Listener listener, String path )
void attach( Listener listener, ModelPath path )
void detach( Listener listener )
void detach( Listener listener, String path )
void detach( Listener listener, ModelPath path )
boolean disposed()
}
In certain situations, it is useful to be able to reference a fully typed ReferenceValue class object. One such scenario occurs when using the context lookup method as part of a service implementation. If context lookup is performed using ReferenceValue.class, the resulting object reference is a raw ReferenceValue. Trying to assign it to a typed ReferenceValue results in an unchecked cast warning. A method has been added to make it easy to reference a fully typed ReferenceValue class object.
ReferenceValue
{
static <RX,TX> Class<ReferenceValue<RX,TX>> of( Class<RX> referenceType, Class<TX> targetType )
}
Example
public class ExampleValidationService extends ValidationService
{
@Override
public Status validate()
{
ReferenceValue<JavaTypeName,JavaType> implementation = context( ReferenceValue.of( JavaTypeName.class, JavaType.class ) );
...
}
}
RequiredConstraintService determines whether a value or an element property is required to have content. Most frequently specified via an @Required annotation, which now supports EL for specifying custom semantics.
Example
public class CustomRequiredConstraintService extends RequiredConstraintService
{
@Override
protected void initRequiredConstraintService()
{
// Optionally register listeners to invoke refresh method when the required constraint
// may need to be updated.
}
@Override
protected Boolean compute()
{
...
}
@Override
public void dispose()
{
super.dispose();
// Remove any listeners that were added during initialization.
}
}
@Service( impl = CustomRequiredConstraintService.class )
ValueProperty PROP_CATEGORY = new ValueProperty( TYPE, "Category" );
Value<String> getCategory();
void setCategory( String value );
When defining a function, the developer needs to decide if a failure will be signaled by an exception or a null return, but the developer of the function is often not in a position to know which approach will be more convenient for the users of the function. The Result class allows this decision to be left to the function caller in a way that is intuitive and does not contribute to API bloat.
Example
Consider a function that looks up a purchase order by id. Notice that when the purchase order is not found, an exception is created, but not thrown.
Result<PurchaseOrder> findPurchaseOrder( String id )
{
PurchaseOrder po = this.orders.get( id );
if( po == null )
{
return Result.failure( new IllegalArgumentException() );
}
return Result.success( po );
}
A function caller that prefers a null return would call optional() on the function result and never see the exception.
PurchaseOrder po = findPurchaseOrder( id ).optional();
Similarly, a function caller that prefers an exception would call required() to get the desired behavior.
PurchaseOrder po = findPurchaseOrder( id ).required();
In certain situations, it is useful to be able to reference a fully typed Transient class object. One such scenario occurs when using the context lookup method as part of a service implementation. If context lookup is performed using Transient.class, the resulting object reference is a raw Transient. Trying to assign it to a typed Transient results in an unchecked cast warning. A method has been added to make it easy to reference a fully typed Transient class object.
Transient
{
static <TX> Class<Transient<TX>> of( Class<TX> type )
}
Example
public class ExampleValidationService extends ValidationService
{
@Override
public Status validate()
{
Transient<IProject> project = context( Transient.of( IProject.class ) );
...
}
}
In certain situations, it is useful to be able to reference a fully typed Value class object. One such scenario occurs when using the context lookup method as part of a service implementation. If context lookup is performed using Value.class, the resulting object reference is a raw Value. Trying to assign it to a typed Value results in an unchecked cast warning. A method has been added to make it easy to reference a fully typed Value class object.
Value
{
static <TX> Class<Value<TX>> of( Class<TX> type )
}
Example
public class ExampleValidationService extends ValidationService
{
@Override
public Status validate()
{
Value<Integer> value = context( Value.of( Integer.class ) );
...
}
}
Certain types of services, such as many ConversionService implementations, are useful across different service contexts. Services that are part of the new root service context are visible to all other contexts.
Sapphire
{
static <S extends Service> S service( Class<S> type )
static <S extends Service> List<S> services( Class<S> type )
static synchronized ServiceContext services()
}
Integer number = Sapphire.service( MasterConversionService.class ).convert( "123", Integer.class );
<extension xmlns="http://www.eclipse.org/sapphire/xmlns/extension">
<service>
<id>Sapphire.ConversionService.StringToInteger</id>
<description>ConversionService implementation for String to Integer conversions.</description>
<implementation>org.eclipse.sapphire.internal.StringToIntegerConversionService</implementation>
<context>Sapphire</context>
</service>
</extension>
Registering services through Sapphire extension system has been made even easier by replacing a ServiceFactory with a direct reference to the implementation class and an optional ServiceCondition.
<extension xmlns="http://www.eclipse.org/sapphire/xmlns/extension">
<service>
<id>Sapphire.ConversionService.StringToInteger</id>
<description>ConversionService implementation for String to Integer conversions.</description>
<implementation>org.eclipse.sapphire.internal.StringToIntegerConversionService</implementation>
<context>Sapphire</context>
</service>
</extension>
The set of available conversions has been expanded. The new conversions can be used in a variety of contexts that draw upon ConversionService implementations, such as when using MasterConversionService or ModelElement.adapt() API.
Source | Target |
---|---|
org.eclipse.sapphire.modeling.ModelElement | org.w3c.dom.Document |
org.eclipse.sapphire.modeling.ModelElement | org.w3c.dom.Element |
org.eclipse.sapphire.modeling.ModelElement | org.eclipse.sapphire.modeling.xml.XmlElement |
org.eclipse.sapphire.modeling.xml.XmlResource | org.w3c.dom.Document |
org.eclipse.sapphire.modeling.xml.XmlResource | org.w3c.dom.Element |
org.eclipse.sapphire.modeling.xml.XmlResource | org.eclipse.sapphire.modeling.xml.XmlElement |
java.lang.String | org.eclipse.sapphire.java.JavaIdentifier |
Typically, possible values have no particular order and it is desirable to present them sorted. In some cases, the order is significant. New API allows these cases to be differentiated.
@PossibleValues
{
boolean ordered() default false
}
PossibleValuesService
{
boolean ordered()
}
Example
An example of ordered possible values is task severity. The severities need to be presented in the severity rank order, not in the alphabetical order.
@PossibleValues
{
values = { "blocker", "critical", "major", "normal", "minor", "trivial", "enhancement" },
ordered = true
}
Easily specify date serialization using the new @Serialization annotation.
Example
@Type( base = Date.class )
@Serialization( primary = "yyyy-MM-dd", alternative = { "MM/dd/yyyy", "MM.dd.yyyy" } )
ValueProperty PROP_DATE = new ValueProperty( TYPE, "Date" );
Value<Date> getDate();
void setDate( String value );
void setDate( Date value );
Log messages and exceptions relating to system operation.
public abstract class LoggingService extends Service
{
public final void logError( String message )
public final void logError( String message, Throwable e )
public final void logWarning( String message )
public final void log( Throwable e )
public abstract void log( Status status )
}
Example
try
{
...
}
catch( Exception e )
{
Sapphire.service( LoggingService.class ).log( e );
}
Two implementations are provided with Sapphire. One writes to the system error stream and another writes to the Eclipse platform log if the framework is running in the context of Eclipse.
If an alternate log strategy is desired, a custom LoggingService implementation can be supplied.
Example
public class ExampleLoggingService extends LoggingService
{
@Override
public void log( Status status )
{
...
}
}
<extension xmlns="http://www.eclipse.org/sapphire/xmlns/extension">
<service>
<id>ExampleLoggingService</id>
<implementation>org.eclipse.sapphire.examples.ExampleLoggingService</implementation>
<context>Sapphire</context>
<overrides>Sapphire.LoggingService.Standard</overrides>
<overrides>Sapphire.LoggingService.Platform</overrides>
</service>
</extension>
Any single argument EL function can now be accessed using property notation. Note that functions have a lower precedence than properties. If a conflict with a property is encountered, function notation must be used to disambiguate.
The following expressions are equivalent. The last variant is new for this release.
${ Size( PurchaseOrder.Entries ) }
${ PurchaseOrder.Entries.Size() }
${ PurchaseOrder.Entries.Size }
Previously, a Sapphire EL function implementations always applied to any parameter signature, regardless of arity or parameter types. That option remains, but in certain cases, it is more useful to have a different function implementation for different situations.
Function implementations can now specify their signature and are matched based on how closely they match actual parameters. Implementations with a declared signature are considered first.
Example
Without a signature, the following example function will be called for any parameter signature. The implementation is responsible for throwing FunctionException if it is unable to deal with a particular signature.
<extension xmlns="http://www.eclipse.org/sapphire/xmlns/extension">
<function>
<name>Increment</name>
<impl>org.eclipse.sapphire.examples.IncrementFunction</impl>
</function>
</extension>
With signatures specified, these next two example functions will only be called when the signature matches their declaration. Sapphire will handle picking the correct implementation and rejecting invalid signatures.
<extension xmlns="http://www.eclipse.org/sapphire/xmlns/extension">
<function>
<name>Increment</name>
<signature>
<parameter>java.math.BigInteger</parameter>
</signature>
<impl>org.eclipse.sapphire.examples.IncrementFunctionForInteger</impl>
</function>
</extension>
<extension xmlns="http://www.eclipse.org/sapphire/xmlns/extension">
<function>
<name>Increment</name>
<signature>
<parameter>java.math.BigDecimal</parameter>
</signature>
<impl>org.eclipse.sapphire.examples.IncrementFunctionForDecimal</impl>
</function>
</extension>
The new @Validation annotation allows an expression to be used to define a validation rule rather than implementing a custom ValidationService. This leads to a model that is easier to understand and maintain.
@Type( base = BigDecimal.class )
@DefaultValue( text = "0" )
@NumericRange( min = "0" )
@Validation
(
rule = "${ Discount <= Subtotal + Delivery }",
message = "Discount must not exceed subtotal plus delivery charge."
)
ValueProperty PROP_DISCOUNT = new ValueProperty( TYPE, "Discount" );
Value<BigDecimal> getDiscount();
void setDiscount( String value );
void setDiscount( BigDecimal value );
Multiple rules can be specified by using @Validations annotation, the message can be formulated using an expression, and the optional severity attribute allows the developer to make a rule failure either an error or a warning.
@Validations
(
{
@Validation
(
rule = "${ Path == null || Path.StartsWith( '/' ) }",
message = "Path \"${ Path }\" must start with a slash."
),
@Validation
(
rule = "${ Path == null || Path.StartsWith( HomePath ) }",
message = "Path \"${ Path }\" is not within the home folder.",
severity = Status.Severity.WARNING
)
}
)
ValueProperty PROP_PATH = new ValueProperty( TYPE, "Path" );
Value<String> getPath();
void setPath( String value );
Use EL in the @Required annotation to define custom semantics.
Example
In this example, the Category property is required only if the Version property is in the given range.
@Required( "${ VersionMatches( Version, '[1.0-2.1)' ) }" )
ValueProperty PROP_CATEGORY = new ValueProperty( TYPE, "Category" );
Value<String> getCategory();
void setCategory( String value );
Use the expression language when overriding the label in a with directive.
Example
<with>
<path>Spouse/PrimaryOccupation</path>
<label>${ Spouse.Name.First }'s primary occupation</label>
<case>
...
</case>
<case>
...
</case>
</with>
Returns the absolute path of a value for properties with a RelativePathService.
${ Path.Absolute }
Returns the content of a value or a transient. For value properties, the default is taken into account, if applicable.
${ PurchaseOrder.FulfillmentDate.Content }
Returns the enablement of a property.
${ PurchaseOrder.FulfillmentDate.Enabled }
In the context of a property editor, Enabled function can also be called with zero arguments. This accesses the enablement of the property editor part.
<property-editor>
<property>FormLoginPage</property>
<visible-when>${ Enabled() }</visible-when>
</property-editor>
Tests if a string ends with the specified suffix.
${ Path.EndsWith( ".png" ) }
Returns a fragment of a string. The fragment starts at the index specified by the second argument and extends to the character before the index specified by the third argument. The length of the fragment is end index minus start index.
${ Value.Fragment( 3, 6 ) }
${ Fragment( "abcdef", 0, 3 ) }
Returns a fragment of a string starting at the beginning and not exceeding the specified length.
${ Value.Head( 3 ) }
${ Head( "abcdef", 3 ) }
Determines the index of a model element within its parent list.
${ This.Index }
Determines whether a string matches a regular expression. The full semantics are specified by Java's String.matches() function.
${ Entity.Name.Matches( "[a-z][a-z0-9]*" ) }
Returns the message from a validation result.
${ PurchaseOrder.FulfillmentDate.Validation.Message }
Returns the parent of the given part. An implementation of this function for model elements was added in an earlier release.
${ Part.Parent.Validation.Severity }
${ Part.Parent.Parent.Validation.Severity }
Returns the context part.
${ Part.Validation.Severity }
Returns the severity of a validation result.
${ PurchaseOrder.FulfillmentDate.Validation.Severity }
Determines the size of a collection, a map, an array or a string.
${ PurchaseOrder.Entries.Size }
${ PurchaseOrder.BillingInformation.Name.Size }
${ Size( "abcdef" ) }
Tests if a string starts with the specified prefix.
${ Path.StartsWith( ".." ) }
Returns the root element of editor page's persistent state, allowing access to various state properties. This is particularly useful when the persistent state is extended with custom properties wired to custom actions, as it allows any EL-enabled facility to integrate with the custom state property.
In the following example, a custom state property is used to control whether content outline node label for an item in the catalog sample should include the manufacturer.
<node-factory>
<property>Items</property>
<case>
<label>
${
Name == null
? "Item"
: (
State().ShowManufacturer && Manufacturer != null
? Concat( Manufacturer, " ", Name )
: Name
)
}
</label>
</case>
</node-factory>
Returns a fragment of a string starting at the end and not exceeding the specified length.
${ Value.Tail( 3 ) }
${ Tail( "abcdef", 3 ) }
Returns the text of a value, taking into account the default, if applicable.
${ PurchaseOrder.FulfillmentDate.Text }
In situations where EL context is established by a model element, it can be useful to directly reference that element in order to pass it to functions. Mirroring Java, the context now exposes "This" property.
In this example, the expression computes the index of the context model element within its parent list.
${ This.Index }
Returns the validation result of a property or a part.
${ PurchaseOrder.FulfillmentDate.Validation }
${ Part.Validation }
Re-use section definitions across multiple node definitions in a master-details editor page.
Example
<definition>
<section>
<id>CommonSection</id>
<label>common</label>
<content>
...
</content>
</section>
<node>
<id>Node-1</id>
<label>node 1</label>
<section-ref>CommonSection</section-ref>
<section>
<label>another section</label>
<content>
...
</content>
</section>
</node>
<node>
<id>Node-2</id>
<label>node 2</label>
<section-ref>CommonSection</section-ref>
<section>
<label>another section</label>
<content>
...
</content>
</section>
</node>
</definition>
Date value properties are easier to define and the user experience is significantly improved. The formats specified by the developer using the new @Serialization annotation are visible as text overlay, in the property editor assistance popup and in the context help.
@Type( base = Date.class )
@Serialization( primary = "yyyy-MM-dd", alternative = "MM/dd/yyyy" )
ValueProperty PROP_ORDER_DATE = new ValueProperty( TYPE, "OrderDate" );
Value<Date> getOrderDate();
void setOrderDate( String value );
void setOrderDate( Date value );
The browse button opens a calendar, making it easy to quickly select the correct date.
Define a color value property using the provided Color type and Sapphire will supply a browse dialog.
@Type( base = Color.class )
ValueProperty PROP_COLOR = new ValueProperty( TYPE, "Color" );
Value<Color> getColor();
void setColor( String value );
void setColor( Color value );
Define wizards with pages that appear based on a condition.
<wizard>
<id>PurchaseComputerWizard</id>
<element-type>org.eclipse.sapphire.samples.po.PurchaseComputerOp</element-type>
<page>
<id>PurchaseComputerWizard.Importance</id>
<label>Expected Usage</label>
<description>The expected usage of the computer determines the optimal components.</description>
<content>
<property-editor>PerformanceImportance</property-editor>
<property-editor>StorageImportance</property-editor>
<property-editor>GamingImportance</property-editor>
</content>
</page>
<page>
<id>PurchaseComputerWizard.Performance</id>
<label>Performance</label>
<description>The processor and memory selection affects the overall performance of the system.</description>
<visible-when>${ PerformanceImportance == 3 }</visible-when>
<content>
...
</content>
</page>
</wizard>
Previously, only directly contained value properties could be edited by the list property editor. Now, nested value properties, accessible through one or more implied element property can be edited as well. This reduces the need to flatten the model or use workarounds, such as the page book details approach.
Example
// *** Employees ***
interface Employee extends Element
{
ElementType TYPE = new ElementType( Employee.class );
// *** Name ***
interface Name extends Element
{
ElementType TYPE = new ElementType( Name.class );
// *** First ***
ValueProperty PROP_FIRST_NAME = new ValueProperty( TYPE, "First" );
Value<String> getFirst();
void setFirst( String value );
// *** Last ***
ValueProperty PROP_LAST_NAME = new ValueProperty( TYPE, "Last" );
Value<String> getLast();
void setLast( String value );
}
@Type( base = Name.class )
ImpliedElementProperty PROP_NAME = new ImpliedElementProperty( TYPE, "Name" );
Name getName();
// *** Location ***
interface Location extends Element
{
ElementType TYPE = new ElementType( Location.class );
// *** City ***
ValueProperty PROP_CITY = new ValueProperty( TYPE, "City" );
Value<String> getCity();
void setCity( String value );
// *** Country ***
ValueProperty PROP_COUNTRY = new ValueProperty( TYPE, "Country" );
Value<String> getCountry();
void setCountry( String value );
}
@Type( base = Location.class )
ImpliedElementProperty PROP_LOCATION = new ImpliedElementProperty( TYPE, "Location" );
Location getLocation();
// *** Salary ***
@Type( base = BigDecimal.class )
ValueProperty PROP_SALARY = new ValueProperty( TYPE, "Salary" );
Value<BigDecimal> getSalary();
void setSalary( String value );
void setSalary( BigDecimal value );
}
@Type( base = Employee.class )
ListProperty PROP_EMPLOYEES = new ListProperty( TYPE, "Employees" );
ElementList<Employee> getEmployees();
<property-editor>
<property>Employees</property>
<child-property>Name/First</child-property>
<child-property>Name/Last</child-property>
<child-property>Location/City</child-property>
<child-property>Location/Country</child-property>
<child-property>Salary</child-property>
</property-editor>
<Employee>
<Name>
<First>John</First>
<Last>Smith</Last>
</Name>
<Location>
<City>Seattle</City>
<Country>USA</Country>
</Location>
<Salary>100000</Salary>
</Employee>
The radio buttons property editor presentation now uses value images, when available.
enum FileType
{
@Label( standard = "Java" )
@Image( path = "JavaFile.png" )
JAVA,
@Label( standard = "XML" )
@Image( path = "XmlFile.png" )
XML,
@Label( standard = "text" )
@Image( path = "TextFile.png" )
TEXT
}
@Type( base = FileType.class )
ValueProperty PROP_TYPE = new ValueProperty( TYPE, "Type" );
Value<FileType> getType();
void setType( String value );
void setType( FileType value );
The handling of labels by the with directive is now consistent with the property editor.
ConnectionService is responsible for listing and establishing connections in a diagram.
Typically, there is no need for the developer to implement this service as the provided StandardConnectionService uses the connection binding definitions in sdef to manage the connections. A custom implementation is only needed if sdef connection binding facilities are not sufficiently expressive or if the developer needs to customize user interaction when a connection is established. In the latter case, StandardConnectionService can be extended instead of implementing ConnectionService from scratch.
Example
In this example from the SQL Schema Editor sample, StandardConnectionService is extended to open a columns association wizard when user defines a foreign key.
public final class SqlSchemaConnectionService extends StandardConnectionService
{
@Override
public StandardDiagramConnectionPart connect( final DiagramNodePart node1, final DiagramNodePart node2, final String connectionType )
{
final StandardDiagramConnectionPart fkConnectionPart = super.connect( node1, node2, connectionType );
final ForeignKey fk = (ForeignKey) fkConnectionPart.getLocalModelElement();
...
final SapphireWizard wizard = new SapphireWizard( fk, DefinitionLoader.sdef( SqlSchemaEditor.class ).wizard( "DefineForeignKeyWizard" ) )
{
@Override
public boolean performCancel()
{
fkConnectionPart.remove();
return true;
}
};
final WizardDialog dialog = new WizardDialog( Display.getDefault().getActiveShell(), wizard );
dialog.open();
return ( fk.disposed() ? null : fkConnectionPart );
}
}
The JavaIdentifier class can be used to represent a legal Java identifier, such as the name of a variable,
a field or a method. Identifiers must conform to
Verification happens in the constructor, so any instance can be assumed to represent a valid identifier. This class can be used by itself or as a type of a value property.
Example
@Type( base = JavaIdentifier.class )
ValueProperty PROP_FIELD_NAME = new ValueProperty( TYPE, "FieldName" );
Value<JavaIdentifier> getFieldName();
void setFieldName( String value );
void setFieldName( JavaIdentifier value );
An ImageData can now be read from an InputStream.
ImageData
{
static Result<ImageData> readFromStream( InputStream stream )
static Result<ImageData> readFromUrl( URL url )
static Result<ImageData> readFromClassLoader( Class<?> cl, String path )
static Result<ImageData> readFromClassLoader( ClassLoader cl, String path )
}
The presentation of popups has been refreshed to have less pronounced rounding of the corners.
CreateWorkspaceFileOp now supports customizable root folder. This restricts where files can be located and helps to focus the corresponding UI. Further, the Folder and File properties now are reference values resolving to their corresponding IResource.
CreateWorkspaceFileOp
{
// *** Root ***
@Type( base = Path.class )
@Reference( target = IContainer.class )
ValueProperty PROP_ROOT = new ValueProperty( TYPE, "Root" );
ReferenceValue<Path,IContainer> getRoot();
void setRoot( String value );
void setRoot( Path value );
// *** Folder ***
@Type( base = Path.class )
@Reference( target = IContainer.class )
ValueProperty PROP_FOLDER = new ValueProperty( TYPE, "Folder" );
ReferenceValue<Path,IContainer> getFolder();
void setFolder( String value );
void setFolder( Path value );
// *** File ***
@Type( base = FileName.class )
@Reference( target = IFile.class )
ValueProperty PROP_FILE = new ValueProperty( TYPE, "File" );
ReferenceValue<FileName,IFile> getFile();
void setFile( String value );
void setFile( FileName value );
}
PropertyEditorPart
{
String label()
String label( CapitalizationType capitalizationType, boolean includeMnemonic )
}
The with directive has an improved handling of labels.
WithPart
{
String label()
String label( CapitalizationType capitalizationType, boolean includeMnemonic )
}