//
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
//
using StellaOps.Spdx3.Model.Software;
using System.Collections.Immutable;
namespace StellaOps.Spdx3.Model;
///
/// Represents a parsed SPDX 3.0.1 document containing all elements.
///
public sealed class Spdx3Document
{
private readonly Dictionary _elementsById;
private readonly Dictionary _creationInfoById;
///
/// Initializes a new instance of the class.
///
/// All elements in the document.
/// All CreationInfo objects.
/// Detected profile conformance.
/// The root SpdxDocument element if present.
public Spdx3Document(
IEnumerable elements,
IEnumerable creationInfos,
IEnumerable profiles,
Spdx3SpdxDocument? spdxDocument = null)
{
var elementList = elements.ToList();
// Use GroupBy to handle duplicates - last element wins for lookup but keeps all for validation
_elementsById = elementList
.GroupBy(e => e.SpdxId, StringComparer.Ordinal)
.ToDictionary(g => g.Key, g => g.Last(), StringComparer.Ordinal);
_creationInfoById = creationInfos
.Where(c => c.Id != null)
.GroupBy(c => c.Id!, StringComparer.Ordinal)
.ToDictionary(g => g.Key, g => g.Last(), StringComparer.Ordinal);
Profiles = profiles.ToImmutableHashSet();
SpdxDocument = spdxDocument;
// Categorize elements by type - use original list to preserve duplicates for counting
Packages = elementList.OfType().ToImmutableArray();
Files = elementList.OfType().ToImmutableArray();
Snippets = elementList.OfType().ToImmutableArray();
Relationships = elementList.OfType().ToImmutableArray();
AllElements = elementList.ToImmutableArray();
}
///
/// Gets all elements including duplicates (for validation).
///
public ImmutableArray AllElements { get; }
///
/// Gets the root SpdxDocument element if present.
///
public Spdx3SpdxDocument? SpdxDocument { get; }
///
/// Gets all elements in the document.
///
public IReadOnlyCollection Elements => _elementsById.Values;
///
/// Gets all packages in the document.
///
public ImmutableArray Packages { get; }
///
/// Gets all files in the document.
///
public ImmutableArray Files { get; }
///
/// Gets all snippets in the document.
///
public ImmutableArray Snippets { get; }
///
/// Gets all relationships in the document.
///
public ImmutableArray Relationships { get; }
///
/// Gets the detected profile conformance.
///
public ImmutableHashSet Profiles { get; }
///
/// Gets all creation info objects in the document.
///
public IReadOnlyCollection CreationInfos => _creationInfoById.Values;
///
/// Gets an element by its SPDX ID.
///
/// The SPDX ID.
/// The element, or null if not found.
public Spdx3Element? GetById(string spdxId)
{
return _elementsById.TryGetValue(spdxId, out var element) ? element : null;
}
///
/// Gets an element by its SPDX ID as a specific type.
///
/// The element type.
/// The SPDX ID.
/// The element, or null if not found or wrong type.
public T? GetById(string spdxId) where T : Spdx3Element
{
return GetById(spdxId) as T;
}
///
/// Gets a CreationInfo by its ID.
///
/// The CreationInfo ID.
/// The CreationInfo, or null if not found.
public Spdx3CreationInfo? GetCreationInfo(string id)
{
return _creationInfoById.TryGetValue(id, out var info) ? info : null;
}
///
/// Gets relationships where the given element is the source.
///
/// The source element ID.
/// Matching relationships.
public IEnumerable GetRelationshipsFrom(string spdxId)
{
return Relationships.Where(r => r.From == spdxId);
}
///
/// Gets relationships where the given element is a target.
///
/// The target element ID.
/// Matching relationships.
public IEnumerable GetRelationshipsTo(string spdxId)
{
return Relationships.Where(r => r.To.Contains(spdxId));
}
///
/// Gets direct dependencies of a package.
///
/// The package SPDX ID.
/// Dependent packages.
public IEnumerable GetDependencies(string packageId)
{
return GetRelationshipsFrom(packageId)
.Where(r => r.RelationshipType == Spdx3RelationshipType.DependsOn)
.SelectMany(r => r.To)
.Select(GetById)
.Where(p => p != null)
.Cast();
}
///
/// Gets all files contained in a package.
///
/// The package SPDX ID.
/// Contained files.
public IEnumerable GetContainedFiles(string packageId)
{
return GetRelationshipsFrom(packageId)
.Where(r => r.RelationshipType == Spdx3RelationshipType.Contains)
.SelectMany(r => r.To)
.Select(GetById)
.Where(f => f != null)
.Cast();
}
///
/// Checks if the document conforms to a specific profile.
///
/// The profile to check.
/// True if the document conforms.
public bool ConformsTo(Spdx3ProfileIdentifier profile)
{
return Profiles.Contains(profile);
}
///
/// Gets all PURLs from packages in the document.
///
/// Package URLs.
public IEnumerable GetAllPurls()
{
return Packages
.SelectMany(p => p.ExternalIdentifier)
.Where(i => i.ExternalIdentifierType == Spdx3ExternalIdentifierType.PackageUrl)
.Select(i => i.Identifier)
.Distinct(StringComparer.Ordinal);
}
///
/// Gets the root package (if any).
///
/// The root package, or null.
public Spdx3Package? GetRootPackage()
{
if (SpdxDocument?.RootElement.Length > 0)
{
var rootId = SpdxDocument.RootElement[0];
return GetById(rootId);
}
// Fallback: find package with no incoming Contains relationships
var containedIds = Relationships
.Where(r => r.RelationshipType == Spdx3RelationshipType.Contains)
.SelectMany(r => r.To)
.ToHashSet(StringComparer.Ordinal);
return Packages.FirstOrDefault(p => !containedIds.Contains(p.SpdxId));
}
}