43 Developing Polyglot Coherence Applications

This chapter demonstrates Coherence's polyglot capabilities by developing Java applications that populates a Coherence Cache with Java Objects and running Coherence using Oracle GraalVM Enterprise Edition. See https://www.oracle.com/graalvm.

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

This chapter includes the following sections:

Setting up the JavaScript Project

To demonstrate Coherence's polyglot capabilities, we need to develop a simple Java application that populates a Coherence Cache with Java objects and then use JavaScript to access and process those cache entries.

The JavaScript modules containing Coherence server side objects must be packaged and deployed to the storage enabled nodes. Let us first set up the necessary tools and project structure that will help us in developing and packaging the polyglot application.

This section includes the following topics:

Installing webpack and webpack-cli

To install webpack and webpack-cli, you need to have Node.js (at least 12.6) and node package manager (npm) installed.

  1. Create a directory for your project and change directory to this project directory.
    mkdir myapp
    cd myapp
    export PRJ_DIR=`pwd`
    This directory is referred to by the shell variable ${PRJ_DIR}.
  2. In the project directory, run:
    npm init -y
    Running this command will create a package.json file, which you will need to edit later.
  3. Install webpack and webpack-cli locally by running the command:
    npm install --save-dev webpack webpack-cli
    Installing the webpack and webpack-cli locally makes it easier to upgrade projects individually when breaking changes are introduced.
  4. Edit the package.json file and modify the scripts attribute. This allows you to run npm run build to build and package the application.
    {
      "name": "myapp",
      "version": "1.0.0",
      "private": true,
      "scripts": {
        "build": "./node_modules/.bin/webpack --config webpack.config.js --mode production"
      },
      "keywords": [],
      "author": "",
      "devDependencies": {
        "webpack": "^4.41.0",
        "webpack-cli": "^3.3.9"
        }
      }

Creating a webpack.config.js File

To create a webpack.config.js file:

  1. Open the ${PRJ_DIR} directory using the cd command.
    cd ${PRJ_DIR}
    
  2. Create a file called webpack.config.js and copy the following code:
    const path = require('path');
    module.exports = [
      {
        entry: './src/main.js',
        output: {
          path: path.resolve(__dirname, 'target/scripts/js'),
          filename: 'myapp.js',
          library: 'myapp',
          libraryTarget: 'commonjs2',
        }
      }
    ]
    This indicates that the newly created application has a single JavaScript module called main.js and the output module is a single module called myapp.js located under the target/scripts/js directory.

Implementing Application Classes

To implement application classes we need to develop server side JavaScript objects that can access and update the object entries in Coherence cache.

In the example to implement application classes, let us assume that the Cache is populated with Person objects with attributes such as firstName, lastName, age, and gender.

This section includes the following topics:

Using Filters

NamedCache 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.

Let us assume that you want to find all those Persons who are in their teens; in the age group of 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:

Navigate to ./src/main.js and edit the edit file to add the following content:

Note:

This file is mentioned in the webpack.config.js
export class IsTeen { // [1]
  evaluate(person) { // [2]
    return person.age >= 13 && person.age <= 19;
  }
}
In the above 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.

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 upper case, 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 cache 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 cache Entry).
  2. If the process(entry) method mutates the cache Entry 's value, then it must update the entry value explicitly.
Implementing an EntryProcessor

To write an EntryProcessor that updates lastName to its upper case value.

Edit the main.js 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 above example:
  • [1] defines and exports a class named UpperCaseProcessor .
  • [2] defines a method called process() that takes a single parameter. The NamedCache 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 upper case.
  • [4] updates the entry with the new value.
  • [5] returns the updated (uppercase) last name as the result of the processor execution.

Using ValueExtractors

A ValueExtractor is used to extract a value from an object. See Query Concepts.

This section includes the following topics:

Using a ValueExtractor Interface

The ValueExtractor interface defines a single method called extract(value) which takes one argument from which a value has to be extracted and returns the extracted value.

Writing a ValueExtractor

To define a ValueExtractor that extracts the age attribute from a Person object:

Change directory to ${PRJ_DIR}/src/main/js. Edit the main.js file and add the following content:

export class AgeExtractor { // [1]
  extract(value) { // [2]
    return value.age; // [3]
  }
}
In the above example:
  • [1] defines and exports a class named AgeExtractor.
  • [2] defines a method named extract that takes a single parameter.
  • [3] returns the value 's age attribute.

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 in this chapter. You can retrieve all entries and then find the Person with max age, but you will realize that we will be moving a lot data to the client. Coherence defines an Aggregator interface which allows you to 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 partial result of the parallel aggregation from each member.
  3. combine(partialResult): Combines partial result 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, edit 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 perviously 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;
  }
}

Installing and Using Dependencies

In real-time environments you may want to use the lodash module or the moment module for date and time manipulation. You can use the well known approach of using npm install to install your dependencies and use the require() function to consume your dependencies.

The following is an example that uses the lodash module:

const _ = require('lodash')
export class InitCaseProcessor {
  process(entry) {
    let value = entry.value;
    value.firstName = _.startCase(_.toLower(value.firstName));
    value.lastName = _.startCase(_.toLower(value.lastName));
    entry.value = value;
    return value.lastName;
  }
}

To install the lodash module, use the following command:

cd ${PRJ_DIR}/src
npm install lodash

Building a Multisource Project

In a complex project, you may want to split the classes into multiple modules (source files). If you have multiple source files, you need to instruct webpack about them so that it can transpile and bundle them into the output directory.

The following is a webpack.config.js file that specifies separate source files for filters and aggregators:

const path = require('path');
module.exports = [
  {
    entry: {
      filters: './src/filters.js',
      aggregators: './src/aggregators.js'
    }
  },
  output: {
    filename: '<name>.js',
    path: __dirname + '/dist'
    library: '<name>',
    libraryTarget: 'commonjs2',
  }
]

Note:

Replace the <name> entries in this example with the actual name of the js file.

Building and Packaging JavaScript Modules

You can build and package the JavaScript module(s) along with their dependencies.
Use the following command to build and package the JavaScript module(s):
cd ${PRJ_DIR}
npm run build

Whether you have a single source or a multi source project, with the webpack.config.js settings file described in Building a Multisource Project, the webpack will compile, transpile, and create one or more optimized JavaScript files in the target/scripts/js directory. The target/scripts/js directory contains not just your JavaScript modules but all its dependencies. In other words, your target/scripts/js directory is self sufficient.

Automatic Pre-loading of JavaScript Modules

When a storage enabled Coherence member is started, all JavaScript files that are placed under scripts/js directory (in the classpath), will be evaluated automatically and will be registered in an internal Map. The map keys are the exported names of the classes and the map values are the class objects. These names can be used in the script() APIs to execute the corresponding classes.

Note:

The parent directory of scripts/js must be in the classpath.

You can add either ${PRJ_DIR}/target to the classpath or create a jar file containing the scripts directory at the root of the jar file.

Setting the Classpath and Starting a Storage Enabled Server

To set the classpath and start a storage enabled server, run the following commands:

Note:

The parent directory of scripts/js should be in the classpath. Since the parent directory of scripts/js must be in the classpath for pre-loading to work, you need to start Coherence by running the following commands in a new terminal window.
CP=${COHERENCE_HOME}/lib/coherence.jar
CP=${PRJ_DIR}/target:/myapp.jar:${CP}
CP=${PRJ_DIR}/target:${CP}
java -cp ${CP} com.tangosol.net.DefaultCacheServer

where, ${COHERENCE_HOME} is the Coherence install directory. Note that you can run as many servers as you want, but for this example running one server will suffice.

Note:

Myapp.jar is from the myapp-client Java project. For instructions to create a Java project, see Using JavaScript Objects in Java Application. For this example to work, you must copy this file from myapp-client/target to myapp/target.

Using JavaScript Objects in Java Application

You need to complete creating the polyglot application by writing a Java class that uses the JavaScript objects.

This section includes the following topics:

Creating a Java Project

Prerequisites:

You need to have Graal VM and maven-3.5+ installed.

To create a Java project:

  • Open a new terminal window and create a new directory:
    mkdir myapp-client
    cd myapp-client
    export JAVA_PRJ_DIR=`pwd`
    cd ${JAVA_PRJ_DIR}
    mkdir -p src/main/java
  • Create a pom.xml and copy the following:
    <?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>
    
      <groupId>com.oracle.coherence.example</groupId>
      <artifactId>myapp</artifactId>
      <version>1.0.0-SNAPSHOT</version>
      
      <name>Coherence Graal Demo</name>
      
      <properties>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>
        
        <graal.version>19.1.1</graal.version>  
        <coherence.version>14.1.1-0-0</coherence.version>
      </properties>
      
      <dependencies>
        <dependency>
          <groupId>com.oracle.coherence</groupId>
          <artifactId>coherence</artifactId>
          <version>${coherence.version}</version>
        </dependency>
       </dependencies>
      
       <build>
        <plugins>
         <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.1</version>
          <configuration>
            <source>1.8</source>
            <target>1.8</target>
          </configuration>
         </plugin>
        </plugins>
       </build>
    
    </project>

Populating the Cache

You will now need to create a Java class that creates and populates a NamedCache with the Person objects described in Implementing Application Classes.

This section includes the following topics:

Defining the Person Java Class

To define a Person class that we will be used to populate cache:

cd ${JAVA_PRJ_DIR}/src/main/java
mkdir -p com/oracle/coherence/example

Create a file called Person.java under ${JAVA_PRJ_DIR}/src/main/java/com/oracle/coherence/example directory and paste the following content:

package com.oracle.coherence.example;

import java.io.Serializable;

public class Person
  implements Serializable
  {
  Person()
    {
    }

  Person(String firstName, String lastName, String gender, int age)
    {
    m_sFirstName = firstName;
    m_sLastName = lastName;
    m_sGender = gender;
    m_iAge = age;
    }

  public String getLastName()
    {
    return m_sLastName;
    }

  public void setLastName(String sLastName)
    {
    m_sLastName = sLastName;
    }

  public String getFirstName()
    {
    return m_sFirstName;
    }

  public void setFirstName(String sFirstName)
    {
    m_sFirstName = sFirstName;
    }

  public String getGender()
    {
    return m_sGender;
    }

  public void setGender(String sGender)
    {
    m_sFirstName = sGender;
    }

  public int getAge()
    {
    return m_iAge;
    }

  public void setAge(int iAge)
    {
    m_iAge = iAge;
    }

  // ----- Object methods --------------------------------------------------

  @Override
  public String toString()
    {
    return "Person{" +
            "first name='" + m_sFirstName + '\'' +
            "last name='" + m_sLastName + '\'' +
            ", age=" + m_iAge +
            ", gender='" + m_sGender +
            '}';
    }

  private String m_sFirstName;
  private String m_sLastName;
  private String m_sGender;
  private int    m_iAge;
  }
Populating the Coherence Cache

To populate the Coherence cache:

Create a file called MyAppClient.java under ${JAVA_PRJ_DIR}/src/main/java/com/oracle/coherence/example directory and add the following:

package com.oracle.coherence.example;

public class MyAppClient
  {
  public NamedCache<Integer, Person> getCache()
    {
    return CacheFactory.getTypedCache("DemoCache", TypeAssertion.withTypes(Integer.class, Person.class));
    }
  
  public void populateCache()
    {
    Map<Integer, Person> persons = new HashMap<>();
    persons.put(1, new Person("Ashley", "Jackson", "Female", 84));
    persons.put(2, new Person("John", "Campbell", "Male", 36));
    persons.put(3, new Person("Jeffry", "Trayton", "Male", 95));
    persons.put(4, new Person("Florence", "Campbell", "Female", 35));
    persons.put(5, new Person("Kevin", "Kelvin", "Male", 15));
    persons.put(5, new Person("Jane", "Doe", "Female", 17));
    getCache().putAll(persons);
    System.out.println("Populated cache with " + getCache().size() + " entries");
    }
Running the Application

To run the application, you will need to first start a Coherence server and populate the cache with a few Person objects using:

cd ${JAVA_PRJ_DIR}
mvn clean install
cp -rf ${PRJ_DIR}/target/scripts ${CLIENT_PRJ_DIR}/target

CP=${COHERENCE_HOME}/lib/coherence.jar
CP=${PRJ_DIR}/target/myapp-client.jar:${CP}
CP=${PRJ_DIR}/target:${CP}
java -Dstore.storageEnabled=false -cp ${CP} com.tangosol.net.DefaultCacheServer

Invoking Server Side JavaScript Objects from Java

You will now need to enhance the client application to access the server side JavaScript objects. See Running the Application to run the updated application.

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 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 (variable number of arguments) can be used to pass constructor arguments to the filter. For example, if your class defines a constructor, you could pass the arguments to be used.

Add the following to MyAppClient.java:

public void displayTeens()
    {
    getCache().getAll(Filters.script("js", "IsTeen")) // Use our "Teen Filter"
              .stream()
              .forEach(System.out::println);
    }
You can then make changes to the main method as follows:
public static void main(String[] args)
    {
    MyAppClient appClient = new MyAppClient();
    appClient.populateCache();
    appClient.displayTeens();
    }
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. It is defined as:

public static <K, V, R> EntryProcessor<K, V, R> script(String language,
                                                           String processorName,
                                                           Object... args);

Add the following to MyAppClient.java:

public void toInitCase()
     {
     // Make Updates using our "InitCaseProcessor"
     getCache().invokeAll(Processors.script("js", "UpperCaseProcessor"));

     // Display updated entries
     getCache().values()
               .forEach(System.out::println);
     }
Using ValueExtractor in a Filter

ValueExtractors are used to construct generic Filters. For example, the BetweenFilter takes a ValueExtractor and two values and checks if the extracted value should be between those two values.

public static <T, E> ValueExtractor<T, E> script(String language,
                                       String extractorName,
                                       Object... args);

Add the following to MyAppClient.java:

public void getPersonsInTeens()
    {
    // Construct a BetweenFilter with our AgeExtractor
    BetweenFilter filter = (BetweenFilter) Filters.between(
            Extractors.script("js", "AgeExtractor"), 13, 19);
    // Retrieve entries using our filter
    getCache().values(filter)
            .forEach(System.out::println);
    }
Running the Aggregator

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

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

Add the following to MyAppClient.java:

public void oldestPersons()
    {
    int oldestMaleKey = getCache().aggregate(Filters.equal("gender", "Male"),
            Aggregators.script("js", "OldestPerson"));
    int oldestFemaleKey = getCache().aggregate(Filters.equal("gender", "Female"),
            Aggregators.script("js", "OldestPerson"));

    System.out.println("Oldest Male : " + getCache().get(oldestMaleKey));
    System.out.println("Oldest Female: " + getCache().get(oldestFemaleKey));
    }

Here is the complete MyAppClient.java file:

package com.oracle.coherence.example;

import com.tangosol.net.NamedCache;
import com.tangosol.net.CacheFactory;
import com.tangosol.net.cache.TypeAssertion;
import com.tangosol.util.Aggregators;
import com.tangosol.util.Extractors;
import com.tangosol.util.Filters;
import com.tangosol.util.Processors;
import com.tangosol.util.filter.BetweenFilter;

import java.util.HashMap;
import java.util.Map;

public class MyAppClient
  {
  public NamedCache<Integer, Person> getCache()
    {
    return CacheFactory.getTypedCache("DemoCache", TypeAssertion.withTypes(Integer.class, Person.class));
    }
  
  public void populateCache()
    {
    Map<Integer, Person> persons = new HashMap<>();
    persons.put(1, new Person("Ashley", "Jackson", "Female", 84));
    persons.put(2, new Person("John", "Campbell", "Male", 36));
    persons.put(3, new Person("Jeffry", "Trayton", "Male", 95));
    persons.put(4, new Person("Florence", "Campbell", "Female", 35));
    persons.put(5, new Person("Kevin", "Kelvin", "Male", 15));
    persons.put(5, new Person("Jane", "Doe", "Female", 17));
    getCache().putAll(persons);
    System.out.println("Populated cache with " + getCache().size() + " entries");
    }

  public void displayTeens()
    {
    getCache().values(Filters.script("js", "IsTeen")) // Use our "Teen Filter"
            .stream()
            .forEach(System.out::println);
    }

  public void getPersonsInTeens()
    {
    // Construct a BetweenFilter with our AgeExtractor
    BetweenFilter filter = (BetweenFilter) Filters.between(
            Extractors.script("js", "AgeExtractor"), 13, 19);
    // Retrieve entries using our filter
    getCache().values(filter)
            .forEach(System.out::println);
    }

  public void oldestPersons()
    {
    int oldestMaleKey = getCache().aggregate(Filters.equal("gender", "Male"),
            Aggregators.script("js", "OldestPerson"));
    int oldestFemaleKey = getCache().aggregate(Filters.equal("gender", "Female"),
            Aggregators.script("js", "OldestPerson"));

    System.out.println("Oldest Male : " + getCache().get(oldestMaleKey));
    System.out.println("Oldest Female: " + getCache().get(oldestFemaleKey));
    }

  public void toInitCase()
    {
    // Make Updates using our "InitCaseProcessor"
    getCache().invokeAll(Processors.script("js", "UpperCaseProcessor"));

    // Display updated entries
    getCache().values()
            .forEach(System.out::println);
    }

  // ----- data members ----------------------------------------------------

  public static void main(String[] args)
    {
    MyAppClient appClient = new MyAppClient();
    appClient.populateCache();
    System.out.println("------------ Display Teens    -------------------");
    appClient.displayTeens();
    System.out.println("------------ Get Teens        -------------------");
    appClient.getPersonsInTeens();
    System.out.println("------------ To Upper Case    -------------------");
    appClient.toInitCase();
    System.out.println("------------ Oldest Persons   -------------------");
    appClient.oldestPersons();
    }
  }