Überblick

Die API eines auf Messages basierenden Programms besteht aus den Beschreibungen der Nachrichten sowie der Nachrichten-Sequenzen. Für den Aufbau der Nachrichten könnten wir ein einfaches Dokument (z.B. ein Spreadsheet) zur eine Beschreibung verwenden. Einfacher für den Benutzer ist jedoch die Verwendung von XML.

Die XML-Beschreibung der Nachrichten kannst du an jeden Interessenten deiner API weitergeben. Der interessierte Programmierer speichert die XML-Dateien in ein JAVA-Package seiner Wahl und ruft den Record-Generator auf. Das ist ein kleines devel.one-Tool, welches aus allen Record-Beschreibungen dann statische Zugriffsklassen für die Nachrichten generiert. Damit kann man dann typsicher auf die Daten in eines Records zugreifen. Zudem funktioniert das auch sprach-unabhängig, ist also nicht auf JAVA beschränkt. Das ganze ist natürlich optional; manuell geht es natürlich auch, aber nicht so effizient.

Die Record-Beschreibung

In einer Record-Datei können beliebig viele Nachrichten beschrieben sein. Hier ist eine XML-Datei, welche die Nachricht zum Erstellen eines Namespaces beschreibt:

<?xml version="1.0" encoding="ISO-8859-1"?>
<records>
    <record id="9f9dfda5-21aa-4fb9-a15f-5dccef2612e0" name="CREATE_NAMESPACE" isService="true">
        <description>
            Create, register and start a Namespace.
        </description>
        <namespace>SYSTEM</namespace>
        <slot keyid="" keytype="WANTED_NID" name="WANTED_NID" answer="false" mandatory="false" type="ID">
            <description>Optional: A namespace ID to use. If the argument is missing, a random numerical NID is used.</description>
        </slot>
        <slot keyid="" keytype="CREATED_NID" name="CREATED_NID" answer="true" mandatory="false" type="ID">
            <description>The namespace ID for the newly created namespace. Only on SUCCESS.</description>
        </slot>
    </record>
</records>

Die Nachricht hat die ID “9f9dfda5-21aa-4fb9-a15f-5dccef2612e0”. Das ist eine UUID in String-Form und damit gut geeignet für die Beschreibung von Services. Im Vergleich mit einem String oder mit einen Integer ist eine UUID recht groß (17 Bytes im Stream), aber dafür kollisionssicher. Für interne Messages kann man ja die schmale Variante wählen, siehe hier.

Der Name des Records “CREATE_NAMESPACE” führt zur Benennung der Klasse (CRecordCreateNamespace). Der Record wird für Services genutzt (isService=true), und der Service automatisch beim StartUp im Namespace “SYSTEM” registriert.

Es werden zwei Slots notiert: einen für den Request, den anderen für die Antwort (answer=true). Beide Slots sind vom Typ ID (Infos zu den Slot-Typen siehe hier). Für die Slotkeys wird nur der keytype verwendet. Die Namen des Slots führen zu der Benennung der Zugriffsmethoden:

static IId getParamCreatedNid(CRecord aRecord, IId aDefault);
static void setParamCreatedNid(CRecord aRecord, IId aValue) throws CException;

static IId getParamWantedNid(CRecord aRecord, IId aDefault);
static void setParamWantedNid(CRecord aRecord, IId aValue) throws CException;

Der Record-Generator erzeugt eine Klasse, mit der man einen Record füllen bzw. auslesen kann:

/**
 * Create, register and start a Namespace.
 */
public final class CRecordCreateNamespace
{
    /**
     * Message ID.
     */
    public static final IId ID = CIdFactory.create("9f9dfda5-21aa-4fb9-a15f-5dccef2612e0");

    /**
     * Slot CREATED_NID<br>
     * data type = ID<br>
     * mandatory = false<br>
     * answer = true<br>
     * The namespace ID for the newly created namespace. Only on SUCCESS.
     */
    public static final ISlotKey SLOT_CREATED_NID = CSlotKeyFactory.createByObject("CREATED_NID", "");

    /**
     * Slot WANTED_NID<br>
     * data type = ID<br>
     * mandatory = false<br>
     * answer = false<br>
     * Optional: A namespace ID to use. If the argument is missing, a random numerical NID is used.
     */
    public static final ISlotKey SLOT_WANTED_NID = CSlotKeyFactory.createByObject("WANTED_NID", "");

    /**
     * Create Record with this ID.
     * @return The record with this ID.
     */
    public static CRecord create()
    {
        return new CRecord(ID);
    }

    /**
     * The namespace ID for the newly created namespace. Only on SUCCESS.
     *
     * @param aRecord
     *        The record.
     * @param aDefault
     *        Default value, if slot (data) not found.
     * @return The value if found, otherwise the default value.
     */
    public static IId getParamCreatedNid(final CRecord aRecord, final IId aDefault)
    {
        final ISlot slot = aRecord.getSlot(SLOT_CREATED_NID);
        if (slot == null)
        {
            return aDefault;
        }
        else
        {
            return (IId) slot.getValue();
        }
    }

    /**
     * Optional: A namespace ID to use. If the argument is missing, a random numerical NID is used.
     *
     * @param aRecord
     *        The record.
     * @param aDefault
     *        Default value, if slot (data) not found.
     * @return The value if found, otherwise the default value.
     */
    public static IId getParamWantedNid(final CRecord aRecord, final IId aDefault)
    {
        final ISlot slot = aRecord.getSlot(SLOT_WANTED_NID);
        if (slot == null)
        {
            return aDefault;
        }
        else
        {
            return (IId) slot.getValue();
        }
    }

    /**
     * The namespace ID for the newly created namespace. Only on SUCCESS.
     *
     * @param aRecord
     *        The record.
     * @param aValue
     *        The value to set.
     * @throws CException
     *        on parsing the value.
     */
    public static void setParamCreatedNid(final CRecord aRecord, final IId aValue) throws CException
    {
        ISlot slot = CSlotFactoryHelper.create(CCommonSlotType.ID, aValue);
        aRecord.addSlot(SLOT_CREATED_NID, slot);
    }

    /**
     * Optional: A namespace ID to use. If the argument is missing, a random numerical NID is used.
     *
     * @param aRecord
     *        The record.
     * @param aValue
     *        The value to set.
     * @throws CException
     *        on parsing the value.
     */
    public static void setParamWantedNid(final CRecord aRecord, final IId aValue) throws CException
    {
        ISlot slot = CSlotFactoryHelper.create(CCommonSlotType.ID, aValue);
        aRecord.addSlot(SLOT_WANTED_NID, slot);
    }

    /**
     * Private Constructor (no instances wanted).
     */
    private CRecordCreateNamespace()
    {
        // no instance
    }
}

Der Aufbau einer Record-Beschreibung ist immer gleich (da generiert):

  • ID
  • SlotKeys
  • Getter
  • Setter

Die Verwendung der Klasse ist dann recht einfach. Hier ein Beispiel für das Abschicken der Nachricht:

final CEnvelope env = new CEnvelope(CWellKnownNID.SYSTEM);

final CRecord rec = new CRecord(CRecordCreateNamespace.ID);
CRecordCreateNamespace.setParamWantedNid(rec, aNid);

sendRequest(env, rec);

Und hier das Beispiel für die Bearbeitung der Antwort:

addMessageHandler(CRecordCreateNamespace.ID, new CMessageHandler(this, "AnswerCreateNamespace")
{
    @Override
    public boolean handleMessage(final CEnvelope aEnvelope,
                                 final CRecord aRecord) throws Exception
    {
        if (aEnvelope.isAnswer())
        {
            final int code = aEnvelope.getResultCode();
            if (code == CResultCode.SUCCESS)
            {
                //
                // Okay, namespace created.
                //
                final IId nid = CRecordCreateNamespace.getParamCreatedNid(aRecord, null);

                ...
            }
            else
            {
                //
                // Error creating namespace.
                //

                ...
            }
        }
        return true;
    }
});

Das Tool

../_images/RecordGenerator1.PNG

Der Generator erzeugt eine Record-XML-Datenbank, die vom Framework beim StartUp geladen wird. U.a. werden die Services, die in der Datenbank gespeichert sind, automatisch registriert. Damit das Tool erfährt, wo überall eine Kopie der Datei angelegt werden soll, kann man hier die Verzeichnisse angeben. Bei mehreren Instanzen von D1 legt man sie am besten auf einem zentralen File-Server ab.

../_images/RecordGenerator2.PNG

Da D1 die Registrierung von eigenen Slot-Typen zulässt, benötigt der Generator hier die CLASS-Files der Slot-Klassen, damit er diese Typen bei der Generierung kennt.

../_images/RecordGenerator3.PNG

Der “Search Path” ist das Grundverzeichnis deines Workspaces, als das Verzeichnis, wo die einzelnen D1-Projekte abgelegt sind. Der Generator durchsucht alle Verzeichnisse nach Dateien mit dem Namen “record.xml” und generiert in jeweils dasselbe Verzeichnis die JAVA-Klassen.

Für die Analyse der Package-Namen benötigt der Generator noch das Grundverzeichnis der Sourcen. In der Regel ist das wohl “src” oder auch “src\main\java” (bei der Benutzung von Gradle). Unter Windows bitte den Backslash benutzen, damit es klappt.

Anschließend kann man alle Klassen generieren. Das muss man natürlich bei jeder Änderung in den XML-Dateien wiederholen. Zum Glück geht das rasend schnell...