MSTeamsCQD.psm1

# (c)2018 Microsoft Corporation. All rights reserved. This document is provided "as-is." Information and views expressed in this document,
# including URL and other Internet Web site references, may change without notice. You bear the risk of using it.
# This document does not provide you with any legal rights to any intellectual property in any Microsoft product.
# You may copy and use this document for your internal, reference purposes. You may modify this document for your internal purposes
#
################################################
# CQD POWERSHELL ##############################
# JUSTW/SETHHA #
# QUERY CQD DIRECT FROM POWERSHELL AND EXPORT #
# #
# Modified by
# Luca Sain
################################################

# Date: 30/03/2022
# Features added
# added UNION filter to perform more advanced queries with AND/OR statements



[CmdletBinding()]
param(
  # Non Dynamic params go here #
)
###############################
# BEGIN JWT DECODE FUNCTIONS #
###############################
function Convert-64String ([string]$datai) {
  $datai = $datai.Replace('-', '+').Replace('_', '/')
  switch ($datai.Length % 4) {
    0 { break }
    2 { $datai += '==' }
    3 { $datai += '=' }
    default { throw New-Object ArgumentException ('datai') }
  }
  return [System.Convert]::FromBase64String($datai)
}
#
function Decode-JWT ([string]$rawToken) {
  $Tokenparts = $rawToken.Split('.');
  $headers = [System.Text.Encoding]::UTF8.GetString((Convert-64String $Tokenparts[0]))
  $claims = [System.Text.Encoding]::UTF8.GetString((Convert-64String $Tokenparts[1]))
  $signature = (Convert-64String $Tokenparts[2])

  $customObject = [pscustomobject]@{
    headers   = ($headers | ConvertFrom-Json)
    claims    = ($claims | ConvertFrom-Json)
    signature = $signature
  }

  #Write-Verbose -Message ("JWT`r`n.headers: {0}`r`n.claims: {1}`r`n.signature: {2}`r`n" -f $headers,$claims,[System.BitConverter]::ToString($signature))
  return $customObject
}
#
function Get-JWTData {
  [CmdletBinding()]
  param
  (
    # Param1 help description
    [Parameter(Mandatory = $true)]
    [string]$Token,
    [switch]$Recurse
  )

  if ($Recurse) {
    $decoded = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Token))
    $DecodedJwt = Decode-JWT -rawToken $decoded
  }
  else {
    $DecodedJwt = Decode-JWT -rawToken $Token
  }
  #Write-Host("Token Values") -ForegroundColor Green
  #Write-Host ($DecodedJwt | Select headers,claims | ConvertTo-Json)
  return $DecodedJwt
}
#############################
# END JWT DECODE FUNCTIONS #
#############################



###############################
# BEGIN AAD AUTH FUNCTIONS #
###############################


####Function to Get the JWT Token VIA OAuth
function Get-CQDToken ([string]$client_id) {

  if ($CQDVer -eq "V2") {
    $CQDUri = "https://cqd.lync.com/spd/"
    $V2Token = $true
  }
  else {
    $CQDUri = "https://cqd.teams.microsoft.com/spd/"
    $V3Token = $true
  }


  
  Add-Type -AssemblyName System.Web
  $resourceUrl = $WebResource
  $redirectUrl = $CQDUri
  $nonce = [guid]::NewGuid().GUID
  $url = "https://login.microsoftonline.com/common/oauth2/authorize?response_type=token&redirect_uri=" +
  [System.Web.HttpUtility]::UrlEncode($redirectUrl) +
  "&client_id=$client_id" +
  "&prompt=none" + "&nonce=$nonce" + "&resource=" + [System.Web.HttpUtility]::UrlEncode($WebResource)


  Add-Type -AssemblyName System.Windows.Forms

  $form = New-Object -TypeName System.Windows.Forms.Form -Property @{ Width = 440; Height = 640 }
  $web = New-Object -TypeName System.Windows.Forms.WebBrowser -Property @{ Width = 420; Height = 600; Url = ($url) }
  $DocComp = {
    $Global:uri = $web.Url.AbsoluteUri
    #Write-Host $Global:uri
    if ($Global:Uri -match "error=[^&]*|access_token=[^&]*") { $form.Close() }
  }

  $web.ScriptErrorsSuppressed = $true
  $web.Add_DocumentCompleted($DocComp)
  $form.Controls.Add($web)
  $form.Add_Shown({ $form.Activate() })
  $form.ShowDialog() | Out-Null



  $Script:TokenLifeTime = [Web.HttpUtility]::ParseQueryString(($web.Url -replace '^.*?(expires_in.+)$', '$1'))['expires_in']
  #Write-Host $TokenLifeTime
  $Script:Token = [Web.HttpUtility]::ParseQueryString(($web.Url -replace '^.*?(access_token.+)$', '$1'))['access_token']
  # Write-Host $Token
  
  
  return ('Bearer {0}' -f $Script:Token)
 
}

#Function to connect to CQD Via AzureAD
function Connect-CqdOnline {
  if ($CQDVer -eq "V2") {
    $UriVar = "https://repository.cqd.lync.com/clientconfiguration"
  }
  else {
    $UriVar = "https://cqd.teams.microsoft.com/repository/clientconfiguration"
  }

  $ClientConfigUri = $UriVar
  $Script:Configuration = Invoke-RestMethod -Uri $ClientConfigUri -Method Get -SessionVariable WebSession -UserAgent "CQDPowerShell V2.0"
  $Script:WebResource = $Configuration.AuthLoginResource
  $Script:AADBearerToken = Get-CQDToken $Configuration.AuthWebAppClientId
  $Script:WebSession = $WebSession
  $Script:RepositoryApiBaseUrl = $Script:Configuration.RepositoryApiBaseUrl
  $WebSession.headers.Add('Authorization', $AADBearerToken)
  $ProvisioningStatus = Invoke-RestMethod -Uri ('{0}tenant/provision' -f $RepositoryApiBaseUrl) -Method Get -WebSession $Script:WebSession -UserAgent "CQDPowerShell V2.0"
  if ($ProvisioningStatus.Status -ne 'Provisioned') {
    throw ('CQD Not Provisioned for TenantId {0}. Stopping...' -f $ProvisioningStatus.TenantId)
  }
  $Script:DataServiceBaseUrl = Invoke-RestMethod -Uri ('{0}tenant/dataservice' -f $Script:RepositoryApiBaseUrl) -WebSession $Script:WebSession -Method Get -UserAgent "CQDPowerShell V2.0"
  
}
Export-ModuleMember -Function Connect-CqdOnline

#Function to validate if token exists or is valid and unexpired
function CheckToken {
  #Check to see if Token already exists
  if ($Script:AADBearerToken -eq $null) {
    Connect-CqdOnline
  }
  else {
    #Check Token
    $JWTDecoded = Get-JWTData $Script:Token
    $JWTI = $JWTDecoded.claims.iat
    $JWTE = $JWTDecoded.claims.exp
    $NowTimeUTC = Get-Date

    $origin = New-Object -Type DateTime -ArgumentList 1970, 1, 1, 0, 0, 0, 0
    $JWTIssuedTime = $origin.AddSeconds($JWTI)
    $JWTExpireTime = $origin.AddSeconds($JWTE)
    #Write-Host $JWTIssuedTime $JWTExpireTime

    if ($CQDVer -eq "V2") {

      if ($JWTDecoded.claims.aud -eq "https://cqd.lync.com") {
        if ($NowTimeUTC.ToUniversalTime() -gt $JWTExpireTime) {
          Write-Host "Token Expired"
          Connect-CqdOnline
        }
      }
      else {
        write-host "Wrong Token, Expecting V2"
        Connect-CqdOnline 
      }
    }
    elseif ($CQDVer -eq "V3") {
      if ($JWTDecoded.claims.aud -eq "https://cqd.teams.microsoft.com") {
        if ($NowTimeUTC.ToUniversalTime() -gt $JWTExpireTime) {
          Write-Host "Token Expired"
          Connect-CqdOnline
        }
      }
      else {
        write-host "Wrong Token, Expecting V3"
        Connect-CqdOnline 
      }

    }

  


  }

}

###############################
# END AAD AUTH FUNCTIONS #
###############################

#Function to get Array list of Dimensions available in CQD Cube
function Get-CQDDimensions {
  $DimensionsCheck = GetParams "Dimensions"
  return $DimensionsCheck

}
Export-ModuleMember -Function Get-CQDDimensions

function Get-CQDMeasures {
  $MeasuresCheck = GetParams "Measures"
  return $MeasuresCheck
}
Export-ModuleMember -Function Get-CQDMeasures

##Generate Dynamic List of CQD Measures and Dimensions
function GetParams ([string]$ParamType) {


  CheckToken

  $CubeRequest = Invoke-WebRequest -Uri ('{0}CubeStructure' -f $DataServiceBaseUrl) -WebSession $WebSession -Method Get -UserAgent "CQDPowerShell V2.0"
  $List = ConvertFrom-Json $CubeRequest

  $D = $List.Dimensions
  $Meas = $List.Measurements
  $Dimensions = @()
  $Measures = @()
  foreach ($i in $D) {
    $Dimensions += $i.Category
    $Dimensions += $i.Attributes


  }

  foreach ($a in $Meas) {
    $Measures += $a.Category
    $Measures += $a.Attributes

  }

  if ($ParamType -eq "Dimensions") {
    $DimensionName = $Dimensions.DataModelName

    $Di = @()
    foreach ($O in $DimensionName) {

      $O = $O.Replace("[", "")
      $O = $O.Replace("]", "")
      $Di += $O
    }
    return $Di
  }
  elseif ($ParamType -eq "Measures") {
    $MeasureName = $Measures.DataModelName
    $Mi = @()
    foreach ($M in $MeasureName) {
      $M = $M.Replace("[", "")
      $M = $M.Replace("]", "")
      $Mi += $M
    }
    return $Mi
  }


}


#Function for handling start-end date
function ProcessDateRange {

  $TS = New-TimeSpan $StartDate $EndDate
  if ($TS.Days -lt 0) {
    $DateErrorStartEnd = 'Error in Timerange. Confirm StartDate is BEFORE EndDate'
    Write-Host $DateErrorStartEnd
    throw $DateErrorStartEnd
  }
  $i = $TS.Days
  if 
($StartDate -eq $EndDate)
  { $DateList = @($EndDate) }
  else {    
    $DateList = @($EndDate)
    do {

      $DateList += $EndDate.AddDays(-$i)
      $i --

    }

    while ($i -ne 0)
  }



  foreach ($D in $DateList) {

    $DateF += "[" + $D.ToString('yyyy-MM-dd') + "],"

  }

  return $DateF
}
#Function to obtain days of week
function GetDaysofWeek {

  foreach ($DofWT in $Weekdays) {

    $DofW += "[" + $DofWT + "],"

  }
  return $DofW
}



function BuildWeekFilter {
  param(
    [Parameter()] [array]$TimeInput
  )

  # Create list containing "start of weeks'" (Sun) for last 6 month
  $endDay = Get-Date
  $startDay = (Get-Date).AddMonths(-6)
  $cqdLastSixMonthWeeks = @()
  for ($i = $startDay; $i -lt $endDay; $i = $i.AddDays(1)) {
    if (($i.DayOfWeek).ToString() -eq "Sunday") {
      $cqdLastSixMonthWeeks += $i.ToString("yyyy-MM-dd")
    }
  }

  # Initialize variable that will keep final list of "start of weeks'"
  $cqdWeeks = @()

  # There are two cases:
  # 1. Month is provided as an input e.g. "09" for Sep. Must be in this format. length of 2
  # 2. Start of week is provided as an input e.g. "2017-09-18". Must be in this format. length of 10
  $TimeInput | ForEach-Object {
    if ($_.Length -eq 2) {
      $tempMonth = $_
      $cqdLastSixMonthWeeks | ForEach-Object {
        if ($_ -like "*-$($tempMonth)-*") { $cqdWeeks += $_ }
      }
    }
    elseif ($_.Length -eq 10) {
      $cqdWeeks += $_
    }
  }

  # Create hash table representing CQD Week filter
  $cqdFilterWeek = @{}

  $Caption = ""
  $Value = ""
  $cqdWeeks | ForEach-Object { $Caption += "$($_) | " }
  $cqdWeeks | ForEach-Object { $Value += "[$($_)]," }

  $cqdFilterWeek.Add("DataModelName", "[AllStreams].[Week]")
  $cqdFilterWeek.Add("Caption", $Caption)
  $cqdFilterWeek.Add("Value", $Value)
  $cqdFilterWeek.Add("Operand", 0)
  $cqdFilterWeek.Add("UnionGroup", "")

  # Return Json object representing CQD Week filter
  return ($cqdFilterWeek | ConvertTo-Json)

}

#Function for creating UA Category Filter
function BuildUACategoryFilter {
  param(
    [Parameter()] [array]$UACategory
  )

  # Create hash table representing CQD UA Category filter
  $cqdFilterUACategory = @{}

  $Caption = ""
  $Value = ""
  $UACategory | ForEach-Object { $Caption += "$($_) | " }
  $Caption | ForEach-Object {
    $Length = $Caption.Trim().Length
    $Caption = $Caption.Trim().Substring(0, $Length - 1)
    $Caption = $Caption.Trim()
  }

  $UACategory | ForEach-Object { $Value += "[$($_)]," }
  $Value | ForEach-Object {
    $Length = $Value.Trim().Length
    $Value = $Value.Trim().Substring(0, $Length - 1)
    $Value = $Value.Trim()
  }

  $cqdFilterUACategory.Add("DataModelName", "[AllStreams].[Second User Agent Category]")
  $cqdFilterUACategory.Add("Caption", $Caption)
  $cqdFilterUACategory.Add("Value", $Value)
  $cqdFilterUACategory.Add("Operand", 0)
  $cqdFilterUACategory.Add("UnionGroup", "")

  # Return Json object representing CQD UA Category filter
  return ($cqdFilterUACategory | ConvertTo-Json)
}

#Function for creating Media Type Filter
function BuildMediaTypeFilter {
  param(
    [Parameter()] [array]$MediaType
  )

  # Create hash table representing CQD Media Type filter
  $cqdFilterMediaType = @{}

  $Caption = ""
  $Value = ""
  $MediaType | ForEach-Object { $Caption += "$($_) | " }
  $Caption | ForEach-Object {
    $Length = $Caption.Trim().Length
    $Caption = $Caption.Trim().Substring(0, $Length - 1)
    $Caption = $Caption.Trim()
  }

  $MediaType | ForEach-Object { $Value += "[$($_)]," }
  $Value | ForEach-Object {
    $Length = $Value.Trim().Length
    $Value = $Value.Trim().Substring(0, $Length - 1)
    $Value = $Value.Trim()
  }

  $cqdFilterMediaType.Add("DataModelName", "[AllStreams].[Media Type]")
  $cqdFilterMediaType.Add("Caption", $Caption)
  $cqdFilterMediaType.Add("Value", $Value)
  $cqdFilterMediaType.Add("Operand", 0)
  $cqdFilterMediaType.Add("UnionGroup", "")

  # Return Json object representing CQD Media Type filter
  return ($cqdFilterMediaType | ConvertTo-Json)
}

#Function for Server Pair Filter
function BuildIsServerPairFilter {
  param(
    [Parameter()] [array]$IsServerPair
  )

  # Create hash table representing CQD Is Server Pair filter
  $cqdFilterIsServerPair = @{}

  $Caption = ""
  $Value = ""
  $IsServerPair | ForEach-Object { $Caption += "$($_) | " }
  $Caption | ForEach-Object {
    $Length = $Caption.Trim().Length
    $Caption = $Caption.Trim().Substring(0, $Length - 1)
    $Caption = $Caption.Trim()
  }

  $IsServerPair | ForEach-Object { $Value += "[$($_)]," }
  $Value | ForEach-Object {
    $Length = $Value.Trim().Length
    $Value = $Value.Trim().Substring(0, $Length - 1)
    $Value = $Value.Trim()
  }

  $cqdFilterIsServerPair.Add("DataModelName", "[AllStreams].[Is Server Pair]")
  $cqdFilterIsServerPair.Add("Caption", $Caption)
  $cqdFilterIsServerPair.Add("Value", $Value)
  $cqdFilterIsServerPair.Add("Operand", 0)
  $cqdFilterIsServerPair.Add("UnionGroup", "")

  # Return Json object representing CQD Is Server Pair filter
  return ($cqdFilterIsServerPair | ConvertTo-Json)
}

#Function for Transport Filter (TCP,UDP)
function BuildTransportFilter {
  param(
    [Parameter()] [array]$Transport
  )

  # Create hash table representing CQD transport filter
  $cqdFilterTransport = @{}

  $Caption = ""
  $Value = ""
  $Transport | ForEach-Object { $Caption += "$($_) | " }
  $Caption | ForEach-Object {
    $Length = $Caption.Trim().Length
    $Caption = $Caption.Trim().Substring(0, $Length - 1)
    $Caption = $Caption.Trim()
  }

  $Transport | ForEach-Object { $Value += "[$($_)]," }
  $Value | ForEach-Object {
    $Length = $Value.Trim().Length
    $Value = $Value.Trim().Substring(0, $Length - 1)
    $Value = $Value.Trim()
  }

  $cqdFilterTransport.Add("DataModelName", "[AllStreams].[Transport]")
  $cqdFilterTransport.Add("Caption", $Caption)
  $cqdFilterTransport.Add("Value", $Value)
  $cqdFilterTransport.Add("Operand", 0)
  $cqdFilterTransport.Add("UnionGroup", "")

  # Return Json object representing CQD transport filter
  return ($cqdFilterTransport | ConvertTo-Json)
}

#Function for building all Filters
function FilterCreation {

  #Build MonthYear Array From Input

  foreach ($MonthYearI in $MonthYear) {
    $MonthYearParams += "[" + $MonthYearI + "],"

  }

  $Filters = New-Object system.Data.DataTable "Filters"
  $FilterName = New-Object system.Data.DataColumn FilterName, ([string])
  $FilterValue = New-Object system.Data.DataColumn FilterValue, ([string])
  $FilterCaption = New-Object system.Data.DataColumn FilterCaption, ([string])
  $FilterOperand = New-Object system.Data.DataColumn FilterOperand, ([string])
  $FilterUnionGroup = New-Object system.Data.DataColumn FilterUnionGroup, ([string])

  #Add the Columns
  $Filters.columns.Add($FilterName)
  $Filters.columns.Add($FilterValue)
  $Filters.columns.Add($FilterCaption)
  $Filters.columns.Add($FilterOperand)
  $Filters.columns.Add($FilterUnionGroup)


  if ($Weekdays -ne $null) {
    $row = $Filters.NewRow()
    $row.FilterName = "AllStreams.Day Of Week" 
    $row.FilterValue = "$DaysofWeekList" 
    $row.FilterCaption = "NA" 
    $row.FilterOperand = "0" 
    $row.FilterUnionGroup = "" 
    $Filters.Rows.Add($row)
  }


  if ($MonthYear -ne $null) {
    #Create a row
    $row = $Filters.NewRow()
    $row.FilterName = "AllStreams.Month Year" 
    $row.FilterValue = "$MonthYearParams" 
    $row.FilterCaption = "NA" 
    $row.FilterOperand = "0" 
    $row.FilterUnionGroup = "" 
    $Filters.Rows.Add($row)
  }



  if ($StartDate -ne $null -and $EndDate -ne $null) {

    $EndDateV = ProcessDateRange
    #Create a row
    $row = $Filters.NewRow()
    $row.FilterName = "AllStreams.Date"
    $row.FilterValue = "$EndDateV" 
    $row.FilterCaption = "NA" 
    $row.FilterOperand = "0" 
    $row.FilterUnionGroup = "" 
    $Filters.Rows.Add($row)  

  }

  if ($Week -ne $null) {
    $WeekV = (BuildWeekFilter $Week) | ConvertFrom-Json
    $row = $Filters.NewRow()
    $row.FilterName = "AllStreams.Week"
    $row.FilterValue = $WeekV.Value
    $row.FilterCaption = "NA" 
    $row.FilterOperand = $WeekV.Operand 
    $row.FilterUnionGroup = $WeekV.UnionGroup 
    $Filters.Rows.Add($row)  
  }

  if ($UACategory -ne $null) {
    $UACategoryV = (BuildUACategoryFilter $UACategory) | ConvertFrom-Json
    $row = $Filters.NewRow()
    $row.FilterName = "AllStreams.Second User Agent Category"
    $row.FilterValue = $UACategoryV.Value
    $row.FilterCaption = "NA" 
    $row.FilterOperand = $UACategoryV.Operand 
    $row.FilterUnionGroup = $UACategoryV.UnionGroup 
    $Filters.Rows.Add($row)  
  }

  if ($MediaType -ne $null) {
    $MediaTypeV = (BuildMediaTypeFilter $MediaType) | ConvertFrom-Json
    $row = $Filters.NewRow()
    $row.FilterName = "AllStreams.Media Type"
    $row.FilterValue = $MediaTypeV.Value
    $row.FilterCaption = "NA" 
    $row.FilterOperand = $MediaTypeV.Operand 
    $row.FilterUnionGroup = $MediaTypeV.UnionGroup 
    $Filters.Rows.Add($row)  
  }

  if ($IsServerPair -ne $null) {
    $IsServerPairV = (BuildIsServerPairFilter $IsServerPair) | ConvertFrom-Json
    $row = $Filters.NewRow()
    $row.FilterName = "AllStreams.Is Server Pair"
    $row.FilterValue = $IsServerPairV.Value
    $row.FilterCaption = "NA" 
    $row.FilterOperand = $IsServerPairV.Operand 
    $row.FilterUnionGroup = $IsServerPairV.UnionGroup 
    $Filters.Rows.Add($row)  
  }

  if ($Transport -ne $null) {
    $TransportV = (BuildTransportFilter $Transport) | ConvertFrom-Json
    $row = $Filters.NewRow()
    $row.FilterName = "AllStreams.Transport"
    $row.FilterValue = $TransportV.Value
    $row.FilterCaption = "NA" 
    $row.FilterOperand = $TransportV.Operand 
    $row.FilterUnionGroup = $TransportV.UnionGroup 
    $Filters.Rows.Add($row)  
  }

  if ($null -ne $CustomFilter) {
    foreach ($F in $CustomFilter) {
      $row = $Filters.NewRow()
      $row.FilterName = $F.FName
      $row.FilterValue = $F.FValue
      $row.FilterCaption = ""
      $row.FilterOperand = $F.Op
      $row.FilterUnionGroup = $F.FUnionGroup
      $Filters.Rows.Add($row)

    }
  }

  return $Filters

}


function Get-CQDData {
  [CmdletBinding()]
  param(




    [Parameter(Mandatory = $false, Position = 3)]
    [array]$Transport = $null,

    [Parameter(Mandatory = $false, Position = 4)]
    [string]$OutPutFilePath = $null,

    [Parameter(Mandatory = $false, Position = 5)]
    [nullable[datetime]]$StartDate = $null,

    [Parameter(Mandatory = $false, Position = 6)]
    [nullable[datetime]]$EndDate = $null,

    [Parameter(Mandatory = $false, Position = 7)]
    [array]$MonthYear = $null,

    [Parameter(Mandatory = $false, Position = 8)]
    [array]$Weekdays = $null,

    [Parameter(Mandatory = $True, Position = 9)]
    [ValidateSet("CSV", "DataTable")]
    [string]$OutPutType,

    [Parameter(Mandatory = $false, Position = 10)]
    [array]$Week = $null,

    [Parameter(Mandatory = $false, Position = 11)]
    [array]$UACategory = $null,

    [Parameter(Mandatory = $false, Position = 12)]
    [array]$MediaType = $null,

    [Parameter(Mandatory = $false, Position = 13)]
    [ValidateSet("Client : Client", "Client : Server", "Server : Server")]
    [array]$IsServerPair = $null,

    [Parameter(Mandatory = $false, Position = 14)]
    [bool]$ShowQuery = $false,
    
    [Parameter(Mandatory = $false, Position = 15)]
    [switch] $OverWriteOutput,
  
    [Parameter(Mandatory = $false, Position = 16)]
    [pscustomobject] $CustomFilter,
  
    [Parameter(Mandatory = $false, Position = 17)]
    [switch] $LargeQuery,

    [Parameter(Mandatory = $false, Position = 18)]
    [ValidateSet("V2", "V3")]
    [string]$CQDVer = "V3"

  

  )


  dynamicparam {


    # Set the dynamic parameters' name for Dimensions and Measures
    $DimensionParameter = 'Dimensions'
    $MeasuresParameter = 'Measures'


    # Create the dictionary
    $RuntimeParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

    # Create the collection of attributes
    $DimensionAttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
    $AttributeCollectionMeasures = New-Object System.Collections.ObjectModel.Collection[System.Attribute]

    # Create and set the parameters' attributes
    $DimensionParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
    $DimensionParameterAttribute.Mandatory = $true
    $DimensionParameterAttribute.Position = 1

    $MeasuresParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
    $MeasuresParameterAttribute.Mandatory = $true
    $MeasuresParameterAttribute.Position = 2

    # Add the attributes to the attributes collection
    $DimensionAttributeCollection.Add($DimensionParameterAttribute)
    $AttributeCollectionMeasures.Add($MeasuresParameterAttribute)

    # Generate and set the ValidateSet
    $DimensionarrSet = GetParams "Dimensions"
    $MeasuresarrSet = GetParams "Measures"
    $DimensionsValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute ($DimensionarrSet)
    $MeasuresValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute ($MeasuresarrSet)

    # Add the ValidateSet to the attributes collection
    $DimensionAttributeCollection.Add($DimensionsValidateSetAttribute)
    $AttributeCollectionMeasures.Add($MeasuresValidateSetAttribute)

    # Create and return the dynamic parameter
    $DimensionsRuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter ($DimensionParameter, [array], $DimensionAttributeCollection)
    $MeasuresRuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter ($MeasuresParameter, [array], $AttributeCollectionMeasures)

    $RuntimeParamDictionary.Add($DimensionParameter, $DimensionsRuntimeParameter)
    $RuntimeParamDictionary.Add($MeasuresParameter, $MeasuresRuntimeParameter)
    return $RuntimeParamDictionary

  }

  begin {
    # Bind the parameter to a friendly variable
    $Dims = $PsBoundParameters[$DimensionParameter]
    $Measures = $PsBoundParameters[$MeasuresParameter]



  }

  process {

    try {
      Write-Host "Working...."

      #Check for Null CSV Path if CSV Chosen output
      if ($OutPutType -eq "CSV" -and $OutPutFilePath -eq "") {
        while ($OutPutFilePath -eq "" -or $OutPutFilePath -eq $null) {
          Write-Host "You must specify an output path when using CSV output type"
          Write-Host "Enter CSV Output Path:" -NoNewline
          $OutputFilePath = Read-Host
        }
        if ($OutputFilePath -eq $null) { Exit-PSSession }
      }

      $DimFixed = @()
      $DimSimple = @()
      $MeasureFixed = @()
      $MeasureSimple = @()
      ###Fix Dimension Brackets
      foreach ($DimShort in $Dims) {
        $Dim1 = $DimShort.Split(".")
        $DimSimple += $Dim1[1].ToString()
        $Dim1[0] = "[" + $Dim1[0] + "]"
        $Dim1[1] = "[" + $Dim1[1] + "]"
        $DimFixed += $Dim1[0] + "." + $Dim1[1]
      }

      foreach ($MeasureShort in $Measures) {
        $Measure1 = $MeasureShort.Split(".")
        $MeasureSimple += $Measure1[1].ToString()
        $Measure1[0] = "[" + $Measure1[0] + "]"
        $Measure1[1] = "[" + $Measure1[1] + "]"
        $MeasureFixed += $Measure1[0] + "." + $Measure1[1]
      }

      $Dims = $DimFixed
      $Measures = $MeasureFixed




      if ($Weekdays -ne $null) {
        $DaysofWeekList = GetDaysofWeek
      }
      else {
        $Weekdays = $null
      }







      $Filters = FilterCreation


      ##Dimension and Measure Arrays##
      $DimensionsArray = $Dims 
      $MeasurementsArray = $Measures 


      ##Create Results Table##
      $DataResults = New-Object system.Data.DataTable "DataResults"
      foreach ($DimensionVar in $DimSimple) {
        $Column = New-Object system.Data.DataColumn $DimensionVar, ([string])
        $DataResults.columns.Add($Column)
      }

      foreach ($MeasureVar in $MeasureSimple) {
        $Column = New-Object system.Data.DataColumn $MeasureVar, ([string])
        $DataResults.columns.Add($Column)
      }

      $FilterRowArray = @()
      $DimensionRowArray = @()
      $MeasureRowArray = @()
      $AllArray = $DimSimple + $MeasureSimple


      ##Build Query Structure##

      #Start/End Query
      $StartQuery = "{"
      $EndQuery = "}"

      #Filter Sections
      $StartFilters = @'
"Filters":[
'@


      #Dimension Sections
      $StartDimensions = @'
"Dimensions":[
'@


      #Measure Sections
      $StartMeasures = @'
"Measurements":[
'@



      ###############Loop Arrays of Each Type

      foreach ($Filter in $Filters) {
        $FLN = $Filter.FilterName
        $FCN = $Filter.FilterCaption
        $FVE = $Filter.FilterValue
        $FOP = $Filter.FilterOperand
        $UNGP = $Filter.FilterUnionGroup
        $FLNSplit = $FLN.Split(".")
        $FLNName = "[" + $FLNSplit[0] + "].[" + $FLNSplit[1] + "]" 
        $FilterRowArray += @"
{"DataModelName":"$FLNName","Caption":"$FCN","Value":"$FVE","Operand":$FOP,"UnionGroup":"$UNGP"},
"@

      }
      $DimCount = $DimensionsArray.Count - 1
      foreach ($DimensionLoop in $DimensionsArray) {

        $DimensionRowArray += @"
{"DataModelName":"$DimensionLoop"},
"@


      }
      $MeaCount = $MeasurementsArray.Count - 1
      foreach ($MeasureLoop in $MeasurementsArray) {

        $MeasureRowArray += @"
{"DataModelName":"$MeasureLoop"},
"@


      }
      ########Build Dynamic Query
      if ($LargeQuery) {

        $Query = @"
  {
    
  $StartFilters
  $FilterRowArray
    
  ]
  ,
  $StartDimensions
  $DimensionRowArray
  ]
  ,
  $StartMeasures
  $MeasureRowArray
  ]
  ,
  LimitResultRowsCount:200000
  }
"@


      }
      else {
  
        $Query = @"
  {
    
  $StartFilters
  $FilterRowArray
    
  ]
  ,
  $StartDimensions
  $DimensionRowArray
  ]
  ,
  $StartMeasures
  $MeasureRowArray
  ],
  LimitResultRowsCount:200000
  }
"@


      }


      #Check to see if ShowQuery is true. Used to show JSON query for debugging queries.
      if ($ShowQuery -eq $true) {
        Write-Host "JSON Query:"
        Write-Host $Query
      }

      CheckToken
      $verb = "POST"
      $content = $Query
      $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
      $IRM = Invoke-RestMethod -Uri ('{0}RunQuery' -f $DataServiceBaseUrl) -WebSession $WebSession -Method Post -Body $Query -ContentType 'application/json' -UserAgent "CQDPowerShell V2.0" -Headers $headers


      $IRM2 = ConvertTo-Json -InputObject $IRM
      $JsonGarb = ConvertFrom-Json -InputObject $IRM2

      $QueryExecutionTime = $JsonGarb.ExecutionTime
      $ErrorType = $JsonGarb.ErrorType
      $ResultList = $JsonGarb.DataResult

      if ($ErrorType -ne 0) {
        Write-Host "Error Querying CQD with Error " $ErrorType
        if ($ErrorType -eq 6) { Write-Host "Invalid Query. Check the query and try again." }
        if ($ErrorType -eq 5) { Write-Host "Query scope too large. Reduce Dimensions, Measures and Timespan" }
        if ($ErrorType -eq 3) { Write-Host "Cube currently unavailable. Try again later." }
      }



      foreach ($ResultItem in $ResultList) {
        if (!$ResultItem) {
          #if(1 -eq 2)
          Write-Host "Warning: Result is Null:" $ResultItem

        }
        else {
          $ItemCount = $ResultItem.Count
          $FieldCount = $DimensionsArray.Count + $MeasurementsArray.Count
          #Create a row
          $Resultrow = $DataResults.NewRow()


          $i = 0

          do {

            $Resultrow.($AllArray[$i]) = $ResultItem.Item($i)
            $i++
          }
          while ($i -lt $AllArray.Count)

          $DataResults.Rows.Add($Resultrow)

        }

      }

      if ($OutPutType -eq "CSV") {
        if ($OverWriteOutput -eq $true) {
          $DataResults | Export-Csv -Path $OutPutFilePath -notypeinformation 
        }
        else {
          $DataResults | Export-Csv -Path $OutPutFilePath -notypeinformation -Append
        }
      }
      elseif ($OutPutType -eq "DataTable") {
        return $DataResults
      }

      Write-Host "Execution Complete. Query took " $QueryExecutionTime
    }
    catch [System.Net.WebException] {
      "Unable to complete query. Please reduce query scope or try again later."
    }
    catch [System.Management.Automation.ParameterBindingException] { "Error Binding" }

    catch { "Error: $Error" }

  }

}
Export-ModuleMember -Function Get-CQDData