src/cmdlets/Get-GraphUriInfo.ps1

# Copyright 2019, Adam Edwards
#
# 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.

. (import-script ../metadata/GraphManager)
. (import-script common/SegmentHelper)
. (import-script common/GraphUriParameterCompleter)

function Get-GraphUriInfo {
    [cmdletbinding()]
    param(
        [parameter(parametersetname='FromUriParents', position=0, mandatory=$true)]
        [parameter(parametersetname='FromUriChildren', position=0, mandatory=$true)]
        [parameter(parametersetname='FromUri', position=0, mandatory=$true)]
        [Uri] $Uri,

        [parameter(parametersetname='FromUriChildren', mandatory=$true)]
        [parameter(parametersetname='FromObjectChildren', mandatory=$true)]
        [Switch] $Children,

        [parameter(parametersetname='FromUriChildren')]
        [parameter(parametersetname='FromObjectChildren')]
        [Switch] $LocatableChildren,

        [parameter(parametersetname='FromUriChildren')]
        [parameter(parametersetname='FromObjectChildren')]
        [uint16] $RecursionDepth = 1,

        [parameter(parametersetname='FromUriChildren')]
        [parameter(parametersetname='FromObjectChildren')]
        [Switch] $IncludeVirtualChildren,

        [parameter(parametersetname='FromUriParents', mandatory=$true)]
        [parameter(parametersetname='FromObjectParents', mandatory=$true)]
        [Switch] $Parents,

        [parameter(parametersetname='FromUriChildren')]
        [parameter(parametersetname='FromObjectChildren')]
        [Switch] $NoCycles,

        [Switch] $IgnoreMissingMetadata,

        [String] $GraphName = $null,

        [parameter(parametersetname='FromObjectParents', valuefrompipeline=$true, mandatory=$true)]
        [parameter(parametersetname='FromObjectChildren', valuefrompipeline=$true, mandatory=$true)]
        [parameter(parametersetname='FromObject', valuefrompipeline=$true, mandatory=$true)]
        [PSCustomObject] $GraphItem
    )

    Enable-ScriptClassVerbosePreference

    $inputs = if ( $graphItem ) {
        $graphItem
    } else {
        $Uri
    }

    $results = @()

    $nextUris = new-object System.Collections.Generic.Queue[object[]]
    $inputs | foreach { $nextUris.Enqueue(@(0, $_)) }

    $DisallowedLocationClasses = if ( ! $IncludeVirtualChildren.IsPresent ) {
        @('EntityType')
    } else {
        @()
    }

    $validLocationClasses = if ( $LocatableChildren.ispresent ) {
        $allLocationClasses = $::.SegmentHelper.GetValidLocationClasses()
        $allLocationClasses | where { $_ -notin $DisallowedLocationClasses }
    } else {
        $null
    }

    $disallowVirtualChildren = $Children.ispresent -and ! $IncludeVirtualChildren.ispresent

    $context = if ( $GraphName ) {
        $::.Logicalgraphmanager.Get().contexts[$GraphName].Context
    }

    while ( $nextUris.Count -gt 0 ) {
        $currentItem = $nextUris.Dequeue()
        $currentDepth = $currentItem[0] + 1
        $currentUri = $currentItem[1]

        $graphCurrentItem = if ( $GraphItem ) {
            $currentUri
        }

        $uriSource = $currentUri

        $responseObject = $graphItem

        $inputUri = if ( $graphCurrentItem ) {
            if ( $graphCurrentItem | gm -membertype ScriptMethod __ItemMetadata -erroraction ignore ) {
                $metadata = $graphCurrentItem.__ItemMetadata()
                $context = $::.LogicalGraphManager.Get().GetContext($metadata.GraphName)
                $metadata.GraphUri
                $responseObject = $null
            } else {
                $uriFromResponse = $::.GraphUtilities.GetAbstractUriFromResponseObject($graphCurrentItem, $true, $null)

                if ( ! $uriFromResponse ) {
                    throw 'The specified object was not a valid Graph response object'
                }

                # Allow the caller to supply a context
                if ( ! $context ) {
                    $context = 'GraphContext' |::> GetCurrent
                }
                $uriFromResponse
            }
        } else {
            # TODO: Remove usage of ParseGraphRelativeLocation or update it -- turns out that if you
            # provide an absolute URI, it has non-deterministic behavior. :( Also, even for relative URIs
            # it assumes the default context which means you end up with this as the context even
            # though it wasn't specified in the URI. This is harmless unless the GraphName was specified
            # to this command, in which case it gets ignored.
            $parsedLocation = $::.GraphUtilities |=> ParseGraphRelativeLocation $currentUri
            if ( $parsedLocation.Context -and ! $GraphName) {
                # TODO: remove check for GraphName -- we are allowing specification of a URI with
                # a graph name in it to be overridden -- this is the lesser of two bad choices. We
                # can remove this capability once ParseGraphRelativeLocation is fixed.
                $context = $parsedLocation.Context
            }
            $parsedLocation.GraphRelativeUri
        }

        $parser = new-so SegmentParser $context $null ( $graphItem -ne $null )

        write-verbose "Uri '$uriSource' translated to '$inputUri'"

        $mustIgnoreMissingMetadata = $IgnoreMissingMetadata.IsPresent -or ! (__Preference__MustWaitForMetadata)

        $contextReady = ($::.GraphManager |=> GetMetadataStatus $context) -eq [MetadataStatus]::Ready

        if ( $mustIgnoreMissingMetadata -and ! $contextReady ) {
            if ( ! $Children.IsPresent ) {
                return $::.SegmentHelper |=> ToPublicSegment $parser $::.GraphSegment.NullSegment
            }
            return @()
        }

        $segments = $::.SegmentHelper |=> UriToSegments $parser $inputUri $responseObject

        $lastSegment = $segments | select -last 1

        $segmentTable = $null
        if ( $NoCycles.IsPresent ) {
            $segmentTable = @{}
            $segments | foreach { $segmentTable.Add($_.graphElement, $_) }
        }

        $instanceId = if ( $graphCurrentItem ) {
            $typeData = ($lastSegment.graphElement |=> GetEntity).typedata
            if ( $typeData.IsCollection ) {
                if ( $graphcurrentItem | gm -membertype noteproperty id -erroraction ignore) {
                    $graphcurrentItem.id
                } else {
                    $null
                }
            }
        }

        $lastPublicSegment = $::.SegmentHelper |=> ToPublicSegment $parser $lastSegment

        $count = if ( $Parents.ispresent ) {
            if ( $segments -is [Object[]] ) { $segments.length } else { 1 }
        } else {
            if ( $instanceId -or $Children.ispresent ) { 0 } else { 1 }
        }

        $segments | select -last $count | foreach {
            $results += ($::.SegmentHelper |=> ToPublicSegment $parser $_)
        }

        $childSegments = $null

        if ( $instanceId ) {
            $idSegment = $lastSegment |=> NewNextSegments ($::.GraphManager |=> GetGraph $context) $instanceId $validLocationClasses

            $additionalSegments = if ( $Children.IsPresent ) {
                $childSegments = $parser |=> GetChildren $idSegment $validLocationClasses | sort-object Name
            } else {
                # Create a new public segment since we are going to modify it
                $instanceSegment = ($::.SegmentHelper |=> ToPublicSegment $parser $idSegment $lastPublicSegment).psobject.copy()
                if ( $graphCurrentItem ) {
                    $::.SegmentHelper.AddContent($instanceSegment, $graphCurrentItem)
                    $::.SegmentHelper.GetNewObjectWithMetadata($graphCurrentItem, $instanceSegment)
                } else {
                    $instanceSegment
                }
            }

            $additionalSegments | foreach {
                $metadata = $_.__ItemMetadata()
                if ( ! $segmentTable -or $segmentTable[$metadata.graphElement] ) {

                    if ( $::.SegmentHelper.IsValidLocationClass($metadata.Class) -and ( $metadata.class -ne 'EntityType' ) ) {
                        $nextUris.Enqueue(@($currentDepth, $metadata.GraphUri))
                    }
                } else {
                    write-verbose "$($_.id) already exists in hierarchy $($metadata.graphUri)"
                }
            }

            $results += $additionalSegments
        } elseif ( $Children.ispresent ) {
            $childSegments = $parser |=> GetChildren $lastSegment $validLocationClasses | sort-object Name
        } else {
            if ( $graphCurrentItem ) {
                # Create a new public segment since we are going to modify it
                $lastOutputSegment = ($results | select -last 1).psobject.copy()
                $::.SegmentHelper.AddContent($lastOutputSegment, $graphCurrentItem)
                $objectWithMetadata = $::.SegmentHelper.GetNewObjectWithMetadata($graphCurrentItem, $lastOutputSegment)
                # Replace the segment in the collection with a new one
                # This is a rather convoluted approach :( -- needs a rewrite
                $results[$results.length - 1] = $objectWithMetadata
            }
        }

        if ( $childSegments ) {
            $publicChildSegments = @()
            $childSegments | foreach {
                $meetsLocationRequirement = ! $LocatableChildren.IsPresent -or $::.SegmentHelper.IsValidLocationClass(($_.graphElement |=> GetEntity).Type)
                $meetsNonvirtualRequirement = ! $disallowVirtualChildren -or ! $_.isVirtual
                $skipSegment = ! $meetsLocationRequirement -or ! $meetsNonvirtualRequirement
                if ( ! $skipSegment ) {
                    $publicChildSegments += ($::.SegmentHelper |=> ToPublicSegment $parser $_ $lastPublicSegment)
                }
            }

            if ( $currentDepth -lt $RecursionDepth ) {
                $publicChildSegments | foreach {
                    if ( ! $segmentTable -or ( ! $segmentTable[$_.details.graphElement] ) ) {
                        if ( $::.SegmentHelper.IsValidLocationClass($_.Class) -and ( $_.class -ne 'entitytype') ) {
                            $nextUris.Enqueue(@($currentDepth, $_.GraphUri))
                        }
                    } else {
                        write-verbose "$($_.id) already exists in hierarchy $($_.GraphUri)"
                    }
                }
            }

            $results += $publicChildSegments
        }
    }
    $results
}

$::.ParameterCompleter |=> RegisterParameterCompleter Get-GraphUriInfo Uri (new-so GraphUriParameterCompleter ([GraphUriCompletionType]::AnyUri))