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 thelodash
module or themoment
module 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 instructwebpack
about 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/js
directory (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
Person
s 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
evaluate
that takes one argument. - The
evaluate(obj)
must returntrue
ifobj
satisfies 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
evaluate
that takes a single parameter. This method checks if theage
attribute 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
process
that 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. TheNamedCache
entry that needs to be processed is accessible through theentry
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.
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
extract
that 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 lodash
Parent 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 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.
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.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
.
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/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; }
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.DefaultCacheServer
Parent 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
ValueExtractor
s 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