Note

You are not reading the most recent version of this documentation. 1.4.7 is the latest version available.

Java API

Installing

The Popili API for Java is distributed both as a Maven artifact, and as an Eclipse plugin. The API includes all of the features of Popili, including the ability to load and typecheck programs, to actually executing the verification and generating code.

The following section shows how to build a simple example program that loads a Coco file and verifies if the file is correct. In addition to this, there is also documentation available on general advice on using the API.

Installation via Maven

For Maven, add the following dependency to your project:

<dependency>
  <groupId>io.cocotec.popili</groupId>
  <artifactId>popili-api</artifactId>
  <version>1.5.0</version>
</dependency>

where you replace version by the Popili version you want to use. You will also need to add a custom repository to your pom.xml file:

<repository>
  <id>cocotec</id>
  <name>Cocotec Repository</name>
  <url>https://dl.cocotec.io/maven2/</url>
</repository>

The popili-api artifact includes the core Popili API, but the Coco EMF API is distributed as a separate artifact. To use this, also add a dependency on the popili-emf artifact:

<dependency>
  <groupId>io.cocotec.popili</groupId>
  <artifactId>popili-emf</artifactId>
  <version>1.5.0</version>
</dependency>

Once these dependencies have been added, you need to create an instance of the Popili APIClient class. This is the main object that provides access to Popili’s features. To do this with the Maven-distributed artifacts, you firstly have to extract the bundled servers to a directory where they can be run. This directory can be a temporary directory, as shown below:

import io.cocotec.popili.APIClient;
import io.cocotec.popili.BundledApiServer;
import io.cocotec.popili.utility.Version;

Path serverFolder = Files.createTempDirectory(prefix);
APIClient client = new APIClient();
client.startServer(BundledApiServer.extract(serverFolder, Version.getRevision()));

If using a temporary directory, then we recommend you delete the serverFolder after calling client.stopServer() to ensure that you do not end up with many copies of the server extracted. Alternatively, you can extract the server to a fixed location and reuse it: the second parameter to BundledAPIServer.extract() is a token that will be used to determine if the server should be re-extracted. Using Version.getRevision() as above will ensure that the server is re-extracted if the version of Popili changes.

At this point continue with the Getting Started section below.

Installation via Eclipse

For Eclipse, add a dependency on the io.cocotec.popili plugin. Once this is done, you then need to create an APIClient and to start the server. The server runs in the background locally on the user’s machine and handles requests.

APIClient client = new APIClient();
client.startServer(Activator.getDefault().getPlatformServerFile().toString());

At this point continue with the Getting Started section below.

Getting Started

After the APIClient has been created, it is now possible to start using the API on Coco files. However, before doing this, it is best to check whether the user has a valid license and to terminate gracefully if not:

UserInfo userInfo = new UserInfo(client);
LicenseResponse licenseResponse = userInfo.hasLicense(Arrays.asList(ProductFeature.PopiliBase));
if (!licenseResponse.getLicensed()) {
  System.out.println("Could not acquire license: " + licenseResponse.getNonLicensedReason());
  return;
}

To load Coco files, we create a StandaloneContext and add various loggers to it that print to the Console (see the downloadable example for implementations of these classes):

StandaloneContext context = new StandaloneContext(client);
CocoDiagnosticLogger diagnosticLogger = new CocoDiagnosticLogger(client);
VerificationLogger verificationLogger = new VerificationLogger(client);
context.addDiagnosticsConsumer(diagnosticLogger);
context.addVerificationProgressListener(verificationLogger);

In order to load the file, we have to add the folder that contains the file as an import path. For the purposes of this example, we assume that there is a file called TestFile.coco stored in the current working directory:

context.addImportPath(System.getProperty("user.dir"));
Node moduleNode = context.loadModule("TestFile.coco", ModulePathType.RelativeFilePath);
if (moduleNode == null) {
  System.out.println("Could not load TestFile.coco");
  return;
}

Assuming that the load took place successfully, we can then execute all assertions as follows:

context.rootAssertion().run();

This will print logging information to the console whilst the checks are executing.

The complete example is appended below:

// Copyright 2024 Cocotec Limited. All rights reserved.
// Unauthorized copying of this file, via any medium is strictly prohibited.
// Proprietary and confidential.
import io.cocotec.popili.coco.Diagnostic;
import io.cocotec.popili.coco.DiagnosticConsumer;
import io.cocotec.popili.coco.LicenseResponse;
import io.cocotec.popili.coco.ModulePathType;
import io.cocotec.popili.coco.Node;
import io.cocotec.popili.coco.StandaloneContext;
import io.cocotec.popili.coco.UserInfo;
import io.cocotec.popili.coco.VerificationAssertion;
import io.cocotec.popili.coco.VerificationProgressListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;

class Example {
  public static void Main(String[] args) throws IOException {
    APIClient client = new APIClient();
    client.startServer(Activator.getDefault().getPlatformServerFile().toString());

    UserInfo userInfo = new UserInfo(client);
    LicenseResponse licenseResponse =
        userInfo.hasLicense(new ArrayList<>(Arrays.asList(ProductFeature.PopiliBase)));
    if (!licenseResponse.getLicensed()) {
      System.out.println("Could not acquire license: " + licenseResponse.getNonLicensedReason());
      return;
    }

    StandaloneContext context = new StandaloneContext(client);
    CocoDiagnosticLogger diagnosticLogger = new CocoDiagnosticLogger(client);
    VerificationLogger verificationLogger = new VerificationLogger(client);
    context.addDiagnosticsConsumer(diagnosticLogger);
    context.addVerificationProgressListener(verificationLogger);

    context.addImportPath(System.getProperty("user.dir"));
    Node moduleNode = context.loadModule("TestFile.coco", ModulePathType.RelativeFilePath);
    if (moduleNode == null) {
      System.out.println("Could not load TestFile.coco");
      return;
    }

    context.rootAssertion().run();
  }

  public static class CocoDiagnosticLogger extends DiagnosticConsumer {

    public CocoDiagnosticLogger(APIClient client) {
      super(client);
    }

    @Override
    public void handleDiagnostic(Diagnostic diagnostic) {
      System.out.println(diagnostic.getMessage());
    }
  }

  public static class VerificationLogger extends VerificationProgressListener {

    public VerificationLogger(APIClient client) {
      super(client);
    }

    private void log(VerificationAssertion assertion, String message) {
      System.out.println(assertion.name() + ": " + assertion.description());
    }

    @Override
    public void verificationStarted(VerificationAssertion assertion) {
      log(assertion, "started");
    }

    @Override
    public void verificationLogMessage(VerificationAssertion assertion, String message) {
      log(assertion, message);
    }

    @Override
    public void verificationStatusUpdated(VerificationAssertion assertion) {}

    @Override
    public void verificationStatisticsUpdated(VerificationAssertion assertion) {
      log(assertion, "statistics updated");
    }

    @Override
    public void verificationFinished(VerificationAssertion assertion) {
      log(assertion, "finished");
    }

    @Override
    public void verificationFinishedAll() {}
  }
}

EMF

Popili for Eclipse API also provides an integration with the Eclipse Modelling Framework. Specifically, it provides:

  • An Ecore metamodel for Coco, allowing standard EMF-based tools to create Coco programmatically.
  • An API that allows an EMF resource using the Coco Ecore model to be loaded into a StandaloneContext, and then, for example, converted into Coco’s textual representation. This effectively allows other languages to be imported into Coco.
  • An API that allows the Coco files to be converted into the EMF representation. This allows Coco to be exported into other languages, or for Coco-to-Coco transformations to be written.

Together, these APIs permit bidirectional transformations, either allowing Coco to be converted into the EMF form, or vice-versa. To use these APIs, create an EMFContext instead of a StandaloneContext. For example, to load a Coco file using the EMFContext.toResource method:

EMFContext context = new EMFContext(client);
Node cocoNativeModule = ...;
ModuleDecl emfModule = context.toResource(cocoNativeModule);

emfModule is an EObject and can be passed to standard EMF tools. The EMF structure can also be viewed directly via Eclipse rather than via the API, by right-clicking on any .coco file within the Project Explorer in Eclipse, and selecting Open With → Other and then selecting Sample Reflective Ecore Model Editor from the list. This will require the EMF SDK to be installed in Eclipse.

The API can also be used to import an EMF-created module into Coco’s native form, as follows:

ModuleDecl emfModule = ...;
EMFContext context = new EMFContext(client);
Node cocoNativeModule = context.loadModule(emfModule);
String cocoText = cocoNativeModule.prettyPrint();

This will load the module into the context, which will report diagnostics via the context’s DiagnosticConsumer, and then pretty-print the text. This could then be saved to a file, for example, effectively allowing textual Coco models to be created from another EMF-based language.