Creating a Component

This walk-through helps in creating a CSW component in Scala/Java. CSW components depend on the csw-framework package, which can be found here. This section discusses constructing an HCD, but the principles apply to an Assembly as well. We will be constructing the Assembly in the next section Working with Multiple Components.

Tutorial: Developing an HCD

This tutorial shows code written in Scala and Java, based on code generated by the giter8 templates with the default values.

Async handling in Scala and Java examples.

Most of CSW is written using asynchronous programming. Therefore, in the examples, you may find constructs that deal with Futures and other asynchronous code in various ways. The following constructs are commonly used throughout this manual:

  • Scala: The Scala async package is used extensively. async marks a block of asynchronous code and allows to await the computation until the Future is complete. For more info, please refer to: https://github.com/scala/scala-async.

    Sometimes, an example may use the Await.result construct. While this is often used in tests, it is a blocking call and typically should not be used in production code.

  • Java non-blocking example: The code snippets use CompletableFuture and its thenAsync, thenApply methods. This style allows to compose multiple Futures and not block the calling thread until Futures are complete.

  • Java blocking example: The code snippets use CompletableFuture using get blocking call. This style also blocks the calling thread until the Future is complete, and it should only be prudently used.

Anatomy of Component

A component consists of a Supervisor actor, a Top Level Actor (TLA) that provides component handlers, and one or more worker actors. The csw-framework provides the Supervisor actor, the Top Level Actor and an abstract class of handlers. Component developers are expected to implement these handlers, which collectively act as the gateway from the framework to the developer’s component code.

Supervisor

A Supervisor actor is the actor first started for any component. The main responsibilities that the Supervisor performs is as follows:

  • Creation of the TLA when the component starts up
  • Implement and manage the component lifecycle for the TLA and for the component (see Lifecycle below).
  • Register the component with the Location Service.
  • Provide an administrative interface to the component to the rest of the system. For instance, the Container can perform some administrative communication with the Supervisor such as restart or shutdown of the component.
  • Allow components outside of the Supervisor and TLA to monitor the lifecycle state of the TLA. This is particularly useful for testing, when the test needs to know that the component is ready before performing its test actions.
  • Supports the locking functionality for the component (see Locking)
  • Receives external commands and passes them to the correct component handlers.

The source code of the Supervisor actor can be found here

Top Level Actor

While the Supervisor works as the external interface for the component and the manager of its lifecycle, the functional implementation of a component is implemented in a Top Level Actor (TLA), spawned by the Supervisor actor for each component. However, the developer is not expected to implement a TLA code entirely. Instead, the component-specific functionality of the TLA is added by implementing the ComponentHandlers abstract class, consisting of a set of methods, or hooks, called by the TLA during specific lifecycle and command events (see Handlers). The ComponentHandlers implementation is specified during construction using a factory (see Constructing The Component)

The source code of the Top Level Actor can be found here.

Handlers

The following hooks may be overridden in your ComponentHandlers implementation class:

  • initialize: called when the component is starting up, prior to be put into the Running state.
  • validateCommand: called when the component receives a command to determine if the command is valid and can be executed. (see Validation)
  • onSubmit: called on Submit command if validateCommand returns Accepted.
  • onOneway: called on Oneway command if validateCommand returns Accepted.
  • onGoOffline: called when the component receives an external message from an administrative client to go offline.
  • onGoOnline: called when the component receives an external message from an administrative client to go online.
  • onDiagnosticMode: called when the component receives an external message from a client to enter a specified diagnostic behavior.
  • onOperationsMode: called when the component receives an external message from a client to exit any diagnostic behavior and return to normal, operations behavior.
  • onLocationTrackingEvent: called when a tracked dependency changes location state. (see Tracking Dependencies)
  • onShutdown: called when the component is shutting down.

The source code of ComponentHandlers can be found here.

More details about handler significance and invocation can be found here

Component Handlers in Java

If the component developer wishes to write the component handler implementation in Java, they need to implement the Java version of ComponentHandlers called JComponentHandlers. The source code of JComponentHandlers can be found here. Any further reference to ComponentHandlers should be inferred as also applying to JComponentHandlers.

Tutorial: Developing an HCD

As seen in the Getting Started page, if you are using the giter8 template, component handler classes for both the HCD and Assembly are written for you, with implementations stubbed out. We will walk-through filling them in below.

Constructing the Component

After writing the component handlers, a developer needs to wire it up with the framework. In order to do this, the developer needs to specify handlers full class path in a ComponentInfo configuration file for the component (see example below). The csw-framework picks up the full class path of the ComponentHandlers from the file and the Supervisor spawns the component using these handlers in the process of booting the component. The factory is instantiated using Java reflection.

Additional sample code to implement the ComponentHandlers can be found here

Tutorial: Developing an HCD

As seen in the Getting Started page, if using the template, this factory class will be implemented for you.

Component Configuration (ComponentInfo)

The component configuration, called the ComponentInfo file, contains details needed to create a component. This configuration defines a few parameters needed for a particular component. The template creates one for our sample HCD as follows:

Scala
sourceprefix = "csw.samplehcd"
componentType = hcd
componentHandlerClassName= "org.tmt.csw.samplehcd.SampleHcdHandlers"
locationServiceUsage = RegisterOnly
Java
sourceprefix = "csw.samplehcd"
componentType = hcd
componentHandlerClassName = "org.tmt.csw.samplehcd.JSampleHcdHandlers"
locationServiceUsage = RegisterOnly
What is a componentHandlerClassName?

componentHandlerClassName refers to class name of the a concrete implementation of ComponentHandlers, which is SampleHcdHandlers for Scala in above example, JSampleHcdHandlers for Java.

The prefix and componentType are used to create the ComponentId identifier, which must be unique within the control system and the Location Service. The prefix must begin with a valid TMT subsystem, which establishes a scope for the component name. The list of valid subsystems is here.

The locationServiceUsage is used by the Supervisor actor to decide whether to only register a component with the Location Service or to register and track other components. It is also possible to choose not register the component with the Location Service.

The ComponentInfo file is parsed to a ComponentInfo object and injected in the Supervisor actor. It is then injected in ComponentHandlers while spawning a component and the contents is available for the developer to access.

Using Prefix

ComponentInfo includes the Prefix for the component. Developers should try to use this prefix value rather than defining a new one to reduce errors.

The ComponentInfo file can also contain a list of components and services it wishes to track as dependencies. See Tracking Dependencies.

More details about ComponentInfo can be found here.

An additional sample configuration file can be found here.

Lifecycle

The Supervisor of a component manages its lifecycle state, which can be one of the following:

  • Idle
  • Running
  • RunningOffline
  • Restart
  • Shutdown
  • Lock

The state the component is in dictates the actions it can take when it receives a message or command, and whether those actions are carried out.

Idle

The component initializes in the idle state. The Top Level Actor calls the initialize hook of ComponentHandlers as the first thing on boot-up. Component developers write their initialization logic in this hook. The logic can also do things like accessing the Configuration Service to fetch information such as hardware configurations to set the hardware to default positions.

After initialization, if the component would have configured RegisterAndTrack for locationServiceUsage, then the Top Level Actor will start tracking the connections configured for that component. This use case is mostly applicable for Sequencers and Assemblies. HCDs should have RegisterOnly configured for locationServiceUsage in most all cases.

If initialize is successful, the Supervisor will register the component with the Location Service. Registering with the Location Service will notify other components tracking this component with a LocationUpdated event containing a Location with a reference to the Supervisor of the component.

After successful registration, the component will transition to the Running state.

Running

When the Supervisor actor receives Initialized message from the Top Level Actor after successful initialization, it registers itself with the Location Service and transitions the component to the Running state. Running state signifies that the component is accessible via the Location Service, which allows other entities to communicate with it by sending commands via messages. Any commands received by the Supervisor actor will be forwarded to the Top Level Actor for processing once the component is in the Running state.

What does Running mean?

A component should be ready for operation after successfully leaving the initialize handler and entering Running. A component should ready to process any command after initialization and must not require specific commands to be issued by users in order to become ready.

RunningOffline

When the Supervisor actor receives a GoOffline message, it transitions the component to the RunningOffline state and forwards it to the Top Level Actor. The Top Level Actor then calls the onGoOffline hook of ComponentHandlers.

If a GoOnline message is received by the Supervisor actor, it transitions the component back to the Running state and forwards it to the Top Level Actor. The Top Level Actor then calls the onGoOnline hook of ComponentHandlers.

Handling RunningOffline

In the RunningOffline state, if any command is received, it is forwarded to the underlying component hook through the Top Level Actor. It is then the responsibility of the component developer to check the isOnline flag provided by csw-framework and process the command according to whether the command is appropriate for the command when offline.

Restart

When the Supervisor actor receives a Restart message, it will transition the component to the Restart state. Then, it will unregister itself from the Location Service so that other components tracking this component will be notified and no commands are received while restart is in progress.

Then, the Top Level Actor is stopped and the postStop hook of the Top Level Actor will call the onShutdown hook of ComponentHandlers. Component developers are expected to write any cleanup of resources or other logic that should be executed for the graceful shutdown of the component.

After successful shutdown of component, the Supervisor actor will re-create the Top Level Actor again from scratch. This will cause the initialize hook of ComponentHandlers to be called again. After successful initialization of the component, the Supervisor actor will register itself with the Location Service once more.

Shutdown

When the Supervisor actor receives a Shutdown message, it transitions the component to the Shutdown state. Any commands received while a shutdown is in progress will be ignored. Then, it will stop the Top Level Actor. The postStop hook of the Top Level Actor will call the onShutdown hook of ComponentHandlers. Component developers are expected to write any cleanup of resources or other logic that should be executed for the graceful shutdown of component. The component, including the Supervisor, exits after onShutdown completes.

Lock

When the Supervisor actor receives a Lock message, it transitions the component to the Lock state. When locked, the Supervisor will only accept commands received from the component that originally locked the component and ignore commands from all others.

In the Lock state, messages like Shutdown and Restart will also be ignored. A component must first be unlocked to accept these commands.

Lock messages are constructed with a duration value specified. When this duration expires, the component will automatically be unlocked. In order to retain the Lock on the component, sender of the orginal Lock must resend the Lock message.

There are two ways component can be unlocked:

  1. Sending Unlock message (Note: This message should be sent by the same component that locked the component.)
  2. Sending Unlock message with an administrative Prefix.

CSW Services Injection

Common Software provides a set of CSW Services provided through the TLA. They are injected into the ComponentHandlers class in the constructor in a CswContext object. This object provides the following services through their respective APIs:

  • Location Service
  • Event Service
  • Alarm Service (Client API)
  • Time Service (Scheduler)
  • Configuration Service (Client API)
  • Logging Service (Logger Factory)

And the following information and support utilities:

  • Component Configuration (ComponentInfo)
  • Command Service Command Response Manager
  • Current State Publisher Actor (intended for HCDs)

Logging

csw-framework provides a LoggerFactory in the CswContext injected in the constructor of ComponentHandlers. The LoggerFactory will have the component’s Prefix predefined so long messages have a clear source. The component developer is expected to and must use this factory to log messages that work with the centralized logging facility.

Logging works much like other popular loggers such as log4j. However, with the development of log management tools such as logstash, the emphasis on log message formatting has been to write structured logging messages in JSON format, so that they can easily be ingested, stored, and searched. Plain text writing to stdout is also supported.
More details on how to use logging can be found here.

Tutorial: Developing an HCD

Let’s use logging to flesh out some of our command handlers. The template will instantiate a logger for you to use by constructing one from the LoggerFactory from in the CswContext passed in the constructor, instantiated as a log object.

Add some simple log messages in the initialize and onShutdown hooks, and to the onLocationTrackingEvent hook as well, although we won’t be using it for this HCD:

Scala
sourcevar maybePublishingGenerator: Option[Cancellable] = None
override def initialize(): Unit = {
  log.info("In HCD initialize")
  maybePublishingGenerator = Some(publishCounter())
}

override def onLocationTrackingEvent(trackingEvent: TrackingEvent): Unit = {
  log.debug(s"TrackingEvent received: ${trackingEvent.connection.name}")
}

override def onShutdown(): Unit = {
  log.info("HCD is shutting down")
}
Java
sourceprivate Optional<Cancellable> maybePublishingGenerator = Optional.empty();

@Override
public void initialize() {
    log.info("In HCD initialize");
    maybePublishingGenerator = Optional.of(publishCounter());
}

@Override
public void onLocationTrackingEvent(TrackingEvent trackingEvent) {
    log.debug(() -> "TrackingEvent received: " + trackingEvent.connection().name());
}

@Override
public void onShutdown() {
    log.info("HCD is shutting down");
}

In the example code, you’ll notice we have added some functionality to start publishing events. We will cover the Event Service later. You can leave that code out for now.

Next we’ll discuss handling commands.

Receiving Commands

A command is something that carries some metadata and a set of parameters. A component sends commands to other components to execute actions. CSW defines three kinds of commands as follows:

  • Setup : Contains goal, command, or demand information to be used to configure the target OMOA component.
  • Observe: Contains goal or demand information to be used by a detector. system. Properties and their value types will be standardized by the ESW subsystem.
  • Wait: Sequencer only. Instructs a sequencer to pause until told to continue.

A Sequencer receives a Sequence, which is a list of the above commands that are executed sequentially.

More details about creating commands can be found here.

Whenever a command is sent to a component, it is sent using a Command Service method. There are two general ways to send a command:

  • submit: A command is sent using submit when a completion result is expected from the destination component. There are two options: submit and submitAndWait.
  • oneway: A command is sent using oneway when the completion of command is not expected from the destination component.

Validation

When any command is received by a component using submit or oneway, the Top Level Actor will first call the validateCommand hook of ComponentHandlers. Component developers are expected to perform appropriate validation of a command to determine if it is valid to execute the requested actions and then return a ValidateCommandResponse. The ValidateCommandResponse returned from this handler will be returned to the sender directly by csw-framework if the command fails validation.

The component developer should return either an Accepted response or an or Invalid response specifying whether the command is valid to be executed or not. CSW defines a set of CommandIssues for use in validation here that should be used within Invalid responses.

If the handler is being called as part of a submit or oneway call, the command will automatically be passed on to the onSubmit or onOneway handlers (see Command Response) only if the validation handler returns a ValidationCommandResponse of Accepted. Otherwise, the Invalid response is returned to the caller immediately.

Different types of command responses and their significance can be found here.

Tutorial: Developing an HCD

Let’s add some command validation to our HCD. For our sample HCD, we will only handle one command, sleep, in which we will cause the HCD to sleep for the time specified in a parameter of the command. This will simulate a long-running command.

Add some code to ensure the command we receive is the sleep command, and return an Invalid response if not. You could imagine much more checking could be added, such as checking the types and values of the parameters of our sleep command, but we will keep it simple for our demonstration.

Scala
sourceoverride def validateCommand(runId: Id, controlCommand: ControlCommand): ValidateCommandResponse = {
  log.info(s"Validating command: ${controlCommand.commandName.name}")
  controlCommand.commandName.name match {
    case "sleep" => Accepted(runId)
    case x       => Invalid(runId, CommandIssue.UnsupportedCommandIssue(s"Command $x. not supported."))
  }
}
Java
source@Override
public CommandResponse.ValidateCommandResponse validateCommand(Id runId, ControlCommand controlCommand) {
    String commandName = controlCommand.commandName().name();
    log.info(() -> "Validating command: " + commandName);
    if (commandName.equals("sleep")) {
        return new CommandResponse.Accepted(runId);
    }
    return new CommandResponse.Invalid(runId, new CommandIssue.UnsupportedCommandIssue("Command " + commandName + ". not supported."));
}

Command Response

The response returned from validateCommand handler of ComponentHandlers will be received by the Supervisor. If the response returned was Accepted, then it either calls the onSubmit hook or the onOneway hook of ComponentHandlers depending on the whether submit, submitAndWait, or oneway was used to send the command.

If sent with submit or submitAndWait and the validation response is Accepted, the framework calls the onSubmit hook of ComponentHandlers. The return value of onSubmit is then returned to the sender, which can be a Completed for commands that return quickly, or Started for long running commands. If the response from validation was Invalid, this is returned to the sender of the command without calling the onSubmit or onOneway handler.

If onSubmit returns Started, the component’s CommandResponseManager keeps track of the status of long-running submit commands. The sender of a Started command (and any component, really) can query a Started command’s status or wait for the final response using queryFinal of the CommandService API.

If a command is sent using oneway, the validation handler if called first. The onOneway handler of ComponentHandlers is also called when the validation result is Accepted.
The validation response is always sent back to the sender as the response for a oneway command. There is no final response from a ownway command and no way to wait for it using query or queryFinal.

The CommandService class provides convenient API methods for communicating with other components, and should be the primary means of sending commands to other components. This will be described in the next tutorial section, Sending Commands.

When the onSubmit hook is called and Started is returned, it is the responsibility of the component developer to update the sender with the final status of the Started command when the actions complete using the CommandResponseManager API. An instance of CommandResponseManager is provided in the CswContext object in ComponentHandlers and should be injected in any worker actor or other actor/class created for the component that needs it.

More details on the methods available in CommandResponseManager can be found here.

Tutorial: Developing an HCD

We will implement command handling in the onSubmit hook. Note that this hook actually receives a ControlCommand as an argument, which can be either a Setup or an Observe. We will use pattern matching to handle the command if it is a Setup and forward to an onSetup handling method. Observe commands will be ignored and returned with as Invalid.

Scala
sourceoverride def onSubmit(runId: Id, controlCommand: ControlCommand): SubmitResponse = {
  log.info(s"Handling command: ${controlCommand.commandName}")

  controlCommand match {
    case setupCommand: Setup => onSetup(runId, setupCommand)
    case observeCommand: Observe => // implement (or not)
      Error(runId, "Observe not supported")
  }
}

def onSetup(runId: Id, setup: Setup): SubmitResponse = {
  val sleepTimeKey: Key[Long] = KeyType.LongKey.make("SleepTime")

  // get param from the Parameter Set in the Setup
  val sleepTimeParam: Parameter[Long] = setup(sleepTimeKey)

  // values of parameters are arrays. Get the first one (the only one in our case) using `head` method available as a convenience method on `Parameter`.
  val sleepTimeInMillis: Long = sleepTimeParam.head

  log.info(s"command payload: ${sleepTimeParam.keyName} = $sleepTimeInMillis")

  workerActor ! Sleep(runId, sleepTimeInMillis)

  Started(runId)
}
Java
source@Override
public CommandResponse.SubmitResponse onSubmit(Id runId, ControlCommand controlCommand) {
    log.info(() -> "Handling command: " + controlCommand.commandName());

    if (controlCommand instanceof Setup) {
        onSetup(runId, (Setup) controlCommand);
        return new CommandResponse.Started(runId);
    } else if (controlCommand instanceof Observe) {
        // implement (or not)
    }
    return new CommandResponse.Error(runId, "Observe command not supported");
}

private void onSetup(Id runId, Setup setup) {
    Key<Long> sleepTimeKey = JKeyType.LongKey().make("SleepTime", JUnits.millisecond);

    // get param from the Parameter Set in the Setup
    Optional<Parameter<Long>> sleepTimeParamOption = setup.jGet(sleepTimeKey);

    // values of parameters are arrays.  Get the first one (the only one in our case) using `head` method available as a convenience method on `Parameter`.
    if (sleepTimeParamOption.isPresent()) {
        Parameter<Long> sleepTimeParam = sleepTimeParamOption.orElseThrow();
        long sleepTimeInMillis = sleepTimeParam.head();

        log.info(() -> "command payload: " + sleepTimeParam.keyName() + " = " + sleepTimeInMillis);

        workerActor.tell(new Sleep(runId, sleepTimeInMillis));
    }
}

In our example, the sleep command has one parameter called SleepTime. We retrieve this parameter from the Setup by creating a Key to this parameter using the name and type, and then calling an apply method on the Setup (the setup(sleepKey) shorthand) which finds the matching Parameter in the Setup’s ParameterSet (use the Setup.jget() method in Java). By doing this, the Parameter is returned with the proper typing, and so the values retrieved from the Parameter are typed as well. Note, all values are stored as an array, so we get our single value for sleepTime by using the head method available as a convenience method on ParameterSet.

At this point, to prevent our HCD from blocking and simulate a long-running command, we pass the sleep function off to a worker actor, which we will specify elsewhere in this class.

Use of an External Class

The worker actor can be defined in a separate class, but writing it as an internal class allows us to use the logging facility and CommandResponseManager without having to inject them into a new actor class. Additional versions of the tutorial code show a separate, enhanced worker actor class.

Note that our onSetup command handling logic returns a Started response. This indicates that the command is a long-running command and will be finished after some time, and that the final result will be posted to the CommandResponseManager. The submitAndWait command in the CommandService is implemented such that when it receives a Started response, it automatically issues a queryFinal to the commanded component to await the final completion response. When the command is updated using the CommandResponseManager, the Future returned by submitAndWait command is completed with this value. Note that in this code, there is a chance that there is no sleepTime parameter. Good validation code would ensure this so onSetup does not need to worry. Enhanced versions of the tutorial code show this.

Scala
sourcesealed trait WorkerCommand
case class Sleep(runId: Id, timeInMillis: Long) extends WorkerCommand

private val workerActor =
  ctx.spawn(
    Behaviors.receiveMessage[WorkerCommand](msg => {
      msg match {
        case sleep: Sleep =>
          log.trace(s"WorkerActor received sleep command with time of ${sleep.timeInMillis} ms")
          // simulate long running command
          val when: UTCTime = UTCTime.after(FiniteDuration(sleep.timeInMillis, MILLISECONDS))
          timeServiceScheduler.scheduleOnce(when) {
            commandResponseManager.updateCommand(CommandResponse.Completed(sleep.runId))
          }
        case _ => log.error("Unsupported message type")
      }
      Behaviors.same
    }),
    "WorkerActor"
  )
Java
sourceprivate interface WorkerCommand {
}

private static final class Sleep implements WorkerCommand {
    private final Id runId;
    private final long timeInMillis;

    private Sleep(Id runId, long timeInMillis) {
        this.runId = runId;
        this.timeInMillis = timeInMillis;
    }
}

private ActorRef<WorkerCommand> createWorkerActor() {
    return actorContext.spawn(
            Behaviors.receiveMessage(msg -> {
                if (msg instanceof Sleep) {
                    Sleep sleep = (Sleep) msg;
                    log.trace(() -> "WorkerActor received sleep command with time of " + sleep.timeInMillis + " ms");
                    UTCTime when = UTCTime.after(new FiniteDuration(sleep.timeInMillis, MILLISECONDS));
                    Runnable task = () -> cswCtx.commandResponseManager().updateCommand(new CommandResponse.Completed(sleep.runId));
                    // simulate long running command that updates CRM when completed
                    cswCtx.timeServiceScheduler().scheduleOnce(when, task);
                } else {
                    log.error("Unsupported message type");
                }
                return Behaviors.same();
            }),
            "WorkerActor"
    );
}

This worker actor takes the time passed in the message and sleeps that amount using the TimeService scheduling API. When the time is expired, the worker updates the CommandResponseManager that the command is Completed.

Events

CSW Events have a similar structure to commands in that along with a name and a prefix (used to represent the source of the event), they include data represented in the Event in a set of parameters. More details about events can be found here.

Access to the Event Service is in the CswContext object passed in to the handlers class in the constructor. The Event Service provides a factory method to create a “default” publisher and subscriber, which can be accessed in various parts of your code to reuse a single connection to the service. In most cases, reusing this connection will provide the performance needed.
But if you prefer to create new connections, custom publishers and subscribers can be constructed. See the manual on the Event Service for more information.

Publishers have an API that allows the publishing of a single event, a stream of events, or periodic events created by an EventGenerator, which is simply a function that returns an Event.

Tutorial: Developing an HCD

Let’s add a publisher to our component. We will use the default publisher that will periodically publish events generated by an EventGenerator.

Scala
sourceimport scala.concurrent.duration._
private def publishCounter(): Cancellable = {
  var counter = 0
  def incrementCounterEvent() =
    Option {
      counter += 1
      val param: Parameter[Int] = KeyType.IntKey.make("counter").set(counter)
      SystemEvent(componentInfo.prefix, EventName("HcdCounter")).add(param)
    }

  log.info("Starting publish stream.")
  eventService.defaultPublisher.publish(incrementCounterEvent(), 2.second, err => log.error(err.getMessage, ex = err))
}

private def stopPublishingGenerator(): Unit = {
  log.info("Stopping publish stream")
  maybePublishingGenerator.foreach(_.cancel())
}
Java
sourceprivate int counter = 0;

private Optional<Event> incrementCounterEvent() {
    counter += 1;
    Parameter<Integer> param = JKeyType.IntKey().make("counter", JUnits.NoUnits).set(counter);
    return Optional.of(new SystemEvent(cswCtx.componentInfo().prefix(), new EventName("HcdCounter")).add(param));
}

private Cancellable publishCounter() {
    log.info("Starting publish stream.");
    return cswCtx.eventService().defaultPublisher().publish(this::incrementCounterEvent, java.time.Duration.ofSeconds(2));
}

private void stopPublishingGenerator() {
    log.info("Stopping publish stream");
    maybePublishingGenerator.ifPresent(Cancellable::cancel);
}

We encapsulate the starting of the publishing in our method publishCounter. Our EventGenerator is the incrementCounterEvent method which increments our integer variable counter and stores it in the ParameterSet of a new SystemEvent and returns it. Once our defaultPublisher is resolved, we pass in a reference to incrementCounterEvent and specify a period of 2 seconds. We log a message when publishing the event so that it can be observed when running the component.

The publish method returns a Cancellable type in a future. When the publishing is set up, the Cancellable can be used to stop the event generator. We demonstrate its usage in the stopPublishingGenerator method, although this method is not called in our tutorial.

We will start this publishing when our component initializes, so we return to our initialize method and add a call to our publishCounter method. We save a reference to the Cancellable object for future use in our stopPublishingGenerator method.

Scala
sourcevar maybePublishingGenerator: Option[Cancellable] = None
override def initialize(): Unit = {
  log.info("In HCD initialize")
  maybePublishingGenerator = Some(publishCounter())
}

override def onLocationTrackingEvent(trackingEvent: TrackingEvent): Unit = {
  log.debug(s"TrackingEvent received: ${trackingEvent.connection.name}")
}

override def onShutdown(): Unit = {
  log.info("HCD is shutting down")
}
Java
sourceprivate Optional<Cancellable> maybePublishingGenerator = Optional.empty();

@Override
public void initialize() {
    log.info("In HCD initialize");
    maybePublishingGenerator = Optional.of(publishCounter());
}

@Override
public void onLocationTrackingEvent(TrackingEvent trackingEvent) {
    log.debug(() -> "TrackingEvent received: " + trackingEvent.connection().name());
}

@Override
public void onShutdown() {
    log.info("HCD is shutting down");
}

Starting CSW Services

Before we run our application, we must first start the Location Service and the Event Service. csw-services application has been provided to simplify the starting and stopping of CSW services. Refer here to install csw-services and understand commands for starting the required services.

Environment Variables used by CSW

There are several environment variables that are used by the CSW framework and services. The environment variables used by CSW services are specified here.

Tutorial: Developing an HCD

Let’s go ahead and start our CSW Services using the csw-services application.

csw-services start

Building and Running component in standalone mode

Once the component is ready, it is started using the ContainerCmd object in a standalone mode. The details for starting the ContainerCmd in a standalone mode can be found here.

There are various ways to build and run the project. A simple way during development is to use sbt to run it. The sbt command runMain can be used to specify an application with a main method and run it with arguments specified at the command line. When this command is executed, sbt will take care of any downloading of dependencies, compiling, or building necessary to run your application.

Our template includes a wrapper application around ContainerCmd that we can use in the deployment module. To run our HCD in a standalone mode, go to the project root directory and type sbt "<deploy-module>/runMain <mainClass> --local <path-to-standalone-config-file>", where

  • <deploy-module> is the name of the deployment module created by the template (sample-deploy if using defaults)
  • <mainClass> is the full class name of our ContainerCmd application, which the template names <package>.<name>deploy.<Name>ContainerCmdApp. If you accept the defaults for the template, it will be org.tmt.csw.sampledeploy.SampleContainerCmdApp. If you are having problems determining the class name, use sbt <deploy-module>/run and it will prompt you the possibilities.
  • <path-to-standalone-config-file> is the filename, which can be an absolute path or relative to the directory of the deployment module. If using defaults, this would be src/main/resources/SampleHcdStandalone.conf for Scala, and src/main/resources/JSampleHcdStandalone.conf for Java.

So if using the template defaults, the full command would be

Scala
sbt "sample-deploy/runMain org.tmt.csw.sampledeploy.SampleContainerCmdApp --local src/main/resources/SampleHcdStandalone.conf"
Java
sbt "sample-deploy/runMain org.tmt.csw.sampledeploy.SampleContainerCmdApp --local src/main/resources/JSampleHcdStandalone.conf"