48 Developing Polyglot Coherence Applications

This chapter demonstrates Coherence's polyglot capabilities by developing an application that uses server-side JavaScript to implement common Coherence server-side operations.

Using this functionality requires that you run Oracle Coherence with Oracle GraalVM Enterprise Edition. See Running Oracle WebLogic Server and Coherence on GraalVM Enterprise Edition.

Note:

To build and run the examples in this chapter, you must have Maven 3.8.6 or later installed.

Note:

The GraalVM Polyglot Runtime does not currently support virtual threads, so make sure that virtual threads are not enabled for the cache service for which you want to use polyglot features.

This chapter includes the following sections:

Setting Up the Project

To demonstrate Coherence's polyglot capabilities, you will create a simple application that populates a Coherence map and then you'll use JavaScript to access and process the map entries you created.

If you already have a Maven Java project for your server-side classes that will be deployed to all Coherence storage members, then you can simply create a src/main/js directory and create your JavaScript project within it.

Otherwise, first you must create a Maven project and then create your JavaScript project under the src/main/js directory:
  1. Change to your Maven project directory.
    cd coherence-js-app
    export PRJ_DIR=`pwd`
    mkdir -p src/main/js
    cd src/main/js
    export JS_DIR=`pwd`

    This main project directory now can be referred to by the shell environment variable ${PRJ_DIR}, and the JavaScript project directory can be referred to by using the ${JS_DIR} environment variable.

  2. In the JavaScript project directory, run:
    npm init -y

    Running this command will create a package.json file, which you will need to edit later.

  3. Create aggregator module, main.mjs, that will be used to export all of the JavaScript classes defined in other modules.
    echo > main.mjs
  4. Modify the main attribute within package.json to point to the newly created main.mjs module.
    {
      "name": "coherence-js-app",
      "version": "1.0.0",
      "main": "main.mjs",
      …
    }

Implementing JavaScript Classes

Now, you will implement JavaScript classes that you can use to access and update the entries in a Coherence map.
For simplicity, assume that the map is populated with Person objects with attributes, such as firstName, lastName, age, and gender.
public class Person
    {
    private String firstName;
    private String lastName;
    private int age;
    private String gender;

    // constructors and accessors omitted for brevity
    }

Now, you can implement JavaScript classes that will allow you to query, aggregate, and update.

This section includes the following topics:

Using Filters

NamedMap provides the get() and put() methods that allow you to access entries based on the entries' keys. However, in many cases you may want to retrieve entries based on attributes other than the key. Coherence defines a Filter interface for these situations.

Assume that you want to find all those persons who are in their teens, in the age group 13-19. One way to implement this is to retrieve all entries and pick only those whose age is between 13 and 19, but this method is inefficient. Coherence helps to move the predicate to the storage nodes, which not only avoids huge data movement, but also enables parallel execution of the predicate on the storage nodes.

Coherence provides a number of built in filters to access entries on the storage nodes. See Querying Data in a Cache.

This section includes the following topics:

Using a Filter Interface

The Filter interface defines a single method evaluate(object), which takes an object to evaluate as an argument, and returns true if the specified object satisfies the criteria defined by the filter, or false if the specified object does not satisfy the criteria defined by the filter.

Your filter must adhere to the following:
  1. Must define and export a class that has one method, named evaluate, that takes one argument.
  2. The evaluate(obj) must return true if obj satisfies the criteria represented by the filter. Otherwise, it should return false .
Implementing a Filter

To write a filter that checks if the person is in his or her teens, do the following.

Change directory to ${JS_DIR}/src/main/js. Create the filters.mjs file and add the following content:

export class IsTeen { // [1]
  evaluate(person) {  // [2]
    return person.age >= 13 && person.age <= 19;
  }
}
In the preceding example:
  • [1] Defines and exports a class named IsTeen .
  • [2] Defines a method named evaluate that takes a single parameter. This method checks if the age attribute is between 13 and 19.

Finally, re-export all classes defined in the filters.mjs module from the aggregator module, main.mjs:

export * from "./filters.mjs"

Using EntryProcessors

Coherence provides built in EntryProcessors which help to perform parallel updates to cache entries. See Overview of Entry Processor Agents.

If you want to update cache entries (that are filled with Person objects as per the example used in this chapter) such that all lastName attributes are in uppercase, one way to do this is to retrieve all entries, iterate over them and update them one by one, and finally write them back into the cache.

This is an inefficient method. Coherence provides a more efficient approach of shipping the processing logic where the data resides and thus eliminating the need for data movement. If you need to perform parallel updates to cache entries efficiently, you should use an EntryProcessor.

This section includes the following topics:

Using an EntryProcessor Interface
An EntryProcessor defines a mandatory method called process(entry), which takes a map entry to process as an argument and returns the result of the processing. To implement an EntryProcessor in your JavaScript, the class must:
  1. Define an exported class with one method named process that takes one argument (which will be the map entry).
  2. If the process(entry) method mutates the map entry 's value, then it must update the entry explicitly.
Implementing an EntryProcessor

To write an EntryProcessor that updates lastName to its uppercase value, create the processors.mjs file and add the following content:

export class UpperCaseProcessor { // [1]
    process(entry) { // [2]
      let person = entry.value;
      person.lastName = person.lastName.toUpperCase(); // [3]
      entry.value = person; // [4]
      return person.lastName; // [5]
    }
}
In the preceding example:
  • [1] Defines and exports a class named UpperCaseProcessor .
  • [2] Defines a method called process() that takes a single parameter. The NamedMap entry that needs to be processed is accessible through the entry argument that is passed to this method.
  • [3] Converts the person's lastName to uppercase.
  • [4] Updates the entry with the new value.
  • [5] Returns the updated (uppercase) last name as the result of the processor execution.
Finally, re-export all classes defined in the processors.mjs module from the aggregator module, main.mjs:
export * from "./ processors.mjs"

Using Aggregators

You can use a Coherence Aggregator to retrive a single aggregated result from a cache, based on certain criteria. For example, retrieving the key of the oldest Person in the Cache you created in the previous examples. You can retrieve all entries and then find the Person with the maximum age, but you will be moving a lot data to the client.

Coherence defines an Aggregator interface which lets you compute partial results and then combine those partial results to get a single aggregated result. See Performing Data Grid Aggregation.

This section includes the following topics:

Using an Aggregator Interface

The Aggregator interface requires you to implement the following methods:

  1. accumulate(entry): Executes in parallel across all members and accumulates a single entry into the partial result for that member. In this method, the partial result is computed by using one or more attributes from entry. This method will be called multiple times on an Aggregator, once for each entry that was selected for aggregation on a given cluster member.
  2. getPartialResult(): Returns the partial result of the parallel aggregation from each member.
  3. combine(partialResult): Combines partial results returned from each cluster member into the final result. In this method, the partial result is computed by using one or more attributes from entry. This method will be called multiple times on the root Aggregator instance, once for each cluster member's partial result.
  4. finalizeResult(): Calculates and returns the final result of the aggregation.
Writing an Aggregator

To define an Aggregator that returns the key of the oldest Person, create the main.js file and add the following content:

export class OldestPerson {
  constructor() {
    this.key = -1;
    this.age = 0;
  }

  accumulate(entry) {
    // Compare this entry's age with the result computed so far.    
    if (entry.value.age > this.age) {
      this.key = entry.key;
      this.age = entry.value.age;
    }
    return true;
  }

  getPartialResult() {
    // Return the partial result accumulated / computed so far.
    return JSON.stringify({key:this.key, age:this.age});
  }

  combine(partialResult) {
    // Compute a (possibly) new result from the previously computed
    // partial result.
    let p = JSON.parse(partialResult);

    if (p.age > this.age) {
      this.key = p.key;
      this.age = p.age;
    }
    return true;
  }
  
  finalizeResult() {
    // Return the final computed result.
    return this.key;
  }
}

Finally, re-export the classes defined from the Aggregator module, main.mjs:

export * from "./aggregators.mjs"

Installing and Using Dependencies

In this section, you'll see how you can use third-party dependencies when implementing your JavaScript classes, which often may be the reason to use JavaScript in the first place.

You can use the standard approach of using npm install to install your dependencies and use the import statement function to consume them. For example, to install the lodash-es and @paravano/utils packages, use the following commands:

cd ${JS_DIR}
npm install -s lodash-es
npm install -s @paravano/utils

After the dependencies are installed, you can implement an entry processor that uses the now function from the lodash-es package, and camel function from the @paravano/utils package:

import {now} from 'lodash-es';
import {camel} from "@paravano/utils";

export class CamelCase {
  process(entry) {
    console.log(`> CamelCase: entry=${entry} time=${now()}`)
    entry.value = camel(entry.value);
    return entry.value;
  }
}

Building a Project

To make your JavaScript classes available to Coherence Polyglot Framework at runtime, you need to package them in a JAR file.

Coherence Polyglot Framework will automatically load all scripts with an .mjs extension from any scripts/js directory in the class path, so in the simplest scenario, when you have no external dependencies, you can simply configure your Maven project to copy all of your source files to the correct location within the target directory, which will ensure that they are packaged into the final JAR along with all other classes and resources:

<build>
  <resources>
    <resource>
      <directory>${project.basedir}/src/main/resources</directory>
    </resource>
    <resource>
      <directory>${project.basedir}/src/main/js</directory>
      <targetPath>${project.build.outputDirectory}/scripts/js</targetPath>
      <includes>
        <include>**/*.mjs</include>
      </includes>
    </resource>
  </resources>
</build>

Note that if you do this, then you also need to redefine the default resource, so all the files from src/main/resources get copied to the output directory as well, like they typically would.

However, when you have external dependencies, and often you likely will, you need to use a bundler to bundle both your source code and any code from the external dependencies it depends on, into the output directory.

The bundler we recommend for this task is esbuild, because it is fast and very simple to use. So now, you'll install it and use it to bundle your JavaScript project into the output directory:

  1. Install esbuild.
    npm install –-save-dev esbuild
  2. Configure the build script within package.json to run esbuild against your main module.
    {
      "name": "coherence-js-app",
      "version": "1.0.0",
      "main": "main.mjs",
      "scripts": {
        "build": "esbuild main.mjs --bundle --format=esm --charset=utf8 --outdir=../../../target/classes/scripts/js/ --out-extension:.js=.mjs"
      },
      "devDependencies": {
        "esbuild": "^0.24.0"
      }
    }
Finally, when using a bundler and third-party dependencies, you need to configure an excellent Frontend Maven Plugin to:
  1. Install Node.js and npm locally.
  2. Install dependencies by running npm install.
  3. Bundle your scripts by running esbuild using the npm run build command, which was defined previously.
This all can be easily accomplished by adding the following plug-in configuration to the pom.xml file:
<plugin>
  <groupId>com.github.eirslett</groupId>
  <artifactId>frontend-maven-plugin</artifactId>
  <executions>
    <execution>
      <id>install node and npm</id>
      <goals>
        <goal>install-node-and-npm</goal>
      </goals>
    </execution>
    <execution>
      <id>npm install</id>
      <goals>
        <goal>npm</goal>
      </goals>
      <configuration>
        <arguments>install</arguments>
      </configuration>
    </execution>
    <execution>
      <id>npm run build</id>
      <goals>
        <goal>npm</goal>
      </goals>
      <configuration>
        <arguments>run build</arguments>
      </configuration>
    </execution>
  </executions>
  <configuration>
    <nodeVersion>v20.17.0</nodeVersion>
    <installDirectory>target</installDirectory>
    <workingDirectory>src/main/js</workingDirectory>
  </configuration>
</plugin>

If you now run mvn install in your project directory, you should see that Node.js and npm are installed, and that your scripts and their dependencies are bundled, using esbuild, into a single target/classes/scripts/js/main.mjs file.

Now, you are ready to consume classes defined in, and exported from, that module in your Java application.

Using JavaScript Classes in a Java Application

In this section, you will complete the polyglot example by writing a Java application that uses the JavaScript classes that you created previously.

This section includes the following topics:

Adding Runtime Dependencies

To use the JavaScript classes that you created previously from your Java application, you need to add dependencies on Coherence itself, and on the GraalVM Polyglot Runtime, and JavaScript language implementation to the pom.xml file:
<dependencies>
  <dependency>
    <groupId>com.oracle.coherence</groupId>
    <artifactId>coherence</artifactId>
    <version14.1.2-0-0</version>
  </dependency>

  <!-- GraalVM Polyglot support -->
  <dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>polyglot</artifactId>
    <version>23.1.4</version>
  </dependency>
  <dependency>
    <groupId>org.graalvm.js</groupId>
    <artifactId>js-language</artifactId>
    <version>23.1.4</version>
  </dependency>
</dependencies>

Implementing the Java Application

You are now ready to implement the Java application that will start the Coherence cluster, populate the people map with a few entries, and use the JavaScript classes you created previously to query and modify entries in the people map.

Create a file called Main.java within the main project containing the following code:

package com.oracle.coherence.example.js;

import com.tangosol.net.Coherence;
import com.tangosol.net.NamedMap;

import com.tangosol.util.Aggregators;
import com.tangosol.util.Filters;
import com.tangosol.util.Processors;

import com.tangosol.util.filter.AlwaysFilter;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;

public class Main
    {
    public static void main(String[] args)
        {
        System.setProperty("coherence.log.level", "1");
        
        try (Coherence coherence = Coherence.clusterMember().start().join())
            {
            NamedMap<Integer, Person> people = coherence.getSession().getMap("people");
            populatePeople(people);

            displayAllTeens(people);
            convertLastNameToUppercase(people);
            displayOldestManAndWoman(people);
            }
        }

    private static void populatePeople(NamedMap<Integer, Person> people)
        {
        printHeader("populatePeople");
        
        Map<Integer, Person> map = new HashMap<>();
        map.put(1, new Person("Ashley", "Jackson", "F", 84));
        map.put(2, new Person("John", "Campbell", "M", 36));
        map.put(3, new Person("Jeffry", "Trayton", "M", 95));
        map.put(4, new Person("Florence", "Campbell", "F", 35));
        map.put(5, new Person("Kevin", "Kelvin", "M", 15));
        map.put(6, new Person("Jane", "Doe", "F", 17));

        people.putAll(map);
        print(people);
        }

    private static void displayAllTeens(NamedMap<Integer, Person> people)
        {
        }

    private static void convertLastNameToUppercase(NamedMap<Integer, Person> people)
        {
        }

    private static void displayOldestManAndWoman(NamedMap<Integer, Person> people)
        {
        }

    private static void printHeader(String header)
        {
        System.out.println();
        System.out.println(header);
        System.out.println("-".repeat(header.length()));
        }

    private static void print(NamedMap<Integer, Person> map)
        {
        print(map.entrySet(AlwaysFilter.INSTANCE));
        }

    private static void print(Set<Map.Entry<Integer, Person>> entrySet)
        {
        TreeMap<Integer, Person> map = new TreeMap<>();
        for (Map.Entry<Integer, Person> e : entrySet)
            {
            map.put(e.getKey(), e.getValue());
            }
        for (Map.Entry<Integer, Person> e : map.entrySet())
            {
            System.out.printf("%d: %s%n", e.getKey(), e.getValue());
            }
        }
    }

Invoking JavaScript Objects from Java

As the last remaining step, you must implement the previous three methods with the empty bodies, which will use JavaScript classes that you implemented earlier.

This section includes the following topics:

Invoking JavaScript Filters

A Filter written in JavaScript can be instantiated by calling the script() method in the com.tangosol.util.Filters utility class. It is defined as:

public static <V> Filter<V> script(String language,
                                   String filterName,
                                   Object... args);

The first argument is used to specify the language in which the script is implemented. For JavaScript, use js. The second argument is used to specify the exported name of the Filter, and the last, optional argument, can be used to pass constructor arguments to the filter, in case it has any.

Add the following method to Main.java:

private static void displayAllTeens(NamedMap<Integer, Person> people)
    {
    printHeader("displayAllTeens");

    print(people.entrySet(Filters.script("js", "IsTeen")));
    }
Making Parallel Updates Using JavaScript EntryProcessor

An EntryProcessor written in JavaScript can be instantiated by calling the script() method in the com.tangosol.util.Processors class.

Add the following methods to Main.java:

private static void convertLastNameToUppercase(
                                 NamedMap<Integer, Person> people)
    {
    printHeader("convertLastNameToUppercase");

    people.invokeAll(Processors.script("js", "UpperCaseProcessor"));
    print(people);
    }
Running the JavaScript Aggregator

An Aggregator written in JavaScript can be instantiated by calling the script() method in the com.tangosol.util.Aggregators class.

Use the OldestPerson aggregator, along with the built in filters, to find the oldest male and female persons.

Add the following to Main.java:

private static void displayOldestManAndWoman(NamedMap<Integer, Person> people)
    {
    printHeader("displayOldestManAndWoman");

    Integer oldestManId   = people.aggregate(
                                  Filters.equal(Person::getGender, "M"),
                                  Aggregators.script("js", "OldestPerson"));
    Integer oldestWomanId = people.aggregate(
                                  Filters.equal(Person::getGender, "F"),
                                  Aggregators.script("js", "OldestPerson"));

    System.out.printf("%d: %s%n", oldestManId, people.get(oldestManId));
    System.out.printf("%d: %s%n", oldestWomanId, people.get(oldestWomanId));
    }

Now, you can run the Java application, and if everything is implemented correctly, see the following output:

Oracle Coherence Version 14.1.2.0.0 (dev-aseovic) Build 0
 Grid Edition: Development mode
Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved.


populatePeople
--------------
1: Person[lastName=Jackson, firstName=Ashley, age=84, gender=F]
2: Person[lastName=Campbell, firstName=John, age=36, gender=M]
3: Person[lastName=Trayton, firstName=Jeffry, age=95, gender=M]
4: Person[lastName=Campbell, firstName=Florence, age=35, gender=F]
5: Person[lastName=Kelvin, firstName=Kevin, age=15, gender=M]
6: Person[lastName=Doe, firstName=Jane, age=17, gender=F]

displayAllTeens
---------------
5: Person[lastName=Kelvin, firstName=Kevin, age=15, gender=M]
6: Person[lastName=Doe, firstName=Jane, age=17, gender=F]

convertLastNameToUppercase
--------------------------
1: Person[lastName=JACKSON, firstName=Ashley, age=84, gender=F]
2: Person[lastName=CAMPBELL, firstName=John, age=36, gender=M]
3: Person[lastName=TRAYTON, firstName=Jeffry, age=95, gender=M]
4: Person[lastName=CAMPBELL, firstName=Florence, age=35, gender=F]
5: Person[lastName=KELVIN, firstName=Kevin, age=15, gender=M]
6: Person[lastName=DOE, firstName=Jane, age=17, gender=F]

displayOldestManAndWoman
------------------------
3: Person[lastName=TRAYTON, firstName=Jeffry, age=95, gender=M]
1: Person[lastName=JACKSON, firstName=Ashley, age=84, gender=F]

If you run into any issues, the example project for this chapter will be available on GitHub.