43 Developing Polyglot Coherence Applications
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. - 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. - Installing and Using Dependencies
In real-time environments you may want to use thelodashmodule or themomentmodule for date and time manipulation. - 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 instructwebpackabout them so that it can transpile and bundle them into the output directory. - Building and Packaging JavaScript Modules
You can build and package the JavaScript module(s) along with their dependencies. - Automatic Pre-loading of JavaScript Modules
When a storage enabled Coherence member is started, all JavaScript files that are placed underscripts/jsdirectory (in the classpath), will be evaluated automatically and will be registered in an internal Map. - Setting the Classpath and Starting a Storage Enabled Server
To set the classpath and start a storage enabled server, run the following commands: - Using JavaScript Objects in Java Application
You need to complete creating the polyglot application by writing a Java class that uses the JavaScript objects.
Parent topic: Developing and Running Polyglot Coherence Applications
Setting up the JavaScript Project
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:
Parent topic: Developing Polyglot Coherence Applications
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.
Parent topic: Setting up the JavaScript Project
Creating a webpack.config.js File
To create a webpack.config.js file:
Parent topic: Setting up the JavaScript Project
Implementing Application Classes
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:
Parent topic: Developing Polyglot Coherence Applications
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.
- Must define and export a class that has one method named
evaluatethat takes one argument. - The
evaluate(obj)must returntrueifobjsatisfies the criteria represented by the filter. Otherwise, it should returnfalse.
Parent topic: Using Filters
Implementing a Filter
To write a filter that checks if the Person is in his or her teens:
./src/main.js and edit the edit
file to add the following content:
Note:
This file is mentioned in thewebpack.config.js
export class IsTeen { // [1]
evaluate(person) { // [2]
return person.age >= 13 && person.age <= 19;
}
}
- [1] defines and exports a class named
IsTeen. - [2] defines a method named
evaluatethat takes a single parameter. This method checks if theageattribute is between 13 and 19.
Parent topic: Using Filters
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:
Parent topic: Implementing Application Classes
Using an EntryProcessor Interface
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:
- Define an exported class with one method named
processthat takes one argument (which will be the cache Entry). - If the
process(entry)method mutates the cache Entry 's value, then it must update the entry value explicitly.
Parent topic: Using EntryProcessors
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]
}
}- [1] defines and exports a class named
UpperCaseProcessor. - [2] defines a method called
process()that takes a single parameter. TheNamedCacheentry that needs to be processed is accessible through theentryargument that is passed to this method. - [3] Converts the person's
lastNameto upper case. - [4] updates the
entrywith the new value. - [5] returns the updated (uppercase) last name as the result of the processor execution.
Parent topic: Using EntryProcessors
Using ValueExtractors
A ValueExtractor is
used to extract a value from an object. See Query Concepts.
This section includes the following topics:
Parent topic: Implementing Application Classes
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.
Parent topic: Using ValueExtractors
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]
}
}- [1] defines and exports a class named
AgeExtractor. - [2] defines a method named
extractthat takes a single parameter. - [3] returns the
value's age attribute.
Parent topic: Using ValueExtractors
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:
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.getPartialResult():Returns partial result of the parallel aggregation from each member.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.finalizeResult():Calculates and returns the final result of the aggregation.
Parent topic: Using Aggregators
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;
}
}Parent topic: Using Aggregators
Installing and Using Dependencies
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 lodashParent topic: Developing Polyglot Coherence Applications
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 thejs file.
Parent topic: Developing Polyglot Coherence Applications
Building and Packaging JavaScript Modules
cd ${PRJ_DIR}
npm run buildWhether 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.
Parent topic: Developing Polyglot Coherence Applications
Automatic Pre-loading of JavaScript Modules
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 ofscripts/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.
Parent topic: Developing Polyglot Coherence Applications
Setting the Classpath and Starting a Storage Enabled Server
Note:
The parent directory ofscripts/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.DefaultCacheServerwhere, ${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.
Parent topic: Developing Polyglot Coherence Applications
Using JavaScript Objects in Java Application
This section includes the following topics:
Parent topic: Developing Polyglot Coherence Applications
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>
Parent topic: Using JavaScript Objects in Java Application
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:
Parent topic: Using JavaScript Objects in Java Application
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/exampleCreate 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;
}Parent topic: Populating the Cache
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");
}Parent topic: Populating the Cache
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.DefaultCacheServerParent topic: Populating the Cache
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
- Making Parallel Updates Using JavaScript EntryProcessor
- Using ValueExtractor in a Filter
- Running the Aggregator
Parent topic: Using JavaScript Objects in Java Application
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);
}
public static void main(String[] args)
{
MyAppClient appClient = new MyAppClient();
appClient.populateCache();
appClient.displayTeens();
}Parent topic: Invoking Server Side JavaScript Objects from Java
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);
}Parent topic: Invoking Server Side JavaScript Objects from Java
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);
}Parent topic: Invoking Server Side JavaScript Objects from Java
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();
}
}Parent topic: Invoking Server Side JavaScript Objects from Java