How to create an application

Computer, van Gogh

In the nyssr.net context, there are usually many more applications than in the normal server environment. This is because the applications and microservices are much smaller than in the Restful environment, due to the fact that a single server (called node in our context) can run hundreds or thousands of modules. To us, applications are always factory microservices that create and launch the application instances.

An application can consist of a single dialog, or very many. Starting an application is as fast as calling a microservice. Applications can also communicate freely with their owner and with other applications. Therefore, dialogs can just as easily be designed as applications that are instantiated, used, and terminated by one or more other applications.

In order for applications to be freely distributed in the nyssr.net network, it is advantageous to use a separate plugin for each application. You can find an example for the initialization of a plugin here.

An application consists of at least one target. How to create targets is described here. There are no further specifications.

package de.sillysky.nyssr.example.create.app;

import de.sillysky.nyssr.address.CNodeAddress;
import de.sillysky.nyssr.address.CTargetAddress;
import de.sillysky.nyssr.kernel.records.CRecordDismiss;
import de.sillysky.nyssr.message.CEnvelope;
import de.sillysky.nyssr.record.CRecord;
import de.sillysky.nyssr.target.CTarget;
import de.sillysky.nyssr.target.registry.records.CRecordStartTarget;
import de.sillysky.nyssr.util.properties.CStringProperties;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.UUID;

class CApp extends CTarget
{
    private final IDependencies mDependencies;
    private final UUID mInstanceId;
    private final CNodeAddress mRemoteSkinClientNode;
    private final CTargetAddress mRemoteSkinTarget;
    private final CStringProperties mProperties;

    CApp(@NotNull final IDependencies aDependencies,
         @NotNull final UUID aInstanceId,
         @Nullable final CNodeAddress aRemoteSkinClientNode,
         @Nullable final CTargetAddress aRemoteSkinTarget,
         @NotNull final CStringProperties aProperties)
    {
        mDependencies = aDependencies;
        mInstanceId = aInstanceId;
        mRemoteSkinClientNode = aRemoteSkinClientNode;
        mRemoteSkinTarget = aRemoteSkinTarget;
        mProperties = aProperties;

        addMessageHandler(CRecordStartTarget.ID,
                          this::asyncStartTarget);
        addMessageHandler(CRecordDismiss.ID,
                          this::asyncDismiss);
    }

    private boolean asyncStartTarget(@NotNull final CEnvelope aEnvelope,
                                     @NotNull final CRecord aRecord)
    {
        if (aEnvelope.isAnswer())
        {
            return false;
        }
        else
        {
            // TODO initialize app
            aEnvelope.setResultSuccess();
            return true;
        }
    }

    private boolean asyncDismiss(@NotNull final CEnvelope aEnvelope,
                                 @NotNull final CRecord aRecord)
    {
        if (aEnvelope.isAnswer())
        {
            return false;
        }
        else
        {
            deregisterTarget();
            aEnvelope.setResultSuccess();
            return true;
        }
    }
}

This is the skeleton for a target and thus also for an application. Note that the class package is private, i.e. it is invisible from outside the package. The class communicates exclusively via messages. Of course, a first message is CRecordStartTarget, which is used to start the target. As a second message a CRecordDismiss is implemented, with which the application is terminated.

As a best practice, we give the application an interface through which the class can fetch its dependencies. That is not part of the specification for applications, but it is helpful to use local services of the node in a highly asynchronous environment (see Package Initialization).

Furthermore, an instance ID is given, which we simply memorize. The other parameters are optional. The parameter aRemoteSkinClientNode is only required if this is a RemoteSkin Swing application. The aRemoteSkinTarget parameter is only required if this is a RemoteSkin web application.

We still need a factory to create the application. We create this factory in the same package, and it will also be package private.

package de.sillysky.nyssr.example.create.app;

import de.sillysky.nyssr.address.CNodeAddress;
import de.sillysky.nyssr.address.CTargetAddress;
import de.sillysky.nyssr.app.factory.collector.records.CRecordLaunchApplication;
import de.sillysky.nyssr.exception.CException;
import de.sillysky.nyssr.id.IId;
import de.sillysky.nyssr.impl.id.CIdFactory;
import de.sillysky.nyssr.message.CEnvelope;
import de.sillysky.nyssr.namespace.INamespace;
import de.sillysky.nyssr.namespace.INamespaceFactory;
import de.sillysky.nyssr.record.CRecord;
import de.sillysky.nyssr.service.IService;
import de.sillysky.nyssr.service.IServiceRegistry;
import de.sillysky.nyssr.target.CTarget;
import de.sillysky.nyssr.target.registry.records.CRecordStartTarget;
import de.sillysky.nyssr.util.properties.CStringProperties;
import org.jetbrains.annotations.NotNull;

import java.util.UUID;

class CAppFactory extends CTarget implements IService
{
    private static final UUID MY_APP_ID = UUID.fromString("3d34990f-ecd2-47d1-b621-2dfc786430fc");
    private final IDependencies mDependencies;

    CAppFactory(@NotNull final IDependencies aDependencies)
    {
        mDependencies = aDependencies;

        addMessageHandler(CRecordStartTarget.ID,
                          this::asyncStartTarget);
        addMessageHandler(CRecordLaunchApplication.ID,
                          this::asyncLaunchApplication);
    }

    @Override
    public void activate(@NotNull final IServiceRegistry aServiceRegistry) throws Exception
    {
        final INamespaceFactory namespaceFactory = mDependencies.getNamespaceFactory();
        final IId namespaceId = CIdFactory.random("MyAppName");
        final INamespace ns = namespaceFactory.createAndRegisterNamespace(namespaceId,
                                                                          "NS for application ...");
        ns.getTargetRegistry()
          .registerTarget(this);
    }

    @Override
    public void deactivate(@NotNull final IServiceRegistry aServiceRegistry) throws Exception
    {
        removeApplicationFactory();
        deregisterTarget();
    }

    private boolean asyncStartTarget(@NotNull final CEnvelope aEnvelope,
                                     @NotNull final CRecord aRecord)
    {
        if (aEnvelope.isAnswer())
        {
            return false;
        }
        else
        {
            registerApplicationFactory();
            aEnvelope.setResultSuccess();
            return true;
        }
    }

    private boolean asyncLaunchApplication(@NotNull final CEnvelope aEnvelope,
                                           @NotNull final CRecord aRecord) throws CException
    {
        if (aEnvelope.isAnswer())
        {
            return false;
        }
        else
        {
            final UUID applicationId = CRecordLaunchApplication.getApplicationId(aRecord,
                                                                                 null);
            if (!MY_APP_ID.equals(applicationId))
            {
                aEnvelope.setResult(34578,
                                    "Unsupported Application ID");
            }
            else
            {
                final CNodeAddress remoteSkinClientNode = CRecordLaunchApplication.getRemoteSkinClientNode(aRecord,
                                                                                                           null);
                final CTargetAddress remoteSkinTarget = CRecordLaunchApplication.getRemoteSkinTarget(aRecord,
                                                                                                     null);
                final CStringProperties properties = CRecordLaunchApplication.getProperties(aRecord,
                                                                                            new CStringProperties());
                final UUID instanceId = UUID.randomUUID();
                final CApp tgt = new CApp(mDependencies,
                                          instanceId,
                                          remoteSkinClientNode,
                                          remoteSkinTarget,
                                          properties);
                final CTargetAddress address = getTargetRegistry().registerTarget(tgt);
                CRecordLaunchApplication.setAppInstanceId(aRecord,
                                                          instanceId);
                CRecordLaunchApplication.setInstanceAddress(aRecord,
                                                            address);
            }
            return true;
        }
    }

    @NotNull
    private CRecord createApplicationRecord() throws CException
    {
        final CRecord record = CRecordApplication.create();
        CRecordApplication.setId(record,
                                 MY_APP_ID);
        CRecordApplication.setName(record,
                                   "MyApp");
        CRecordApplication.setShortDescription(record,
                                               "A sample application");
        CRecordApplication.setLongDescription(record,
                                              "A sample application");
        CRecordApplication.setIcon(record,
                                   "apps/examples/exampleApp.png");
        CRecordApplication.setPermissions(record,
                                          new String[]{});

        return record;
    }

    private void registerApplicationFactory() throws CException
    {
        final IId microserviceId = CIdFactory.fromObject("NY_ApplicationFactoryCollector");
        final CEnvelope env = CEnvelope.forMicroService(microserviceId);

        final CRecord record = CRecordAddAppFactory.create();
        CRecordAddAppFactory.setApplication(record,
                                            createApplicationRecord());
        CRecordAddAppFactory.setFactoryAddress(record,
                                               getAddress());

        sendRequest(env,
                    record);
    }

    private void removeApplicationFactory() throws CException
    {
        final IId microserviceId = CIdFactory.fromObject("NY_ApplicationFactoryCollector");
        final CEnvelope env = CEnvelope.forMicroService(microserviceId);

        final CRecord record = CRecordRemoveAppFactory.create();
        CRecordRemoveAppFactory.setApplicationId(record,
                                                 MY_APP_ID);
        CRecordRemoveAppFactory.setFactoryAddress(record,
                                                  getAddress());

        sendRequest(env,
                    record);
    }
}

The factory implements the interface IService to get the benefit of the activate() and deactivate() methods. The two methods are called by the package initializer. See also Package Initialization.

The application is not registered until the message handler for the CRecordStartTarget message, since the address of the target is known from the arrival of this message.

Before the target is deregistered in the deactivate() method, it deregisters the application.

The application is launched with the arrival of the CRecordLaunchApplication message. The message was forwarded from the application registry to our factory. The application itself is merely a target that is instantiated and registered with the target registry of a namespace. In the example we use the same namespace in which the factory was registered, but of course you can change that. It makes sense to have a separate namespace per application for larger applications. You can see an example of namespace creation in the activate() method.

For completeness, the IDependencies interface is still missing. This interface is also package private, and it should be given to all classes of the application so that they can access the required system services.

interface IDependencies
{
    INamespaceFactory getNamespaceFactory();
}

Now the package initializer is missing, where our dependencies are defined.

public class CPackageExampleCreateApp implements IServiceStarter, IDependencies
{
    private IService mService;

    private INamespaceFactory mNamespaceFactory;

    @Override
    public void getDependencies(@NotNull final IServiceDependencyList aDependencyList)
    {
        aDependencyList.add(INamespaceFactory.class);
    }

    @Override
    public void start(@NotNull final IServiceRegistry aServiceRegistry) throws Exception
    {
        if (mService == null)
        {
            mNamespaceFactory = aServiceRegistry.getServiceOrThrow(INamespaceFactory.class);

            mService = new CAppFactory(this);
            mService.activate(aServiceRegistry);
        }
    }

    @Override
    public void stop(@NotNull final IServiceRegistry aServiceRegistry) throws Exception
    {
        if (mService != null)
        {
            mService.deactivate(aServiceRegistry);
            mService = null;
        }
    }

    @Override
    public INamespaceFactory getNamespaceFactory()
    {
        return mNamespaceFactory;
    }
}

We also see the implementation of the IDependencies interface here. When the start() method is called, the system has ensured that the service INamespaceFactory already exists. For this to be the case, the getDependencies() method specifies this service.

The CPackageExampleCreateApp class is instantiated and made known to the service registry in the plugin initializer. See also Package Initialization.