Extensions/ConvertFrom-ExistingIapSubmission.ps1

# Copyright (C) Microsoft Corporation. All rights reserved.

<#
    .SYNOPSIS
        Script for converting an existing In-AppProduct (IAP) submission in the Store
        to the January 2017 PDP schema.
 
    .DESCRIPTION
        Script for converting an existing In-AppProduct (IAP) submission in the Store
        to the January 2017 PDP schema.
 
        The Git-repo for the StoreBroker module can be found here: http://aka.ms/StoreBroker
 
    .PARAMETER IapId
        The ID of the IAP that the PDP's will be getting created for.
        The most recent submission for this IAP will be used unless a SubmissionId is
        explicitly specified.
 
    .PARAMETER SubmissionId
        The ID of the application submission that the PDP's will be getting created for.
        The most recent submission for IapId will be used unless a value for this parameter is
        provided.
 
    .PARAMETER Release
        The release to use. This value will be placed in each new PDP and used in conjunction with '-OutPath'.
        Some examples could be "1601" for a January 2016 release, "March 2016", or even just "1".
 
    .PARAMETER PdpFileName
        The name of the PDP file that will be generated for each region.
 
    .PARAMETER OutPath
        The output directory.
        This script will create two subfolders of OutPath:
           <OutPath>\PDPs\<Release>\
           <OutPath>\Images\<Release>\
        Each of these sub-folders will have region-specific subfolders for their file content.
 
    .EXAMPLE
        .\ConvertFrom-ExistingSubmission -IapId 0ABCDEF12345 -Release "March Release" -OutPath "C:\NewPDPs"
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [string] $IapId,

    [string] $SubmissionId = $null,

    [Parameter(Mandatory)]
    [string] $Release,

    [string] $PdpFileName = "PDP.xml",

    [Parameter(Mandatory)]
    [string] $OutPath
)

# Import Write-Log
$rootDir = Split-Path -Path $PSScriptRoot -Parent
$helpers = "$rootDir\StoreBroker\Helpers.ps1"
if (-not (Test-Path -Path $helpers -PathType Leaf))
{
    throw "Script execution requires Helpers.ps1 which is part of the git repo. Please execute this script from within your cloned repo."
}
. $helpers

#region Comment Constants
$script:LocIdAttribute = "_locID"
$script:LocIdFormat = "Iap_{0}"
$script:CommentFormat = " _locComment_text=`"{{MaxLength={0}}} {1}`" "
#endregion Comment Constants

function Add-ToElement
{
<#
    .SYNOPSIS
        Adds an arbitrary number of comments and attributes to an XmlElement.
 
    .PARAMETER Element
        The XmlElement to be modified.
 
    .PARAMETER Comment
        An array of comments to add to the element.
 
    .PARAMETER Attribute
        A hashtable where the keys are the attribute names and the values are the attribute values.
 
    .NOTES
        If a provided attribute already exists on the Element, the Element will NOT be modified.
        It will ONLY be modified if the Element does not have that attribute.
 
    .EXAMPLE
        PS C:\>$xml = [xml] (Get-Content $filePath)
        PS C:\>$root = $xml.DocumentElement
        PS C:\>Add-ToElement -Element $root -Comment "Comment1", "Comment2" -Attribute @{ "Attrib1"="Val1"; "Attrib2"="Val2" }
 
        Adds two comments and two attributes to the root element of the XML document.
         
#>

    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlElement] $Element,

        [string[]] $Comment,

        [hashtable] $Attribute
    )

    if ($Comment.Count -gt 1)
    {
        # Reverse 'Comment' array in order to preserve order because of nature of 'Prepend'
        # Input array is modified in place, no need to capture result
        [Array]::Reverse($Comment)
    }

    foreach ($text in $Comment)
    {
        if (-not [String]::IsNullOrWhiteSpace($text))
        {
            $elem = $Element.OwnerDocument.CreateComment($text)
            $Element.PrependChild($elem) | Out-Null
        }
    }

    foreach ($key in $Attribute.Keys)
    {
        if ($null -eq $Element.$key)
        {
            $Element.SetAttribute($key, $Attribute[$key])
        }
        else
        {
            $out = "For element $($Element.LocalName), did not create attribute '$key' with value '$($Attribute[$key])' because the attribute already exists."
            Write-Log $out -Level Warning
        }
    }
}

function Ensure-RootChild
{
<#
    .SYNOPSIS
        Creates the specified element as a child of the XML root node, only if that element does not exist already.
 
    .PARAMETER Xml
        The XmlDocument to (potentially) modify.
 
    .PARAMETER Element
        The name of the element to existence check.
 
    .OUTPUTS
        XmlElement. Returns a reference to the (possibly newly created) element requested.
 
    .EXAMPLE
        PS C:\>$xml = [xml] (Get-Content $filePath)
        PS C:\>Ensure-RootChild -Xml $xml -Element "SomeElement"
 
        $xml.DocumentElement.SomeElement now exists and is an XmlElement object.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Best description for purpose")]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlDocument] $Xml,

        [Parameter(Mandatory)]
        [string] $Element
    )

    # ProductDescription node
    $root = $Xml.DocumentElement

    if ($root.GetElementsByTagName($Element).Count -eq 0)
    {
        $elem = $Xml.CreateElement($Element, $Xml.DocumentElement.NamespaceURI)
        $root.AppendChild($elem) | Out-Null
    }

    return $root.GetElementsByTagName($Element)[0]
}

function Add-Title
{
<#
    .SYNOPSIS
        Creates the Title node.
 
    .PARAMETER Xml
        The XmlDocument to modify.
 
    .PARAMETER Listing
        The base listing from the submission for a specific Lang.
#>

    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlDocument] $Xml,

        [Parameter(Mandatory)]
        [PSCustomObject] $Listing
    )

    $elementName = "Title"
    $elementNode = Ensure-RootChild -Xml $Xml -Element $elementName
    $elementNode.InnerText = $Listing.title

    # Add comment to parent
    $maxChars = 100
    $paramSet = @{
        "Element" = $elementNode;
        "Attribute" = @{ $script:LocIdAttribute = $script:LocIdFormat -f $elementName };
        "Comment" = @($script:CommentFormat -f $maxChars, "IAP $elementName"; " [required] ")
    }

    Add-ToElement @paramSet
}

function Add-Description
{
<#
    .SYNOPSIS
        Creates the description node
 
    .PARAMETER Xml
        The XmlDocument to modify.
 
    .PARAMETER Listing
        The base listing from the submission for a specific Lang.
#>

    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlDocument] $Xml,

        [Parameter(Mandatory)]
        [PSCustomObject] $Listing
    )

    $elementName = "Description"
    $elementNode = Ensure-RootChild -Xml $Xml -Element $elementName
    $elementNode.InnerText = $Listing.description

    # Add comment to parent
    $maxChars = 200
    $paramSet = @{
        "Element" = $elementNode;
        "Attribute" = @{ $script:LocIdAttribute = $script:LocIdFormat -f $elementName };
        "Comment" = @("$script:CommentFormat" -f $maxChars, "IAP $elementName"; " [optional] ")
    }

    Add-ToElement @paramSet
}

function Add-Icon
{
<#
    .SYNOPSIS
        Creates the icon node.
 
    .PARAMETER Xml
        The XmlDocument to modify.
 
    .PARAMETER Listing
        The base listing from the submission for a specific Lang.
 
    .OUTPUTS
        [String] The path specified for the icon (if available)
 
    .NOTES
        This function is implemented a bit differently than the others.
        Icon is an optional element, but if it's specified, the Filename attribute must
        be included with a non-empty value. This is fine if the Listing has an icon defined,
        but if it doesn't, then we want to include the element, but commented out so that users
        know later what they need to add if they wish to include an icon at some future time.
        We will create/add the icon node the way we do in all other cases so that we have its XML,
        but if we then determine that there is no icon for that listing, we'll convert the element
        to its XML string representation that we can add as a comment, and then remove the actual
        node.
#>

    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlDocument] $Xml,

        [Parameter(Mandatory)]
        [PSCustomObject] $Listing
    )

    # For this element, we want the comment above, rather than inside, the element.
    $comment = $Xml.CreateComment(' [optional] Specifying an icon is optional. If provided, the icon must be a 300 x 300 png file. ')
    $Xml.DocumentElement.AppendChild($comment ) | Out-Null

    $iconFilename = $Listing.icon.fileName

    $elementName = "Icon"
    [System.Xml.XmlElement] $elementNode = Ensure-RootChild -Xml $Xml -Element $elementName

    $paramSet = @{
        "Element" = $elementNode;
        "Attribute" = @{ 'Filename' = $iconFilename };
    }

    Add-ToElement @paramSet

    if ($null -eq $iconFilename)
    {
        # We'll just comment this out for now since it's not being used.
        # We do a tiny bit of extra processing to remove the unnecessary xmlns attribute that
        # is added to the node when we get the OuterXml.
        $iconElementXml = $elementNode.OuterXml -replace 'xmlns="[^"]+"', ""
        $comment = $Xml.CreateComment(" $iconElementXml ")
        $Xml.DocumentElement.RemoveChild($elementNode) | Out-Null
        $Xml.DocumentElement.AppendChild($comment ) | Out-Null
    }

    return $iconFilename
}

function ConvertFrom-Listing
{
<#
    .SYNOPSIS
        Converts a base listing for an existing submission into a PDP file that conforms with
        the January 2017 PDP schema.
 
    .PARAMETER Listing
        The base listing from the submission for the indicated Lang.
 
    .PARAMETER Lang
        The language / region code for the PDP (e.g. "en-us")
 
    .PARAMETER Release
        The release to use. This value will be placed in each new PDP.
        Some examples could be "1601" for a January 2016 release, "March 2016", or even just "1".
 
    .PARAMETER PdpRootPath
        The root / base path that all of the language sub-folders will go for the PDP files.
 
    .PARAMETER FileName
        The name of the PDP file that will be generated.
 
    .OUTPUTS
        [String[]] Array of image names that the PDP references
 
    .EXAMPLE
        ConvertFrom-Listing -Listing ($sub.listings."en-us".baseListing) -Lang "en-us" -Release "1701" -PdpRootPath "C:\PDPs\" -FileName "PDP.xml"
 
        Converts the given "en-us" base listing to the current PDP schema,
        and saves it to "c:\PDPs\en-us\PDP.xml"
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [PSCustomObject] $Listing,

        [Parameter(Mandatory)]
        [string] $Lang,

        [Parameter(Mandatory)]
        [string] $Release,

        [Parameter(Mandatory)]
        [string] $PdpRootPath,

        [Parameter(Mandatory)]
        [string] $FileName
    )

    $xml = [xml]([String]::Format('<?xml version="1.0" encoding="utf-8"?>
    <InAppProductDescription language="en-us"
        xmlns="http://schemas.microsoft.com/appx/2012/InAppProductDescription"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xml:lang="{0}"
        Release="{1}"/>'
, $Lang, $Release))

    Add-Title -Xml $Xml -Listing $Listing    
    Add-Description -Xml $Xml -Listing $Listing
    $icon = Add-Icon -Xml $Xml -Listing $Listing

    $imageNames = @()
    $imageNames += $icon

    # Save XML object to file
    $filePath = Ensure-PdpFilePath -PdpRootPath $PdpRootPath -Lang $Lang -FileName $FileName
    $xml.Save($filePath)

    # Post-process the file to ensure CRLF (sometimes is only LF).
    $content = Get-Content -Encoding UTF8 -Path $filePath
    $content -join [Environment]::NewLine | Out-File -Force -Encoding utf8 -FilePath $filePath

    return $imageNames
}

function Ensure-PdpFilePath
{
<#
    .SYNOPSIS
        Ensures that the containing folder for a PDP file that will be generated exists so that
        it can successfully be written.
 
    .DESCRIPTION
        Ensures that the containing folder for a PDP file that will be generated exists so that
        it can successfully be written.
 
    .PARAMETER PdpRootPath
        The root / base path that all of the language sub-folders will go for the PDP files.
 
    .PARAMETER Lang
        The language / region code for the PDP (e.g. "en-us")
 
    .PARAMETER FileName
        The name of the PDP file that will be generated.
 
    .EXAMPLE
        Ensure-PdpFilePath -PdpRootPath "C:\PDPs\" -Lang "en-us" -FileName "PDP.xml"
 
        Ensures that the path c:\PDPs\en-us\ exists, creating any sub-folder along the way as
        necessary, and then returns the path "c:\PDPs\en-us\PDP.xml"
 
    .OUTPUTS
        [String] containing the full path to the PDP file.
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Best description for purpose")]
    param(
        [Parameter(Mandatory)]
        [string] $PdpRootPath,

        [string] $Lang,

        [string] $FileName
    )

    $dropFolder = Join-Path -Path $PdpRootPath -ChildPath $Lang
    if (-not (Test-Path -PathType Container -Path $dropFolder))
    {
        New-Item -Force -ItemType Directory -Path $dropFolder | Out-Null
    }

    return (Join-Path -Path $dropFolder -ChildPath $FileName)
}

function Show-ImageFileNames
{
<#
    .SYNOPSIS
        Informs the user what the image filenames are that they need to make available to StoreBroker.
 
    .DESCRIPTION
        Informs the user what the image filenames are that they need to make available to StoreBroker.
 
    .PARAMETER LangImageNames
        A hashtable, indexed by langcode, containing an array of image names that the listing
        for that langcode references.
 
    .PARAMETER Release
        The release name that was added to the PDP files.
 
    .EXAMPLE
        Show-ImageFileNames -LangImageNames $langImageNames -Release "1701"
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="The most common scenario is that there will be multiple images, not a singular image.")]
    param(
        [Parameter(Mandatory)]
        [hashtable] $LangImageNames,

        [Parameter(Mandatory)]
        [string] $Release
    )

    # If there are no screenshots, nothing to do here
    if ($LangImageNames.Count -eq 0)
    {
        return
    }

    # If there are no images being used at all, then we can also early return
    $imageCount = 0;
    foreach ($lang in $LangImageNames.GetEnumerator())
    {
        $imageCount += $lang.Value.Count
    }

    if ($imageCount.Count -eq 0)
    {
        return
    }

    $output = @()
    $output += "You now need to find all of your images and place them here: <ImagesRootPath>\$Release\<langcode>\..."
    $output += " where <ImagesRootPath> is the path defined in your config file,"
    $output += " and <langcode> is the same langcode for the directory of the PDP file referencing those images."
    Write-Log $($output -join [Environment]::NewLine)

    # Quick analysis to help teams out if they need to do anything special with their PDP's

    $langs = $LangImageNames.Keys | ConvertTo-Array
    $seenImages = $LangImageNames[$langs[0]]
    $imagesDiffer = $false
    for ($i = 1; ($i -lt $langs.Count) -and (-not $imagesDiffer); $i++)
    {
        if (($LangImageNames[$langs[$i]].Count -ne $seenImages.Count))
        {
            $imagesDiffer = $true
            break
        }

        foreach ($image in $LangImageNames[$langs[$i]])
        {
            if ($seenImages -notcontains $image)
            {
                $imagesDiffer = $true
                break
            }
        }
    }

    # Now show the user the image filenames
    if ($imagesDiffer)
    {
        $output = @()
        $output += "It appears that you don't have consistent images across all languages."
        $output += "While StoreBroker supports this scenario, some localization systems may"
        $output += "not support this without additional work. Please refer to the FAQ in"
        $output += "the documentation for more info on how to best handle this scenario."
        Write-Log $($output -join [Environment]::NewLine) -Level Warning

        $output = @()
        $output += "The currently referenced image filenames, per langcode, are as follows:"
        foreach ($langCode in ($LangImageNames.Keys.GetEnumerator() | Sort-Object))
        {
            $output += " * [$langCode]: " + ($LangImageNames.$langCode -join ", ")
        }
    
        Write-Log $($output -join [Environment]::NewLine)
    }
    else
    {
        $output = @()
        $output += "Every language that has a PDP references the following images:"
        $output += "`t$($seenImages -join `"`n`t`")"
        Write-Log $($output -join [Environment]::NewLine)
    }
}

# function Main is invoked at the bottom of the file
function Main
{
    [CmdletBinding()]
    param()

    if ($null -eq (Get-Module StoreBroker))
    {
        $message = "The StoreBroker module is not available in this PowerShell session. Please import the module, authenticate correctly using Set-StoreBrokerAuthentication, and try again."
        throw $message
    }

    if ([String]::IsNullOrEmpty($SubmissionId))
    {
        $iap = Get-InAppProduct -IapId $IapId
        $SubmissionId = $iap.lastPublishedInAppProductSubmission.id
        if ([String]::IsNullOrEmpty($SubmissionId))
        {
            $SubmissionId = $iap.pendingInAppProductSubmission.id
            Write-Log "No published submission exists for this In-App Product. Using the current pending submission." -Level Warning
        }
    }

    $sub = Get-InAppProductSubmission -IapId $IapId -SubmissionId $SubmissionId

    $langImageNames = @{}
    $langs = ($sub.listings | Get-Member -type NoteProperty)
    $pdpsGenerated = 0
    $langs |
        ForEach-Object {
            $lang = $_.Name
            Write-Log "Creating PDP for $lang" -Level Verbose
            Write-Progress -Activity "Generating PDP" -Status $lang -PercentComplete $(($pdpsGenerated / $langs.Count) * 100)
            try
            {
                $imageNames = ConvertFrom-Listing -Listing ($sub.listings.$lang) -Lang $lang -Release $Release -PdpRootPath $OutPath -FileName $PdpFileName
                $langImageNames[$lang] = $imageNames
                $pdpsGenerated++
            }
            catch
            {
                Write-Log "Error creating [$lang] PDP: $($Error[0].Exception.Message )" -Level Error
                throw
            }
        }

    if ($pdpsGenerated -gt 0)
    {
        Write-Log "PDP's have been created here: $OutPath"
        Show-ImageFileNames -LangImageNames $langImageNames -Release $Release
    }
    else
    {
        $output = @()
        $output += "No PDPs were generated."
        $output += "Please verify that this existing In-App Product has one or more language listings that this extension can convert,"
        $output += "otherwise you can start fresh using the sample PDP\InAppProductDescription.xml as a starting point."
        Write-Log $($output -join [Environment]::NewLine) -Level Warning
    }
}




# Script body
$OutPath = Resolve-UnverifiedPath -Path $OutPath

# function Main invocation
Main

# SIG # Begin signature block
# MIIdrwYJKoZIhvcNAQcCoIIdoDCCHZwCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUGsdFkYQnE9rsfychk7dSMAT/
# JlmgghhTMIIEwjCCA6qgAwIBAgITMwAAAMRudtBNPf6pZQAAAAAAxDANBgkqhkiG
# 9w0BAQUFADB3MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSEw
# HwYDVQQDExhNaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EwHhcNMTYwOTA3MTc1ODUy
# WhcNMTgwOTA3MTc1ODUyWjCBsjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjEMMAoGA1UECxMDQU9DMScwJQYDVQQLEx5uQ2lwaGVyIERTRSBFU046
# MjEzNy0zN0EwLTRBQUExJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNl
# cnZpY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCoA5rFUpl2jKM9
# /L26GuVj6Beo87YdPTwuOL0C+QObtrYvih7LDNDAeWLw+wYlSkAmfmaSXFpiRHM1
# dBzq+VcuF8YGmZm/LKWIAB3VTj6df05JH8kgtp4gN2myPTR+rkwoMoQ3muR7zb1n
# vNiLsEpgJ2EuwX5M/71uYrK6DHAPbbD3ryFizZAfqYcGUWuDhEE6ZV+onexUulZ6
# DK6IoLjtQvUbH1ZMEWvNVTliPYOgNYLTIcJ5mYphnUMABoKdvGDcVpSmGn6sLKGg
# iFC82nun9h7koj7+ZpSHElsLwhWQiGVWCRVk8ZMbec+qhu+/9HwzdVJYb4HObmwN
# Daqpqe17AgMBAAGjggEJMIIBBTAdBgNVHQ4EFgQUiAUj6xG9EI77i5amFSZrXv1V
# 3lAwHwYDVR0jBBgwFoAUIzT42VJGcArtQPt2+7MrsMM1sw8wVAYDVR0fBE0wSzBJ
# oEegRYZDaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMv
# TWljcm9zb2Z0VGltZVN0YW1wUENBLmNybDBYBggrBgEFBQcBAQRMMEowSAYIKwYB
# BQUHMAKGPGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljcm9z
# b2Z0VGltZVN0YW1wUENBLmNydDATBgNVHSUEDDAKBggrBgEFBQcDCDANBgkqhkiG
# 9w0BAQUFAAOCAQEAcDh+kjmXvCnoEO5AcUWfp4/4fWCqiBQL8uUFq6cuBuYp8ML4
# UyHSLKNPOoJmzzy1OT3GFGYrmprgO6c2d1tzuSaN3HeFGENXDbn7N2RBvJtSl0Uk
# ahSyak4TsRUPk/WwMQ0GOGNbxjolrOR41LVsSmHVnn8IWDOCWBj1c+1jkPkzG51j
# CiAnWzHU1Q25A/0txrhLYjNtI4P3f0T0vv65X7rZAIz3ecQS/EglmADfQk/zrLgK
# qJdxZKy3tXS7+35zIrDegdAH2G7d3jvCNTjatrV7cxKH+ZX9oEsFl10uh/U83KA2
# QiQJQMtbjGSzQV2xRpcNf2GpHBRPW0sK4yL3wzCCBgAwggPooAMCAQICEzMAAADD
# Dpun2LLc9ywAAAAAAMMwDQYJKoZIhvcNAQELBQAwfjELMAkGA1UEBhMCVVMxEzAR
# BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2ln
# bmluZyBQQ0EgMjAxMTAeFw0xNzA4MTEyMDIwMjRaFw0xODA4MTEyMDIwMjRaMHQx
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xHjAcBgNVBAMTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
# ggEBALtX1zjRsQZ/SS2pbbNjn3q6tjohW7SYro3UpIGgxXXFLO+CQCq3gVN382MB
# CrzON4QDQENXgkvO7R+2/YBtycKRXQXH3FZZAOEM61fe/fG4kCe/dUr8dbJyWLbF
# SJszYgXRlZSlvzkirY0STUZi2jIZzqoiXFZIsW9FyWd2Yl0wiKMvKMUfUCrZhtsa
# ESWBwvT1Zy7neR314hx19E7Mx/znvwuARyn/z81psQwLYOtn5oQbm039bUc6x9nB
# YWHylRKhDQeuYyHY9Jkc/3hVge6leegggl8K2rVTGVQBVw2HkY3CfPFUhoDhYtuC
# cz4mXvBAEtI51SYDDYWIMV8KC4sCAwEAAaOCAX8wggF7MB8GA1UdJQQYMBYGCisG
# AQQBgjdMCAEGCCsGAQUFBwMDMB0GA1UdDgQWBBSnE10fIYlV6APunhc26vJUiDUZ
# rzBRBgNVHREESjBIpEYwRDEMMAoGA1UECxMDQU9DMTQwMgYDVQQFEysyMzAwMTIr
# YzgwNGI1ZWEtNDliNC00MjM4LTgzNjItZDg1MWZhMjI1NGZjMB8GA1UdIwQYMBaA
# FEhuZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93
# d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFf
# MjAxMS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEA
# TZdPNH7xcJOc49UaS5wRfmsmxKUk9N9E1CS6s2oIiZmayzHncJv/FB2wBzl/5DA7
# EyLeDsiVZ7tufvh8laSQgjeTpoPTSQLBrK1Z75G3p2YADqJMJdTc510HAsooNGU7
# OYOtlSqOyqDoCDoc/j57QEmUTY5UJQrlsccK7nE3xpteNvWnQkT7vIewDcA12SaH
# X/9n7yh094owBBGKZ8xLNWBqIefDjQeDXpurnXEfKSYJEdT1gtPSNgcpruiSbZB/
# AMmoW+7QBGX7oQ5XU8zymInznxWTyAbEY1JhAk9XSBz1+3USyrX59MJpX7uhnQ1p
# gyfrgz4dazHD7g7xxIRDh+4xnAYAMny3IIq5CCPqVrAY1LK9Few37WTTaxUCI8aK
# M4c60Zu2wJZZLKABU4QBX/J7wXqw7NTYUvZfdYFEWRY4J1O7UPNecd/311HcMdUa
# YzUql36fZjdfz1Uz77LKvCwjqkQe7vtnSLToQsMPilFYokYCYSZaGb9clOmoQHDn
# WzBMfIDUUGeipe4O6z218eV5HuH1WBlvu4lteOIgWCX/5Eiz5q/xskAEF0ZQ1Axs
# kRR97sri9ibeGzsEZ1EuD6QX90L/P5GJMfinvLPlOlLcKjN/SmSRZdhlEbbbare0
# bFL8v4txFsQsznOaoOldCMFFRaUphuwBMW1edMZWMQswggYHMIID76ADAgECAgph
# Fmg0AAAAAAAcMA0GCSqGSIb3DQEBBQUAMF8xEzARBgoJkiaJk/IsZAEZFgNjb20x
# GTAXBgoJkiaJk/IsZAEZFgltaWNyb3NvZnQxLTArBgNVBAMTJE1pY3Jvc29mdCBS
# b290IENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0wNzA0MDMxMjUzMDlaFw0yMTA0
# MDMxMzAzMDlaMHcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# ITAfBgNVBAMTGE1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQTCCASIwDQYJKoZIhvcN
# AQEBBQADggEPADCCAQoCggEBAJ+hbLHf20iSKnxrLhnhveLjxZlRI1Ctzt0YTiQP
# 7tGn0UytdDAgEesH1VSVFUmUG0KSrphcMCbaAGvoe73siQcP9w4EmPCJzB/LMySH
# nfL0Zxws/HvniB3q506jocEjU8qN+kXPCdBer9CwQgSi+aZsk2fXKNxGU7CG0OUo
# Ri4nrIZPVVIM5AMs+2qQkDBuh/NZMJ36ftaXs+ghl3740hPzCLdTbVK0RZCfSABK
# R2YRJylmqJfk0waBSqL5hKcRRxQJgp+E7VV4/gGaHVAIhQAQMEbtt94jRrvELVSf
# rx54QTF3zJvfO4OToWECtR0Nsfz3m7IBziJLVP/5BcPCIAsCAwEAAaOCAaswggGn
# MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFCM0+NlSRnAK7UD7dvuzK7DDNbMP
# MAsGA1UdDwQEAwIBhjAQBgkrBgEEAYI3FQEEAwIBADCBmAYDVR0jBIGQMIGNgBQO
# rIJgQFYnl+UlE/wq4QpTlVnkpKFjpGEwXzETMBEGCgmSJomT8ixkARkWA2NvbTEZ
# MBcGCgmSJomT8ixkARkWCW1pY3Jvc29mdDEtMCsGA1UEAxMkTWljcm9zb2Z0IFJv
# b3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5ghB5rRahSqClrUxzWPQHEy5lMFAGA1Ud
# HwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3By
# b2R1Y3RzL21pY3Jvc29mdHJvb3RjZXJ0LmNybDBUBggrBgEFBQcBAQRIMEYwRAYI
# KwYBBQUHMAKGOGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWlj
# cm9zb2Z0Um9vdENlcnQuY3J0MBMGA1UdJQQMMAoGCCsGAQUFBwMIMA0GCSqGSIb3
# DQEBBQUAA4ICAQAQl4rDXANENt3ptK132855UU0BsS50cVttDBOrzr57j7gu1BKi
# jG1iuFcCy04gE1CZ3XpA4le7r1iaHOEdAYasu3jyi9DsOwHu4r6PCgXIjUji8FMV
# 3U+rkuTnjWrVgMHmlPIGL4UD6ZEqJCJw+/b85HiZLg33B+JwvBhOnY5rCnKVuKE5
# nGctxVEO6mJcPxaYiyA/4gcaMvnMMUp2MT0rcgvI6nA9/4UKE9/CCmGO8Ne4F+tO
# i3/FNSteo7/rvH0LQnvUU3Ih7jDKu3hlXFsBFwoUDtLaFJj1PLlmWLMtL+f5hYbM
# UVbonXCUbKw5TNT2eb+qGHpiKe+imyk0BncaYsk9Hm0fgvALxyy7z0Oz5fnsfbXj
# pKh0NbhOxXEjEiZ2CzxSjHFaRkMUvLOzsE1nyJ9C/4B5IYCeFTBm6EISXhrIniIh
# 0EPpK+m79EjMLNTYMoBMJipIJF9a6lbvpt6Znco6b72BJ3QGEe52Ib+bgsEnVLax
# aj2JoXZhtG6hE6a/qkfwEm/9ijJssv7fUciMI8lmvZ0dhxJkAj0tr1mPuOQh5bWw
# ymO0eFQF1EEuUKyUsKV4q7OglnUa2ZKHE3UiLzKoCG6gW4wlv6DvhMoh1useT8ma
# 7kng9wFlb4kLfchpyOZu6qeXzjEp/w7FW1zYTRuh2Povnj8uVRZryROj/TCCB3ow
# ggVioAMCAQICCmEOkNIAAAAAAAMwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBS
# b290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDExMB4XDTExMDcwODIwNTkwOVoX
# DTI2MDcwODIxMDkwOVowfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0
# b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh
# dGlvbjEoMCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMTCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKvw+nIQHC6t2G6qghBNNLry
# tlghn0IbKmvpWlCquAY4GgRJun/DDB7dN2vGEtgL8DjCmQawyDnVARQxQtOJDXlk
# h36UYCRsr55JnOloXtLfm1OyCizDr9mpK656Ca/XllnKYBoF6WZ26DJSJhIv56sI
# UM+zRLdd2MQuA3WraPPLbfM6XKEW9Ea64DhkrG5kNXimoGMPLdNAk/jj3gcN1Vx5
# pUkp5w2+oBN3vpQ97/vjK1oQH01WKKJ6cuASOrdJXtjt7UORg9l7snuGG9k+sYxd
# 6IlPhBryoS9Z5JA7La4zWMW3Pv4y07MDPbGyr5I4ftKdgCz1TlaRITUlwzluZH9T
# upwPrRkjhMv0ugOGjfdf8NBSv4yUh7zAIXQlXxgotswnKDglmDlKNs98sZKuHCOn
# qWbsYR9q4ShJnV+I4iVd0yFLPlLEtVc/JAPw0XpbL9Uj43BdD1FGd7P4AOG8rAKC
# X9vAFbO9G9RVS+c5oQ/pI0m8GLhEfEXkwcNyeuBy5yTfv0aZxe/CHFfbg43sTUkw
# p6uO3+xbn6/83bBm4sGXgXvt1u1L50kppxMopqd9Z4DmimJ4X7IvhNdXnFy/dygo
# 8e1twyiPLI9AN0/B4YVEicQJTMXUpUMvdJX3bvh4IFgsE11glZo+TzOE2rCIF96e
# TvSWsLxGoGyY0uDWiIwLAgMBAAGjggHtMIIB6TAQBgkrBgEEAYI3FQEEAwIBADAd
# BgNVHQ4EFgQUSG5k5VAF04KqFzc3IrVtqMp1ApUwGQYJKwYBBAGCNxQCBAweCgBT
# AHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgw
# FoAUci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDov
# L2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0
# MjAxMV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAwTgYIKwYBBQUHMAKG
# Qmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0
# MjAxMV8yMDExXzAzXzIyLmNydDCBnwYDVR0gBIGXMIGUMIGRBgkrBgEEAYI3LgMw
# gYMwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv
# ZG9jcy9wcmltYXJ5Y3BzLmh0bTBABggrBgEFBQcCAjA0HjIgHQBMAGUAZwBhAGwA
# XwBwAG8AbABpAGMAeQBfAHMAdABhAHQAZQBtAGUAbgB0AC4gHTANBgkqhkiG9w0B
# AQsFAAOCAgEAZ/KGpZjgVHkaLtPYdGcimwuWEeFjkplCln3SeQyQwWVfLiw++MNy
# 0W2D/r4/6ArKO79HqaPzadtjvyI1pZddZYSQfYtGUFXYDJJ80hpLHPM8QotS0LD9
# a+M+By4pm+Y9G6XUtR13lDni6WTJRD14eiPzE32mkHSDjfTLJgJGKsKKELukqQUM
# m+1o+mgulaAqPyprWEljHwlpblqYluSD9MCP80Yr3vw70L01724lruWvJ+3Q3fMO
# r5kol5hNDj0L8giJ1h/DMhji8MUtzluetEk5CsYKwsatruWy2dsViFFFWDgycSca
# f7H0J/jeLDogaZiyWYlobm+nt3TDQAUGpgEqKD6CPxNNZgvAs0314Y9/HG8VfUWn
# duVAKmWjw11SYobDHWM2l4bf2vP48hahmifhzaWX0O5dY0HjWwechz4GdwbRBrF1
# HxS+YWG18NzGGwS+30HHDiju3mUv7Jf2oVyW2ADWoUa9WfOXpQlLSBCZgB/QACnF
# sZulP0V3HjXG0qKin3p6IvpIlR+r+0cjgPWe+L9rt0uX4ut1eBrs6jeZeRhL/9az
# I2h15q/6/IvrC4DqaTuv/DDtBEyO3991bWORPdGdVk5Pv4BXIqF4ETIheu9BCrE/
# +6jMpF3BoYibV3FWTkhFwELJm3ZbCoBIa/15n8G9bW1qyVJzEw16UM0xggTGMIIE
# wgIBATCBlTB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgw
# JgYDVQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExAhMzAAAAww6b
# p9iy3PcsAAAAAADDMAkGBSsOAwIaBQCggdowGQYJKoZIhvcNAQkDMQwGCisGAQQB
# gjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkE
# MRYEFPWTq/3GsW095x1pB+u3k4O4JANoMHoGCisGAQQBgjcCAQwxbDBqoCiAJgBT
# AHQAbwByAGUAQgByAG8AawBlAHIAIABTAGkAZwBuAGkAbgBnoT6APGh0dHA6Ly9l
# ZHdlYi9zaXRlcy9JU1NFbmdpbmVlcmluZy9FbmdGdW4vU2l0ZVBhZ2VzL0hvbWUu
# YXNweDANBgkqhkiG9w0BAQEFAASCAQCYLatSIGnrrKPRYVNOEYdltGaG+j/AlbKp
# gzOIMkSe6qCmeWqN6e2F39C++tZ/SaWWkh2RQzID9Jd3VE/i2HetQJBmTTIF+X8n
# G9Z96Pzr/9Iui8srVvcp5gd3ewdZwrEa7F7EUgHrOsEwIdn8Cx9KGZVqCKdAlEnF
# L0Y3OGC8XPpZ+BJ0Y2tM/YP24qvyRMgLWE3O5acBIWViDE8pD8xAr4/F14HeF8gG
# 0YNikTbShI/rRuIJjgB7ViVhQlUwSMnFxSsVPBeCP+IE/q2uFNusEFaNpVTslpuL
# W72eLTDr1laaOZLfnAxQ5bNfgmLwI6KvvFEl52xNckJ6X/YqroqzoYICKDCCAiQG
# CSqGSIb3DQEJBjGCAhUwggIRAgEBMIGOMHcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
# ZnQgQ29ycG9yYXRpb24xITAfBgNVBAMTGE1pY3Jvc29mdCBUaW1lLVN0YW1wIFBD
# QQITMwAAAMRudtBNPf6pZQAAAAAAxDAJBgUrDgMCGgUAoF0wGAYJKoZIhvcNAQkD
# MQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMTcxMjE0MjMxMzA1WjAjBgkq
# hkiG9w0BCQQxFgQU6cRL04k8uZwnhnI3s6YQdZmok3QwDQYJKoZIhvcNAQEFBQAE
# ggEAgntX8RA3mnsu4W14aATLUMgo5zQmoT+1cyVWTOaHmpNuDPkkP+4mZP1MGRVz
# pxZmsjcG0mcqItTUS5kKUuDYOYpfgqkMk7VkcrwUFXlm2TbC7ycqFH0JTkOgWnou
# uKbenZYRx6guY9LB3I3SZ45zlxRP6KTbAtDShXda5+Jyb/ZahVtv509ZvNXqUf+v
# FgTI0lWkDImh9LG+Th9a5h38F4H8kIfckfgBOu62e0mjTUAY/XAGxj8F2bsPFmNQ
# 7j2NpRTHX0kzQHOifcn0G3I4wxnkdlWpcJPL2aYusjULDkscIYVuyQuGLpTte9QA
# MZL/GMdLEoyjbXEqMZCn5bcgew==
# SIG # End signature block