Practical Tips for Using the MAL Java API
The intended audience of this tutorial is any software developer wishing to use the CCSDS Message Abstraction Layer (MAL) Java API in order to implement a new MO service or a new transport binding (and/or data encoding). It also might be helpful to integrators whishing to integrate available Java components into a complete MO stack. It is not intended for people whising to define new MO services or technology bindings or developers not using the MAL Java API. It is assumed you are familiar with the CCSDS Mission Operations (MO) framework in general and the MO reference model and the MO MAL in particular. It is the purpose of this document to help you get familiar with the MAL Java API.
The second part of this document tries to get you started with the reference test bed distributed by the CCSDS so that you can easily verify your implementation is compliant to the standard.
The MAL Java API is a concrete technology binding of the abstract MAL. It provides an interface to the top, i.e. for consuming and providing MO services, and an interface to the bottom, i.e. for mapping the MAL messages to a concrete transport protocol. If any MAL implementation or transport binding implementation adheres to this standard, they can be plugged into the MO stack without major effort.
This tutorial gives you a hands-on approach for implementing a new transport binding and for implementing new MO services (the latter is planned for a future revision of the tutorial).
The MAL Java API is standardised in CCSDS 523.1-M-1. Based on this standard you may implement your own MAL Java API, but it is more likely you would like to use an existing implementation. CCSDS provides a reference implementation developed by ESA. Additionally, several transport bindings are available as well as a code generator to transform an abstract service specification in XML to stubs for different technologies (e.g. Java, Word documents, …).
The ESA contributions can be obtained from GitHub. In particular this tutorial assumes you have cloned the following repositories:
CCSDS_MO_POM
CCSDS_MO_XML
CCSDS_MO_StubGenerator
CCSDS_MO_APIS
CCSDS_MO_MAL_IMPL
CCSDS_MO_TRANS
CCSDS_MO_APPS
These repositories contain a full MO stack to get you started (service definitions in XML, stub code auto generator, MAL API and reference implementation, different transport bindings and a test application). Please note that the test bed is contained in the CCSDS_MO_TESTBEDS
repository, which helps you with testing and validating your implementations but is not part of an MO stack. Please refer to The MAL test bed section in this document to learn more about the test bed.
In order to start implementing your own service or transport binding you first need to compile the cloned repositories and make them available to you and your team. This process heavily relies on Maven and it is assumed you are familiar with its concepts. Please execute
mvn install
in repositories 1 to 5 of the above list in exactly this order. If you wish to make use of one of the example transport bindings execute
mvn -P ESA install
in CCSDS_MO_TRANS
(repository #6). You may specify a different profile in the POM in order to use a different transport binding. If you want to make the libraries available to your team using a respoitory manager replace install
with deploy
in the commands above and do not forget to change the POMs according to your infrastructure. If you want to attach sources and documentation the Maven goals source:jar
and javadoc:jar
are your friends.
If you want to check that all artifacts installed successfully you can run the demo application which is included in CCSDS_MO_APPS
(repository #7). The demo application consists of a data provider and several data consumers connecting to the provider. Compile it with mvn install
and then execute
runProvider.bat
in the CCSDS_MAL_DEMO
directory to start the provider (assuming you are on a Windows machine; running on different systems should be straight-forward with the batch file as starting point). You can start one or more consumers by executing
runConsumer1.bat
where you replace 1
with 2
or 3
to start a second and a third consumer. For each service entity a window pops up. Connect the consumers to the provider by clicking Consumer > Reconnect in the consumer window. Start delivering telemetry by clicking Publisher > Start TM in the provider window, optionally try different settings. You should see the consumers displaying the data.
The concepts set forth in the MAL are realized in the MAL Java API. Unfortunately, the connection between the two is not always obvious. This section aims to explain how some of the MAL concepts are realized in the Java API.
- Realization of primitives (requests and indications) in MAL Java API.
- Representation of the MAL data types.
- …
The MAL Java API is organized in several packages:
-
org.ccsds.moims.mo.mal
Interfaces and classes representing operations, services, standard errors and exceptions, and top-level MAL functions. Encoding and decoding interfaces are also defined here. -
org.ccsds.moims.mo.mal.structures
Representation of data types defined by the MAL. -
org.ccsds.moims.mo.mal.consumer
This part of the API is dedicated to the MAL clients initiating interactions as service consumers. -
org.ccsds.moims.mo.mal.provider
This part of the API is dedicated to the MAL clients handling interactions as service providers. -
org.ccsds.moims.mo.mal.broker
This part of the API is dedicated to the MAL clients handling PUBLISH-SUBSCRIBE interactions as brokers. -
org.ccsds.moims.mo.mal.transport
The transport API defines the interfaces and classes required by the MAL transport interface. -
org.ccsds.moims.mo.mal.accesscontrol
The access control API defines the interfaces and classes required by the MAL access control interface. -
org.ccsds.moims.mo.mal.encoding
The encoding API should be used by the transport modules that need to externalise the encoding behaviour of a body element.
For the sake of simplicity the package name prefixes will be omitted from here on.
This tutorial provides guidelines on how to use the MAL Java API in the following situations:
- Implementation of a transport binding. It will make use of the following MAL Java API packages:
-
transport
- transport API -
encoding
- (transport) encoding API
-
- Implementation of an MO service (TODO for a future revision). It will make use of the following MAL Java API packages:
consumer
provider
The packages mal
and structures
are always needed. Of course not all classes and interfaces in these packages are implemented in the reference MAL implementation. Unfortunately, there does not seem to be any information about the implementation responsibilities available, so this tutorial will try to give an overview of what has been or needs to be implemented by a MAL implementation, a transport binding implementation and a service implementation.
The implementation of access controls, brokers or extensions to the MAL data model are not part of this tutorial.
In order to implement a transport binding most of the transport
interfaces and classes (the transport API) have to be implemented. These are:
- class
MALTransportFactory
- interface
MALTransport
- interface
MALEndpoint
- interface
MALMessage
- interface
MALMessageHeader
- interface
MALMessageBody
- interface
MALErrorBody
- interface
MALRegisterBody
- interface
MALDeregisterBody
- interface
MALPublishRegisterBody
- interface
MALPublishBody
- interface
MALNotifyBody
The MAL exclusively uses this API for binding to a transport. However, in face of modularity and reusability it is recommended to use the encoding API provided by the MAL Java API if applicable to the transport. Usage of the encoding API is optional, but provides a means to separate the actual encoding from the transport interface. Because some API parts responsible for encoding reside outside the encoding
package but inside the mal
package, a more precise wording will be employed throughout this document: The API contained in the encoding
package is called transport encoding API from now on (the MAL Java API calls this just encoding API). The API parts responsible for encoding in the mal
package are now called data encoding API. Together they form the encoding API, but we try to avoid this term because it collides with the different meaning of what the MAL Java API defines as encoding API.
Data/transport encoding here means the on-the-wire encoding of the message body contents of a MAL message, while transport stands for encoding everything else which is necessary to transport a MAL message (MAL message header, protocol specific information). The transport component uses the encoded MAL message body created by the data/transport encoding component as a Blob
or byte array. The MAL Java API does not enforce this modularization and you are free to implement a monolithic transport binding, especially if the distinction between data/transport encoding and transport does not work for your transport binding.
If you decide to make use of this optional API, these are the classes and interfaces that need to be implemented:
-
transport encoding API (package
encoding
)- class
MALElementStreamFactory
- interface
MALElementInputStream
- interface
MALElementOutputStream
- class
-
data encoding API (package
mal
)- interface
MALEncoder
- interface
MALDecoder
- interface
MALListEncoder
- interface
MALListDecoder
- interface
So how do you know, if your transport is compatible with the data/transport encoding API? If you can split the encoding of a MAL message such that it can be encoded element-wise using a stream of elements, chances are high your transport is compatible with this API. You might even consider to reuse an already existing encoding implementation.
In order to implement a transport binding you should know how the transport API is supposed to be used by the MAL. This does not become very clear from the MAL Java API, so we try to give an overview here.
In order to use a transport the MAL creates an object that implements MALTransportFactory
. It is by configuration that the MAL knows which MALTransportFactory
implementations are able to handle which protocol (all available transport factories are held in a registry). This logic resides in the static methods of MALTransportFactory
, which are already implemented. Because one factory class may handle multiple protocols, the protocol name is passed in during factory construction. There will be one transport factory instance for each protocol. When your specific transport protocol gets used for the first time the MAL calls createTransport()
, which is the only method besides the constructor you have to implement.
createTransport()
is then responsible for creating an instance of MALTransport
. Thus, there should be one instance of a MALTransport
implementation for each protocol. Now, if the MAL wants service instances to communicate with each other, it assigns a so called endpoint to each of them (actually, there is a one-to-many relationship between endpoint and service instances). Each endpoint gets assigned a URI unique to this specific protocol. It is your responsibility as transport binding implementor to allocate this URI. The endpoint is represented by the MALEndpoint
interface and created by the MAL calling createEndpoint()
on the transport object. After the endpoint has been created, it can be used by the MAL to send and receive messages, which will be described in the following sections.
When the MAL has to send a message it does so by first constructing it with a call to the endpoint’s createMessage()
method. This method exists in four overloaded definitions with the only difference being how the MAL operation and the message contents, the body, are passed in. While the representation of the MAL operation is only a technical detail, message content passing is more interesting. The MAL either passes an Object
array or a MALEncodedBody
. While the MALEncodedBody
represents a message body that is already encoded (e.g. because it has been received by a broker and does not need to be decoded in order to relay it to subscribers), the Object
array represents the message body elements. Unfortunately, the MAL Java API does not get concrete about the possible types in the Object
array, so it is best to assume to get anything, from a Java mapping type, a Union
type, null
or even a MALEncodedBodyElement
. The MAL expects a MALMessage
as return type of createMessage()
. If available, you should provide a specialized class of MALMessage
depending on the operation being executed. The framework provides you with these specialized interfaces:
- interface
MALErrorBody
- interface
MALRegisterBody
- interface
MALDeregisterBody
- interface
MALPublishRegisterBody
- interface
MALPublishBody
- interface
MALNotifyBody
At some later point the MAL may decide to actually send a message and will call an endpoint’s sendMessage()
or sendMessages()
method. Now, it is up to the binding implementation to properly encode the message and send it out. If your binding uses splitting into transport and data/transport encoding, then this is all orchestrated by the transport component and you have to know how to use the encoding component properly. The idea behind the encoding API is that you can read and write elements from and to a stream. You extend MALElementStreamFactory
to create these input and output streams (MALElementInputStream
, MALElementOutputStream
). Your implementation of MALElementOutputStream
contains the method writeElement()
that takes a MAL element, encodes it and writes it to the stream. Encoding of an element works by calling its encode()
method. Because the element implementation is part of the MAL, it does not know how to encode itself. Therefore you pass in an implementation of MALEncoder
, which contains encoding methods for all basic MAL element types. The element’s encode()
method then uses these methods to encode itself. It is up to you as transport binding implementor to implement MALEncoder
. If lists are about to be encoded, you should use an implementation of MALListEncoder
in order to encode the list the way you want (e.g. with a length descriptor at the beginning or with a stop symbol or whatever you can think of or are required to do in order to stay compliant with your transport). To start encoding of the message body elements you can use MALElementStreamFactory.encode()
that takes an array of message body elements.
It is the responsibility of the transport binding to ensure messages sent to it are received properly. This depends very much on the nature of the transport and you should know best how to do this (e.g. by creating threads waiting for input on some communication socket). The MAL is informed about new messages by calling the onMessage()
method of a MALMessageListener
. The object implementing the MALMessageListener
interface has been previously set by the MAL on a specific endpoint using the enpoint’s setMessageListener()
method. It is the MAL’s responsibility to provide a MALMessageListener
implementation.
Decoding elements works similar to encoding them: You create an implementation of MALElementInputStream
, call the readElement()
method to read in an encoded element from the input stream and return the decoded element. However, you will soon realize that you need to pass in an element before you can read an element. This is on purpose, because in order to choose the right decoding method for the following element you need to know the type of the element to decode. The MAL passes in some kind of prototype element with a throw-away value, but whose decode()
method you can use to properly decode the element in the input stream. This will not work if polymorphism is employed for the last message body element (it is only allowed there). In this case the passed in element is null
and you have to determine the type by decoding type information that your encoder should have written to the stream. As for obtaining type information you can also query the MAL service that triggered the message to be generated. The service description defines the operations and data types and the MAL provides an API to retrieve this information. The following code snippets shows an example on how to use a MALEncodingContext
to find out information about the current interaction and possible types:
MALEncodingContext ctx = ...;
MALMessageHeader header = ctx.getHeader();
UOctet stage = header.getInteractionStage();
InteractionType interaction = header.getInteractionType();
Boolean isError = header.getIsErrorMessage();
MALOperationStage operationStage = ctx.getOperation().getOperationStage(stage);
int bodyElementIndex = ctx.getBodyElementIndex();
Long shortForm = (Long) operationStage.getElementShortForms()[bodyElementIndex];
Object[] allowedShortForms = operationStage.getLastElementShortForms();
This can be useful if your encoding scheme makes a distinction between a general abstract element and an abstract attribute. Because the passed in prototype element is null
in both cases, you have to retrieve type information from the service (the allowed short forms) to decide if the element is an abstract attribute or not. You can then create your own prototype element using
MALContextFactory.getElementFactoryRegistry().lookupElementFactory(Long.valueOf(shortForm)).createElement();
Unfortunately, these convenience methods are missing from the MAL Java API. Conceptually, decoding then works like encoding: The decode()
method uses the decoding methods of the passed in implementation of MALDecoder
or MALListDecoder
that you provide.
future revision, ideas:
- How to run code auto-generator if service XML is available?
- Explain the structure of the auto-generated code and how to go on from there (in fact this should already become clear from the MAL Java API, but wrap it up here in a way suitable as tutorial).
- Make clear how services are used from user applications.
elaborate on:
- Meaning of MAL element types.
- Union types and native Java types.
- Durations, Time, FineTime: Configuration and epoch information.
- Somewhat convoluted configuration system.
- Missing convenience functions.
The MO test bed provides a common ground for testing new implementations of MO interfaces. At the time of writing tests exist covering the Message Abstraction Layer (MAL) and the Space Packet Protocol binding (MAL/SPP binding). Any implementation of these interfaces should pass the tests defined in this test bed in order to be considered MO compliant. The MAL/SPP binding tests are still work in progress and not yet usable as a reference test system.
The test bed is organized as a collection of several Maven artifacts. Therefore the described usage of the test bed heavily depends on Maven. Depending on your or your organization’s needs a repository manager such as Nexus is in place. Here it is assumed that you have a working infrastructure for getting and deploying Java artifacts.
The following prerequisites have to be met in order to get the test bed up and running:
- Java 7 JDK
- local Maven setup (with working repository manager if necessary)
- the actual test bed (not yet publicly accessible), extracted into a directory without spaces (will be the root directory for all subsequent directory references)
At the time of writing the test bed consists of these five components:
-
MOIMS_TESTBED_MAL
Contains test definition FitNesse wiki pages for testing a MAL implementation. The fixture code in Java connecting the wiki pages to the components under test is also included here. -
MOIMS_TESTBED_POM
The parent POM used by all test bed components. It is advisable to declare your dependencies in its<dependencyManagement>
section. -
MOIMS_TESTBED_SPP
Contains test definition FitNesse wiki pages for testing a Space Packet Protocol binding implementation. In future this might serve as a template for testing other protocol bindings as well. -
MOIMS_TESTBED_SPP_FRAMEWORK
Contains the Space Packet Protocol binding fixture code in Java connecting the wiki pages to the components under test. In future this might serve as a template for other protocol bindings as well, provided they are structured similarly to the Space Packet Protocol binding implementation. -
MOIMS_TESTBED_UTIL
Contains the test bed components responsible for setting up the test framework, i.e. starting the FitNesse server and setting up providers and consumers. The exact setup is controlled by configuration files.
The test bed provides a means to set up two complete MO stacks communicating with each other through some underlying technology. The stacks are called remote and local and are executed in their own processes. Each stack consists of (from top to bottom):
- test application
- service provider/consumer
- MAL implementation
- transport implementation
The test bed is run as a dummy JUnit test contained in MOIMS_TESTBED_MAL
or MOIMS_TESTBED_SPP
using the FitNesse JUnit test runner. This test is contained in src/test/java/org/ccsds/moims/mo/mal[spp]/test/suite/JUnitSuiteTest.java
and configured with annotations. The @FitnesseDir
annotation denotes the root directory of FitNesse, the @Name
annotation the test or suite name to be executed. Thus, in order to execute acceptance tests involving the whole MO stack simply execute this JUnit test. FitNesse then controls the test execution using the wiki pages and the fixture code.
It is your responsibility to define a sensible MO stack. First you need to define the dependencies you need in order to execute your tests. You do this by defining your own profile in the POM files of the tests and adding your artifacts there. In a second step you need to tell the FitNesse system how to wire together these components, i.e. you need to specify which implementation of the MO interfaces you want to use. This is done with two sets of configuration files, one for the local stack, another one for the remote stack. Your stack should consist of tested components except for the one you want to test. Otherwise you will have a hard time finding the culprit in case tests fail.
Configuration of the test bed is split across several files. The format is that of a standard Java property file. These files are loaded during start-up of the test bed in the order they are listed here. Their content is put in the global System
properties (?). Not all of these files need to be present, as long as you properly set all configuration parameters required by your deployment. The file structure assists you in keeping like parameters together in one file. File location for the local stack is ./src/main/resources/deployment/[profile]/
, where [profile]
is a profile for your configurations that should be present in the POM and selected with Maven’s -P
option for executing the tests. The remote stack’s configuration files reside in ./
.
File name | Description |
---|---|
SuiteManagementEnv.properties |
deployment specific properties (?) |
SuiteManagement.properties |
test specific properties (?) |
BaseLocalMALInstanceEnv.properties |
|
BaseLocalMALInstanceMAL.properties |
|
LocalMALInstanceEnv.properties |
|
LocalMALInstanceMAL.properties |
|
RemoteMALInstance.properties |
(test specific??) |
BaseTestServiceProviderEnv.properties ?? |
|
BaseTestServiceProviderMAL.properties ?? |
|
TestServiceProviderEnv.properties |
|
TestServiceProviderMAL.properties |
The configuration properties currently understood by the test bed are listed here, together with a description. Names in the following tables’ first columns have to be prepended with org.ccsds.
to yield the proper property name. Each component (including your own) may add its own list of configuration properties, which are not listed here of course.
Property name | Description |
---|---|
moims.mo.mal.transport.default.protocol |
|
moims.mo.testbed.transport.factory |
fully qualified class name of the test bed transport factory |
moims.mo.testbed.transport.protocol |
transport protocol used for the test bed (e.g. malspp or rmi) |
Property name | Description |
---|---|
moims.mo.testbed.local.class |
local MAL class; the local MAL instance, brokers and service providers and consumers are brought into life here; for testing the MAL this would be org.ccsds.moims.mo.mal.test.suite.LocalMALInstance
|
moims.mo.testbed.local.configuration.dir |
local configuration directory (?) |
Property name | Description |
---|---|
remote.cmdline.{OS_NAME}.{#} |
remote command line prefix; used to start the remote process; replace {OS_NAME} with your operating system name and replace spaces with underscores; replace {#} with the position of the command line argument, starting at 1 (e.g. ...Windows_XP.1=cmd , ...Windows_XP.2=/c ) |
moims.mo.testbed.remote.class |
fully qualified class name of the test service provider; for testing the MAL this would be org.ccsds.moims.mo.mal.test.suite.TestServiceProvider
|
moims.mo.testbed.remote.output.dir |
|
moims.mo.testbed.remote.configuration.dir |
|
moims.mo.testbed.remote.extra.args |
extra arguments for starting the remote JVM; this could be useful if you want to connect a debugger to the remote JVM; in this case use something like -agentlib:jdwp=transport=dt_socket,server=y,address=54320,suspend=n
|
moims.mo.testbed.remote.classpath.extra |
|
moims.mo.testbed.remote.classpath.maven |
|
moims.mo.testbed.remote.classpath.filter |
Property name | Description |
---|---|
moims.mo.testbed.wait.timeout |
|
moims.mo.testbed.transport.level.shared.broker |
|
moims.mo.mal.security.factory.class |
Property name | Description |
---|---|
moims.mo.mal.accesscontrol.factory.class |
fully qualified class name of the access control factory class; for a test access control factory class use org.ccsds.moims.mo.mal.test.accesscontrol.TestAccessControlFactory
|
moims.mo.mal.factory.class |
fully qualified class name of the MAL factory class; for the ESA MAL implementation this is esa.mo.mal.impl.MALContextFactoryImpl
|
In addition to those properties each component of the MAL stack may provide its own configuration parameters. Refer to the component’s documentation for more information. It is good practice to prepend the configuration parameters of your own components with their package names.