SourceWriteBackSupport.java
package org.egothor.methodatlas.gui.service;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
import org.egothor.methodatlas.api.SourcePatcher;
import org.egothor.methodatlas.api.TestDiscoveryConfig;
/**
* GUI helper that loads all {@link SourcePatcher} implementations registered
* via {@link ServiceLoader}, configures them with the active
* {@link TestDiscoveryConfig}, and exposes lookup helpers used by the
* panels and the save-all flow.
*
* <p>
* Source write-back is only supported for languages whose discovery plugin
* ships a {@link SourcePatcher}. At the time of writing this means
* <strong>Java</strong> (handled by {@code methodatlas-discovery-jvm}) and
* <strong>C#</strong> (handled by {@code methodatlas-discovery-dotnet}).
* For every other discovered language the GUI prevents staging and
* the Save All / apply-tags flows skip the file with a clear notice.
* </p>
*
* <p>
* Instances are immutable after construction. Rebuild a fresh instance
* whenever the {@link TestDiscoveryConfig} changes (typically at the start
* of every scan).
* </p>
*
* @see SourcePatcher
* @see TestDiscoveryConfig
*/
public final class SourceWriteBackSupport {
/**
* Mapping from a patcher's {@code pluginId()} to the human-readable
* language label used in {@link #supportedLanguagesLabel()}. Keys must
* match the {@code pluginId()} return values of the actual
* {@link SourcePatcher} implementations:
* {@code JavaSourcePatcher.pluginId() == "java"} and
* {@code DotNetSourcePatcher.pluginId() == "dotnet"}.
*/
private static final Map<String, String> LANGUAGE_LABELS = Map.of(
"java", "Java",
"dotnet", "C#"
);
private final List<SourcePatcher> patchers;
/**
* Loads all {@link SourcePatcher} providers from the classpath and
* configures each with {@code config}.
*
* @param config runtime configuration forwarded to every patcher; never
* {@code null}
*/
public SourceWriteBackSupport(TestDiscoveryConfig config) {
List<SourcePatcher> loaded = new ArrayList<>();
ServiceLoader.load(SourcePatcher.class).forEach(p -> {
p.configure(config);
loaded.add(p);
});
this.patchers = List.copyOf(loaded);
}
/**
* Returns the list of configured patchers, in ServiceLoader iteration
* order. The list is unmodifiable.
*
* @return list of loaded {@link SourcePatcher}s; never {@code null}; may
* be empty if no provider jars are on the classpath
*/
public List<SourcePatcher> patchers() {
return patchers;
}
/**
* Returns {@code true} if any loaded patcher accepts the given source file.
*
* @param sourceFile path to a source file; may be {@code null}
* @return {@code true} if at least one patcher's
* {@link SourcePatcher#supports(Path)} returns {@code true};
* {@code false} when {@code sourceFile} is {@code null} or no
* patcher accepts it
*/
public boolean supports(Path sourceFile) {
if (sourceFile == null) {
return false;
}
for (SourcePatcher p : patchers) {
if (p.supports(sourceFile)) {
return true;
}
}
return false;
}
/**
* Returns the first loaded patcher that accepts {@code sourceFile}, or
* {@code null} when none does.
*
* @param sourceFile path to a source file; never {@code null}
* @return matching patcher or {@code null}
*/
public SourcePatcher findPatcher(Path sourceFile) {
for (SourcePatcher p : patchers) {
if (p.supports(sourceFile)) {
return p;
}
}
return null;
}
/**
* Returns a human-readable description of the languages currently
* supported for source write-back, e.g. {@code "Java, C#"}.
*
* <p>
* The label is built from each patcher's {@link SourcePatcher#pluginId()}
* by looking it up in a built-in mapping. Unrecognised plugin IDs are
* shown as-is in uppercase. When no patchers are loaded the method
* returns {@code "(none)"}.
* </p>
*
* @return non-{@code null} comma-separated label
*/
public String supportedLanguagesLabel() {
if (patchers.isEmpty()) {
return "(none)";
}
return patchers.stream()
.map(p -> LANGUAGE_LABELS.getOrDefault(p.pluginId(),
p.pluginId().toUpperCase(Locale.ROOT)))
.distinct()
.collect(Collectors.joining(", "));
}
}