M10, AP-001 freigegeben
This commit is contained in:
@@ -74,3 +74,4 @@ replay_pid*
|
|||||||
/review-input.zip
|
/review-input.zip
|
||||||
/run-milestone.ps1
|
/run-milestone.ps1
|
||||||
/run-v11.ps1
|
/run-v11.ps1
|
||||||
|
.m2repo
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derived change-state view for the editor content.
|
||||||
|
* <p>
|
||||||
|
* The value is computed from the comparison between the baseline values and the current
|
||||||
|
* editor values.
|
||||||
|
*/
|
||||||
|
public enum GuiChangeState {
|
||||||
|
|
||||||
|
/** The current editor state matches its baseline. */
|
||||||
|
CLEAN,
|
||||||
|
|
||||||
|
/** The current editor state has diverged from its baseline. */
|
||||||
|
DIRTY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether this state represents unsaved changes.
|
||||||
|
*
|
||||||
|
* @return {@code true} when the editor is dirty, otherwise {@code false}
|
||||||
|
*/
|
||||||
|
public boolean isDirty() {
|
||||||
|
return this == DIRTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root editor state for the GUI configuration editor.
|
||||||
|
* <p>
|
||||||
|
* The state keeps the loaded file snapshot, the baseline values and the current editable values
|
||||||
|
* separate so later GUI layers can compare the current editor content against the original
|
||||||
|
* representation without relying on a manually maintained dirty flag.
|
||||||
|
*
|
||||||
|
* @param loadedFileSnapshot loaded file representation, or empty when the editor was created
|
||||||
|
* from the standard template
|
||||||
|
* @param baselineValues values representing the baseline the editor compares against
|
||||||
|
* @param values current editable configuration values
|
||||||
|
*/
|
||||||
|
public record GuiConfigurationEditorState(
|
||||||
|
Optional<GuiConfigurationFileSnapshot> loadedFileSnapshot,
|
||||||
|
GuiConfigurationValues baselineValues,
|
||||||
|
GuiConfigurationValues values) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new editor state.
|
||||||
|
*
|
||||||
|
* @param loadedFileSnapshot loaded file representation; {@code null} becomes empty
|
||||||
|
* @param baselineValues baseline values; must not be {@code null}
|
||||||
|
* @param values current editable configuration values; must not be {@code null}
|
||||||
|
*/
|
||||||
|
public GuiConfigurationEditorState {
|
||||||
|
loadedFileSnapshot = loadedFileSnapshot == null ? Optional.empty() : loadedFileSnapshot;
|
||||||
|
baselineValues = Objects.requireNonNull(baselineValues, "baselineValues must not be null");
|
||||||
|
values = Objects.requireNonNull(values, "values must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the editor currently contains unsaved changes.
|
||||||
|
*
|
||||||
|
* @return {@code true} when the current values differ from the baseline, otherwise {@code false}
|
||||||
|
*/
|
||||||
|
public boolean isDirty() {
|
||||||
|
return !baselineValues.equals(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the derived change state for the current editor content.
|
||||||
|
*
|
||||||
|
* @return {@link GuiChangeState#DIRTY} when the editor is dirty, otherwise {@link GuiChangeState#CLEAN}
|
||||||
|
*/
|
||||||
|
public GuiChangeState changeState() {
|
||||||
|
return isDirty() ? GuiChangeState.DIRTY : GuiChangeState.CLEAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the editor currently has a loaded file snapshot.
|
||||||
|
*
|
||||||
|
* @return {@code true} when the editor was loaded from disk, otherwise {@code false}
|
||||||
|
*/
|
||||||
|
public boolean hasLoadedFileSnapshot() {
|
||||||
|
return loadedFileSnapshot.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the editor represents a newly created configuration.
|
||||||
|
*
|
||||||
|
* @return {@code true} when no file snapshot is loaded, otherwise {@code false}
|
||||||
|
*/
|
||||||
|
public boolean isNewConfiguration() {
|
||||||
|
return loadedFileSnapshot.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy with different current editable values.
|
||||||
|
*
|
||||||
|
* @param values the new editable values; must not be {@code null}
|
||||||
|
* @return a new editor state containing the supplied values
|
||||||
|
*/
|
||||||
|
public GuiConfigurationEditorState withValues(GuiConfigurationValues values) {
|
||||||
|
return new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy with a different baseline.
|
||||||
|
*
|
||||||
|
* @param baselineValues the new baseline values; must not be {@code null}
|
||||||
|
* @return a new editor state using the supplied baseline
|
||||||
|
*/
|
||||||
|
public GuiConfigurationEditorState withBaselineValues(GuiConfigurationValues baselineValues) {
|
||||||
|
return new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy with a loaded file snapshot.
|
||||||
|
*
|
||||||
|
* @param snapshot the loaded file snapshot; must not be {@code null}
|
||||||
|
* @return a new editor state linked to the supplied snapshot
|
||||||
|
*/
|
||||||
|
public GuiConfigurationEditorState withLoadedFileSnapshot(GuiConfigurationFileSnapshot snapshot) {
|
||||||
|
return new GuiConfigurationEditorState(Optional.of(snapshot), baselineValues, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy without any loaded file snapshot.
|
||||||
|
*
|
||||||
|
* @return a new editor state without a loaded file snapshot
|
||||||
|
*/
|
||||||
|
public GuiConfigurationEditorState withoutLoadedFileSnapshot() {
|
||||||
|
return new GuiConfigurationEditorState(Optional.empty(), baselineValues, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a clean copy of this editor state by resetting the current values to the baseline.
|
||||||
|
*
|
||||||
|
* @return a clean editor state
|
||||||
|
*/
|
||||||
|
public GuiConfigurationEditorState markClean() {
|
||||||
|
return baselineValues.equals(values)
|
||||||
|
? this
|
||||||
|
: new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, baselineValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Snapshot of a configuration file loaded from disk.
|
||||||
|
* <p>
|
||||||
|
* This type is intentionally separate from the editable editor state. It represents the file
|
||||||
|
* that was loaded, not the mutable values currently being edited.
|
||||||
|
*/
|
||||||
|
public final class GuiConfigurationFileSnapshot {
|
||||||
|
|
||||||
|
private final Path filePath;
|
||||||
|
private final Properties properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a snapshot for the given file and properties.
|
||||||
|
*
|
||||||
|
* @param filePath path of the loaded file; must not be {@code null}
|
||||||
|
* @param properties loaded properties; must not be {@code null}
|
||||||
|
*/
|
||||||
|
public GuiConfigurationFileSnapshot(Path filePath, Properties properties) {
|
||||||
|
this.filePath = Objects.requireNonNull(filePath, "filePath must not be null");
|
||||||
|
this.properties = copyOf(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the file path of the loaded configuration.
|
||||||
|
*
|
||||||
|
* @return the snapshot file path; never {@code null}
|
||||||
|
*/
|
||||||
|
public Path filePath() {
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a defensive copy of the loaded properties.
|
||||||
|
*
|
||||||
|
* @return a copy of the loaded properties; never {@code null}
|
||||||
|
*/
|
||||||
|
public Properties properties() {
|
||||||
|
return copyOf(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether this snapshot was loaded from the given path.
|
||||||
|
*
|
||||||
|
* @param candidatePath the path to compare; may be {@code null}
|
||||||
|
* @return {@code true} when the paths match, otherwise {@code false}
|
||||||
|
*/
|
||||||
|
public boolean hasFilePath(Path candidatePath) {
|
||||||
|
return filePath.equals(candidatePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (this == other) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(other instanceof GuiConfigurationFileSnapshot that)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return filePath.equals(that.filePath) && properties.equals(that.properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(filePath, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "GuiConfigurationFileSnapshot{"
|
||||||
|
+ "filePath=" + filePath
|
||||||
|
+ ", properties=" + properties
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Properties copyOf(Properties source) {
|
||||||
|
Properties copy = new Properties();
|
||||||
|
copy.putAll(Objects.requireNonNull(source, "properties must not be null"));
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the standard GUI configuration template.
|
||||||
|
* <p>
|
||||||
|
* The template contains both known provider blocks, a complete set of general configuration
|
||||||
|
* values and a clean editor state that can be used for a brand-new configuration.
|
||||||
|
*/
|
||||||
|
public final class GuiConfigurationTemplateFactory {
|
||||||
|
|
||||||
|
private static final String SOURCE_FOLDER = "./work/local/source";
|
||||||
|
private static final String TARGET_FOLDER = "./work/local/target";
|
||||||
|
private static final String SQLITE_FILE = "./work/local/pdf-umbenenner.db";
|
||||||
|
private static final String PROMPT_TEMPLATE_FILE = "./config/prompts/template.txt";
|
||||||
|
private static final String RUNTIME_LOCK_FILE = "./work/local/pdf-umbenenner.lock";
|
||||||
|
private static final String LOG_DIRECTORY = "./work/local/logs";
|
||||||
|
private static final String LOG_LEVEL = "INFO";
|
||||||
|
private static final String MAX_RETRIES_TRANSIENT = "3";
|
||||||
|
private static final String MAX_PAGES = "10";
|
||||||
|
private static final String MAX_TEXT_CHARACTERS = "5000";
|
||||||
|
|
||||||
|
private static final String OPENAI_BASE_URL = "https://api.openai.com/v1";
|
||||||
|
private static final String OPENAI_MODEL = "gpt-4o-mini";
|
||||||
|
private static final String OPENAI_TIMEOUT_SECONDS = "30";
|
||||||
|
|
||||||
|
private static final String CLAUDE_BASE_URL = "https://api.anthropic.com";
|
||||||
|
private static final String CLAUDE_MODEL = "claude-3-5-sonnet-20241022";
|
||||||
|
private static final String CLAUDE_TIMEOUT_SECONDS = "60";
|
||||||
|
|
||||||
|
private GuiConfigurationTemplateFactory() {
|
||||||
|
// Utility class.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a brand-new clean editor state containing the standard configuration template.
|
||||||
|
*
|
||||||
|
* @return a clean editor state with no loaded file snapshot
|
||||||
|
*/
|
||||||
|
public static GuiConfigurationEditorState createStandardTemplate() {
|
||||||
|
GuiConfigurationValues standardValues = createStandardValues();
|
||||||
|
return new GuiConfigurationEditorState(Optional.empty(), standardValues, standardValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the editable values for the standard configuration template.
|
||||||
|
*
|
||||||
|
* @return the standard editable configuration values
|
||||||
|
*/
|
||||||
|
public static GuiConfigurationValues createStandardValues() {
|
||||||
|
Map<AiProviderFamily, GuiProviderConfigurationState> providerConfigurations = new LinkedHashMap<>();
|
||||||
|
providerConfigurations.put(AiProviderFamily.CLAUDE, new GuiProviderConfigurationState(
|
||||||
|
CLAUDE_BASE_URL,
|
||||||
|
CLAUDE_MODEL,
|
||||||
|
CLAUDE_TIMEOUT_SECONDS,
|
||||||
|
GuiProviderApiKeyState.unresolved()));
|
||||||
|
providerConfigurations.put(AiProviderFamily.OPENAI_COMPATIBLE, new GuiProviderConfigurationState(
|
||||||
|
OPENAI_BASE_URL,
|
||||||
|
OPENAI_MODEL,
|
||||||
|
OPENAI_TIMEOUT_SECONDS,
|
||||||
|
GuiProviderApiKeyState.unresolved()));
|
||||||
|
|
||||||
|
return new GuiConfigurationValues(
|
||||||
|
SOURCE_FOLDER,
|
||||||
|
TARGET_FOLDER,
|
||||||
|
SQLITE_FILE,
|
||||||
|
PROMPT_TEMPLATE_FILE,
|
||||||
|
RUNTIME_LOCK_FILE,
|
||||||
|
LOG_DIRECTORY,
|
||||||
|
LOG_LEVEL,
|
||||||
|
MAX_RETRIES_TRANSIENT,
|
||||||
|
MAX_PAGES,
|
||||||
|
MAX_TEXT_CHARACTERS,
|
||||||
|
Boolean.toString(false),
|
||||||
|
AiProviderFamily.CLAUDE.getIdentifier(),
|
||||||
|
providerConfigurations);
|
||||||
|
}
|
||||||
|
}
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editable GUI values for the complete configuration document.
|
||||||
|
* <p>
|
||||||
|
* This object combines all known general configuration values and the provider-specific blocks
|
||||||
|
* in a single model that can be rendered and edited by the GUI.
|
||||||
|
*
|
||||||
|
* @param sourceFolder source-folder path as editable text
|
||||||
|
* @param targetFolder target-folder path as editable text
|
||||||
|
* @param sqliteFile SQLite file path as editable text
|
||||||
|
* @param promptTemplateFile prompt-template path as editable text
|
||||||
|
* @param runtimeLockFile optional runtime lock file path as editable text
|
||||||
|
* @param logDirectory optional log directory as editable text
|
||||||
|
* @param logLevel log level as editable text
|
||||||
|
* @param maxRetriesTransient transient retry limit as editable text
|
||||||
|
* @param maxPages page limit as editable text
|
||||||
|
* @param maxTextCharacters text limit as editable text
|
||||||
|
* @param logAiSensitive raw value of {@code log.ai.sensitive} as editable text
|
||||||
|
* @param activeProviderFamily raw value of {@code ai.provider.active} as editable text
|
||||||
|
* @param providerConfigurations provider-specific editor state keyed by provider family
|
||||||
|
*/
|
||||||
|
public record GuiConfigurationValues(
|
||||||
|
String sourceFolder,
|
||||||
|
String targetFolder,
|
||||||
|
String sqliteFile,
|
||||||
|
String promptTemplateFile,
|
||||||
|
String runtimeLockFile,
|
||||||
|
String logDirectory,
|
||||||
|
String logLevel,
|
||||||
|
String maxRetriesTransient,
|
||||||
|
String maxPages,
|
||||||
|
String maxTextCharacters,
|
||||||
|
String logAiSensitive,
|
||||||
|
String activeProviderFamily,
|
||||||
|
Map<AiProviderFamily, GuiProviderConfigurationState> providerConfigurations) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new editable configuration state.
|
||||||
|
*
|
||||||
|
* @param sourceFolder source-folder path; {@code null} becomes an empty string
|
||||||
|
* @param targetFolder target-folder path; {@code null} becomes an empty string
|
||||||
|
* @param sqliteFile SQLite file path; {@code null} becomes an empty string
|
||||||
|
* @param promptTemplateFile prompt-template path; {@code null} becomes an empty string
|
||||||
|
* @param runtimeLockFile runtime lock file path; {@code null} becomes an empty string
|
||||||
|
* @param logDirectory log directory; {@code null} becomes an empty string
|
||||||
|
* @param logLevel log level; {@code null} becomes an empty string
|
||||||
|
* @param maxRetriesTransient transient retry limit; {@code null} becomes an empty string
|
||||||
|
* @param maxPages page limit; {@code null} becomes an empty string
|
||||||
|
* @param maxTextCharacters text limit; {@code null} becomes an empty string
|
||||||
|
* @param logAiSensitive raw {@code log.ai.sensitive} value; {@code null} becomes an empty string
|
||||||
|
* @param activeProviderFamily raw {@code ai.provider.active} value; {@code null} becomes an empty string
|
||||||
|
* @param providerConfigurations provider-specific state map; must not be {@code null}
|
||||||
|
*/
|
||||||
|
public GuiConfigurationValues {
|
||||||
|
sourceFolder = normalizeText(sourceFolder);
|
||||||
|
targetFolder = normalizeText(targetFolder);
|
||||||
|
sqliteFile = normalizeText(sqliteFile);
|
||||||
|
promptTemplateFile = normalizeText(promptTemplateFile);
|
||||||
|
runtimeLockFile = normalizeText(runtimeLockFile);
|
||||||
|
logDirectory = normalizeText(logDirectory);
|
||||||
|
logLevel = normalizeText(logLevel);
|
||||||
|
maxRetriesTransient = normalizeText(maxRetriesTransient);
|
||||||
|
maxPages = normalizeText(maxPages);
|
||||||
|
maxTextCharacters = normalizeText(maxTextCharacters);
|
||||||
|
logAiSensitive = normalizeText(logAiSensitive);
|
||||||
|
activeProviderFamily = normalizeText(activeProviderFamily);
|
||||||
|
|
||||||
|
LinkedHashMap<AiProviderFamily, GuiProviderConfigurationState> copy = new LinkedHashMap<>();
|
||||||
|
Objects.requireNonNull(providerConfigurations, "providerConfigurations must not be null")
|
||||||
|
.forEach(copy::put);
|
||||||
|
providerConfigurations = Collections.unmodifiableMap(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the editable state for the requested provider family.
|
||||||
|
*
|
||||||
|
* @param providerFamily the provider family to look up; must not be {@code null}
|
||||||
|
* @return the matching provider state, or {@code null} when not present
|
||||||
|
*/
|
||||||
|
public GuiProviderConfigurationState providerConfiguration(AiProviderFamily providerFamily) {
|
||||||
|
return providerConfigurations.get(providerFamily);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy with a different active provider-family identifier.
|
||||||
|
*
|
||||||
|
* @param providerFamily raw provider-family identifier; {@code null} becomes an empty string
|
||||||
|
* @return a new configuration values object with the requested active provider value
|
||||||
|
*/
|
||||||
|
public GuiConfigurationValues withActiveProviderFamily(String providerFamily) {
|
||||||
|
return new GuiConfigurationValues(sourceFolder, targetFolder, sqliteFile, promptTemplateFile,
|
||||||
|
runtimeLockFile, logDirectory, logLevel, maxRetriesTransient, maxPages, maxTextCharacters,
|
||||||
|
logAiSensitive, providerFamily, providerConfigurations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeText(String value) {
|
||||||
|
return value == null ? "" : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editable state for a provider-specific API key.
|
||||||
|
* <p>
|
||||||
|
* This value object keeps the editable property value separate from the later displayable
|
||||||
|
* origin of the effective key value. The editor can therefore preserve the property text
|
||||||
|
* while later layers determine whether the effective value comes from the file, an
|
||||||
|
* environment variable, a compatibility fallback, or a technical default.
|
||||||
|
*
|
||||||
|
* @param propertyValue editable API-key property value as entered or loaded from the file
|
||||||
|
* @param effectiveValueOrigin provenance of the effective value shown or inferred later
|
||||||
|
* @param effectiveValueDescription human-readable provenance description for later display
|
||||||
|
*/
|
||||||
|
public record GuiProviderApiKeyState(
|
||||||
|
String propertyValue,
|
||||||
|
GuiValueOrigin effectiveValueOrigin,
|
||||||
|
String effectiveValueDescription) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a key state with a blank property value and unresolved provenance.
|
||||||
|
*
|
||||||
|
* @return a new unresolved API-key state
|
||||||
|
*/
|
||||||
|
public static GuiProviderApiKeyState unresolved() {
|
||||||
|
return new GuiProviderApiKeyState("", GuiValueOrigin.UNKNOWN, "nicht aufgeloest");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a key state with the supplied property value and unresolved provenance.
|
||||||
|
*
|
||||||
|
* @param propertyValue editable property value; may be {@code null}
|
||||||
|
* @return a new unresolved API-key state
|
||||||
|
*/
|
||||||
|
public static GuiProviderApiKeyState unresolved(String propertyValue) {
|
||||||
|
return new GuiProviderApiKeyState(propertyValue, GuiValueOrigin.UNKNOWN, "nicht aufgeloest");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a key state with an explicit effective-value provenance.
|
||||||
|
*
|
||||||
|
* @param propertyValue editable property value; may be {@code null}
|
||||||
|
* @param effectiveValueOrigin origin of the effective value; must not be {@code null}
|
||||||
|
* @param effectiveValueDescription human-readable provenance description; may be {@code null}
|
||||||
|
* @return a new API-key state with the requested provenance
|
||||||
|
*/
|
||||||
|
public static GuiProviderApiKeyState resolved(String propertyValue,
|
||||||
|
GuiValueOrigin effectiveValueOrigin,
|
||||||
|
String effectiveValueDescription) {
|
||||||
|
return new GuiProviderApiKeyState(propertyValue, effectiveValueOrigin, effectiveValueDescription);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new key state.
|
||||||
|
*
|
||||||
|
* @param propertyValue editable property value; {@code null} is converted to an empty string
|
||||||
|
* @param effectiveValueOrigin origin of the effective value; {@code null} becomes {@link GuiValueOrigin#UNKNOWN}
|
||||||
|
* @param effectiveValueDescription human-readable provenance description; {@code null} becomes an empty string
|
||||||
|
*/
|
||||||
|
public GuiProviderApiKeyState {
|
||||||
|
propertyValue = propertyValue == null ? "" : propertyValue;
|
||||||
|
effectiveValueOrigin = Objects.requireNonNullElse(effectiveValueOrigin, GuiValueOrigin.UNKNOWN);
|
||||||
|
effectiveValueDescription = effectiveValueDescription == null ? "" : effectiveValueDescription;
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editable GUI state for one provider-family configuration block.
|
||||||
|
* <p>
|
||||||
|
* The fields are stored as editor-friendly strings so later GUI layers can bind them directly
|
||||||
|
* to text-based controls without prematurely parsing or validating them.
|
||||||
|
*
|
||||||
|
* @param baseUrl provider endpoint base URL as editable text
|
||||||
|
* @param model model identifier as editable text
|
||||||
|
* @param timeoutSeconds timeout value as editable text
|
||||||
|
* @param apiKey API-key state for the provider
|
||||||
|
*/
|
||||||
|
public record GuiProviderConfigurationState(
|
||||||
|
String baseUrl,
|
||||||
|
String model,
|
||||||
|
String timeoutSeconds,
|
||||||
|
GuiProviderApiKeyState apiKey) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a provider configuration state with all text fields blank and the API key unresolved.
|
||||||
|
*
|
||||||
|
* @return a blank provider configuration state
|
||||||
|
*/
|
||||||
|
public static GuiProviderConfigurationState blank() {
|
||||||
|
return new GuiProviderConfigurationState("", "", "", GuiProviderApiKeyState.unresolved());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new provider configuration state.
|
||||||
|
*
|
||||||
|
* @param baseUrl provider endpoint base URL; {@code null} becomes an empty string
|
||||||
|
* @param model model identifier; {@code null} becomes an empty string
|
||||||
|
* @param timeoutSeconds timeout value; {@code null} becomes an empty string
|
||||||
|
* @param apiKey API-key state; when {@code null}, an unresolved blank key state is used
|
||||||
|
*/
|
||||||
|
public GuiProviderConfigurationState {
|
||||||
|
baseUrl = baseUrl == null ? "" : baseUrl;
|
||||||
|
model = model == null ? "" : model;
|
||||||
|
timeoutSeconds = timeoutSeconds == null ? "" : timeoutSeconds;
|
||||||
|
apiKey = Objects.requireNonNullElse(apiKey, GuiProviderApiKeyState.unresolved());
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the effective origin of a configuration value shown by the editor.
|
||||||
|
* <p>
|
||||||
|
* The enum is intentionally broad enough to accommodate later provenance display for
|
||||||
|
* provider-specific values such as API keys.
|
||||||
|
*/
|
||||||
|
public enum GuiValueOrigin {
|
||||||
|
|
||||||
|
/** The value has not been resolved to a concrete origin yet. */
|
||||||
|
UNKNOWN,
|
||||||
|
|
||||||
|
/** The value comes from the editable property value stored in the configuration file. */
|
||||||
|
PROPERTY_VALUE,
|
||||||
|
|
||||||
|
/** The value comes from a provider-specific environment variable. */
|
||||||
|
ENVIRONMENT_VARIABLE,
|
||||||
|
|
||||||
|
/** The value comes from the legacy environment variable accepted for compatibility. */
|
||||||
|
LEGACY_ENVIRONMENT_VARIABLE,
|
||||||
|
|
||||||
|
/** The value is a technical default supplied by the application. */
|
||||||
|
DEFAULT_VALUE
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Editor state and template model for the JavaFX configuration editor.
|
||||||
|
* <p>
|
||||||
|
* This package contains the GUI-side representation of configuration data that can be edited
|
||||||
|
* independently from file I/O and validation. It separates loaded file snapshots, baseline
|
||||||
|
* editor values, current editor values, provider-specific API key state, and the derived
|
||||||
|
* dirty-state view used by the GUI.
|
||||||
|
* <p>
|
||||||
|
* The classes in this package are intentionally free of JavaFX controls so they can be reused
|
||||||
|
* by later GUI layers without coupling the model to a particular layout implementation.
|
||||||
|
*/
|
||||||
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class GuiConfigurationEditorStateTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dirtyStateReflectsComparisonAgainstBaseline() {
|
||||||
|
GuiConfigurationEditorState state = GuiConfigurationTemplateFactory.createStandardTemplate();
|
||||||
|
GuiConfigurationValues editedValues = new GuiConfigurationValues(
|
||||||
|
state.values().sourceFolder(),
|
||||||
|
state.values().targetFolder(),
|
||||||
|
state.values().sqliteFile(),
|
||||||
|
state.values().promptTemplateFile(),
|
||||||
|
state.values().runtimeLockFile(),
|
||||||
|
state.values().logDirectory(),
|
||||||
|
state.values().logLevel(),
|
||||||
|
state.values().maxRetriesTransient(),
|
||||||
|
state.values().maxPages(),
|
||||||
|
state.values().maxTextCharacters(),
|
||||||
|
"maybe",
|
||||||
|
"claude-42",
|
||||||
|
state.values().providerConfigurations());
|
||||||
|
|
||||||
|
GuiConfigurationEditorState dirty = state.withValues(editedValues);
|
||||||
|
|
||||||
|
assertTrue(dirty.isDirty());
|
||||||
|
assertEquals(state.baselineValues(), dirty.baselineValues());
|
||||||
|
assertEquals(editedValues, dirty.values());
|
||||||
|
assertEquals(GuiChangeState.DIRTY, dirty.changeState());
|
||||||
|
|
||||||
|
GuiConfigurationEditorState cleanAgain = dirty.markClean();
|
||||||
|
assertFalse(cleanAgain.isDirty());
|
||||||
|
assertEquals(state.baselineValues(), cleanAgain.baselineValues());
|
||||||
|
assertEquals(state.baselineValues(), cleanAgain.values());
|
||||||
|
assertEquals(GuiChangeState.CLEAN, cleanAgain.changeState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadedFileSnapshotCanBeAttachedWithoutChangingDirtyState() {
|
||||||
|
GuiConfigurationEditorState state = GuiConfigurationTemplateFactory.createStandardTemplate();
|
||||||
|
GuiConfigurationFileSnapshot snapshot = new GuiConfigurationFileSnapshot(
|
||||||
|
java.nio.file.Path.of("config/application.properties"),
|
||||||
|
new java.util.Properties());
|
||||||
|
|
||||||
|
GuiConfigurationEditorState withSnapshot = state.withLoadedFileSnapshot(snapshot);
|
||||||
|
|
||||||
|
assertTrue(withSnapshot.hasLoadedFileSnapshot());
|
||||||
|
assertFalse(withSnapshot.isNewConfiguration());
|
||||||
|
assertEquals(snapshot, withSnapshot.loadedFileSnapshot().orElseThrow());
|
||||||
|
assertFalse(withSnapshot.isDirty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resettingTheCurrentValuesToTheBaselineMakesTheEditorCleanAgain() {
|
||||||
|
GuiConfigurationEditorState state = GuiConfigurationTemplateFactory.createStandardTemplate();
|
||||||
|
GuiConfigurationValues editedValues = new GuiConfigurationValues(
|
||||||
|
state.values().sourceFolder(),
|
||||||
|
state.values().targetFolder(),
|
||||||
|
state.values().sqliteFile(),
|
||||||
|
state.values().promptTemplateFile(),
|
||||||
|
state.values().runtimeLockFile(),
|
||||||
|
state.values().logDirectory(),
|
||||||
|
state.values().logLevel(),
|
||||||
|
state.values().maxRetriesTransient(),
|
||||||
|
state.values().maxPages(),
|
||||||
|
state.values().maxTextCharacters(),
|
||||||
|
"true",
|
||||||
|
"openai-compatible",
|
||||||
|
state.values().providerConfigurations());
|
||||||
|
|
||||||
|
GuiConfigurationEditorState reverted = state.withValues(editedValues).withValues(state.baselineValues());
|
||||||
|
|
||||||
|
assertFalse(reverted.isDirty());
|
||||||
|
assertEquals(state.baselineValues(), reverted.values());
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class GuiConfigurationFileSnapshotTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void propertiesAreDefensivelyCopiedOnRead() {
|
||||||
|
Properties properties = new Properties();
|
||||||
|
properties.setProperty("source.folder", "./source");
|
||||||
|
|
||||||
|
GuiConfigurationFileSnapshot snapshot = new GuiConfigurationFileSnapshot(Path.of("config/application.properties"), properties);
|
||||||
|
|
||||||
|
Properties firstRead = snapshot.properties();
|
||||||
|
firstRead.setProperty("source.folder", "./changed");
|
||||||
|
|
||||||
|
Properties secondRead = snapshot.properties();
|
||||||
|
assertNotSame(firstRead, secondRead);
|
||||||
|
assertEquals("./source", secondRead.getProperty("source.folder"));
|
||||||
|
assertTrue(snapshot.hasFilePath(Path.of("config/application.properties")));
|
||||||
|
}
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
|
||||||
|
|
||||||
|
class GuiConfigurationTemplateFactoryTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createStandardTemplate_providesCompleteCleanEditorState() {
|
||||||
|
GuiConfigurationEditorState state = GuiConfigurationTemplateFactory.createStandardTemplate();
|
||||||
|
|
||||||
|
assertFalse(state.isDirty());
|
||||||
|
assertFalse(state.hasLoadedFileSnapshot());
|
||||||
|
assertTrue(state.isNewConfiguration());
|
||||||
|
|
||||||
|
GuiConfigurationValues values = state.values();
|
||||||
|
assertEquals("./work/local/source", values.sourceFolder());
|
||||||
|
assertEquals("./work/local/target", values.targetFolder());
|
||||||
|
assertEquals("./work/local/pdf-umbenenner.db", values.sqliteFile());
|
||||||
|
assertEquals("./config/prompts/template.txt", values.promptTemplateFile());
|
||||||
|
assertEquals("./work/local/pdf-umbenenner.lock", values.runtimeLockFile());
|
||||||
|
assertEquals("./work/local/logs", values.logDirectory());
|
||||||
|
assertEquals("INFO", values.logLevel());
|
||||||
|
assertEquals("3", values.maxRetriesTransient());
|
||||||
|
assertEquals("10", values.maxPages());
|
||||||
|
assertEquals("5000", values.maxTextCharacters());
|
||||||
|
assertEquals("false", values.logAiSensitive());
|
||||||
|
assertEquals(AiProviderFamily.CLAUDE.getIdentifier(), values.activeProviderFamily());
|
||||||
|
|
||||||
|
List<AiProviderFamily> providerOrder = values.providerConfigurations().keySet().stream().toList();
|
||||||
|
assertEquals(List.of(AiProviderFamily.CLAUDE, AiProviderFamily.OPENAI_COMPATIBLE), providerOrder);
|
||||||
|
|
||||||
|
GuiProviderConfigurationState claude = values.providerConfiguration(AiProviderFamily.CLAUDE);
|
||||||
|
assertNotNull(claude);
|
||||||
|
assertEquals("https://api.anthropic.com", claude.baseUrl());
|
||||||
|
assertEquals("claude-3-5-sonnet-20241022", claude.model());
|
||||||
|
assertEquals("60", claude.timeoutSeconds());
|
||||||
|
assertEquals("", claude.apiKey().propertyValue());
|
||||||
|
assertEquals(GuiValueOrigin.UNKNOWN, claude.apiKey().effectiveValueOrigin());
|
||||||
|
|
||||||
|
GuiProviderConfigurationState openAi = values.providerConfiguration(AiProviderFamily.OPENAI_COMPATIBLE);
|
||||||
|
assertNotNull(openAi);
|
||||||
|
assertEquals("https://api.openai.com/v1", openAi.baseUrl());
|
||||||
|
assertEquals("gpt-4o-mini", openAi.model());
|
||||||
|
assertEquals("30", openAi.timeoutSeconds());
|
||||||
|
assertEquals("", openAi.apiKey().propertyValue());
|
||||||
|
assertEquals(GuiValueOrigin.UNKNOWN, openAi.apiKey().effectiveValueOrigin());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void providerConfigurationMap_isImmutableFromOutside() {
|
||||||
|
GuiConfigurationValues values = GuiConfigurationTemplateFactory.createStandardValues();
|
||||||
|
|
||||||
|
assertThrows(UnsupportedOperationException.class, () ->
|
||||||
|
values.providerConfigurations().put(AiProviderFamily.CLAUDE, GuiProviderConfigurationState.blank()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withActiveProviderFamily_reusesProviderBlocksAndChangesSelection() {
|
||||||
|
GuiConfigurationValues values = GuiConfigurationTemplateFactory.createStandardValues();
|
||||||
|
|
||||||
|
GuiConfigurationValues switched = values.withActiveProviderFamily(AiProviderFamily.OPENAI_COMPATIBLE.getIdentifier());
|
||||||
|
|
||||||
|
assertEquals(AiProviderFamily.OPENAI_COMPATIBLE.getIdentifier(), switched.activeProviderFamily());
|
||||||
|
assertEquals(values.providerConfigurations(), switched.providerConfigurations());
|
||||||
|
assertEquals(values.sourceFolder(), switched.sourceFolder());
|
||||||
|
}
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
|
||||||
|
|
||||||
|
class GuiConfigurationValuesTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rawGeneralValuesRemainUntouched() {
|
||||||
|
Map<AiProviderFamily, GuiProviderConfigurationState> providerConfigurations = new LinkedHashMap<>();
|
||||||
|
providerConfigurations.put(AiProviderFamily.CLAUDE, GuiProviderConfigurationState.blank());
|
||||||
|
|
||||||
|
GuiConfigurationValues values = new GuiConfigurationValues(
|
||||||
|
"./source",
|
||||||
|
"./target",
|
||||||
|
"./config/db.sqlite",
|
||||||
|
"./config/prompt.txt",
|
||||||
|
"./config/runtime.lock",
|
||||||
|
"./logs",
|
||||||
|
"DEBUG",
|
||||||
|
"12",
|
||||||
|
"34",
|
||||||
|
"56",
|
||||||
|
"maybe",
|
||||||
|
"not-a-provider-family",
|
||||||
|
providerConfigurations);
|
||||||
|
|
||||||
|
assertEquals("maybe", values.logAiSensitive());
|
||||||
|
assertEquals("not-a-provider-family", values.activeProviderFamily());
|
||||||
|
assertEquals(GuiProviderConfigurationState.blank(), values.providerConfiguration(AiProviderFamily.CLAUDE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void providerConfigurationMapIsDefensivelyCopied() {
|
||||||
|
Map<AiProviderFamily, GuiProviderConfigurationState> providerConfigurations = new LinkedHashMap<>();
|
||||||
|
providerConfigurations.put(AiProviderFamily.CLAUDE, GuiProviderConfigurationState.blank());
|
||||||
|
|
||||||
|
GuiConfigurationValues values = new GuiConfigurationValues(
|
||||||
|
"./source",
|
||||||
|
"./target",
|
||||||
|
"./config/db.sqlite",
|
||||||
|
"./config/prompt.txt",
|
||||||
|
"./config/runtime.lock",
|
||||||
|
"./logs",
|
||||||
|
"DEBUG",
|
||||||
|
"12",
|
||||||
|
"34",
|
||||||
|
"56",
|
||||||
|
"true",
|
||||||
|
AiProviderFamily.CLAUDE.getIdentifier(),
|
||||||
|
providerConfigurations);
|
||||||
|
|
||||||
|
providerConfigurations.put(AiProviderFamily.OPENAI_COMPATIBLE, GuiProviderConfigurationState.blank());
|
||||||
|
|
||||||
|
assertNotSame(providerConfigurations, values.providerConfigurations());
|
||||||
|
assertEquals(1, values.providerConfigurations().size());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user