Resource/Resource.ps1

#region Copyright & License

# Copyright © 2012 - 2020 François Chabot
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

#endregion

Set-StrictMode -Version Latest

function Get-ResourceItem {
    [CmdletBinding()]
    [OutputType([System.IO.FileInfo[]])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string[]]
        $Name,

        [Parameter(Mandatory = $false)]
        [string]
        $RootPath = $MyInvocation.PSScriptRoot,

        [Parameter(Mandatory = $false)]
        [string[]]
        $Include = @('*.dll', "*.exe")
    )
    process {
        $Name | ForEach-Object -Process {
            $items = Get-ChildItem -Path $RootPath `
                -Filter "$_.*" `
                -Include $Include `
                -File `
                -Recurse
            if ($null -eq $items ) {
                throw "Resource item not found [Path: '$RootPath', Name: '$_', Include = '$($Include -join ", ")']."
            }
            $duplicateItems = $items | Group-Object Name | Where-Object Count -GT 1
            if ($duplicateItems | Test-Any) {
                throw "Ambiguous resource items found ['$($duplicateItems.Name -join "', '")'] matching criteria [Path: '$RootPath', Name: '$_', Include = '$($Include -join ", ")']."
            }
            $items
        }
    }
}

function New-ResourceItem {
    [CmdletBinding(DefaultParameterSetName = 'file-resource')]
    [OutputType([PSCustomObject[]])]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'named-resource')]
        [Parameter(Mandatory = $true, ParameterSetName = 'file-resource')]
        [ValidateNotNullOrEmpty()]
        [string]
        $Resource,

        [Parameter(Mandatory = $true, ParameterSetName = 'named-resource')]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'file-resource')]
        [ValidateScript( { $_ | Test-Path -PathType Leaf } )]
        [psobject[]]
        $Path,

        [Parameter(Mandatory = $false, ParameterSetName = 'named-resource')]
        [Parameter(Mandatory = $false, ParameterSetName = 'file-resource')]
        [ValidateScript( { $_ -is [bool] -or $_ -is [ScriptBlock] })]
        [ValidateNotNullOrEmpty()]
        [psobject]
        $Condition = $true,

        [Parameter(Mandatory = $false, ParameterSetName = 'named-resource')]
        [Parameter(Mandatory = $false, ParameterSetName = 'file-resource')]
        [switch]
        $PassThru,

        [Parameter(DontShow, Mandatory = $false, ParameterSetName = 'named-resource', ValueFromRemainingArguments = $true)]
        [Parameter(DontShow, Mandatory = $false, ParameterSetName = 'file-resource', ValueFromRemainingArguments = $true)]
        [AllowEmptyString()]
        [AllowEmptyCollection()]
        [object[]]
        $UnboundArguments = @()
    )
    Resolve-ActionPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $splattedArguments = ConvertTo-SplattedArguments -UnboundArguments $UnboundArguments
    if (-not($Condition -is [bool]) -or -not($Condition)) { $splattedArguments.Add('Condition', $Condition) }

    $(if ($PSCmdlet.ParameterSetName -eq 'named-resource') { $Name } else { $Path | Resolve-Path | Select-Object -ExpandProperty ProviderPath }) | ForEach-Object -Process {
        $item = New-Object -TypeName PSCustomObject
        if ($PSCmdlet.ParameterSetName -eq 'named-resource') {
            Add-Member -InputObject $item -MemberType NoteProperty -Name Name -Value $_
        } else {
            Add-Member -InputObject $item -MemberType NoteProperty -Name Name -Value (Split-Path -Path $_ -Leaf)
            Add-Member -InputObject $item -MemberType NoteProperty -Name Path -Value $_
        }
        Add-ResourceItemMembers -Item $item -DynamicMembers $splattedArguments
        if ($PassThru) {
            $item
        } else {
            # TODO support $ItemUnicityScope
            # TODO write-verbose no matter the ItemUnicityScope
            # TODO ?? write-error about Item redefinition according to the ItemUnicityScope,
            # unicity => where Path is the unique criterium xor all the properties must be unique
            if ($Manifest.ContainsKey($Resource)) {
                $Manifest.$Resource = @($Manifest.$Resource) + $item
            } else {
                $Manifest.Add($Resource, $item)
            }
        }
    }
}

function New-ResourceManifest {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Type,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        [Parameter(Mandatory = $false)]
        [AllowNull()]
        [string]
        $Description,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Manifest', 'Resource', 'None')]
        [string]
        $ItemUnicityScope = 'Manifest',

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [scriptblock]
        $Build,

        [Parameter(DontShow, Mandatory = $false, ValueFromRemainingArguments = $true)]
        [ValidateNotNullOrEmpty()]
        [object[]]
        $UnboundArguments = @()
    )
    Resolve-ActionPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    $item = New-Object -TypeName PSCustomObject
    Add-Member -InputObject $item -MemberType NoteProperty -Name Type -Value $Type
    Add-Member -InputObject $item -MemberType NoteProperty -Name Name -Value $Name
    Add-Member -InputObject $item -MemberType NoteProperty -Name Description -Value $Description
    Add-ResourceItemMembers -Item $item -DynamicMembers (ConvertTo-SplattedArguments -UnboundArguments $UnboundArguments)

    $manifestBuildScript = [scriptblock] {
        [CmdletBinding()]
        [OutputType([void])]
        param (
            [Parameter(Mandatory = $true)]
            [ValidateNotNullOrEmpty()]
            [hashtable]
            $Manifest
        )
        . $Build
    }

    $manifest = @{ }
    $manifest.Add('Properties', $item)
    & $manifestBuildScript -Manifest $manifest
    $manifest
}

#region helpers

function Add-ResourceItemMembers {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [PSCustomObject]
        $Item,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [AllowEmptyCollection()]
        [hashtable]
        $DynamicMembers,

        [Parameter(Mandatory = $false)]
        [switch]
        $PassThru
    )
    process {
        $DynamicMembers.Keys | ForEach-Object -Process {
            if ($DynamicMembers.$_ -is [ScriptBlock]) {
                # ScriptMethod instead of ScriptProperty to avoid any error to be silenced; see https://stackoverflow.com/a/19777735/1789441
                Add-Member -InputObject $item -MemberType ScriptMethod -Name $_ -Value $DynamicMembers.$_
            } else {
                Add-Member -InputObject $item -MemberType NoteProperty -Name $_ -Value $DynamicMembers.$_
            }
        }
        if ($PassThru) { $Item }
    }
}

function Compare-ResourceItem {
    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [ValidateScript( { $_.GetType().Name -eq 'PSCustomObject' })]
        [PSCustomObject]
        $ReferenceItem,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [PSCustomObject]
        $DifferenceItem
    )
    Resolve-ActionPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    $referenceProperties = @(Get-Member -InputObject $ReferenceItem -MemberType  NoteProperty, ScriptProperty | Select-Object -ExpandProperty Name)
    $differenceProperties = @(Get-Member -InputObject $DifferenceItem -MemberType  NoteProperty, ScriptProperty | Select-Object -ExpandProperty Name)
    $referenceProperties + $differenceProperties | Select-Object -Unique -PipelineVariable key | ForEach-Object -Process {
        if ($referenceProperties.Contains($key) -and !$differenceProperties.Contains($key)) {
            [PSCustomObject]@{Property = $key ; ReferenceValue = $ReferenceItem.$key ; SideIndicator = '<' ; DifferenceValue = $null } | Tee-Object -Variable difference
            Write-Verbose -Message $difference
        } elseif (!$referenceProperties.Contains($key) -and $differenceProperties.Contains($key)) {
            [PSCustomObject]@{Property = $key ; ReferenceValue = $null ; SideIndicator = '>' ; DifferenceValue = $DifferenceItem.$key } | Tee-Object -Variable difference
            Write-Verbose -Message $difference
        } else {
            $referenceValue, $differenceValue = $ReferenceItem.$key, $DifferenceItem.$key
            if ($referenceValue -is [array] -and $differenceValue -is [array]) {
                $arrayDifferences = Compare-Object -ReferenceObject $referenceValue -DifferenceObject $differenceValue
                if ($arrayDifferences | Test-Any) {
                    $uniqueReferenceValues = $arrayDifferences | Where-Object -FilterScript { $_.SideIndicator -eq '<=' } | ForEach-Object -Process { $_.InputObject } | Join-String -Separator ", "
                    $uniqueDifferenceValues = $arrayDifferences | Where-Object -FilterScript { $_.SideIndicator -eq '=>' } | ForEach-Object -Process { $_.InputObject } | Join-String -Separator ", "
                    [PSCustomObject]@{Property = $key ; ReferenceValue = "($uniqueReferenceValues)" ; SideIndicator = '<>' ; DifferenceValue = "($uniqueDifferenceValues)" } | Tee-Object -Variable difference
                    Write-Verbose -Message $difference
                }
            } elseif ($referenceValue -is [hashtable] -and $differenceValue -is [hashtable]) {
                Compare-HashTable -ReferenceHashTable $referenceValue -DifferenceHashTable $differenceValue -Prefix "$Key"
            } elseif ($referenceValue -ne $differenceValue) {
                [PSCustomObject]@{Property = $key ; ReferenceValue = $referenceValue ; SideIndicator = '<>' ; DifferenceValue = $differenceValue } | Tee-Object -Variable difference
                Write-Verbose -Message $difference
            }
        }
    }
}

function ConvertTo-SplattedArguments {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [AllowEmptyCollection()]
        [psobject[]]
        $UnboundArguments
    )
    $splattedArguments = @{ }
    $UnboundArguments | ForEach-Object -Process {
        if ($_ -is [array]) {
            $splattedArguments.$lastParameterName = $_
        } else {
            switch -regex ($_) {
                # parse parameter name
                '^-(\w+):?$' {
                    $splattedArguments.Add(($lastParameterName = $matches[1]), $null)
                    break
                }
                # parse values of last parsed parameter
                default {
                    $splattedArguments.$lastParameterName = $_
                    break
                }
            }
        }
    }
    $splattedArguments
}

#endregion
# SIG # Begin signature block
# MIITugYJKoZIhvcNAQcCoIITqzCCE6cCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUZQFAQagSPUDpoxYHSHVNwGqj
# Iauggg46MIID7jCCA1egAwIBAgIQfpPr+3zGTlnqS5p31Ab8OzANBgkqhkiG9w0B
# AQUFADCBizELMAkGA1UEBhMCWkExFTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEUMBIG
# A1UEBxMLRHVyYmFudmlsbGUxDzANBgNVBAoTBlRoYXd0ZTEdMBsGA1UECxMUVGhh
# d3RlIENlcnRpZmljYXRpb24xHzAdBgNVBAMTFlRoYXd0ZSBUaW1lc3RhbXBpbmcg
# Q0EwHhcNMTIxMjIxMDAwMDAwWhcNMjAxMjMwMjM1OTU5WjBeMQswCQYDVQQGEwJV
# UzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xMDAuBgNVBAMTJ1N5bWFu
# dGVjIFRpbWUgU3RhbXBpbmcgU2VydmljZXMgQ0EgLSBHMjCCASIwDQYJKoZIhvcN
# AQEBBQADggEPADCCAQoCggEBALGss0lUS5ccEgrYJXmRIlcqb9y4JsRDc2vCvy5Q
# WvsUwnaOQwElQ7Sh4kX06Ld7w3TMIte0lAAC903tv7S3RCRrzV9FO9FEzkMScxeC
# i2m0K8uZHqxyGyZNcR+xMd37UWECU6aq9UksBXhFpS+JzueZ5/6M4lc/PcaS3Er4
# ezPkeQr78HWIQZz/xQNRmarXbJ+TaYdlKYOFwmAUxMjJOxTawIHwHw103pIiq8r3
# +3R8J+b3Sht/p8OeLa6K6qbmqicWfWH3mHERvOJQoUvlXfrlDqcsn6plINPYlujI
# fKVOSET/GeJEB5IL12iEgF1qeGRFzWBGflTBE3zFefHJwXECAwEAAaOB+jCB9zAd
# BgNVHQ4EFgQUX5r1blzMzHSa1N197z/b7EyALt0wMgYIKwYBBQUHAQEEJjAkMCIG
# CCsGAQUFBzABhhZodHRwOi8vb2NzcC50aGF3dGUuY29tMBIGA1UdEwEB/wQIMAYB
# Af8CAQAwPwYDVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NybC50aGF3dGUuY29tL1Ro
# YXd0ZVRpbWVzdGFtcGluZ0NBLmNybDATBgNVHSUEDDAKBggrBgEFBQcDCDAOBgNV
# HQ8BAf8EBAMCAQYwKAYDVR0RBCEwH6QdMBsxGTAXBgNVBAMTEFRpbWVTdGFtcC0y
# MDQ4LTEwDQYJKoZIhvcNAQEFBQADgYEAAwmbj3nvf1kwqu9otfrjCR27T4IGXTdf
# plKfFo3qHJIJRG71betYfDDo+WmNI3MLEm9Hqa45EfgqsZuwGsOO61mWAK3ODE2y
# 0DGmCFwqevzieh1XTKhlGOl5QGIllm7HxzdqgyEIjkHq3dlXPx13SYcqFgZepjhq
# IhKjURmDfrYwggSjMIIDi6ADAgECAhAOz/Q4yP6/NW4E2GqYGxpQMA0GCSqGSIb3
# DQEBBQUAMF4xCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRTeW1hbnRlYyBDb3Jwb3Jh
# dGlvbjEwMC4GA1UEAxMnU3ltYW50ZWMgVGltZSBTdGFtcGluZyBTZXJ2aWNlcyBD
# QSAtIEcyMB4XDTEyMTAxODAwMDAwMFoXDTIwMTIyOTIzNTk1OVowYjELMAkGA1UE
# BhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMTQwMgYDVQQDEytT
# eW1hbnRlYyBUaW1lIFN0YW1waW5nIFNlcnZpY2VzIFNpZ25lciAtIEc0MIIBIjAN
# BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAomMLOUS4uyOnREm7Dv+h8GEKU5Ow
# mNutLA9KxW7/hjxTVQ8VzgQ/K/2plpbZvmF5C1vJTIZ25eBDSyKV7sIrQ8Gf2Gi0
# jkBP7oU4uRHFI/JkWPAVMm9OV6GuiKQC1yoezUvh3WPVF4kyW7BemVqonShQDhfu
# ltthO0VRHc8SVguSR/yrrvZmPUescHLnkudfzRC5xINklBm9JYDh6NIipdC6Anqh
# d5NbZcPuF3S8QYYq3AhMjJKMkS2ed0QfaNaodHfbDlsyi1aLM73ZY8hJnTrFxeoz
# C9Lxoxv0i77Zs1eLO94Ep3oisiSuLsdwxb5OgyYI+wu9qU+ZCOEQKHKqzQIDAQAB
# o4IBVzCCAVMwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAO
# BgNVHQ8BAf8EBAMCB4AwcwYIKwYBBQUHAQEEZzBlMCoGCCsGAQUFBzABhh5odHRw
# Oi8vdHMtb2NzcC53cy5zeW1hbnRlYy5jb20wNwYIKwYBBQUHMAKGK2h0dHA6Ly90
# cy1haWEud3Muc3ltYW50ZWMuY29tL3Rzcy1jYS1nMi5jZXIwPAYDVR0fBDUwMzAx
# oC+gLYYraHR0cDovL3RzLWNybC53cy5zeW1hbnRlYy5jb20vdHNzLWNhLWcyLmNy
# bDAoBgNVHREEITAfpB0wGzEZMBcGA1UEAxMQVGltZVN0YW1wLTIwNDgtMjAdBgNV
# HQ4EFgQURsZpow5KFB7VTNpSYxc/Xja8DeYwHwYDVR0jBBgwFoAUX5r1blzMzHSa
# 1N197z/b7EyALt0wDQYJKoZIhvcNAQEFBQADggEBAHg7tJEqAEzwj2IwN3ijhCcH
# bxiy3iXcoNSUA6qGTiWfmkADHN3O43nLIWgG2rYytG2/9CwmYzPkSWRtDebDZw73
# BaQ1bHyJFsbpst+y6d0gxnEPzZV03LZc3r03H0N45ni1zSgEIKOq8UvEiCmRDoDR
# EfzdXHZuT14ORUZBbg2w6jiasTraCXEQ/Bx5tIB7rGn0/Zy2DBYr8X9bCT2bW+IW
# yhOBbQAuOA2oKY8s4bL0WqkBrxWcLC9JG9siu8P+eJRRw4axgohd8D20UaF5Mysu
# e7ncIAkTcetqGVvP6KUwVyyJST+5z3/Jvz4iaGNTmr1pdKzFHTx/kuDDvBzYBHUw
# ggWdMIIDUaADAgECAhAoE4COAwM7nkDtQn+T+DmdMEEGCSqGSIb3DQEBCjA0oA8w
# DQYJYIZIAWUDBAIBBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAIBBQCiAwIB
# IDAmMSQwIgYDVQQDDBtpY3JhZnRzb2Z0d2FyZUBzdGF0ZWxlc3MuYmUwHhcNMjAw
# NjIzMTE0MzU2WhcNMjEwNjIzMTIwMzU2WjAmMSQwIgYDVQQDDBtpY3JhZnRzb2Z0
# d2FyZUBzdGF0ZWxlc3MuYmUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
# AQCZBxvQbCUEcEcEln280zwR7A3tP6eGxZVTRYGhacjDpQBP31HD3HlFx85BTaHG
# oqZHWT6IjoH6p12lMLdUtIKED+aHU/ikIMOxl7JH/Sef+v8N7OEN7zHmNySNHzwp
# JFyAOiHSQuOB+tOgOnH0S8FzBeY0koPAEha7lIG+TTht5Thc7s4eMGiDSQxofEJl
# z0dxZ93MF58/59tXO7p/WPVaASpmf2yvUwva6UdF27br7nrEar1FkYm9fJjWZj4r
# maoFwRP7VtXalmcGszcZz+GWa9OTCsLRkYEAstlZmqqktWsMJjl6gc/DYLSQDgnM
# rhDWjbXyz7Bdu4NyNhEhmoFLAjx+LNH/gNL7p0SN9reTOzbP8yuQk6SEnTq2IxJG
# vnm1fUNHw3BUt2I2pli+zjM/Zk1Ewwjy4UKOQ/9afWF8Gv4ZI+WB0urZMWKyFjam
# Pk7VaUT+0LP4HRguE9Z19tUSnyQHd8YGxV/u7DhJMr/AMDUxhEgeKS3D4r2C11/R
# 4hH11hf0IzCgM3ZM0srq+cJYyvNYV7kQ5Tf+iWNQGTJBPzfxrkDrAy5wZ67sLCN2
# KDKW3tQtpOCUvs5EjJpFvOSW3F37WhpDbWSOXh5/RkPaBYuPtvCtlH4pYJ+ZocWh
# mVVEo0+1Jy7I6eKU8YZnpPtI27BXFJcVG1un5xB5rhTHFQIDAQABo18wXTAOBgNV
# HQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwFwYDVR0RBBAwDoIMc3Rh
# dGVsZXNzLmJlMB0GA1UdDgQWBBSriwKgTYio3gri7A26JuOp3nI01DBBBgkqhkiG
# 9w0BAQowNKAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFl
# AwQCAQUAogMCASADggIBAFH3xqYukA0oWVzuY+WRpXhm1La5OZsnp06rPJYonbJO
# a/tT3Krw90Qf2Y8mXFi8bI2DGoeihwq/VJ2OBiHbtIzykOex9TY2kRHor/ewgLLo
# 6uH2+EL5TXwGn3dagsR7OiVoFwXRyjf+j4drM6+z/bMEU40UcyR5/3/cGKmbSx33
# m14ejne8spWIduNKagbFiy8kmJghMHhl6jrGBQCbBxSkvVOjrYtvdE8IMsplDmHw
# ivTuedxXgcur7SoXf4b1jQhccm/pByv9dNMujQnzvsdGpLftYlyAXz7adtluo701
# W+nXgDidOql4MWbN7ANTfeGJm/O4scGP/86AuAZn2Uk/EK7S1XF8VZkg0eDVgene
# UxoDDTRDe1v++FzmQToqXsWdedROy7iP69ShoUxaFh7OjKf2bisP7E1EhOtss9l1
# YBm9U4nx6GazBGF+IxnWenBuuTspTVROyYxLs8dERZLJQzbxaUwV/aK63xOVj8xX
# 9p1QavYamoFaHGnkNGB+XW5qYqbbAWUKtrf+RF5WVCAL8Fuh3Yk6Ala3zMJs7icn
# H58ljAz1EbvSH9Oa6rM/y4KGwe0pyCzZi1eZKbXYllqRdjgh+Uicjul10MSz1Q5+
# xZcOyqm+YVBlvCAZ414Sw+TcV2bUzMO1L08FcyTiMYZc3MSxVSDB/jbYe4NZ5f/i
# MYIE6jCCBOYCAQEwOjAmMSQwIgYDVQQDDBtpY3JhZnRzb2Z0d2FyZUBzdGF0ZWxl
# c3MuYmUCECgTgI4DAzueQO1Cf5P4OZ0wCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcC
# AQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYB
# BAGCNwIBCzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFCasqSG4AmVn
# WNB5GcWHNOiXQUleMA0GCSqGSIb3DQEBAQUABIICAJZ3b3RRGOTZtwWpCujWFC67
# qzUPDBZfDWBcvUgWsfU5HfoaHd1NHyOdoZ18o9cfvoEfKfCR/pYv95Mk3G6bhd41
# mo/c2E8kw2edCNKsMo057ItY+nopT3yS9tmrW0v3eeZEgKjJQkkMW22OpVqI861A
# VS/gc1uieJAQHKrj+wKBibVIAZgoV1BqQnQb8kgOf9HBVDd3CczuWrAyqaRH0c46
# oqNrQRRGJNngTYxU3HomzNjTd2FAJz4TfjUOtQYx/mXW1ChcfRDrFCTAgrPaFAUe
# 2xpr061zUooltXjWiY/yk4+wiXDAE7amsOlG8PKrEUdb+0xfm4865JO1teXUcGx+
# iz6XR7PxfDusDiu332/kxIGrDPS6FPClj9tZcjV1Dx/93pldq2tR9OEjcBqWLFAs
# X1vuj6lbhiIxuHpsVHJmP2zxd1ySrznxhTYdsbb7+ITWAHTyXJHdCf+/So02Edw4
# dIvnjIqHuwCMDwKpXnhb42dj76TGYwQPynjZCRNwHaJzqKbojLt5BXCcFqL3OdfC
# Di/8c8WyZ7e1cC2BjMNRZMedCkIJjSkHCODWNWB7MEdsyEiXIubaeLdLEBB1284M
# Sps3VGLX3SloETA9C0iA5P6vf6JRA+GJPu+WbhIcqPeaiVvXi72DkbctMJP8ywqv
# QyUZ/eQNWMVgDkUCE7jaoYICCzCCAgcGCSqGSIb3DQEJBjGCAfgwggH0AgEBMHIw
# XjELMAkGA1UEBhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMTAw
# LgYDVQQDEydTeW1hbnRlYyBUaW1lIFN0YW1waW5nIFNlcnZpY2VzIENBIC0gRzIC
# EA7P9DjI/r81bgTYapgbGlAwCQYFKw4DAhoFAKBdMBgGCSqGSIb3DQEJAzELBgkq
# hkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIwMTEyNDEwNDU1MFowIwYJKoZIhvcN
# AQkEMRYEFODpIBvzVcjnAykfT45RdkkTJd8EMA0GCSqGSIb3DQEBAQUABIIBAFNm
# FphxUiA+eZ5rIUxEHYuYZcwFIpY1R7kvyT1wawc1nHOKMhofLzJ+dxG6Zc5pjSeG
# J1Uu75Nvl/UfkXlWCxxl+Ykqd57uMakUidy3ZV4qky3QMAr3kIP/rf482l0Om7Kg
# dkRjZFPGlckyKbnBJnafQf0Rpbup6ffFrbi4acEWfWOfSPM+g08bGYogAd5nTdcO
# iy/9q1gDJsCaH24XLyYSwLlOiEiFLnclvSllE+sgUwmMS7uOuE4uGs2qROi/Z71e
# /ey84UkX3cr43duL1+oGb6P9cbQa9U7sgI9O6/3eK9Ce07ab5yIHfVMr4YZoSufy
# 3D95q025fo5cKlK/JnE=
# SIG # End signature block