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 Servers. Furthermore, only the latest published version of a script (document) will be executed.
To create a new script,
- Mark the menu item "Scripts" and select "New: Script" from the context menu.
- Within the emerging dialog you may change the script document's location (structure node) or its ID stem.
- Click "Finish" and an empty form opens in the editor area where you have to specify the code.
- Save the Script.
To edit an existing script,
- Expand the menu item "Scripts".
- Double-click the script you want to edit or use "Open" within its context menu.
- Enter your changes.
- 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.retrievalresult.*;
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.search.*;
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.lang3.*;
import org.slf4j.*
Of course you may provide additional import statements to meet your individual requirements.
Execution of Scripts on Sophora Replica 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 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:
Action | Event |
---|---|
Save | NONE |
Publish | PUBLISH |
Delete (Moved to trash) | DELETE |
Delete from trash (Physically deleted) | COMPLETELY_DELETED |
Release | RELEASE |
Set offline | OFFLINE |
Deactivate | DISABLE |
Activate | ENABLE |
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.
Action | Event | |
---|---|---|
Clone document | CLONE | |
Restore document | RESTORE | |
Save version as clone | CLONE_FROM_VERSION | |
Other cause | SAVE |
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
Name | Type | Description |
---|---|---|
lock(UUID uuid) | void | Locks 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) | void | Unlocks the document with the given UUID. |
saveDocument(INode serverNode, String idStem, boolean preserveHistory) | String | Saves 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) | void | Publishes the document with the given UUID. |
publishDocument(UUID uuid, VersionParameters versionParameters) | void | The same as above, but with version parameters as additional metadata for the publication process. Read the documentation of the document lifecyle for more information about version parameters and their usage. See also the Example 4 script in this section. |
release(UUID uuid) | void | Releases the document with the given UUID. |
setOffline(UUID uuid) | void | Sets the document with the given UUID offline. |
delete(UUID uuid) | void | Deletes the document with the given UUID. |
getDocumentBySophoraId(String sophoraId) | INode | Returns the document specified by the given Sophora ID. |
getDocumentUuidBySophoraId(String sophoraId) | String | Returns the UUID of the document specified by the given Sophora ID. |
getDocumentByExternalId(String externalId) | INode | Returns the document with the given external ID. |
getDocumentByUuid(UUID uuid, boolean includeBinaryData, boolean allAsExternalReferences) | INode | Returns the document specified by the given UUID. |
getDocumentSummaryByUuid(UUID uuid) | INode | Returns a summary of the document specified by the given UUID. |
getDocumentSummaryByExternalId(String externalId) | INode | Returns a summary of the document with the given external ID. |
getPublishedDocumentByUuid(UUID uuid) | INode | Returns 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) | NodeType | Returns the NodeType with the given name. |
getBinaryData(UUID uuid, String path) | BinaryData | Returns the BinaryData of the given child node in the given document. |
getStructureNode(UUID structureNodeUuid) | IStructureNode | Returns the structure node with the given UUID or null if the node is not accessible. |
getPublishedStructureNode(UUID structureNodeUuid) | IStructureNode | Returns 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) | IStructureNode | Returns 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) | StructureInfo | Returns the structure info for the structure node with the given UUID. |
getSites() | Set<ISite> | Returns all sites that are not deleted. |
getSystemSite() | ISite | Returns the system site. This is only experimental API and should not be used. |
getSelectValues(String nodeTypeName, String variantName, String propertyName) | SelectValues | Returns the configured select values of the specified property in the given node type. |
getImageVariantByName(String name) | ImageVariant | Returns 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) | UuidSearchResult | Finds 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) | UuidSearchResult | Finds document UUIDs for the specified query and returns them as a UuidSearchResult .This method ignores user permissions. |
saveYellowData(UUID documentUuid, YellowData data) | YellowData | Saves 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) | void | Deletes the yellow data with the given ID in the specified document. |
getChannels() | Set<Channel> | Returns all channels. |
getChannelByName(String channelName) | Channel | Returns the channel with the given name or null if no channel with such a name exists. |
isValidForChannel(String channelName, INode document) | boolean | Returns whether the given document is now valid for the specified channel. |
isValidForChannelOnDate(String channelName, INode document, Date referenceDate) | boolean | Returns whether the given document will be (or was) valid for the specified channel on the specified date. If no date is given, date specific restrictions will not be checked. |
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()
Example 4: Update a property when the Sophora document is published with a certain version parameter
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 "publish"-events are considered
if (event.getStateChange() != DocumentChangedEvent.StateChange.PUBLISH) {
return
}
def manager = event.getDocumentManager()
def document = event.getDocument()
if (hasVersionParameter(event.getVersionParameters(), "specialPublish")) {
def now = Calendar.getInstance()
document.setDate("my-namespace:my-property", now)
manager.saveDocument(document, null, false)
}
}
private static boolean hasVersionParameter(VersionParameters versionParameters, String parameterName) {
versionParameters.getParameters()
.any { it.getParameterName() == parameterName }
}
}
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" structure nodes can be configured with time scheduling parameters "Days until offline" (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 Server.
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.
Method | Description |
---|---|
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. |
getAdditionalIQuery() | May return an additional IQuery that any document to be processed by this script must satisfy or null or new EmptyQuery if not extra filter applies. |
getAdditionalXPath() | May return an additional XPath criteria that any document to be processed by this script must satisfy or null, if no extra criteria needs to be respected. This is DEPRECATED. Use getAdditionalIQuery instead. |
getNodeType() | May return a node type name or null. When set only documents with the corresponding node type are processed by this script. |
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() {
}
IQuery getAdditionalIQuery() {
return new EmptyQuery();
}
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()
}
IQuery getAdditionalIQuery() {
return new EmptyQuery();
}
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
:
Name | Description | Value Type | All Form Fields |
---|---|---|---|
CONFIG_CHAR_COUNTER | Determines if a character counter should be displayed. | Boolean | no |
CONFIG_DESCRIPTION | The description of the form field is normally displayed as tool tip. | String | yes |
CONFIG_LABEL | The label of the form field. | String | yes |
CONFIG_PARAMETERS | The parameters are specific for each field type. | Map<String, String> | no |
CONFIG_READ_ONLY* | Determines if the form field can be edited. | Boolean | yes |
CONFIG_REQUIRED | Determines if the form field is required. | Boolean | yes |
CONFIG_SELECT_VALUES | The possible options of form fields which use SelectValues. | List<SelectValue> | no |
VALUE* | The current value of the field. | IValue | yes |
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>
):
Script Type | Dependency (<artifactId> ) | |
---|---|---|
IScriptDocumentChangeListener | com.subshell.sophora.api | |
IEventScript | com.subshell.sophora.api | |
ITimingActionScript | com.subshell.sophora.api | |
IValidationScript | com.subshell.sophora.api | |
IFormFieldChangeScript | com.subshell.sophora.api | |
Custom DeskClient Actions | com.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.
Monitoring Scripts
The execution of scripts can be monitored using the regular Sophora monitoring facilities. There are different types of metrics available:
- Execution duration of scripts (by type)
- Evaluation time (aka "compilation duration") (by type)
- Number of executions (per script)
- Number of retrieved documents (per script)
- Number of execution errors (overall)
- Number of evaluation failures (overall)