M10, AP-001 freigegeben
This commit is contained in:
@@ -74,3 +74,4 @@ replay_pid*
|
||||
/review-input.zip
|
||||
/run-milestone.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