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.