2. Tutorials

2.1. Developing an EDG-AF app with Java servlets

In this tutorial you will:

  1. Create an EDG importer in Java using the EDG App Framework

  2. Integrate the importer into the EDG user interface

The code is based on the Java servlet example in examples/java/servlet.

The tutorial assumes you are familiar with Java, Java servlet programming, and Maven projects.

Create a Maven project for the EDG-AF app

Create a new Maven project with the usual structure (src/main/java, et al.) and a pom.xml. The example pom.xml can be used as a guide.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <artifactId>edg-af-example-java-servlet</artifactId>
    <groupId>org.topbraid</groupId>
    <packaging>war</packaging>
    <description>Example app using Java servlets</description>

    <!-- The example includes a parent for the convenience of sharing configuration. A real app should have its own parent or not declare one. -->
    <parent>
        <artifactId>edg-af-example</artifactId>
        <groupId>org.topbraid</groupId>
        <version>1.0.1-SNAPSHOT</version>
        <relativePath>../../pom.xml</relativePath>
    </parent>

    <build>
        <plugins>
            <!-- Include the maven-war-plugin so that the package goal produces a .war -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.3.1</version>
            </plugin>
            <!-- Include the jetty-maven-plugin in order to run the servlet directly from Maven -->
            <plugin>
                <groupId>org.eclipse.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>9.4.34.v20201102</version>
                <configuration>
                    <!-- The EDG server may be running on port 8080, so we use 8081. -->
                    <httpConnector>
                        <host>localhost</host>
                        <port>8081</port>
                    </httpConnector>
                    <webApp>
                        <!-- All of the example apps are mounted under /app-example/, so that the configuration URL is GET /app-example/configuration -->
                        <contextPath>/app-example</contextPath>
                    </webApp>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <!-- Use Mustache templates to pass query string parameters sent from EDG to the importer form. -->
        <dependency>
            <groupId>com.github.spullara.mustache.java</groupId>
            <artifactId>compiler</artifactId>
            <version>0.9.7</version>
        </dependency>
        <!-- The Java servlet API will be provided by the servlet container -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        <!-- The EDG client logs with SLF4J, so we should supply an implementation -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.30</version>
        </dependency>
        <!-- The EDG client lets us interact with the EDG server APIs via a Java facade -->
        <dependency>
            <groupId>org.topbraid</groupId>
            <artifactId>edg-client-java</artifactId>
        </dependency>
        <!-- The framework API project provides Java versions of the JSON objects expected by the framework, so we don't have to worry about serialization in Java -->
        <dependency>
            <groupId>org.topbraid</groupId>
            <artifactId>edg-af-api</artifactId>
            <version>1.0.1-SNAPSHOT</version>
        </dependency>
        <!-- A JSON-B implementation to take care of serializing the above. Jackson or another library that can serialize POJOs would also be fine. -->
        <dependency>
            <groupId>org.eclipse</groupId>
            <artifactId>yasson</artifactId>
            <version>1.0.0-RC1</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.json</artifactId>
            <version>1.1</version>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>tq-maven-releases-private-ro</id>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
            <url>https://nexus.topquadrant.net/repository/tq-maven-releases-private/</url>
        </repository>
        <repository>
            <id>tq-maven-snapshots-private-ro</id>
            <releases>
                <enabled>false</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
            <url>https://nexus.topquadrant.net/repository/tq-maven-snapshots-private/</url>
        </repository>
    </repositories>
</project>

The example uses the EDG-AF EDG-AF Maven artifacts.

Implement the servlet

The following section contains the complete example from example/java/servlet, with inline explanatory comments. This file would be put in the appropriate directory of your project’s src/main/java.

package org.topbraidlive.app.example.java.servlet;

import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import org.apache.jena.graph.Graph;
import org.apache.jena.graph.NodeFactory;
import org.apache.jena.graph.Triple;
import org.apache.jena.sparql.graph.GraphFactory;
import org.apache.jena.vocabulary.RDFS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.topbraidlive.client.java.Client;
import org.topbraidlive.client.java.ClientConfiguration;
import org.topbraidlive.client.java.UsernamePasswordClientCredentials;
import org.topbraidlive.client.java.api.type.Uri;
import org.topbraidlive.app.framework.api.configuration.AppConfiguration;
import org.topbraidlive.app.framework.api.configuration.ImporterConfiguration;

import javax.json.bind.JsonbBuilder;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Writer;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * Example app using Java servlets.
 * <p>
 * The app exposes a single importer with the id "example". The importer uses the EDG client to add fake data to the data graph on which it was invoked.
 * <p>
 * The servlet answers multiple URL paths, in a sequence corresponding to the app's installation and use of the importer.
 * <p>
 * When the app is installed:
 * 1. GET /configuration returns the app's configuration, which indicates that the app has a single data graph importer "example".
 * <p>
 * When the user goes to the data graph Import tab and clicks on the link for the example importer,
 * 1. GET /importer/example?projectGraph=x:urn-evn-master:some_data_graph...&serverBaseUrl=... receives query string parameters from the EDG server about the current data graph and returns an HTML form with the inputs to the importer. The query string parameters from the EDG server are embedded in the form as hidden inputs.
 * 2. POST /importer/example/action receives the form submission and performs the import.
 */
public final class ExampleAppServlet extends HttpServlet {
    private final static Logger logger = LoggerFactory.getLogger(ExampleAppServlet.class);
    private final Mustache importerFormHtmlMustache;
    private final Mustache importerResultHtmlMustache;

    public ExampleAppServlet() {
        // Load Mustache templates from the classpath (src/main/resources in the source tree).
        final MustacheFactory mustacheFactory = new DefaultMustacheFactory();
        importerFormHtmlMustache = mustacheFactory.compile("importer-form.html.mustache");
        importerResultHtmlMustache = mustacheFactory.compile("importer-result.html.mustache");
    }

    @Override
    protected final void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
        final String reqPathInfo = req.getPathInfo();
        if (reqPathInfo == null) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        switch (reqPathInfo) {
            case "/configuration":
                doGetConfiguration(req, resp);
                break;
            case "/importer/example":
                doGetExampleImporter(req, resp);
                break;
            default:
                logger.warn("not found: {}", reqPathInfo);
                resp.sendError(HttpServletResponse.SC_NOT_FOUND);
                return;
        }
    }

    /**
     * Get the configuration of this app as a JSON object.
     */
    private void doGetConfiguration(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
        // Create the configuration as a Java object
        final AppConfiguration configuration = AppConfiguration.builder()
                .setId("example")
                .setImporters(
                        Collections.singletonList(
                                ImporterConfiguration.builder()
                                        .setId("example")
                                        .setLabel("Example importer")
                                        .setComment("Imports some example data")
                                        // The importer should only work on data graphs.
                                        .setProjectTypes(Collections.singletonList("http://teamwork.topbraidlive.org/datagraph/datagraphprojects#ProjectType"))
                                        .build()
                        ))
                .setLabel("Example app")
                .build();
        // Then serialize it to a JSON object
        final String configurationJson = JsonbBuilder.create().toJson(configuration);
        logger.info("configuration: \n{}", configurationJson);
        resp.setContentType("application/json");
        try (final Writer respWriter = resp.getWriter()) {
            respWriter.write(configurationJson);
        }
    }

    /**
     * Get the HTML form for the "example" importer's inputs.
     */
    private void doGetExampleImporter(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
        // Pass the request query string parameters as-is to the Mustache template
        final Map<String, String> importerFormHtmlMustacheContext = new HashMap<>();
        for (final Map.Entry<String, String[]> entry : req.getParameterMap().entrySet()) {
            logger.info("get example importer HTML: query parameter from EDG: {} = {}", entry.getKey(), entry.getValue()[0]);
            importerFormHtmlMustacheContext.put(entry.getKey(), entry.getValue()[0]);
        }
        resp.setContentType("text/html");
        try (final Writer respWriter = resp.getWriter()) {
            importerFormHtmlMustache.execute(respWriter, importerFormHtmlMustacheContext);
        }
    }

    @Override
    protected final void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
        final String reqPathInfo = req.getPathInfo();
        if (reqPathInfo == null) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        switch (reqPathInfo) {
            case "/importer/example/action":
                doPostExampleImporterForm(req, resp);
                break;
            default:
                logger.warn("not found: {}", reqPathInfo);
                resp.sendError(HttpServletResponse.SC_NOT_FOUND);
                return;
        }
    }

    private void doPostExampleImporterForm(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
        for (final Map.Entry<String, String[]> entry : req.getParameterMap().entrySet()) {
            logger.info("post example importer form: input from user: {} = {}", entry.getKey(), entry.getValue()[0]);
        }

        // On the importer GET we embedded query parameters in the form HTML as hidden inputs in the Mustache template.
        // We get these back on the form submission POST.

        // Project graph URI e.g., urn:x-evn-master:test
        final String projectGraph = req.getParameter("projectGraph");
        if (projectGraph == null || projectGraph.isEmpty()) {
            logger.warn("form POST missing projectGraph");
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        // Base URL of the EDG server e.g., http://localhost:8080
        String serverBaseUrl = req.getParameter("serverBaseUrl");
        if (serverBaseUrl == null || serverBaseUrl.isEmpty()) {
            logger.warn("form POST missing serverBaseUrl");
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        // Instantiate an EDG client
        if (serverBaseUrl.endsWith("/tbl/")) {
            serverBaseUrl = serverBaseUrl.substring(0, serverBaseUrl.length() - "/tbl/".length());
        }
        final Client edgClient =
                Client.create(ClientConfiguration.builder()
                        .setCredentials(
                                UsernamePasswordClientCredentials.builder()
                                        .setPassword("password32")
                                        .setUsername("Admin_user").build()
                        )
                        .setServerBaseUrl(serverBaseUrl)
                        .build());

        // Call a method on the EDG client with some dummy data
        final Graph graph = GraphFactory.createDefaultGraph();
        graph.add(new Triple(NodeFactory.createURI(projectGraph), RDFS.comment.asNode(), NodeFactory.createLiteral("Successfully imported")));
        edgClient.addTriples(true, new Uri(projectGraph), graph.getPrefixMapping(), graph.find());

        // Use another Mustache template for the results of the form POST.
        final Map<String, String> importerResultHtmlMustacheContext = new HashMap<>();
        importerResultHtmlMustacheContext.put("projectGraph", projectGraph);
        importerResultHtmlMustacheContext.put("triplesCount", Integer.toString(graph.size()));
        resp.setContentType("text/html");
        try (final Writer respWriter = resp.getWriter()) {
            importerResultHtmlMustache.execute(respWriter, importerResultHtmlMustacheContext);
        }
    }
}

Set up the servlet in web.xml

A web.xml in src/main/webapp/WEB-INF maps URL paths to the servlet.

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
         http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <servlet>
        <servlet-name>ExampleAppServlet</servlet-name>
        <servlet-class>org.topbraidlive.app.example.java.servlet.ExampleAppServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>ExampleAppServlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

Run the web application

During development you can run the web application using Maven:

mvn org.eclipse.jetty:jetty-maven-plugin:run

To deploy the web application in production you will build a .war file with:

mvn package

and copy it to your servlet container’s webapps directory.

With the web application running you should be able to access its configuration at http://localhost:8081/app-example/configuration.

Install the app into EDG

The EDG App Framework provides a command line interface (CLI) to install apps into EDG-AF. The simplest way to run this command is by executing:

framework/script/cli

from the root of the EDG-AF repository. This will build the framework Maven projects, including an assembly .jar for the CLI.

The example app directories contain script/install scripts that invoke the framework CLI using a command line like:

framework/script/cli upload-zip --app-base-url http://localhost:8081/app-example --edg-base-url http://localhost:8080/ --edg-password password32 --edg-username Admin_user

See the Glossary for definitions of the command line options, such as app base URL.

The installation process creates an EDG project that defines a new importer for Data Graphs. This appears as a new section under the Import tab of a Data Graph asset collection.

Create a new Data Graph project for testing

In your web browser, navigate to the Data Graphs asset collection type. (You can click to enlarge the image.)

Then create a new Data Graph named test.

After creating the test Data Graph you will be redirected to the Import tab, as shown in the image below.

You will see the “Example importer” defined by the app as the first section in the tab.

Initiate the import

Click on the “Example importer” link under the Import tab. This opens up a new page which contains an <iframe> to the EDG-AF app’s URL http://localhost:8081/app-example/importer/example.

In a real importer this page would include an HTML form or similar mechanism for accepting inputs to the import process. The example has no visible inputs, only a submit button to initiate the import.

As noted in the servlet Java, the form contains hidden inputs such as the current projectGraph, which will be the target of the import.

Click on the submit button to initiate the import.

View the importer results

The example HTML form POSTs to an action URL on the EDG-AF app. The EDG App Framework dictates that the importer page /app-example/importer/example must be answered by the EDG-AF app. Beyond that there are no constraints on how the app works. Within the confines of the <iframe> the app can submit a form to any URL, use JavaScript, or communicate between the browser and the app using WebSockets. The latter is the standard approach to displaying progress indicators.

The results of the form POST are shown in the image.