Remote Beautiful WebForms

What is it?

Remote Beautiful WebForm is an extension package for Script Console that allows you to deploy a Beautiful WebForms powered webform created on Content Server on the Script Console engine.

The main purpose of this extension is to simplify the process of gathering the contribution of users that do not have access to Content Server and synchronize these information back on Content Server. An other quite common scenario, is the off-line usage of Content Server webforms: the possibility of accessing, through a locally deployed Script Console instance, a copy of a Content Server webform, even when a connection with Content Server is not available.

In both the cases the information submitted through the remote webform are stored locally within the Script Console to be later synchronize back towards Content Server.

Extension setup

Installing the remote-webform extension package on a Script Console instance, is a straight forward procedure which consists of just two steps:

  • Run the Script Console master installer and install the Remotable WebForms extension package

  • Copy all the static resources from the Beautiful WebForms Support Module in:

<Script Console Home>\config\img\ansbwebform

Create remote package

Beautiful WebForms deployable packages can be created either programmatically, using the Content Script forms service or manually, through the Beautiful Webforms Studio application.

Using forms.createExPackage API

Content Script forms.createExPackage API can be used to programmatically create a deployable Beautiful WebForms remote package. The API can be used from within a Beautiful WebForms View CLEH script, or from any other Content Script object.

In most of the cases, if used within a stand-alone script, this API is used in conjunction with forms.getFormInfo or forms.listFormData APIs.

Properly initialize the form object

It's important that you keep in mind that when the form object is loaded using the form service it is not initialized. You can either initialize it as part of your script or rely on it's OnLoad CLEH for its proper initialization. Here below an example of how properly initialize the form object:

Minimum initialization required

def formNode= docman.getNodeByPath("Path:to:your:form")
form = formNode.getFormInfo()
forms.addResourceDependencies( form, true, true)

Initialization through the OnLoad script (if any)

def formNode= docman.getNodeByPath("Path:to:your:form")

form = formNode.getFormInfo()
def bwfView = docman.getNode(form.amViewId)
def onLoad = bwfView.childrenFast.find{it.name == "OnLoad"}

if(onLoad){
    docman.runContentScript(onLoad, binding)
}

forms.createExPackage(
                      Form form,   // The form to export
                      String name, // An alpha-numeric identifier for the package to be created
                      String instructions, // The instruction to be displayed to help the user filling in the form
                      String nextUrl, // Where to redirect the user upon submission
                      Date validUpTo, // A date after which the form should no longer be available (can be null)
                      List<String> viewsToExport, // The names of the views you want to export as part of the package (can be null)
                                                  // if null all the views will be exported
                      String pin,                 // An optional pin that can be used to protect the access to the form on the console
                      CSDocument[] arrayOfDocuments // An optional list of documents to be exported as 'attachments' with the package
                     )

Using Beautiful Webforms Studio

Beautiful Webforms Studio which can be found at the following location: Content Script Volume:CSTools:Beautiful WebForm Studio

Among the possibilities offered the studio application can help you leveraging the forms.createExPackage through a simplified visual wizard. The first step is to select Export Remote Form among the available actions.

than you'll be asked for a space on Content Server to be used as the wizard workspace (where objects and content will be created):

finally you will be asked about export configuration parameters

  • Form: the form object to be exported

  • Title: the form's title as it will be displayed on the script console default dashboard

  • Name: the export package name (should be an alpha-numeric value)

  • Description: the form's description as it will be displayed on the script console default dashboard

  • PIN: an optional PIN to be used in order to protect un-authorized access to the form on the console

  • Redirect: an URL where to redirect user's navigation upon submission

  • View: an optional list of views names to be exported

  • Attachment(s): an optional list of documents to be exported

upon submission the export package file will be created in the selected workspace.

How to deploy a Beautiful WebForms remote form package

The Beautiful WebForms remote form package is actually a .zip archive containing all objects necessary to the form (view files, scripts, templates, etc.).

You can manually extract its contents in a new folder inside:

<Script Console Home>\config\scripts\ext\forms\forms

for example:

<Script Console Home>\config\scripts\ext\forms\forms\myform

at this point, you should be able to access the form via the Script Console Dashboard, or via direct URL.

Synchronize form data back to Content Server

Form data submitted on Script Console can be synchronized back to Content Server in different ways which all are based on the same paradigm: the asynchronous exchange of information is based on data files.

Data files can be moved from the Script Console to Content Server no matter which transportation mechanism is used.

In the following paragraphs we will cover the most common scenarios.

Remote data pack files are produced on Script Console and sent over to Content Server

Script Console and Content Server can be isolated

In order to implement this scenario there is no need for the two systems to communicate each other.

In this scenario a local script is executed (or scheduled) on the Script Console in order to collect submitted data and prepare the exchange data files to be sent over Content Server.

The Remotable Beautiful WebForms extension for Script Console comes with several exemplar scripts of this kind that can be found at the following location:

<Script Console Home>\config\scripts\ext\forms

E.g synchLocal.cs

import groovy.json.JsonSlurper
import groovy.io.FileType
import java.util.zip.ZipOutputStream
import java.util.zip.ZipEntry
formsAvailable = []
system = context.getAttribute("system")
formRepository =  system.extensionRepositories.find{
    it.repoHome.name == 'forms'
}
formRepositoryDir = new File(formRepository.getAbsolutePath(), "forms")
formRepositoryDirLocal = new File(formRepository.getAbsolutePath(), "inout")
if(formRepositoryDir && formRepositoryDir.isDirectory()){
    def deleteFile = []
    formRepositoryDirLocal.eachFileRecurse(FileType.FILES){
        if(it.name.endsWith(".amf")){
            File newForm = new File(formRepositoryDir, it.name-'.amf')
            if(!newForm.mkdir()){
                return
            }
            def zipFile = new java.util.zip.ZipFile(it)
            zipFile.entries().each { 
                ins = zipFile.getInputStream(it)
                new File(newForm, it.name) << ins
                ins.close()
            }
            zipFile.close();
            deleteFile << it
        }
    }
    deleteFile.each {
        it.delete()
    }
}
if (params.upload == 'true' && params.selform){
    list =[]
    list.addAll( params.selform)
    toBeDeleted = []
    list.each{ form->
        formRepositoryDir = new File(formRepository.getAbsolutePath(), "data/$form")
        if(formRepositoryDir && formRepositoryDir.isDirectory()){
            formRepositoryDir.eachFileRecurse(FileType.FILES){
                if(it.name == "data.amf"){
                    File dataPack = it.getParentFile()
                    String zipFileName = "${dataPack.name}.rpf"
                    File zipFile = new File(new File(formRepository.getAbsolutePath(), "temp"), zipFileName)                    
                    ZipOutputStream zipOS = new ZipOutputStream(new FileOutputStream(zipFile))
                    zapDir(dataPack.path, zipOS, dataPack.path)
                    zipOS.close()
                    zipFile.renameTo(new File(formRepositoryDirLocal, zipFile.name))
                    toBeDeleted << dataPack
                }
            }
        }
    }
    toBeDeleted.each{
         it.deleteDir()
    }   
}
def static zapDir(String dir2zip, ZipOutputStream zos, String stripDir) {
        File zipDir = new File(dir2zip)
        def dirList = zipDir.list()
        byte[] readBuffer = new byte[2156]
        int bytesIn = 0
        dirList.each {
            File f = new File(zipDir, it)
            if(f.isDirectory())
                zapDir(f.path, zos, stripDir)
            else {
                FileInputStream fis = new FileInputStream(f)
                ZipEntry anEntry = new ZipEntry(f.path.substring(stripDir.length()+1))
                zos.putNextEntry(anEntry)
                while((bytesIn = fis.read(readBuffer)) != -1) {
                    zos.write(readBuffer, 0, bytesIn);
                }
                fis.close();
            }
        }
}
redirect params.nextUrl

If you want to schedule this kind of scripts to be automatically executed by the Script Console you have to configure the job in the cs-console-schedulerConfiguration.xml file, which is a standard Quartz scheduler configuration file. You should find a sample job in there.

Here below a configuration example:

<?xml version="1.0" encoding="UTF-8"?>
<job-scheduling-data
    xmlns="http://www.quartz-scheduler.org/xml/JobSchedulingData"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.quartz-scheduler.org/xml/JobSchedulingData http://www.quartz-scheduler.org/xml/job_scheduling_data_1_8.xsd"
    version="1.8">
    <pre-processing-commands>
        <delete-jobs-in-group>*</delete-jobs-in-group>  <!-- clear all jobs in scheduler -->
        <delete-triggers-in-group>*</delete-triggers-in-group> <!-- clear all triggers in scheduler -->
    </pre-processing-commands>
    <processing-directives>
        <!-- if there are any jobs/trigger in scheduler of same name (as in this 
            file), overwrite them -->
        <overwrite-existing-data>true</overwrite-existing-data>
        <!-- if there are any jobs/trigger in scheduler of same name (as in this 
            file), and over-write is false, ignore them rather then generating an error -->
        <ignore-duplicates>false</ignore-duplicates>
    </processing-directives>
    <schedule>
        <job>
            <name>PollJobSynchronization</name>
            <group>Synchronization</group>
            <job-class>com.answer.modules.cscript.console.scheduler.CommandLauncherJob</job-class>
            <job-data-map>
                <entry>
                    <key>script</key>
                    <value>ext/forms/synchLocal.cs</value>
                </entry>
                <entry>
                    <key>system</key>
                    <value>LOCAL</value>
                </entry>
            </job-data-map>
        </job>
        <trigger>
            <cron>
                <name>LaunchEvery1Minutes</name>
                <group>SynchronizationTriggerGroup</group>
                <job-name>PollJobSynchronization</job-name>
                <job-group>Synchronization</job-group>
                <start-time>2010-02-09T12:26:00.0</start-time>
                <end-time>2020-02-09T12:26:00.0</end-time>
                <misfire-instruction>MISFIRE_INSTRUCTION_SMART_POLICY</misfire-instruction>
                <cron-expression>0 * * ? * *</cron-expression>
                <time-zone>America/Los_Angeles</time-zone>
            </cron>
        </trigger>
    </schedule>
</job-scheduling-data>

Later on Content Server the data files are unpacked using the forms service from within a Content Script that can be either manually executed or scheduled.

E.g.

// remPack is a data pack file, how this file was obtained is not relevant. 
// It may have been fetched from an email folder, a ftp server, a shared folder a cloud service, 
// or even uploaded on Content Server using web-services, etc...
def packList = forms.getExPackageContent( remPack) // returns a Map<String, CSResource>

if(packList."data.amf"){
    def res = packList.find{it.key == "data.amf"}.value 
    def form  = forms.deserializeForm(res.content.getText("UTF-8"))     

    // The form object can be used for various purposes
    // Submitting the data back to Content Server
    forms.submitForm(form)

    // Starting a workflow
    def damageInvestigation = docman.getNodeByPath("Fleet Management:Workflows:Damage Ingestigation Map")

    def inst = forms.startWorkFlow(damageInvestigation, form, "Form", "Damage Ingestigation - Veichle: ${form.number.value} - Employee: ${form.employee.value} " )

    // Seding on a running workflow
    def task = workflow.getWorkFlowTask(form.getAmWorkID(), form.getAmSubWorkID(), form.getAmTaskID())

    forms.updateWorkFlowForm( 
                             task, //The task
                             "Form Name", //The form name
                             form,  //The form object
                             true   // True if the task should be sent on
                            )

}

Form data are submitted directly from Script Console

Script Console and Content Server can't be isolated

In order to implement this scenario the two systems shall be able to communicate each other.

This scenario can be implemented executing or scheduling a script similar to the one reported here below on the Script Console:

import groovy.io.FileType

log.debug("Running Your Form Synch Job")

formsAvailable = []
system = context.get("system")
formRepository =  system.extensionRepositories.find{
    it.repoHome.name == 'forms'
}

//Synch up
formRepositoryDirParent = new File(formRepository.getAbsolutePath(), "data")
def toBeDeleted = []
formRepositoryDirParent.eachFileRecurse(FileType.DIRECTORIES){ formRepositoryDir->

    if(("yourform").equalsIgnoreCase(formRepositoryDir.name)){
        if(formRepositoryDir && formRepositoryDir.isDirectory()){
            formRepositoryDir.eachFileRecurse(FileType.FILES){
                if(it.name == "data.amf"){
                    formObj = forms.deserializeForm(it.text)
                    File dataPack = it.getParentFile()
                    try{
                        forms.submitForm(formObj)
                        toBeDeleted << dataPack
                    }catch(e){
                        log.error("Unable to synch data back to OTCS",e)
                    }
                }
            }
        }
    }
}
toBeDeleted.each{
    it.deleteDir()
}   

If you want to schedule this kind of scripts to be automatically executed by the Script Console you have to configure the job in the cs-console-schedulerConfiguration.xml file, which is a standard Quartz scheduler configuration file. You should find a sample job in there.

Here below a configuration example:

<?xml version="1.0" encoding="UTF-8"?>
<job-scheduling-data
    xmlns="http://www.quartz-scheduler.org/xml/JobSchedulingData"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.quartz-scheduler.org/xml/JobSchedulingData http://www.quartz-scheduler.org/xml/job_scheduling_data_1_8.xsd"
    version="1.8">
    <pre-processing-commands>
        <delete-jobs-in-group>*</delete-jobs-in-group>  <!-- clear all jobs in scheduler -->
        <delete-triggers-in-group>*</delete-triggers-in-group> <!-- clear all triggers in scheduler -->
    </pre-processing-commands>
    <processing-directives>
        <!-- if there are any jobs/trigger in scheduler of same name (as in this 
            file), overwrite them -->
        <overwrite-existing-data>true</overwrite-existing-data>
        <!-- if there are any jobs/trigger in scheduler of same name (as in this 
            file), and over-write is false, ignore them rather then generating an error -->
        <ignore-duplicates>false</ignore-duplicates>
    </processing-directives>
    <schedule>
        <job>
            <name>PollJobSynchronization</name>
            <group>Synchronization</group>
            <job-class>com.answer.modules.cscript.console.scheduler.CommandLauncherJob</job-class>
            <job-data-map>
                <entry>
                    <key>script</key>
                    <value>ext/forms/submitMyFormLocal.cs</value>
                </entry>
                <entry>
                    <key>system</key>
                    <value>LOCAL</value>
                </entry>
            </job-data-map>
        </job>
        <trigger>
            <cron>
                <name>LaunchEvery1Minutes</name>
                <group>SynchronizationTriggerGroup</group>
                <job-name>PollJobSynchronization</job-name>
                <job-group>Synchronization</job-group>
                <start-time>2010-02-09T12:26:00.0</start-time>
                <end-time>2020-02-09T12:26:00.0</end-time>
                <misfire-instruction>MISFIRE_INSTRUCTION_SMART_POLICY</misfire-instruction>
                <cron-expression>0 * * ? * *</cron-expression>
                <time-zone>America/Los_Angeles</time-zone>
            </cron>
        </trigger>
    </schedule>
</job-scheduling-data>