This provides a JAX-RS standard, Type-Safe means of interacting with Symphony pods via REST apis.
bindings
is a set of swagger-generated domain objects and interfaces that provide a Java API for interacting with Symphony's REST endpoints.
Since these interfaces are mainly generated, there isn't a lot of code checked in here.
<dependency>
<groupId>org.finos.symphony.toolkit</groupId>
<artifactId>bindings</artifactId>
<version>-- see above for version --</version>
<scope>compile</scope>
</dependency>
There are alternatives to using this package, such as:
- Symphony API Client Java - Authored by Symphony. Quite new.
- Symphony Java Client - Older, unsupported.
So why do we need a new one? Some reasons:
- Configuration: These libraries all try to specify configuration formats. However, in many complex corporate environments involving multiple proxies, firewalls and DMZs, the configuration is often too complex to express in the JSON files that they require. This package leaves configuration up to JAX-RS apis.
- Code Generation: The above libraries all rely on hand-maintained code to marshall/unmarshall REST, and the code is checked in. Here it is swagger generated and not human-maintained (this project only contributes 8 hand-written classes).
- Interface Consistency. Because the interfaces are generated this has the advantage that they exactly match the REST endpoints documented by Symphony.
- Apache CXF or Jersey: Since the apis use Jax-RS specification, you can use any compatible implementation. Tests exist for these two popular offerings. Other JAX-RS implementations (e.g. RestEASY) are available, but you may have to work around the limitations of multipart support in the standard (see below).
The simplest way to instantiate any Symphony API is via the ApiBuilder
interface and it's subclasses, JerseyApiBuilder
and CXFApiBuilder
like so:
MessagesApi messagesApi = apiBuilder.getApi(MessagesApi.class);
You can instantiate one of the builders in the following way:
ApiBuilder apiBuilder = new JerseyApiBuilder(<pod endpoint url>);
ApiBuilder apiBuilder = new CXFApiBuilder(<pod endpoint url>);
The apis themselves are held in four packages, com.symphony.api.pod
, com.symphony.api.agent
, com.symphony.api.authenticator
and com.symphony.api.login
.
If you are trying to find the right class given the Symphony endpoint, the search box on this page is very helpful.
This library doesn't transitively include either Apache CXF or Jersey. It is up to you which JAX-RS implementation you use, and you will need to import the classes to support this into you project like so (maven example included):
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-rs-client</artifactId>
<version>3.3.1 (or later)</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>2.28</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>2.28</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.connectors</groupId>
<artifactId>jersey-apache-connector</artifactId>
<version>2.28</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-multipart</artifactId>
<version>2.28</version>
<scope>compile</scope>
</dependency>
NB: Do not include (either directly or transitively) jersey-media-json-jackson
. This screws up forward-compatibility for message reading, and in any case is not recommended anymore by the Jersey/Jackson teams.
Both Apache CXF and Jersey support proxies. You can either configure your JAX-RS Client
yourself, or set the proxy inside the ApiBuilder
implementation by calling setProxy
.
Both Certificate Based Authentication and RSA Based Authentication are tested.
You can get a Symphony Session Token for a bot with the sessionauth
endpoint as shown below. Note you will need a Java KeyManager
implementation containing the certificate chain and private key for the bot identity for this to work. Jersey code is shown here but it will work the same if you swap JerseyApiBuilder
for CXFApiBuilder
.
JerseyApiBuilder b = new JerseyApiBuilder("https://<your pod>-api.symphony.com:443/sessionauth", <key manager>);
b.setProxyDetails(...);
Token t = b.getApi(AuthenticationApi.class).v1AuthenticatePost()
You can get a Symphony Key Manager token in the same way:
JerseyApiBuilder b = new JerseyApiBuilder("https://<your key manager host>:8444/keyauth", <key manager>);
Token t = b.getApi(AuthenticationApi.class).v1AuthenticatePost()
RSA Authentication is done against the pod, using JWT Tokens. JWTTokens can be generated from a private key via JWTToken
class which is supplied to help you. You can get a Symphony Session Token like so:
String jwt = JWTHelper.createSignedJwt(<bot service account id>, <expiry time>, <private key>);
JerseyApiBuilder b = new JerseyApiBuilder("https://<your pod>.symphony.com/login");
b.setProxyDetails(...);
AuthenticationApi sessionApi = s.getRSASessionAuthApi(); //nb. com.symphony.api.login.AuthenticationApi
AuthenticateRequest req = new AuthenticateRequest();
req.setToken(jwt);
Token done = sessionApi.pubkeyAuthenticatePost(req);
And a Key Manager token like so. Note that instead of calling the key manager directly, you call via the pod relay:
String jwt = JWTHelper.createSignedJwt(<bot service account id>, <expiry time>, <private key>);
JerseyApiBuilder b = new JerseyApiBuilder("https://<your pod>.symphony.com/relay");
b.setProxyDetails(...);
AuthenticationApi sessionApi = b.getApi(AuthenticationApi.class); // nb: com.symphony.api.login.AuthenticationApi
AuthenticateRequest req = new AuthenticateRequest();
req.setToken(jwt);
Token done = sessionApi.pubkeyAuthenticatePost(req);
Most of the REST Endpoints in the Symphony Spec require either a key-manager token or a session token.
It can be cumbersome to manage these parameters and pass them around, so the TokenManager
class can take care of this. It decorates the api instances to supply the tokens needed, and also to retry in the event of the tokens expiring.
You can instantiate and use a TokenManager
like so:
TokenManager tm = new TokenManager(() -> sessionAuthApi.v1AuthenticatePost(), () -> keyAuthApi.v1AuthenticatePost());
ConfigurableApiBuilder apiBuilder = ... // construct jersey or cxf api here
apiBuilder.setWrappers(new ApiWrapper[] { tm });
This will automatically be used to decorate apis returned by the builder. This means you can make the following call, for example:
MessagesApi messageAPi = apiBuilder.getApi(MessagesApi.class);
messageAPi.v4StreamSidMessageCreatePost(null, "1+rc4tMrxupuRV5OA3f09X___pXvTHdQdA==",
"<messageML>Trigger Listener.</messageML>", null, null, null, null, null);
Note that in this example, normally v4StreamSidMessageCreatePost
would take both the key manager token and the session token, but since we are using the token manager, neither need to be supplied in the call.
One interesting design decision is that Symphony use Stream IDs such as y3EJYqKMwG7Jn7/YqyYdiX///pR3YrnTdA==
to refer to their chat rooms. However, at the same time, they also use these Stream IDs as path elements within their REST API. This is a problem, as =
, +
and /
are not allowed within path elements. The class StreamHelp
provides a utility method that converts from the unsafe to the safe form of the StreamID.
StreamHelp
can be placed into the apiBuilder
much like the TokenManager
, e.g.:
apiBuilder.setWrappers(new ApiWrapper[] { tm, new StreamHelp() });
In this way, all REST calls will automatically have their stream IDs made safe.
Sadly, JAX-RS has not standardized multipart REST calls (yet - this will come). In the meantime, we are forced to supply implementations for Jersey and CXF to make these work, as these are used in the Symphony APIs DlpPoliciesAndDictionaryManagementApi
and MessagesApi
. (The latter is commonly used, as you need it for posting in Symphony.
ContentDispositionMultipartProvider
: this allows Apache CXF to correctly call these APIs and send multipart messages in a format Symphony can understand.MultipartWebResourceFactory
: This enhances Jersey's API implementation to support multipart messages.
These classes are automatically used when you instantiate APIs via JerseyApiBuilder
or CXFApiBuilder
.
The usual workflow of a bot is to listen to messages and respond to them. Ideally, this means you want to be programming via callbacks. symphony-client-bindings-java
provides a helper class, Streams
, so that you can consume events from Symphony in a stream fashion.
Here is an example:
JerseyApiBuilder apiBuilder = new JerseyApiBuilder("https://<agent host>:8444/agent"); (1)
apiBuilder.setTokenManager(...);
DatafeedApi dfApi = apiBuilder.getAgentApi(DatafeedApi.class); (2)
Datafeed df = dfApi.v4DatafeedCreatePost(null, null); (3)
Streams.createWorker(
() -> dfApi.v4DatafeedIdReadGet(df.getId(), null, null, 100), (4)
(e) -> e.printStackTrace()) (5)
.stream() (6)
.filter(e -> e.getType().equals("MESSAGESENT")) (7)
.map(e -> e.getPayload().getMessageSent().getMessage())
.forEach(m -> onRoomMessage(m)); (8)
- This constructs the apiBuilder to return agent APIs.
- This gives back the DatafeedAPI
- Returns the Datafeed. This is an ID you use to subscribe to messages in Symphony.
- Here, we are constructing the Stream Worker. It takes 2 parameters. The first is the api call we are going to make to get events. Here, we are reading from the datafeed. Note that we are using the Token Manager described above so that we don't need to worry about session/key manager tokens in our code.
- Here, you handle any errors that occur. In this example, we are simply printing them to the output. (Don't do this in production).
- The
Streams.Worker
instance has astream()
method, returning a regular Java8 stream. In this case, it will be returning Symphony Events. - Here, we are filtering to just find "MESSAGESENT" events. There are other events.
- We extract the message from each event.
- Then, we perform the callback to process the message.
This workflow is perhaps more complex than using other Symphony clients, but it has the following advantages:
- No Threads are created by the client directly (Jersey/CXF will probably create some though).
- Error handling is explicit. You get to decide what to do in the event of failures.
- Use of modern Java8 streams idiom.
- No opinion on which logging mechanism you are going to use for reporting errors.
Let's say you know the Symphony API you want to call. How should you find the Java interface, and the method on it? Sadly, this is not very clear from the swagger definitions!
You should be able to check the Javadocs to find this out - but be prepared to use the search function a lot.