Skip to content

Java client

The Java client lets an external Java application publish real-time objects to a Relay, answer commands, and subscribe to Center tables in 3forge Enterprise, using classes that ship inside the install. Two classes divide the work and they differ in what they require:

Class Use it for Needs a license
AmiClient Publishing real-time objects to a Relay, and answering commands No
AmiCenterClient Subscribing to Center tables Yes

Prerequisites

Item Value
Java JDK 8 or later
Classpath lib/amicore.jar from the Enterprise install. There is no separate client-only jar
Relay host and port The Relay's realtime port, default 3289, set by ami.port
Center port, for subscribing Default 3270, set by ami.center.port
A login id The identity the application connects as. Authentication is covered in Client library configuration
A client keystore Step 1 of Connect creates one if it does not exist, and writes a sibling .pub beside it
Your public key registered on the server Give that .pub line to whoever administers the server. It goes in the authorized-clients file, keys/3forge.auth, and the server must be restarted before it takes effect. See Connecting to a secured server
The server's public key keys/3forge.pub from the server host, used to pin the server

The realtime port expects a certificate from both sides

A default install sets both a keystore (ami.port.keystore.file) and a client allowlist (ami.port.ssl.authorized.clients.file) on the realtime port, so the default posture is mutual TLS. An application that connects without a client certificate is accepted, reports a successful login, and is then disconnected.

Connect

The publishing client cannot build a mutual-TLS connection on its own, so the application builds the socket and hands it over. How the client authenticates explains why.

Substitute these values:

Placeholder in the example Meaning
RELAY_HOST Host running the Relay
RELAY_PORT The Relay's realtime port, default 3289
LOGIN_ID The identity the application connects as
KEYSTORE_PATH Where your client keystore is, or should be created
KEYSTORE_PASSWORD Password protecting that keystore
SERVER_PUB_PATH The server's published public key, keys/3forge.pub on the server host
  1. Build an SSLContext. One call does both halves: it presents the client's identity and pins the server against the public key the server publishes. If the keystore does not exist yet, it is created, along with a sibling .pub file to hand to whoever administers the server.

    import com.f1.ami.client.AmiClient;
    import com.f1.ami.client.AmiClientListener;
    import com.f1.utils.impl.KeyHelper;
    import com.f1.utils.BasicResourceOpener;
    import com.f1.base.Password;
    import javax.net.ssl.SSLContext;
    import javax.net.ssl.SSLSocket;
    import java.util.Map;
    
    SSLContext sslContext = KeyHelper.loadOrGenerateClientSSLContext(
            BasicResourceOpener.INSTANCE.toFile(KEYSTORE_PATH),
            Password.valueOf(KEYSTORE_PASSWORD),
            BasicResourceOpener.INSTANCE.toFile(SERVER_PUB_PATH));
    

    The file arguments are a platform file type rather than a path or a java.io.File, which is why each one goes through BasicResourceOpener.INSTANCE.toFile(...).

    Do not trust the server from your own keystore

    It is tempting to initialise a TrustManagerFactory from the same keystore that holds your client key. That appears to work only if the keystore happens to contain the server's certificate, which is true of the server's own keystore and false of one you generated. Pin the server against its published .pub instead, as above.

  2. Implement a listener. AmiClientListener declares six methods, and every one must be present. onMessageReceived is the one that reports success or failure.

    class RelayListener implements AmiClientListener {
        public void onConnect(AmiClient c) { }
        public void onLoggedIn(AmiClient c) { System.out.println("logged in"); }
        public void onDisconnect(AmiClient c) { System.out.println("disconnected"); }
        public void onMessageSent(AmiClient c, CharSequence message) { }
        public void onMessageReceived(AmiClient c, long now, long seqnum,
                                      int status, CharSequence message) {
            System.out.println("status=" + status + " " + message);
        }
        public void onCommand(AmiClient c, String requestId, String cmd, String userName,
                              String objectType, String objectId, Map<String, Object> params) { }
    }
    
  3. Open the socket and complete the handshake.

    1
    2
    3
    SSLSocket socket =
            (SSLSocket) sslContext.getSocketFactory().createSocket(RELAY_HOST, RELAY_PORT);
    socket.startHandshake();
    
  4. Start the client on that socket.

    1
    2
    3
    AmiClient client = new AmiClient();
    client.addListener(new RelayListener());
    client.start(socket, LOGIN_ID, AmiClient.ENABLE_AUTO_PROCESS_INCOMING);
    

    ENABLE_AUTO_PROCESS_INCOMING is a constant on AmiClient. It starts a thread that reads incoming messages and dispatches them to the listener. Without it, nothing arrives. Combine constants with |, for example AmiClient.ENABLE_AUTO_PROCESS_INCOMING | AmiClient.DISABLE_AUTO_RECONNECT.

Result

The listener prints one message:

status=0 Welcome to 3forge AMI v1.0

That single line is the success signal, and nothing further arrives until the client sends something. A connection that stays open after it is working.

A successful login followed shortly by a disconnect is a failure, not a success: see Troubleshooting.

Publish data to a Relay

An object has a type, an identifier and named parameters. Build it, then flush it.

1
2
3
4
client.startObjectMessage("Trades", "trade-1")
      .addMessageParamString("sym", "IBM")
      .addMessageParamLong("qty", 100L);
client.sendMessageAndFlush();

Result

The listener reports:

status=0 OK, object accepted

The object also reaches a Center table named after its type, where named parameters become columns and the object's identifier lands in a column called I.

To confirm that from Java alone, subscribe to the same table from a second program, as described in Receive data from a Center. Running it in a separate process after the publisher has exited shows the row is held by the server rather than echoed locally:

onSchemaChanged table=Trades columns=[sym, qty, I]
ADD:13009338:Trades{sym=IBM, qty=100}

This page does not cover running SQL from Java. If a SQL surface is available to you, the same check is SELECT * FROM Trades.

The table is created on first publish

Publishing an object whose type the Center has not seen creates a table for it, so this round trip completes on a default install with no routing configured in advance. A table created this way adds unknown parameters as new columns. A table that already exists may instead be set to reject them, in which case an unexpected parameter is refused rather than added. The setting is the table's OnUndefColumn.

The data outlives the publisher. A row stays in the table after the publishing program exits, so a subscriber started later still receives it. Removing a table created this way is a server-side task, not something the client API does.

Receive data from a Center

Subscribing uses AmiCenterClient, which accepts an SSLContext directly and so needs no hand-built socket.

import com.f1.ami.client.AmiCenterClient;
import com.f1.ami.client.AmiCenterClientSubscription;
import com.f1.utils.BasicResourceOpener;
import java.util.Collections;

AmiCenterClient centerClient =
        new AmiCenterClient(BasicResourceOpener.INSTANCE, LOGIN_ID);
centerClient.connect("C1", CENTER_HOST, CENTER_PORT, sslContext, new CenterListener());
centerClient.subscribe("C1", Collections.singletonList(
        new AmiCenterClientSubscription("Trades")));

BasicResourceOpener.INSTANCE is the default file-access strategy the client uses to resolve paths. "C1" is a name the application chooses for the connection and then passes to subscribe.

AmiCenterClientListener declares six methods, and all six must be present:

import com.f1.ami.amicommon.AmiCenterDefinition;
import com.f1.ami.amicommon.AmiColumnDef;
import com.f1.ami.amicommon.centerclient.AmiCenterClientListener;
import com.f1.ami.amicommon.centerclient.AmiCenterClientObjectMessage;

class CenterListener implements AmiCenterClientListener {
    public void onCenterConnect(AmiCenterDefinition center) { }
    public void onCenterDisconnect(AmiCenterDefinition center) { }
    public void onSchemaChanged(AmiCenterDefinition center, String[] tableNames,
                                byte[] tableHints, String[][] columnNames,
                                AmiColumnDef[][] defs) { }
    public void onCenterMessage(AmiCenterDefinition center,
                                AmiCenterClientObjectMessage[] messages) {
        for (AmiCenterClientObjectMessage m : messages) {
            System.out.println(m);
        }
    }
    public void onCenterMessageBatchDone(AmiCenterDefinition center) { }
    public void onUnsubscribed(AmiCenterDefinition center, String tableName) { }
}

Result

The callbacks arrive in this order:

  1. onCenterConnect, once the connection is established.
  2. onSchemaChanged, before any subscription, describing every table in the Center rather than only the one subscribed to later. Use it to learn what is available.
  3. onCenterMessage after subscribe, carrying the subscribed table's rows. Each message states an operation, a sequence number, the table, and the row:

    ADD:13000002:__TABLE{TableName=__ANNOTATION, Broadcast=true, RefreshPeriodMs=250, ...}
    
  4. onCenterMessageBatchDone, once per batch.

The initial delivery carries one message per existing row, so a table of 22 rows produces 22 messages in a single batch. Later changes arrive as further messages with their own operation, such as UPD for an update to a row already delivered.

The identifier column is not in the message body

onSchemaChanged lists I among the table's columns, but the message text carries only the named parameters, as {sym=IBM, qty=100}. A subscriber can see that the column exists and cannot read back the identifier a publisher used. Where a subscriber needs the identifier, make it one of the named parameters as well.

Subscribing requires a license; publishing does not

Creating AmiCenterClient starts a 3forge container inside the application, and that container requires a license. Without one the application exits with code 23 and the message License File not found, which also lists the accepted ways to supply a license file. Point it at a license with -Df1.license.file=<path>. On acceptance it reports the file and its expiry date, and subscription then proceeds normally.

AmiClient has no such requirement. An application that only sends data never meets this.

Respond to commands

The platform can invoke commands an application has registered, which is how an operator triggers work inside a running client. Define the command, send the definition, and answer the callback in onCommand.

1
2
3
4
5
6
7
import com.f1.ami.client.AmiClientCommandDef;

AmiClientCommandDef def = new AmiClientCommandDef("restartFeed");
def.setName("restartFeed");
def.setHelp("What an operator sees when choosing this command");
client.sendCommandDefinition(def);
client.flush();

Result

The listener reports:

status=0 OK, command accepted

The definition is registered centrally for as long as the client stays connected, and is removed when the client disconnects. A command is therefore scoped to the connection rather than stored, so it needs no cleaning up after a client exits, and a client must re-register its commands on every reconnect.

When the command is invoked, onCommand receives a request id, the command id, the invoking user, the object type and id the command was run against, and a map of arguments. Reply by starting a response message against that request id and flushing it:

client.startResponseMessage(requestId, AmiClient.RESPONSE_STATUS_OKAY, "done");
client.sendMessageAndFlush();

Registering a command is covered here; invoking one is not

This page covers the client side: defining a command, sending the definition, and answering onCommand. How an operator triggers a registered command from the platform is not described here, so confirm that end of the round trip with whoever administers your deployment before relying on it.

How the client authenticates

AmiClient offers four ways to start. Only the socket form can present a client certificate, which on a default install makes it the only one that stays connected.

Call Behavior
start(socket, loginId, options) The application builds the TLS socket, so it controls both the certificate presented and the trust material.
start(host, port, loginId, options) Opens a plain connection. Against a TLS port it logs in, then is dropped.
start(host, port, loginId, options, file, String password) The file is used to pin the server's public key. The password is not used.
start(host, port, loginId, options, file, Password password) As above. Passing a keystore to the file parameter fails.

The file parameter on the last two is not a path or a java.io.File: it is a platform file type that must be obtained from a resource opener rather than constructed directly. Since neither form sends a client certificate, there is no reason to call either, and this page does not show how.

The file-and-password forms do not send a client certificate

Their shape suggests the file is the application's keystore. It is not: it pins the server's public key, and the password argument is ignored. No host-and-port form presents a client certificate, which is why step 3 of Connect opens the socket directly.

AmiCenterClient is built differently and takes an SSLContext, so the limitation above applies only to publishing.

Troubleshooting

Symptom Cause What to do
onLoggedIn fires, then onDisconnect. Most often with General error: missing @ for string ' ', sometimes with a socket abort A plain connection to a TLS port. The parser error is the client reading raw TLS bytes as protocol text, so grep for missing @ as well as for a socket error Build the socket as in Connect
The handshake itself fails, with a socket write error. onConnect never fires The certificate's public key is not in the server's authorized-clients file, or it was added and the server has not been restarted since Register the key and restart the server. See Connecting to a secured server
That sequence repeating Automatic reconnection is on Pass AmiClient.DISABLE_AUTO_RECONNECT while debugging so the failure holds still
java.lang.RuntimeException: Error building sslcontext from public key A keystore was passed where a server public key is expected Use the socket form instead
Exit code 23, License File not found AmiCenterClient needs a license Supply a license by one of the routes the message lists, or publish only, which needs none
Nothing arrives on the listener ENABLE_AUTO_PROCESS_INCOMING was not passed, so no thread is reading Add it to the options argument

What this client does not do

  • It does not run SQL. There is no JDBC or query entry point in these classes. Use the Python or C++ client where a program needs to query.
  • Calls are asynchronous. Connecting and subscribing return immediately and the results arrive on your listener, so there is no equivalent of the Python client's sync=True. Wait on your own signal inside the callback rather than expecting the call to block.
  • A subscriber cannot read back a publisher's identifier, as described above.