The Cyclos scripting module (available from version 4.2 onwards) provides an integration layer that allows connecting from Cyclos to third party software, as well executing custom operations and scheduled tasks within Cyclos self. The scripting module offers an easy way to customize and extend Cyclos, without losing compatibility with future Cyclos versions. The scripting engine can access the full Cyclos services layer which makes it a powerful feature. For security reasons only global administrators can add scripts. Network administrators can be given permissions to bound the scripts to elements such as extension points (eg. payment, user profile, advertisement), custom validations (for input fields), custom calculations (account fees, transaction fees), custom operations and scheduled tasks. Any internal entity in Cyclos (e.g. user, address, payment, authorization, reference etc.) can be accessed by the scripts. When developing custom operations it is likely that you want to store and use new values/entities. It is possible to create specific record types and custom fields and make them available to the scripts. The record types can be of the type 'system' or 'user' depending on the requirements.
On this page you will find links with documentation about the available extensions and examples. In the future we will add a repository of useful scripts. If you wrote a script that could serve other projects we will be happy to add it. Please post it on our Forum or send it to info@cyclos.org.
Global admins can write and store scripts directly within Cyclos. Each script ‘type’ has its own functions which have to be implemented. A network admin can chose from the available scripts and bind them to Cyclos operations and events, or to new operations. The variables used in the scripts can be managed outside the scripts in the extensions self (by the network admin). This avoids the need for a global admin having to modify a script every time a new or different input value is required. It is also possible to define additional information and confirmation texts that can be displayed to the user when a custom operation is initiated or submitted.
The scripting language currently supported is Groovy. It offers a powerful scripting language that is very similar to Java, with a close to zero learning curve for Java developers. It is possible to write scripts that will be available in a shared script library, so that other scripts within the same context can make use of it. All scripts are compiled to Java bytecode which makes them highly performatic. Currently Cyclos requires Java 8 or above.
Debugging scripts can sometimes be tricky, because the exact context is only available at runtime, and errors can be hidden. A good approach is to set cyclos.dumpAllErrors to true in cyclos.properties. This way whenever an error is triggered, it is dumped to the application server (i.e., Tomcat) console.
Regarding database transactions, normally scripts run inside a database, and returning without errors means the transaction is committed, while throwing an exception means the transaction is rolled-back. So, be aware that silencing database error in the script (catching them without throwing another exception) may cause a transaction not to be rolled back, and if multiple database operations were performed, the final state can be inconsistent. For example, when performing a payment, a transaction (representing the payment) is created. Then one or more transfers are created (transfering of funds between accounts - there can be multiple if there are fees). Before each transfer the account balance is checked, to make sure it has enough funds. In this case, if some account has no balance and the exception is silenced, the database will have a processed transaction without a corresponding transfer, which is an inconsitent state for Cyclos.
When running, scripts have a set of bindings, that is, available top-level variables. At runtime, the bindings will vary according to the script type and context. For example, each extension point type has one or more specific bindings. On all cases, however, the following variables are bound:
scriptParameters: In the script details page, or in every every page where a script is chosen to be used (for example, in the extension point or custom operation details page) there will be a textarea where parameters may be added to the script. They allow scripts to be reused in different contexts, just with different parameters. The text is parsed as Java Properties, and the format is described here. The library parameters are included first (if any), then the own script parameters (if any), then the specific page parameters. This allows overridding parameters at more specific levels.
scriptHelper: An instance of org.cyclos.impl.system.ScriptHelper. Besides having the instance, all methods are automatically exported as closures on the default binding, making it possible to call its methods without using the 'scriptHelper.' prefix. The ScriptHelper contains some useful methods, like:
wrap(object[, customFields]): wraps the given object in a Map, with some custom characteristics:
If the wrapped object contains custom fields, it will allow getting / setting custom field values using the internal name
Values will be automatically converted to the expected destination type
If a list of custom fields are passed, then they are considered. If not, will attempt to read the current fields for the object, which might not always be available (for example, when creating a new record) or even no longer active (for example, when the product of a user just removed a field, and the value is still there)
Example:
def bean = scriptHelper.wrap(user) def gender = bean.gender // gender will be a org.cyclos.entities.system.CustomFieldPossibleValue // if gender is an enumerated field def date = bean.customDate // date will be a java.util.Date if customDate is a date field def relatedUser = bean.relatedUser // relatedUser will be an org.cyclos.entities.users.User // if relatedUser is linked entity field of type user
bean(class): returns a bean by type. The class reference needs to be passed.
addOnCommit(runnable), addOnRollback(runnable): Adds callbacks to be executed after the main database transaction ends, either successfully or with failure. Be aware that those callbacks will be invoked outside any transaction scope within Cyclos, so things like 'sessionData.loggedUser' won't work (because it requires retrieving the User object from the database). However, it is more efficient, as no new database access needs to be done. This is mostly useful to notify an external application that some data has been persisted in Cyclos (after we're 100% sure that the data is persistent). Keep in mind that there is a (very) small chance that the main transaction is committed / rolled back but then the server crashes, and the callback weren't yet called. So, when synchronizing with external systems, it is always wise to do some form of timeout / recovery mechanism.
addOnCommitTransactional(runnable), addOnRollbackTransactional(runnable): Same as the non-transactional counterparts, but they are executed inside a new transaction in Cyclos.
maskId(id), unmaskId(id): Returns a suitable representation of an entity id to send to clients, or convert back one received from clients into the original form.
Starting with Cyclos 4.6, a general-purpose storage is available for scripts. It is a key/value storage, implementing the ObjectParameterStorage interface. It stores the values as JSON in the database. Besides the methods for get/set String, Boolean, Decimal, Integer, Long and Enum, it also supports storing objects. Also, a mechanism is provided for Groovy scripts to access objects directly via the property name, such as storage.value = value or value = storage.value.
A script storage is obtained using a key (string), and a timeout can (optionally) be set before the storage expires. The storage is accessed via the ScriptStorageHandler. It provides the following methods:
Some restrictions apply on which kind of objects can be stored or retrieved. Entities can only be stored if they are already persisted (only the id is stored, and the entity is loaded by id from the database when retrieved). Other objects need to have a public empty constructor, plus getters and setters for fields.
Example:
// Storage retrieval def timeout = 60 * 60 * 3 // Expires in 3 hours def key = "requests_for_${sessionData.loggedUser.id}" def storage = scriptStorageHandler.get(key, timeout) // First, store the number of requests for the logged user storage.requests = (storage.requests ?: 0) + 1 // Then, later on, maybe on another script... return "There are ${storage.requests} requests for user ${sessionData.loggedUser.name}"