Scripts | Version 4

Script-controlled Sophora Extensions

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

Preface to Scripts in Sophora

Scripts can be added and managed under the menu item "Scripts" within Sophora's administrator view. Sophora supports Groovy as script language. Scripts are only recognised on Sophora Sophora Primary (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 Sophora Replica (Slave) Servers and Sophora Staging Servers

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 Sophora Primary (Master) Server. But in some situations it is necessary to execute scripts additionally in replication or Sophora Staging Servers.

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 sample script Listener from the sample section 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);

Overview of methods provided by IScriptingDocumentManager

NameTypeDescription
lock(UUID uuid)voidLocks the document with the given UUID for the current session. An existing lock of another user will be broken when the system permission "breakLock" is set to true.
unlock(UUID uuid)voidUnlocks the document with the given UUID.
saveDocument(INode serverNode, String idStem, boolean preserveHistory)StringSaves a document. If the document has not been saved yet, the idStem must not be null. In other cases, the idStem can be null to keep the existing ID stem.
publishDocument(UUID uuid)voidPublishes the document with the given UUID.
release(UUID uuid)voidReleases the document with the given UUID.
setOffline(UUID uuid)voidSets the document with the given UUID offline.
delete(UUID uuid)voidDeletes the document with the given UUID.
getDocumentBySophoraId(String sophoraId)INodeReturns the document specified by the given Sophora ID.
getDocumentUuidBySophoraId(String sophoraId)StringReturns the UUID of the document specified by the given Sophora ID.
getDocumentByExternalId(String externalId)INodeReturns the document with the given external ID.
getDocumentByUuid(UUID uuid, boolean includeBinaryData, boolean allAsExternalReferences)INodeReturns the document specified by the given UUID.
getDocumentSummaryByUuid(UUID uuid)INodeReturns a summary of the document specified by the given UUID.
getDocumentSummaryByExternalId(String externalId)INodeReturns a summary of the document with the given external ID.
getPublishedDocumentByUuid(UUID uuid)INodeReturns the last published version of the document with the given UUID, if the document has a current live version. A document has a current live version, if it is published and not deleted or set offline.
getReferencesByUuid(UUID uuid)Set<IReference>Returns a set of references to the document specified by the given UUID.
getNodeType(String nodeTypeName)NodeTypeReturns the NodeType with the given name.
getBinaryData(UUID uuid, String path)BinaryDataReturns the BinaryData of the given child node in the given document.
getStructureNode(UUID structureNodeUuid)IStructureNodeReturns the structure node with the given UUID or null if the node is not accessible.
getPublishedStructureNode(UUID structureNodeUuid)IStructureNodeReturns the latest published version of the structure node with the given UUID.
getChildStructureNodes(UUID structureNodeUuid)List<IStructureNode>Returns all child structure nodes of the structure node with the given UUID, which are not deleted and for which the user has permissions. If the given node is itself not accessible, an empty list is returned.
getStructureHierarchyPath(UUID structureNodeUuid)List<IStructureNode>Returns a hierarchically ordered List of structure nodes from the site to the given structure node. The first item in the list is the site, the last one the structure node itself.
getStructureNodeByPath(String path, UUID rootStructureNodeUuid)IStructureNodeReturns the structure node specified by the given path. The search starts with the structure node specified by rootStructureNodeUuid. If rootStructureNodeUuid is null, the search starts from the sites. Aliases are not considered.
getStructureInfo(UUID structureNodeUuid)StructureInfoReturns the structure info for the structure node with the given UUID.
getSites()Set<ISite>Returns all sites that are not deleted.
getSystemSite()ISiteReturns the system site.
This is only experimental API and should not be used.
getSelectValues(String nodeTypeName, String variantName, String propertyName)SelectValuesReturns the configured select values of the specified property in the given node type.
getImageVariantByName(String name)ImageVariantReturns the image variant specified by the given name.
getConfigurationDocumentProperties()Map<String, List<String>>Returns the configuration property map. The returned map is never null.

The map contains the merged properties from the configuration document and the "sophora.properties". In case a property is present in both sources, the configuration document takes precedence.
findDocumentUuids(IQuery query, SearchParameters searchParameters)UuidSearchResultFinds document UUIDs for the specified query and returns them as a UuidSearchResult.
This method returns only documents for which the user has read permissions.
findDocumentUuids(IQuery query, SearchParameters searchParameters)UuidSearchResultFinds document UUIDs for the specified query and returns them as a UuidSearchResult.
This method ignores user permissions.
saveYellowData(UUID documentUuid, YellowData data)YellowDataSaves the given yellow data for the specified document.
getYellowData(UUID documentUuid, String type)List<YellowData>Returns the yellow data of the given type for the specified document.
deleteYellowData(UUID documentUuid, String dataId)voidDeletes the yellow data with the given ID in the specified document.

Sample Scripts

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 a dash 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.

Sample Script

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 Primary (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.

Methods in ITimingActionScripts

Implementations of timing actions scripts may implement several methods. We propose to implement at least init and either processDocument or processOrIgnoreDocument.

MethodDescription
init(ITimingActionScriptContext)Initializes this script. This method is invoked after this script has been constructed, but before documents are processed.
processOrIgnoreDocument(UUID)Accepts a document and then either processes it or ignores it. In the latter case this document is not included as being part of the batch size that is configured by the property sophora.documentTimingActions.batchLimit. Use this method if the parameters additionalXPath and nodeType are insufficient to specify to which documents this script has to be applied. If ignored is returned then this script should not have made excessive use of the context's content manager.
This method is new in Sophora 4 and by default just calls processDocument(UUID) and returns PROCESSED.
It is not necessary to implement processOrIgnoreDocument and processDocument.
processDocument(UUID)Processes a document. After the resulting documents for a timing action run have been calculated, this method is invoked with each document.
destroy()Destroys this script. This method may be used for custom cleanup. It is also invoked if init() fails to complete.
getReferencePropertyName()Returns the name of the date property in the document that is compared to age configured for the timing action. May return null, sophora:publicationDate will be used as default.
getScriptUserName()Returns the name of the user that the script will use for login.
getAdditionalXPath()May return an additional XPath criteria that any document to be processed by this script must satisfied or null, if no extra criteria needs to be respected.
getNodeType()May return a node type name or null. When set only documents with the corresponding node type are processed by this script.

Sample Timing Action Scripts

Moving documents to another structure node

class MoveScript implements ITimingActionScript {
 
    private IContentManager contentManager
    private SessionToken sessionToken
    private Logger logger
 
    void init(ITimingActionScriptContext context) {
        logger = context.getLogger()
        sessionToken = context.getSessionToken()
        contentManager = context.getContentManager()
    }
 
    void destroy() {
    }
 
    String getAdditionalXPath() {
        return null
    }
 
    String getReferencePropertyName() {
        return "sophora:publicationDate"
    }
 
    String getScriptUserName() {
        return "timingaction"
    }
 
    void processDocument(UUID 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()

This script would write its log statements to the logger com.subshell.sophora.server.application.timingaction.DocumentTimingActionsJob.script.MoveScript.

Set Offline Script

class SetOfflineAfterDaysScript implements ITimingActionScript {
 
    private IContentManager contentManager
    private SessionToken sessionToken
    private Logger logger
 
    void init(ITimingActionScriptContext context) {
        sessionToken = context.getSessionToken()
        contentManager = context.getContentManager()
        logger = context.getLogger()
    }
 
    String getAdditionalXPath() {
        return null
    }
 
    String getReferencePropertyName() {
        return null
    }
 
    String getScriptUserName() {
        return "admin"
    }
 
    void processDocument(UUID 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.

Sample Validation Script

The following example 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()

Sample 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()

Sample 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();

Sample Validation Script validating a field with hyphenations

// sophora.hyphenation is available in client scripts
import com.subshell.sophora.hyphenation.xml.HyphenationXmlProcessor;

(...)
  
  String value = document.getString(TELETEXT_PROPERTY);
  HyphenationXmlProcessor processor = new HyphenationXmlProcessor();
  if (processor.isHyphenationXml(value)) {
    return processor.getLogicalDocument(value).getTextLines() 
      // search for line(s) containing 'foobar' and return validation errors with the line(s)
      .withIndex()
      .findAll {element, index -> element.contains("foobar")}
      .collect {element, index ->
        PropertyValidationError.createInvalidInput(TELETEXT_PROPERTY, "Contains 'foobar' in line: " + (index + 1))}
  }

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

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. This section explains a sample 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.

Last modified on 1/15/21

The content of this page is licensed under the CC BY 4.0 License. Code samples are licensed under the MIT License.

Icon