Hybris Scripting Engine

Reference :https://wiki.hybris.com/display/release5/hybris+Scripting+Engine


What Can I Use Scripts For?

These are just some example applications of scripts:

Supported Languages

Scripting Engine implementation in hybris platform is based on the JSR-223 standard. At the moment, three languages are supported out-of-the-box: 
  • Groovy
  • BeanShell
  • JavaScript
It is possible to add more languages from the JSR-223 support list, see Adding New Languages.

API Overview

From the user perspective, the API is very simple. It is based on four main interfaces:
  • ScriptingLanguagesService provides methods for obtaining a ScriptExecutable object.
  • ScriptExecutable represents an executable object, which provides methods for executing it.
  • ScriptContent represents an object that wraps script content in a specific language type.
  • ScriptExecutionResult represents the script execution result.

Creating Scripts


There are three ways of creating scripts:
Let's create a Groovy script, which searches for all Media types that have the mime attribute empty, and sets the value of this attribute based on their realFilename attribute.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import de.hybris.platform.servicelayer.search.FlexibleSearchQuery
flexibleSearchService = spring.getBean "flexibleSearchService"
mimeService = spring.getBean "mimeService"
modelService = spring.getBean "modelService"
def findMediasWithoutMime() {
    query = new FlexibleSearchQuery("SELECT {PK} FROM {Media} WHERE {mime} IS NULL")
     
    flexibleSearchService.search(query).result
}
findMediasWithoutMime().each {
    it.mime = mimeService.getMimeFromFileExtension(it.realfilename)
     
    modelService.save it
}

Note how the interaction with the spring context is done. You can access any Spring bean from the hybris application context by referring to a special spring global variable. This topic is covered later on in this document.

Executing Scripts

Tip

Scripts are run with the same rights that the current user has.
Warning

Scripts are not secured in any way, so you can use any language-specific structures; hence, calling System.exit(-1) is possible.
Now that you have this code sample, let's execute it directly by using the ScriptingLanguagesService. To do that, prepare a special wrapper for the real script content.

Executing Scripts from String - SimpleScriptContent

This implementation allows you to execute the script content directly as a String. Let's imagine that you have the previously mentioned script content in the String variable content:
1
2
3
4
5
6
7
8
9
10
11
12
final String content = ".... content of the script ...";
final String engineName = "groovy";
  
// Let's assume we have scriptingLanguagesService injected by Spring
final ScriptContent scriptContent = new SimpleScriptContent(engineName, content);
final ScriptExecutable executable = scriptingLanguagesService.getExecutableByContent(scriptContent);
  
// now we can execute script
final ScriptExecutionResult result = executable.execute();
  
// to obtain result of execution 
System.out.println(result.getScriptResult());

Executing Scripts from Resources - ResourceScriptContent

Writing scripts directly in the Java code and keeping them in variables is not the most convenient way to do it. It is much better to write them in your favorite editor with syntax highlighting. Script Engine allows you to execute scripts that are resource based, stored for instance on the classpath or directly in the file system.

Executing Scripts from the File System

Assuming that our sample script is located on the disk, with the path /Users/zeus/scripts/setMimesForMedias.groovy, you can execute it as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.core.io.Resource;
import org.springframework.core.io.FileSystemResource;
  
final Resource resource = new FileSystemResource("/Users/zeus/scripts/setMimesForMedias.groovy");
// Let's assume we have scriptingLanguagesService injected by the Spring
final ScriptContent scriptContent = new ResourceScriptContent(resource);
final ScriptExecutable executable = scriptingLanguagesService.getExecutableByContent(scriptContent);
  
// now we can execute script
final ScriptExecutionResult result = executable.execute();
  
// to obtain result of execution
System.out.println(result.getScriptResult());

Executing Scripts by Using Classpath

Or, if you have the same script but in the classpath in the folder scripts:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.core.io.Resource;
import org.springframework.core.io.ClassPathResource;
  
final Resource resource = new ClassPathResource("scripts/setMimesForMedias.groovy");
// Let's assume we have scriptingLanguagesService injected by the Spring
final ScriptContent scriptContent = new ResourceScriptContent(resource);
final ScriptExecutable executable = scriptingLanguagesService.getExecutableByContent(scriptContent);
  
// now we can execute script
final ScriptExecutionResult result = executable.execute();
  
// to obtain result of execution
System.out.println(result.getScriptResult());

Executing Scripts Stored Remotely

You can also store the script content remotely, for instance as a gist at github.com under the URL  https://gist.githubusercontent.com/zeus/testMimesForMedias.groovy . In this way, it is easy to get hold of and execute:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
  
final Resource resource = new UrlResource("https://gist.githubusercontent.com/zeus/setMimesForMedias.groovy");
// Let's assume we have scriptingLanguagesService injected by the Spring
final ScriptContent scriptContent = new ResourceScriptContent(resource);
final ScriptExecutable executable = scriptingLanguagesService.getExecutableByContent(scriptContent);
  
// now we can execute script
final ScriptExecutionResult result = executable.execute();
  
// to obtain result of execution
System.out.println(result.getScriptResult());


Keep in mind that ResourceScriptContent determines the script engine name by the proper file extension, thus all scripts stored in files must have valid extensions for the language in which they are written.

Handling Exceptions

The scripting engine implementation uses only unchecked exceptions. This is a list of all exceptions that you can encounter:
Exception
Description
de.hybris.platform.scripting.engine.exception.ScriptingExceptionMain exception class for all scripting engine related exceptions
de.hybris.platform.scripting.engine.exception.ScriptExecutionException Exception thrown when the execution of a script has failed (for instance, a script contains errors in the body of the script)
de.hybris.platform.scripting.engine.exception.ScriptCompilationException Exception thrown when the compilation of a script has failed.
de.hybris.platform.scripting.engine.exception.ScriptNotFoundExceptionException thrown when a persisted script cannot be found in the repository.
de.hybris.platform.scripting.engine.exception.ScriptURIExceptionException thrown when the Script URI contains an error.
de.hybris.platform.scripting.engine.exception.DisabledScriptExceptionException thrown when a given ScriptExecutable is disabled due to previous errors.

Logging

To turn on logging for the scripting engine, set the logging level to debug by using the property  log4j.logger.de.hybris.platform.scripting.engine=DEBUG.

Scripts Backed by Models - ModelScriptContent

hybris Commerce Suite also comes with the model-based ScriptContent that is backed by the Script type. This special type is a container for storing scripts in the database. Let's now create a Script instance that keeps the media maintenance script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import de.hybris.platform.scripting.enums.ScriptType;
  
// Let's assume we have modelService injected by the Spring
final ScriptModel script = modelService.create(ScriptModel.class);
script.setScriptType(ScriptType.GROOVY);
script.setContent(".... content of the script ...");
// code must be unique
script.setCode("setMimesForMedias");
modelService.save(script);
  
// now having our model we can wrap it using ModelScriptContent
final ScriptContent scriptContent = new ModelScriptContent(script);
final ScriptExecutable executable = scriptingLanguagesService.getExecutableByContent(scriptContent);
  
// now we can execute script
final ScriptExecutionResult result = executable.execute();
  
// to obtain result of execution
System.out.println(result.getScriptResult());

Autodisabling of Model-Based Scripts

Model-based scripts can be autodisabled. This comes in handy if a script throws an execution exception. In that case, the status of an autodisabling script changes to disabled. Otherwise, a faulty script would endlessly throw execution exceptions.
ScriptModel  has two boolean properties:  autodisabling  and  disabled . Both are by default set to false. To enable the auto-disabling feature, s et  ScriptModel#autodisabling item property to  true. You can also use the hMC to set Autodisabling to True.
 
Scripts are marked as disabled on first throw of de.hybris.platform.scripting.engine.exception.ScriptExecutionException. Each next call to ScriptExecutable#execute on a script which was already marked as disabled throws a de.hybris.platform.scripting.engine.exception.DisabledScriptException

If you want to re-enable the script for execution, go to the hMC, find the corresponding ScriptModel in Scripts section and change the flag disabled to false.
Getting Script Executables by scriptURI
Playing with the ScriptContent interface for something persisted and whose exact address you know is not very handy. You always need to create a proper ScriptContent object. It is better to use the address of the script and get hold of the executable directly. Hence the concept of Script Repositories. A script repository allows you to look for a script in a particular storage - it may be a database, a local disk, or some remote storage. hybris comes with four implementations that are used internally by the ScriptingLanguagesService to resolve script addresses transparently. These are called scriptURIs and prepare appropriate ScriptContent objects behind the scenes to return a ScriptExecutable to the user.
  • ModelScriptsRepository
  • ClasspathScriptsRepository
  • FileSystemScriptsRepository
  • RemoteScriptsRepository
ModelScriptsRepository
This repository looks for scripts in the database. It supports the following type of scriptURI:
model://uniqueCodeOfScript
To get hold of a script using this repository, the user needs to use the ScriptingLanguagesService as follows:
1
2
3
4
5
6
7
final ScriptExecutable executable = scriptingLanguagesService.getExecutableByURI("model://setMimesForMedias");
  
// now we can execute script
final ScriptExecutionResult result = executable.execute();
  
// to obtain result of execution
System.out.println(result.getScriptResult());

ClasspathScriptsRepository

This repository looks for scripts in the database. It supports the following type of scriptURI
1
 classpath://path/to/uniqueCodeOfScript.groovy
To get hold of a script using this repository, the user needs to use ScriptingLanguagesService as follows:
1
2
3
4
5
6
7
final ScriptExecutable executable = scriptingLanguagesService.getExecutableByURI("classpath://scripts/setMimesForMedias.groovy");
  
// now we can execute script
final ScriptExecutionResult result = executable.execute();
  
// to obtain result of execution
System.out.println(result.getScriptResult());

Keep in mind that scripts in this repository must contain a valid file extension according to the language they are written in.

FileSystemScriptsRepository

This repository looks for scripts in the database. It supports the following type of scriptURI
1
2
file:///absolute/path/to/uniqueCodeOfScript.groovy
file://c:/absolute/path/to/uniqueCodeOfScript.groovy
To get hold of a script using this repository, use the ScriptingLanguagesService as follows:
1
2
3
4
5
6
7
final ScriptExecutable executable = scriptingLanguagesService.getExecutableByURI("file:///Users/zeus/scripts/setMimesForMedias.groovy");
  
// now we can execute script
final ScriptExecutionResult result = executable.execute();
  
// to obtain result of execution
System.out.println(result.getScriptResult());

Scripts in this repository must contain a valid file extension according to the language in which they are written.

RemoteScriptsRepository

This repository looks for scripts in the database. It supports the following type of scriptURI
1
2
3
http:///server.com/path/to/uniqueCodeOfScript.groovy
https:///server.com/path/to/uniqueCodeOfScript.groovy
ftp:///server.com/path/to/uniqueCodeOfScript.groovy
To get hold of a script using this repository, use the ScriptingLanguagesService as follows:
1
2
3
4
5
6
7
final ScriptExecutable executable = scriptingLanguagesService.getExecutableByURI("http:///server.com/scripts/setMimesForMedias.groovy");
  
// now we can execute script
final ScriptExecutionResult result = executable.execute();
  
// to obtain result of execution
System.out.println(result.getScriptResult());

Scripts in this repository must contain a valid file extension according to the language in which they are written.

Executing a Script Using Arguments - ScriptExecutable and ScriptExecutionResult

So far you have seen a simple usage of ScriptExecutable that shows how to execute a script without any arguments. Let's now look at a more advanced scenario, where you want to fix all mime types in medias whose realFilename has a specific extension:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import de.hybris.platform.servicelayer.search.FlexibleSearchQuery
  
flexibleSearchService = spring.getBean "flexibleSearchService"
mimeService = spring.getBean "mimeService"
modelService = spring.getBean "modelService"
// the query finds all medias for which mime type is null and whose realfile nasme has a specific pattern.
def findMediasWithoutMime() {
    query = new FlexibleSearchQuery("SELECT {PK} FROM {Media} WHERE {mime} IS NULL AND {realfilename} LIKE ?realfilename")
    query.addQueryParameter("realfilename", realfilename);
    flexibleSearchService.search(query).result
}
findMediasWithoutMime().each {
    it.mime = mimeService.getMimeFromFileExtension(it.realfilename)
     
    modelService.save it
}
Now you can execute the script as follows (let's suppose the script is on the classpath) for all medias with a  filename with the extension xml:
1
2
3
4
5
6
7
8
9
final ScriptExecutable executable = scriptingLanguagesService.getExecutableByURI("classpath://scripts/setMimesForMedias.groovy");
final Map<String, Object> params = new HashMap<>();
// here we pass arguments into the hashmap.
params.put("realfilename""%.xml"); 
// now we can execute script
final ScriptExecutionResult result = executable.execute(params);
  
// to obtain result of execution
System.out.println(result.getScriptResult());
The example above is written in Groovy. This means that the last line of the script always affects the result. If it returns something, you get a script result Object. If it does not, the call to the ScriptExecutionResult#getScriptResult() returns null. A script may also yield some output. Let's modify the script a little bit to print at the end the number of fixed mime types in Medias:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import de.hybris.platform.servicelayer.search.FlexibleSearchQuery
  
flexibleSearchService = spring.getBean "flexibleSearchService"
mimeService = spring.getBean "mimeService"
modelService = spring.getBean "modelService"
def findMediasWithoutMime() {
    query = new FlexibleSearchQuery("SELECT {PK} FROM {Media} WHERE {mime} IS NULL AND {realfilename} LIKE ?realfilename")
    query.addQueryParameter("realfilename", realfilename);
    flexibleSearchService.search(query).result
}
  
def mediaHit = 0
findMediasWithoutMime().each {
    it.mime = mimeService.getMimeFromFileExtension(it.realfilename)
     
    modelService.save it
    mediaHit++
}
  
println "Num of fixed mimetypes for Medias - ${mediaHit}"
Now the last line executes the println function, which means that it does not return anything but prints something to the standard output. Now you can read the message.
1
2
3
4
5
6
7
8
final ScriptExecutable executable = scriptingLanguagesService.getExecutableByURI("classpath://scripts/setMimesForMedias.groovy");
final Map<String, Object> params = new HashMap<>();
params.put("realfilename""%.xml"); 
// now we can execute script
final ScriptExecutionResult result = executable.execute(params);
  
// to obtain result of execution
System.out.println(result.getOutputWriter());// The message is: Num of fixed mimetypes for Medias - 5

Note that ScriptExecutionResult contains two methods for getting output and error writers - getOutputWriter() and getErrorWriter(). When calling methods on ScriptExecutable both, by default, return standard StringWriter objects. If you want to pass your own Writer implementation, you may call the method ScriptExecutable#execute(Map<String, Object> context, Writer outputWriter, Writer errorWriter)

Using Returned Objects as Interfaces

It is also possible to get an object of a class from a script and call methods on it directly in the Java code. Let's rewrite our Medias-related script to something more object oriented:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class GroovyMimeFixer implements MimeFixer {
    final static FIND_ALL_QUERY = "SELECT {PK} FROM {Media} WHERE {mime} IS NOT NULL"
    final static FIND_FOR_EXT_QUERY = FIND_ALL_QUERY + " AND {realfilename} LIKE ?realfilename"
     
    def flexibleSearchService
    def mimeService
    def modelService
    def int fixAllMimes() {
        def query = new FlexibleSearchQuery(FIND_ALL_QUERY)
        def counter = 0
        flexibleSearchService.search(query).result.each {
            it.mime = mimeService.getMimeFromFileExtension(it.realfilename)
            modelService.save it
            counter++
        }
        counter
    }
  
    def int fixMimesForExtension(String extension) {
        def query = new FlexibleSearchQuery(FIND_FOR_EXT_QUERY)
        query.addQueryParameter("realfilename""%.${extension}");
        def counter = 0
        flexibleSearchService.search(FIND_FOR_EXT_QUERY).result.each {
            it.mime = mimeService.getMimeFromFileExtension(it.realfilename)
            modelService.save it
            counter++
        }
        counter
    }
}
flexibleSearchService = spring.getBean "flexibleSearchService"
mimeService = spring.getBean "mimeService"
modelService = spring.getBean "modelService"
  
new GroovyMimeFixer(flexibleSearchService: flexibleSearchService, mimeService: mimeService, modelService: modelService)

Note that an instance of the class is returned in the last line of the script.
The script above defines a new Groovy class GroovyMimeFixer that implements the Java interface MimeFixer, which you have in the code base and which looks as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface MimeFixer
{
    /**
     * Fix all empty mimes.
     *
     * @return num of fixed mimes
     */
    int fixAllMimes();
    /**
     * Fix mimes for particular file extension.
     *
     * @param extension
     * @return num of fixed mimes
     */
    int fixMimesForExtension(String extension);
Now you can execute this script a bit differently to obtain the object returned by the script:
1
2
3
4
5
6
final ScriptExecutable executable = scriptingLanguagesService.getExecutableByURI("classpath://scripts/setMimesForMedias.groovy");
  
final MimeFixer mimeFixer = executable.getAsInterface(MimeFixer.class);
  
mimeFixer.fixMimesForExtension("xml");
mimeFixer.fixAllMimes();

Accessing Spring Beans 

Accessing Spring Beans in Groovy

In all of the examples, the Spring application context was referred to using the global variable spring. This variable is available only at the top level of the script, thus you need to pass it into the classes and other structures that have a limited scope. You can also access spring beans directly by their bean names but remember that top level script scope also applies here. So the following example works:
1
2
3
import de.hybris.platform.scripting.model.ScriptModel
  
modelService.create(ScriptModel.class
But this one does not:
1
2
3
4
5
6
7
8
9
import de.hybris.platform.scripting.model.ScriptModel
  
class ScriptModelCreator {
    def create() {
        modelService.create(ScriptModel.class)
    }
}
  
new ScriptModelCreator().create()
You must pass ModelService into the ScriptModelCreator class:
1
2
3
4
5
6
7
8
9
10
11
import de.hybris.platform.scripting.model.ScriptModel
  
class ScriptModelCreator {
    def modelService
  
    def create() {
        modelService.create(ScriptModel.class)
    }
}
  
new ScriptModelCreator(modelService: modelService).create()

Accessing Beans in Javascript

Javascript is much less strict, so you can access Spring beans directly:
1
2
3
4
5
6
7
8
9
10
11
FlexibleSearchQuery = Packages.de.hybris.platform.servicelayer.search.FlexibleSearchQuery;
var query = FlexibleSearchQuery("SELECT {PK} FROM {Media} WHERE {mime} IS NULL")
var found = flexibleSearchService.search(query).getResult()
for each (var media in found.toArray()) {
  media.setMime(mimeService.getMimeFromFileExtension(media.getRealFileName()));
  modelService.save(media);
  
Note how the import of Java FlexibleSearchQuery class is done - via a special Packages object.

Adding New Languages

Adding a new language is simple, but you need to know which libraries are required in the classpath. Here you'll see how to add Ruby. JRuby implementation has built-in JSR-223 support.
First, you need to define library dependencies in appropriate external-dependencies.xml file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    <modelVersion>4.0.0</modelVersion>
    <groupId>de.hybris.platform</groupId>
    <artifactId>scripting</artifactId>
    <version>5.0.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>
  
    <dependencies>
        <dependency>
            <groupId>org.jruby</groupId>
            <artifactId>jruby-complete</artifactId>
            <version>1.7.11</version>
        </dependency>
    </dependencies>
</project
Next, declare bean of type de.hybris.platform.scripting.engine.internal.ScriptEngineType in the Spring context as follows:
1
2
3
4
5
6
7
8
9
10
11
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
     
    <bean id="jrubyEngine" class="de.hybris.platform.scripting.engine.internal.impl.DefaultScriptEngineType"
          c:name="ruby" c:fileExtension="rb" c:mime="text/x-ruby" />
</beans

Note the c namespace in Spring file definition ( http://www.springframework.org/schema/c) - this is a handy shortcut for passing constructor arguments to the bean.
This bean generally represents a new type of the scripting engine and keeps its name, file extension, and mime.
Finally, add a new value to the existing ScriptType enumtype in the hybris type system. This one is required by the Script type mentioned above - if you need to store your scripts into a database. The value is the name of the scripting engine.
1
2
3
4
5
6
7
8
9
10
11
12
       xsi:noNamespaceSchemaLocation="items.xsd">
<enumtypes>
        <enumtype code="ScriptType">
            <value code="GROOVY"/>
            <value code="BEANSHELL"/>
            <value code="JAVASCRIPT"/>
            <!-- new value comes here -->
            <value code="RUBY"/>
        </enumtype>
    </enumtypes>
</items
That's it - your new language is ready to use.

Methods of interface ScriptExecutable are not synchronised, so the underlying ScriptEngine must provide thread-safety. Before adding a new script language, test that the new language behaves as expected in a multithread environment.

Script Versioning

1) Let's create a simple print script with the code testScript in the Scripting Language Console in hybris Administration Console. 
println "Groovy Rocks"
Save the script. 
2) Now modify this script such that it prints:
println "Groovy Wows"
Save the modified script.
3) Modify the script again:
println "Groovy Amazes"
4) Now go to the Browse tab. You can see only one instance of the testScript in the directory tree. 
 However, if you query the database using the following query, you will see three versions of the testScript.
select * from scripts where p_code = 'testScript' order by p_version;
  
You can call revision 1 of this script in the following manner:
final ScriptExecutable executable = scriptingLanguagesService.getExecutablebyURI("model://testScript/1");
final ScriptExecutionResult result = executable.execute();
  
If you do not specify any revision number, the latest version will be used.

Managing Script Versions in hMC

You can create, edit, and search for your scripts in hMC.
Searching for existing scripts only shows the latest script version. To see all versions, change the Active flag attribute to No preference.


Comments

Popular Posts