Declaring the Service
Declaring a component to define a service is similar to declaring a component to contribute to an existing extension point.
The first step is to create a dedicate XML file, named for instance, myproject-service-framework.xml
:
<?xml version="1.0"?>
<component name="org.mycompany.myproject.MyService">
</component>
This file should be declared in the bundle MANIFEST.
The second step is to create corresponding Java objects:
- Create a Java interface for this component service,
MyService.java
. Create a Java class for this component implementation,
MyComponent.java
, and make it extendorg.nuxeo.runtime.model.DefaultComponent
.package org.mycompany.myproject.api; public class MyComponent extends DefaultComponent implements MyService { }
NB: Most of the time, it is convenient to make the component instance also implement the service interface, but this is not strictly necessary. It is possible to separate concerns, especially if you'd like to define several services for this component: in this case, the service implementation will need to be kept as a field on the component instance, and returned by the
#getAdapter
method. Keep in mind that a given component implementation (and corresponding service implementation) are singletons inside the Nuxeo application.package org.mycompany.myproject; public class MyComponent extends DefaultComponent { protected final MyService service = new MyServiceImpl(); @Override public <T> T getAdapter(Class<T> adapter) { if (adapter.isAssignableFrom(MyService.class)) { return (T) this; } return null; } }
The service interface and the component implementation should then be referenced in the component declaration:
<?xml version="1.0"?>
<component name="org.mycompany.myproject.MyService">
<documentation>
This component can be documented here, HTML tags are accepted.
</documentation>
<implementation class="org.mycompany.myproject.MyComponent" />
<service>
<provide interface="org.mycompany.myproject.api.MyService" />
</service>
</component>
Only one component implementation can be given here. But multiple <provide>
elements can be added inside the <service>
element: the logic inside the #getAdapter
method just need to be changed to return the right Java object depending on the target interface:
<?xml version="1.0"?>
<component name="org.mycompany.myproject.MyService">
<implementation class="org.mycompany.myproject.MyComponent" />
<service>
<provide interface="org.mycompany.myproject.api.MyService" />
<provide interface="org.mycompany.myproject.api.MyOtherService" />
</service>
</component>
with:
public class MyComponent extends DefaultComponent {
protected final MyService service = new MyServiceImpl();
protected final MyOtherService otherService = new MyOtherServiceImpl();
@Override
public <T> T getAdapter(Class<T> adapter) {
if (adapter.isAssignableFrom(MyService.class)) {
return (T) service;
}
if (adapter.isAssignableFrom(MyOtherService.class)) {
return (T) otherService;
}
return null;
}
}
or:
public class MyComponent extends DefaultComponent implements MyService {
protected final MyOtherService otherService = new MyOtherServiceImpl();
@Override
public <T> T getAdapter(Class<T> adapter) {
if (adapter.isAssignableFrom(MyService.class)) {
return (T) this;
}
if (adapter.isAssignableFrom(MyOtherService.class)) {
return (T) otherService;
}
return null;
}
}
Now let's define an extension point for this component, to accept configurable objects, and maybe use them in the service API.
This involves creating a XMap object, here is a sample:
package org.mycompany.myproject.api;
@XObject("sample")
@XRegistry
public class SampleDescriptor {
@XNode("@id")
@XRegistryId
protected String id;
@XNode("@class")
protected String klass;
@XNode("title")
protected String title;
@XNodeList(value = "display/on", type = ArrayList.class, componentType = String.class)
protected List<String> displays;
@XNodeMap(value = "properties/property", key = "@name", type = HashMap.class, componentType = String.class)
protected Map<String, String> properties;
// add convenient getters here
}
This will allow parsing a contribution with a format similar to:
<sample id="myId" class="org.mycompany.myproject.contrib.MySampleImpl">
<title>My title</title>
<display>
<on>MyDisplay1</on>
<on>MyDisplay2</on>
</display>
<properties>
<property name="MyPropName1">MyPropValue1</property>
<property name="MyPropName2">MyPropValue2</property>
</properties>
</sample>
This class needs to be referenced in the component declaration for the target extension point:
<?xml version="1.0"?>
<component name="org.mycompany.myproject.MyService">
<!-- previous configuration here... -->
<extension-point name="myPoint">
<documentation>
This extension point can be documented here. HTML tags are accepted.
</documentation>
<object class="org.mycompany.myproject.api.SampleDescriptor" />
</extension-point>
</component>
The @XRegistry
annotation on this class will automatically create a registry to handle contributions. If the @XRegistryId
annotation is present, contributions will be identified by the corresponding String identifier field. Otherwise, the registry will assume that only one single contribution should be handled.
Default registries will handle merge, enablement, removal and hot-reload behaviors on contributions. Note that multiple descriptors can be defined on the same extension point, but a custom registry class will be needed in this case.
Now let's see how to access contributions in the Java code, and implement corresponding service logic.
When the component is started, all registries for their extension points are initialized: XML content is parsed and Java contributions can be retrieved thanks to helper methods:
public class MyComponent extends DefaultComponent {
protected static final XP_NAME = "myPoint";
@Override
public void start(ComponentContext context) {
super.start(context);
List<SampleDescriptor> descriptors = getRegistryContributions(XP_NAME);
// implement custom logic here
}
}
Calls to this registry can also be done directly when implementing the component service API (assuming these methods are not called during deployment, when registries might not have registered all contributions yet):
public class MyComponent extends DefaultComponent implements MyService {
protected static final XP_NAME = "myPoint";
@Override
public void myServiceMethod() {
List<SampleDescriptor> descriptors = getRegistryContributions(XP_NAME);
// implement custom logic here
}
@Override
public void myServiceMethodWithParam(String id) {
Optional<SampleDescriptor> descriptor = getRegistryContribution(XP_NAME, id);
// implement custom logic here
}
}
Existing API to retrieve contributions depends on the registry implementation, typically org.nuxeo.common.xmap.registry.MapRegistry
for contributions declaring an identifier, and org.nuxeo.common.xmap.registry.SingleRegistry
otherwise.
It is also possible to retrieve a registry outside of the component declaring the related extension point, by using the ComponentManager
API:
MapRegistry sampleReg = Framework.getRuntime()
.getComponentManager()
.getExtensionPointRegistry("org.mycompany.myproject.MyService", "myPoint")
.orElseThrow(() -> new IllegalArgumentException("Missing myPoint"));
At this point, the component is ready to receive contributions, and corresponding unit tests can pass.
Here is a full sample contribution to the target extension point:
<?xml version="1.0"?>
<component name="org.mycompany.myproject.MyService.testContrib">
<extension target="org.mycompany.myproject.MyService" point="myPoint">
<sample id="myId" class="org.mycompany.myproject.contrib.MySampleImpl">
<title>My title</title>
<display>
<on>MyDisplay1</on>
<on>MyDisplay2</on>
</display>
<properties>
<property name="MyPropName1">MyPropValue1</property>
<property name="MyPropName2">MyPropValue2</property>
</properties>
</exporter>
</extension>
</component>
Defining Properties on the Service Component
Defining an extension point is not strictly necessary if you just want to expose a service and do not need configurable elements. If some simple elements need to be configurable, there are several options:
Define a property on the service. Note that this property value will not be able to be overridden by another contribution, but it might be useful to keep that value in the XML component declaration:
<?xml version="1.0"?> <component name="org.mycompany.myproject.MyService"> <implementation class="org.mycompany.myproject.MyComponent" /> <property name="myProp" value="myValue" /> <!-- previous configuration here... --> </component>
From the component code, that property can be retrieved from the Runtime context, when activating the component:
public class MyComponent extends DefaultComponent { @Override public void activate(ComponentContext context) { String myPropValue = (String) context.getPropertyValue("myProp"); } }
Contribute a property to the default service
ConfigurationService
. This property can be defined alongside the component and service declarations. Note that this property value will be able to be overridden by another contribution, and will be able to be documented specifically:<?xml version="1.0"?> <component name="org.mycompany.myproject.MyService"> <implementation class="org.mycompany.myproject.MyComponent" /> <!-- previous configuration here... --> <extension target="org.nuxeo.runtime.ConfigurationService" point="configuration"> <documentation> This configuration property can be documented here. HTML tags are accepted. </documentation> <property name="org.mycompany.myproject.myprop">myDefaultValue</property> </extension> </component>
From the component code, that property can be retrieved from the Runtime framework, after starting the component, for instance (to make sure ConfigurationService is available):
import org.nuxeo.runtime.services.config.ConfigurationService; public class MyComponent extends DefaultComponent { protected static final String MY_PROP_NAME = "org.mycompany.myproject.myprop"; protected static final String DEFAULT_VALUE = "defaultValue"; @Override public void start(ComponentContext context) { super.start(context); String myPropValue = Framework.getService(ConfigurationService.class).getString(MY_PROP_NAME, DEFAULT_VALUE); } }
It can also be retrieved from other Java classes in the code, outside the component implementation.
Yet another alternative is possible if this configuration property needs to be changed easily depending on the infrastructure, from
nuxeo.conf
properties or Configuration Templates:public class MyComponent extends DefaultComponent { protected static final String MY_PROP_NAME = "org.mycompany.myproject.myprop"; protected static final String DEFAULT_VALUE = "defaultValue"; @Override public void start(ComponentContext context) { super.start(context); String myPropValue = Framework.getProperty(MY_PROP_NAME, DEFAULT_VALUE); } }
Last but not least, using the
ConfigurationService
and the Nuxeo property can be combined, by using a variable in the ConfigurationService contribution:<extension target="org.nuxeo.runtime.ConfigurationService" point="configuration"> <property name="org.mycompany.myproject.myprop"> ${org.mycompany.myproject.mypropFromNuxeoConf:=myDefaultValue} </property> </extension>
Retrieving this property from the code can be done through the same previous lookup
Framework.getService(ConfigurationService.class).getString(MY_PROP_NAME, DEFAULT_VALUE)
: during the deployment, if the Nuxeo propertyorg.mycompany.myproject.mypropFromNuxeoConf
is defined in thenuxeo.conf
or in configuration templates, this value will be used. Otherwise, the default value (after the:=
marker) will be resolved.Note that, using the same property name, a warning will be issued at startup, to avoid confusions, but the Nuxeo property value will still be taken into account.
Handling Dependencies
Sometimes, a component or service will interact with another. The Framework#getService
lookup will allow references from the Java code, but the Deployment Flow needs to be taken into account.
To make sure your component is resolved after another one, the <require>
tag can be used in the component declaration:
<?xml version="1.0"?>
<component name="org.mycompany.myproject.MyService">
<!-- multiple requirements can be added -->
<require>org.nuxeo.ecm.core.operation.OperationServiceComponent</require>
<require>org.mycompany.myproject.MyBaseService</require>
</component>
Note that, you do not need to require another component if you contribute to it: this dependency is implicit. In the previous example where a contribution was made to the org.nuxeo.runtime.ConfigurationService
extension point configuration
, no requirement is needed.
To make sure your component is started after another one, the #getApplicationStartedOrder
method should be implemented on the component:
public class MyComponent extends DefaultComponent {
@Override
public int getApplicationStartedOrder() {
return 700;
}
}
Components are sorted according to this reference value, in increasing order, before starting all components.
For "pure XML" components, only holding contributions, the default start value is 0
.
Otherwise, the default value is 1000
, and the repository initialization uses number 100
. Negative values can also be used.
If you'd like this start order to be relative to another one, the following lookup can also be done (for instance):
public class MyComponent extends DefaultComponent {
@Override
public int getApplicationStartedOrder() {
ComponentInstance requiredComponent = Framework.getRuntime().getComponentInstance("the.component.name");
if (requiredComponent == null || requiredComponent.getInstance() == null) {
return super.getApplicationStartedOrder();
}
return ((DefaultComponent) requiredComponent.getInstance()).getApplicationStartedOrder() + 1;
}
@Override
public void start(ComponentContext context) {
super.start(context);
// at this point, the required component #start method has already been called
}
}
As a reference, this declared start order is visible on the Explorer Component Listing.