- Java (J2SE) development:
- Experience with managing and using dependencies in Java.
- Experience with Object Oriented design and code.
- OSGi framework:
- Although this document provides details on OSC plugin creation with respect to OSGi, framework experience is helpful.
- Computer Networking:
- Knowledge of IP address, DNS and NAT environment.
- Understanding of web communication.
- REST APIs.
- Implementation and consumption of REST APIs.
- Java(JDK) 1.8+
- Java IDE, Eclipse preferred.
This guide describes how to develop and assemble an OSC plugin using Maven, Eclipse, and the Bndtools plugin for Eclipse. The plugin implementation will be created as a single OSGi bundle to be packaged along with its dependencies in a plugin archive suitable for deployment into the OSC server.
Like the creation of a JAR file, OSGi bundles can be easily created using a standard Maven project. It is important to note that the project needs to add a build plugin to generate the necessary OSGi metadata. In the following example, we assume that the plugin project has the groupId, org.osc.example
, and the artifactId example-manager-impl
.
The bnd-maven-plugin uses the bnd library to generate an OSGi manifest for your Maven project. It will also generate other OSGi metadata, such as Declarative Services component descriptors. Adding the bnd-maven-plugin to your project is simple. By default, the plugin binds to the process-classes lifecycle phase of your build using the following configuration:
<plugin>
<groupId>biz.aQute.bnd</groupId>
<artifactId>bnd-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>bnd-process</goal>
</goals>
</execution>
</executions>
</plugin>
Due to a bug in current versions of the maven-jar, Maven builds do not include manifest information generated by plugins by default. Therefore, the following build configuration must also be added:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestFile>${project.build.outputDirectory}/METAINF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
The bnd-maven-plugin accepts further configuration from a bnd.bnd file, or is included directly in the POM file as CDATA. The use of a bnd.bnd file is preferred, as the Bndtools plugin provides a convenient editor and additional validation. If no special configuration instructions are needed, no bnd file needs to be created.
In addition to using the bnd-maven-plugin, it is important to use caution when scoping the dependencies for your OSGi bundle project. In many Maven builds, large numbers of project dependencies are added with no thought for the scope at which they should be included.
Correctly scoping a dependency is a relatively simple process, and can be accomplished by the following the steps below:
- Is the dependency for an API provided by the platform, such as in the example of the OSGi framework API, or the OSC plugin API? If yes, then the scope is
provided
. - Is the dependency going to be repackaged inside the bundle being built by this project? If yes, then the scope is
provided
. - Is the dependency for a build-time annotation (e.g. OSGi’s @Component)? If yes, then the scope is
provided
. - Is the dependency for a service or specification implementation that is needed at runtime? If yes, then the scope is
runtime
. - Is the dependency for a library or service API that is needed for compilation? If yes, then the scope is
compile
. - Is the dependency needed to compile or run tests for the project? If yes, then the scope is
test
.
These rules for OSGi bundles are no different than those for non-OSGi JARs. When creating OSGi bundles in Maven however, it is increasingly important to scope your dependencies correctly. Projects that incorrectly declare their dependencies are more difficult to deploy, and can add significant overhead to the applications that attempt to use them.
Now that a basic OSGi project is set up, an OSC plugin implementation is needed.
Note: This tutorial refers mostly to an OSC Manager Plugin however, the process is similar for an SDN Controller plugin.
To implement a plugin, we must make the OSC Plugin API dependency available as a Maven dependency:
For Security Manager plugins:
<dependency>
<groupId>org.osc.api</groupId>
<artifactId>security-mgr-api</artifactId>
<version>1.0</version>
<scope>provided</scope>
</dependency>
For SDN Controller plugins:
<dependency>
<groupId>org.osc.api</groupId>
<artifactId>sdn-controller-api</artifactId>
<version>1.0</version>
<scope>provided</scope>
</dependency>
Note: The API dependency is pulled in as a
provided
scope dependency because the SDK should be provided by the OSC server (step 1 of the scope selection process) at runtime.
The manager plugin implementation can be created automatically in the IDE, and will appear similar to the following:
public class ExampleApplianceManager implements ApplianceManagerApi
{
@Override
public ManagerDeviceApi createManagerDeviceApi(
ApplianceManagerConnectorElement mc, VirtualSystemElement vs)
{
throw new UnsupportedOperationException(“Not implemented”);
}
// ...
}
The interface would be SdnControllerApi
for an SDN controller plugin.
The various methods of the API should be completed as appropriate, and will likely involve creating other classes that implement parts of the OSC SDK.
The OSC plugin implementations are shared with the OSC server runtime using the OSGi service registry. There are a number of ways to register an Object as an OSGi service, but the simplest and most flexible is to use an OSGi compendium specification called, Declarative Services.
Declarative Services allows build tools to create standard metadata describing how a component (a managed instance of an object) should be injected with dependencies and/or exposed as an OSGi service.
Declarative Services descriptors can be written by hand, but the simplest way to use Declarative Services is to generate the metadata from the standard annotations. The bnd-maven-plugin
already processes these annotations by default. All that is required is the addition of the dependency:
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.service.component.annotations</artifactId>
<version>1.3.0</version>
<scope>provided</scope>
</dependency>
Note: The annotations are pulled in as a
provided
scope dependency because they are for build-time processing only (step 3 of the scope selection process).
Once the Declarative Services annotations are available, you can annotate the ApplianceManagerApi
implementation type with the @Component
annotation to register it as a component.
Note: Because the implementation directly implements an interface, it will automatically be registered as an OSGi service using this interface. You also need to include all the required service properties to allow OSC to identify and correctly use this plugin. See Security Manager Plugin Properties and SDN Controller Plugin Properties for more details on the required properties for each of these plugin types.
@Component(
property={
PLUGIN_NAME + "=Example",
VENDOR_NAME + "=ExampleVendor",
SERVICE_NAME + "=ExampleService",
EXTERNAL_SERVICE_NAME + "=ExampleService",
AUTHENTICATION_TYPE + "=BASIC_AUTH",
NOTIFICATION_TYPE + "=CALLBACK_URL",
SYNC_SECURITY_GROUP + ":Boolean=false",
PROVIDE_DEVICE_STATUS + ":Boolean=true",
SYNC_POLICY_MAPPING + ":Boolean=true"}))
public class ExampleApplianceManager implements ApplianceManagerApi
{
// …
}
For an SDN Controller plugin:
- The
@Component
annotation should be applied to the class implementing theSdnControllerApi
interface. - You must also add the property
scope=Service.PROTOTYPE
as shown below:@Component(scope=ServiceScope.PROTOTYPE, property={/*…*/})
A Declarative Services XML descriptor will be generated and added to the bundle when building.
Note: As of the current specification (Declarative Services 1.3), a component must have a no-argument constructor.
Declarative Services components can only be activated when their mandatory dependencies are available. When a component becomes eligible for activation, it may not yet be ready for use although it is injected with all of its dependencies. Components may require some level of initialization after injection has finished. In Declarative Services, this can be requested by annotating a startup method with @Activate
. For example:
@Component(property={/*…*/})
public class ExampleApplianceManager implements ApplianceManagerApi
{
@Activate
void start() {
// Do some work in here
}
// …
}
There are a number of behaviors that can be relied upon in an activate method:
- Before the activate method is called, all static references in the component will have been bound and are therefore safe to use in the activate method.
- The component instance is not available to be called by another object until after the activate method call has returned.
- The activate method will be called once by the container on a given instance.
Activate methods should not block or steal the incoming thread as this risks deadlocking the system. Long running work should be started on a separate thread. Listening for incoming services should be done using @Reference
, and not by waiting for the service to arrive.
The corollary to the Activate method is the Deactivate method, which is marked using the @Deactivate annotation. Deactivation methods allow a component to run tidy up code when the component is being destroyed. Importantly:
- The deactivate method is called before any static references in the component are unbound, meaning that they are available to call if needed.
- The component instance has been released by any and all bundles that were referencing it, meaning that no future service method calls should be expected.
- The deactivate method will be called once by the container on a given instance.
Deactivate methods should not block or steal the incoming thread as this risks deadlocking the system. Furthermore, deactivate methods should halt any threads and close any resources held by the component. Failing to do so can cause leaks in the system, as no other actor is responsible for cleaning up these resources.
For example, OSC plugins will often want to use a REST client to communicate with a remote resource. The REST client can be created in the activate method and closed in the deactivate method.
@Component(property={/*…*/})
public class ExampleApplianceManager implements ApplianceManagerApi
{
private Client client;
@Activate
void start() {
client = ClientBuilder.newClient();
}
@Deactivate
void start() {
client.close();
}
// …
}
To generate a local index for the OSC plugin, it is necessary to gather the plugin and all of its runtime dependencies into a folder. This can be achieved using the copy-dependencies
goal of the maven-dependency-plugin
:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<configuration>
<includeScope>runtime</includeScope>
<outputDirectory>${project.build.directory}/plugin</outputDirectory>
</configuration>
<executions>
<execution>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<execution>
</executions>
</plugin>
Note: This task has been configured to put all of the compile and runtime dependencies in the
plugin
sub-folder of the build output. This collection step is a key reason why dependency scopes must be carefully managed. We must also make sure to add our plugin implementation project as a dependency so that the maven-dependency-plugin has some dependencies to gather:
<dependency>
<groupId>org.osc.example</groupId>
<artifactId>example-manager-impl</artifactId>
<version>1.0.0</version>
</dependency>
Indexing The Dependencies
Once the dependencies for the plugin have been gathered, they must be indexed:
<plugin>
<groupId>biz.aQute.bnd</groupId>
<artifactId>bnd-indexer-maven-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<inputDir>${project.build.directory}/plugin</inputDir>
<outputFile>${project.build.directory}/plugin/index.xml</outputFile>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>local-index</goal>
</goals>
<execution>
</executions>
</plugin>
This configuration creates a local index XML inside the same folder as the dependencies.
Once the dependencies have been gathered and the index generated, it is time to package the plugin binary.
The OSC plugin packaging format looks a lot like an OSGi bundle, in that it is a zip format archive that contains a manifest with identifying metadata. The metadata identifies the location of the XML index (usually contained within the archive), and if the index is local, the archive will also contain the indexed resources.
The following manifest headers are defined for the plugin packaging:
-
Deployment-Name
— This provides the name shown in the UI. It must also match the value provided in the propertyPLUGIN_NAME
of the plugin declaritive service. -
Deployment-SymbolicName
— This provides an identifier for the deployment. -
Deployment-Version
— This provides a version for the deployment. If not supplied, the version will default to 0.0.0 -
Index-Path
— This provides a URI to the index XML that should be used when resolving the deployment. URI paths (i.e. URIs with no scheme) are relative to the root of the bundle archive. The default value for this header isindex.xml
. -
Require-Bundle
— Provides a list of bundles that should be resolved and deployed using the index. This header has the same syntax as the standard Require-Bundle header for an OSGi bundle. This list is turned into a set of OSGi requirements, and then added to the list defined byRequire-Capability
. -
Require-Capability
— Provides a list of generic capabilities that should be resolved against the index, and then deployed.
The following provides information as to how the plugin can be packaged using the maven-antrun-plugin
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun—plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
<configuration>
<tasks>
<jar destfile=“${project.build.directory}/Example.bar”>
<fileset dir=“${project.build.directory}/plugin”/>
<manifest>
<attribute name=“Deployment-SymbolicName” value=“Example”/>
<attribute name=“Deployment-Version” value=“1.0.0”/>
<attribute name=“Require-Bundle” value=“org.osc.example.example-plugin-impl” />
</manifest>
</jar>
</tasks>
</configuration>
</plugin>
Use caution if you wish to use ${project.version}
in the Deployment-Version header. Most Maven release versions are compatible with OSGi version syntax, but SNAPSHOT versions are not. A simple, regular expression can fix up the version if needed. The following two examples demonstrate how to set an osgi.version
property that works with SNAPSHOT versions.
MAVEN
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.12</version>
<executions>
<execution>
<goals>
<goal>regex-property</goal>
</goals>
<configuration>
<name>osgi.version</name>
<value>${project.version}</value>
<regex>-SNAPSHOT</regex>
<replacement>.SNAPSHOT</replacement>
<failIfNoMatch>false</failIfNoMatch>
</configuration>
</execution>
</executions>
</plugin>
ANT
<loadresource property="osgi.safe.version">
<propertyresource name="project.version"/>
<filterchain>
<tokenfilter>
<replacestring from="-SNAPSHOT" to=".SNAPSHOT"/>
</tokenfilter>
</filterchain>
</loadresource>
Once the output file has been created, it must be attached as an output of the project using the build-helper-maven-plugin
:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.12</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>attach-artifact</goal>
</goals>
<configuration>
<artifacts>
<artifact>
<file>${project.build.directory}/Example.bar</file>
<type>bar</type>
</artifact>
</artifacts>
</configuration>
</execution>
</executions>
</plugin>
This maven build is now creating a valid plugin suitable for use in the OSC application. The .bar
file can now be uploaded to OSC as a valid plugin.