Sapphire Developer Guide > Releases > 0.7

Enhancements in Sapphire 0.7

  1. Key Enhancements
    1. Diagram Node Shape Language
    2. On-Demand Element Compilation
    3. Consolidated Conversions
    4. Extensible Persistent State
    5. Improved Localization for Java Code
    6. List Index Facility
    7. Property Instance Objects
  2. Core
    1. Element
    2. ElementHandle
    3. ElementList
    4. ElementType
    5. Property
    6. ReferenceValue
    7. RequiredConstraintService
    8. Result
    9. Transient
    10. Value
    11. Root Service Context
    12. Service Registration Without a Factory
    13. New Conversions
    14. Ordered Possible Values
    15. Simpler Date Serialization
    16. LoggingService
  3. Expression Language
    1. Functions as Properties
    2. Overloaded Functions
    3. Use for Validation
    4. Use in @Required
    5. Use in With Directive Label
    6. New Functions and Properties
      1. Absolute
      2. Content
      3. Enabled
      4. EndsWith
      5. Fragment
      6. Head
      7. Index
      8. Matches
      9. Message
      10. Parent
      11. Part
      12. Severity
      13. Size
      14. StartsWith
      15. State
      16. Tail
      17. Text
      18. This
      19. Validation
  4. Forms
    1. Section Reference
    2. Improved Date Support
    3. Color Browsing
    4. Conditional Wizard Pages
    5. Nested Properties in List Property Editor
    6. Radio Buttons with Images
    7. With Directive Label
  5. Diagrams
    1. ConnectionService
  6. Miscellaneous
    1. JavaIdentifier
    2. ImageData
    3. Modernized Popups
    4. CreateWorkspaceFileOp
    5. PropertyEditorPart
    6. WithPart

Diagram Node Shape Language

Develop complex diagrams as easily as forms with a new diagram node shape language.

The language is composed of six primitives.

On-Demand Element Compilation

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.

  1. Easier getting started process since customizing the build to compile implementation classes is no longer necessary
  2. Smaller footprint for applications as implementation classes do not need to be distributed and stored
  3. Faster element instantiation as the bytecode generator is faster than most disk systems

Consolidated Conversions

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

Extensible Persistent State

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>

Improved Localization for Java Code

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.

Before After
public class Validator
{
    public String validate( Integer value, Integer max )
    {
        if( value == null )
        {
            return Resources.mustBeSpecifiedMessage;
        }
        else if( max != null && value.intValue() > max.intValue() )
        {
            return NLS.bind( Resources.mustNotBeLargerThanMessage, max );
        }

        return null;
    }

    private static final class Resources extends NLS 
    {
        public static String mustBeSpecifiedMessage;
        public static String mustNotBeLargerThanMessage;

        static 
        {
            initializeMessages( Validator.class.getName(), Resources.class );
        }
    }
}

# Content of Validator.properties file

mustBeSpecifiedMessage = Value must be specified.
mustNotBeLargerThanMessage = Value must not be larger than {0}.
public class Validator
{
    @Text( "Value must be specified." )
    private static LocalizableText mustBeSpecifiedMessage;

    @Text( "Value must not be larger than {0}." )
    private static LocalizableText mustNotBeLargerThanMessage;

    static
    {
        LocalizableText.init( Validator.class );
    }

    public String validate( Integer value, Integer max )
    {
        if( value == null )
        {
            return mustBeSpecifiedMessage.text();
        }
        else if( max != null && value.intValue() > max.intValue() )
        {
            return mustNotBeLargerThanMessage.format( max );
        }

        return null;
    }
}

List Index Facility

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

Property Instance Objects

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

Element

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

ElementHandle

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

ElementList

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

ElementType

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

Property

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

ReferenceValue

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

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

Result

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

Transient

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

Value

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

Root Service Context

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>

Service Registration Without a Factory

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>

New Conversions

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

Ordered Possible Values

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
}

Simpler Date Serialization

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

LoggingService

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>

EL Functions as Properties

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 }

Overloaded Functions in EL

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>

Use EL for Validation

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 @Required

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 EL in With Directive Label

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>

Absolute Function

Returns the absolute path of a value for properties with a RelativePathService.

${ Path.Absolute }

Content Function

Returns the content of a value or a transient. For value properties, the default is taken into account, if applicable.

${ PurchaseOrder.FulfillmentDate.Content }

Enabled Function

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>

EndsWith Function

Tests if a string ends with the specified suffix.

${ Path.EndsWith( ".png" ) }

Fragment Function

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

Head Function

Returns a fragment of a string starting at the beginning and not exceeding the specified length.

${ Value.Head( 3 ) }
${ Head( "abcdef", 3 ) }

Index Function

Determines the index of a model element within its parent list.

${ This.Index }

Matches Function

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]*" ) }

Message Function

Returns the message from a validation result.

${ PurchaseOrder.FulfillmentDate.Validation.Message }

Parent Function

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 }

Part Function

Returns the context part.

${ Part.Validation.Severity }

Severity Function

Returns the severity of a validation result.

${ PurchaseOrder.FulfillmentDate.Validation.Severity }

Size Function

Determines the size of a collection, a map, an array or a string.

${ PurchaseOrder.Entries.Size }
${ PurchaseOrder.BillingInformation.Name.Size }
${ Size( "abcdef" ) }

StartsWith Function

Tests if a string starts with the specified prefix.

${ Path.StartsWith( ".." ) }

State Function

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>

Tail Function

Returns a fragment of a string starting at the end and not exceeding the specified length.

${ Value.Tail( 3 ) }
${ Tail( "abcdef", 3 ) }

Text Function

Returns the text of a value, taking into account the default, if applicable.

${ PurchaseOrder.FulfillmentDate.Text }

This Property

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 }

Validation Function

Returns the validation result of a property or a part.

${ PurchaseOrder.FulfillmentDate.Validation }
${ Part.Validation }

Section Reference

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>

Improved Date Support

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.

Color Browsing

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

Conditional Wizard Pages

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>

Nested Properties in List Property Editor

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>

Radio Buttons with Images

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

With Directive Label

The handling of labels by the with directive is now consistent with the property editor.

  1. The label can be shown for any presentation style
  2. The property label is used by default, but this can be overridden in sdef
  3. EL can be used when overriding the label in sdef
  4. An explicit show label flag in sdef provides developer with control over label visibility

ConnectionService

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

JavaIdentifier

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 [a-zA-Z_$][a-zA-Z0-9_$]* pattern.

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

ImageData

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

Modernized Popups

The presentation of popups has been refreshed to have less pronounced rounding of the corners.

CreateWorkspaceFileOp

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

PropertyEditorPart
{
    String label()
    String label( CapitalizationType capitalizationType, boolean includeMnemonic )
}

WithPart

The with directive has an improved handling of labels.

WithPart
{
    String label()
    String label( CapitalizationType capitalizationType, boolean includeMnemonic )
}