MyJavaManager.psm1

#
# Script module for module 'MyJavaManager'
#

#Requires -Version 5.1

#using namespace System.Management.Automation


#region Classes
class PathFormat {
    static [string] NormalizePath([string] $Path) {

        <#
        .Description
        Format a given string to a normalized path.
        #>


        $Path_ = $Path
        $Path_ = [System.IO.Path]::GetFullPath($Path_)
        $Path_ = $Path_.TrimEnd([System.IO.Path]::DirectorySeparatorChar)
        return $Path_
    }
}
class InventoryEntry {
    [string] $Name
    [string] $Path

    InventoryEntry([string] $Name, [string] $Path) {
        $this.Name = $Name
        $this.Path = [PathFormat]::NormalizePath($Path)
    }
}
class Inventory {
    [string] $Path = (Join-Path -Path $HOME -ChildPath "MyJavaManager.json")
    [string] $Encoding = "UTF8"
    [InventoryEntry[]] $JavaEntries

    [InventoryEntry] GetJavaEntry([string] $Name) {

        <#
        .Description
        Get a Java entry from the inventory.
        #>


        return $this.JavaEntries | Where-Object -Property Name -EQ $Name
    }

    [bool] JavaEntryExists([string] $Name) {

        <#
        .Description
        Test if a Java entry exists in the inventory.
        #>


        return $this.GetJavaEntry($Name).Count -gt 0
    }

    [void] AddJavaEntry([string] $Name, [string] $Path) {

        <#
        .Description
        Add a Java entry to the inventory.
        #>


        $this.JavaEntries += New-Object -TypeName InventoryEntry -ArgumentList $Name, $Path
        Write-Debug -Message "Java entry `"$Name`" has been added to the inventory."
    }

    [void] RemoveJavaEntry([string] $Name) {

        <#
        .Description
        Remove a Java entry from the inventory.
        #>


        $this.JavaEntries = $this.JavaEntries | Where-Object -Property Name -NE $Name
        Write-Debug -Message "Java entry `"$Name`" has been removed from the inventory."
    }

    [bool] FileExists() {

        <#
        .Description
        Test if the inventory file exists.
        #>


        return Test-Path -Path $this.Path -PathType Leaf
    }

    [void] ReadFile() {

        <#
        .Description
        Read Java entries from inventory file.
        #>


        Write-Debug -Message "Start to read the inventory file."

        # Get content of the inventory file
        $GetContentParams = @{
            Path     = $this.Path
            Encoding = $this.Encoding
        }
        try {
            $Content = Get-Content @GetContentParams -ErrorAction Stop
        }
        catch {
            throw "Cannot get content of the inventory file: {0}" -f $_.Exception.Message
        }

        # Convert from JSON
        $Items = $Content | ConvertFrom-Json

        # Re-initialize JavaEntries array
        $this.JavaEntries = @()

        # Add JavaEntry objects to JavaEntries array
        foreach ($Item in $Items) {
            $this.AddJavaEntry($Item.Name, $Item.Path)
        }

        Write-Debug -Message "The inventory file has been read."
    }

    [void] SaveFile() {

        <#
        .Description
        Save inventory in file.
        #>


        Write-Debug -Message "Start to save the inventory file."

        # Set content of the inventory file
        $SetContentParams = @{
            Path     = $this.Path
            Value    = ConvertTo-Json -InputObject @($this.JavaEntries)
            Encoding = $this.Encoding
        }
        try {
            Set-Content @SetContentParams -ErrorAction Stop
        }
        catch {
            throw "Cannot set content of the inventory file: {0}" -f $_.Exception.Message
        }

        Write-Debug -Message "The inventory file has been saved."
    }
}
class AdoptiumApi {

    # API documentation: https://api.adoptium.net/q/swagger-ui/

    static [int[]] GetAvailableFeatureVersions() {

        <#
        .Description
        Get the available feature versions of Java.
        #>


        $RestMethodParams = @{
            Method = "Get"
            Uri    = "https://api.adoptium.net/v3/info/available_releases"
        }
        $RestMethodResult = Invoke-RestMethod @RestMethodParams

        return $RestMethodResult.available_releases
    }

    static [object] GetLatestAsset([int] $FeatureVersion) {

        <#
        .Description
        Get the latest assets of a Java feature version.
        #>


        $RestMethodParams = @{
            Method = "Get"
            Uri    = "https://api.adoptium.net/v3/assets/latest/$FeatureVersion/hotspot"
        }
        $RestMethodResult = Invoke-RestMethod @RestMethodParams

        return $RestMethodResult | Where-Object -FilterScript {
            $_.binary.architecture -eq "x64" -and
            $_.binary.heap_size -eq "normal" -and
            $_.binary.image_type -eq "jdk" -and
            $_.binary.jvm_impl -eq "hotspot" -and
            $_.binary.os -eq "windows" -and
            $_.binary.project -eq "jdk"
        } | Select-Object -First 1
    }
}
class AdoptiumPackage {
    [string] $Name
    [string] $Uri
    [string] $Checksum

    AdoptiumPackage([int] $FeatureVersion) {
        $Asset = [AdoptiumApi]::GetLatestAsset($FeatureVersion)

        $this.Name = $Asset.release_name
        $this.Uri = $Asset.binary.package.link
        $this.Checksum = $Asset.binary.package.checksum
    }

    [System.IO.FileSystemInfo] Download($DestinationDirectory) {

        <#
        .Description
        Download and expand a Java package from Adoptium to a destination directory.
        #>


        $TempGuid = New-Guid
        $TempDirectory = Join-Path -Path $env:TEMP -ChildPath "java_$TempGuid"

        $DownloadDestinationArchive = Join-Path -Path $TempDirectory -ChildPath "package.zip"

        try {
            # Create temporary directory
            New-Item -Path $TempDirectory -ItemType Directory | Out-Null
            Write-Debug -Message "Temporary directory created `"$TempDirectory`"."

            # Download archive
            $WebClient = New-Object -TypeName System.Net.WebClient
            $WebClient.DownloadFile($this.Uri, $DownloadDestinationArchive)
            Write-Debug -Message ("Java archive `"$DownloadDestinationArchive`" downloaded from `"{0}`"." -f $this.Uri)

            # Validate checksum
            if ((Get-FileHash -Path $DownloadDestinationArchive).Hash -eq $this.Checksum) {
                Write-Debug -Message "Downloaded file `"$DownloadDestinationArchive`" checksum matches to the public one."
            }
            else {
                throw "Downloaded file `"$DownloadDestinationArchive`" checksum does not match to the public one."
            }

            # Expand archive
            Expand-Archive -Path $DownloadDestinationArchive -DestinationPath $TempDirectory
            Write-Debug -Message "Java archive `"$DownloadDestinationArchive`" extracted to `"$TempDirectory`"."

            # Move to destination directory
            $SourcePath = Join-Path -Path $TempDirectory -ChildPath $this.Name
            $DestinationPath = Join-Path -Path $DestinationDirectory -ChildPath $this.Name
            if (Test-Path -Path $DestinationPath) {
                throw "Package `"{0}`" already exists in destination directory `"{1}`"." -f $this.Name, $DestinationDirectory
            }
            $Item = Move-Item -Path $SourcePath -Destination $DestinationPath -Force -PassThru
            Write-Debug -Message "Java package `"$SourcePath`" moved to `"$DestinationPath`"."
        }
        catch {
            throw "Cannot download Java package: {0}" -f $_.Exception.Message
        }
        finally {
            Remove-Item -Path $TempDirectory -Recurse -Force
            Write-Debug -Message "Temporary directory removed `"$TempDirectory`"."
        }

        return $Item
    }
}
#endregion Classes

#region Private functions
function Export-BatchPathsToEnvPath {

    <#
 
    .SYNOPSIS
    Export paths including Batch variables to Path environment variable.
 
    .DESCRIPTION
    This function uses a Microsoft.Win32.Registry method to write
    the value of the Path environment variable to the Windows Registry.
    The motivation is to keep the batch variables in the value.
    This is only available for the Machine and User target scopes.
 
    .PARAMETER Paths
    Array of paths to export to the Path environment variable.
 
    .PARAMETER Target
    Target scope of the environment variable.
 
    .INPUTS
    None. You cannot pipe objects to Export-BatchPathsToEnvPath.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Export-BatchPathsToEnvPath -Value $ArrayOfPaths -Target Machine
 
    Set Path environment variables with the given paths.
 
    .LINK
    - Batch "Windows Environment Variables" documentation: https://ss64.com/nt/syntax-variables.html
    - PowerShell "working with registry entries" documentation: https://docs.microsoft.com/en-us/powershell/scripting/samples/working-with-registry-entries
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Mandatory = $true,
            HelpMessage = "Array of paths to set in the Path environment variable"
        )]
        [string[]] $Paths,

        [Parameter(
            Mandatory = $true,
            HelpMessage = "Target scope of the environment variable"
        )]
        [ValidateSet("Machine", "User")]
        [string] $Target
    )

    process {
        $Value = $Paths -join [System.IO.Path]::PathSeparator

        if ($PSCmdlet.ShouldProcess("Path environment variable in '$Target' scope", "Set value to '$Value'")) {
            $KeyName = switch ($Target) {
                "Machine" {
                    "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment\Path"
                }
                "User" {
                    "HKEY_CURRENT_USER\Environment"
                }
            }
            [Microsoft.Win32.Registry]::SetValue($KeyName, "Path", $Value, [Microsoft.Win32.RegistryValueKind]::ExpandString)
            Write-Debug -Message "New value of the Path environement variable in $Target scope: `"$Value`"."
        }
    }
}
function Import-BatchPathsFromEnvPath {

    <#
 
    .SYNOPSIS
    Imports paths including Batch variables from Path environment variable.
 
    .DESCRIPTION
    This function uses an WshShell Object to read the value of the Path
    environment variable from the Windows Registry.
    The motivation is to keep the batch variables in the value.
    This is only available for the Machine and User target scopes.
 
    .PARAMETER Target
    Target scope of the environment variable.
 
    .INPUTS
    None. You cannot pipe objects to Import-BatchPathsFromEnvPath.
 
    .OUTPUTS
    System.String[]. Array of Batch paths.
 
    .EXAMPLE
    PS> Import-BatchPathsFromEnvPath -Target Machine
    C:\windows
    %SystemRoot%\system32
    %SYSTEMROOT%\System32\WindowsPowerShell\v1.0\
 
    Returns an array which contains every path currently set in the Path environment variable.
 
    .LINK
    - Batch "Windows Environment Variables" documentation: https://ss64.com/nt/syntax-variables.html
    - PowerShell "working with registry entries" documentation: https://docs.microsoft.com/en-us/powershell/scripting/samples/working-with-registry-entries
 
    #>


    [OutputType([string[]])]
    [CmdletBinding()]
    param (
        [Parameter(
            Mandatory = $true,
            HelpMessage = "Target scope of the environment variable"
        )]
        [ValidateSet("Machine", "User")]
        [string] $Target
    )

    process {
        $WScriptShell = New-Object -ComObject WScript.Shell

        $Value = switch ($Target) {
            "Machine" {
                $WScriptShell.RegRead("HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment\Path")
            }
            "User" {
                $WScriptShell.RegRead("HKEY_CURRENT_USER\Environment\Path")
            }
        }
        Write-Debug -Message "Current value of the Path environement variable in $Target scope: `"$Value`"."

        $Value -split [System.IO.Path]::PathSeparator | Where-Object -FilterScript { $_ } | Select-Object -Unique
    }
}
function Send-WMSettingChangeMessage {

    <#
 
    .SYNOPSIS
    Send WM_SETTINGCHANGE message to apply environment changes.
 
    .DESCRIPTION
    Send WM_SETTINGCHANGE message to all top-level windows in the system to apply environment changes.
 
    .INPUTS
    None. You cannot pipe objects to Send-WMSettingChangeMessage.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Send-WMSettingChangeMessage
 
    Send message to all windows in the system to apply environment changes.
 
    .LINK
    - "WM_SETTINGCHANGE message" documentation: https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-settingchange
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param()

    begin {
        # Import SendMessageTimeout from Win32
        if (-not ("Win32.NativeMethods" -as [Type])) {
            Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @'
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr SendMessageTimeout(
    IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags,
    uint uTimeout, out UIntPtr lpdwResult);
'@

        }
    }

    process {
        # Set SendMessageTimeout arguments to send WM_SETTINGCHANGE message
        [IntPtr] $HWnd = 0xffff
        [uint] $Msg = 0x1a  # WM_SETTINGCHANGE flag
        [UIntPtr] $LpdwResult = [UIntPtr]::Zero

        if ($PSCmdlet.ShouldProcess("Send WM_SETTINGCHANGE message to all top-level windows")) {
            $ReturnedValue = [Win32.NativeMethods]::SendMessageTimeout(
                $HWnd,
                $Msg,
                [UIntPtr]::Zero,
                "Environment",
                2,
                5000,
                [ref] $LpdwResult
            )

            if ($ReturnedValue -ne 0) {
                # The function succeeded
                Write-Debug -Message "Environment changes have been successfully applied to all top-level windows."
            }
            else {
                # The function failed or timed out
                Write-Debug -Message "Environment changes have not been applied to all top-level windows."
            }
        }
    }
}
function Update-EnvPath {

    <#
 
    .SYNOPSIS
    Update the Path environment variable.
 
    .DESCRIPTION
    Update the Path environment variable in the current session.
 
    .INPUTS
    None. You cannot pipe objects to Update-EnvPath.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Update-EnvPath
 
    Update the Path environment variable.
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param ()

    begin {
        $Separator = [System.IO.Path]::PathSeparator
    }

    process {
        if ($PSCmdlet.ShouldProcess("Update the Path environment variable in the current session")) {
            $Paths = "Machine", "User" | ForEach-Object -Process {
                [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::"$_") -split $Separator | Where-Object -FilterScript { $_ } | Select-Object -Unique
            }
            $env:Path = $Paths -join $Separator
            Write-Debug "Path environment variable has been updated in the current session."
        }
    }
}
function Use-InteractiveSelectionMenu {

    <#
 
    .SYNOPSIS
    Use interactive selection menu.
 
    .DESCRIPTION
    Use interactive selection menu with given items inside the PowerShell console.
 
    .PARAMETER Items
    List of items to add in the menu.
 
    .PARAMETER Header
    Menu header.
 
    .INPUTS
    None. You cannot pipe objects to Use-InteractiveSelectionMenu.
 
    .OUTPUTS
    System.String. The selected item.
 
    .EXAMPLE
    PS> Use-InteractiveSelectionMenu -Items @("item1", "item2", "item3")
    Select item:
    > item1
      item2
      item3
 
    Show interactive menu to select an item.
 
    .EXAMPLE
    PS> Use-InteractiveSelectionMenu -Items @("coffee", "tea") -Header "Pick your hot beverages"
    Pick your hot beverages
    > coffee
      tea
 
    Show interactive menu to select an item.
 
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Scope = "Function", Target = "*")]
    [OutputType([string])]
    [CmdletBinding()]
    param (
        [Parameter(
            Mandatory = $true,
            HelpMessage = "List of items to add in the menu"
        )]
        [array] $Items,

        [Parameter(
            HelpMessage = "Menu header"
        )]
        [string] $Header = "Select item:"
    )

    process {
        $FirstPrint = $true
        $Position = 0
        $Entered = $false
        $Escaped = $false

        if ($Items.Length -gt 0) {
            try {
                [System.Console]::CursorVisible = $false

                Write-Host $Header -ForegroundColor Gray
                while (-not $Entered -and -not $Escaped) {
                    if (-not $FirstPrint) {
                        $Host.UI.RawUI.FlushInputBuffer()
                        $Key = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

                        switch ($Key.VirtualKeyCode) {
                            <# Key mapping
                            38 = UP ARROW
                            75 = K
                            #>

                            { $_ -in 38, 75 -and $Position -gt 0 } { $Position-- }

                            <# Key mapping
                            40 = DOWN ARROW
                            74 = J
                            #>

                            { $_ -in 40, 74 -and $Position -lt $Items.Length - 1 } { $Position++ }

                            <# Key mapping
                            33 = PAGE UP
                            36 = HOME
                            #>

                            { $_ -in 33, 36 } { $Position = 0 }

                            <# Key mapping
                            34 = PAGE DOWN
                            35 = END
                            #>

                            { $_ -in 34, 35 } { $Position = $Items.Length - 1 }

                            <# Key mapping
                            13 = ENTER
                            #>

                            13 { $Entered = $true }

                            <# Key mapping
                            27 = ESC
                            #>

                            27 { $Escaped = $true }
                        }

                        $StartPosition = [System.Console]::CursorTop - $Items.Length
                        [System.Console]::SetCursorPosition(0, $StartPosition)
                    }
                    else { $FirstPrint = $false }

                    # Print to console
                    for ($i = 0; $i -lt $Items.Length; $i++) {
                        if ($i -eq $Position) {
                            Write-Host ">" $Items[$i] -ForegroundColor Yellow
                        }
                        else {
                            Write-Host " " $Items[$i]
                        }
                    }
                }
            }
            finally {
                [System.Console]::SetCursorPosition(0, $StartPosition + $Items.Length)
                [System.Console]::CursorVisible = $true
            }
        }

        if ($Entered) {
            $Items[$Position]
        }
        else {
            $null
        }
    }
}
function Get-JavaVersion {

    <#
 
    .SYNOPSIS
    Get Java version.
 
    .DESCRIPTION
    Get version of a Java application.
 
    .PARAMETER Path
    Path to the Java home directory.
 
    .INPUTS
    None. You cannot pipe objects to Get-JavaVersion.
 
    .OUTPUTS
    System.String. Version of the Java application.
 
    .EXAMPLE
    PS> Get-JavaVersion -Path C:\path\to\java_home_directory
    jdk-11.0.13.0
 
    Get version of the Java application.
 
    #>


    [OutputType([string])]
    [CmdletBinding()]
    param (
        [Parameter(
            Mandatory = $true,
            HelpMessage = "Path to the Java home directory"
        )]
        [string] $Path
    )

    process {
        $JavaApplicatonPath = Join-Path -Path $Path -ChildPath "bin\java.exe"
        $JavaCompilerPath = Join-Path -Path $Path -ChildPath "bin\javac.exe"

        try {
            $Version = (Get-Command -Name $JavaApplicatonPath -ErrorAction Stop).Version.ToString()
            Write-Debug -Message "Version of Java application `"$JavaApplicatonPath`" is `"$Version`"."
        }
        catch {
            throw "Cannot get the version of Java application."
        }

        if (Get-Command -Name $JavaCompilerPath -ErrorAction SilentlyContinue) {
            $Type = "jdk"
            Write-Debug -Message "Java compiler `"$JavaCompilerPath`" exists. Assuming it is JDK (Java Development Kit)."
        }
        else {
            $Type = "jre"
            Write-Debug -Message "Java compiler `"$JavaCompilerPath`" does not exist. Assuming it is JRE (Java Runtime Environment)."
        }

        "{0}-{1}" -f $Type, $Version
    }
}
function Set-EnvJavaHome {

    <#
 
    .SYNOPSIS
    Set JAVA_HOME environment variable.
 
    .DESCRIPTION
    Set JAVA_HOME environment variable in a target scope.
 
    .PARAMETER Path
    Path to the Java home directory.
 
    .PARAMETER Target
    Target scope of the environment variable.
 
    .INPUTS
    None. You cannot pipe objects to Set-EnvJavaHome.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Set-EnvJavaHome -Path C:\path\to\java_home_directory -Target User
 
    Set JAVA_HOME environment variable with given path in user scope.
 
    .NOTES
    BUG REG_EXPAND_SZ environment variables not properly expanded for shells (related Github issue: https://github.com/microsoft/terminal/issues/9741)
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Mandatory = $true,
            HelpMessage = "Path to the Java home directory"
        )]
        [string] $Path,

        [Parameter(
            Mandatory = $true,
            HelpMessage = "Target scope of the environment variable"
        )]
        [ValidateSet("Machine", "User", "Process")]
        [string] $Target
    )

    process {
        $NormalizedPath = [PathFormat]::NormalizePath($Path)
        $PreviousValue = $env:JAVA_HOME

        if ($PSCmdlet.ShouldProcess("JAVA_HOME environment variable in '$Target' scope", "Set value to '$NormalizedPath'")) {
            if ($Target -ne "Process") {
                try {
                    [System.Environment]::SetEnvironmentVariable("JAVA_HOME", $NormalizedPath, [System.EnvironmentVariableTarget]::"$Target")
                    Write-Debug -Message "JAVA_HOME environment variable in $Target scope has been set to `"$NormalizedPath`"."
                }
                catch {
                    throw "Cannot set JAVA_HOME environment variable in $Target scope: {0}" -f $_.Exception.Message
                }
            }
            $env:JAVA_HOME = $NormalizedPath
        }

        if ($Target -ne "Process") {
            $BatchPaths = Import-BatchPathsFromEnvPath -Target $Target
            if ($BatchPaths -notcontains "%JAVA_HOME%\bin") {
                if ($PSCmdlet.ShouldProcess("Path environment variable in '$Target' scope", "Add path '%JAVA_HOME%\bin'")) {
                    $BatchPaths += "%JAVA_HOME%\bin"
                    try {
                        Export-BatchPathsToEnvPath -Paths $BatchPaths -Target $Target
                        Write-Debug -Message "`"%JAVA_HOME%\bin`" has been added to the Path environment variable in $Target scope."
                    }
                    catch {
                        throw "Cannot add `"%JAVA_HOME%\bin`" to Path environment variable in $Target scope: {0}" -f $_.Exception.Message
                    }
                    Send-WMSettingChangeMessage
                    Update-EnvPathProcess
                }
            }
            else {
                Write-Debug -Message "`"%JAVA_HOME%\bin`" is already present in the Path environment variable in $Target scope."
            }
        }

        if ($PreviousValue) {
            $env:Path = $env:Path.Replace($PreviousValue, $NormalizedPath)
            Write-Debug -Message "JAVA_HOME path from the Path environment variable in Process scope has been updated to `"$NormalizedPath`"."
        }
        else {
            if ($Target -ne "Process") {
                $env:Path = $env:Path.Replace("%JAVA_HOME%", $NormalizedPath)
                Write-Debug -Message "`"%JAVA_HOME%`" from the Path environment variable in Process scope has been replaced by `"$NormalizedPath`"."
            }
            else {
                $PathToAppend = "{0}$NormalizedPath\bin" -f [System.IO.Path]::PathSeparator
                $env:Path += $PathToAppend
                Write-Debug -Message "`"$PathToAppend`" has been appended to the Path environment variable in Process scope."
            }
        }
    }
}
function Test-JavaHomeDirectory {

    <#
 
    .SYNOPSIS
    Test Java home directory.
 
    .DESCRIPTION
    Test if a given directory is a Java home directory.
 
    .PARAMETER Path
    Path to the directory.
 
    .INPUTS
    None. You cannot pipe objects to Test-JavaHomeDirectory.
 
    .OUTPUTS
    System.Boolean. True if directory is a Java home directory.
 
    .EXAMPLE
    PS> Test-JavaHomeDirectory -Path C:\path\to\java_home_directory
    $true
 
    Test Java home directory.
 
    #>


    [OutputType([bool])]
    [CmdletBinding()]
    param (
        [Parameter(
            Mandatory = $true,
            HelpMessage = "Path to the directory"
        )]
        [string] $Path
    )

    process {
        $JavaApplicatonPath = Join-Path -Path $Path -ChildPath "bin\java.exe"

        $IsDirectory = Test-Path -Path $Path -PathType Container
        Write-Debug -Message "Is `"$Path`" a directory? $IsDirectory."

        $JavaApplicatonExists = Test-Path -Path $JavaApplicatonPath -PathType Leaf
        Write-Debug -Message "Does `"$JavaApplicatonPath`" exist? $JavaApplicatonExists."

        $IsDirectory -and $JavaApplicatonExists
    }
}
#endregion Private functions

#region Public functions
function Add-JavaEntry {

    <#
 
    .SYNOPSIS
    Add a Java entry.
 
    .DESCRIPTION
    Add a Java entry to the inventory file.
 
    .PARAMETER Path
    Path to the Java home directory to add as entry to the inventory file.
 
    .PARAMETER CustomName
    Custom name to set for the Java entry to add to the inventory file.
 
    .INPUTS
    None. You cannot pipe objects to Add-JavaEntry.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Add-JavaEntry -Path C:\path\to\java_home_directory
 
    Add java entry to the inventory with default name (version name).
 
    .EXAMPLE
    PS> Add-JavaEntry -Path C:\path\to\java_home_directory -CustomName "MyJava"
 
    Add java entry to the inventory with custom name.
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Mandatory = $true,
            Position = 0,
            HelpMessage = "Path to the Java home directory to add as entry to the inventory file"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Path,

        [Parameter(
            HelpMessage = "Custom name to set for the Java entry to add to the inventory file"
        )]
        [ValidateNotNullOrEmpty()]
        [Alias("Name")]
        [string] $CustomName
    )

    begin {
        $InformationPreference = "Continue"
        $ErrorActionPreference = "Stop"
    }

    process {
        if (-not (Test-JavaHomeDirectory -Path $Path)) {
            Write-Error -Message "Path `"$Path`" is not a Java home directory."
        }

        $Version = Get-JavaVersion -Path $Path
        Write-Debug -Message "Java version is `"$Version`"."

        $Name = if ($CustomName) { $CustomName } else { $Version }
        Write-Debug -Message "Java entry will be set with name `"$Name`"."

        $Inventory = New-Object -TypeName Inventory

        if ($Inventory.FileExists()) {
            $Inventory.ReadFile()
        }

        if ($Inventory.JavaEntryExists($Name)) {
            Write-Error -Message "Java entry `"$Name`" already exists."
        }

        if ($PSCmdlet.ShouldProcess($Inventory.Path, "Add Java entry '$Name' to inventory file")) {
            $Inventory.AddJavaEntry($Name, $Path)
            $Inventory.SaveFile()
            Write-Information -MessageData "Java entry `"$Name`" added."
        }
    }
}
function Get-JavaEntry {

    <#
 
    .SYNOPSIS
    Get Java entries.
 
    .DESCRIPTION
    Get all the Java entries from the inventory file.
 
    .INPUTS
    None. You cannot pipe objects to Get-JavaEntry.
 
    .OUTPUTS
    System.Object. Array of Java entry objects.
 
    .EXAMPLE
    PS> Get-JavaEntry
 
    Name Path
    ---- ----
    jdk-8.0.3120.7 C:\Users\user\.java_packages\jdk8u312-b07
    jdk-11.0.13.0 C:\Users\user\.java_packages\jdk-11.0.13+8
    jdk-17.0.1.0 C:\Users\user\.java_packages\jdk-17.0.1+12
    my_jdk C:\Users\user\.java_packages\jdk-17.0.1+12
 
    Get all the Java entries from the inventory file.
 
    #>


    [OutputType([object[]])]
    [CmdletBinding()]
    param ()

    begin {
        $ErrorActionPreference = "Stop"
    }

    process {
        $Inventory = New-Object -TypeName Inventory

        if (-not $Inventory.FileExists()) {
            Write-Error -Message "The inventory file does not exist."
        }

        $Inventory.ReadFile()

        if (-not $Inventory.JavaEntries) {
            Write-Error -Message "There is no Java entry in the inventory."
        }

        $Inventory.JavaEntries
    }
}
function Invoke-DownloadJavaPackage {

    <#
 
    .SYNOPSIS
    Download a Java package.
 
    .DESCRIPTION
    Download and install a Java package from Adoptium and add an associated entry to the inventory file.
 
    .PARAMETER Version
    Version of Java to download.
 
    .INPUTS
    None. You cannot pipe objects to Invoke-DownloadJavaPackage.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Invoke-DownloadJavaPackage
 
    If in interactive PowerShell session, select a version of Java using menu.
    Else, pick the latest available version of Java.
    Download and install the Java package in the .java_package folder.
 
    .EXAMPLE
    PS> Invoke-DownloadJavaPackage -Version 11
 
    Download and install Java 11 in the .java_package folder.
 
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Scope = "Function", Target = "*")]
    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Position = 0,
            HelpMessage = "Version of Java to download"
        )]
        [int] $Version
    )

    begin {
        $InformationPreference = "Continue"
        $ErrorActionPreference = "Stop"
        $AvailableVersions = [AdoptiumApi]::GetAvailableFeatureVersions()
        $DestinationDirectory = Join-Path -Path $HOME -ChildPath ".java_packages"
    }

    process {
        if ($Version) {
            if ($Version -notin $AvailableVersions) {
                Write-Error -Message ("Java version $Version is not available. Available versions: {0}." -f ($AvailableVersions -join ", "))
            }
        }
        else {
            if ([Environment]::UserInteractive) {
                $Version = Use-InteractiveSelectionMenu -Items $AvailableVersions -Header "Select Java version:"
                if (-not $Version) { break }
            }
            else {
                $Version = ($AvailableVersions | Measure-Object -Maximum).Maximum
            }
        }

        $Package = New-Object -TypeName AdoptiumPackage -ArgumentList $Version

        if ($PSCmdlet.ShouldProcess($DestinationDirectory, ("Download and install Java package '{0}' to directory" -f $Package.Name))) {
            if (-not (Test-Path -Path $DestinationDirectory -PathType Container)) {
                New-Item -Path $DestinationDirectory -ItemType Directory | Out-Null
            }

            Write-Information -MessageData ("Downloading Java package `"{0}`"..." -f $Package.Name)
            $PackageObject = $Package.Download($DestinationDirectory)
            $PackagePath = $PackageObject.FullName
            Write-Information -MessageData "Java package installed in `"$PackagePath`"."

            try {
                Add-JavaEntry -Path $PackagePath
            }
            catch {
                Write-Error -Message ("Cannot add Java entry to the inventory file: {0}" -f $_.Exception.Message)
            }
        }
    }
}
function Remove-JavaEntry {

    <#
 
    .SYNOPSIS
    Remove a Java entry.
 
    .DESCRIPTION
    Remove a Java entry to the inventory file.
 
    .PARAMETER Name
    Name of the Java entry to remove from the inventory file.
 
    .INPUTS
    None. You cannot pipe objects to Remove-JavaEntry.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Remove-JavaEntry -Name undesired_java
 
    Remove a Java entry with defined name from the inventory file.
 
    #>


    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(
            Position = 0,
            HelpMessage = "Name of the Java entry to remove from the inventory file"
        )]
        [string] $Name
    )

    begin {
        $InformationPreference = "Continue"
        $ErrorActionPreference = "Stop"
    }

    process {
        $Inventory = New-Object -TypeName Inventory

        if (-not $Inventory.FileExists()) {
            Write-Error -Message "The inventory file does not exist."
        }

        $Inventory.ReadFile()

        if (-not $Inventory.JavaEntries) {
            Write-Error -Message "There is no Java entry in the inventory."
        }

        $JavaEntryNames = @()
        $Inventory.JavaEntries | ForEach-Object -Process {
            $JavaEntryNames += $_.Name
        }

        if (-not $Name) {
            if ([Environment]::UserInteractive) {
                $Name = Use-InteractiveSelectionMenu -Items $JavaEntryNames -Header "Select Java entry:"
                if (-not $Name) { break }
            }
            else {
                Write-Error -Message "Parameter `"Name`" cannot be an empty string."
            }
        }

        if (-not $Inventory.JavaEntryExists($Name)) {
            Write-Error -Message "Java entry with name `"$Name`" does not exist."
        }

        if ($PSCmdlet.ShouldProcess($Inventory.Path, "Remove Java entry '$Name' from the inventory file")) {
            $Inventory.RemoveJavaEntry($Name)
            $Inventory.SaveFile()
            Write-Information -MessageData "Java entry `"$Name`" removed."
        }
    }
}
function Switch-JavaVersion {

    <#
 
    .SYNOPSIS
    Switch Java version.
 
    .DESCRIPTION
    Switch the version of Java which is being used in a specific environment target scope.
 
    .PARAMETER Name
    Name of the Java entry from the inventory file to use.
 
    .PARAMETER Path
    Path to the Java home directory to use.
 
    .PARAMETER Target
    Target scope of the environment variable.
 
    .INPUTS
    None. You cannot pipe objects to Switch-JavaVersion.
 
    .OUTPUTS
    System.Void. None.
 
    .EXAMPLE
    PS> Switch-JavaVersion -Name MyJava
 
    Select a Java version which is defined in the inventory file and use it in default scope (user scope).
 
    .EXAMPLE
    PS> Switch-JavaVersion
 
    (Interactive menu)
 
    Select a Java version which is defined in the inventory file using an interactive menu and use it in default scope (user scope).
 
    .EXAMPLE
    PS> Switch-JavaVersion -Name MyJava -Target Process
 
    Select a Java version which is defined in the inventory file and use it in process scope.
 
    .EXAMPLE
    PS> Switch-JavaVersion -Path C:\path\to\java_home_directory
 
    Use the Java version from the given path to a Java home directory in default scope (user scope).
 
    #>


    [OutputType([void])]
    [CmdletBinding(
        SupportsShouldProcess = $true,
        DefaultParameterSetName = "UseInventory"
    )]
    param (
        [Parameter(
            ParameterSetName = "UseInventory",
            Position = 0,
            HelpMessage = "Name of the Java entry from the inventory file to use"
        )]
        [string] $Name,

        [Parameter(
            ParameterSetName = "UsePath",
            Mandatory = $true,
            HelpMessage = "Path to the Java home directory to use"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Path,

        [Parameter(
            HelpMessage = "Target scope of the environment variable"
        )]
        [ValidateSet("Machine", "User", "Process")]
        [string] $Target = "User"
    )

    begin {
        $InformationPreference = "Continue"
        $ErrorActionPreference = "Stop"
    }

    process {
        if ($PSCmdlet.ParameterSetName -eq "UseInventory") {
            $Inventory = New-Object -TypeName Inventory

            if (-not $Inventory.FileExists()) {
                Write-Error -Message "The inventory file does not exist."
            }

            $Inventory.ReadFile()

            if (-not $Inventory.JavaEntries) {
                Write-Error -Message "There is no Java entry in the inventory."
            }

            $JavaEntryNames = @()
            $Inventory.JavaEntries | ForEach-Object -Process {
                $JavaEntryNames += $_.Name
            }

            if (-not $Name) {
                if ([Environment]::UserInteractive) {
                    $Name = Use-InteractiveSelectionMenu -Items $JavaEntryNames -Header "Select Java entry:"
                    if (-not $Name) { break }
                }
                else {
                    Write-Error -Message "Parameter `"Name`" cannot be an empty string."
                }
            }

            if (-not $Inventory.JavaEntryExists($Name)) {
                Write-Error -Message "Java entry with name `"$Name`" does not exist."
            }

            $Path = $Inventory.GetJavaEntry($Name).Path
        }

        if (-not (Test-JavaHomeDirectory -Path $Path)) {
            Write-Error -Message "Path `"$Path`" is not a Java home directory."
        }

        $Version = Get-JavaVersion -Path $Path

        if ($PSCmdlet.ShouldProcess("Java home directory in '$Target' scope", "Use $Path")) {
            Set-EnvJavaHome -Path $Path -Target $Target
            Write-Information -MessageData "Java version `"$Version`" is now in use."
        }
    }
}
#endregion Public functions