tests fixes and sprints work
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// JavaLicenseDetector.cs
|
||||
// Sprint: SPRINT_20260119_024_Scanner_license_detection_enhancements
|
||||
// Task: TASK-024-006 - Upgrade Java license detector
|
||||
// Description: Enhanced Java license detection returning LicenseDetectionResult
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Core.Licensing;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.BuildMetadata;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.License;
|
||||
|
||||
/// <summary>
|
||||
/// Enhanced Java license detector that returns full LicenseDetectionResult.
|
||||
/// </summary>
|
||||
internal sealed class JavaLicenseDetector
|
||||
{
|
||||
private readonly ILicenseCategorizationService _categorizationService;
|
||||
private readonly ILicenseTextExtractor _textExtractor;
|
||||
private readonly ICopyrightExtractor _copyrightExtractor;
|
||||
private readonly SpdxLicenseNormalizer _normalizer;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Java license detector with the specified services.
|
||||
/// </summary>
|
||||
public JavaLicenseDetector(
|
||||
ILicenseCategorizationService categorizationService,
|
||||
ILicenseTextExtractor textExtractor,
|
||||
ICopyrightExtractor copyrightExtractor)
|
||||
{
|
||||
_categorizationService = categorizationService;
|
||||
_textExtractor = textExtractor;
|
||||
_copyrightExtractor = copyrightExtractor;
|
||||
_normalizer = SpdxLicenseNormalizer.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Java license detector with default services.
|
||||
/// </summary>
|
||||
public JavaLicenseDetector()
|
||||
{
|
||||
_categorizationService = new LicenseCategorizationService();
|
||||
_textExtractor = new LicenseTextExtractor();
|
||||
_copyrightExtractor = new CopyrightExtractor();
|
||||
_normalizer = SpdxLicenseNormalizer.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects license information from Java license info.
|
||||
/// </summary>
|
||||
/// <param name="licenseInfo">The license info from project metadata.</param>
|
||||
/// <param name="projectDirectory">Project directory for license file extraction.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The full license detection result.</returns>
|
||||
public async Task<LicenseDetectionResult?> DetectAsync(
|
||||
JavaLicenseInfo licenseInfo,
|
||||
string? projectDirectory = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (licenseInfo.Name is null && licenseInfo.Url is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use existing normalizer
|
||||
var normalized = _normalizer.Normalize(licenseInfo.Name, licenseInfo.Url);
|
||||
var spdxId = normalized.SpdxId ?? BuildLicenseRef(licenseInfo.Name);
|
||||
|
||||
// Determine confidence
|
||||
var confidence = MapConfidence(normalized.SpdxConfidence);
|
||||
|
||||
// Extract license text if project directory is available
|
||||
LicenseTextExtractionResult? licenseTextResult = null;
|
||||
string? noticeContent = null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(projectDirectory))
|
||||
{
|
||||
// Extract LICENSE file
|
||||
var licenseFiles = await _textExtractor.ExtractFromDirectoryAsync(projectDirectory, ct);
|
||||
licenseTextResult = licenseFiles.FirstOrDefault();
|
||||
|
||||
// Look for NOTICE file (common in Apache projects)
|
||||
noticeContent = await TryReadNoticeFileAsync(projectDirectory, ct);
|
||||
}
|
||||
|
||||
// Get copyright notices from LICENSE and NOTICE files
|
||||
var copyrightNotices = new List<CopyrightNotice>();
|
||||
if (licenseTextResult?.CopyrightNotices.Length > 0)
|
||||
{
|
||||
copyrightNotices.AddRange(licenseTextResult.CopyrightNotices);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(noticeContent))
|
||||
{
|
||||
copyrightNotices.AddRange(_copyrightExtractor.Extract(noticeContent));
|
||||
}
|
||||
|
||||
var primaryCopyright = copyrightNotices.Count > 0
|
||||
? copyrightNotices[0].FullText
|
||||
: null;
|
||||
|
||||
var result = new LicenseDetectionResult
|
||||
{
|
||||
SpdxId = spdxId,
|
||||
OriginalText = FormatOriginalText(licenseInfo),
|
||||
LicenseUrl = licenseInfo.Url,
|
||||
Confidence = confidence,
|
||||
Method = DetermineDetectionMethod(licenseInfo),
|
||||
SourceFile = "pom.xml",
|
||||
Category = LicenseCategory.Unknown,
|
||||
Obligations = [],
|
||||
LicenseText = licenseTextResult?.FullText,
|
||||
LicenseTextHash = licenseTextResult?.TextHash,
|
||||
CopyrightNotice = primaryCopyright,
|
||||
IsExpression = false,
|
||||
ExpressionComponents = []
|
||||
};
|
||||
|
||||
return _categorizationService.Enrich(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects licenses from multiple license declarations (pom.xml can have multiple).
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<LicenseDetectionResult>> DetectMultipleAsync(
|
||||
IEnumerable<JavaLicenseInfo> licenses,
|
||||
string? projectDirectory = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var results = new List<LicenseDetectionResult>();
|
||||
|
||||
foreach (var license in licenses)
|
||||
{
|
||||
var result = await DetectAsync(license, projectDirectory, ct);
|
||||
if (result is not null)
|
||||
{
|
||||
results.Add(result);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a combined expression from multiple license results.
|
||||
/// </summary>
|
||||
public LicenseDetectionResult? CombineAsExpression(IReadOnlyList<LicenseDetectionResult> results)
|
||||
{
|
||||
if (results.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (results.Count == 1)
|
||||
{
|
||||
return results[0];
|
||||
}
|
||||
|
||||
// Multiple licenses - create OR expression (dual licensing is common in Java)
|
||||
var spdxIds = results
|
||||
.Select(r => r.SpdxId)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(s => s, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
var expression = string.Join(" OR ", spdxIds);
|
||||
|
||||
// Use the first result as base and update
|
||||
var first = results[0];
|
||||
return first with
|
||||
{
|
||||
SpdxId = expression,
|
||||
IsExpression = true,
|
||||
ExpressionComponents = [.. spdxIds],
|
||||
OriginalText = string.Join("; ", results.Select(r => r.OriginalText).Where(t => t is not null))
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects license from LICENSE file content.
|
||||
/// </summary>
|
||||
public LicenseDetectionResult? DetectFromLicenseFile(string licenseText, string? sourceFile = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(licenseText))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var textResult = _textExtractor.Extract(licenseText, sourceFile);
|
||||
|
||||
var spdxId = textResult.DetectedLicenseId ?? "LicenseRef-Unknown";
|
||||
var confidence = textResult.DetectedLicenseId is not null
|
||||
? textResult.Confidence
|
||||
: LicenseDetectionConfidence.Low;
|
||||
|
||||
var result = new LicenseDetectionResult
|
||||
{
|
||||
SpdxId = spdxId,
|
||||
Confidence = confidence,
|
||||
Method = LicenseDetectionMethod.LicenseFile,
|
||||
SourceFile = sourceFile ?? "LICENSE",
|
||||
Category = LicenseCategory.Unknown,
|
||||
Obligations = [],
|
||||
LicenseText = licenseText,
|
||||
LicenseTextHash = textResult.TextHash,
|
||||
CopyrightNotice = textResult.CopyrightNotices.Length > 0
|
||||
? textResult.CopyrightNotices[0].FullText
|
||||
: null
|
||||
};
|
||||
|
||||
return _categorizationService.Enrich(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects license synchronously without file extraction.
|
||||
/// </summary>
|
||||
public LicenseDetectionResult? Detect(JavaLicenseInfo licenseInfo)
|
||||
{
|
||||
if (licenseInfo.Name is null && licenseInfo.Url is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalized = _normalizer.Normalize(licenseInfo.Name, licenseInfo.Url);
|
||||
var spdxId = normalized.SpdxId ?? BuildLicenseRef(licenseInfo.Name);
|
||||
var confidence = MapConfidence(normalized.SpdxConfidence);
|
||||
|
||||
var result = new LicenseDetectionResult
|
||||
{
|
||||
SpdxId = spdxId,
|
||||
OriginalText = FormatOriginalText(licenseInfo),
|
||||
LicenseUrl = licenseInfo.Url,
|
||||
Confidence = confidence,
|
||||
Method = DetermineDetectionMethod(licenseInfo),
|
||||
SourceFile = "pom.xml",
|
||||
Category = LicenseCategory.Unknown,
|
||||
Obligations = []
|
||||
};
|
||||
|
||||
return _categorizationService.Enrich(result);
|
||||
}
|
||||
|
||||
private static async Task<string?> TryReadNoticeFileAsync(string directory, CancellationToken ct)
|
||||
{
|
||||
var noticeFiles = new[] { "NOTICE", "NOTICE.txt", "NOTICE.md" };
|
||||
|
||||
foreach (var noticeFile in noticeFiles)
|
||||
{
|
||||
var path = Path.Combine(directory, noticeFile);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
return await File.ReadAllTextAsync(path, ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore file read errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static LicenseDetectionConfidence MapConfidence(SpdxConfidence spdxConfidence)
|
||||
{
|
||||
return spdxConfidence switch
|
||||
{
|
||||
SpdxConfidence.High => LicenseDetectionConfidence.High,
|
||||
SpdxConfidence.Medium => LicenseDetectionConfidence.Medium,
|
||||
SpdxConfidence.Low => LicenseDetectionConfidence.Low,
|
||||
_ => LicenseDetectionConfidence.None
|
||||
};
|
||||
}
|
||||
|
||||
private static LicenseDetectionMethod DetermineDetectionMethod(JavaLicenseInfo licenseInfo)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(licenseInfo.Url))
|
||||
{
|
||||
return LicenseDetectionMethod.UrlMatching;
|
||||
}
|
||||
|
||||
return LicenseDetectionMethod.PackageMetadata;
|
||||
}
|
||||
|
||||
private static string? FormatOriginalText(JavaLicenseInfo licenseInfo)
|
||||
{
|
||||
if (licenseInfo.Name is not null && licenseInfo.Url is not null)
|
||||
{
|
||||
return $"{licenseInfo.Name} ({licenseInfo.Url})";
|
||||
}
|
||||
|
||||
return licenseInfo.Name ?? licenseInfo.Url;
|
||||
}
|
||||
|
||||
private static string BuildLicenseRef(string? name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return "LicenseRef-Unknown";
|
||||
}
|
||||
|
||||
// Sanitize for SPDX LicenseRef
|
||||
var sanitized = new char[Math.Min(name.Length, 50)];
|
||||
for (var i = 0; i < sanitized.Length; i++)
|
||||
{
|
||||
var c = name[i];
|
||||
sanitized[i] = char.IsLetterOrDigit(c) || c == '.' || c == '-'
|
||||
? c
|
||||
: '-';
|
||||
}
|
||||
|
||||
return $"LicenseRef-{new string(sanitized).Trim('-')}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user