powershellxml-parsingxsd-validation

XML Parser in Powershell and move file after validation


The following script already works pretty good. I can validate my given xml file against the xsd file and log all validation errors. My issue is, I want to move the xml file, if no validation error occurs. But I don't know where to place the move-item statement. I always get the error file can not moved because it is still in use. Here is the code. Hopefully someone can help.

# Get all XML files in the folder that start with "products"
$xmlFiles = Get-ChildItem -Path $sourceDir -Filter products*.xml -File

foreach ($xmlFile in $xmlFiles) {
    $path = $xmlFile.FullName #$sourceDir + $xmlFile

    # Checking if file exits
    if (-not (Test-Path $path)) { 
        Write-Warning "The XML file $path does not exist."
        logger -ErrMessage "File not exits." -log_file $logfile -xml_file $path

    # Checking if file is readable
    } elseif ($xmlFile.IsReadOnly) {
        Write-Warning "XML file not readable: $path"
        logger -ErrMessage "File not readable." -log_file $logfile -xml_file $path
    
    } else {
        # Set up validation settings
        $valSettings = [System.Xml.XmlReaderSettings]::new();
        $valSettings.ValidationType = [System.Xml.ValidationType]::Schema;
        $valSettings.CheckCharacters = $true;
        $valSettings.ValidationFlags = ([System.Xml.Schema.XmlSchemaValidationFlags]::ProcessIdentityConstraints -bor [System.Xml.Schema.XmlSchemaValidationFlags]::ReportValidationWarnings)
        $valSettings.Schemas.Add($URL, $xsdFilename);

        # Store validation errors in an array
        $errors = New-Object System.Collections.Generic.List[System.Xml.Schema.ValidationEventArgs]

        # Create a validation event handler
        $validationHandler = {
            param ([object] $sender, [System.Xml.Schema.ValidationEventArgs] $e)

            $lineNumber = ""
            $linePosition = ""

            # Get the line number and position of the error
          #  if ($e.Exception) {
                $lineNumber = $e.Exception.LineNumber
                $linePosition = $e.Exception.LinePosition
          #  }

            if ($e.Severity -eq [System.Xml.Schema.XmlSeverityType]::Warning) {
                Write-Host("WARNING___: ")
                $errors.Add($e)
                Write-Warning($e.Message, $lineNumber)
                Write-Host("--Line: $lineNumber, Position: $linePosition")
                logger -ErrMessage $e.Message -line $lineNumber -log_file $logfile -xml_file $path
            } 
            elseif ($e.Severity -eq [System.Xml.Schema.XmlSeverityType]::Error) {
                if ($e.Exception -and $e.Exception.Message.StartsWith("The 'text' element is not declared.")) {
                    $e.Severity = [System.Xml.Schema.XmlSeverityType]::Warning
                    Write-Host("WARNING___: ")
                    $errors.Add($e)
                    Write-Warning($e.Exception.Message)
                    Write-Host("--Line: $lineNumber, Position: $linePosition")
                    logger -ErrMessage $e.Message -line $lineNumber -log_file $logfile -xml_file $path
                }
                else {
                    $errors.Add($e)
                    Write-Warning $e.Message;
                    Write-Host("--Line: $lineNumber, Position: $linePosition");
                    if (![string]::IsNullOrEmpty($logfile)) {
                        #write-host $e.Exception.GetType().FullName
                        logger -ErrMessage $e.Message -line $lineNumber -log_file $logfile -xml_file $path
                    } 
                }
            } 
        }

        # Set up validation event handler to capture errors
        #$valSettings.ValidationEventHandler += {$errors.Add($_)}#/ turn it on, if 'Add_ValidationEventHandler' runs into error

        $destination = "C:\Users\BeKa-Coca\Downloads\XML_Validation\$xmlFile"

        try {
            # Read each xml node, validate it and close the xml file
        $valSettings.Add_ValidationEventHandler($validationHandler)   
        $xmlReader = [System.Xml.XmlReader]::Create([System.IO.File]::OpenRead($path), $valSettings);
            while ($xmlReader.Read()) { }
            $xmlReader.Close()
            #Copy-Item $xmlFile.FullName $destination
            #Move-Item $xmlFile.FullName $destination -Force

        } catch{
            $lineNumber = ""
            $linePosition = ""
         #   if ($_.Exception) {
                $lineNumber = [regex]::Match($_.Exception.Message, "Zeile (\d+)").Groups[1].Value
                $linePosition = [regex]::Match($_.Exception.Message, "Position (\d+)").Groups[1].Value
         #   }
            $errorDesc = $_.Exception.Message  
            write-warning $_.Exception.Message
            Write-Host("--Line: $lineNumber, Position: $linePosition")
            logger -ErrMessage $errorDesc -line $lineNumber -log_file $logfile -xml_file $path
        }
        finally {

            # make sure the xml reader is closed even if an exception occurs
           if ($xmlReader) {
                $xmlReader.Dispose()

                # Copy / move only valid xml files
                If (-not($errors)) {
                    #Copy-Item $xmlFile.FullName $destination -Force
                    Move-Item $xmlFile.FullName $destination -Force
                } else {WRITE-Host "ERROR!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"}
            }
        }
    } #End If-else

             #Copy-Item $xmlFile.FullName $destination
             #logger -ErrMessage "The file is in use by another process." -log_file $logfile -xml_file $path

 } #End ForEach Loop

The created xmlReader gets closed, before i try to move the file. But the file is still blocks and I don't know why.


Solution

  • Ok, try this new rewrite of your code.
    Of course, I cannot see what your function logger does exactly, but I can't imagine that is causing the file to stay locked.

    I did change the code now to initialize all validation settings variables on top AND adde this: $valSettings.CloseInput = $true, to make sure the underlying stream or TextReader gets closed. See CloseInput

    # Set up validation settings
    $valSettings = [System.Xml.XmlReaderSettings]::new()
    $valSettings.ValidationType  = [System.Xml.ValidationType]::Schema
    $valSettings.CheckCharacters = $true
    $valSettings.CloseInput      = $true  # close the underlying stream or TextReader when the reader is closed
    $valSettings.ValidationFlags = ([System.Xml.Schema.XmlSchemaValidationFlags]::ProcessIdentityConstraints -bor 
                                    [System.Xml.Schema.XmlSchemaValidationFlags]::ReportValidationWarnings)
    $valSettings.Schemas.Add($URL, $xsdFilename)
    
    # Store validation errors in an List object
    $errors = New-Object System.Collections.Generic.List[System.Xml.Schema.ValidationEventArgs]
    
    # find all xml files in the source directory
    $xmlFiles = Get-ChildItem -Path $sourceDir -Filter 'products*.xml' -File
    
    foreach ($xmlFile in $xmlFiles) {
        $errors.Clear()
        $path = $xmlFile.FullName
        if ($xmlFile.IsReadOnly) {
            Write-Warning "XML file not readable: $path"
            logger -ErrMessage "File not readable." -log_file $logfile -xml_file $path
            continue   # skip this file and proceed with the next
        } 
    
        # Create a validation event handler
        $validationHandler = {
            param ([object] $sender, [System.Xml.Schema.ValidationEventArgs] $e)
            # Get the line number and position of the error
            $lineNumber   = $e.Exception.LineNumber
            $linePosition = $e.Exception.LinePosition
    
            if ($e.Severity -eq [System.Xml.Schema.XmlSeverityType]::Warning) {
                Write-Host "WARNING___: "
                [void]$errors.Add($e)
                Write-Warning ($e.Message, $lineNumber)
                Write-Host "--Line: $lineNumber, Position: $linePosition"
                logger -ErrMessage $e.Message -line $lineNumber -log_file $logfile -xml_file $path
            } 
            elseif ($e.Severity -eq [System.Xml.Schema.XmlSeverityType]::Error) {
                if ($e.Exception.Message -like "The 'text' element is not declared*") {
                    Write-Host "ERROR___: "
                    [void]$errors.Add($e)
                    Write-Warning $e.Exception.Message
                    Write-Host "--Line: $lineNumber, Position: $linePosition"
                    logger -ErrMessage $e.Message -line $lineNumber -log_file $logfile -xml_file $path
                }
                else {
                    [void]$errors.Add($e)
                    Write-Warning $e.Message
                    Write-Host "--Line: $lineNumber, Position: $linePosition" 
                    # write-host $e.Exception.GetType().FullName
                    logger -ErrMessage $e.Message -line $lineNumber -log_file $logfile -xml_file $path
                }
            } 
        }
    
        $destination = 'C:\Users\BeKa-Coca\Downloads\XML_Validation'  # just the path here
    
        try {
            # Read each xml node, validate it and close the xml file
            $valSettings.Add_ValidationEventHandler($validationHandler)   
            $xmlReader = [System.Xml.XmlReader]::Create([System.IO.File]::OpenRead($path), $valSettings)
            while (!$xmlReader.EOF -and $xmlReader.Read()) {}
            $xmlReader.Dispose()
            $xmlReader = $null
            # Copy / move only valid xml files
            if ($errors.Count -eq 0) {
                #$xmlFile | Copy-Item -Destination $destination
                $xmlFile | Move-Item -Destination $destination -Force
            }
        } 
        catch{
            $lineNumber   = [regex]::Match($_.Exception.Message, "Zeile (\d+)").Groups[1].Value
            $linePosition = [regex]::Match($_.Exception.Message, "Position (\d+)").Groups[1].Value
            $errorDesc    = $_.Exception.Message
            Write-Warning $errorDesc
            Write-Host "--Line: $lineNumber, Position: $linePosition"
            logger -ErrMessage $errorDesc -line $lineNumber -log_file $logfile -xml_file $path
        }
        finally {
            # make sure the xml reader is closed even if an exception occurs
            if ($xmlReader) { $xmlReader.Dispose() }
            $xmlReader = $null
        }
    } # End foreach loop