Script-controlled Sophora Extensions

Scripts can be added and managed under the menu item "Scripts" within Sophora's administrator view

Table of Contents

Preface to Scripts in Sophora

Scripts can be added and managed under the menu item "Scripts" within Sophora's administrator view. Available script languages are Groovy and BeanShell. Scripts are only recognised on Sophora master servers. Furthermore, only the latest published version of a script (document) will be executed.

To create a new script,

  1. Mark the menu item "Scripts" and select "New: Script" from the context menu.
  2. Within the emerging dialog you may change the script document's location (structure node) or its ID stem.
  3. Click "Finish" and an empty form opens in the editor area where you have to specify the code.
  4. Save the Script.

To edit an existing script,

  1. Expand the menu item "Scripts".
  2. Double-click the script you want to edit or use "Open" within its context menu.
  3. Enter your changes.
  4. Save the script when you have finished.

Imports

Each script is automatically prepended with a preamble which contains the package declaration and some default import statements as given in the snippet below.

package script;
 
import java.io.*;
import java.util.*;
import java.text.*;
import com.subshell.sophora.api.*;
import com.subshell.sophora.api.access.*;
import com.subshell.sophora.api.content.*;
import com.subshell.sophora.api.content.value.*;
import com.subshell.sophora.api.content.validation.*;
import com.subshell.sophora.api.exceptions.*;
import com.subshell.sophora.api.event.*;
import com.subshell.sophora.api.event.DocumentChangedEvent.StateChange;
import com.subshell.sophora.api.nodetype.*;
import com.subshell.sophora.api.server.*;
import com.subshell.sophora.api.structure.*;
import com.subshell.sophora.api.scripting.*;
import com.subshell.sophora.api.scripting.formfieldconfigchange.*; 
import org.apache.commons.io.*;
import org.apache.commons.lang.*;
import org.slf4j.*

Of course you may provide additional import statements to meet your individual requirements.

Execution of Scripts on Slaves and Staging Slaves

Scripts for document state changes and scripts listening for server events are executed in the server. Per default are these scripts only executed on the master server. But in some situations it is necessary to execute scripts additionally in replication or staging slaves. To accomplish this, the script has to implement the method isActiveOnSlave() respectivly the method isActiveOnStagingSlave(). These two methods are only considered if they return the boolean value true.

Scripts for State Changes of Documents

To automatically apply customised operations upon documents when they are either saved or their state have changed, you may add scripts that are invoked on certain events.In general, a script has to return an object of the type com.subshell.sophora.api.scripting.IScriptDocumentChangeListener. Using the init method you receive a script context as a parameter on start-up. This context contains a Logger object that will be used by the script itself to write logging statements to the server's log file. Each script receives a log category that is assembled from the prefix "com.subshell.sophora.server.application.scripting.ScriptManager" and the class name of the generated objects. The exemplary script in section BeanShell Example would result in the following Logback configuration:

<logger name="com.subshell.sophora.server.application.scripting.ScriptManager.script.Listener" level="DEBUG" />

Events Overview

The method com.subshell.sophora.api.scripting.IScriptDocumentChangeEvent.getStateChange() provides information about what kind of state change triggered the script's execution. The possible state changes (actions) and correspondent events are displayed in the following table:

ActionEvent
SaveNONE
PublishPUBLISH
Delete
(Moved to trash) 
DELETE
Delete from trash
(Physically deleted)
COMPLETELY_DELETED
ReleaseRELEASE
Set offlineOFFLINE
DeactivateDISABLE
ActivateENABLE

Note that actions possibly trigger a script to be executed multiple times. For example, if a user changes a document and publishes it without saving beforehand, the document (internally) be saved first and than published. Thus, a script might be triggered twice: once with the event type NONE and afterwards with an event of the type PUBLISH.

Since scripts are called on state changes (so before the according document actually is in the new state), you can distinguish between a document that has just been created (never saved before) and an existing document: New documents do not have a UUID until they are saved the first time.

If the change event is a save event ("StateChange.NONE") you can modify documents during the saving procedure. The method com.subshell.sophora.api.scripting.IScriptDocumentChangeEvent.getDocument() provides a reference to the documents that is currently saved. This can be edited arbitrarily. The saving procedure continues implicitly after the script terminates. If you want to modify the id stem of the document during the first save operation, you can cast the IScriptDocumentChangeEvent to an IScriptDocumentSavingEvent which provides you with a get and a set method for manipulating the id stem. For more information on modifying the id stem during save procedure see example no. 3 in the next section.

The method com.subshell.sophora.api.scripting.IScriptDocumentChangeEvent.getCause() provides information whether the event is caused by clone, restore or clone document from version.

ActionEvent
Clone documentCLONE
Restore documentRESTORE
Save version as cloneCLONE_FROM_VERSION
Other causeSAVE

Manipulating the document during publishing, setting offline etc.

If "documentChanging" is not triggered by a save event (publish event, offline event etc.) and you want to modify the event's document, it is not sufficient just to set a property or childnode at the "document"-object. Additionally you must explicitly save the "document"-object. This can be done by calling:

IScriptingDocumentManager manager = event.getDocumentManager();
manager.saveDocument(document, null, false);
Never call "saveDocument" during "save"-events: Be careful that you never call "saveDocument" if you are handling an save event ("StateChange.NONE")! If you do so you might end up in an infinite loop.

BeanShell Examples

Example 1: Set a date property every time a document is saved

class Listener implements IScriptDocumentChangeListener {
  private Logger log;
 
  public void init(IScriptContext context) {
    this.log = context.getLogger();
    log.info("init");
  }
 
  public void destroy() {
    log.info("destroy");
  }
 
  public void documentChanging(IScriptDocumentChangeEvent event) {
    log.info("documentChangingState: " + event.getDocument().getUuid() + " -> " + event.getStateChange());
 
    // Only "save"-events are considered:
    if (event.getStateChange().equals(DocumentChangedEvent.StateChange.NONE)) {
      INode document = event.getDocument();
      IScriptingDocumentManager manager = event.getDocumentManager();
      NodeType nodetype = manager.getNodeType(document.getPrimaryType());
      Set mixins = nodetype.getMixins();
      if (mixins.contains("sophora-mix:document")) {
        if (! document.getProperty("xxxx:webTimeHidden").getBoolean()) {
           // Modify the document
           document.setDate("xxxxx:webTime", Calendar.getInstance());
 
           // The document will be saved automatically,
           // because this script runs in the save operation.
        }
      }
    }
  }
 
}
return new Listener();

Example 2: Set a date property every time a document is published

class Listener implements IScriptDocumentChangeListener {
  private Logger log;
 
  public void init(IScriptContext context) {
    this.log = context.getLogger();
    log.info("init");
  }
 
  public void destroy() {
    log.info("destroy");
  }
 
  public void documentChanging(IScriptDocumentChangeEvent event) {
    log.info("documentChangingState: " + event.getDocument().getUuid() + " -> " + event.getStateChange());
 
    // Only "publish"-events are considered:
    if (event.getStateChange().equals(DocumentChangedEvent.StateChange.PUBLISH)) {
      INode document = event.getDocument();
      IScriptingDocumentManager manager = event.getDocumentManager();
      NodeType nodetype = manager.getNodeType(document.getPrimaryType());
      Set mixins = nodetype.getMixins();
      if (mixins.contains("sophora-mix:document")) {
        if (! document.getProperty("xxxx:webTimeHidden").getBoolean()) {
           // Modify the document
           document.setDate("xxxxx:webTime", Calendar.getInstance());
 
           // Save the document manually,
           // because this script runs in the publish operation.
           // Attention: This will trigger a "save"-event (DocumentChangedEvent.StateChange.NONE)!
           manager.saveDocument(document, null, false);
        }
      }
    }
  }
 
}
return new Listener();

Example 3: Append minus char ("-") to the id stem when document is newly created and saved for the first time

class Listener implements IScriptDocumentChangeListener {
  private Logger log;
 
  public void init(IScriptContext context) {
    this.log = context.getLogger();
    log.info("init");
  }
 
  public void destroy() {
    log.info("destroy");
  }
 
  public void documentChanging(IScriptDocumentChangeEvent event) {
    log.info("Document " + event.getDocument().getUuid() + " changing state to: " + event.getStateChange());
 
    // Only "save"-events are considered
    if (event.getStateChange().equals(DocumentChangedEvent.StateChange.NONE) && (event instanceof IScriptDocumentSaveEvent)) {
      IScriptDocumentSaveEvent saveEvent = (IScriptDocumentSaveEvent) event;
      String idStem = saveEvent.getIdStem();
      boolean hasSophoraId = event.getDocument().hasProperty("sophora:id");
      // if id stem is set, document doesn't have a sophora id yet
      // and does not end with a minus char ("-"), then append this char
      if (StringUtils.isNotBlank(idStem) &&  !hasSophoraId && !(idStem.endsWith("-"))) {
        String newIdStem = idStem + "-";
        saveEvent.setIdStem(newIdStem);
        log.info("Document's Id Stem changed from " + idStem + " to " + newIdStem);
      }
    }
  }
}
return new Listener();

Scripts for Server Events

Scripts that should listen for server events must implement the interface com.subshell.sophora.api.scripting.IEventScript. Thereby, such scripts may step in the server's event mechanism. In contrast to the scripts for state changes of documents these scripts are called on every server event. As a further difference, they are executed after the triggering operation. The available API is defined by the interface IContentManager.

Note that server event scripts run in a separate session each and the corresponding username has to be provided by the individual script. The maximun delay within the events are passed to the server scripts is 3 seconds. For each script the functions (proccessEvent) are called sequentially, whereas different scripts are processed concurrently.

Exemplary Server Script with BeanShell

The following example shows a server script, that gets invoked when a document changes its state. The script then logs the old and the new state of the document along with its ID.

public class ServerTestSkript implements IEventScript {
 
   private Logger log;
 
   public void proccessEvent(ServerEvent event) {
      if (event instanceof DocumentChangedEvent) {
         DocumentChangedEvent dce = (DocumentChangedEvent) event;
         String sid = dce.getSophoraId();
         DocumentState oldState = dce.getOldState();
         DocumentState newState = dce.getNewState();
 
         StringBuilder builder = new StringBuilder();
         builder.append("Document changed [");
         builder.append("id: " + sid + ", ");
         builder.append("old state: " + formatState(oldState) + " to ");
         builder.append("new state: " + formatState(newState));
         log.info(builder.toString());
      }
   }
 
   private String formatState(DocumentState state) {
      StringBuilder builder = new StringBuilder();
      builder.append(state.getState().name());
      builder.append(" (");
      builder.append("isLive: " + state.isLiveVersionAvailable() + ", ");
      builder.append("isDeleted: " + state.isDeleted() + ", ");
      builder.append("isEnabled: " + state.isEnabled() + ", ");
      builder.append("isOffline: " + state.isOffline() + ", ");
      builder.append(")");
      return builder.toString();
   }
 
   public void init(IEventScriptContext context) {
      log = context.getLogger();
   }
 
   public void destroy() {
      // do nothing
   }
 
   public String getScriptUserName() {
      return "admin";
   }
 
}
 
return new ServerTestSkript();

Scripts for Timing Actions

Analogous to the document properties "Days until offline" and "Days until archive" structure nodes can be configured with time scheduling parameters "Days until <action>" (which will be inherited to subordinate nodes and documents as default value). The corresponding "action" is defined by a script that will be executed within the Sophora master server.

Configuration of Time Scheduling

Adding Custom Timing Actions to the Timing Configuration Table

To add a custom timing action to a structure node's timing configuration table, the node type configuration sophora-nt:timingConfig has to be extended by a new mixin containing a property of type long. The CND for the new mixin looks like this:

['yourproject-mix:timingActionExtension']
orderable
mixin
- yourproject:yourAction (long)

This mixin has to be added in the node type configuration of sophora-nt:timingConfig on the "Attributes" tab. Don't forget to save your changes. Afterwards, change to the "Properties" tab of the node type editor and move the newly appended property (the one provided by the mixin. Here that's yourproject:yourAction) to the base tab of this node type configuration (if it's not configured on any tab, it will be ignored). The label you provide will be used as headline of the new column in the timing configuration table of structure nodes.

The ordering of columns in the timing configuration table can be modified by moving the individual table rows with the arrow up and arrow down icon on the left-hand side. The columns defined by childnodes will always be displayed after those defined by properties.

Choosing the Reference Date Property Name Via Script

Note that you can specify a reference date property name in the corresponding script by overriding the method String getReferencePropertyName().

Choosing the Reference Date Property Name Based on Node and Content Type

If you need to specify a reference date property for each node and content type individually, you have to add a childnode instead of a long property to the new mixin. Afterwards the mixin should look like this:

['yourproject-mix:timingActionExtension']
orderable
mixin
+ yourproject:yourAction (nt:base)

Next, you have to move this childnode to the base tab, provide a label (will be used as column header of the timing action) and add sophora-nt:timingConfigData to its list of valid childnode types.

In the structure node's timing configuration table a column Your Action reference date property will be added to the right of the Your Action after days column. Use this column to specify the reference date property name for the action. The corresponding action will be performed when the configured number of days relative to the date specified by this date property name is expired. If you specify a reference property name in the structure node's timing action table and in the script as well, the value configured in the timing action table has priority.

Choosing the Reference Date Property Name Based on the Appropriate Parameter in the Property Configuration

You can also specify a reference date property in the property configuration. That property has to be a long property with the input field type "Scheduling". To do so, add the name of a date property within the document's node type for the "Reference date property name" parameter.

Setting Up the Time Scheduling within Single Documents

To configure a field in individual documents with which to overwrite the (default) time scheduling values that are set in the site or structure nodes, you need a corresponding long property within the document. See input field Time Scheduling Data for further information.

Choosing the Script Document

To define which script should be triggered for the new timing action, the InputFieldType of yourproject:yourAction (property or childnode; depending on what option you've chosen) has to be set to "Scheduling" and the parameter field "Script for Timing Action" needs to contain a reference to an existing script document (which must be published). If the parameter field "Script for Timing Action" is left blank, no script will be executed.

Script Execution Frequency

A cron expression (the server property sophora.documentTimingActions.cronTriggerExpression) determines how often scripts should be run (for more details about this server property please refer to the Sophora server's documentation). By default, scripts are executed once a day at 03:00 a.m..

Creating a Script

Scripts for timing actions have to return an object of type com.subshell.sophora.api.scripting.ITimingActionScript and the method init provides an instance of ITimingActionScriptContext as a parameter. This context contains the session in which the script itself is executed, a ContentManager object and a Logger object that handles all log statements. Each script receives a log category that is assembled from the prefix "com.subshell.sophora.server.application.timingaction.DocumentTimingActionsJob.script." and the class name of the generated objects.

If the script should search for documents in the live workspace, you have to implement the following Interface: ITimingActionScriptSearchInLiveWorkspace

The exemplary script below, for instance, would result in the following Logback configuration:

<logger name="com.subshell.sophora.server.application.timingaction.DocumentTimingActionsJob.script.MoveScript" level="DEBUG" />

Further, the subsequent example also contains three specific methods:

  • getScriptUserName() has to return the name of the user for whom a Sophora Session will be created. The corresponding Session Token will be passed through the ITimingActionScriptContext in the init() method.
  • getReferencePropertyName() may return the name of an alternative date property that should be respected to calculated whether a document needs to be processed by the timing action (evaluation of the customised field "Days until <yourAction>"). If this method returns null, "sophora:publicationDate" is taken as default.
  • getAdditionalXPath() can be used to define an optional xPath query which documents need to match additionally to be processed by this script.

Exemplary Move Script With Beanshell

public class MoveScript implements ITimingActionScript {
 
    private IContentManager contentManager;
    private SessionToken sessionToken;
    private Logger logger;
 
    public void init(ITimingActionScriptContext context) {
        logger = context.getLogger();
        sessionToken = context.getSessionToken();
        contentManager = context.getContentManager();
    }
 
    public void destroy() {
    }
 
    public String getAdditionalXPath() {
        return null;
    }
 
    public String getReferencePropertyName() {
        return "sophora:publicationDate";
    }
 
    public String getScriptUserName() {
        return "timingaction";
    }
 
    public void processDocument(String uuid) {
        INode document = contentManager.getDocumentByUuid(sessionToken, uuid, false);
        logger.info("processing document " + document.getProperty("sophora:id").getString());
        IStructureNode structureNode = contentManager.getStructureNodeByPath(sessionToken, "testsite/timingtarget", null);
        contentManager.moveDocumentToStructureNode(sessionToken, uuid, structureNode.getUuid());
    }
 
    public String getNodeType() {
        return null;
    }
}
 
return new MoveScript();

Set Offline Script With Beanshell

public class SetOfflineAfterDaysScript implements ITimingActionScript {
 
    private IContentManager contentManager;
    private SessionToken sessionToken;
    private Logger logger;
 
    public void init(ITimingActionScriptContext context) {
        sessionToken = context.getSessionToken();
        contentManager = context.getContentManager();
        logger = context.getLogger();
    }
 
    public String getAdditionalXPath() {
        return null;
    }
 
    public String getReferencePropertyName() {
        return null;
    }
 
    public String getScriptUserName() {
        return "admin";
    }
 
    public void processDocument(String uuid) {
        IContent document = contentManager.getDocumentSummaryByUuid(sessionToken, uuid);
        if (document.isLiveVersionAvailable() && !document.isOffline()) {
            logger.info("Setting document offline: " + uuid);
            contentManager.setOffline(sessionToken, uuid);
        } else {
            logger.info("Document is already offline: " + uuid);
        }
    }
 
    public String getNodeType() {
        return null;
    }
 
    public void destroy() {
        // nothing to do
    }
}
 
return new SetOfflineAfterDaysScript();

Validation Scripts

Scripts may also be applied to validate any kind of input made in Sophora. A validation script's possibilities exceed common validation expressions in the properties configuration by far.

Validation scripts have to return an object of type com.subshell.sophora.api.scripting.IValidationScript. The according interface only dictates one method:

List<IValidationMessage> validateDocument(INode document, IValidationScriptDocumentManager validationScriptDocumentManager);

Each time an arbitrary document is saved all validations scripts are conducted. It is incumbent on the individual script whether it is in charge of the actual document type. If the saved document is valid, either null or an empty list (of validation errors) can be returned. On the contrary, if there is at least one validation error, the saving operation is cancelled and an error message will be displayed to the user. A script can detect multiple validation errors at once and returns them in a list as depicted in the exemplary validation script below. You can instantiate a validation error and define an appropriate error message for it in one step by using the following static method:

com.subshell.sophora.api.content.validation.PropertyValidationError.createInvalidInput(String propertyName, String message)

With the help of the property name, the corresponding label within the deskclient as well as the display tab is identified in order to provide a meaningful error message so that the user can associate it with the erroneous input field. However, it is also possible to create a validation error object that only contains an error message. This might be handy, if errors occur in childnodes rather than properties. Simply call the constructor of the ValidationError class:

new com.subshell.sophora.api.content.validation.ValidationError(String message)

Please note that you can also create info messages via com.subshell.sophora.api.content.validation.PropertyValidationError.createInfoInput(). These should be used for information that is noteworthy, but not necessarily indicates an error.

Exemplary Validation Script

The following example is written in Groovy and defines a validation script that checks whether the date property "Online from" of the document type sophora-content-nt:story is not set to a date after the one defined in the field "Online until". In addition, the script validates that both dates are not in the past.

import static com.subshell.sophora.api.SophoraConstants.*;
 
public class TimeValidation implements IValidationScript {
 
   public List<IValidationMessage> validateDocument(INode document, IValidationScriptDocumentManager validationScriptDocumentManager) {
      List<IValidationMessage> result = new ArrayList<IValidationMessage>();
 
      if (document.getPrimaryType().equals("sophora-content-nt:story")) {
 
         if (document.hasProperty(SOPHORA_ENDDATE)) {
            if (document.getProperty(SOPHORA_ENDDATE).getDate().before(Calendar.getInstance())) {
               result.add(PropertyValidationError.createInvalidInput(SOPHORA_ENDDATE, "The \"Online until\" date is in the past"));
            }
         }
 
         if (document.hasProperty(SOPHORA_STARTDATE)) {
            if (document.getProperty(SOPHORA_STARTDATE).getDate().before(Calendar.getInstance())) {
               result.add(PropertyValidationError.createInvalidInput(SOPHORA_STARTDATE, "The \"Online from\" date is in the past"));
            }
         }
 
         if (document.hasProperty(SOPHORA_ENDDATE) && document.hasProperty(SOPHORA_STARTDATE)) {
            if (document.getProperty(SOPHORA_ENDDATE).getDate().before(document.getProperty(SOPHORA_STARTDATE).getDate())) {
               result.add(PropertyValidationError.createInvalidInput(SOPHORA_ENDDATE, "The \"Online until\" date is before the \"Online from\" date"));
               result.add(PropertyValidationError.createInvalidInput(SOPHORA_STARTDATE, "The \"Online from\" date is after the \"Online until\" date"));
            }
         }
 
      }
 
      return result;
   }
 
}
return new TimeValidation();

Exemplary Validation Script generating a NotificationMessage

public class Validation implements IValidationScript {
 
   public List<IValidationMessage> validateDocument(INode document, IValidationScriptDocumentManager validationScriptDocumentManager) {
      List<IValidationMessage> result = new ArrayList<IValidationMessage>();
 
      result.add(NotificationMessage.createNotificationMessage("Notification!"));
            
      return result;
   }
 
}
return new Validation();

Exemplary Validation Script generating a ChildNodeValidationError

public class ChildNodeValidation implements IValidationScript {
 
   public List<IValidationMessage> validateDocument(INode document, IValidationScriptDocumentManager validationScriptDocumentManager) {
      List<IValidationMessage> result = new ArrayList<IValidationMessage>();
 
      result.add(new ChildNodeValidationError("No Copytext!", null, ErrorType.NONE, "sophora-content:copytext", new ItemPath("sophora-content:copytext", 0)));
            
      return result;
   }
 
}
return new ChildNodeValidation();

ValidationScriptDocumentManager

In the example above, there is no need to interact with the ValidationScriptDocumentManager. Nonetheless, this class offers various ways to gather further information that do not stem from the document to validate directly, but are required for the validation process and/or the display of meaningful error messages. Such information might be:

  • The label of a specific select value key (handy for the assembly of error messages)
  • The label of a childnode (handy for the assembly of error messages)
  • Structure nodes
  • Node types of documents
  • YellowData of documents
  • Property configurations (accessible via the node type)
  • Childnode configurations (accessible via the node type)
  • other documents referenced with a UUID, external ID or Sophora ID
  • the latest live version of a document
  • Mime types
  • Image variants
  • Select values in general
  • The IDocumentTemplateSorter to get the (default) document template(s) for a specific node type and structure node

Scripts for Changing Form Fields

Scripts can be used to modify input fields (mainly the configuration). These scripts are triggered in the validation mechanism just before the validation takes place. This means that these scripts will be executed for each change to a form field. Additionally changes by form field change scripts are directly respected by the validation.

A form field change script must implement the interface IFormFieldChangeScript. This interface defines a single method that returns a list of changes which should be applied to the current editor.

List<IFormFieldChange<?>> changeFormField(INode document, IValidationScriptDocumentManager validationScriptDocumentManager);

Each IFormFieldChange denotes the property and an attribute of a form field that should be changed. The possible attributes are defined in the class FormFieldAttribute:

Form Field Attributes
NameDescriptionValue TypeAll Form Fields
CONFIG_CHAR_COUNTERDetermines if a character counter should be displayed.Booleanno
CONFIG_DESCRIPTIONThe description of the form field is normally displayed as tool tip.Stringyes
CONFIG_LABELThe label of the form field.Stringyes
CONFIG_PARAMETERSThe parameters are specific for each field type.Map<String, String>no
CONFIG_READ_ONLY*Determines if the form field can be edited.Booleanyes
CONFIG_REQUIREDDetermines if the form field is required.Booleanyes
CONFIG_SELECT_VALUESThe possible options of form fields which use SelectValues.List<SelectValue>no
VALUE*The current value of the field.IValueyes
* These attributes are not changed when the editor is in read-only mode.

Some of these attributes are global and are applied to every type of form field. The other attributes are only applied to form fields which support them. The configuration attribute for displaying a character counter is only used by text fields. Changes to select values are only applied to form fields which use select values. For configuration parameters which can be changed by a script refer to Existing Input Field Types. Parameters which can be changed by a script are marked with a foot note.

Example Form Field Change Script

The following script changes the label of the input field for the property "example:title", makes the property "example:favorite" read only, sets the maximum height of a fixed size text field and changes the value of the short text. In a real script these changes will only be applied in defined conditions.

import static com.subshell.sophora.api.scripting.formfieldchange.FormFieldAttribute.*
 
def exampleScript = { document, validationScriptDocumentManager ->
	def changes = []
	changes.add(CONFIG_LABEL.changeTo("example:title", "Title changed by script"))
	changes.add(CONFIG_READ_ONLY.changeTo("example:favorite", true))
	changes.add(CONFIG_PARAMETERS.changeTo("example:fixedSizeText", ["maxHeightInRows": "13"]))
	changes.add(VALUE.changeTo("example:shorttext", new StringValue("value from script")))
	def selectValues = [ new SelectValue("key1", "label 1", false),
		new SelectValue("key2", "label 2", false),
		new SelectValue("key3", "label 3", false) ]
	changes.add(CONFIG_SELECT_VALUES.changeTo("example:choice", selectValues))
	return changes
} as IFormFieldChangeScript
 
return exampleScript

Scripts for Custom DeskClient Actions

Scripts can be used to extend the Sophora DeskClient. These extensions appear as actions in the toolbar and in the document menu.

Developing Scripts

Sophora scripts can be developed directly in the Sophora DeskClient. However, the included code editor provides only few features easing the development (mainly syntax highlighting). Therefore, most developers choose to use an IDE of their choice to develop Sophora scripts. In this section, we explain an exemplary setup for developing Groovy scripts using the Maven build tool.

Setting up the development environment

First you need to create a Maven project (or module) for your scripts. As dependencies vary by the type of scripts (state change scripts, validation script, DeskClient scripts, ...), we suggest creating a Maven module within this parent project for each type.
Next, set up the dependencies for your modules. For developing Groovy scripts, add:

<dependency>
	<groupId>org.codehaus.groovy</groupId>
	<artifactId>groovy-all</artifactId>
	<version>2.4.1</version>
	<scope>provided</scope>
</dependency>

Different types of scripts require different further dependencies (all with <groupId>com.subshell.sophora</groupId>):

Dependencies for Sophora Scripts
Script TypeDependency (<artifactId>)
IScriptDocumentChangeListenercom.subshell.sophora.api
IEventScriptcom.subshell.sophora.api
ITimingActionScriptcom.subshell.sophora.api
IValidationScriptcom.subshell.sophora.api
IFormFieldChangeScriptcom.subshell.sophora.api
Custom DeskClient Actionscom.subshell.sophora.client

You may add further dependencies if they are available at runtime for the scripts. For instance, everything from com.subshell.sophora.commons is available for all scripts, and com.subshell.sophora.server is available for scripts that are executed in the server (IScriptDocumentChangeListener, IEventScript, ITimingActionScript).

Implementing and Debugging Scripts

Scripts run directly in the execution environment of the DeskClient or the Sophora server. Thus, they cannot be easily debugged at runtime using breakpoints. At runtime, the only way of debugging is to insert logging outputs and monitor the log files.

Often this is not very efficient, especially for more complex scripts. Thus, scripts are typically developed best using test-driven development (TDD). A test for a script first reads a Sophora document saved as JSON file, converts it to a INode, and passes it to the script under test. You can use com.subshell.sophora.json.content.JsonSophoraDocumentReader from the Maven module com.subshell.sophora.json to read a JSON file.

Converting Scripts to Sophora Documents

Once the development of the script is finished, the script has to be imported to Sophora. To simplify this process, we developed the Sophora Script Maven plugin. It can be included as Maven build plugin (available via the Maven repository software.subshell.com). It automates the conversion of .groovy script files to SophoraXML that can be imported. For more information, see the documentation at Bitbucket.

The Sophora Script Maven plugin is provided as open-source software without warranty of any kind. Especially, we do not provide support for it. Feel free to use the source.