Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

Difference between revisions of "Riena/Getting Started with injecting services and extensions"

(The Idea of Injecting Services and Extensions in Riena)
 
(44 intermediate revisions by 4 users not shown)
Line 1: Line 1:
[[Riena_Project | Riena Project]] > [[Riena_Getting_started | Getting Started]]> Getting Started with Injecting Services and Extensions
+
{{#eclipseproject:rt.riena}}
 +
{{RienaBreadcrumbs|
 +
[[Riena Project]] | [[Riena Getting started|Getting Started]] | Injecting Services and Extensions}}
  
== Getting Started with Injecting Services and Extensions ==
+
{| align="right"
 +
| __TOC__
 +
|}
 +
== The Idea of Injecting Services and Extensions in Riena ==
  
=== The Idea of Injecting Services and Extensions in Riena ===
+
Services and extensions are the main building blocks of [[Rich Client Platform|Eclipse RCP]] applications. There is a long list of [http://www.eclipsezone.com/articles/extensions-vs-services/ discussions] [http://www.eclipsezone.com/eclipse/forums/t90486.html and] [http://dev.eclipse.org/newslists/news.eclipse.technology.equinox/msg04687.html debates] about when to use services and when to use extensions. So, we will not add another discussion here. We think that both are useful and have their uses.
  
Services and extensions are the main building blocks of Eclipse RCP applications. There is a long list of discussions and debates about when to use services and when to use extensions. So, we will not add another discussion here. We think that both are useful and have their permission.
+
Both approaches require a lot of code for respecting their life cycle, which is prone to errors. Most of this code is embedded within the code that needs other services or extensions, like models, which one might prefer to be simple [http://en.wiktionary.org/wiki/POJO POJOs]. Concerns are not clearly separated, and unit testing is more difficult.
  
One problem with both of them is that using them requires a lot of code that respects their life cycle and is quite error prone. Most of this code is embedded within the code that needs other services or extensions. This approach does not clearly separate concerns and it makes unit testing more difficult.
+
Dependency injection frameworks such as [http://springframework.org/ Spring] or [http://hivemind.apache.org/ HiveMind] are commonly used to counter this problem. With '''[[Riena]]''' we offer a simpler approach to injecting both services and extensions into objects (pojos). The advantages:
 +
* code-based, i.e. no XML configuration files
 +
* [http://en.wikipedia.org/wiki/Fluent_interface fluent interfaces]
 +
* conventions that simplify the injection definitions.
  
A common solution for this problem is the use of dependency injection frameworks such as Spring (http://springframework.org/) or HiveMind (http://hivemind.apache.org/).
+
== Injecting Services ==
  
Within Riena we are using a simple to use approach to inject services and extensions into objects (pojos). Our approach is code based, i.e. no xml configuration files. Furthermore we are using fluent interfaces (http://en.wikipedia.org/wiki/Fluent_interface) and conventions to simplify the injection definitions.
+
The service injection allows to inject services into target objects (pojos). The selection of the services to be injected can either be done by service ranking or by using a filter expression.
  
Service injecting examples:
+
=== Using service ranking ===
<pre>
+
[http://www.osgi.org/javadoc/r4v41/org/osgi/framework/Constants.html#SERVICE_RANKING Service ranking] is a property of a service that can be supplied when registering a service. The default service ranking (i.e. when it is not supplied) is <tt>0</tt>. Riena defines most of its services with a ranking of <tt>-100</tt>. This has the effect that services without a specified ranking (thus using the default ranking) will override a Riena service.
Inject.service("ServiceClazz1").into(target).andStart(context);
+
<source lang="java">
Inject.service("ServiceClazz2").useFilter(filter).into(target).bind("register").unbind("unregister").andStart(context);
+
Inject.service("org.eclipse.riena.sample.app.common.model.ICustomerSearch").useRanking().into(target).andStart(context);
Inject.service("ServiceClazz3").useRanking().into(target).bind("register").unbind("unregister").andStart(context);
+
</source>
</pre>
+
or shorter
where ´target´ is the pojo that gets a service injected. When services come and go they will be injected into or removed from the pojos. The method names for injecting/removing can be specified with the bind() and unbind() methods. If they are not defined default method names are assumed.
+
<source lang="java">
 +
Inject.service(ICustomerSearch.class).useRanking().into(target).andStart(context);
 +
</source>
 +
where ''target'' is the pojo into which the service shall be injected. In this example, ''target'' could be a <tt>Customer</tt> model.
  
Extensions get injected into pojos as instances of an interface. The interfaces need only to declare the <i>getters</i>. Dynamic proxies will be created for the interfaces which map the data from the extensions. A few simple rules are used for the mapping from extensions to the interfaces (lightweight xml/java mapping).
+
Within the target we need to implement the binding and unbinding methods, which both take an instance of the injected service's type as their only parameter. Since we did not specify any custom method names, Riena expects the two methods to be named "bind" and "unbind", respectively:
Extension properties. i.e. attributes, sub-elements and element value can then accessed by <i>getters</i> in the interface definition. It is only necessary to define the interfaces for the mapping.
+
<source lang="java">
 +
...
 +
public void bind(ICustomerSearch service) {
 +
this.service = service;
 +
}
  
The extension injector does not evaluate the extension schema, so it can only trust that the extension and the interface match with regards to a few basic rules:
+
public void unbind(ICustomerSearch service) {
* one interface maps to one extension element type
+
this.service = null;
* the interface has to be annotated with <code>@ExtensionInterface</code>
+
}
* interface methods can contain <i>getters</i> prefixed with:
+
...
** get...
+
</source>
** is...
+
** create...
+
* Such prefixed methods enforce a default mapping to attribute or element names, i.e. the remainder of the methods name is interpreted as the attribute or element name. A simple name mangling is performed, e.g for the method getDatabaseURL the mapping looks for the attribute name databaseURL.
+
To enforce another name mapping a method can be annotated with @MapName("name"). The name specifies the name of the element or attribute. The extension element´s value can be retrieved by annotating the method with @MapValue(). The return type must be String. The method names of such annotated methods can be arbitrary
+
  
 +
=== Using filters ===
 +
The other possibility is to use an [http://www.faqs.org/rfcs/rfc1960.html RFC 1960]-based filter expression to select the services to be injected.
 +
<source lang="java">
 +
Inject.service(ISomeService.class).into(target).andStart(context);
 +
Inject.service("org.eclipse.riena.ISomeService").useFilter("(remote=true)").into(target).bind("register").unbind("unregister").andStart(context);
 +
</source>
 +
The <tt>andStart(...)</tt> method starts the injector. From now on the service injector will track the specified services, and when services come and go they will be injected into or removed from the pojos. The method names for injecting/removing can be specified with the <code>bind("myBindMethod")</code> and <code>unbind("myUnbindMethod")</code> methods. If they are not defined, default method names will be used, currently "bind" and "unbind", which is why we needed to implement the two methods with these names in the ''target'', see listing above.
 +
 +
When service ranking is used, ''at most one'' service will be injected. When the filter-based approach is used, multiple services or none can be injected. In the case of multiple services, the bind/unbind methods will be called multiple times; the target object must keep track of the services injected.
 +
 +
To stop the injection, it is possible to call the <tt>stop()</tt> method on the injector at any time. This causes all injected services to be unbound. However, when a bundle associated with an injector via the <tt>andStart(...)</tt> method stops, all injected services will be stopped as well.
 +
 +
=== Syntax summary / Cheatsheet ===
 +
 +
<source lang="java">
 +
Inject
 +
.service( IMyService.class | "org.eclipse.myproject.IMyService" )
 +
[.useRanking() | .useFilter("(remote=true)") ]
 +
.into(myTargetObject)
 +
[.bind("myBindMethod")]
 +
[.unbind("myUnbindMethod")]
 +
.andStart(myContext)
 +
</source>
 +
 +
== Injecting Extensions ==
 +
 +
Extensions get injected into pojos as instances of an interface. The interfaces need only declare the ''getters''. Dynamic proxies will be created for the interfaces which map the data from the extensions. A few simple rules are used for the mapping from extensions to the interfaces (lightweight XML/Java mapping). Extension properties. i.e. attributes, sub-elements and element value can then accessed by ''getters'' in the interface definition. It is only necessary to define the interfaces for the mapping.
 +
 +
=== Conformity to the extension schema ===
 +
The extension injector does ''not'' evaluate the extension schema, so it can only trust that the extension and the interface match with regards to a few basic rules:
 +
* One interface maps to one extension element type.
 +
* The interface must be annotated with <code>@ExtensionInterface</code>.
 +
* Interface methods can contain ''getters'' prefixed with:
 +
** <code>get</code>...
 +
** <code>is</code>...
 +
** <code>create</code>...
 +
* Such prefixed methods enforce a default mapping to attribute or element names, i.e. the remainder of the methods name is interpreted as the attribute or element name. Simple name mangling is performed, e.g for the method <code>getDatabaseURL()</code> the mapping looks for the attribute named <tt>databaseURL</tt>.
 +
: To enforce another name mapping a method can be annotated with <code>@MapName("name")</code>. The name specifies the name of the element or attribute. The extension element's content can be retrieved by annotating the method with <code>@MapContent()</code>. The return type must be <code>String</code>. The method names of such annotated methods can be arbitrary.
 
* The return type of a method indicates how the value of an attribute will be converted. If the return type is
 
* The return type of a method indicates how the value of an attribute will be converted. If the return type is
** a <i>primitive</i> type or <code>java.lang.String</code> than the mapping converts the attribute's value to the corresponding type.
+
*; a ''primitive'' type or <code>java.lang.String</code> : then the mapping converts the attribute's value to the corresponding type.
** an interface or an array of interfaces annotated with <code>@ExtensionInterface</code> than the mapping tries to resolve to a nested element or to nested elements.
+
*; an interface or an array of interfaces annotated with <code>@ExtensionInterface</code> : then the mapping tries to resolve to a nested element or to nested elements.
** <code>org.osgi.framework.Bundle</code> than the method returns the contributing bundle.
+
*; <code>org.osgi.framework.Bundle</code> : then the method returns the contributing bundle.
** <code>java.lang.Class</code> than the attribute is interpreted as a class name and a class instance will be returned.
+
*; <code>java.lang.Class</code> : then the attribute is interpreted as a class name and a class instance will be returned.
** and finally if none of the above matches the mapping tries to create an new instance of the attribute´s value (interpreted as class name) each time it is called. If the extension attribute is not defined <code>null</code> will be returned.
+
*; and finally, if none of the above matches : the mapping tries to create an new instance of the attribute's value (interpreted as class name) each time it is called. If the extension attribute is not defined <code>null</code> will be returned.
  
Extension injecting examples:
+
=== Examples of extension injection ===
<pre>
+
This is how you would inject an extension into a pojo ''target'':
 +
<source lang="java">
 
Inject.extension("extpoint1").into(target).andStart(context);
 
Inject.extension("extpoint1").into(target).andStart(context);
Inject.extension("extpoint2").useType(interface).into(target).bind("configure").doNotReplaceSymbols().andStart(context);
+
Inject.extension("extpoint2").useType(interface).into(target).update("configure").doNotReplaceSymbols().andStart(context);
Inject.extension("extpoint3").expectExactly(1).into(target).andStart(context);
+
Inject.extension("extpoint3").expectingExactly(1).into(target).andStart(context);
</pre>
+
</source>
 +
 
 +
A sample interface from Riena:
 +
<source lang="java">
 +
/**
 +
* Interface for a SubModule extension that defines how an activated submodule
 +
* appears in the work area.
 +
*/
 +
@ExtensionInterface
 +
public interface ISubModuleExtension {
 +
 
 +
/**
 +
* @return A controller that controlles the UI widgets in the view through
 +
*        ridgets (see org.eclipse.riena.ui.internal.ridgets.IRidget)
 +
*/
 +
IController createController();
 +
 
 +
/**
 +
* Indicates whether the view is shared i.e. whether one instance of the
 +
* view should be used for all submodule instances.
 +
*
 +
* @return true if the specified view should be a shared view, false
 +
*        otherwise
 +
*/
 +
boolean isShared();
 +
 
 +
/**
 +
* @return For the SWT-based Riena UI this is the ID of the view associated
 +
*        with the submodule. Must match the ID field of an
 +
*        "org.eclipse.ui.view" extension.
 +
*/
 +
String getView();
 +
 
 +
/**
 +
* Return the contributing bundle of the extension.
 +
*
 +
* @return The contributing bundle
 +
*/
 +
Bundle getContributingBundle();
 +
 
 +
/**
 +
* @return The type part of the ID of a navigation node.
 +
* @see NavigationNodeId#getTypeId()
 +
*/
 +
String getTypeId();
 +
 
 +
}
 +
</source>
 +
 
 +
By executing:
 +
<source lang="java">
 +
Inject.extension("org.eclipse.riena.navigation.subModule").useType(ISubModuleExtension.class).into(target).andStart(Activator.getDefault().getContext());
 +
</source>
 +
where <code>target</code> is an instance of <code>SubModuleExtensionInjectionHelper</code>, the extension gets injected into:
 +
<source lang="java">
 +
public class SubModuleExtensionInjectionHelper {
 +
private ISubModuleExtension[] data;
 +
 
 +
public void update(ISubModuleExtension[] data) {
 +
this.data = data.clone();
 +
}
 +
 
 +
public ISubModuleExtension[] getData() {
 +
return data.clone();
 +
}
 +
}
 +
</source>
 +
 
 +
=== Syntax summary / Cheatsheet ===
 +
<source lang="java">
 +
Inject
 +
.extension( ["org.eclipse.myproject.myExtensionPointId"] )
 +
[.expectingExactly(int) | .expectingMinMax(int, int)]
 +
[.useType(MyExtensionClass.class)]
 +
.into(myTargetObject)
 +
[.update("myUpdateMethod")]
 +
.andStart(myContext)
 +
</source>
 +
The above is incomplete, it does not show all the possible ways of configuring and using extension injection. It is merely intended as a rough guide. Look up the Javadoc in <code>ExtensionDescriptor</code> for documentation on <code>heterogeneous()</code>, <code>isHomogeneous()</code>, <code>getMin/MaxOccurrences()</code>, <code>requiresArrayUpdateMethod()</code>, and in <code>ExtensionInjector</code> for <code>specific()</code>, <code>doNotReplaceSymbols()</code>.
 +
 
 +
== Next: Wiring ==
 +
See how you can avoid using <code>Inject.…</code> sentences in your controllers by [[Riena Getting Started with Wiring|using Riena's Wiring mechanism]].
 +
 
  
 
[[Category:Riena]]
 
[[Category:Riena]]

Latest revision as of 03:39, 8 July 2011

{{#eclipseproject:rt.riena}}

Riena ProjectGetting Started ▶ Injecting Services and Extensions

The Idea of Injecting Services and Extensions in Riena

Services and extensions are the main building blocks of Eclipse RCP applications. There is a long list of discussions and debates about when to use services and when to use extensions. So, we will not add another discussion here. We think that both are useful and have their uses.

Both approaches require a lot of code for respecting their life cycle, which is prone to errors. Most of this code is embedded within the code that needs other services or extensions, like models, which one might prefer to be simple POJOs. Concerns are not clearly separated, and unit testing is more difficult.

Dependency injection frameworks such as Spring or HiveMind are commonly used to counter this problem. With Riena we offer a simpler approach to injecting both services and extensions into objects (pojos). The advantages:

  • code-based, i.e. no XML configuration files
  • fluent interfaces
  • conventions that simplify the injection definitions.

Injecting Services

The service injection allows to inject services into target objects (pojos). The selection of the services to be injected can either be done by service ranking or by using a filter expression.

Using service ranking

Service ranking is a property of a service that can be supplied when registering a service. The default service ranking (i.e. when it is not supplied) is 0. Riena defines most of its services with a ranking of -100. This has the effect that services without a specified ranking (thus using the default ranking) will override a Riena service.

Inject.service("org.eclipse.riena.sample.app.common.model.ICustomerSearch").useRanking().into(target).andStart(context);

or shorter

Inject.service(ICustomerSearch.class).useRanking().into(target).andStart(context);

where target is the pojo into which the service shall be injected. In this example, target could be a Customer model.

Within the target we need to implement the binding and unbinding methods, which both take an instance of the injected service's type as their only parameter. Since we did not specify any custom method names, Riena expects the two methods to be named "bind" and "unbind", respectively:

...
public void bind(ICustomerSearch service) {
	this.service = service;
}
 
public void unbind(ICustomerSearch service) {
	this.service = null;
}
...

Using filters

The other possibility is to use an RFC 1960-based filter expression to select the services to be injected.

Inject.service(ISomeService.class).into(target).andStart(context);
Inject.service("org.eclipse.riena.ISomeService").useFilter("(remote=true)").into(target).bind("register").unbind("unregister").andStart(context);

The andStart(...) method starts the injector. From now on the service injector will track the specified services, and when services come and go they will be injected into or removed from the pojos. The method names for injecting/removing can be specified with the bind("myBindMethod") and unbind("myUnbindMethod") methods. If they are not defined, default method names will be used, currently "bind" and "unbind", which is why we needed to implement the two methods with these names in the target, see listing above.

When service ranking is used, at most one service will be injected. When the filter-based approach is used, multiple services or none can be injected. In the case of multiple services, the bind/unbind methods will be called multiple times; the target object must keep track of the services injected.

To stop the injection, it is possible to call the stop() method on the injector at any time. This causes all injected services to be unbound. However, when a bundle associated with an injector via the andStart(...) method stops, all injected services will be stopped as well.

Syntax summary / Cheatsheet

Inject
.service( IMyService.class | "org.eclipse.myproject.IMyService" )
[.useRanking() | .useFilter("(remote=true)") ]
.into(myTargetObject)
[.bind("myBindMethod")]
[.unbind("myUnbindMethod")]
.andStart(myContext)

Injecting Extensions

Extensions get injected into pojos as instances of an interface. The interfaces need only declare the getters. Dynamic proxies will be created for the interfaces which map the data from the extensions. A few simple rules are used for the mapping from extensions to the interfaces (lightweight XML/Java mapping). Extension properties. i.e. attributes, sub-elements and element value can then accessed by getters in the interface definition. It is only necessary to define the interfaces for the mapping.

Conformity to the extension schema

The extension injector does not evaluate the extension schema, so it can only trust that the extension and the interface match with regards to a few basic rules:

  • One interface maps to one extension element type.
  • The interface must be annotated with @ExtensionInterface.
  • Interface methods can contain getters prefixed with:
    • get...
    • is...
    • create...
  • Such prefixed methods enforce a default mapping to attribute or element names, i.e. the remainder of the methods name is interpreted as the attribute or element name. Simple name mangling is performed, e.g for the method getDatabaseURL() the mapping looks for the attribute named databaseURL.
To enforce another name mapping a method can be annotated with @MapName("name"). The name specifies the name of the element or attribute. The extension element's content can be retrieved by annotating the method with @MapContent(). The return type must be String. The method names of such annotated methods can be arbitrary.
  • The return type of a method indicates how the value of an attribute will be converted. If the return type is
    a primitive type or java.lang.String 
    then the mapping converts the attribute's value to the corresponding type.
    an interface or an array of interfaces annotated with @ExtensionInterface 
    then the mapping tries to resolve to a nested element or to nested elements.
    org.osgi.framework.Bundle 
    then the method returns the contributing bundle.
    java.lang.Class 
    then the attribute is interpreted as a class name and a class instance will be returned.
    and finally, if none of the above matches 
    the mapping tries to create an new instance of the attribute's value (interpreted as class name) each time it is called. If the extension attribute is not defined null will be returned.

Examples of extension injection

This is how you would inject an extension into a pojo target:

Inject.extension("extpoint1").into(target).andStart(context);
Inject.extension("extpoint2").useType(interface).into(target).update("configure").doNotReplaceSymbols().andStart(context);
Inject.extension("extpoint3").expectingExactly(1).into(target).andStart(context);

A sample interface from Riena:

/**
 * Interface for a SubModule extension that defines how an activated submodule
 * appears in the work area.
 */
@ExtensionInterface
public interface ISubModuleExtension {
 
	/**
	 * @return A controller that controlles the UI widgets in the view through
	 *         ridgets (see org.eclipse.riena.ui.internal.ridgets.IRidget)
	 */
	IController createController();
 
	/**
	 * Indicates whether the view is shared i.e. whether one instance of the
	 * view should be used for all submodule instances.
	 * 
	 * @return true if the specified view should be a shared view, false
	 *         otherwise
	 */
	boolean isShared();
 
	/**
	 * @return For the SWT-based Riena UI this is the ID of the view associated
	 *         with the submodule. Must match the ID field of an
	 *         "org.eclipse.ui.view" extension.
	 */
	String getView();
 
	/**
	 * Return the contributing bundle of the extension.
	 * 
	 * @return The contributing bundle
	 */
	Bundle getContributingBundle();
 
	/**
	 * @return The type part of the ID of a navigation node.
	 * @see NavigationNodeId#getTypeId()
	 */
	String getTypeId();
 
}

By executing:

Inject.extension("org.eclipse.riena.navigation.subModule").useType(ISubModuleExtension.class).into(target).andStart(Activator.getDefault().getContext());

where target is an instance of SubModuleExtensionInjectionHelper, the extension gets injected into:

public class SubModuleExtensionInjectionHelper {
	private ISubModuleExtension[] data;
 
	public void update(ISubModuleExtension[] data) {
		this.data = data.clone();
	}
 
	public ISubModuleExtension[] getData() {
		return data.clone();
	}
}

Syntax summary / Cheatsheet

Inject
.extension( ["org.eclipse.myproject.myExtensionPointId"] )
[.expectingExactly(int) | .expectingMinMax(int, int)]
[.useType(MyExtensionClass.class)]
.into(myTargetObject)
[.update("myUpdateMethod")]
.andStart(myContext)

The above is incomplete, it does not show all the possible ways of configuring and using extension injection. It is merely intended as a rough guide. Look up the Javadoc in ExtensionDescriptor for documentation on heterogeneous(), isHomogeneous(), getMin/MaxOccurrences(), requiresArrayUpdateMethod(), and in ExtensionInjector for specific(), doNotReplaceSymbols().

Next: Wiring

See how you can avoid using Inject.… sentences in your controllers by using Riena's Wiring mechanism.

Back to the top