JobScheduler.psm1

<#
.SYNOPSIS
JobScheduler command line interface
 
For further information see
 
    PS > about_JobScheduler
 
If the documentation is not available for your language then consider to use
 
    PS > [System.Threading.Thread]::CurrentThread.CurrentUICulture = 'en-US'
 
#>


# --------------------------------
# Globals with JobScheduler Master
# --------------------------------

# JobScheduler Master Object
[PSObject] $script:js = $null

# CLI operated for a JobScheduler job or monitor
[bool] $script:jsOperations = ( $spooler -and $spooler.id() )

# JobScheduler Master environment
[hashtable] $script:jsEnv = @{}

# Commands that require a local Master instance (Management of Windows Service)
[string[]] $script:jsLocalCommands = @( 'Install-JobSchedulerService', 'Remove-JobSchedulerService', 'Start-JobSchedulerMaster' )

# -------------------------------
# Globals with JobScheduler Agent
# -------------------------------

# JobScheduler Agent Object
[PSObject] $script:jsAgent = $null

# Commands that require a local Agent instance (Management of Windows Service)
[string[]] $script:jsAgentLocalCommands = @( 'Install-JobSchedulerAgentService', 'Remove-JobSchedulerAgentService', 'Start-JobSchedulerAgent' )

# -------------------------------------
# Globals with JobScheduler Web Service
# -------------------------------------

# JobScheduler Web Service Object
[PSObject] $script:jsWebService = $null

# JobScheduler Web Service Request
# Credentials
[System.Management.Automation.PSCredential] $script:jsWebServiceCredential = $null
# Use default credentials of the current user?
[bool] $script:jsWebServiceOptionWebRequestUseDefaultCredentials = $false
# Proxy Credentials
[System.Management.Automation.PSCredential] $script:jsWebServiceProxyCredential = $null
# Use default credentials of the current user?
[bool] $script:jsWebServiceOptionWebRequestProxyUseDefaultCredentials = $true

# --------------------
# Globals with Options
# --------------------

# Options
# Debug Message: responses exceeding the max. output size are stored in temporary files
[int] $script:jsOptionDebugMaxOutputSize = 1000
# Master Web Request: timeout for establishing the connection in ms
[int] $script:jsOptionWebRequestTimeout = 30

# ----------------------------------------------------------------------
# Public Functions
# ----------------------------------------------------------------------

$moduleRoot = Split-Path -Path $MyInvocation.MyCommand.Path
"$moduleRoot\functions\*.ps1" | Resolve-Path | ForEach-Object { . $_.ProviderPath }
Export-ModuleMember -Function "*"

# ----------------------------------------------------------------------
# Public Function Alias Management
# ----------------------------------------------------------------------

function Use-JobSchedulerAlias
{
<#
.SYNOPSIS
This cmdlet creates alias names for JobScheduler cmdlets.
 
.DESCRIPTION
To create alias names this cmdlet has to be dot sourced, i.e. use
 
* . Use-JobSchedulerAlias -Prefix JS: works as expected
* Use-JobSchedulerAlias-Prefix JS: has no effect
 
When using a number of modules from different vendors then naming conflicts might occur
for cmdlets with the same name from different modules.
 
The JobScheduler CLI makes use of the following policy:
 
* All cmdlets use a unique qualifier for the module as e.g. Use-JobSchedulerMaster, Get-JobSchedulerInventory etc.
* Users can use this cmdlet to create a shorthand notation for cmdlet alias names. Two flavors are offered:
** use a shorthand notation as e.g. Use-JSMaster instead of Use-JobSchedulerMaster. This notation is recommended as is suggests fairly unique names.
** use a shorthand notation as e.g. Use-Master instead of Use-JobSchedulerMaster. This notation can conflict with cmdlets of the PowerShell Core, e.g. for Start-Job, Stop-Job
* Users can exclude shorthand notation for specific cmdlets by use of an exclusion list.
 
You can find the resulting aliases by use of the command Get-Command -Module JobScheduler.
 
.PARAMETER Prefix
Specifies the prefix that is used for a shorthand notation, e.g.
 
* with the parameter -Prefix "JS" used this cmdlet creates an alias Use-JSMaster for Use-JobSchedulerMaster
* with the parameter -Prefix being omitted this cmdlet creates an alias Use-Master for Use-JobSchedulerMaster
 
By default aliases are created for both the prefix "JS" and with an empty prefix being assigned which results in the following possible notation:
 
* Use-JobSchedulerMaster
* Use-JSMaster
* Use-Master
 
Default: . UseJobSchedulerAlias -Prefix JS
Default: . UseJobSchedulerAlias -NoDuplicates -ExcludesPrefix JS
 
.PARAMETER Excludes
Specifies a list of resulting alias names that are excluded from alias creation.
 
When omitting the -Prefix parameter then
- at the time of writing - the following alias names would conflict with cmdlet names from the PowerShell Core:
 
* Get-Event
* Get-Job
* Start-Job
* Stop-Job
 
.PARAMETER ExcludesPrefix
Specifies a prefix that is used should a resulting alias be a member of the list of
excluded aliases that is specified with the -Excludes parameter.
 
When used with the -NoDuplicates parameter then this parameter specifies the prefix that is used
for aliases that would conflict with any exsting cmdlets, functions or aliases.
 
.PARAMETER NoDuplicates
This parameters specifies that no alias names should be created that conflict with existing cmdlets, functions or aliases.
 
.EXAMPLE
 . Use-JobSchedulerAlias -Prefix JS
 
Creates aliases for all JobScheduler CLI cmdlets that allow to use, e.g. Use-JSMaster for Use-JobSchedulerMaster
 
.EXAMPLE
 . Use-JobSchedulerAlias -Exclude Get-Job,Start-Job,Stop-Job -ExcludePrefix JS
 
Creates aliases for all JobScheduler CLI cmdlets that allow to use, e.g. Use-Master for Use-JobSchedulerMaster.
This is specified by omitting the -Prefix parameter.
 
For the resulting alias names Get-Job, Start-Job and Stop-Job the alias names
Get-JSJob, Start-JSJob and Stop-JSJob are created by use of the -ExcludePrefix "JS" parameter.
 
.EXAMPLE
 . Use-JobSchedulerAlias -NoDuplicates -ExcludesPrefix JS
 
Creates aliases for all JobScheduler CLI cmdlets that allow to use e.g. Use-Master for Use-JobSchedulerMaster.
Should any alias name conflict with an existing cmdlet, function or alias then the alias will be created with the
prefix specified by the -ExcludesPrefix parameter.
 
The JobScheduler CLI module uses this alias setting by defalt.
.LINK
about_jobscheduler
 
#>

[cmdletbinding()]
param
(
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $Prefix,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string[]] $Excludes,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $ExcludesPrefix,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [switch] $NoDuplicates
)
    Process
    {
        if ( $NoDuplicates )
        {
            $allCommands = Get-Command | Select-Object -Property Name | ForEach-Object { $_.Name }
        }

        $commands = Get-Command -Module JobScheduler -CommandType 'Function'
        foreach( $command in $commands )
        {
            $aliasName = $command.name.Replace( '-JobScheduler', "-$($Prefix)" )

            if ( $Excludes -contains $aliasName )
            {
                continue
            }

            if ( $Excludes -contains $aliasName )
            {
                if ( $ExcludesPrefix )
                {
                    $aliasName = $command.name.Replace( '-JobScheduler', "-$($ExcludesPrefix)" )
                } else {
                    continue
                }
            }

            if ( $NoDuplicates )
            {
                if ( $allCommands -contains $aliasName )
                {
                    if ( $ExcludesPrefix )
                    {
                        $aliasName = $command.name.Replace( '-JobScheduler', "-$($ExcludesPrefix)" )
                    } else {
                        continue
                    }
                }
            }

            Set-Alias -Name $aliasName -Value $command.Name

            switch( $aliasName )
            {
                'Start-JobEditor'    {
                                        Set-Alias -Name "Start-$($Prefix)JOE" -Value $command.Name
                                        break;
                                    }
                'Start-Dashboard'    {
                                        Set-Alias -Name "Start-$($Prefix)JID" -Value $command.Name
                                        break;
                                    }
            }
        }

        Set-Alias -Name Use-JobSchedulerWebService -Value Connect-JobScheduler
        Set-Alias -Name Use-JSWebService -Value Connect-JobScheduler
        Set-Alias -Name Stop-JobSchedulerJob -Value Stop-JobSchedulerTask
        Set-Alias -Name Stop-JSJob -Value Stop-JobSchedulerTask

        Export-ModuleMember -Alias "*"
    }
}

# create alias names to shorten 'JobScheduler' to 'JS'
. Use-JobSchedulerAlias -Prefix JS -Excludes 'Connect-','Disconnect-','Use-JSAlias','Use-Alias'
# create alias names that drop 'JobScheduler' in the name but avoid conflicts with existing alias names
. Use-JobSchedulerAlias -NoDuplicates -ExcludesPrefix JS -Excludes 'Connect-','Disconnect-','Use-JSAlias','Use-Alias'

# ----------------------------------------------------------------------
# Private Functions
# ----------------------------------------------------------------------

function Approve-JobSchedulerCommand( [System.Management.Automation.CommandInfo] $command )
{
    if ( !$jsWebServiceCredential )
    {
        throw "$($command.Name): no valid session, login to the JobScheduler Web Service with the Connect-JobScheduler cmdlet"
    }

    if ( !$SCRIPT:js.Local )
    {
        if ( $SCRIPT:jsLocalCommands -contains $command.Name )
        {
            throw "$($command.Name): cmdlet is available exclusively for local JobScheduler Master. Switch instance with the Use-JobSchedulerMaster cmdlet and specify the -Id or -InstallPath parameter for a local JobScheduler Master"
        }
    }

    if ( !$SCRIPT:js.Url -and !$SCRIPT:jsOperations -and !$SCRIPT:jsWebService.JobSchedulerId )
    {
        if ( $SCRIPT:jsLocalCommands -notcontains $command.Name )
        {
            throw "$($command.Name): cmdlet requires a JobScheduler URL. Switch instance with the Connect-JobScheduler cmdlet and specify the -Url parameter"
        }
    }
}

function Approve-JobSchedulerAgentCommand( [System.Management.Automation.CommandInfo] $command )
{
    if ( !$SCRIPT:jsAgent.Local )
    {
        if ( $SCRIPT:jsAgentLocalCommands -contains $command.Name )
        {
            throw "$($command.Name): cmdlet is available exclusively for local JobScheduler Agent. Switch instance with the Use-JobSchedulerAgent cmdlet and specify the -InstallPath parameter for a local JobScheduler Agent"
        }
    }

    if ( !$SCRIPT:jsAgent.Url -and !$SCRIPT:jsOperations )
    {
        if ( $SCRIPT:jsAgentLocalCommands -notcontains $command.Name )
        {
            throw "$($command.Name): cmdlet requires a JobScheduler Agent URL. Switch instance with the Use-JobSchedulerAgent cmdlet and specify the -Url parameter"
        }
    }
}

function Start-JobSchedulerStopWatch
{
[cmdletbinding(SupportsShouldProcess)]
[OutputType([System.Diagnostics.Stopwatch])]
param
()

    if ( $PSCmdlet.ShouldProcess( 'Stopwatch' ) )
    {
        [System.Diagnostics.Stopwatch]::StartNew()
    }
}

function Trace-JobSchedulerStopWatch( [string] $CommandName, [System.Diagnostics.Stopwatch] $StopWatch )
{
    if ( $StopWatch )
    {
        Write-Verbose ".. $($CommandName): time elapsed: $($StopWatch.Elapsed.TotalMilliseconds) ms"
    }
}

function New-JobSchedulerObject
{
[cmdletbinding(SupportsShouldProcess)]
param
()

    if ( $PSCmdlet.ShouldProcess( 'JS' ) )
    {
        $js = New-Object PSObject
        $jsInstall = New-Object PSObject
        $jsConfig = New-Object PSObject
        $jsService = New-Object PSObject

        $js | Add-Member -Membertype NoteProperty -Name Id -Value ''
        $js | Add-Member -Membertype NoteProperty -Name Url -Value ''
        $js | Add-Member -Membertype NoteProperty -Name ProxyUrl -Value ''
        $js | Add-Member -Membertype NoteProperty -Name Local -Value $false

        $jsInstall | Add-Member -Membertype NoteProperty -Name Directory -Value ''
        $jsInstall | Add-Member -Membertype NoteProperty -Name ExecutableFile -Value ''
        $jsInstall | Add-Member -Membertype NoteProperty -Name Params -Value ''
        $jsInstall | Add-Member -Membertype NoteProperty -Name StartParams -Value ''
        $jsInstall | Add-Member -Membertype NoteProperty -Name ClusterOptions -Value ''
        $jsInstall | Add-Member -Membertype NoteProperty -Name PidFile -Value ''

        $jsConfig | Add-Member -Membertype NoteProperty -Name Directory -Value ''
        $jsConfig | Add-Member -Membertype NoteProperty -Name FactoryIni -Value ''
        $jsConfig | Add-Member -Membertype NoteProperty -Name SosIni -Value ''
        $jsConfig | Add-Member -Membertype NoteProperty -Name SchedulerXml -Value ''

        $jsService | Add-Member -Membertype NoteProperty -Name ServiceName -Value ''
        $jsService | Add-Member -Membertype NoteProperty -Name ServiceDisplayName -Value ''
        $jsService | Add-Member -Membertype NoteProperty -Name ServiceDescription -Value ''

        $js | Add-Member -Membertype NoteProperty -Name Install -Value $jsInstall
        $js | Add-Member -Membertype NoteProperty -Name Config -Value $jsConfig
        $js | Add-Member -Membertype NoteProperty -Name Service -Value $jsService

        $js
    }
}

function New-JobSchedulerStatisticsObject
{
[cmdletbinding(SupportsShouldProcess)]
param
()

    if ( $PSCmdlet.ShouldProcess( 'Statistics' ) )
    {
        $stat = New-Object PSObject

        $stat | Add-Member -Membertype NoteProperty -Name JobsExist -Value 0
        $stat | Add-Member -Membertype NoteProperty -Name JobsPending -Value 0
        $stat | Add-Member -Membertype NoteProperty -Name JobsRunning -Value 0
        $stat | Add-Member -Membertype NoteProperty -Name JobsStopped -Value 0
        $stat | Add-Member -Membertype NoteProperty -Name JobsNeedProcess -Value 0

        $stat | Add-Member -Membertype NoteProperty -Name TasksExist -Value 0
        $stat | Add-Member -Membertype NoteProperty -Name TasksRunning -Value 0
        $stat | Add-Member -Membertype NoteProperty -Name TasksStarting -Value 0

        $stat | Add-Member -Membertype NoteProperty -Name OrdersExist -Value 0
        $stat | Add-Member -Membertype NoteProperty -Name OrdersClustered -Value 0
        $stat | Add-Member -Membertype NoteProperty -Name OrdersStanding -Value 0

        $stat | Add-Member -Membertype NoteProperty -Name SchedulesExist -Value 0
        $stat | Add-Member -Membertype NoteProperty -Name ProcessClassesExist -Value 0
        $stat | Add-Member -Membertype NoteProperty -Name FoldersExist -Value 0
        $stat | Add-Member -Membertype NoteProperty -Name LocksExist -Value 0
        $stat | Add-Member -Membertype NoteProperty -Name MonitorsExist -Value 0

        $stat
    }
}

function New-JobSchedulerJobChainObject
{
[cmdletbinding(SupportsShouldProcess)]
param
()

    if ( $PSCmdlet.ShouldProcess( 'JobChain' ) )
    {
        $jobChain = New-Object PSObject

        $jobChain | Add-Member -Membertype NoteProperty -Name JobChain -Value ''
        $jobChain | Add-Member -Membertype NoteProperty -Name Path -Value ''
        $jobChain | Add-Member -Membertype NoteProperty -Name Directory -Value ''
        $jobChain | Add-Member -Membertype NoteProperty -Name Volatile -Value ''
        $jobChain | Add-Member -Membertype NoteProperty -Name Permanent -Value ''

        $jobChain
    }
}

function New-JobSchedulerOrderObject
{
[cmdletbinding(SupportsShouldProcess)]
param
()

    if ( $PSCmdlet.ShouldProcess( 'Order' ) )
    {
        $order = New-Object PSObject

        $order | Add-Member -Membertype NoteProperty -Name OrderId -Value ''
        $order | Add-Member -Membertype NoteProperty -Name JobChain -Value ''
        $order | Add-Member -Membertype NoteProperty -Name Path -Value ''
        $order | Add-Member -Membertype NoteProperty -Name Directory -Value ''
        $order | Add-Member -Membertype NoteProperty -Name Volatile -Value ''
        $order | Add-Member -Membertype NoteProperty -Name Permanent -Value ''
        $order | Add-Member -Membertype NoteProperty -Name OrderHistory -Value @()

        $order
    }
}

function New-JobSchedulerJobObject
{
[cmdletbinding(SupportsShouldProcess)]
param
()

    if ( $PSCmdlet.ShouldProcess( 'Job' ) )
    {
        $job = New-Object PSObject

        $job | Add-Member -Membertype NoteProperty -Name Job -Value ''
        $job | Add-Member -Membertype NoteProperty -Name Path -Value ''
        $job | Add-Member -Membertype NoteProperty -Name Directory -Value ''
        $job | Add-Member -Membertype NoteProperty -Name Volatile -Value ''
        $job | Add-Member -Membertype NoteProperty -Name Permanent -Value ''
        $job | Add-Member -Membertype NoteProperty -Name Tasks -Value @()
        $job | Add-Member -Membertype NoteProperty -Name TaskHistory -Value @()

        $job
    }
}

function New-JobSchedulerEventObject
{
[cmdletbinding(SupportsShouldProcess)]
param
()

    if ( $PSCmdlet.ShouldProcess( 'Event' ) )
    {
        $jsEvent = New-Object PSObject

        $jsEvent | Add-Member -Membertype NoteProperty -Name EventClass -Value ''
        $jsEvent | Add-Member -Membertype NoteProperty -Name EventId -Value ''
        $jsEvent | Add-Member -Membertype NoteProperty -Name ExitCode -Value 0
        $jsEvent | Add-Member -Membertype NoteProperty -Name Job -Value ''
        $jsEvent | Add-Member -Membertype NoteProperty -Name JobChain -Value ''
        $jsEvent | Add-Member -Membertype NoteProperty -Name Order -Value ''
        # $jsEvent | Add-Member -Membertype NoteProperty -Name MasterUrl -Value ''
        $jsEvent | Add-Member -Membertype NoteProperty -Name ExpirationDate -Value ''
        $jsEvent | Add-Member -Membertype NoteProperty -Name ExpirationCycle -Value ''
        $jsEvent | Add-Member -Membertype NoteProperty -Name ExpirationPeriod -Value ''
        $jsEvent | Add-Member -Membertype NoteProperty -Name Created -Value ''

        $jsEvent
    }
}

function New-JobSchedulerAgenObject
{
[cmdletbinding(SupportsShouldProcess)]
param
()

    if ( $PSCmdlet.ShouldProcess( 'Agent' ) )
    {
        $jsAgent = New-Object PSObject
        $jsAgentInstall = New-Object PSObject
        $jsAgentConfig = New-Object PSObject
        $jsAgentService = New-Object PSObject

        $jsAgent | Add-Member -Membertype NoteProperty -Name Url -Value ''
        $jsAgent | Add-Member -Membertype NoteProperty -Name ProxyUrl -Value ''
        $jsAgent | Add-Member -Membertype NoteProperty -Name Local -Value $false

        $jsAgentInstall | Add-Member -Membertype NoteProperty -Name Directory -Value ''
        $jsAgentInstall | Add-Member -Membertype NoteProperty -Name ExecutableFile -Value ''
        $jsAgentInstall | Add-Member -Membertype NoteProperty -Name Params -Value ''
        $jsAgentInstall | Add-Member -Membertype NoteProperty -Name StartParams -Value ''
        $jsAgentInstall | Add-Member -Membertype NoteProperty -Name HttpPort -Value ''
        $jsAgentInstall | Add-Member -Membertype NoteProperty -Name HttpsPort -Value ''
        $jsAgentInstall | Add-Member -Membertype NoteProperty -Name LogDirectory -Value ''
        $jsAgentInstall | Add-Member -Membertype NoteProperty -Name PidFileDirectory -Value ''
        $jsAgentInstall | Add-Member -Membertype NoteProperty -Name WorkingDirectory -Value ''
        $jsAgentInstall | Add-Member -Membertype NoteProperty -Name KillScript -Value ''
        $jsAgentInstall | Add-Member -Membertype NoteProperty -Name InstanceScript -Value ''

        $jsAgentConfig | Add-Member -Membertype NoteProperty -Name Directory -Value ''

        $jsAgentService | Add-Member -Membertype NoteProperty -Name ServiceName -Value ''
        $jsAgentService | Add-Member -Membertype NoteProperty -Name ServiceDisplayName -Value ''
        $jsAgentService | Add-Member -Membertype NoteProperty -Name ServiceDescription -Value ''

        $jsAgent | Add-Member -Membertype NoteProperty -Name Install -Value $jsAgentInstall
        $jsAgent | Add-Member -Membertype NoteProperty -Name Config -Value $jsAgentConfig
        $jsAgent | Add-Member -Membertype NoteProperty -Name Service -Value $jsAgentService

        $jsAgent
    }
}

function New-JobSchedulerWebServiceObject
{
[cmdletbinding(SupportsShouldProcess)]
param
()

    if ( $PSCmdlet.ShouldProcess( 'WebService' ) )
    {
        $jsWebService = New-Object PSObject

        $jsWebService | Add-Member -Membertype NoteProperty -Name Url -Value ''
        $jsWebService | Add-Member -Membertype NoteProperty -Name ProxyUrl -Value ''
        $jsWebService | Add-Member -Membertype NoteProperty -Name Base -Value ''
        $jsWebService | Add-Member -Membertype NoteProperty -Name Timeout -Value $script:jsOptionWebRequestTimeout
        $jsWebService | Add-Member -Membertype NoteProperty -Name SkipCertificateCheck -Value $false
        $jsWebService | Add-Member -Membertype NoteProperty -Name SSLProtocol -Value ''
        $jsWebService | Add-Member -Membertype NoteProperty -Name Certificate -Value ''
        $jsWebService | Add-Member -Membertype NoteProperty -Name JobSchedulerId -Value ''
        $jsWebService | Add-Member -Membertype NoteProperty -Name AccessToken -Value ''
        $jsWebService | Add-Member -Membertype NoteProperty -Name Masters -Value @()

        $jsWebService
    }
}

function isPowerShellVersion( [int] $Major=-1, [int] $Minor=-1, [int] $Patch=-1 )
{
    $rc = $false

    if ( $Major -gt -1 )
    {
        if ( $PSVersionTable.PSVersion.Major -eq $Major )
        {
            if ( $Minor -gt -1 )
            {
                if ( $PSVersionTable.PSVersion.Minor -eq $Minor )
                {
                    if ( $Patch -gt - 1 )
                    {
                        if ( $PSVersionTable.PSVersion.Patch -ge $Patch )
                        {
                            $rc = $true
                        }
                    } else {
                        $rc = $true
                    }
                } elseif ( $PSVersionTable.PSVersion.Minor -gt $Minor ) {
                    $rc = $true
                } else {
                    $rc = $true
                }
            } else {
                $rc = $true
            }
        } elseif ( $PSVersionTable.PSVersion.Major -gt $Major ) {
            $rc = $true
        }
    }

    $rc
}

function Invoke-JobSchedulerWebRequest( [string] $Path, [string] $Body, [string] $ContentType='application/json', [hashtable] $Headers=@{'Accept' = 'application/json'}, [Uri] $Url, [string] $Method='POST' )
{
    if ( $Url )
    {
        $requestUrl = $Url.OriginalString + $Path
    } elseif ( $script:jsWebService.Url.UserInfo )
    {
        $requestUrl = $script:jsWebService.Url.scheme + '://' + $script:jsWebService.Url.UserInfo + '@' + $script:jsWebService.Url.Authority + $script:jsWebService.Base + $Path
    } else {
        $requestUrl = $script:jsWebService.Url.scheme + '://' + $script:jsWebService.Url.Authority + $script:jsWebService.Base + $Path
    }

    $requestParams = @{}
    $requestParams.Add( 'Verbose', $false )
    $requestParams.Add( 'Uri', $requestUrl )
    $requestParams.Add( 'Method', $Method )
    $requestParams.Add( 'ContentType', $ContentType )

    $Headers.Add( 'Content-Type', $ContentType )
    $Headers.Add( 'X-Access-Token', $script:jsWebService.AccessToken )
    $requestParams.Add( 'Headers', $Headers )

    if ( isPowerShellVersion 6 )
    {
        $requestParams.Add( 'AllowUnencryptedAuthentication', $true )
    }

    if ( isPowerShellVersion 7 )
    {
        $requestParams.Add( 'SkipHttpErrorCheck', $true )
    }

    if ( $script:jsWebService.Timeout )
    {
        $requestParams.Add( 'TimeoutSec', $script:jsWebService.Timeout )
    }

    if ( $script:jsWebService.SkipCertificateCheck )
    {
        $requestParams.Add( 'SkipCertificateCheck', $true )
    }

    if ( $script:jsWebService.SSLProtocol )
    {
        $requestParams.Add( 'SSLProtocol', $script:jsWebService.SSLProtocol )
    }

    if ( $script:jsWebService.Certificate )
    {
        $requestParams.Add( 'Certificate', $script:jsWebService.Certificate )
    }

    if ( $Body )
    {
        $requestParams.Add( 'Body', $Body )
    }

    try
    {
        Write-Debug ".. $($MyInvocation.MyCommand.Name): sending request to JobScheduler Web Service $($requestUrl)"
        Write-Debug ".... Invoke-WebRequest:"

        $requestParams.Keys | ForEach-Object {
            if ( $_ -eq 'Headers' )
            {
                $item = $_
                $requestParams.Item($_).Keys | ForEach-Object {
                    Write-Debug "...... Header: $_ : $($requestParams.Item($item).Item($_))"
                }
            } else {
                Write-Debug "...... Argument: $_ $($requestParams.Item($_))"
            }
        }

        if ( isPowerShellVersion 7 )
        {
            $response = Invoke-WebRequest @requestParams
        } else {
            try
            {
                $response = Invoke-WebRequest @requestParams
            } catch {
                $response = $_.Exception.Response
            }
        }

        if ( $response -and $response.StatusCode -and $response.Content )
        {
            $response
        } elseif ( $response -and !(isPowerShellVersion 7) ) {
            $response
        } else {
            $message = $response | Format-List -Force | Out-String
            throw $message
        }
    } catch {
        $message = $_.Exception | Format-List -Force | Out-String
        throw $message
    }
}

function Invoke-JobSchedulerWebRequestXmlCommand( [string] $Command, [switch] $IgnoreResponse, [string] $Path='/jobscheduler/commands', [Uri] $Uri, $Method='POST', $ContentType='application/xml', [hashtable] $Headers=@{'Accept' = 'application/xml'} )
{
    $xmlDoc = [xml] $command
    if ($xmlDoc.commands)
    {
        $command = $xmlDoc.commands.innerXml
    }

    # handle XML and JSON requests
    if ( $Command.startsWith( '<' ) )
    {
        if ( $Command -notcontains '<jobscheduler_commands' )
        {
            $Command = "<jobscheduler_commands jobschedulerId='$($script:jsWebService.JobSchedulerId)'>$($Command)</jobscheduler_commands>"
        }

        $ContentType = 'application/xml'
    }

    if ( $Uri )
    {
        $response = Invoke-JobSchedulerWebRequest -Uri $Uri -Method $Methhod -Body $Command -ContentType $ContentType -Headers $Headers
    } else {
        $response = Invoke-JobSchedulerWebRequest -Path $Path -Method $Methhod -Body $Command -ContentType $ContentType -Headers $Headers
    }

    if ( $response.StatusCode -ne 200 )
    {
        throw ( $response | Format-List -Force | Out-String )
    }

    if ( $IgnoreResponse )
    {
        return $response
    }

    if ( $response.Headers.'Content-Type' -eq 'application/xml' )
    {
        try
        {
            $answer = Select-XML -Content $response.Content -Xpath '/spooler/answer'
            if ( !$answer )
            {
                throw 'missing answer element /spooler/answer in response'
            }
        } catch {
            throw 'not a valid JobScheduler XML response: ' + $_.Exception.Message
        }

        $errorText = Select-XML -Content $response.Content -Xpath '/spooler/answer/ERROR/@text'
        if ( $errorText.Node."#text" )
        {
            throw $errorText.Node."#text"
        }

        try
        {
            [xml] $response.Content
        } catch {
            throw ( $_.Exception | Format-List -Force | Out-String )
        }
    } else {
        throw "Web Service response received with unsupported content type: $($response.Headers.'Content-Type')"
    }
}

# return the directory name of a path
function Get-DirectoryName( [string] $path )
{
    if ( $path.LastIndexOf('\') -ge 0 )
    {
        $path = $path.Substring( $path.LastIndexOf('\')+1 )
    } elseif ( $path.LastIndexOf('/') -ge 0 ) {
        $path = $path.Substring( $path.LastIndexOf('/')+1 )
    }

    $path
}

# return the basename of an object
function Get-JobSchedulerObject-Basename( [string] $objectPath )
{
    if ( $objectPath.LastIndexOf('/') -ge 0 )
    {
        $objectPath = $objectPath.Substring( $objectPath.LastIndexOf('/')+1 )
    }

    $objectPath
}

# return the parent folder of an object
function Get-JobSchedulerObject-Parent( [string] $objectPath )
{
    if ( $objectPath.LastIndexOf('/') -ge 0 )
    {
        $objectPath.Substring( 0, $objectPath.LastIndexOf('/') )
    }
}

# return the canonical path of an object, i.e. the full path
function Get-JobSchedulerObject-CanonicalPath( [string] $objectPath )
{
    if ( $objectPath.LastIndexOf('/') -ge 0 )
    {
        $objectPath = $objectPath.Substring( 0, $objectPath.LastIndexOf('/') )
    }

    $objectPath = ([string] $spooler.configuration_directory()) + $objectPath

    $objectPath
}

# execute Windows command script and return environment variables
function Invoke-CommandScript
{
<#
.SYNOPSIS
 
Invoke the specified batch file (and parameters), but also propagate any
environment variable changes back to the PowerShell environment that
called it.
 
#>

param
(
    [Parameter(Mandatory = $true)]
    [string] $Path,
    [string] $ArgumentList
)

    #Set-StrictMode -Version 3

    $tempFile = [IO.Path]::GetTempFileName()

    ## Store the output of cmd.exe. We also ask cmd.exe to output
    ## the environment table after the batch file completes
    ## cmd /c " `"$Path`" $ArgumentList && set > `"$tempFile`" "

    $process = Start-Process -FilePath "cmd.exe" "/c ""`"$Path`" $ArgumentList && set > `"$tempFile`""" " -WindowStyle Hidden -PassThru -Wait
    if ( !$process.ExitCode -eq 0 )
    {
        throw "$($MyInvocation.MyCommand.Name): command script execution failed with exit code: $($process.ExitCode)"
    }

    ## Go through the environment variables in the temp file.
    ## For each of them, set the variable in our local environment.
    Get-Content $tempFile | Foreach-Object {
        if($_ -match "^(.*?)=(.*)$")
        {
# Set-Content "env:\$($matches[1])" $matches[2]
            $script:jsEnv["$($matches[1])"] = $matches[2]
        }
    }

    Remove-Item $tempFile
}

function New-JobSchedulerParamNode
{
[cmdletbinding(SupportsShouldProcess)]
param
(
    [Parameter(Mandatory = $true)]
    [xml] $XmlDoc,
    [string] $Name,
    [string] $Value
)

    if ( $PSCmdlet.ShouldProcess( 'ParamNode' ) )
    {
        $paramNode = $XmlDoc.CreateElement( 'param' )
        $paramNode.SetAttribute( 'name', $Name )
        $paramNode.SetAttribute( 'value', $Value )

        $paramNode
    }
}

function New-JobSchedulerAgentInstanceScript
{
[cmdletbinding(SupportsShouldProcess)]
param
(
    [Parameter(Mandatory = $false)]
    [string] $SchedulerHome,
    [string] $SchedulerData,
    [string] $HttpPort='127.0.0.1:4445',
    [string] $HttpsPort,
    [string] $LogDirectory,
    [string] $PidFileDirectory,
    [string] $WorkingDirectory,
    [string] $KillScript,
    [string] $JavaHome,
    [string] $JavaOptions
)

    $script = "
@echo off
 
rem # -----------------------------------------------------------------------
rem # Company: Software- und Organisations-Service GmbH
rem # Purpose: Instance (service) startscript for JobScheduler Agent
rem # -----------------------------------------------------------------------
 
SETLOCAL
 
rem ### USAGE OF THIS FILE ####################################
rem #
rem # This is a template for the JobScheduler Agent Instance
rem # script.
rem # It can be used as service startscript.
rem #
rem # Each instance of the JobScheduler Agent must have a
rem # different HTTP port. For example if the port 4445
rem # is used for the instance then copy this file
rem #
rem # '.\bin\jobscheduler_agent_instance.cmd-example'
rem # -> '.\bin\jobscheduler_agent_4445.cmd'
rem #
rem # and set the SCHEDULER_HTTP_PORT variable below.
rem #
rem # See also the other environment variables below.
rem #
rem ###########################################################
 
rem ### SETTINGS ##############################################
 
rem # This variable has to point to the installation path of
rem # the JobScheduler Agent.
rem # If this variable not defined then the parent directory
rem # of this startscript is used.
 
"

    if ( $SchedulerHome )
    {
        $script += "
set SCHEDULER_HOME=$($SchedulerHome)
"

    } else {
        $script += "
rem set SCHEDULER_HOME=
"

    }

    $script += "
 
rem # The http port of the JobScheduler Agent can be set here,
rem # as command line option -http-port (see usage) or as
rem # environment variable. Otherwise the above default port
rem # is used.
rem # If only a port is specified then the JobScheduler Agent
rem # listens to all available network interfaces.
rem # It is the same like 0.0.0.0:port.
rem # Use the form <ip address or hostname>:port for indicating
rem # which network interfaces the JobScheduler Agent should
rem # listen to.
rem # The command line option -http-port beats the environment
rem # variable SCHEDULER_HTTP_PORT and the environment variable
rem # SCHEDULER_HTTP_PORT beats the default port from
rem # SCHEDULER_AGENT_DEFAULT_HTTP_PORT(=4445).
rem ### NOTE:
rem # If you start the JobScheduler Agent with the command line
rem # option -http-port then you must enter -http-port for
rem # stop, status, restart too (see usage). It's recommended
rem # to set this environment variable instead.
 
"


    if ( $HttpPort )
    {
        $script += "
set SCHEDULER_HTTP_PORT=$($HttpPort)
"

    } else {
        $script += "
rem set SCHEDULER_HTTP_PORT=
"

    }

    $script += "
 
rem # In addition to the http port a https port of the
rem # JobScheduler Agent can be set here, as command line option
rem # -https-port (see usage) or as environment variable.
rem # If only a port is specified then the JobScheduler Agent
rem # listens to all available network interfaces.
rem # It is the same like 0.0.0.0:port.
rem # Use the form <ip address or hostname>:port for indicating
rem # which network interfaces the JobScheduler Agent should
rem # listen to.
rem # The command line option -https-port beats the environment
rem # variable SCHEDULER_HTTPS_PORT.
 
"


    if ( $HttpsPort )
    {
        $script += "
set SCHEDULER_HTTPS_PORT=$($HttpsPort)
"

    } else {
        $script += "
rem set SCHEDULER_HTTPS_PORT=
"

    }

    $script += "
 
rem # Set the directory where the JobScheduler Agent has the
rem # configuration, logs, etc.
rem # This directory must be unique for each instance of the
rem # JobScheduler Agent. The default is
rem # SCHEDULER_HOME\var_SCHEDULER_HTTP_PORT
rem # Make sure that the JobScheduler Agent user has read/write
rem # permissions
 
"


    if ( $SchedulerData )
    {
        $script += "
set SCHEDULER_DATA=$($SchedulerData)
"

    } else {
        $script += "
rem set SCHEDULER_DATA=
"

    }

    $script += "
 
rem # Set the directory where the JobScheduler Agent log file
rem # is created. The default is SCHEDULER_DATA\logs
rem ### NOTE:
rem # Make sure that the JobScheduler Agent user has write
rem # permissions
 
"


    if ( $LogDirectory )
    {
        $script += "
set SCHEDULER_LOG_DIR=$($LogDirectory)
"

    } else {
        $script += "
rem set SCHEDULER_LOG_DIR=
"

    }

    $script += "
 
rem # Set the directory where the JobScheduler Agent pid file
rem # is created. The default is SCHEDULER_LOG_DIR
rem ### NOTE:
rem # Make sure that the JobScheduler Agent user has write
rem # permissions
 
"


    if ( $PidFileDirectory )
    {
        $script += "
set SCHEDULER_PID_FILE_DIR=$($PidFileDirectory)
"

    } else {
        $script += "
rem set SCHEDULER_PID_FILE_DIR=
"

    }

    $script += "
 
rem # The working directory of the JobScheduler Agent is
rem # SCHEDULER_HOME. Here you can set a different working
rem # directory (e.g. %USERPROFILE%).
 
"


    if ( $WorkingDirectory )
    {
        $script += "
set SCHEDULER_WORK_DIR=$($WorkingDirectory)
"

    } else {
        $script += "
rem set SCHEDULER_WORK_DIR=
"

    }

    $script += "
 
rem # Set the location of a script which is called by the
rem # JobScheduler Agent to kill a process and it's children.
 
"


    if ( $KillScript )
    {
        $script += "
set SCHEDULER_KILL_SCRIPT=$($KillScript)
"

    } else {
        $script += "
rem set SCHEDULER_KILL_SCRIPT=
"

    }

    $script += "
 
rem # Actually JAVA_HOME is already set. If you want to use
rem # another Java environment then you can set it here. If
rem # no JAVA_HOME is set then the Java from the path is used.
 
"


    if ( $JavaHome )
    {
        $script += "
set JAVA_HOME=$($JavaHome)
"

    } else {
        $script += "
rem set JAVA_HOME=
"

    }

    $script += "
 
rem # With Java 1.8 the initial memory allocation has changed,
rem # for details see https://kb.sos-berlin.com/x/aIC9
rem # As a result on start-up of the JobScheduler Agent an
rem # excessive amount of virtual memory is assigned by Java.
rem # The environment variable JAVA_OPTIONS can use to apply
rem # memory settings such as '-Xms100m' (default).
 
"


    if ( $JavaOptions )
    {
        $script += "
set JAVA_OPTIONS=$($JavaOptions)
"

    } else {
        $script += "
rem set JAVA_OPTIONS=
"

    }

    $script += '
 
rem ###########################################################
 
if not defined SCHEDULER_HOME set SCHEDULER_HOME=%~dp0..
 
"%SCHEDULER_HOME%\bin\jobscheduler_agent.cmd" %*
 
ENDLOCAL
'

    if ( $PSCmdlet.ShouldProcess( 'Script' ) )
    {
        $script
    }
}

function Get-JobSchedulerJobConfiguration( [string] $Job, [string] $Mime='XML' )
{
    $body = New-Object PSObject
    Add-Member -Membertype NoteProperty -Name 'jobschedulerId' -value $script:jsWebService.JobSchedulerId -InputObject $body
    Add-Member -Membertype NoteProperty -Name 'mime' -value $Mime -InputObject $body
    Add-Member -Membertype NoteProperty -Name 'job' -value $Job -InputObject $body

    [string] $requestBody = $body | ConvertTo-Json -Depth 100
    $response = Invoke-JobSchedulerWebRequest -Path '/job/configuration' -Body $requestBody

    if ( $response.StatusCode -eq 200 )
    {
        $configuration = ( $response.Content | ConvertFrom-JSON ).configuration.content.xml
    } else {
        throw ( $response | Format-List -Force | Out-String )
    }

    $configuration
}

function Get-JobSchedulerJobChainConfiguration( [string] $JobChain, [string] $Mime='XML' )
{
    $body = New-Object PSObject
    Add-Member -Membertype NoteProperty -Name 'jobschedulerId' -value $script:jsWebService.JobSchedulerId -InputObject $body
    Add-Member -Membertype NoteProperty -Name 'mime' -value $Mime -InputObject $body
    Add-Member -Membertype NoteProperty -Name 'jobChain' -value $JobChain -InputObject $body

    [string] $requestBody = $body | ConvertTo-Json -Depth 100
    $response = Invoke-JobSchedulerWebRequest -Path '/job_chain/configuration' -Body $requestBody

    if ( $response.StatusCode -eq 200 )
    {
        $configuration = ( $response.Content | ConvertFrom-JSON ).configuration.content.xml
    } else {
        throw ( $response | Format-List -Force | Out-String )
    }

    $configuration
}

function Get-JobSchedulerOrderConfiguration( [string] $OrderId, [string] $JobChain, [string] $Mime='XML' )
{
    $body = New-Object PSObject
    Add-Member -Membertype NoteProperty -Name 'jobschedulerId' -value $script:jsWebService.JobSchedulerId -InputObject $body
    Add-Member -Membertype NoteProperty -Name 'mime' -value $Mime -InputObject $body
    Add-Member -Membertype NoteProperty -Name 'orderId' -value $OrderId -InputObject $body
    Add-Member -Membertype NoteProperty -Name 'jobChain' -value $JobChain -InputObject $body

    [string] $requestBody = $body | ConvertTo-Json -Depth 100
    $response = Invoke-JobSchedulerWebRequest -Path '/order/configuration' -Body $requestBody

    if ( $response.StatusCode -eq 200 )
    {
        $configuration = ( $response.Content | ConvertFrom-JSON ).configuration.content.xml
    } else {
        throw ( $response | Format-List -Force | Out-String )
    }

    $configuration
}

function Get-JobSchedulerLockConfiguration( [string] $Lock, [string] $Mime='XML' )
{
    $body = New-Object PSObject
    Add-Member -Membertype NoteProperty -Name 'jobschedulerId' -value $script:jsWebService.JobSchedulerId -InputObject $body
    Add-Member -Membertype NoteProperty -Name 'mime' -value $Mime -InputObject $body
    Add-Member -Membertype NoteProperty -Name 'lock' -value $Lock -InputObject $body

    [string] $requestBody = $body | ConvertTo-Json -Depth 100
    $response = Invoke-JobSchedulerWebRequest -Path '/lock/configuration' -Body $requestBody

    if ( $response.StatusCode -eq 200 )
    {
        $configuration = ( $response.Content | ConvertFrom-JSON ).configuration.content.xml
    } else {
        throw ( $response | Format-List -Force | Out-String )
    }

    $configuration
}

function ConvertFrom-JobSchedulerXmlJobStream
{
[cmdletbinding()]
param
(
    [Parameter(Mandatory=$True,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $Path,
    [Parameter(Mandatory=$True,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $OutputDirectory
# [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
# [string] $BaseFolder,
# [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
# [string] $DefaultAgentName,
# [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
# [string] $ForcedAgentName,
# [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
# [hashtable] $MappedAgentNames
)

    Begin
    {
        $jobStreams = @()
    }

    Process
    {
        $jobStreams += $Path
    }

    End
    {
                if ( $jobFolder -eq '/' )
                {
                    $outputFolder = "$([System.IO.Path]::GetFullPath($OutputDirectory))".Replace( '\', '/' )
                } else {
                    $outputFolder = "$([System.IO.Path]::GetFullPath($OutputDirectory))$($jobFolder)".Replace( '\', '/' )
                }

                if ( !(Test-Path -Path $outputFolder) )
                {
                    New-Item -Path $outputFolder -ItemType Directory | Out-Null
                }

                Write-Debug ".... writing job stream: $($outputFolder)/$($jobName).jobstream.json"

                $jsonJobStream = $objJobSream | ConvertTo-Json -Depth 100
                $jsonJobStream | Out-File "$($outputFolder)/$($jobName).jobstream.json"
    }
}

function ConvertFrom-JobSchedulerXmlJob
{
[cmdletbinding()]
param
(
    [Parameter(Mandatory=$True,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $Path,
    [Parameter(Mandatory=$True,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $OutputDirectory,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $BaseFolder,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $DefaultAgentName,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $ForcedAgentName,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [hashtable] $MappedAgentNames,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [switch] $PrefixOrders,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [switch] $SubmitOrders,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [switch] $PlanOrders
)

    Begin
    {
        $jobs = @()
        Write-Debug "prefixOrders: $PrefixOrders"
    }

    Process
    {
        $jobs += $Path
    }

    End
    {
        foreach( $job in $jobs )
        {
            try
            {
                Write-Verbose ".... processing job: $job"

                $jobName = Get-NormalizedObjectName -ObjectName ([System.IO.Path]::GetFileName( $job ))
                $jobFolder = Get-NormalizedObjectName -ObjectName ([System.IO.Path]::GetDirectoryName( $job ).Replace( '\', '/' ))
                $objectFolder = $jobFolder

                if ( $BaseFolder )
                {
                    $jobFolder = "$($BaseFolder)$($jobFolder)"
                }

                [xml] $xmlJob = Get-JobSchedulerJobConfiguration -Job $job

# ---------- Begin: Create Workflow ----------
                $workflowInstructions = @()
                $objWorkflowJobs = New-Object PSObject

    # ---------- Begin: Create Workflow Instruction for job ----------
                $objWorkflowInstruction = New-Object PSObject
                Add-Member -Membertype NoteProperty -Name 'TYPE' -value "Execute.Named" -InputObject $objWorkflowInstruction
                Add-Member -Membertype NoteProperty -Name 'jobName' -value $jobName -InputObject $objWorkflowInstruction
                Add-Member -Membertype NoteProperty -Name 'label' -value $jobName -InputObject $objWorkflowInstruction

                $objWorkflowDefaultArguments = New-Object PSObject
                Add-Member -Membertype NoteProperty -Name 'defaultArguments' -value $objWorkflowDefaultArguments -InputObject $objWorkflowInstruction

        # ---------- Begin: Create Workflow Job ----------
                $objWorkflowJobProperties = New-Object PSObject

                if ( $xmlJob.job.title )
                {
                    Add-Member -Membertype NoteProperty -Name 'title' -value $xmlJob.job.title -InputObject $objWorkflowJobProperties
                }

                if ( $ForcedAgentName )
                {
                    $agentId = $ForcedAgentName
                } elseif ( $xmlJob.job.process_class ) {
                    $agentId = (Get-CanonicalObjectPath -ObjectName $xmlJob.job.process_class -ObjectFolder $objectFolder)
                } else {
                    $agentId = $DefaultAgentName
                }

                if ( $MappedAgentNames -and ($item = $MappedAgentNames.Item( $agentId ) ) )
                {
                    $agentId = $item.AgentName
                }

                Add-Member -Membertype NoteProperty -Name 'agentId' -value $agentId -InputObject $objWorkflowJobProperties

                    $objWorkflowJobExecutable = New-Object PSObject
                    Add-Member -Membertype NoteProperty -Name 'TYPE' -value 'ExecutableScript' -InputObject $objWorkflowJobExecutable

                    $scriptCode = Get-JobSchedulerXmlScriptInclude -IncludeNodes $xmlJob.job.script.include -ObjectFolder $objectFolder
                    Add-Member -Membertype NoteProperty -Name 'script' -value ($scriptCode + $xmlJob.job.script."#cdata-section") -InputObject $objWorkflowJobExecutable

                Add-Member -Membertype NoteProperty -Name 'executable' -value $objWorkflowJobExecutable -InputObject $objWorkflowJobProperties

                    $objWorkflowJobReturnCode = New-Object PSObject
                    $returnCodeMeaningSuccess = @( 0 )
                    Add-Member -Membertype NoteProperty -Name 'success' -value $returnCodeMeaningSuccess -InputObject $objWorkflowJobReturnCode

                Add-Member -Membertype NoteProperty -Name 'returnCodeMeaning' -value $objWorkflowJobReturnCode -InputObject $objWorkflowJobProperties

                if ( $xmlJob.job.tasks )
                {
                    Add-Member -Membertype NoteProperty -Name 'taskLimit' -value $xmlJob.job.tasks -InputObject $objWorkflowJobProperties
                } else {
                    Add-Member -Membertype NoteProperty -Name 'taskLimit' -value 1 -InputObject $objWorkflowJobProperties
                }

            # ---------- Begin: Create the Job Arguments ----------
                $objWorkflowJobDefaultArguments = New-Object PSObject

                # ---------- Begin: Create Job Arguments from <job><params><include> elements ----------
                $xmlArguments = Get-JobSchedulerXmlParamsInclude -IncludeNodes $xmlJob.job.params.include -ObjectFolder $objectFolder
                foreach( $argumentNode in $xmlArguments.params.param )
                {
                    Add-Member -Membertype NoteProperty -Name $argumentNode.name -value $argumentNode.value -InputObject $objWorkflowJobDefaultArguments -Force
                }
                # ---------- End: Create Job Arguments from <job><params><include> elements ----------

                # ---------- Begin: Create Job Arguments from <job><params><param> elements ----------
                $argumentNodes = $xmlJob.job.params.param
                foreach( $argumentNode in $argumentNodes )
                {
                    Add-Member -Membertype NoteProperty -Name $argumentNode.name -value $argumentNode.value -InputObject $objWorkflowJobDefaultArguments -Force
                }
                # ---------- Begin: Create Job Arguments from <job><params><param> elements ----------

                Add-Member -Membertype NoteProperty -Name 'defaultArguments' -value $objWorkflowJobDefaultArguments -InputObject $objWorkflowJobProperties
            # ---------- End: Create the Job Arguments ----------

                $workflowInstructions += $objWorkflowInstruction
                Add-Member -Membertype NoteProperty -Name $jobName -value $objWorkflowJobProperties -InputObject $objWorkflowJobs
        # ---------- End: Create Workflow Job ----------

        # ---------- Begin: Create Workflow Retry ----------
                if ( $xmlJob.job.delay_after_error )
                {
                    Write-Debug "...... delay_after_error found"

                    $objTryInstruction = New-Object PSObject
                    Add-Member -Membertype NoteProperty -Name 'instructions' -value $workflowInstructions -InputObject $objTryInstruction

                    $objRetryInstruction = New-Object PSObject
                    Add-Member -Membertype NoteProperty -Name 'TYPE' -value 'Retry' -InputObject $objRetryInstruction

                    $objCatchInstruction = New-Object PSObject
                    Add-Member -Membertype NoteProperty -Name 'instructions' -value @( $objRetryInstruction ) -InputObject $objCatchInstruction

                    $objWorkflowInstruction = New-Object PSObject
                    Add-Member -Membertype NoteProperty -Name 'TYPE' -value 'Try' -InputObject $objWorkflowInstruction
                    Add-Member -Membertype NoteProperty -Name 'try' -value $objTryInstruction -InputObject $objWorkflowInstruction
                    Add-Member -Membertype NoteProperty -Name 'catch' -value $objCatchInstruction -InputObject $objWorkflowInstruction

                    if ( $errorCount = ( $xmlJob.job.delay_after_error | Where-Object -Property 'delay' -match -value 'STOP' ).error_count )
                    {
                        Add-Member -Membertype NoteProperty -Name 'maxTries' -value $errorCount -InputObject $objWorkflowInstruction
                    }

                    $lastDelay = 1
                    $lastErrorCount = 1
                    $errorDelays = @()
                    $errors = ( $xmlJob.job.delay_after_error | Sort-Object -Property @{expression={$_.error_count -as [int]}} )

                    foreach( $error in $errors )
                    {
                        for( $i=$lastErrorCount+1; $i -lt ($error.error_count -as [int]); $i++ )
                        {
                            $errorDelays += $lastDelay
                        }

                        if ( $error.delay -and $error.delay -ne 'STOP' )
                        {
                            if ( ([regex]::Matches($error.delay, ':' )).count -eq 0 )
                            {
                                $error.delay = "00:00:$($error.delay)"
                            } elseif ( ([regex]::Matches($error.delay, ':' )).count -eq 1 ) {
                                $error.delay = "00:$($error.delay):00"
                            }

                            $delay = [TimeSpan]::Parse( $error.delay )
                            $errorDelays += [int] $delay.TotalSeconds
                            $lastErrorCount = ( $error.error_count -as [int] )
                            $lastDelay = [int] $delay.TotalSeconds
                        }
                    }

                    Add-Member -Membertype NoteProperty -Name 'retryDelays' -value $errorDelays -InputObject $objWorkflowInstruction

                    Write-Debug "...... adding instruction for retry"
                    $WorkflowInstructions = @( $objWorkflowInstruction )
                }

    # ---------- End: Create Workflow Instruction for job ----------

                $objWorkflow = New-Object PSObject
                Add-Member -Membertype NoteProperty -Name 'instructions' -value $workflowInstructions -InputObject $objWorkflow
                Add-Member -Membertype NoteProperty -Name 'jobs' -value $objWorkflowJobs -InputObject $objWorkflow
# ---------- End: Create Workflow ----------

                if ( $jobFolder -eq '/' )
                {
                    $outputFolder = "$([System.IO.Path]::GetFullPath($OutputDirectory))".Replace( '\', '/' )
                } else {
                    $outputFolder = "$([System.IO.Path]::GetFullPath($OutputDirectory))$($jobFolder)".Replace( '\', '/' )
                }

                if ( !(Test-Path -Path $outputFolder) )
                {
                    New-Item -Path $outputFolder -ItemType Directory | Out-Null
                }

                Write-Debug ".... writing workflow: $($outputFolder)/$($jobName).workflow.json"

                $jsonWorkflow = $objWorkflow | ConvertTo-Json -Depth 100
                $jsonWorkflow | Out-File "$($outputFolder)/$($jobName).workflow.json"


# ---------- Begin: Create Schedule ----------
                $scheduleName = $jobName
                $scheduleFolder = $jobFolder

                Write-Verbose ".... processing schedule $scheduleName for job: $jobName in folder: $scheduleFolder"
                $objSchedule = New-Object PSObject

                if ( $scheduleFolder -eq '/' )
                {
                    Add-Member -Membertype NoteProperty -Name 'path' -value "$($scheduleName)" -InputObject $objSchedule
                } else {
                    Add-Member -Membertype NoteProperty -Name 'path' -value "$($scheduleFolder)$($scheduleName)" -InputObject $objSchedule
                }

                if ( $jobFolder -eq '/' )
                {
                    Add-Member -Membertype NoteProperty -Name 'workflowPath' -value "$($jobName)" -InputObject $objSchedule
                } else {
                    Add-Member -Membertype NoteProperty -Name 'workflowPath' -value "$($jobFolder)$($jobName)" -InputObject $objSchedule
                }

                if ( $xmlJob.job.title )
                {
                    Add-Member -Membertype NoteProperty -Name 'title' -value $xmlJob.job.title -InputObject $objSchedule
                }

                Add-Member -Membertype NoteProperty -Name 'submitOrderToControllerWhenPlanned' -value ($SubmitOrders -eq $True) -InputObject $objSchedule
                Add-Member -Membertype NoteProperty -Name 'planOrderAutomatically' -value ($PlanOrders -eq $True) -InputObject $objSchedule

                $workingDayCalendars = @()
                $nonWorkingDayCalendars = @()

                if ( $xmlJob.job.run_time.calendars )
                {
                    if ( isPowerShellVersion 6 )
                    {
                        $jsonCalendars = $xmlJob.job.run_time.calendars."#cdata-section" | ConvertFrom-Json -Depth 100
                    } else {
                        $jsonCalendars = $xmlJob.job.run_time.calendars."#cdata-section" | ConvertFrom-Json
                    }

                    if ( $jsonCalendars )
                    {
                        foreach( $calendar in $jsonCalendars.calendars )
                        {
                            foreach( $period in $calendar.periods )
                            {
                                if ( $period.whenHoliday )
                                {
                                    if ( $period.whenHoliday -eq 'suppress' )
                                    {
                                        $period.whenHoliday = 'SUPPRESS'
                                    } elseif ( $period.whenHoliday -eq 'next_non_holiday' ) {
                                        $period.whenHoliday = 'NEXTNONWORKINGDAY'
                                    } elseif ( $period.whenHoliday -eq 'previous_non_holiday' ) {
                                        $period.whenHoliday = 'PREVIOUSNONWORKINGDAY'
                                    } elseif ( $period.whenHoliday -eq 'ignore' ) {
                                        $period.whenHoliday = 'IGNORE'
                                    }
                                }
                            }

                            $objCalendar = New-Object PSObject

                            if ( $BaseFolder )
                            {
                                Add-Member -Membertype NoteProperty -Name 'calendarPath' -value ($BaseFolder + (Get-NormalizedObjectName -ObjectName $calendar.basedOn)) -InputObject $objCalendar
                            } else {
                                Add-Member -Membertype NoteProperty -Name 'calendarPath' -value (Get-NormalizedObjectName -ObjectName $calendar.basedOn) -InputObject $objCalendar
                            }

                            Add-Member -Membertype NoteProperty -Name 'timeZone' -value $xmlOrder.order.run_time.time_zone -InputObject $objCalendar
                            Add-Member -Membertype NoteProperty -Name 'periods' -value $calendar.periods -InputObject $objCalendar
                            Add-Member -Membertype NoteProperty -Name 'includes' -value $calendar.includes -InputObject $objCalendar

                            if ( $calendar.type -eq 'WORKING_DAYS' )
                            {
                                $workingDayCalendars += $objCalendar
                            } else {
                                $nonWorkingDayCalendars += $objCalendar
                            }
                        }
                    }
                }

                Add-Member -Membertype NoteProperty -Name 'calendars' -value $workingDayCalendars -InputObject $objSchedule
                Add-Member -Membertype NoteProperty -Name 'nonWorkingDayCalendars' -value $nonWorkingDayCalendars -InputObject $objSchedule
# ---------- End: Create Schedule ----------

                if ( $scheduleFolder -eq '/' )
                {
                    $outputFolder = "$([System.IO.Path]::GetFullPath($OutputDirectory))".Replace( '\', '/' )
                } else {
                    $outputFolder = "$([System.IO.Path]::GetFullPath($OutputDirectory))$($scheduleFolder)".Replace( '\', '/' )
                }

                if ( !(Test-Path -Path $outputFolder) )
                {
                    New-Item -Path $outputFolder -ItemType Directory | Out-Null
                }

                Write-Debug ".... writing schedule: $($outputFolder)/$($scheduleName).schedule.json"

                $jsonSchedule = $objSchedule | ConvertTo-Json -Depth 100
                $jsonSchedule | Out-File "$($outputFolder)/$($scheduleName).schedule.json"
            } catch {
                $message = $_.Exception | Format-List -Force | Out-String
                Write-Error "could not convert job: $jobName`n$message"
            }
        }
    }
}

function ConvertFrom-JobSchedulerXmlJobChain
{
[cmdletbinding()]
param
(
    [Parameter(Mandatory=$True,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $Path,
    [Parameter(Mandatory=$True,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $OutputDirectory,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $BaseFolder,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $DefaultAgentName,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $ForcedAgentName,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [hashtable] $MappedAgentNames
)

    Begin
    {
        $jobChains = @()
    }

    Process
    {
        $jobChains += $Path
    }

    End
    {
        foreach( $jobChain in $jobChains )
        {
            try
            {
                Write-Verbose ".... processing job chain: $jobChain"

                $jobChainName = Get-NormalizedObjectName -ObjectName ([System.IO.Path]::GetFileName( $jobChain ))
                $jobChainFolder = Get-NormalizedObjectName -ObjectName ([System.IO.Path]::GetDirectoryName( $jobChain ).Replace( '\', '/' ))

                $objectFolder = $jobChainFolder

                if ( $BaseFolder )
                {
                    $jobChainFolder = "$($BaseFolder)$($jobChainFolder)"
                }


                [xml] $xmlJobChain = Get-JobSchedulerJobChainConfiguration -JobChain $jobChain

                $workflowInstructions = @{}
                $workflowJobs = @{}
                $workflowNodes = @{}
                $firstState = $null

                $forkBranchState = $null
                $forkLevel = 0
                $forkStates = @{}
                $forks = @{}

                foreach( $node in $xmlJobChain.job_chain.job_chain_node )
                {
                    if ( !$firstState )
                    {
                        $firstState = $node.state
                    }

                    if ( !$node.job )
                    {
                        $workflowNodes.Add( $node.state, @{ 'type' = 0; 'state' = $node.state } )
                        continue
                    }

                    $job = Get-CanonicalObjectPath -ObjectName $node.job -ObjectFolder $objectFolder
                    $jobName = Get-NormalizedObjectName -ObjectName ([System.IO.Path]::GetFileName( $job ))

                    Write-Debug "...... processing job: $($job) for node state: $($node.state)"

                    [xml] $xmlJob = Get-JobSchedulerJobConfiguration -Job $job
                    $workflowNodes.Add( $node.state, @{ 'state' = $node.state; 'job' = $jobName; 'next_state' = $node.next_state; 'error_state' = $node.error_state; 'on_error' = $node.on_error; 'xmlJob' = $xmlJob } )

# ---------- Begin: initiate fork from Split Job ----------
                    if ( $xmlJob.job.script.java_class -eq 'com.sos.jitl.splitter.JobChainSplitterJSAdapterClass' )
                    {
                        $stateNames = ( $xmlJob.job.params.param | Where-Object -property 'name' -match -value 'state_names' ).value

                        if ( !$stateNames )
                        {
                            throw "parameter 'state_names' missing in split job: $jobName"
                        }

                        $forkLevel++
                        Write-Debug ".... split job found, adding fork branches for level: $forkLevel"

                        $forkBranches = @{}
                        $stateNamesList = $stateNames.split( ';' )

                        for( $i=0; $i -lt $stateNamesList.count; $i++ )
                        {
                            Write-Debug "...... adding fork branch with id branch_$($i+1) and state: $($stateNamesList[$i])"
                            $forkBranches.Add( $stateNamesList[$i], @{ 'id' = "branch_$($i+1)"; 'workflows' = @() } )
                        }

                        $forkBranchState = $stateNamesList[0]

                        $forkStates.Add( $forkLevel, $node.state )
                        $forks.Add( $node.state, $forkBranches )
                        continue
                    }
# ---------- End: initiate fork from Split Job ----------

# ---------- Begin: add fork branches from Join Job ----------
                    if ( $xmlJob.job.script.java_class -eq 'com.sos.jitl.join.JobSchedulerJoinOrdersJSAdapterClass' )
                    {
                        $objWorkflowInstruction = New-Object PSObject
                        Add-Member -Membertype NoteProperty -Name 'TYPE' -value "Fork" -InputObject $objWorkflowInstruction

                        $branches = @()
                        foreach( $forkBranch in $forks[$forkStates[$forkLevel]].getEnumerator() )
                        {
                            Write-Debug "........ fork branch found, id: $($forkBranch.value.id)"
                            $objBranchInstruction = New-Object PSObject
                            Add-Member -Membertype NoteProperty -Name 'id' -value (Get-NormalizedObjectName $forkBranch.value.id) -InputObject $objBranchInstruction

                            $objBranchWorkflowInstructions = New-Object PSObject
                            Add-Member -Membertype NoteProperty -Name 'instructions' -value $forkBranch.value.workflows -InputObject $objBranchWorkflowInstructions

                            Add-Member -Membertype NoteProperty -Name 'workflow' -value $objBranchWorkflowInstructions -InputObject $objBranchInstruction
                            $branches += $objBranchInstruction
                        }

                        Add-Member -Membertype NoteProperty -Name 'branches' -value $branches -InputObject $objWorkflowInstruction

                        Write-Debug "...... join job found, adding workflow instruction for state: $($node.state)"
                        $workflowInstructions.Add( $forkStates[$forkLevel], $objWorkflowInstruction )

                        $forkBranchState = $null
                        $forklevel--
                        continue
                    }
# ---------- End: add fork branches from Join Job ----------

# ---------- Begin: Create the Job Object ----------
                    $objWorkflowJobProperties = New-Object PSObject

                    if ( $xmlJob.job.title )
                    {
                        Add-Member -Membertype NoteProperty -Name 'title' -value $xmlJob.job.title -InputObject $objWorkflowJobProperties
                    }

                    if ( $ForcedAgentName )
                    {
                        $agentId = $ForcedAgentName
                    } elseif ( $xmlJob.job.process_class ) {
                        $agentId = (Get-CanonicalObjectPath -ObjectName $xmlJob.job.process_class -ObjectFolder $objectFolder)
                    } elseif ( $xmlJobChain.job_chain.process_class ) {
                        $agentId = (Get-CanonicalObjectPath -ObjectName $xmlJobChain.job_chain.process_class -ObjectFolder $objectFolder)
                    } else {
                        $agentId = $DefaultAgentName
                    }

                    if ( $MappedAgentNames -and ($item = $MappedAgentNames.Item( $agentId )) )
                    {
                        $agentId = $item.AgentName
                    }

                    Add-Member -Membertype NoteProperty -Name 'agentId' -value $agentId -InputObject $objWorkflowJobProperties

                        $objWorkflowJobExecutable = New-Object PSObject
                        Add-Member -Membertype NoteProperty -Name 'TYPE' -value 'ExecutableScript' -InputObject $objWorkflowJobExecutable

                        $scriptCode = Get-JobSchedulerXmlScriptInclude -IncludeNodes $xmlJob.job.script.include -ObjectFolder $objectFolder
                        Add-Member -Membertype NoteProperty -Name 'script' -value ($scriptCode + $xmlJob.job.script."#cdata-section") -InputObject $objWorkflowJobExecutable

                    Add-Member -Membertype NoteProperty -Name 'executable' -value $objWorkflowJobExecutable -InputObject $objWorkflowJobProperties

                        $objWorkflowJobReturnCode = New-Object PSObject
                        $returnCodeMeaningSuccess = @( 0 )
                        Add-Member -Membertype NoteProperty -Name 'success' -value $returnCodeMeaningSuccess -InputObject $objWorkflowJobReturnCode

                    Add-Member -Membertype NoteProperty -Name 'returnCodeMeaning' -value $objWorkflowJobReturnCode -InputObject $objWorkflowJobProperties

                    if ( $xmlJob.job.tasks )
                    {
                        Add-Member -Membertype NoteProperty -Name 'taskLimit' -value $xmlJob.job.tasks -InputObject $objWorkflowJobProperties
                    } else {
                        Add-Member -Membertype NoteProperty -Name 'taskLimit' -value 1 -InputObject $objWorkflowJobProperties
                    }

    # ---------- Begin: Create Job Arguments ----------
                    $objWorkflowJobDefaultArguments = New-Object PSObject

                    $xmlArguments = Get-JobSchedulerXmlParamsInclude -IncludeNodes $xmlJob.job.params.include -ObjectFolder $objectFolder
                    foreach( $argumentNode in $xmlArguments.params.param )
                    {
                        Add-Member -Membertype NoteProperty -Name $argumentNode.name -value $argumentNode.value -InputObject $objWorkflowJobDefaultArguments -Force
                    }

                    $argumentNodes = $xmlJob.job.params.param
                    foreach( $argumentNode in $argumentNodes )
                    {
                        Add-Member -Membertype NoteProperty -Name $argumentNode.name -value $argumentNode.value -InputObject $objWorkflowJobDefaultArguments -Force
                    }

                    Add-Member -Membertype NoteProperty -Name 'defaultArguments' -value $objWorkflowJobDefaultArguments -InputObject $objWorkflowJobProperties
    # ---------- End: Create Job Arguments ----------


                    if ( !$workflowJobs.Item( $jobName ) )
                    {
                        $workflowJobs.Add( $jobName, $objWorkflowJobProperties )
                    }
# ---------- End: Create the Job Object ----------


# TODO: return codes
# $node.on_return_codes mapped to junctions


# ---------- Begin: Create the Workflow Instruction ----------
                    $objWorkflowInstruction = New-Object PSObject
                    Add-Member -Membertype NoteProperty -Name 'TYPE' -value "Execute.Named" -InputObject $objWorkflowInstruction
                    Add-Member -Membertype NoteProperty -Name 'jobName' -value $jobName -InputObject $objWorkflowInstruction
                    Add-Member -Membertype NoteProperty -Name 'label' -value (Get-NormalizedObjectName $node.state) -InputObject $objWorkflowInstruction

                        # ---------- Begin: Add job chain node arguments ----------
                        $objWorkflowDefaultArguments = New-Object PSObject

                        $masterUrl = (Get-JobSchedulerMasterCluster -Active).url
                        $canonicalPath = Get-CanonicalObjectPath -ObjectName "$($jobChainName).config.xml" -ObjectFolder $objectFolder

                        $response = Invoke-JobSchedulerWebRequest -Url $masterUrl -Path "/jobscheduler/master/api/live$canonicalPath" -Method 'GET' -Headers @{ 'Accept' = 'application/octet-stream' }

                        if ( $response.StatusCode -eq 200 )
                        {
                            [Xml] $xmlConfig = [System.Text.Encoding]::UTF8.GetString( $response.Content )
                        } else {
                            # file might not exist
                            $xmlConfig = $null
                        }

                        if ( $xmlConfig )
                        {
                            $argumentNodes = $xmlConfig | Select-Xml -Xpath "/settings/job_chain/order/process[@state = '$($node.state)']/params/param"
                            foreach( $argumentNode in $argumentNodes.node )
                            {
                                Add-Member -Membertype NoteProperty -Name $argumentNode.name -value $argumentNode.value -InputObject $objWorkflowDefaultArguments -Force
                            }
                        }

                        Add-Member -Membertype NoteProperty -Name 'defaultArguments' -value $objWorkflowDefaultArguments -InputObject $objWorkflowInstruction
                        # ---------- End: Add job chain node arguments ----------

                    # job found for fork branch
                    if ( $forkLevel )
                    {
                        if ( !$forks[$forkStates[$forkLevel]][$node.state] )
                        {
                            Write-Debug ".... adding workflow instruction: forks[$($forkStates[$forkLevel])][$($forkBranchState)].workflows"
                            $forks[$forkStates[$forkLevel]][$forkBranchState].workflows += $objWorkflowInstruction
                        } else {
                            Write-Debug ".... adding workflow instruction: forks[$($forkStates[$forkLevel])][$($node.state)].workflows"
                            $forks[$forkStates[$forkLevel]][$node.state].workflows += $objWorkflowInstruction
                        }
                    } else {
                        Write-Debug ".... adding workflow instruction: $($node.state)"
                        $workflowInstructions.Add( $node.state, $objWorkflowInstruction )
                    }
# ---------- End: Create the Workflow Instruction ----------
                }

                $objWorkflowJobs = New-Object PSObject
                foreach( $workflowJob in $workflowJobs.GetEnumerator() )
                {
                    Add-Member -Membertype NoteProperty -Name $workflowJob.Name -value $workflowJob.Value -InputObject $objWorkflowJobs
                }

                $instructions = New-JobSchedulerInstructionNodes -Node $workflowNodes.Item( $firstState ) -WorkflowNodes $workflowNodes -WorkflowInstructions $workflowInstructions -Forks $forks -Instructions @()

                $objWorkflow = New-Object PSObject
                Add-Member -Membertype NoteProperty -Name 'instructions' -value ([object[]] $instructions) -InputObject $objWorkflow
                Add-Member -Membertype NoteProperty -Name 'jobs' -value $objWorkflowJobs -InputObject $objWorkflow

                if ( $jobChainFolder -eq '/' )
                {
                    $outputFolder = "$([System.IO.Path]::GetFullPath($OutputDirectory))".Replace( '\', '/' )
                } else {
                    $outputFolder = "$([System.IO.Path]::GetFullPath($OutputDirectory))$($jobChainFolder)".Replace( '\', '/' )
                }


                if ( !(Test-Path -Path $outputFolder) )
                {
                    New-Item -Path $outputFolder -ItemType Directory | Out-Null
                }

                Write-Debug ".... writing workflow: $($outputFolder)/$($jobChainName).workflow.json"

                $jsonWorkflow = $objWorkflow | ConvertTo-Json -Depth 100
                $jsonWorkflow | Out-File "$($outputFolder)/$($jobChainName).workflow.json"
            } catch {
                $message = $_.Exception | Format-List -Force | Out-String
                Write-Error "could not convert job chain: $jobChainName`n$message"
            }
        }
    }
}

function Get-JobSchedulerXmlParamsInclude( [System.Xml.XmlElement] $IncludeNodes, [string] $ObjectFolder )
{
    [Xml] $xmlArguments = '<params/>'
    $masterUrl = $null

    foreach( $includeNode in $IncludeNodes )
    {
        if ( !$includeNode.file -and !$includeNode.live_file )
        {
            continue
        }

        if ( !$masterUrl )
        {
            $masterUrl = (Get-JobSchedulerMasterCluster -Active).url
        }

        if ( $includeNode.file )
        {
            $canonicalPath = Get-CanonicalObjectPath -ObjectName $includeNode.file -ObjectFolder $ObjectFolder
        } else {
            $canonicalPath = Get-CanonicalObjectPath -ObjectName $includeNode.live_file -ObjectFolder $ObjectFolder
        }

        $response = Invoke-JobSchedulerWebRequest -Url $masterUrl -Path "/jobscheduler/master/api/live$canonicalPath" -Method 'GET' -Headers @{ 'Accept' = 'application/octet-stream' }

        if ( $response.StatusCode -eq 200 )
        {
            [Xml] $xmlInclude = [System.Text.Encoding]::UTF8.GetString( $response.Content )
        } else {
            throw ( $response | Format-List -Force | Out-String )
        }

        foreach( $argumentNode in $xmlInclude.params.param )
        {
            $xmlParam = $xmlArguments.CreateElement( 'param' )
            $xmlParam.SetAttribute( 'name', $argumentNode.name )
            $xmlParam.SetAttribute( 'value', $argumentNode.value )
            $xmlArguments.SelectSingleNode( '/params' ).AppendChild( $xmlParam )
        }
    }

    $xmlArguments
}

function Get-JobSchedulerXmlScriptInclude( [System.Xml.XmlElement] $IncludeNodes, [string] $ObjectFolder )
{
    $scriptCode = $null
    $masterUrl = $null

    foreach( $includeNode in $IncludeNodes )
    {
        if ( !$includeNode.file -and !$includeNode.live_file )
        {
            continue
        }

        if ( !$masterUrl )
        {
            $masterUrl = (Get-JobSchedulerMasterCluster -Active).url
        }

        if ( $includeNode.file )
        {
            $canonicalPath = Get-CanonicalObjectPath -ObjectName $includeNode.file -ObjectFolder $ObjectFolder
        } else {
            $canonicalPath = Get-CanonicalObjectPath -ObjectName $includeNode.live_file -ObjectFolder $ObjectFolder
        }

        $response = Invoke-JobSchedulerWebRequest -Url $masterUrl -Path "/jobscheduler/master/api/live$canonicalPath" -Method 'GET' -Headers @{ 'Accept' = 'application/octet-stream' }

        if ( $response.StatusCode -eq 200 )
        {
            $scriptCode += [System.Text.Encoding]::UTF8.GetString( $response.Content ) + "`n"
        } else {
            throw ( $response | Format-List -Force | Out-String )
        }
    }

    $scriptCode
}

function New-JobSchedulerInstructionNodes
{
[cmdletbinding(SupportsShouldProcess)]
param
(
    $Node,
    [hashtable] $WorkflowNodes,
    [hashtable] $WorkflowInstructions,
    [hashtable] $Forks,
    [object[]] $Instructions
)

    while ( $Node )
    {
        Write-Debug ".... constructing workflow instructions, state: $($Node.state), on_error: $($Node.on_error), job: $($Node.job)"

        if ( $Node.on_error -eq 'setback' )
        {
            Write-Debug "...... setback found, state: $($Node.state), job: $($Node.job)"

            $objTryInstruction = New-Object PSObject
            Add-Member -Membertype NoteProperty -Name 'instructions' -value @( $WorkflowInstructions.Item( $Node.state ) ) -InputObject $objTryInstruction

            $objRetryInstruction = New-Object PSObject
            Add-Member -Membertype NoteProperty -Name 'TYPE' -value 'Retry' -InputObject $objRetryInstruction

            $objCatchInstruction = New-Object PSObject
            Add-Member -Membertype NoteProperty -Name 'instructions' -value @( $objRetryInstruction ) -InputObject $objCatchInstruction

            $objWorkflowInstruction = New-Object PSObject
            Add-Member -Membertype NoteProperty -Name 'TYPE' -value 'Try' -InputObject $objWorkflowInstruction
            Add-Member -Membertype NoteProperty -Name 'try' -value $objTryInstruction -InputObject $objWorkflowInstruction
            Add-Member -Membertype NoteProperty -Name 'catch' -value $objCatchInstruction -InputObject $objWorkflowInstruction

            if ( $setbackCount = ( $Node.xmlJob.job.delay_order_after_setback | Where-Object -Property 'is_maximum' -match -value 'true' ).setback_count )
            {
                Add-Member -Membertype NoteProperty -Name 'maxTries' -value $setbackCount -InputObject $objWorkflowInstruction
            }

            $lastDelay = 1
            $lastSetbackCount = 1
            $setbackDelays = @()
            $setbacks = ( $Node.xmlJob.job.delay_order_after_setback | Sort-Object -Property @{expression={$_.setback_count -as [int]}} )

            foreach( $setback in $setbacks )
            {
                for( $i=$lastSetbackCount+1; $i -lt ($setback.setback_count -as [int]); $i++ )
                {
                    $setBackDelays += $lastDelay
                }

                if ( $setback.delay )
                {
                    if ( ([regex]::Matches($setback.delay, ':' )).count -eq 0 )
                    {
                        $setback.delay = "00:00:$($setback.delay)"
                    } elseif ( ([regex]::Matches($setback.delay, ':' )).count -eq 1 ) {
                        $setback.delay = "00:$($setback.delay):00"
                    }

                    $delay = [TimeSpan]::Parse( $setback.delay )
                    $setBackDelays += [int] $delay.TotalSeconds
                    $lastSetbackCount = ( $setback.setback_count -as [int] )
                    $lastDelay = [int] $delay.TotalSeconds
                }
            }

            Add-Member -Membertype NoteProperty -Name 'retryDelays' -value $setbackDelays -InputObject $objWorkflowInstruction

            Write-Debug "...... adding instruction for state (0): $($Node.state)"
            $WorkflowInstructions[$Node.state] = $objWorkflowInstruction
        }

        if ( $Node.error_state -and $WorkflowNodes.Item( $Node.error_state ).job )
        {
            Write-Debug "...... error node found, state: $($Node.error_state), job: $($Node.job)"

            $objTryInstruction = New-Object PSObject
            Add-Member -Membertype NoteProperty -Name 'instructions' -value @( $WorkflowInstructions.Item( $Node.state ) ) -InputObject $objTryInstruction

            $catchInstructions = @()

            if ( $WorkflowNodes.Item( $Node.error_state ).job )
            {
                Write-Debug "...... creating catch instruction for state: $($Node.error_state)"
                $catchInstructions += New-JobSchedulerInstructionNodes -Node $WorkflowNodes.Item( $Node.error_state ) -WorkflowNodes $WorkflowNodes -WorkflowInstructions $WorkflowInstructions -Forks $Forks -Instructions @()
            }

            $objCatchInstruction = New-Object PSObject
            Add-Member -Membertype NoteProperty -Name 'instructions' -value $catchInstructions -InputObject $objCatchInstruction

            $objWorkflowInstruction = New-Object PSObject
            Add-Member -Membertype NoteProperty -Name 'TYPE' -value "Try" -InputObject $objWorkflowInstruction
            Add-Member -Membertype NoteProperty -Name 'try' -value $objTryInstruction -InputObject $objWorkflowInstruction
            Add-Member -Membertype NoteProperty -Name 'catch' -value $objCatchInstruction -InputObject $objWorkflowInstruction

            Write-Debug "...... adding instruction for state (1): $($Node.error_state)"
            $Instructions += $objWorkflowInstruction
        } elseif ( $WorkflowInstructions.Item( $Node.state ) ) {
            Write-Debug "...... adding instruction for state (2): $($Node.state)"
            $Instructions += $WorkflowInstructions.Item( $Node.state )
        }

        if ( $Node.next_state -and $Node.next_state -ne $Node.error_state )
        {
            Write-Debug "...... continue with state: $($Node.next_state)"
            $Node = $WorkflowNodes.item( $Node.next_state )

            if ( !$Node.job )
            {
                break
            }
        } else {
            break
        }
    }

    if ( $PSCmdlet.ShouldProcess( 'Instructions' ) )
    {
        $Instructions
    }
}

function ConvertFrom-JobSchedulerXmlOrder
{
[cmdletbinding()]
param
(
    [Parameter(Mandatory=$True,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $OrderId,
    [Parameter(Mandatory=$True,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $JobChain,
    [Parameter(Mandatory=$True,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $OutputDirectory,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $BaseFolder,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [switch] $PrefixOrders,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [switch] $SubmitOrders,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [switch] $PlanOrders
)

    Begin
    {
        $orders = @()
    }

    Process
    {
        $orders += @{'orderId' = $OrderId; 'jobChain' = $JobChain }
    }

    End
    {
        foreach( $order in $orders )
        {
            try
            {
                Write-Verbose ".... processing OrderID: $($order.orderId), Job Chain: $($order.jobChain)"

                if ( $PrefixOrders )
                {
                    $scheduleName = Get-NormalizedObjectName -ObjectName ([System.IO.Path]::GetFileName( $order.jobChain ) + "-$($order.orderId)")
                } else {
                    $scheduleName = Get-NormalizedObjectName -ObjectName $order.orderId
                }

                $scheduleFolder = Get-NormalizedObjectName -ObjectName ([System.IO.Path]::GetDirectoryName( $order.jobChain ).Replace( '\', '/' ))
                $objectFolder = $scheduleFolder

                if ( $BaseFolder )
                {
                    $scheduleFolder = "$($BaseFolder)$($scheduleFolder)"
                }

                [xml] $xmlOrder = Get-JobSchedulerOrderConfiguration -OrderId $order.orderId -JobChain $order.jobChain

                $objSchedule = New-Object PSObject
                Add-Member -Membertype NoteProperty -Name 'path' -value "$($scheduleFolder)/$($scheduleName)" -InputObject $objSchedule

                if ( $BaseFolder )
                {
                    Add-Member -Membertype NoteProperty -Name 'workflowPath' -value ($BaseFolder + (Get-NormalizedObjectName -ObjectName $order.jobChain)) -InputObject $objSchedule
                } else {
                    Add-Member -Membertype NoteProperty -Name 'workflowPath' -value (Get-NormalizedObjectName -ObjectName $order.jobChain) -InputObject $objSchedule
                }

                if ( $xmlOrder.order.title )
                {
                    Add-Member -Membertype NoteProperty -Name 'title' -value $xmlOrder.order.title -InputObject $objSchedule
                }

                Add-Member -Membertype NoteProperty -Name 'submitOrderToControllerWhenPlanned' -value ($SubmitOrders -eq $True) -InputObject $objSchedule
                Add-Member -Membertype NoteProperty -Name 'planOrderAutomatically' -value ($PlanOrders -eq $True) -InputObject $objSchedule

# ---------- Begin: Add order variables ----------
                $variables = @()

                # ---------- Begin: Add order variables from <job_chain>.config.xml ----------
                $masterUrl = (Get-JobSchedulerMasterCluster -Active).url
                $canonicalPath = Get-CanonicalObjectPath -ObjectName "$($order.jobChain).config.xml" -ObjectFolder $objectFolder
                $response = Invoke-JobSchedulerWebRequest -Url $masterUrl -Path "/jobscheduler/master/api/live$canonicalPath" -Method 'GET' -Headers @{ 'Accept' = 'application/octet-stream' }

                if ( $response.StatusCode -eq 200 )
                {
                    [Xml] $xmlConfig = [System.Text.Encoding]::UTF8.GetString( $response.Content )
                } else {
                    # file might not exist
                    $xmlConfig = $null
                }

                if ( $xmlConfig )
                {
                    $argumentNodes = $xmlConfig.settings.job_chain.order.process.params.param
                    foreach( $argumentNode in $argumentNodes.node )
                    {
                        $objVariable = New-Object PSObject
                        Add-Member -Membertype NoteProperty -Name 'name' -value $argumentNode.name -InputObject $objVariable
                        Add-Member -Membertype NoteProperty -Name 'value' -value $argumentNode.value -InputObject $objVariable
                        $variables += $objVariable
                    }
                }
                # ---------- End: Add order variables from <job_chain>.config.xml ----------

                # ---------- Begin: Add order variables from <order><params><include> elements in <order>.order.xml ----------
                $xmlArguments = Get-JobSchedulerXmlParamsInclude -IncludeNodes $xmlOrder.order.params.include -ObjectFolder $objectFolder
                foreach( $argumentNode in $xmlArguments.params.param )
                {
                    $objVariable = New-Object PSObject
                    Add-Member -Membertype NoteProperty -Name 'name' -value $argumentNode.name -InputObject $objVariable
                    Add-Member -Membertype NoteProperty -Name 'value' -value $argumentNode.value -InputObject $objVariable
                    $variables += $objVariable
                }
                # ---------- End: Add order variables from <order><params><include> elements in <order>.order.xlm ----------

                # ---------- End: Add order variables from <order><params><include> elements in <order>.order.xml ----------

                # ---------- Begin: Add order variables from <order><params><params> elements in <order>.order.xml ----------
                $variableNodes = $xmlOrder.order.params.param
                foreach( $variableNode in $variableNodes )
                {
                    $objVariable = New-Object PSObject
                    Add-Member -Membertype NoteProperty -Name 'name' -value $variableNode.name -InputObject $objVariable
                    Add-Member -Membertype NoteProperty -Name 'value' -value $variableNode.value -InputObject $objVariable
                    $variables += $objVariable
                }
                # ---------- End: Add order variables from <order><params><param> elements in <order>.order.xml ----------

                Add-Member -Membertype NoteProperty -Name 'variables' -value $variables -InputObject $objSchedule
# ---------- End: Add order variables ----------

                $workingDayCalendars = @()
                $nonWorkingDayCalendars = @()

                if ( $xmlOrder.order.run_time.calendars )
                {
                    if ( isPowerShellVersion 6 )
                    {
                        $jsonCalendars = $xmlOrder.order.run_time.calendars."#cdata-section" | ConvertFrom-Json -Depth 100
                    } else {
                        $jsonCalendars = $xmlOrder.order.run_time.calendars."#cdata-section" | ConvertFrom-Json
                    }

                    if ( $jsonCalendars )
                    {
                        foreach( $calendar in $jsonCalendars.calendars )
                        {
                            foreach( $period in $calendar.periods )
                            {
                                if ( $period.whenHoliday )
                                {
                                    if ( $period.whenHoliday -eq 'suppress' )
                                    {
                                        $period.whenHoliday = 'SUPPRESS'
                                    } elseif ( $period.whenHoliday -eq 'next_non_holiday' ) {
                                        $period.whenHoliday = 'NEXTNONWORKINGDAY'
                                    } elseif ( $period.whenHoliday -eq 'previous_non_holiday' ) {
                                        $period.whenHoliday = 'PREVIOUSNONWORKINGDAY'
                                    } elseif ( $period.whenHoliday -eq 'ignore' ) {
                                        $period.whenHoliday = 'IGNORE'
                                    }
                                }
                            }

                            $objCalendar = New-Object PSObject

                            if ( $BaseFolder )
                            {
                                Add-Member -Membertype NoteProperty -Name 'calendarPath' -value ($BaseFolder + (Get-NormalizedObjectName -ObjectName $calendar.basedOn)) -InputObject $objCalendar
                            } else {
                                Add-Member -Membertype NoteProperty -Name 'calendarPath' -value (Get-NormalizedObjectName -ObjectName $calendar.basedOn) -InputObject $objCalendar
                            }

                            Add-Member -Membertype NoteProperty -Name 'timeZone' -value $xmlOrder.order.run_time.time_zone -InputObject $objCalendar
                            Add-Member -Membertype NoteProperty -Name 'periods' -value $calendar.periods -InputObject $objCalendar
                            Add-Member -Membertype NoteProperty -Name 'includes' -value $calendar.includes -InputObject $objCalendar

                            if ( $calendar.type -eq 'WORKING_DAYS' )
                            {
                                $workingDayCalendars += $objCalendar
                            } else {
                                $nonWorkingDayCalendars += $objCalendar
                            }
                        }
                    }
                }

                Add-Member -Membertype NoteProperty -Name 'calendars' -value $workingDayCalendars -InputObject $objSchedule
                Add-Member -Membertype NoteProperty -Name 'nonWorkingDayCalendars' -value $nonWorkingDayCalendars -InputObject $objSchedule


                if ( $scheduleFolder -eq '/' )
                {
                    $outputFolder = "$([System.IO.Path]::GetFullPath($OutputDirectory))".Replace( '\', '/' )
                } else {
                    $outputFolder = "$([System.IO.Path]::GetFullPath($OutputDirectory))$($scheduleFolder)".Replace( '\', '/' )
                }

                if ( !(Test-Path -Path $outputFolder) )
                {
                    New-Item -Path $outputFolder -ItemType Directory | Out-Null
                }

                Write-Debug ".... writing schedule: $($outputFolder)/$($scheduleName).schedule.json"

                $jsonSchedule = $objSchedule | ConvertTo-Json -Depth 100
                $jsonSchedule | Out-File "$($outputFolder)/$($scheduleName).schedule.json"
            } catch {
                $message = $_.Exception | Format-List -Force | Out-String
                Write-Error "could not convert order: $scheduleName`n$message"
            }
        }
    }
}

function ConvertFrom-JobSchedulerXmlCalendar
{
[cmdletbinding()]
param
(
    [Parameter(Mandatory=$True,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $Path,
    [Parameter(Mandatory=$True,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $OutputDirectory,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $BaseFolder
)

    Begin
    {
        $calendars = @()
    }

    Process
    {
        $calendars += $Path
    }

    End
    {
        foreach( $calendar in $calendars )
        {
            try
            {
                Write-Verbose ".... processing calendar: $calendar"

                $calendarName = Get-NormalizedObjectName -ObjectName ([System.IO.Path]::GetFileName( $calendar ))
                $calendarFolder = Get-NormalizedObjectName -ObjectName ([System.IO.Path]::GetDirectoryName( $calendar ).Replace( '\', '/' ))

                if ( $BaseFolder )
                {
                    $calendarFolder = "$($BaseFolder)$($calendarFolder)"
                }

                $body = New-Object PSObject
                Add-Member -Membertype NoteProperty -Name 'jobschedulerId' -value $script:jsWebService.JobSchedulerId -InputObject $body
                Add-Member -Membertype NoteProperty -Name 'calendars' -value @( $calendar ) -InputObject $body

                $headers = @{ 'Accept-Encoding' = 'gzip, deflate, br' }

                [string] $requestBody = $body | ConvertTo-Json -Depth 100
                $response = Invoke-JobSchedulerWebRequest -Path '/calendars/export' -Body $requestBody -Headers $headers

                if ( $response.StatusCode -ne 200 )
                {
                    throw ( $response | Format-List -Force | Out-String )
                }

                if ( isPowerShellVersion 6 )
                {
                    $jsonCalendars = ([System.Text.Encoding]::UTF8.GetString( $response.Content )) | ConvertFrom-Json -Depth 100
                } else {
                    $jsonCalendars = ([System.Text.Encoding]::UTF8.GetString( $response.Content )) | ConvertFrom-Json
                }

                $jsonCalendar = $jsonCalendars.calendars[0]

                $objCalendar = New-Object PSObject

                if ( $calendarFolder -eq '/' )
                {
                    Add-Member -Membertype NoteProperty -Name 'path' -value "$calendarFolder$calendarName" -InputObject $objCalendar
                } else {
                    Add-Member -Membertype NoteProperty -Name 'path' -value "$calendarFolder/$calendarName" -InputObject $objCalendar
                }

                if ( $jsonCalendar.type -eq 'WORKING_DAYS' )
                {
                    Add-Member -Membertype NoteProperty -Name 'type' -value 'WORKINGDAYSCALENDAR' -InputObject $objCalendar
                } elseif ( $jsonCalendar.type -eq 'NON_WORKING_DAYS' ) {
                    Add-Member -Membertype NoteProperty -Name 'type' -value 'NONWORKINGDAYSCALENDAR' -InputObject $objCalendar
                }

                if ( $jsonCalendar.title )
                {
                    Add-Member -Membertype NoteProperty -Name 'title' -value $jsonCalendar.title -InputObject $objCalendar
                }

                if ( $jsonCalendar.from )
                {
                    Add-Member -Membertype NoteProperty -Name 'from' -value $jsonCalendar.from -InputObject $objCalendar
                }

                if ( $jsonCalendar.to )
                {
                    Add-Member -Membertype NoteProperty -Name 'to' -value $jsonCalendar.to -InputObject $objCalendar
                }

                if ( $jsonCalendar.includes )
                {
                    Add-Member -Membertype NoteProperty -Name 'includes' -value $jsonCalendar.includes -InputObject $objCalendar
                }


                if ( $calendarFolder -eq '/' )
                {
                    $outputFolder = "$([System.IO.Path]::GetFullPath($OutputDirectory))".Replace( '\', '/' )
                } else {
                    $outputFolder = "$([System.IO.Path]::GetFullPath($OutputDirectory))$($calendarFolder)".Replace( '\', '/' )
                }

                if ( !(Test-Path -Path $outputFolder) )
                {
                    New-Item -Path $outputFolder -ItemType Directory | Out-Null
                }

                Write-Debug ".... writing calendar: $($outputFolder)/$($calendarName).calendar.json"

                $objCalendar | ConvertTo-Json -Depth 100 | Out-File "$($outputFolder)/$($calendarName).calendar.json"
            } catch {
                $message = $_.Exception | Format-List -Force | Out-String
                Write-Error "could not convert calendar: $calendarName`n$message"
            }
        }
    }
}

function ConvertFrom-JobSchedulerXmlLock
{
[cmdletbinding()]
param
(
    [Parameter(Mandatory=$True,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $Path,
    [Parameter(Mandatory=$True,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $OutputDirectory,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $BaseFolder
)

    Begin
    {
        $locks = @()
    }

    Process
    {
        $locks += $Path
    }

    End
    {
        foreach( $lock in $locks )
        {
            try
            {
                Write-Verbose ".... processing lock: $lock"

                $lockName = Get-NormalizedObjectName -ObjectName ([System.IO.Path]::GetFileName( $lock ))
                $lockFolder = Get-NormalizedObjectName -ObjectName ([System.IO.Path]::GetDirectoryName( $lock ).Replace( '\', '/' ))

                if ( $BaseFolder )
                {
                    $lockFolder = "$($BaseFolder)$($lockFolder)"
                }

                [xml] $xmlLock = Get-JobSchedulerLockConfiguration -Lock $lock

                $objLock = New-Object PSObject
                Add-Member -Membertype NoteProperty -Name 'TYPE' -value "Lock" -InputObject $objLock
                # TODO: locks with id instead of path
                Add-Member -Membertype NoteProperty -Name 'path' -value $lock -InputObject $objLock
                Add-Member -Membertype NoteProperty -Name 'id' -value $lock -InputObject $objLock

                if ( $xmlLock.lock.max_non_exclusive )
                {
                    Add-Member -Membertype NoteProperty -Name 'limit' -value $xmlLock.lock.max_non_exclusive -InputObject $objLock
                }

                if ( $lockFolder -eq '/' )
                {
                    $outputFolder = "$([System.IO.Path]::GetFullPath($OutputDirectory))".Replace( '\', '/' )
                } else {
                    $outputFolder = "$([System.IO.Path]::GetFullPath($OutputDirectory))$($lockFolder)".Replace( '\', '/' )
                }

                if ( !(Test-Path -Path $outputFolder) )
                {
                    New-Item -Path $outputFolder -ItemType Directory | Out-Null
                }

                Write-Debug ".... writing lock: $($outputFolder)/$($lockName).lock.json"

                $jsonLock = $objLock | ConvertTo-Json -Depth 100
                $jsonLock | Out-File "$($outputFolder)/$($lockName).lock.json"
            } catch {
                $message = $_.Exception | Format-List -Force | Out-String
                Write-Error "could not convert lock: $lockName`n$message"
            }
        }
    }
}

function ConvertFrom-JobSchedulerXmlAgentCluster
{
[cmdletbinding()]
param
(
    [Parameter(Mandatory=$True,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $Path,
    [Parameter(Mandatory=$True,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $OutputDirectory,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [string] $BaseFolder,
    [Parameter(Mandatory=$False,ValueFromPipeline=$False,ValueFromPipelinebyPropertyName=$True)]
    [hashtable] $MappedAgentNames
)

    Begin
    {
        $agentClusters = @()
    }

    Process
    {
        $agentClusters += $Path
    }

    End
    {
        foreach( $agentCluster in $agentClusters )
        {
            try
            {
                Write-Verbose ".... processing Agent Cluster: $agentCluster"

                $objAgentCluster = Get-JobSchedulerAgentCluster -AgentCluster $agentCluster

                $agentClusterName = Get-NormalizedObjectName -ObjectName ([System.IO.Path]::GetFileName( $objAgentCluster.AgentCluster ))
                $agentClusterFolder = Get-NormalizedObjectName -ObjectName ([System.IO.Path]::GetDirectoryName( $objAgentCluster.AgentCluster ).Replace( '\', '/' ))

                if ( $BaseFolder )
                {
                    $agentClusterFolder = "$($BaseFolder)$($agentClusterFolder)"
                }

                if ( $ForcedAgentName )
                {
                    $agentClusterName = $ForcedAgentName
                    $objAgentCluster.AgentCluster = "$agentClusterFolder/$agentClusterName"
                    $objAgentCluster.Directory = "$agentClusterFolder/$agentClusterName"
                }

                if ( $MappedAgentNames -and ($item = $MappedAgentNames.Item( $objAgentCluster.AgentCluster) ) ) {
                    Add-Member -Membertype NoteProperty -Name 'AgentId' -value $item.AgentId -InputObject $objAgentCluster
                    Add-Member -Membertype NoteProperty -Name 'AgentName' -value $item.AgentName -InputObject $objAgentCluster
                } else {
                    Add-Member -Membertype NoteProperty -Name 'AgentId' -value $agentClusterName -InputObject $objAgentCluster
                    Add-Member -Membertype NoteProperty -Name 'AgentName' -value $agentClusterName -InputObject $objAgentCluster
                }

                if ( $objAgentCluster.Agents.count )
                {
                    Add-Member -Membertype NoteProperty -Name 'Url' -value $objAgentCluster.Agents[0] -InputObject $objAgentCluster
                }

                if ( $agentClusterFolder -eq '/' )
                {
                    $outputFolder = "$([System.IO.Path]::GetFullPath($OutputDirectory))".Replace( '\', '/' )
                } else {
                    $outputFolder = "$([System.IO.Path]::GetFullPath($OutputDirectory))$($agentClusterFolder)".Replace( '\', '/' )
                }

                if ( !(Test-Path -Path $outputFolder) )
                {
                    New-Item -Path $outputFolder -ItemType Directory | Out-Null
                }

                Write-Debug ".... writing Agent Cluster: $($outputFolder)/$($agentClusterName).agentcluster.json"

                $objAgentCluster | ConvertTo-Json -Depth 100 | Out-File "$($outputFolder)/$($agentClusterName).agentcluster.json"
            } catch {
                $message = $_.Exception | Format-List -Force | Out-String
                Write-Error "could not convert Agent Cluster: $agentClusterName`n$message"
            }
        }
    }
}

function Get-CanonicalObjectPath( [string] $ObjectName, [string] $ObjectFolder )
{
    $objectFolders = $ObjectFolder.split( '/' )

    if ( $ObjectName.startsWith( '/' ) )
    {
        # absolute reference, nothing to do
    } elseif ( $ObjectName.startsWith( './' ) ) {
        # relative reference for same location
        $ObjectName = $ObjectName.Substring( 2 )
    } elseif ( $ObjectName.startsWith( '../' ) ) {
        # relative reference
        $ObjectFolder = ''
        $index = 1
        do
        {
            $ObjectFolder = ''
            $ObjectName = $ObjectName.Substring( 3 )
            #$ObjectFolder = "/$($objectFolders[$objectFolders.length - $index])/$($ObjectFolder)"
            for( $i=0; $i -lt $objectFolders.length-$index; $i++ )
            {
                $ObjectFolder += $objectFolders[$i] + '/'
            }
            $index++
        } while ( $ObjectName.startsWith( '../' ) )

        $ObjectName = "$($ObjectFolder)$($ObjectName)"
    } else {
        if ( $ObjectFolder -ne '/' )
        {
            $ObjectName = "$($ObjectFolder)/$($ObjectName)"
        } else {
            $ObjectName = "/$($ObjectName)"
        }
    }

    $ObjectName.Replace( ' ', '' )
}

function Get-NormalizedObjectName( [string] $ObjectName )
{
    $ObjectName.Replace( ' ', '' ).Replace( ':', '_' )
}

# ----------------------------------------------------------------------
# Main
# ----------------------------------------------------------------------

$script:js = New-JobSchedulerObject
$script:jsWebService = New-JobSchedulerWebServiceObject

<#
if ( $script:jsOperations )
{
    # no addtional connection to Master required
    $script:js.Url = "http://$($spooler.hostname()):$($spooler.tcp_port())"
    $script:js.Id = $spooler.id()
    $script:js.Local = $false
    $script:jsWebService.JobSchedulerId = $js.Id
}
#>