Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ServiceProviders;
|
||||
|
||||
internal static class JavaServiceProviderScanner
|
||||
{
|
||||
public static JavaServiceProviderAnalysis Scan(JavaClassPathAnalysis classPath, JavaSpiCatalog catalog, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(classPath);
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
|
||||
var services = new Dictionary<string, ServiceAccumulator>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var segment in classPath.Segments.OrderBy(static s => s.Order))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
foreach (var kvp in segment.ServiceDefinitions)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (kvp.Value.IsDefaultOrEmpty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!services.TryGetValue(kvp.Key, out var accumulator))
|
||||
{
|
||||
accumulator = new ServiceAccumulator();
|
||||
services[kvp.Key] = accumulator;
|
||||
}
|
||||
|
||||
var providerIndex = 0;
|
||||
foreach (var provider in kvp.Value)
|
||||
{
|
||||
var normalizedProvider = provider?.Trim();
|
||||
if (string.IsNullOrEmpty(normalizedProvider))
|
||||
{
|
||||
providerIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
accumulator.AddCandidate(new JavaServiceProviderCandidateRecord(
|
||||
ProviderClass: normalizedProvider,
|
||||
SegmentIdentifier: segment.Identifier,
|
||||
SegmentOrder: segment.Order,
|
||||
ProviderIndex: providerIndex++,
|
||||
IsSelected: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var records = new List<JavaServiceProviderRecord>(services.Count);
|
||||
|
||||
foreach (var pair in services.OrderBy(static entry => entry.Key, StringComparer.Ordinal))
|
||||
{
|
||||
var descriptor = catalog.Resolve(pair.Key);
|
||||
var accumulator = pair.Value;
|
||||
var orderedCandidates = accumulator.GetOrderedCandidates();
|
||||
|
||||
if (orderedCandidates.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var selectedIndex = accumulator.DetermineSelection(orderedCandidates);
|
||||
var warnings = accumulator.BuildWarnings();
|
||||
|
||||
var candidateArray = ImmutableArray.CreateRange(orderedCandidates.Select((candidate, index) =>
|
||||
candidate with { IsSelected = index == selectedIndex }));
|
||||
|
||||
var warningsArray = warnings.Count == 0
|
||||
? ImmutableArray<string>.Empty
|
||||
: ImmutableArray.CreateRange(warnings);
|
||||
|
||||
records.Add(new JavaServiceProviderRecord(
|
||||
ServiceId: pair.Key,
|
||||
DisplayName: descriptor.DisplayName,
|
||||
Category: descriptor.Category,
|
||||
Candidates: candidateArray,
|
||||
SelectedIndex: selectedIndex,
|
||||
Warnings: warningsArray));
|
||||
}
|
||||
|
||||
return new JavaServiceProviderAnalysis(records.ToImmutableArray());
|
||||
}
|
||||
|
||||
private sealed class ServiceAccumulator
|
||||
{
|
||||
private readonly List<JavaServiceProviderCandidateRecord> _candidates = new();
|
||||
private readonly Dictionary<string, HashSet<string>> _providerSources = new(StringComparer.Ordinal);
|
||||
|
||||
public void AddCandidate(JavaServiceProviderCandidateRecord candidate)
|
||||
{
|
||||
_candidates.Add(candidate);
|
||||
|
||||
if (!_providerSources.TryGetValue(candidate.ProviderClass, out var sources))
|
||||
{
|
||||
sources = new HashSet<string>(StringComparer.Ordinal);
|
||||
_providerSources[candidate.ProviderClass] = sources;
|
||||
}
|
||||
|
||||
sources.Add(candidate.SegmentIdentifier);
|
||||
}
|
||||
|
||||
public IReadOnlyList<JavaServiceProviderCandidateRecord> GetOrderedCandidates()
|
||||
=> _candidates
|
||||
.OrderBy(static c => c.SegmentOrder)
|
||||
.ThenBy(static c => c.ProviderIndex)
|
||||
.ThenBy(static c => c.ProviderClass, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
public int DetermineSelection(IReadOnlyList<JavaServiceProviderCandidateRecord> orderedCandidates)
|
||||
=> orderedCandidates.Count == 0 ? -1 : 0;
|
||||
|
||||
public List<string> BuildWarnings()
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
foreach (var pair in _providerSources.OrderBy(static entry => entry.Key, StringComparer.Ordinal))
|
||||
{
|
||||
if (pair.Value.Count <= 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var locations = pair.Value
|
||||
.OrderBy(static value => value, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
warnings.Add($"duplicate-provider: {pair.Key} ({string.Join(", ", locations)})");
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record JavaServiceProviderAnalysis(ImmutableArray<JavaServiceProviderRecord> Services)
|
||||
{
|
||||
public static readonly JavaServiceProviderAnalysis Empty = new(ImmutableArray<JavaServiceProviderRecord>.Empty);
|
||||
}
|
||||
|
||||
internal sealed record JavaServiceProviderRecord(
|
||||
string ServiceId,
|
||||
string DisplayName,
|
||||
string Category,
|
||||
ImmutableArray<JavaServiceProviderCandidateRecord> Candidates,
|
||||
int SelectedIndex,
|
||||
ImmutableArray<string> Warnings);
|
||||
|
||||
internal sealed record JavaServiceProviderCandidateRecord(
|
||||
string ProviderClass,
|
||||
string SegmentIdentifier,
|
||||
int SegmentOrder,
|
||||
int ProviderIndex,
|
||||
bool IsSelected);
|
||||
@@ -0,0 +1,103 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.ServiceProviders;
|
||||
|
||||
internal sealed class JavaSpiCatalog
|
||||
{
|
||||
private static readonly Lazy<JavaSpiCatalog> LazyDefault = new(LoadDefaultCore);
|
||||
private readonly ImmutableDictionary<string, JavaSpiDescriptor> _descriptors;
|
||||
|
||||
private JavaSpiCatalog(ImmutableDictionary<string, JavaSpiDescriptor> descriptors)
|
||||
{
|
||||
_descriptors = descriptors;
|
||||
}
|
||||
|
||||
public static JavaSpiCatalog Default => LazyDefault.Value;
|
||||
|
||||
public JavaSpiDescriptor Resolve(string serviceId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(serviceId))
|
||||
{
|
||||
return JavaSpiDescriptor.CreateUnknown(string.Empty);
|
||||
}
|
||||
|
||||
var key = serviceId.Trim();
|
||||
if (_descriptors.TryGetValue(key, out var descriptor))
|
||||
{
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
return JavaSpiDescriptor.CreateUnknown(key);
|
||||
}
|
||||
|
||||
private static JavaSpiCatalog LoadDefaultCore()
|
||||
{
|
||||
var assembly = typeof(JavaSpiCatalog).GetTypeInfo().Assembly;
|
||||
var resourceName = "StellaOps.Scanner.Analyzers.Lang.Java.Internal.ServiceProviders.java-spi-catalog.json";
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName)
|
||||
?? throw new InvalidOperationException($"Embedded SPI catalog '{resourceName}' not found.");
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, leaveOpen: false);
|
||||
var json = reader.ReadToEnd();
|
||||
|
||||
var items = JsonSerializer.Deserialize<List<JavaSpiDescriptor>>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
}) ?? new List<JavaSpiDescriptor>();
|
||||
|
||||
var descriptors = items
|
||||
.Select(Normalize)
|
||||
.Where(static item => !string.IsNullOrWhiteSpace(item.ServiceId))
|
||||
.ToImmutableDictionary(
|
||||
static item => item.ServiceId,
|
||||
static item => item,
|
||||
StringComparer.Ordinal);
|
||||
|
||||
return new JavaSpiCatalog(descriptors);
|
||||
}
|
||||
|
||||
private static JavaSpiDescriptor Normalize(JavaSpiDescriptor descriptor)
|
||||
{
|
||||
var serviceId = descriptor.ServiceId?.Trim() ?? string.Empty;
|
||||
var category = string.IsNullOrWhiteSpace(descriptor.Category)
|
||||
? "unknown"
|
||||
: descriptor.Category.Trim().ToLowerInvariant();
|
||||
var displayName = string.IsNullOrWhiteSpace(descriptor.DisplayName)
|
||||
? serviceId
|
||||
: descriptor.DisplayName.Trim();
|
||||
|
||||
return descriptor with
|
||||
{
|
||||
ServiceId = serviceId,
|
||||
Category = category,
|
||||
DisplayName = displayName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record class JavaSpiDescriptor
|
||||
{
|
||||
[JsonPropertyName("serviceId")]
|
||||
public string ServiceId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("category")]
|
||||
public string Category { get; init; } = "unknown";
|
||||
|
||||
[JsonPropertyName("displayName")]
|
||||
public string DisplayName { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("notes")]
|
||||
public string? Notes { get; init; }
|
||||
|
||||
public static JavaSpiDescriptor CreateUnknown(string serviceId)
|
||||
=> new()
|
||||
{
|
||||
ServiceId = serviceId,
|
||||
Category = "unknown",
|
||||
DisplayName = serviceId,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
[
|
||||
{
|
||||
"serviceId": "java.sql.Driver",
|
||||
"category": "jdk",
|
||||
"displayName": "JDBC Driver"
|
||||
},
|
||||
{
|
||||
"serviceId": "javax.annotation.processing.Processor",
|
||||
"category": "jdk",
|
||||
"displayName": "Annotation Processor"
|
||||
},
|
||||
{
|
||||
"serviceId": "org.slf4j.spi.SLF4JServiceProvider",
|
||||
"category": "logging",
|
||||
"displayName": "SLF4J Service Provider"
|
||||
},
|
||||
{
|
||||
"serviceId": "ch.qos.logback.classic.spi.Configurator",
|
||||
"category": "logging",
|
||||
"displayName": "Logback Configurator"
|
||||
},
|
||||
{
|
||||
"serviceId": "com.fasterxml.jackson.core.TokenStreamFactory",
|
||||
"category": "jackson",
|
||||
"displayName": "Jackson Token Stream Factory"
|
||||
},
|
||||
{
|
||||
"serviceId": "com.fasterxml.jackson.databind.Module",
|
||||
"category": "jackson",
|
||||
"displayName": "Jackson Module"
|
||||
},
|
||||
{
|
||||
"serviceId": "org.springframework.boot.Bootstrapper",
|
||||
"category": "spring",
|
||||
"displayName": "Spring Boot Bootstrapper"
|
||||
},
|
||||
{
|
||||
"serviceId": "org.springframework.boot.SpringApplicationRunListener",
|
||||
"category": "spring",
|
||||
"displayName": "Spring Application Run Listener"
|
||||
},
|
||||
{
|
||||
"serviceId": "org.eclipse.microprofile.config.spi.ConfigSourceProvider",
|
||||
"category": "microprofile",
|
||||
"displayName": "MicroProfile Config Source Provider"
|
||||
},
|
||||
{
|
||||
"serviceId": "org.eclipse.microprofile.config.spi.Converter",
|
||||
"category": "microprofile",
|
||||
"displayName": "MicroProfile Converter"
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user