Skip to content

Client Library Introduction & Tutorial

Thomas Pedersen edited this page Mar 13, 2019 · 17 revisions

This document is currently a work-in-progress and thus is both incomplete and barely tested.

Background: Skeletons versus Library

Currently, the Arrowhead consortium provides application system developers with client skeletons. These are stand-alone code examples of how to implement the communication with the Arrowhead Core Systems. These skeletons are intended to be downloaded and then modified to fit the developers' current project. This approach allows for maximum flexibility as the developers can easily modify any part of the code.

However, using skeletons also come at a cost. The skeletons are essentially copied for every new application system made. The developers therefore leave numerous cases of code duplication in their wake. As any good practices in software development will state, code duplication is generally bad - or at the very least a bad smell. One of the more notable reasons being the issue of handling bugs. For example, if we say that serious security one were to be found in these skeletons, hundreds of application systems will have to be manually updated to fix it - most likely leaving Arrowhead in a state where many application systems have not been updated thus will have the security issue.

The client library is an alternative to the skeletons, proposed to reduce code duplication, make it easier to implement application systems, and allow the consortium to update the client code without requiring any actions by the application system developers (well, in some cases). Where skeletons are code that are downloaded and modified, libraries are not modified by the application system developers. They can therefore be distributed in a binary format and linked dynamically at runtime. As a result, the library can be updated without any modification to the application systems, as long as the API is either left intact or backward compatible.

On the plus side, a library reduces code duplication, but it also introduces a bit of rigidity. Where code can easily be modified in a skeleton, a library would have to explicitly support modifications, for example by allowing application system developers to inherit (and thereby override methods) or supply listeners to modify behaviour.

The client library described below have been designed to simplify the develop of application systems, yet make it clear for the developers when they are communicating with an Arrowhead Core System - and which one. It will therefore contain a number of classes with names ending in with Client, like the OrchestrationClient. These emphasises the communication with a core system and let the developer apply generic Arrowhead theory, which states that a consumer should look up the provider using the Orchestration core system. The library has also been designed to support a wide range of different use cases, letting the application system developers tailor its behaviour to their needs. However, it has only been tested on a very few limited use cases so far, and thus please do not hesitate to report missing features or inconveniences, for example through GitHub issues.

Development Repository and Branches

The Client library is developed under the official Arrowhead Client repository in the refactored branches, currently feature/refactored+common is the most recent head, which (in contrast to feature/refactored) supports installation of application systems with Debian packages. Note, that this branch matches with the Core repository branch feature/common, where the two in combination will allow core and client systems to be installed side-by-side on the same Debian-based server.

Installation

This guide is written for Linux, for other operating systems you may have to adapt some of the commands yourself. Also, I assume you are familiar with Git and Maven.

The following procedure describes how application system developers should currently download and install the library. For a guide on how to develop on the client library (in combination with the core system), see: Setting up an Intellij IDEA project for Arrowhead development. Note that building application systems with Maven (even through IntelliJ IDEA) requires the mvn install step below.

Currently, no binary packages are provided. Ideally, the Client library should be provided within a Maven repository for developers to easily include this in their projects - preferably the official one, but alternatively in a 3rd party repository by the consortium. However, for now, the only way is to download the code and compile it for yourself:

git clone --depth 1 \
          --branch feature/refactored+common \
          https://github.com/arrowhead-f/client-java.git
cd client-java
mvn compile

To include the Client library in other Maven projects, install it into your local Maven repository:

mvn install

When generating Debian install packages for your application systems, you will also need the packages for the client library. These can be generated with:

mvn package

The packages can be found inside the target folder. Note, that this will generate both the arrowhead-common and the arrowhead-client-common packages. The arrowhead-common package is the same as generated for the core systems (assuming you used the feature/common branch), and thus is only required for systems not already having the core systems installed. Clients, built with this client library, should work against all (recent) branches of the core systems, you will only need the feature/common branch to install core and client systems side-by-side.

When deploying your application systems, install the common packages first:

dpkg -i arrowhead-client-common_*.deb arrowhead-common_*.deb

The Demo Project

To illustrate the basic use of the Client Library, we can redo the four skeleton examples. For simplicity I will skip the import lines, but they should be pretty straight forward to add. The full example is available in the System of System repository under the feature/refactored branch.

Let us call this project for "Demo", and make a parent pom.xml for it:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>eu.arrowhead</groupId>
  <artifactId>demo</artifactId>
  <version>4.1.1-SNAPSHOT</version>
  <packaging>pom</packaging>

  <modules>
    <module>client-demo-common</module>
    <module>client-demo-consumer</module>
    <module>client-demo-provider</module>
    <module>client-demo-publisher</module>
    <module>client-demo-subscriber</module>
  </modules>

  <properties>
    <mvn.compiler.version>3.8.0</mvn.compiler.version>
    <mvn.compile.target>1.8</mvn.compile.target>
    <maven.jar.version>3.1.1</maven.jar.version>
    <maven.dependency.version>3.1.1</maven.dependency.version>
    <arrowhead.version>4.1.1-SNAPSHOT</arrowhead.version>
  </properties>

  <dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>eu.arrowhead</groupId>
            <artifactId>arrowhead-client-common</artifactId>
            <version>${arrowhead.version}</version>
        </dependency>
        <dependency>
            <groupId>eu.arrowhead.demo</groupId>
            <artifactId>client-demo-common</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>
  </dependencyManagement>

  <build>
    <defaultGoal>clean install</defaultGoal>
    <plugins>
        <plugin>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>${mvn.compiler.version}</version>
            <configuration>
                <source>${mvn.compile.target}</source>
                <target>${mvn.compile.target}</target>
            </configuration>
        </plugin>
    </plugins>
  </build>
</project>

The Demo Common

Most projects will need a module for sharing common classes and functionality. For this Demo project we will create a client-demo-common package to hold the data model used in the communication. The pom.xml file is pretty straight forward:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>demo</artifactId>
    <groupId>eu.arrowhead</groupId>
    <version>4.1.1-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <groupId>eu.arrowhead.demo</groupId>
  <artifactId>client-demo-common</artifactId>
</project>

Here we add a single class, describing a temperature readout:

package eu.arrowhead.demo.model;

public class Temperature {
  
  private long timestamp;
  private double temperature;

  public Temperature() {
  }

  public Temperature(long timestamp, double temperature) {
    this.timestamp = timestamp;
    this.temperature = temperature;
  }

  public long getTimestamp() {
    return timestamp;
  }

  public void setTimestamp(long timestamp) {
    this.timestamp = timestamp;
  }

  public double getTemperature() {
    return temperature;
  }

  public void setTemperature(double temperature) {
    this.temperature = temperature;
  }
}

The Demo Provider

Moving ahead to our first Application system, we first have to define the pom.xml for it. This should refer to client library and the demo common library as dependencies:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>demo</artifactId>
    <groupId>eu.arrowhead</groupId>
    <version>4.1.1-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <groupId>eu.arrowhead.demo</groupId>
  <artifactId>client-demo-provider</artifactId>

  <dependencies>
    <dependency>
      <groupId>eu.arrowhead</groupId>
      <artifactId>arrowhead-client-common</artifactId>
    </dependency>
    <dependency>
      <groupId>eu.arrowhead.demo</groupId>
      <artifactId>client-demo-common</artifactId>
    </dependency>
  </dependencies>
</project>

A provider basically consists of two classes, a main class and a resource class. The resource class defines the webservice and should be quiet familiar if you are experienced with java webservices:

package eu.arrowhead.demo.provider;

@Path("temperature")
@Produces(MediaType.APPLICATION_JSON)
public class TemperatureResource extends ArrowheadResource {

  public TemperatureResource(ArrowheadHttpServer server) throws ArrowheadException {
    super(server);
  }

  @GET
  public Response get() {
    Temperature readout = new Temperature(System.currentTimeMillis(), 1);
    return Response.status(200).entity(readout).build();
  }
}

Notes:

  1. It should extend ArrowheadResource and provide a matching constructor, passing the arguments along. While we are not using any of this functionality in this simple example, it is useful for more complicated systems as it allows us to consume services and perform other more complicated tasks in our provider.

  2. The get() method constructs our dummy temperature response, and replies to any requests.

  3. Security is implicitly added through a security filter in the web server, thus on secure clouds get() will only be called if the caller provides a valid token and signature in accordance with the Arrowhead specification. To change security method, it is possible to override the security filter when configuring the web server in the main class.

Now, to the main class:

package eu.arrowhead.demo.provider;

public class ProviderMain extends ArrowheadApplication {

  public ProviderMain(String[] args) throws ArrowheadException {
    super(args);
  }

  @Override
  protected void onStart() throws ArrowheadException {
    final ArrowheadSecurityContext securityContext = ArrowheadSecurityContext
        .createFromProperties();
    final ArrowheadHttpServer server = ArrowheadGrizzlyHttpServer
        .createFromProperties(securityContext)
        .addResources(TemperatureResource.class)
        .addPackages("eu.arrowhead.demo")
        .start();

    final ServiceRegistryClient registry = ServiceRegistryClient
        .createFromProperties(securityContext);
    registry.register(ServiceRegistryEntry.createFromProperties(server));
  }

  @Override
  protected void onStop() {

  }

  public static void main(String[] args) throws ArrowheadException {
    new ProviderMain(args).start();
  }
}

Notes:

  1. The main class should inherit from the ArrowheadApplication class.

  2. Add a matching constructor which just parses the parameters along.

  3. Implement the onStart() method, this will be called automatically by ArrowheadApplication when it is ready for your application system to start:

    1. Create a security context - you pretty much always need one of these. Notice the use of the static method createFromProperties() which constructs the object based on the information in the configuration file (shown below). Many library classes implements these kinds of methods. It is also possible to configure them directly in the code or using another set of properties.

    2. Create and start the HTTP server, again we use the createFromProperties() method, add our temperature resource class, and add the package containing the model classes.

    3. Finally, our new provider is registered in the Service Registry, by constructing a client to it and calling register() with the Entry for our system. Everything created from the data in the configuration file.

  4. Implement the onStop() method. No need to do anything here. Classes inheriting from ArrowheadServer will have their onStop() methods called automatically. This includes the ArrowheadGrizzlyHttpServer instance we just started, as it also inherits from ArrowheadServer. However, if we started something not from the client library, it should be stopped here.

  5. Finally, we add a main method for creating the application and calling start(), which is an inherited method from ArrowheadApplication.

The configuration file default.conf, from which the create methods fetches the information:

# Directories
cert_dir=config/certificates

# Webserver parameters
#address=0.0.0.0
port=8461
secure=true

# System registry entry parameters
system_name=client-demo-provider
service_name=temperature
service_uri=temperature
interfaces=JSON, XML
service_metadata=unit-celsius

# Key and trust stores
auth_pub=authorization.pub
keystore=client-demo-provider.p12
keystorepass=123456
truststore=truststore.p12
truststorepass=123456
keypass=123456

# Core systems
#sr_address=0.0.0.0
#eh_address=0.0.0.0
#ca_address=0.0.0.0
#orch_address=0.0.0.0

# Logging parameters
#log4j.rootLogger=INFO, CONSOLE
#log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
#log4j.appender.CONSOLE.target=System.err
#log4j.appender.CONSOLE.ImmediateFlush=true
#log4j.appender.CONSOLE.Threshold=debug
#log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
#log4j.appender.CONSOLE.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss}, %l, %p, %m%n

The configuration file is essentially a Java properties file with a list of key/value pairs. Any comment line (starting with #) means that the client library will use the default value - this are defined in the class ArrowheadProperties. A quick run through the settings above:

  • cert_dir: Location of the certificates.
  • address, port: The webserver will bind to this IP and port.
  • secure: Whether to start the system in secure or insecure mode.
  • system_name: Name of your system.
  • service_name, service_uri, interfaces, service_metadata: Strings for the service registry entry we sent when registering our service.
  • auth_pub, keystore, truststore: Keys and truststore for secure mode.
  • keystorepass, truststorepass, keypass: Passwords for keys and truststore.
  • sr_address, eh_address, ca_address, orch_address: Addresses of the core systems.
  • log4j.*: Logging parameters.

Note that you can create a file app.conf beside default.conf to override values in the latter. This is useful for avoiding passwords and other system specific values inversion control systems when developing for example.

Notes on running the provider:

  1. Configuration files will be loaded from the current working directory per default.

  2. Use the VM option -DconfDir=config-insecure to change the configuration directory to config-insecure under the current working directory.

  3. Use the program arguments -daemon and -d to start in daemon and debug mode.

The Demo Consumer

The pom.xmlfor the consumer is essentially the same as for the provider:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <parent>
    <artifactId>demo</artifactId>
    <groupId>eu.arrowhead</groupId>
    <version>4.1.1-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <groupId>eu.arrowhead.demo</groupId>
  <artifactId>client-demo-consumer</artifactId>

  <dependencies>
    <dependency>
      <groupId>eu.arrowhead</groupId>
      <artifactId>arrowhead-client-common</artifactId>
    </dependency>
    <dependency>
      <groupId>eu.arrowhead.demo</groupId>
      <artifactId>client-demo-common</artifactId>
    </dependency>
  </dependencies>
</project>

The consumer is just a single main class:

package eu.arrowhead.demo.consumer;

class ConsumerMain extends ArrowheadApplication {

    public ConsumerMain(String[] args) throws ArrowheadException {
        super(args);
    }

    @Override
    protected void onStart() {
        final ArrowheadSecurityContext securityContext = ArrowheadSecurityContext
            .createFromProperties(true);
        
        final ArrowheadSystem me = ArrowheadSystem.createFromProperties();
        final ServiceRequestForm srf = new ServiceRequestForm.Builder(me)
            .requestedService(getProps().getServiceName(), "json", getProps().isSecure())
            .metadata(ServiceMetadata.Keys.UNIT, "celsius")
            .flag(OrchestrationFlags.Flags.OVERRIDE_STORE, true)
            .flag(OrchestrationFlags.Flags.PING_PROVIDERS, false)
            .flag(OrchestrationFlags.Flags.METADATA_SEARCH, false)
            .flag(OrchestrationFlags.Flags.ENABLE_INTER_CLOUD, true)
            .build();
        log.info("Service Request payload: " + ArrowheadConverter.json().toString(srf));

        final OrchestrationClient orchestration = OrchestrationClient
            .createFromProperties(securityContext);
        final OrchestrationStrategy strategy = new OrchestrationStrategy
            .Always(orchestration, srf);
        final HttpClient client = new HttpClient(strategy, securityContext);

        Timer timer = new Timer();
        TimerTask authTask = new TimerTask() {
            @Override
            public void run() {
                final Response response = client.request(HttpClient.Method.GET);
                Temperature readout = response.readEntity(Temperature.class);
                log.info("The indoor temperature is " +
                         readout.getTemperature() +
                         " degrees celsius.");
            }
        };
        timer.schedule(authTask, 2L * 1000L, 8L * 1000L);
    }

    @Override
    protected void onStop() {

    }

    public static void main(String[] args) throws ArrowheadException {
        new ConsumerMain(args).start();
    }
}
  1. Like in the provider, we inherit from ArrowheadApplication, and implement a matching constructor.

  2. In the onStart() method:

    1. Again, a security context is needed.

    2. For the service request form, we need a ArrowheadSystem describing the requester. Again, the createFromProperties() method is used to create this from the properties file.

    3. The ServiceRequestForm is built using the builder pattern, you should check the documentation on orchestration system for the details on the flags. We get the service name from the properties file.

    4. This request should be sent to the orchestration system, thus we create a client for it.

    5. Now, the tricky part. Local clouds are dynamic, thus application systems may join, leave, and be replaced by others over time. We can therefore not rely on an orchestration response for all eternity. Currently, we have three orchestration strategies to choose from:

      • OrchestrationStrategy.Never: Will never do orchestration, you have to provide details of the service manually. This is mostly used within the core system clients.

      • OrchestrationStrategy.Once: Orchestration will only be done once (when the strategy is constructed). Meaning that subsequent request may fail if the service is no longer available.

      • OrchestrationStrategy.Always: Orchestration will be done at every request made - this might generate extra overhead on your cloud, but you will always be up to date with the current service registry.

      • Ideally, there should also be an on demand strategy that will only do orchestration on outdated tokens and when a service does not respond. If you create one, please submit it so we can include it.

    6. To create requests, we create a HTTP client for the temperature service using the orchestration strategy and security context.

    7. Now we create a timer task, that regularly performs a GET request against using this client and prints the temperature into the log. Note that since we picked the always strategy, the orchestration is automatically performed during the call to request().

  3. The onStop()and main methods are also the same as in the provider.

The configuration file and run parameters are essentially the same as for the provider, but the client does not use the values for the service registry entry as we do not register any services in it.

The Demo Publisher

Most of the basic elements for constructing application systems with the Client Library should be clear now, so I will skip the common elements, and just show the features for making a publisher.

When publishing event to the event handler core system, you have the option of receiving feedback on the results. To receive those, create a resource class, like in the provider:

package eu.arrowhead.demo.publisher;

@Path("publisher")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class PublisherResource extends ArrowheadResource {

  public PublisherResource(ArrowheadHttpServer server) throws ArrowheadException {
    super(server);
  }

  @POST
  @Path("feedback")
  public Response eventFeedback(Map<String, Boolean> results) {
    log.info("Event publishing results: " + results.toString());
    return Response.ok().build();
  }
}

Now this also mean that the main class have to start a web server like the provider does:

package eu.arrowhead.demo.publisher;

class PublisherMain extends ArrowheadApplication {
  
  private PublisherMain(String[] args) throws ArrowheadException {
    super(args);
  }

  @Override
  protected void onStart() throws ArrowheadException {
    final ArrowheadSecurityContext securityContext = ArrowheadSecurityContext
        .createFromProperties(true);
    final ArrowheadHttpServer server = ArrowheadGrizzlyHttpServer
        .createFromProperties(securityContext)
        .addResources(PublisherResource.class)
        .setSecurityFilter(new ArrowheadSecurityFilter())
        .start();

    final EventHandlerClient eventHandler = EventHandlerClient
        .createFromProperties(securityContext);
    
    final ArrowheadSystem me = ArrowheadSystem.createFromProperties(server);
    final ArrowheadProperties props = getProps();
    final String eventType = props.getEventType();
    final boolean secure = props.isSecure();

    Timer timer = new Timer();
    TimerTask authTask = new TimerTask() {
      @Override
      public void run() {
        final Event event = new Event(eventType,
                                      String.format("Hello %s World",
                                                    secure ? "secure" : "insecure"));
        eventHandler.publish(event, me);
      }
    };
    timer.schedule(authTask, 2L * 1000L, 8L * 1000L);
  }

  @Override
  protected void onStop() {

  }

  public static void main(String[] args) throws ArrowheadException {
    new PublisherMain(args).start();
  }
}

Notes on the publisher main class:

  1. Notice the change of security filter when starting the web server. Currently the Event Handler does not use token/signature security, so we have to reduce the security of the server. You should be careful here if you are creating a combined publisher and provider, as this will also reduce security on your provider. In these cases, you can either create your own security filter (inheriting from ContainerRequestFilter, ArrowheadSecurityFilter or ArrowheadTokenSignatureSecurityFilter), or you can start multiple servers on different ports.

  2. Now, sending events requires interaction with the event handler, thus we create a client for it.

  3. For each event we need to describe, who the publisher is and which event type we are sending. Also, for this scenario we need to know if the system is started in secure mode or not. The two latter we get directly as values from the configuration file.

  4. The publishing is encapsulated in a timer task to perform it regularly, and consists of constructing an event and publishing it through the event handler client.

The configuration file contains three new entries:

# Event parameters
feedback=true
feedback_uri=publisher/feedback
event_type=hello
  • feedback controls whether the event handler client will send the feedback to our system or not.

  • feedback_uri the path of the feedback service.

  • event_type describes what type of event our system sends. Note that we explicitly have to obtain and use this value, in contrast to the other values which are implicitly used by the createFromProperties() methods. You can also add your own custom parameters to the configuration file, and get them with the primitive get methods from the ArrowheadProperties class.

The Demo Subscriber

Events are pushed to a web service, thus our subscriber also need to implement a resource class:

package eu.arrowhead.demo.subscriber;

@Path("subscriber")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class SubscriberResource extends ArrowheadResource {

  public SubscriberResource(ArrowheadHttpServer server) throws ArrowheadException {
    super(server);
  }

  @POST
  @Path("notify")
  public Response receiveEvent(Event event) {
    log.info("Received new event: " + event.toString());
    return Response.ok().build();
  }
}

The main class, which starts the web server for this resource:

package eu.arrowhead.demo.subscriber;

class SubscriberMain extends ArrowheadApplication {

  private SubscriberMain(String[] args) throws ArrowheadException {
    super(args);
  }

  @Override
  protected void onStart() throws ArrowheadException {
    final ArrowheadSecurityContext securityContext = ArrowheadSecurityContext
        .createFromProperties(true);
    final ArrowheadHttpServer server = ArrowheadGrizzlyHttpServer
            .createFromProperties(securityContext)
            .addResources(SubscriberResource.class)
            .setSecurityFilter(new ArrowheadSecurityFilter())
            .start();

    final EventHandlerClient eventHandler = EventHandlerClient
        .createFromProperties(securityContext);
    final ArrowheadSystem me = ArrowheadSystem.createFromProperties(server);
    eventHandler.subscribe(getProps().getEventType(), me);
  }

  @Override
  protected void onStop() {

  }

  public static void main(String[] args) throws ArrowheadException {
    new SubscriberMain(args).start();
  }
}

Notes on the subscriber main:

  1. Like in the publisher, we have to reduce the security. Again, be aware of this if you are building a combined publisher and provider.

  2. To subscribe to events, we need an event handler client first, and a description of the subscriber.

  3. Then we can subscribe to events, notice that we get the type from the configuration file again.

Besides the already introduced properties, the configuration file for the subscriber contains one new entry, which describes where the event handler should send the events:

notify_uri=subscriber/notify

Installable Application Systems

To add Debian package generation to your application systems, first add a jdeb version property to the properties list in your project pom.xml:

<jdeb.version>1.7</jdeb.version>

Making Demo Common Installable

First, we need to make the demo common module installable, so in its pom.xml add the build section:

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-dependency-plugin</artifactId>
      <version>${maven.dependency.version}</version>
      <executions>
        <execution>
          <id>copy-dependencies</id>
          <phase>package</phase>
          <goals>
            <goal>copy-dependencies</goal>
          </goals>
          <configuration>
            <outputDirectory>${basedir}/target/lib/</outputDirectory>
          </configuration>
        </execution>
      </executions>
    </plugin>
    <plugin>
      <artifactId>jdeb</artifactId>
      <groupId>org.vafer</groupId>
      <version>${jdeb.version}</version>
      <executions>
        <execution>
          <phase>package</phase>
          <goals>
            <goal>jdeb</goal>
          </goals>
          <configuration>
            <deb>target/${project.artifactId}_${project.version}.deb</deb>
            <verbose>true</verbose>
            <controlDir>${basedir}/src/deb/control</controlDir>
            <dataSet>
              <data>
                <type>file</type>
                <src>${project.build.directory}/${project.build.finalName}.jar</src>
                <mapper>
                  <type>perm</type>
                  <prefix>/usr/share/arrowhead/lib/demo</prefix>
                </mapper>
              </data>
            </dataSet>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>
  1. First maven-dependency-plugin copies all our dependencies into the lib folder.

  2. Then we use the jdeb plugin to generate the install package with the jar file for the demo common module.

Now, in this simplistic demo project we have no additional dependencies. However if we had, these could be added by additional data sections. For example if we used PostgreSQL in our project:

<data>
  <type>file</type>
  <src>${project.build.directory}/lib/postgresql-${postgresql.version}.jar</src>
  <mapper>
    <type>perm</type>
    <prefix>/usr/share/arrowhead/lib/demo</prefix>
  </mapper>
</data>

Note that you should install these under /usr/share/arrowhead/lib/PROJECT_NAME to avoid conflicts with official arrowhead packages and other projects.

Then, in the directory src/deb/control add four files. First control (you should change the values were appropriate):

Package: [[name]]
Version: [[version]]
Section: contrib/java
Priority: optional
Architecture: all
Maintainer: Your Name <your@email.address>
Homepage: http://www.arrowhead.eu
Description: Common library for the library demo
Distribution: development
Depends: java-runtime-headless, arrowhead-client-common

Then postinst:

#!/bin/sh

set -e

case "$1" in
    configure)
    ;;

    abort-upgrade|abort-remove|abort-deconfigure)
    ;;

    *)
        echo "postinst called with unknown argument \`$1'" >&2
        exit 1
    ;;
esac

exit 0

And postrm:

#!/bin/sh

set -e

. /usr/share/debconf/confmodule

case "$1" in
    purge)
        AH_CONF_DIR="/etc/arrowhead"
        AH_SYSTEMS_DIR="${AH_CONF_DIR}/systems"

        rmdir ${AH_SYSTEMS_DIR} 2>/dev/null || true
        rmdir ${AH_CONF_DIR} 2>/dev/null || true
        rmdir /var/log/arrowhead 2>/dev/null || true

        db_purge

        if [ -d ${AH_CONF_DIR} ]; then
            echo "Directory '${AH_CONF_DIR}' is not empty, please remove manually" >&2
        fi
    ;;
    remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
    ;;

    *)
        echo "postrm called with unknown argument \`$1'" >&2
        exit 1
    ;;
esac

exit 0

And preinst:

#!/bin/sh

set -e

case "$1" in
    install|upgrade)
    ;;

    abort-upgrade)
    ;;

    *)
        echo "preinst called with unknown argument \`$1'" >&2
        exit 1
    ;;
esac

exit 0

And finally prerm:

#!/bin/sh

set -e

case "$1" in
    remove|upgrade|deconfigure)
    ;;

    failed-upgrade)
    ;;

    *)
        echo "prerm called with unknown argument \`$1'" >&2
        exit 1
    ;;
esac

exit 0

Now you can run mvn package to generate the Debian install package for the demo common module.

Making Demo Provider Installable

In the pom.xml for the application system, add a build section:

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <version>${maven.jar.version}</version>
      <configuration>
        <archive>
          <manifestEntries>
            <Class-Path>
              lib/demo/client-demo-common-${project.version}.jar
            </Class-Path>
          </manifestEntries>
          <manifest>
            <addClasspath>true</addClasspath>
            <mainClass>eu.arrowhead.demo.provider.ProviderMain</mainClass>
            <classpathPrefix>lib/</classpathPrefix>
          </manifest>
        </archive>
      </configuration>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-dependency-plugin</artifactId>
      <version>${maven.dependency.version}</version>
      <executions>
        <execution>
          <id>copy-dependencies</id>
          <phase>package</phase>
          <goals>
            <goal>copy-dependencies</goal>
          </goals>
          <configuration>
            <outputDirectory>${basedir}/target/lib/</outputDirectory>
          </configuration>
        </execution>
      </executions>
    </plugin>
    <plugin>
      <artifactId>jdeb</artifactId>
      <groupId>org.vafer</groupId>
      <version>${jdeb.version}</version>
      <executions>
        <execution>
          <phase>package</phase>
          <goals>
            <goal>jdeb</goal>
          </goals>
          <configuration>
            <deb>target/${project.artifactId}_${project.version}.deb</deb>
            <verbose>true</verbose>
            <controlDir>${basedir}/src/deb/control</controlDir>
            <dataSet>
              <data>
                <type>file</type>
                <src>${project.build.directory}/${project.build.finalName}.jar</src>
                <mapper>
                  <type>perm</type>
                  <prefix>/usr/share/arrowhead</prefix>
                  <user>arrowhead</user>
                  <filemode>755</filemode>
                </mapper>
              </data>
              <data>
                <type>link</type>
                <linkName>/usr/share/arrowhead/${project.artifactId}.jar</linkName>
                <linkTarget>/usr/share/arrowhead/${project.build.finalName}.jar</linkTarget>
              </data>
              <data>
                <type>file</type>
                <src>${project.basedir}/src/deb/client-demo-provider.service</src>
                <mapper>
                  <type>perm</type>
                  <prefix>/etc/systemd/system</prefix>
                  <filemode>664</filemode>
                </mapper>
              </data>
            </dataSet>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Notes on the pom.xml:

  1. In the section for maven-jar-plugin you have to add all the libraries you install as part of your own packages. Otherwise, Java will not be able to find these. Also, remember to set a full reference to the main class.

  2. Again, maven-dependency-plugin and jdeb is used to generate the package. Note that we add a service file to start and stop the Arrowhead system using systemd. Again, if you have addition dependencies specifically to this application system, you should add them with additional data section, like described under the pom.xml for the common module.

Create the service file src/deb/client-demo-provider.service (if you want to install the application system on systems without the core systems, you should remove the dependency on the service registry):

[Unit]
Description=client-demo-provider
After=network.target arrowhead-serviceregistry-sql.service
Requires=arrowhead-serviceregistry-sql.service

[Service]
WorkingDirectory=/etc/arrowhead/systems/client-demo-provider
ExecStart=/usr/bin/java -jar /usr/share/arrowhead/client-demo-provider.jar -d -daemon
TimeoutStopSec=5
Type=simple
User=arrowhead
Group=arrowhead

[Install]
WantedBy=default.target

Again, in the src/deb/control directory, we need five files. First control:

Package: [[name]]
Version: [[version]]
Section: contrib/java
Priority: optional
Architecture: all
Maintainer: Your Name <your@email.address>
Homepage: http://www.arrowhead.eu
Description: Arrowhead Demo Provider
Distribution: development
Depends: java-runtime-headless, arrowhead-common, arrowhead-client-common,
         arrowhead-core-common, client-demo-common

Note we added arrowhead-core-common here, because we will add auto generation of the certificate to the installation scripts below. You can remove this dependency, but then you also need to remove the generate code from the scripts below, and the dependency on the service registry in the service file above.

Then postinst:

#!/bin/sh

set -e

. /usr/share/debconf/confmodule

SYSTEM_NAME="client-demo-provider"
PKG_NAME="client-demo-provider"

case "$1" in
    configure)
        . /usr/share/arrowhead/conf/ahconf.sh
        SYSTEM_DIR="${AH_SYSTEMS_DIR}/${SYSTEM_NAME}"

        if [ ! -d "${SYSTEM_DIR}" ]; then
          mkdir -p ${SYSTEM_DIR}
        fi

        ah_cert_signed_system ${SYSTEM_NAME}
        ah_cert_export_pub "${AH_SYSTEMS_DIR}/authorization" "authorization" "${SYSTEM_DIR}"

        if [ ! -f "${SYSTEM_DIR}/default.conf" ]; then
          /bin/cat <<EOF >${SYSTEM_DIR}/default.conf
# Directories
cert_dir=.

# Webserver parameters
address=$(hostname -I | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
port=8461
secure=true

# System registry entry parameters
system_name=client-demo-provider
service_name=temperature
service_uri=temperature
interfaces=JSON, XML
service_metadata=unit-celsius

# Key and trust stores
keystorepass=${AH_PASS_CERT}
truststorepass=${AH_PASS_CERT}
keypass=${AH_PASS_CERT}

# Core systems
#sr_address=0.0.0.0
#eh_address=0.0.0.0
#ca_address=0.0.0.0
#orch_address=0.0.0.0

# Logging parameters
log4j.rootLogger=INFO, DB, FILE, CONSOLE

log4j.appender.DB=org.apache.log4j.jdbc.JDBCAppender
log4j.appender.DB.URL=jdbc:mysql://127.0.0.1:3306/arrowhead
log4j.appender.DB.user=arrowhead
log4j.appender.DB.password=${AH_PASS_DB}
log4j.appender.DB.sql=INSERT INTO logs(id, date, origin, level, message) \
                      VALUES(DEFAULT,'%d{yyyy-MM-dd HH:mm:ss}','%C','%p','%m')
log4j.appender.DB.layout=org.apache.log4j.PatternLayout
log4j.logger.org.hibernate=fatal

log4j.appender.FILE=org.apache.log4j.FileAppender
log4j.appender.FILE.File=/var/log/arrowhead/${SYSTEM_NAME}.log
log4j.appender.FILE.ImmediateFlush=true
log4j.appender.FILE.Threshold=debug
log4j.appender.FILE.Append=false
log4j.appender.FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.FILE.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss}, %C, %p, %m%n

log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.target=System.err
log4j.appender.CONSOLE.ImmediateFlush=true
log4j.appender.CONSOLE.Threshold=debug
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss}  %p  %-160m  \
                                                %c{1}.%M(%F:%L)%n
EOF
          chown root:arrowhead ${SYSTEM_DIR}/default.conf
          chmod 640 ${SYSTEM_DIR}/default.conf
        fi

        echo "Restarting ${PKG_NAME}..." >&2
        systemctl daemon-reload
        systemctl restart ${PKG_NAME}.service
    ;;

    abort-upgrade|abort-remove|abort-deconfigure)
    ;;

    *)
        echo "postinst called with unknown argument \`$1'" >&2
        exit 1
    ;;
esac

exit 0

In postinst, we used the scripts from the core system Debian packages to automatically generate a new certificate for the provider system. It also writes a new configuration file, if it does not already exist. Finally, we also start the new system.

And postrm, where we simply remove what we created:

#!/bin/sh

set -e

. /usr/share/debconf/confmodule

SYSTEM_NAME="client-demo-provider"

case "$1" in
    purge)
        AH_CONF_DIR="/etc/arrowhead"
        AH_SYSTEMS_DIR="${AH_CONF_DIR}/systems"
        SYSTEM_DIR="${AH_SYSTEMS_DIR}/${SYSTEM_NAME}"

        rm -f \
            /var/log/arrowhead/${SYSTEM_NAME}.log \
            ${SYSTEM_DIR}/default.conf \
            ${SYSTEM_DIR}/authorization.pub \
            ${SYSTEM_DIR}/${SYSTEM_NAME}.p12
        rmdir ${SYSTEM_DIR} 2>/dev/null || true
        rmdir /var/log/arrowhead 2>/dev/null || true
        db_purge
    ;;
    remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
    ;;

    *)
        echo "postrm called with unknown argument \`$1'" >&2
        exit 1
    ;;
esac

exit 0

And preinst:

#!/bin/sh
set -e

case "$1" in
    install|upgrade)
    ;;

    abort-upgrade)
    ;;

    *)
        echo "preinst called with unknown argument \`$1'" >&2
        exit 1
    ;;
esac

exit 0

Finally prerm, where we only stop the system before uninstalling it:

#!/bin/sh

set -e

PKG_NAME="client-demo-provider"

case "$1" in
    remove)
        systemctl stop ${PKG_NAME}.service
    ;;

    upgrade|deconfigure)
    ;;

    failed-upgrade)
    ;;

    *)
        echo "prerm called with unknown argument \`$1'" >&2
        exit 1
    ;;
esac

exit 0

Making the other demo systems installable should be the same as for the provider (with minor modifications).