Sapphire Developer Guide > Releases > 0.6

Enhancements in 0.6

  1. Key Enhancements
    1. Versions and Version Constraints
    2. Minimal Generated Code
    3. Improved Entry Points
  2. Modeling
    1. Context
    2. ConversionService
    3. FileName
    4. ListenerContext
    5. MasterConversionService
    6. ModelElement
    7. ModelElementList
    8. ModelElementType
    9. PossibleValuesService
    10. @PreferDefaultValue
    11. Service
    12. ServiceContext
    13. ServiceEvent
  3. Expression Language
    1. Enabled Function
    2. Cast Through a String
    3. UpperCase Function
    4. LowerCase Function
    5. Use in More Places
  4. Forms
    1. Section Description
    2. ActuatorActionHandlerEvent
    3. ProblemsTraversalService
  5. Miscellaneous
    1. XmlUtil.convertToNamespaceFrom and convertFromNamespaceForm
    2. Enhancements to Collection Factories
    3. JdtUtil
    4. MasterDetailsContentNode
    5. SapphirePart
    6. Status
    7. Value
    8. Overwrite Existing File
    9. Dynamic Action Visibility
    10. Show Next Problem

Versions and Version Constraints

In many complex Sapphire models, it is useful to be able to constrain functionality based on a version. To simplify these scenarios, Sapphire now has native constructs for dealing with versions and version constraints.

Version - Represents a version as a sequence of long integers. In string format, it is represented as a dot-separated list of numeric segments, such as "1.2.3" or "5.7.3.2012070310003".

VersionConstraint - A boolean expression that can check versions for applicability. In string format, it is represented as a comma-separated list of specific versions, closed ranges (expressed using "[1.2.3-4.5)" syntax and open ranges (expressed using "[1.2.3" or "4.5)" syntax). The square brackets indicate that the range includes the specified version. The parenthesis indicate that the range goes up to, but does not actually include the specified version.

Sapphire.version() - Determines the version of Sapphire.

Both Version and VersionConstraint classes can be used as a type of a value property.

Example

// *** Version ***

@Type( base = Version.class )

ValueProperty PROP_VERSION = new ValueProperty( TYPE, "Version" );

Value<Version> getVersion();
void setVersion( String value );
void setVersion( Version value );

// *** VersionConstraint ***

@Type( base = VersionConstraint.class )

ValueProperty PROP_VERSION_CONSTRAINT = new ValueProperty( TYPE, "VersionConstraint" );

Value<VersionConstraint> getVersionConstraint();
void setVersionConstraint( String value );
void setVersionConstraint( VersionConstraint value );

Further, version constraints can be evaluated in an expression via a pair of new functions. The VersionMatches function takes a version as the first parameter, a version constraint as a second parameter and returns a boolean. The SapphireVersionMatches function takes a version constraint as the sole parameter, evaluates it against Sapphire version and returns a boolean.

Example

In this example, the VersionMatches function is used to control property enablement.

// *** Provider ***

@Label( standard = "provider" )
@Enablement( expr = "${ VersionMatches( Root().Version, '[1.1' ) }" )
@XmlBinding( path = "provider" )

ValueProperty PROP_PROVIDER = new ValueProperty( TYPE, "Provider" );

Value<String> getProvider();
void setProvider( String value );

Example

In this example, the VersionMatches function is used in sdef to control visibility of a properties view page.

<properties-view>
    <page>
        <label>provider</label>
        <visible-when>${ VersionMatches( Root().Version, '[1.1' ) }</visible-when>
        <content>
            <property-editor>Provider</property-editor>
            <property-editor>
                <property>Copyright</property>
                <scale-vertically>true</scale-vertically>
            </property-editor>
        </content>
    </page>
</properties-view>

Even simpler, version compatibility can be attached to a property by using an @Since or an @VersionCompatibility annotation. This will configure enablement, validation and visibility. The @VersionCompatibilityTarget annotation works in conjunction with these annotations to specify the current version. This annotation is typically placed on the root element of the model and typically references a property that defines the version.

Example

@VersionCompatibilityTarget( version = "${ Version }", versioned = "Purchase Order" )
@GenerateImpl

public interface PurchaseOrder extends IModelElement
{
    ModelElementType TYPE = new ModelElementType( PurchaseOrder.class );

    // *** Version ***

    @Type( base = Version.class )
    @DefaultValue( text = "2.0" )

    ValueProperty PROP_VERSION = new ValueProperty( TYPE, "Version" );

    Value<Version> getVersion();
    void setVersion( String value );
    void setVersion( Version value );

    // *** Id ***

    @Required

    ValueProperty PROP_ID = new ValueProperty( TYPE, "Id" );

    Value<String> getId();
    void setId( String value );

    // *** Customer ***

    @Required

    ValueProperty PROP_CUSTOMER = new ValueProperty( TYPE, "Customer" );

    Value<String> getCustomer();
    void setCustomer( String value );

    // *** InitialQuoteDate ***

    @Type( base = Date.class )
    @Since( "1.5" )

    ValueProperty PROP_INITIAL_QUOTE_DATE = new ValueProperty( TYPE, "InitialQuoteDate" );

    Value<Date> getInitialQuoteDate();
    void setInitialQuoteDate( String value );
    void setInitialQuoteDate( Date value );

    // *** OrderDate ***

    @Type( base = Date.class )

    ValueProperty PROP_ORDER_DATE = new ValueProperty( TYPE, "OrderDate" );

    Value<Date> getOrderDate();
    void setOrderDate( String value );
    void setOrderDate( Date value );

    // *** FulfillmentDate ***

    @Type( base = Date.class )
    @Since( "2.0" )

    ValueProperty PROP_FULFILLMENT_DATE = new ValueProperty( TYPE, "FulfillmentDate" );

    Value<Date> getFulfillmentDate();
    void setFulfillmentDate( String value );
    void setFulfillmentDate( Date value );

    // *** BillingInformation ***

    @Type( base = BillingInformation.class )

    ImpliedElementProperty PROP_BILLING_INFORMATION = new ImpliedElementProperty( TYPE, "BillingInformation" );

    BillingInformation getBillingInformation();

    // *** ShippingInformation ***

    @Type( base = ShippingInformation.class )
    @Since( "2.0" )

    ImpliedElementProperty PROP_SHIPPING_INFORMATION = new ImpliedElementProperty( TYPE, "ShippingInformation" );

    ShippingInformation getShippingInformation();

    // *** Payment ***

    @Type( base = Payment.class, possible = { CreditCardPayment.class, CheckPayment.class, CashPayment.class } )
    @Label( standard = "payment" )
    @Since( "1.5" )

    ElementProperty PROP_PAYMENT = new ElementProperty( TYPE, "Payment" );

    ModelElementHandle<Payment> getPayment();
}

With the default 2.0 version, all of the purchase order fields are visible.

When user changes the version to 1.0, the fulfillment date, payment information and shipping information fields are automatically hidden. The initial quote date field remains visible because it has a value. A validation error alerts the user to the issue.

If the user chooses to resolve the version compatibility validation error by removing the initial quote date, the field automatically disappears.

If more flexibility is necessary, the annotations can be replaced by VersionCompatibilityService and VersionCompatibilityTargetService implementations.

Example

public class ExampleVersionCompatibilityService extends VersionCompatibilityService
{
    private VersionCompatibilityTargetService versionCompatibilityTargetService;
    private Listener versionCompatibilityTargetServiceListener;

    protected void initVersionCompatibilityService()
    {
        final IModelElement element = context( IModelElement.class );
        final ModelProperty property = context( ModelProperty.class );

        this.versionCompatibilityTargetService = VersionCompatibilityTargetService.find( element, property );

        this.versionCompatibilityTargetServiceListener = new Listener()
        {
            @Override
            public void handle( final Event event )
            {
                refresh();
            }
        };

        this.versionCompatibilityTargetService.attach( this.versionCompatibilityTargetServiceListener );
    }

    @Override
    protected Data compute()
    {
        final Version version = this.versionCompatibilityTargetService.version();
        final String versioned = this.versionCompatibilityTargetService.versioned();

        final boolean compatible = ...

        return new Data( compatible, version, versioned );
    }

    @Override
    public void dispose()
    {
        super.dispose();

        if( this.versionCompatibilityTargetService != null )
        {
            this.versionCompatibilityTargetService.detach( this.versionCompatibilityTargetServiceListener );
        }
    }
}
public class ExampleVersionCompatibilityTargetService extends VersionCompatibilityTargetService
{
    @Override
    protected void initContextVersionService()
    {
        // Listen on the source of the version and call refresh() when necessary.
    }

    @Override
    protected Data compute()
    {
        Version version = ...
        String versioned = ...

        return new Data( version, versioned );
    }

    @Override
    public void dispose()
    {
        super.dispose();

        // Detach any listeners attached in the initContextVersionService() method.
    }
}

Minimal Generated Code

The size of the generated model element implementation classes has been greatly reduced by shifting as much as possible into the base class provided by the framework. This reduces the disk and memory footprint of a Sapphire application.

Example

public final class BugReportImpl

    extends ModelElement
    implements BugReport

{
    public BugReportImpl( final IModelParticle parent, final ModelProperty parentProperty, final Resource resource )
    {
        super( TYPE, parent, parentProperty, resource );
    }

    public Value<String> getCustomerId()
    {
        return (Value) read( PROP_CUSTOMER_ID );
    }

    public void setCustomerId( String value )
    {
        write( PROP_CUSTOMER_ID, value );
    }

    public Value<String> getDetails()
    {
        return (Value) read( PROP_DETAILS );
    }

    public void setDetails( String value )
    {
        write( PROP_DETAILS, value );
    }

    public ModelElementList<HardwareItem> getHardware()
    {
        return (ModelElementList) read( PROP_HARDWARE );
    }

    public Value<ProductStage> getProductStage()
    {
        return (Value) read( PROP_PRODUCT_STAGE );
    }

    public void setProductStage( String value )
    {
        write( PROP_PRODUCT_STAGE, value );
    }

    public void setProductStage( final ProductStage value )
    {
        write( PROP_PRODUCT_STAGE, value );
    }

    public Value<ProductVersion> getProductVersion()
    {
        return (Value) read( PROP_PRODUCT_VERSION );
    }

    public void setProductVersion( String value )
    {
        write( PROP_PRODUCT_VERSION, value );
    }

    public void setProductVersion( final ProductVersion value )
    {
        write( PROP_PRODUCT_VERSION, value );
    }

    public Value<String> getTitle()
    {
        return (Value) read( PROP_TITLE );
    }

    public void setTitle( String value )
    {
        write( PROP_TITLE, value );
    }
}

Improved Entry Points

The entry points by which Sapphire UI is embedded into an overall SWT or Eclipse RCP application have been improved.

The new DefinitionLoader facility standardizes how sdef is accessed by entry points and is not based on OSGi assumptions.

DefinitionLoader
{
    static DefinitionLoader context( Context context )
    static DefinitionLoader context( ClassLoader loader )
    static DefinitionLoader context( Class<?> cl )

    static DefinitionLoader sdef( Class<?> cl )
    DefinitionLoader sdef( String name )

    Reference<T extends IModelElement>
    {
        T resolve()
        void dispose()
    }

    Reference<ISapphireUiDef> root()
    Reference<EditorPageDef> page()
    Reference<EditorPageDef> page( String id )
    Reference<WizardDef> wizard()
    Reference<WizardDef> wizard( String id )
    Reference<DialogDef> dialog()
    Reference<DialogDef> dialog( String id )
    Reference<FormComponentDef> form()
    Reference<FormComponentDef> form( String id )
}

Example

SapphireDialog dialog = new SapphireDialog
(
    shell, element,
    DefinitionLoader.context( getClass() ).sdef( "SecurityDialogs" ).dialog( "PasswordDialog" )
);

SapphireEditorForXml can now be parameterized with a reference to sdef directly in the plugin.xml declaration. In the past, one needed to subclass it just to reference sdef.

<extension point="org.eclipse.ui.editors">
  <editor
    id="org.eclipse.sapphire.samples.contacts.ContactRepositoryEditor"
    name="Contact Repository Editor (Sapphire Sample)"
    icon="org/eclipse/sapphire/samples/SapphireFile.png"
    filenames="contacts.xml"
    default="true">
    <class class="org.eclipse.sapphire.ui.swt.xml.editor.SapphireEditorForXml">
      <parameter name="sdef" value="org.eclipse.sapphire.samples.contacts.ContactRepositoryEditor"/>
    </class>
  </editor>
</extension>

A new entry point has been added for wizards that create workspace files.

CreateWorkspaceFileOp - Base element type that should be extended to specify file constraints and to further parameterize the creation.

CreateWorkspaceFileWizard - Corresponding wizard implementation that can be referenced declaratively in an extension contributing the wizard or subclassed.

CreateWorkspaceFileForm.sdef - A standard form to be used on the first page of a workspace file creation wizard in order to gather folder and file name input from the user.

@WorkspaceFileType - An annotation used to link the file creation operation with root element type of the created file (in cases where there is a Sapphire model for the file). If this annotation is present, the default execution logic of CreateWorkspaceFileOp will instantiate the model for the created file, apply initial values (as specified by the model) and save the result. For instance, for an XML file, these extra steps will ensure that the file at least has the XML declaration line and the appropriate root element.

Example

@WorkspaceFileType( ContactRepository.class )
@GenerateImpl

public interface CreateContactRepositoryOp extends CreateWorkspaceFileOp
{
    ModelElementType TYPE = new ModelElementType( CreateContactRepositoryOp.class );

    // *** FileName ***

    @DefaultValue( text = "contacts.xml" )
    @PreferDefaultValue

    ValueProperty PROP_FILE_NAME = new ValueProperty( TYPE, CreateWorkspaceFileOp.PROP_FILE_NAME );
}
<definition>
    <import>
        <definition>org.eclipse.sapphire.workspace.ui.CreateWorkspaceFileForm</definition>
    </import>
    <wizard>
        <id>CreateContactRepositoryWizard</id>
        <label>New Contact Repository (Sapphire Sample)</label>
        <page>
            <id>CreateContactRepositoryWizard.MainPage</id>
            <label>Contact Repository (Sapphire Sample)</label>
            <description>Create a contact repository.</description>
            <image>org/eclipse/sapphire/samples/SapphireWizardBanner.png</image>
            <scale-vertically>true</scale-vertically>
            <content>
                <include>CreateWorkspaceFileForm</include>
            </content>
        </page>
        <element-type>org.eclipse.sapphire.samples.contacts.CreateContactRepositoryOp</element-type>
    </wizard>    
</definition>
<extension point="org.eclipse.ui.newWizards">
    <wizard
        id="org.eclipse.sapphire.samples.contacts.CreateContactRepositoryWizard"
        category="Sapphire/Samples"
        name="Contact Repository (Sapphire Sample)"
        icon="org/eclipse/sapphire/samples/SapphireCreateFileWizard.png">
        <description>Create a contact repository.</description>
        <class class="org.eclipse.sapphire.workspace.ui.CreateWorkspaceFileWizard">
            <parameter name="sdef" value="org.eclipse.sapphire.samples.contacts.CreateContactRepositoryWizard"/>
            <parameter name="editor" value="org.eclipse.sapphire.samples.contacts.ContactRepositoryEditor"/>
        </class>
    </wizard>
</extension>

Context

The existing ClassLocator and ResourceLocator abstractions have been combined into a single abstraction. This abstraction enables Sapphire to find classes and other resources typically loaded from a class loader even if Sapphire is used in a context such as OSGi where a ClassLoader instance may not always be exposed.

Context
{
     static Context adapt( ClassLoader loader )
     static Context adapt( Class<?> cl )

     abstract <T> Class<T> findClass( String name )
     abstract URL findResource( String name )
     abstract List<URL> findResources( String name )
}

BundleBasedContext extends Context
{
     static Context adapt( Bundle bundle )
}

In addition to merging of the locators, this enhancement includes two new features:

  1. The findResources() method, which is used for locating all resources with the same name.
  2. When Context is constructed based on a class instead of a class loader, the implementation is capable of resolving classes and resources using a simple name relative to the package of context class.

ConversionService

The ConversionService converts an object to the specified type. One common application is to convert an input (such as a file) to a resource when instantiating the model.

Sapphire.ConversionService.IFileToWorkspaceFileResourceStore
Capable of converting an IFile to a WorkspaceFileResourceStore or a ByteArrayResourceStore.
Sapphire.ConversionService.ByteArrayResourceStoreToXmlResource
Capable of converting a ByteArrayResourceStore to an XmlResource or a Resource. Conversion is only performed if resource store corresponds to a file with "xml" extension or if the context element type has XML binding annotations.

A ConversionService can delegate to other conversion services to create a conversion chain. In fact, a common conversion of IFile to XmlResource is a chain of two ConversionService implementations.

Example

In the purchase order sample, a custom ConversionService is used because the default file extension for purchase order files is "po" rather than "xml" and PurchaseOrder element does not have XML binding annotations. The combination of these two factors prevent the framework-provided ConversionService implementations from engaging.

@Service( impl = PurchaseOrderResourceConversionService.class )

public interface PurchaseOrder extends IModelElement
{
    ...
}
public class PurchaseOrderResourceConversionService extends ConversionService
{
    @Override
    public <T> T convert( Object object, Class<T> type )
    {
        if( type == XmlResource.class || type == Resource.class )
        {
            final ByteArrayResourceStore store = service( MasterConversionService.class ).convert( object, ByteArrayResourceStore.class );

            if( store != null )
            {
                return type.cast( new RootXmlResource( new XmlResourceStore( store ) ) );
            }
        }

        return null;
    }
}

Note the use of chaining as part of the presented ConversionService implementation. The input could be any number of things, but as long as another ConversionService implementation knows how to convert it to a ByteArrayResourceStore, this implementation will take the conversion the rest of the way to a Resource.

FileName

When implementing operations that create files, a common need is to gather the name of a file from the user. The file name must be validated to be compatible with user's platform and certain normalization should be performed to simplify file name input.

Sapphire now provides FileName class and associated services.

  1. ValidationService Flags invalid file names as errors.
  2. ValueSerializationService Only creates FileName objects that are valid for the current platform.
  3. ValueNormalizationService
    1. Leading whitespace is removed.
    2. Trailing whitespace and dots are removed.
    3. Extension is added if the file name does not have one already and if the property has a FileExtensionsService (usually via @FileExtensions annotation).

ListenerContext

A new post method provides a way to add an event to the delivery queue without actually delivering it. This is useful when a section of code needs to issue one or more events, but should not be interrupted by event delivery. The zero-argument broadcast method is used to deliver all events in the queue. The variant of the broadcast method that takes an event is simply a post followed by a broadcast.

ListenerContext
{
    void post( Event event )
    void broadcast()
    void broadcast( Event event )
}

MasterConversionService

The MasterConversionService converts an object to the specified type by delegating to available ConversionService implementations. If object is null or is already of desired type, the object is returned unchanged.

An implementation of this service is provided with Sapphire. This service is not intended to be implemented by adopters. See ConversionService instead.

Example

In this example, an IFile is converted to a Resource as part of instantiating the model. Note how this code is not aware of the details of the conversion or what type of a resource is created.

IFile file = project.getFile( "contacts.xml" )
Resource resource = ContactRepository.TYPE.service( MasterConversionService.class ).convert( file, Resource.class );
ContactRepository model = ContactRepository.TYPE.instantiate( resource );

ModelElement

The existing ModelElement.initialize() method has been enhanced to be consistent with fluent interface principles by returning the element instead of void. This facilitates chaining.

Example

The following two snippets are equivalent.

Operation op = Operation.TYPE.instantiate( resource );
op.initialize();
Operation op = Operation.TYPE.instantiate( resource ).initialize();

A new variant of attach() and detach() methods has been added to make it a bit less verbose to listen for changes to a particular property.

ModelElement
{
    boolean attach( Listener listener )
    void attach( Listener listener, String path )
    void attach( Listener listener, ModelPath path )
    void attach( Listener listener, ModelProperty property )

    boolean detach( Listener listener )
    void detach( Listener listener, String path )
    void detach( Listener listener, ModelPath path )
    void detach( Listener listener, ModelProperty property )
}

Example

The following two snippets are equivalent.

op.attach( listener, Operation.PROP_FILE_NAME.getName() );
op.attach( listener, Operation.PROP_FILE_NAME );

Several method have been enhanced with an overloaded variant that accepts a string property name in place of a ModelProperty object. This reduces verbosity in situations where code does not already have a ModelProperty instance handy.

ModelElement
{
    <T> Value<T> read( ValueProperty property )
    <T> Transient<T> read( TransientProperty property )    
    <T extends IModelElement> ModelElementHandle<T> read( ElementProperty property )
    <T extends IModelElement> ModelElementList<T> read( ListProperty property )
    Object read( ModelProperty property )
    Object read( String property )

    void write( ValueProperty property, Object content ) [removed]
    void write( TransientProperty property, Object content ) [removed]
    void write( ModelProperty property, Object content )
    void write( String property, Object content )

    boolean enabled( ModelProperty property )
    boolean enabled( String property )

    Status validation( ModelProperty property )
    Status validation( String property )

    void refresh( ModelProperty property )
    void refresh( String property )
    void refresh( ModelProperty property, boolean force )
    void refresh( String property, boolean force )
    void refresh( ModelProperty property, boolean force, boolean deep )
    void refresh( String property, boolean force, boolean deep )

    <S extends Service> S service( ModelProperty property, Class<S> serviceType )
    <S extends Service> S service( String property, Class<S> serviceType )

    <S extends Service> List<S> services( ModelProperty property, Class<S> serviceType )
    <S extends Service> List<S> services( String property, Class<S> serviceType )
}

Example

The following two snippets are equivalent.

Status validation = op.validation( op.property( "FileName" ) );
Status validation = op.validation( "FileName" );

A method has been added to easily check if a particular property is empty without knowing the type of the property.

ModelElement
{
    boolean empty( ModelProperty property )
    boolean empty( String property )
}

The empty state is defined as follows:

ModelElementList

The new ModelElementList.enabled() method provides a simpler way to check list property's enablement directly through the property accessor.

Example

The following two statements are equivalent.

container.getEntities().enabled()
container.enabled( Container.PROP_ENTITIES )

ModelElementType

A new variant of instantiate method provides a way to create a model instance from an arbitrary input without knowing which resource needs to be created. The resource creation is handled by delegating to ConversionService implementations.

ModelElementType
{
    <T extends IModelElement> T instantiate()
    <T extends IModelElement> T instantiate( Resource resource )
    <T extends IModelElement> T instantiate( Object input )
}

Example

The following three snippets are equivalent.

Resource resource = new RootXmlResource( new XmlResourceStore( new WorkspaceFileResourceStore( file ) ) );
PurchaseOrder po = PuchaseOrder.TYPE.instantiate( resource );
Resource resource = PurchaseOrder.TYPE.service( MasterConversionService.class ).convert( file, Resource.class );
PurchaseOrder po = PuchaseOrder.TYPE.instantiate( resource );
PurchaseOrder po = PuchaseOrder.TYPE.instantiate( file );

PossibleValueService

PossibleValueService can now be attached to the list property instead of the value property when modeling the list of possible values pattern in order to be able to utilize the model in the service implementation and be compatible with property editors like the slush bucket and the check box list.

Example

public interface Example extends IModelElement
{
    ModelElementType TYPE = new ModelElementType( Example.class );

    // *** FavoriteColors ***

    interface FavoriteColor extends IModelElement
    {
        ModelElementType TYPE = new ModelElementType( FavoriteColor.class );

        // *** Name ***

        @NoDuplicates

        ValueProperty PROP_NAME = new ValueProperty( TYPE, "Name" );

        Value<String> getName();
        void setName( String value );
    }

    @Type( base = MultiSelectStringItem.class )
    @Service( impl = ColorsPossibleValuesService.class )

    ListProperty PROP_FAVORITY_COLORS = new ListProperty( TYPE, "FavoriteColors" );

    ModelElementList<FavoriteColor> getMultiSelectString();
}

@PreferDefaultValue

In certain cases, deviations from property's default value are allowed, but not recommended.

Consider a file creation wizard where the file should have a certain name to function properly. This is common for configuration files. For flexibility and consistency, the wizard should allow a different file name to be used if the user so desires, but it should warn the user that they are deviating from the file name recommendation.

The new @PreferDefaultValue annotation is used to specify the above semantics. Utilizing this annotation produces a warning and a fact statement.

Warning: [property] should be [default-value].
Fact: Recommended value is [default-value].

Example

@DefaultValue( text = "sapphire-extension.xml" )
@PreferDefaultValue

ValueProperty PROP_FILE_NAME = new ValueProperty( TYPE, CreateWorkspaceFileOp.PROP_FILE_NAME );

Service

In the past, when a service implementation needed to leverage another service from the same context, service context needed to be referenced explicitly. This led to unnecessary verbosity, so a pair of convenience methods have been added that delegate to the current context.

Service
{
    protected final <S extends Service> S service( Class<S> serviceType )
    protected final <S extends Service> List<S> services( Class<S> serviceType )
}

Example

The following two snippets are equivalent within Service implementation.

context().service( ExampleService.class )
service( ExampleService.class )

Code outside the service implementation can now access service context. The existing protected methods have been made public.

Service
{
    public ServiceContext context()
    public <T> T context( final Class<T> type )
}

ServiceContext

Solve certain types of out-of-order event delivery problems by making services use an existing event delivery queue.

ServiceContext
{
    void coordinate( ListenerContext context )
}

ServiceEvent

Events broadcast by a service must now extend ServiceEvent class. This facilitates differentiation of service events from other events and identifies the originating service.

Service
{
    broadcast( ServiceEvent service )
    broadcast()
}
ServiceEvent extends Event
{
    ServiceEvent( Service service )
    Service service()
}

Enabled Function

Determines if a property is enabled. Can be used either with two arguments (element and property name) or with a single property name argument. In the single argument form, the context element is used.

Examples

In these examples, the context element is Employee which has Assistant and Manager properties. The first example asks if Assistant property is enabled for this employee. The second example asks if Assistant property is enable for this employee's manager.

${ Enabled( 'Assistant' ) }
${ Enabled( Manager, 'Assistant' ) }

Cast Through a String

Sapphire EL performs type casts in order to convert the type of an operand into the type that the operator expects. Any type can be cast to a string and many types can be cast from a string since that's necessary in order to specify a literal. So, while a direct X-to-Y cast may not be implemented, an indirect X-to-String-to-Y cast may be possible.

Sapphire EL will now attempt an indirect cast through a string if a direct cast fails before giving up.

UpperCase Function

Converts a string to upper case.

Examples

${ UpperCase( Name ) }
${ Name.UpperCase() }

LowerCase Function

Converts a string to lower case.

Examples

${ LowerCase( Name ) }
${ Name.LowerCase() }

Use EL in More Places

Use expression language when specifying editor page header text,

Example

<editor-page>
    <page-header-text>contacts (${ Contacts.Size })</page-header-text>
</editor-page>

and when specifying master details outline header text,

Example

<editor-page>
    <outline-header-text>outline (${ Contacts.Size })</outline-header-text>
</editor-page>

and when specifying a conditional (if-then-else),

Example

<if>
    <condition>${ VersionMatches( Root().Version, "[1.1" ) }</condition>
    <then>
        ...
    </then>
    <else>
        ...
    </else>
</if>

and when specifying visibility of a section,

Example

<section>
    <visible-when>${ VersionMatches( Root().Version, "[1.1" ) }</condition>
    <content>
        ...
    </content>
</section>

and when specifying visibility of a master-details content node,

Example

<node>
    <visible-when>${ VersionMatches( Root().Version, "[1.1" ) }</condition>
    <section>
        ...
    </section>
</node>

and when specifying visibility of a master-details content node factory,

Example

<node-factory>
    <property>Entities</property>
    <visible-when>${ VersionMatches( Root().Version, "[1.1" ) }</condition>
    <case>
        ...
    <case>
</node-factory>

and when specifying visibility of a property editor.

Example

<property-editor>
    <property>Description</property>
    <visible-when>${ VersionMatches( Root().Version, "[1.1" ) }</condition>
</property-editor>

Section Description

A common pattern in forms involves a description text at the top of a section to explain the contents of the section. This pattern can be implemented using a label followed by a spacer in the section content, but this approach fails to fully convey the developer's intent and can lead to inconsistent presentation. A common mistake is failing to add a spacer after the label.

The description for a section can now be specified directly.

Example

<section>
    <label>shipping information</label>
    <description>If not specified, billing information is used for shipping purposes.</description>
    <content>
        <with>
            <path>ShippingInformation</path>
            <default-panel>
                <content>
                    <property-editor>Name</property-editor>
                    <property-editor>Organization</property-editor>
                    <property-editor>Street</property-editor>
                    <property-editor>City</property-editor>
                    <property-editor>State</property-editor>
                    <property-editor>ZipCode</property-editor>
                </content>
            </default-panel>
        </with>
    </content>
</section>

ActuatorActionHandlerEvent

Detect when an actuator's action handler changes using ActuatorActionHandlerEvent.

ProblemsTraversalService

ProblemsTraversalService produces a problem-annotated traversal order through the content outline, which can be used to find the next error or warning from any location in the content outline.

An implementation of this service is provided with Sapphire. This service is not intended to be implemented by adopters.

XmlUtil.convertToNamespaceFrom and convertFromNamespaceForm

Two static utility methods have been added to XmlUtil to make it easier to convert documents to and from namespace-qualified form.

public static void convertToNamespaceForm( Document document, String namespace )
public static void convertToNamespaceForm( Document document, String namespace, String schemaLocation )
public static void convertFromNamespaceForm( Document document )

Enhancements to Collection Factories

The collection factories have been enhanced with new methods to support broader range of use cases and a brand new SortedSetFactory.

ListFactory
{
    static List<E> empty()
    static List<E> singleton( E element )
    static List<E> unmodifiable( E... elements )
    static List<E> unmodifiable( Collection<E> elements )

    static ListFactory<E> start()

    ListFactory<E> add( E element )
    ListFactory<E> add( E... element )
    ListFactory<E> add( Collection<E> element )
    ListFactory<E> filter( Filter<E> filter )
    E remove( int index )
    E get( int index )
    boolean contains( E element )
    int size()

    ListFactory<E> result()
}

SetFactory
{
    static Set<E> empty()
    static Set<E> singleton( E element )
    static Set<E> unmodifiable( E... elements )
    static Set<E> unmodifiable( Collection<E> elements )

    static SetFactory<E> start()

    SetFactory<E> add( E element )
    SetFactory<E> add( E... element )
    SetFactory<E> add( Collection<E> element )
    SetFactory<E> filter( Filter<E> filter )
    boolean remove( E element )
    boolean contains( E element )
    int size()

    SortedSet<E> result()
}

SortedSetFactory
{
    static SortedSet<E> empty()
    static SortedSet<E> singleton( E element )
    static SortedSet<E> unmodifiable( E... elements )
    static SortedSet<E> unmodifiable( Collection<E> elements )

    static SortedSetFactory<E> start()
    static SortedSetFactory<E> start( Comparator<E> comparator )

    SortedSetFactory<E> add( E element )
    SortedSetFactory<E> add( E... element )
    SortedSetFactory<E> add( Collection<E> element )
    SortedSetFactory<E> filter( Filter<E> filter )
    boolean remove( E element )
    E first()
    E last()
    boolean contains( E element )
    int size()

    SortedSet<E> result()
}

MapFactory
{
    static Map<K,V> empty()
    static Map<K,V> singleton( K key, V value )
    static Map<K,V> unmodifiable( Map<K,V> map )

    static MapFactory<K,V> start()

    MapFactory<K,V> add( K key, V value )
    MapFactory<K,V> add( Map<K,V> map )
    MapFactory<K,V> filter( Filter<Map.Entry<K,V>> filter )
    V remove( K key )
    V get( K key )
    boolean contains( K key )
    boolean containsKey( K key )
    boolean containsValue( V value )
    int size()

    Map<K,V> result()
}

JdtUtil

Various scenarios in both Sapphire and common usage of Sapphire require locating Java source folders. Sapphire JDT integration now includes a set of utility functions to make this easier.

JdtUtil
{
    // Finds the nearest Java source folder for the specified resource.
    // The nearest is defined as either the source folder containing 
    // the specified resource or the first non-derived source folder 
    // of the containing project.

    static IContainer findSourceFolder( IResource resource )

    // Finds all source folders for a project, which is specified either
    // directly or indirectly via a contained resource.

    static List<IContainer> findSourceFolders( IResource resource )
    static List<IContainer> findSourceFolders( IProject project )
    static List<IContainer> findSourceFolders( IJavaProject project )
}

MasterDetailsContentNode

The method for accessing child nodes has been enhanced. The MasterDetailsContentNode.nodes() method now returns a customized list object that provides additional operations specific to list of nodes. For instance, there is a method to filter list of nodes to contain only visible nodes.

Example

The following produces all nodes whether or not they are visible.

root.nodes()

A slight variation on the above statement produces only the visible nodes.

root.nodes().visible()

The new NodeListEvent provides notification when the list of child nodes changes. Note that this event does not fire when child node visibility changes. See VisibilityChangedEvent for that functionality.

node.attach
(
    new FilteredListener<NodeListEvent>()
    {
        @Override
        protected void handleTypedEvent( NodeListEvent event )
        {
            ...
        }
    }
)

SapphirePart

Check part's visibility using SapphirePart.visible() method. To get notified of changes to part's visibility, listen for VisibilityChangedEvent.

Example

boolean visible = editor.visible()

editor.attach
(
    new FilteredListener<VisibilityChangedEvent>()
    {
        @Override
        protected void handleTypedEvent( VisibilityChangedEvent event )
        {
            ...
        }
    }
);

Check whether a part has been initialized by using SapphirePart.initialized() method. To get notified when the part has completed its initialization, listen for PartInitializationEvent.

Example

boolean initialized = editor.initialized()

editor.attach
(
    new FilteredListener<PartInitializationEvent>()
    {
        @Override
        protected void handleTypedEvent( PartInitializationEvent event )
        {
            ...
        }
    }
);

The SapphirePart.executeAfterInitialization() method provides a way to execute a job after a part has been fully initialized. If the part has already been initialized, the job is executed immediately.

Example

editor.executeAfterInitialization
(
    new Runnable()
    {
        @Override
        public void run()
        {
            ...
        }
    }
);

Status

The new Status.contains() method provides a simpler way to check if property validation has a problem of certain type. This can be useful when implementing automated validation problem fixes.

Example

if( op.getFileName().validation().contains( "Sapphire.Workspace.CreateFileOp.FileExists" ) )
{
    ...
}

Value

The new Value.enabled() method provides a simpler way to check value property's enablement directly through the property accessor.

Example

The following two statements are equivalent.

entity.getName().enabled()
entity.enabled( Entity.PROP_NAME )

Overwrite Existing File

The reusable form for creating workspace files has been enhanced with an automated problem resolution to overwrite an existing file. This makes it slightly easier for user to go from problem to resolution since both are presented in the same popup.

Dynamic Action Visibility

Dynamic visibility of actions has been made easier. An action handler is now able to update its own visibility. An action whose handlers are all invisible is itself invisible. This approach mimics the previously-existing support for dynamic action enablement.

SapphireActionSystemPart
{
    boolean isEnabled()
    void setEnabled( boolean enabled )
    boolean isVisible()
    void setVisible( boolean visible )
}

Example

public class ExampleActionHandler extends SapphireActionHandler
{
    @Override
    public void init( SapphireAction action, ActionHandlerDef def )
    {
        super.init( action, def );

        // Attach a listener to something that will call refreshVisibility()
        // when needed.

        ...

        // Remove the listener on action handler dispose.

        attach
        (
            new FilteredListener<DisposeEvent>()
            {
                @Override
                protected void handleTypedEvent( DisposeEvent event )
                {
                    ...
                }
            }
        }

        // Perform initial visibility refresh.

        refreshVisibility();
    }

    private void refreshVisibility()
    {
        setVisible( ... );
    }

    @Override
    protected Object run( SapphireRenderingContext context )
    {
        ...
    }
}

Show Next Problem

A pair of new actions have been added to the master-details content outline editor page to make it easier to navigate to the next validation error or warning. This facility is especially helpful in a deeply nested outline.