On behalf of all who contributed, I am very proud to announce the availability of the Sapphire 0.7 release. This is the biggest release in Sapphire’s history. It includes major enhancements, such the new diagram node shape definition language, runtime bytecode generation of implementation classes, improved API for property instances, and much more.
Wednesday, December 18, 2013
Sapphire 0.7 : Diagram Node Shape Language
The keystone feature of Sapphire 0.7 release is the brand new diagram node shape definition language. Use it to construct complex diagram editors with the same ease as forms.
The language is composed of six primitives.
Text - Displays static text or content from the model, as specified by an expression. If the expression contains a reference to a single property, the property can be edited in place by double-clicking on the text.
Line - Displays a vertical or a horizontal line. Typically used as a separator. Line style, weight and color can be set as appropriate.
Spacer - Leaves a blank space of specified size. Frequently in conjunction with a visibility rule.
Image - Displays an image. The image can be produced by an expression for a dynamic effect.
Validation Marker - Displays an image communicating the validation status of the immediate context. The user can click on the image to get the validation details and the related actions.
Rectangle - Contains one or more shape in a rectangular space with an optional border and background. The contained shapes are arranged either vertically, horizontally or in a stack, as specified by the rectangle's layout.
Shape Factory - Produces a shape for each entry in a list property using the specified shape templates. The list items can be added, removed and re-arranged through actions exposed by the shape factory. Drag-n-drop is also supported as a means to re-order the list items.
Friday, December 13, 2013
Sapphire 0.7 : Improved Localization for Java
Within Java code, such as an implementation of a service, Sapphire has previously relied on the NLS class copied from Eclipse platform. The developer experience has been improved.
-
The static fields holding localizable text are able to exist in any class, not just a dedicated resource bundle class.
-
Formatting messages using localizable text as a template is more fluid.
Resources.message.format( x, y )
vsNLS.bind( Resources.message, x, y )
-
The original text can be specified using an annotation, concentrating text closer to the point of use and avoiding numerous problems associated with maintaining text in discrete resource files. When time comes to do the translation, an annotation processor or a similar build system can be used to extract text into resource files. Only the translated versions of the resource files need to be distributed. The original text will be read from the annotations at runtime.
Before | After |
---|---|
|
|
Sapphire 0.7 : 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" );
List<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 );
Thursday, December 12, 2013
Sapphire 0.7 : New EL Functions and Properties
Many new functions have been added to the expression language.
Returns the absolute path of a value for properties with a RelativePathService.
${ Path.Absolute }
Returns the content of a value or a transient. For value properties, the default is taken into account, if applicable.
${ PurchaseOrder.FulfillmentDate.Content }
Returns the enablement of a property.
${ PurchaseOrder.FulfillmentDate.Enabled }
In the context of a property editor, Enabled function can also be called with zero arguments. This accesses the enablement of the property editor part.
<property-editor>
<property>FormLoginPage</property>
<visible-when>${ Enabled() }</visible-when>
</property-editor>
Tests if a string ends with the specified suffix.
${ Path.EndsWith( ".png" ) }
Returns a fragment of a string. The fragment starts at the index specified by the second argument and extends to the character before the index specified by the third argument. The length of the fragment is end index minus start index.
- A negative start index is normalized to zero.
- A start index exceeding the length of the input is normalized to the length of the input.
- An end index exceeding the length of the input is normalized to the length of the input.
- An end index that is smaller than the start index is normalized to the start index.
${ Value.Fragment( 3, 6 ) }
${ Fragment( "abcdef", 0, 3 ) }
Returns a fragment of a string starting at the beginning and not exceeding the specified length.
- A negative fragment length is normalized to zero.
- A fragment length exceeding the length of the input is normalized to the length of the input.
${ Value.Head( 3 ) }
${ Head( "abcdef", 3 ) }
Determines the index of a model element within its parent list.
${ This.Index }
Determines whether a string matches a regular expression. The full semantics are specified by Java's String.matches() function.
${ Entity.Name.Matches( "[a-z][a-z0-9]*" ) }
Returns the message from a validation result.
${ PurchaseOrder.FulfillmentDate.Validation.Message }
Returns the parent of the given part. An implementation of this function for model elements was added in an earlier release.
${ Part.Parent.Validation.Severity }
${ Part.Parent.Parent.Validation.Severity }
Returns the context part.
${ Part.Validation.Severity }
Returns the severity of a validation result.
${ PurchaseOrder.FulfillmentDate.Validation.Severity }
Determines the size of a collection, a map, an array or a string.
${ PurchaseOrder.Entries.Size }
${ PurchaseOrder.BillingInformation.Name.Size }
${ Size( "abcdef" ) }
Tests if a string starts with the specified prefix.
${ Path.StartsWith( ".." ) }
Returns the root element of editor page's persistent state, allowing access to various state properties. This is particularly useful when the persistent state is extended with custom properties wired to custom actions, as it allows any EL-enabled facility to integrate with the custom state property.
In the following example, a custom state property is used to control whether content outline node label for an item in the catalog sample should include the manufacturer.
<node-factory>
<property>Items</property>
<case>
<label>
${
Name == null
? "Item"
: (
State().ShowManufacturer && Manufacturer != null
? Concat( Manufacturer, " ", Name )
: Name
)
}
</label>
</case>
</node-factory>
Returns a fragment of a string starting at the end and not exceeding the specified length.
- A negative fragment length is normalized to zero.
- A fragment length exceeding the length of the input is normalized to the length of the input.
${ Value.Tail( 3 ) }
${ Tail( "abcdef", 3 ) }
Returns the text of a value, taking into account the default, if applicable.
${ PurchaseOrder.FulfillmentDate.Text }
In situations where EL context is established by a model element, it can be useful to directly reference that element in order to pass it to functions. Mirroring Java, the context now exposes "This" property.
In this example, the expression computes the index of the context model element within its parent list.
${ This.Index }
Returns the validation result of a property or a part.
${ PurchaseOrder.FulfillmentDate.Validation }
${ Part.Validation }
Sapphire 0.7 : 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 }
Sapphire 0.7 : 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 );
Sapphire 0.7 : 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.
- Easier getting started process since customizing the build to compile implementation classes is no longer necessary
- Smaller footprint for applications as implementation classes do not need to be distributed and stored
- Faster element instantiation as the bytecode generator is faster than most disk systems
Wednesday, December 11, 2013
Sapphire 0.7 : 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>
Sapphire 0.7 : 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 );
Sapphire 0.7 : 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 );
Sapphire 0.7 : Improved Date Support
As the Sapphire 0.7 release nears, I will be making a series of posts to highlight some of the exciting new features of this release. This is the first post in the series.
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.
Thursday, June 27, 2013
To throw or not to throw
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 to the users of the function. How can we leave this decision to the function caller?
Let’s consider this problem in the context of a concrete example, a function that finds a purchase order by id.
One possible solution is to add a boolean flag to function arguments.
PurchaseOrder findPurchaseOrder( String id, boolean failIfNotFound )
{
PurchaseOrder po = this.orders.get( id );
if( po == null && failIfNotFound )
{
throw new IllegalArgumentException();
}
return po;
}
The problem with boolean flags is that they tend to obfuscate the calling code. A developer reading the code will not know how the function behaves without researching the semantics of the flag. The problem is magnified if the function already has many arguments.
Another possible solution is to create two variants of the function, with function name signaling its semantics.
PurchaseOrder findOptionalPurchaseOrder( String id )
{
return this.orders.get( id );
}
PurchaseOrder findRequiredPurchaseOrder( String id )
{
PurchaseOrder po = this.orders.get( id );
if( po == null )
{
throw new IllegalArgumentException();
}
return po;
}
That should make the calling code more clear, but now we've doubled the number of functions, contributing to API bloat and higher implementation costs.
Perhaps we should look for inspiration in fluent interface design principles. Maybe the function can return an intermediary object that has optional() and required() methods. That has the clarity of encoding the semantics in the function name without doubling the number of functions.
public final class Result<T>
{
private final T object;
private final RuntimeException exception;
private Result( final T object )
{
this.object = object;
this.exception = null;
}
private Result( final RuntimeException exception )
{
this.object = null;
this.exception = exception;
}
public static <T> Result<T> success( final T object )
{
return new Result<T>( object );
}
public static <T> Result<T> failure( final RuntimeException exception )
{
return new Result<T>( exception );
}
public T required()
{
if( this.exception != null )
{
throw this.exception;
}
return this.object;
}
public T optional()
{
return this.object;
}
}
Result<PurchaseOrder> findPurchaseOrder( String id )
{
PurchaseOrder po = this.orders.get( id );
if( po == null )
{
return Result.failure( new IllegalArgumentException() );
}
return Result.success( po );
}
Notice that when the purchase order is not found, an exception is created, but not thrown. To throw or not to throw decision has been deferred to the caller.
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();
Friday, March 15, 2013
When 2 is less than 1
Suppose you have a data structure with a boolean field, but the field is lazily initialized, so you need to know whether it has been initialized. Do you immediately think of using a Boolean object instead of a boolean primitive so that you can use the null reference to represent not initialized? What about using a second boolean instead to represent initialization state?
Which way is better? Let’s analyze this question from memory usage standpoint. A boolean primitive takes up one byte. The object header for an empty object on a 64-bit JVM is twelve bytes. Of course, a Boolean object is not empty, it has a boolean primitive inside of it, so that’s thirteen bytes. So we are looking at thirteen bytes for the one field solution vs. two bytes for the two field solution.
Does this matter in all situations? Of course, not. But if there will be many instances of your data structure, the memory savings add up very quickly.
Tuesday, February 19, 2013
Centering SWT ColorDialog
SWT includes ColorDialog class which exposes the native color dialog. Unfortunately, the dialog opens in the upper-left corner of the parent shell and there is no API to change this behavior. Fortunately, there is a trick that can be used to center the dialog instead.
final Shell parent = ...
final Rectangle bounds = parent.getBounds();
// There is no means to compute the size of the color dialog. In the following
// computations, measurements of the dialog on Windows 7 are used.
final int x = bounds.x + bounds.width / 2 - 120;
final int y = bounds.y + bounds.height / 2 - 170;
final Shell shell = new Shell( parent );
try
{
shell.setBounds( x, y, 0, 0 );
final ColorDialog dialog = new ColorDialog( shell );
dialog.setText( ... dialog title ... );
dialog.setRGB( ... initial color ... );
final RGB pickedColor = dialog.open();
if( pickedColor != null )
{
...
}
}
finally
{
shell.dispose();
}
The same trick can be used for centering other native dialogs exposed by SWT.
Monday, February 18, 2013
Use EL for Sapphire Validation
Sapphire includes a number of annotations (such as @Required and @NumericRange) that allow the developer to declaratively specify semantics which the framework translates to validation. If the provided annotations are not sufficient, the developer can implement ValidationService directly.
In a recent review of a large Sapphire project, I noticed that a fair number of custom ValidationService implementations were implementing rather simple semantics. Many of these semantics could be easily expressed using Sapphire Expression Language, leading to a model that is easier to understand and maintain. Now that’s possible in the latest Sapphire 0.7 build.
// *** Min ***
@Type( base = Integer.class )
@DefaultValue( text = "0" )
@Validation( rule = "${ Min <= Max }", message = "Must not be larger than max." )
ValueProperty PROP_MIN = new ValueProperty( TYPE, "Min" );
Value<Integer> getMin();
void setMin( String value );
void setMin( Integer value );
// *** Max ***
@Type( base = Integer.class )
@DefaultValue( text = "0" )
@Validation( rule = "${ Max >= Min }", message = "Must not be smaller than min." )
ValueProperty PROP_MAX = new ValueProperty( TYPE, "Max" );
Value<Integer> getMax();
void setMax( String value );
void setMax( Integer value );
The new @Validation annotation has an optional severity attributes that allows the developer to choose between error and warning severity for the validation problem. Multiple validations can be specified by using @Validations annotation that simply holds a set of @Validation annotations.
Friday, January 4, 2013
Runtime bytecode generation comes to Sapphire
Sapphire developers define the model by writing Java interfaces annotated with data semantics. A simple element definition might look like this:
@GenerateImpl
public interface Person extends IModelElement
{
ModelElementType TYPE = new ModelElementType( Person.class );
// *** Name ***
@Required
ValueProperty PROP_NAME = new ValueProperty( TYPE, "Name" );
Value<String> getName();
void setName( String value );
// *** Age ***
@Type( base = Integer.class )
ValueProperty PROP_AGE = new ValueProperty( TYPE, "Age" );
Value<Integer> getAge();
void setAge( String value );
void setAge( Integer value );
}
To instantiate a Person object, we need a concrete class that implements this interface. For the last few years, Sapphire developers relied on an annotation processor that is part of Sapphire SDK and is triggered by the @GenerateImpl annotation. The annotation processor would generate an implementation class like this:
public final class PersonImpl extends ModelElement implements Person
{
public PersonImpl( final IModelParticle parent, final ModelProperty parentProperty, final Resource resource )
{
super( TYPE, parent, parentProperty, resource );
}
public Value<String> getName()
{
return (Value) read( PROP_NAME );
}
public void setName( final String value )
{
write( PROP_NAME, value );
}
public Value<Integer> getAge()
{
return (Value) read( PROP_AGE );
}
public void setAge( final String value )
{
write( PROP_AGE, value );
}
public void setAge( final Integer value )
{
write( PROP_AGE, value );
}
}
The generated class is trivial as all the heavy lifting is done by the code in the ModelElement base class. Nevertheless, generating these implementation classes is important. No one wants to write any significant amount of code with a model that is accessible only via the generic read and write methods.
The annotation processor has been working well enough, but I have been wanting to see if on-demand runtime bytecode generation would be a better solution. Deferring generation of implementation classes until runtime removes the burden of incorporating Sapphire compilation into the application build.
Let me preface the next part by saying that I know next to nothing about Java bytecode, so I have been putting off this project for a while. Bytecode generation is difficult, I thought. I would have to learn a lot of new concepts and it would take a long time to re-implement the compiler. Boy was I wrong! I started this project two days ago and today I was able to remove the old annotation processor and push the changes. I haven’t kept track of how long it took to implement the original compiler, but it wasn’t two days!
Another surprising aspect is that the new compiler is significantly simpler than the original one. Purely in numerical terms:
- Old Compiler: 17 classes, 3219 lines of code
- New Compiler: 3 classes, 808 lines of code
I attribute the size disparity primarily to two factors:
- Java reflection API is far easier to use than the equivalent Java mirror API that you must use to build an annotation processor.
- Generating readable Java source code requires managing formatting and imports. Neither factors into bytecode generation.
The fast progress on the new compiler was further made possible by ASM, a Java bytecode manipulation framework. Leveraging ASM, a framework completely new to me, was made particularly easy by the Bytecode Outline plugin for Eclipse and its ASMifier mode. With the Bytecode Outline view open, you just select a method and you see either the Java bytecode or an ASM code snippet. An incredibly effective way to use ASM without taking the time to learn new API.
Major kudos to those behind ASM and Bytecode Outline. Secondary kudos to Java Decompiler Project. I used the standalone version (JD-GUI) to check the bytecode that I was generating.
The new compiler referenced here will ship as part of the upcoming Sapphire 0.7 release.