Many aspects of Sapphire can be extended by third parties.
The AdapterService provides means to extend the behavior of the adapt method in a given context.
For example, out of the box, Sapphire can adapt IModelElement to IProject assuming that the model is using an IFile for the underlying resource store. However, in some cases there may be no underlying IFile, so the default logic will no be able to find IProject. To solve this problem, we can introduce an adapter service to provide a custom method for adopting IModelElement to IProject.
Example
@GenerateImpl
@Service( impl = NewProjectFileOpAdapterService.class )
public interface INewProjectFileOp extends IModelElement
{
...
}
public class NewProjectFileOpAdapterService extends AdapterService
{
@Override
public <A> A adapt( Class<A> adapterType )
{
if( IProject.class == adapterType )
{
INewProjectFileOp op = context( INewProjectFileOp.class );
IProject project = op.getProject().resolve();
if( project != null )
{
return adapterType.cast( project );
}
}
return null;
}
}
The ContentProposalService provides a conduit for content assist in property editors. If a property has this service, content assist will be automatically enabled in the property editor. The manner in which content assist is presented is specific to the presentation, but usually involves a popup window with proposals, activated by some combination of key strokes (such as CTRL+SPACE).
The framework provides an implementation of ContentProposalService for properties with @PossibleValues annotation or a custom PossibleValuesService, but this service can also be implemented directly by adopters.
The DependenciesAggregationService combines the data from all applicable dependencies services in order to produce a single set of dependencies. A dependency is a model paths that points to parts of the model that the property depends on. A property listens on all of its dependencies and triggers refresh when any of the dependencies change.
An implementation of this service is provided with Sapphire. This service is not intended to be implemented by adopters. See DependenciesService instead.
The DependenciesService produces the set of model paths that point to parts of the model that the property depends on. A property listens on all of its dependencies and triggers refresh when any of the dependencies change.
Although custom implementations are supported, in most cases the supplied implementation that is configured via @DependsOn annotation should be sufficient.
Example
@DependsOn( "Name" )
ValueProperty PROP_ID = new ValueProperty( TYPE, "Id" );
Value<String> getId();
void setId( String value );
Other annotations, such as @NoDuplicates can also inject implied dependencies (via their own DependenciesService implementations). For instance, placing @NoDuplicates annotation on a Name property automatically adds "#/Name" dependency.
If declarative approach is not sufficient, a custom DependenciesService implementation can be supplied.
Example
public class CustomDependenciesService extends DependenciesService
{
@Override
protected DependenciesServiceData compute()
{
// Compute the list of dependencies.
List<ModelPath> dependencies = new ArrayList<ModelPath>();
...
return new DependenciesServiceData( dependencies );
}
}
@Service( impl = CustomDependenciesService.class )
ValueProperty PROP_NAME = new ValueProperty( TYPE, "Name" );
Value<String> getName();
void setName( String value );
The DiagramLayoutPersistenceService is responsible for persisting layout of the diagram, such a location and size of nodes, connection bend points, etc.
Unlike other services, DiagramLayoutPersistenceService is not defined by methods that must be implemented, but rather by its expected behavior.
Example
The architecture sample provides a comprehensive example of a custom implementation that persists layout in the same file as data.
The DragAndDropService provides means to implement drag-n-drop behavior in a diagram editor.
Example
public class MapDragAndDropService extends DragAndDropService
{
@Override
public boolean droppable( DropContext context )
{
return context.object() instanceof IFile;
}
@Override
public void drop( DropContext context )
{
IFile file = (IFile) context.object();
List locations = new ArrayList();
InputStream in = null;
try
{
in = file.getContents();
final BufferedReader br = new BufferedReader( new InputStreamReader( in ) );
for( String line = br.readLine(); line != null; line = br.readLine() )
{
if( line != null )
{
line = line.trim();
if( line.length() > 0 )
{
locations.add( line );
}
}
}
}
catch( CoreException e )
{
LoggingService.log( e );
}
catch( IOException e )
{
LoggingService.log( e );
}
finally
{
if( in != null )
{
try
{
in.close();
}
catch( IOException e ) {}
}
}
if( ! locations.isEmpty() )
{
SapphireDiagramEditorPagePart diagram = context( SapphireDiagramEditorPagePart.class );
Map map = context( Map.class );
Point initialDropPosition = context.position();
int x = initialDropPosition.getX();
int y = initialDropPosition.getY();
for( String locationName : locations )
{
if( ! map.hasLocation( locationName ) )
{
Location location = map.getLocations().insert();
location.setName( locationName );
DiagramNodePart locationNodePart = diagram.getDiagramNodePart(location);
locationNodePart.setNodeBounds( x, y );
x += 50;
y += 50;
}
}
}
}
}
The DragAndDropService is typically registered as part of diagram page definition in the sdef file.
<diagram-page>
<service>
<implementation>MapDropService</implementation>
</service>
</diagram-page>
The EqualityService provides means to implement equals() and hashCode() methods when the context object doesn't support implementing these methods directly. One such context is model elements, where the framework does not rely on a particular implementation of these methods, but having these methods behave in a way consistent with semantics of the data being modeled can be useful for other purposes.
Example
public class ContactEqualityService extends EqualityService
{
@Override
public boolean doEquals( Object obj )
{
Contact c1 = context( Contact.class );
Contact c2 = (Contact) obj;
return equal( c1.getLastName().getText(), c2.getLastName().getText() ) &&
equal( c1.getFirstName().getText(), c2.getFirstName().getText() );
}
@Override
public int doHashCode()
{
Contact c = context( Contact.class );
String lastName = c.getLastName().getText();
String firstName = c.getFirstName().getText();
return ( lastName == null ? 1 : lastName.hashCode() ) ^ ( firstName == null ? 1 : firstName.hashCode() );
}
private static boolean equal( Object obj1, Object obj2 )
{
if( obj1 == obj2 )
{
return true;
}
else if( obj1 != null && obj2 != null )
{
return obj1.equals( obj2 );
}
return false;
}
}
@Services( { @Service( impl = ContactEqualityService.class ), ... } )
public interface Contact extends IModelElement
{
ModelElementType TYPE = new ModelElementType( Contact.class );
...
}
The FactsAggregationService combines the data from all applicable facts services in order to produce a single list of facts. A fact is a short statement describing property semantics.
An implementation of this service is provided with Sapphire. This service is not intended to be implemented by adopters. See FactsService to contribute facts.
When a property is described to a user in documentation one does it with a series of short statements that define its semantics, such as "must be specified" or "maximum value is 100". When a property is described to Sapphire one does it with a series of annotations, such as @Required or @NumericRange. This duplicate specification is a maintenance problem.
A FactsService provides a means to dynamically derive statements about property's semantics based on property's metadata. The derived facts can then be presented to the user as part of documentation, property editor information popup and in other relevant places.
A single facts service can produce multiple facts and multiple facts services can be active concurrently for a given property. See FactsAggregationService for an easier way to consume all facts.
Sapphire includes a number of FactsService implementations.
##facts-servicess##Example
This screen capture shows user experience with some of the provided FactsService implementation. See if you can match facts in the screen capture to service implementations above.
Adopters can provide custom FactService implementations either globally using Sapphire extension system or at the property level using @Service annotation.
Example
A simple global FactsService implementation that is triggered by a hypothetical @Since property annotation.
public class SinceVersionFactsService extends FactsService
{
@Override
protected void facts( List facts )
{
Since since = property().getAnnotation( Since.class );
facts.add( "Since version " + since.version() + "." );
}
public static class Factory extends ServiceFactory
{
@Override
public boolean applicable( ServiceContext context,
Class<? extends Service> service )
{
return context.find( ModelProperty.class ).hasAnnotation( Since.class );
}
@Override
public Service create( ServiceContext context,
Class<? extends Service> service )
{
return new SinceVersionFactsService();
}
}
}
The service implementation is registered in META-INF/sapphire-extension.xml file.
<extension xmlns="http://www.eclipse.org/sapphire/xmlns/extension">
<service>
<id>Example.SinceVersionFactsService</id>
<type>org.eclipse.sapphire.services.FactsService</type>
<context>Sapphire.Property.Instance</context>
<factory>example.SinceVersionFactsService$Factory</factory>
</service>
</extension>
Facts can also be statically specified for a given property by using @Fact annotation. Use @Facts annotation to specify multiple facts. The facts contained in these annotations are surfaced by an included FactsService implementation (id:Sapphire.FactsService.Static).
Example
// *** ExampleOne ***
@Fact( statement = "Important fact.")
ValueProperty PROP_EXAMPLE_ONE = new ValueProperty( TYPE, "ExampleOne" );
Value<String> getExampleOne();
void setExampleOne( String value );
// *** ExampleMultiple ***
@Facts( { @Fact( statement = "First important fact." ), @Fact( statement = "Second important fact." ) } )
ValueProperty PROP_EXAMPLE_MULTIPLE = new ValueProperty( TYPE, "ExampleMultiple" );
Value<String> getExampleMultiple();
void setExampleMultiple( String value );
The FileExtensionsService produces the list of file extensions that are allowed for a path value property.
Although custom implementations are supported, in most cases the supplied implementation that is configured via @FileExtensions annotation should be sufficient. In many cases, specifying file extensions is as simple as listing them with a comma in between.
Example
@Type( base = Path.class )
@AbsolutePath
@MustExist
@ValidFileSystemResourceType( FileSystemResourceType.FILE )
@FileExtensions( expr = "jar,zip" )
ValueProperty PROP_FILE_PATH = new ValueProperty( TYPE, "FilePath" );
Value<Path> getFilePath();
void setFilePath( String value );
void setFilePath( Path value );
File extensions can also be specified via an expression that takes into account values of other properties.
Examples
@FileExtensions( expr = "${ Extension }" )
@FileExtensions( expr = "${ LossyFormat ? "jpeg,jpg" : "png,gif" }" )
If declarative approach is not sufficient, a custom FileExtensionsService implementation can be supplied.
Example
public class CustomFileExtensionsService extends FileExtensionsService
{
@Override
protected void initFileExtensionsService()
{
// Optionally register listeners to invoke refresh method when the list of extensions
// may need to be updated.
}
@Override
protected FileExtensionsServiceData compute()
{
// Compute the list of extensions.
List<String> extensions = new ArrayList<String>();
...
return new FileExtensionsServiceData( extensions );
}
@Override
public void dispose()
{
super.dispose();
// Remove any listeners that were added during initialization.
}
}
@Type( base = Path.class )
@AbsolutePath
@MustExist
@ValidFileSystemResourceType( FileSystemResourceType.FILE )
@Service( impl = CustomFileExtensionsService.class )
ValueProperty PROP_FILE_PATH = new ValueProperty( TYPE, "FilePath" );
Value<Path> getFilePath();
void setFilePath( String value );
void setFilePath( Path value );
The InitialValueService produces a value to assign to a property when the containing model element is created.
The concept of an initial value is different from a default value. The initial value is explicitly assigned to the property during containing model element's creation. This includes writing to the backing resource (such as an XML document). In comparison, the default value is used when null is read for a property from the backing resource. As such, the default value is only visible to model consumers (such as the user interface), while the initial value is persisted.
Whether you use an initial value or a default value is frequently dictated by the requirements of the backing resource. As an example, let's consider an XML document that stores phone numbers. In this XML document, the phone number element has a type child element which contains a value like home, mobile, work, etc. Let's further say that semantically, we wish to use mobile phone number type unless specified differently. Now, if the XML schema dictates that the phone number type element is required, we would need to specify "mobile" as the initial value. If the phone number type element is optional, it would be better to specify "mobile" as the default value.
In many situations, the initial value is static and should be configured using @InitialValue annotation.
Example
@Required
@PossibleValues( values = { "home", "mobile", "work", "other" }, invalidValueSeverity = Status.Severity.OK )
@InitialValue( text = "mobile" )
ValueProperty PROP_TYPE = new ValueProperty( TYPE, "Type" );
Value<String> getType();
void setType( String type );
When the initial value varies due to runtime conditions, a custom implementation of InitialValueService can be provided.
Example
public class PhoneTypeInitialValueService extends InitialValueService
{
@Override
protected void initInitialValueService()
{
// Register listeners to invoke refresh() method when the initial value
// may have changed.
}
@Override
protected InitialValueServiceData compute()
{
// Compute the initial value.
String value;
...
return new InitialValueServiceData( value );
}
@Override
public void dispose()
{
super.dispose();
// Remove any listeners that were added during initialization.
}
}
@Required
@PossibleValues( values = { "home", "mobile", "work", "other" }, invalidValueSeverity = Status.Severity.OK )
@Service( impl = PhoneTypeInitialValueService.class )
ValueProperty PROP_TYPE = new ValueProperty( TYPE, "Type" );
Value<String> getType();
void setType( String type );
The JavaTypeConstraintService describes constraints on the Java type that a property can reference, such as the kind of type (class, interface, etc.) and the types that the type must extend or implement. The information provided by this service is used for validation, content assist and other needs.
In majority of situations, the Java type constraint is static and should be configured using @JavaTypeConstraint annotation. The framework provides an implementation of JavaTypeConstraintService that works with this annotation.
Example
@Type( base = JavaTypeName.class )
@Reference( target = JavaType.class )
@JavaTypeConstraint( kind = JavaTypeKind.CLASS, type = "javax.servlet.Filter" )
ValueProperty PROP_SERVLET_FILTER_IMPL = new ValueProperty( TYPE, "ServletFilterImpl" );
ReferenceValue<JavaTypeName,JavaType> getServletFilterImpl();
void setServletFilterImpl( JavaTypeName value );
void setServletFilterImpl( String value );
When the Java type constraint varies due to runtime conditions, a custom implementation of JavaTypeConstraintService can be provided.
Example
public class CustomJavaTypeConstraintService extends JavaTypeConstraintService
{
@Override
protected void initJavaTypeConstraintService()
{
// Register listeners to invoke refresh() method when Java type constraint
// may have changed.
}
@Override
protected JavaTypeConstraintServiceData compute()
{
// Compute Java type constraint.
List<JavaTypeKind> kinds = new ArrayList<JavaTypeKind>();
List<String> types = new ArrayList<String>();
JavaTypeConstraintBehavior behavior;
...
return new JavaTypeConstraintServiceData( kinds, types, behavior );
}
@Override
public void dispose()
{
super.dispose();
// Remove any listeners that were added during initialization.
}
}
@Type( base = JavaTypeName.class )
@Reference( target = JavaType.class )
@Service( impl = CustomJavaTypeConstrainService.class )
ValueProperty PROP_SERVLET_FILTER_IMPL = new ValueProperty( TYPE, "ServletFilterImpl" );
ReferenceValue<JavaTypeName,JavaType> getServletFilterImpl();
void setServletFilterImpl( JavaTypeName value );
void setServletFilterImpl( String value );
The ListSelectionService functions as a conduit between the presentation layer and anything that may want to see or change the selection. The presentation layer pushes selection changes made by the user to ListSelectionService and at the same time listens for changes to selection coming from ListSelectionService.
An implementation of this service is provided with Sapphire. This service is not intended to be implemented by adopters.
Example
In this example, an action handler attaches a listener to the ListSelectionService to refresh action handler's enablement state when selection changes.
public class ExampleActionHandler extends SapphireActionHandler
{
@Override
public void init( SapphireAction action, ActionHandlerDef def )
{
super.init( action, def );
final ListSelectionService selectionService = action.getPart().service( ListSelectionService.class );
final Listener selectionListener = new Listener()
{
@Override
public void handle( Event event )
{
refreshEnablementState();
}
};
selectionService.attach( selectionListener );
attach
(
new Listener()
{
@Override
public void handle( Event event )
{
if( event instanceof DisposeEvent )
{
selectionService.detach( selectionListener );
}
}
}
);
}
}
The PossibleTypesService enumerates the possible child element types for a list or an element property. Each returned type is required to derive from the property's base type.
In majority of situations, the set of possible types is static and should be configured using @Type annotation. The framework provides an implementation of PossibleTypesService that works with this annotation.
Example
@Type( base = Shape.class, possible = { Circle.class, Triangle.class, Rectangle.class } )
ListProperty PROP_SHAPES = new ListProperty( TYPE, "Shapes" );
ModelElementList<Shape> getShapes();
When the set of possible types varies due to model extensibility or runtime conditions, a custom implementation of PossibleTypesService can be provided.
Example
public class ShapesPossibleTypesService extends PossibleTypesService
{
@Override
protected void initPossibleTypesService()
{
// Register listeners to invoke refresh() method when the list of possible types
// may have changed.
}
@Override
protected PossibleTypesServiceData compute()
{
// Compute the list of possible types.
List<ModelElementType> types = new ArrayList<ModelElementType>();
...
return new PossibleTypesServiceData( types );
}
@Override
public void dispose()
{
super.dispose();
// Remove any listeners that were added during initialization.
}
}
@Type( base = Shape.class )
@Service( impl = ShapesPossibleTypesService.class )
ListProperty PROP_SHAPES = new ListProperty( TYPE, "Shapes" );
ModelElementList<Shape> getShapes();
If the set of possible types is not specified via @Type annotation or via a custom PossibleTypesService implementation, the set of possible types is defined to be a singleton set composed of the property's base type.