Skip to content

Latest commit

 

History

History
258 lines (176 loc) · 15.8 KB

File metadata and controls

258 lines (176 loc) · 15.8 KB

Named signature-setting presets (+ XDG-aware config location)

Status: proposal Target release: 3.0.0 Tracking: GitHub issue #252 (config location); no dedicated issue for presets (user-request driven) Scope: JavaFX GUI; CLI unchanged (but shares the config-location code path)

1. Context and motivation

Today every signing setting lives in a single flat properties file at ~/.JSignPdf. There is only one implicit profile — "last used" — so anyone who routinely switches between several configurations has to re-enter them by hand each time.

Issue #252 (unrelated, but adjacent) asks that the config file move out of $HOME and into an XDG-compliant location. A preset feature needs a directory the app owns anyway, so both changes land together in 3.0.0.

2. Design goals and non-goals

Goals

  • Let the user save an arbitrary number of named signature-setting presets and switch between them with one click.
  • Store configuration in a platform-appropriate, XDG-respecting location on Linux; %APPDATA% on Windows; ~/Library/Application Support on macOS.
  • Do not break existing installs: if users already have ~/.JSignPdf, their settings must survive the upgrade untouched.
  • Allow arbitrary display names (Unicode, diacritics, punctuation) without imposing filename-safety rules on users.
  • Keep the data format as plain Java properties so a user can inspect and hand-edit preset files if they want.

Non-goals

  • No export/import UI. (Users can copy *.properties files around themselves; we don't need to ship a dialog for it.)
  • No cross-machine preset sharing as a first-class feature. JSignEncryptor uses a machine-local seed, so encrypted passwords won't decrypt elsewhere. We accept this.
  • No split of config / state / cache across separate XDG dirs. Everything stays under one jsignpdf/ directory for now.
  • No CLI changes. The CLI will continue to load the same main config file (from its new location), but has no notion of presets.

3. Decisions settled

The following were pinned by the project owner before this doc was written:

Decision Choice
Migration strategy One-shot: if the new config dir exists, use it; otherwise fall back to ~/.JSignPdf, then seed the new dir from it on first write.
Preset display name Stored as preset.displayName inside each preset file.
Preset filename Always written as preset-<epochMillis>.properties. Reads accept any *.properties file in the presets dir.
Export/import UI Out of scope for 3.0.0.
UI surface Both a top-level Presets menu and an always-visible combo box in the main window.
PropertyProvider Refactor away from the singleton where it makes sense.

Sections 4–10 build on these choices.

4. Config location resolution

Introduce ConfigLocationResolver (new class, net.sf.jsignpdf.utils). Its job is to resolve a single Path — the application's config directory — exactly once per JVM, and to hand out Paths for the main config file and the presets subdir.

Resolution order:

  1. Environment override: JSIGNPDF_CONFIG_DIR env var. If set (non-empty), use that path verbatim. For portable installs, tests, and power users.
  2. Platform-native location — used both when reading (if it exists) and when creating a fresh dir:
    • Linux / BSD / other Unix: $XDG_CONFIG_HOME/jsignpdf/ if XDG_CONFIG_HOME is set and non-empty, otherwise $HOME/.config/jsignpdf/ (lowercase name per XDG convention).
    • Windows: %APPDATA%\JSignPdf\, falling back to %USERPROFILE%\AppData\Roaming\JSignPdf\ if APPDATA is unset.
    • macOS: $HOME/Library/Application Support/JSignPdf/.
  3. Legacy fallback: $HOME/.JSignPdf (as a file, not a dir — see §6 for how it's handled).

OS detection via System.getProperty("os.name") with the usual toLowerCase() substring checks (win, mac, otherwise Unix).

If HOME / user.home is missing entirely (exotic headless envs), log a warning and refuse to persist anything rather than dumping files in the CWD. CLI can still sign a document; only persistence breaks.

5. File layout

After migration, everything is under one dir. Call it <cfg> for brevity.

<cfg>/
  config.properties          # former ~/.JSignPdf content
  presets/
    preset-1745270400123.properties
    preset-1745270500456.properties

Main config (config.properties). Same keys and encoding as the existing ~/.JSignPdf. No schema change — the main config tracks "last used settings," not "last used preset." Preset selection is fire-and-forget (see §9.1), so nothing about it is persisted.

Preset file. A properties file with:

  • preset.displayName — required, UTF-8, the user-visible name. Can contain any character except control chars and / \ (validated in the dialog).
  • preset.createdAt — optional ISO-8601 timestamp, informational.
  • A subset of the keys from BasicSignerOptions.storeOptions() — see §7 for the exact scope.

No preset.name or other identity field beyond displayName; the file itself is the identity.

6. Migration (one-shot)

On first ConfigLocationResolver call per JVM:

  1. Resolve target dir (§4, step 1 or 2).
  2. If target dir exists → done, it is authoritative.
  3. If target dir doesn't exist and $HOME/.JSignPdf exists as a file:
    • Create target dir and its presets/ subdir.
    • Copy $HOME/.JSignPdf<cfg>/config.properties (byte-for-byte copy, Files.copy).
    • Leave the legacy file in place. Rationale: if the user downgrades to a pre-3.0.0 build, their old install still works.
    • Log an INFO line: Migrated legacy settings from ~/.JSignPdf to <cfg>/config.properties.
  4. If target dir doesn't exist and no legacy file exists → create target dir empty, the app starts with defaults. (Fresh install.)

After migration, the legacy file is never read or written by the new code. We do not attempt to detect divergence if both exist (e.g. user ran an old version after a new one); the new dir always wins.

7. Preset scope — which keys get saved

A preset captures the signing configuration, not the session state. Concretely:

Included (the bulk of BasicSignerOptions):

  • Keystore: ksType, ksFile, keyAlias, keyIndex (password intentionally excluded — see below).
  • Passwords: ksPasswd, keyPasswd, pdfOwnerPwd, pdfUserPwd, tsaPasswd, tsaCertFilePwd.
  • Signature metadata: signerName, reason, location, contact.
  • Visible signature: visible, page, positionLLX/Y/URX/Y, bgImgScale, renderMode, l2Text, l4Text, l2TextFontSize, imgPath, bgImgPath, acro6Layers.
  • TSA: all tsa.* keys.
  • PDF encryption and rights (all current keys).
  • OCSP/CRL, proxy.

Excluded:

  • Recent-file list, last-used input/output paths, window geometry — these aren't part of the signing configuration.

8. Code architecture

8.1 PropertyProvider refactor

Current state: PropertyProvider is a singleton tied to the hardcoded $HOME/.JSignPdf path. It holds a Properties instance and exposes typed getters/setters (getAsInt, setPropNullSensitive, etc.).

Problem: presets are N independent property stores; a single-instance design doesn't fit. Also, path resolution belongs in ConfigLocationResolver, not here.

Refactor:

  1. Remove the getInstance() singleton and the hardcoded PROPERTY_FILE constant. Make the constructor public and accept a Path.
  2. loadDefault() / saveDefault() go away; callers use load() / save() which operate on the path handed to the constructor.
  3. Add a thin factory on PropertyStoreFactory (new class):
    • mainConfig()PropertyProvider backed by <cfg>/config.properties.
    • preset(String filename)PropertyProvider backed by <cfg>/presets/<filename>.
    • newPreset()PropertyProvider backed by a fresh preset-<epochMillis>.properties (with collision-retry).
  4. All call sites currently using PropertyProvider.getInstance() are rewritten to go through PropertyStoreFactory.mainConfig() (dependency-inject the instance where practical, use the factory at composition roots otherwise).

The typed getters/setters (getAsInt, getAsBool, …) stay intact — that API is fine.

8.2 PresetManager (new)

net.sf.jsignpdf.fx.preset.PresetManager. Lives in the JavaFX module; owns the list of presets as an ObservableList<Preset> so the combo box can bind to it directly.

Responsibilities:

  • Scan <cfg>/presets/ for *.properties on startup; build Preset records (filename, displayName, createdAt).
  • Provide: saveAsNew(BasicSignerOptions, String displayName), overwrite(Preset, BasicSignerOptions), rename(Preset, newName), delete(Preset), load(Preset).

No notion of a "currently active" preset is tracked. Loading is a one-way transfer (preset → live options); after load returns, the manager has no opinion about which preset the user is "on." That means no dirty tracking, no confirmation prompts on switch, no restore-on-launch.

saveAsNew always writes a fresh preset-<epochMillis>.properties (retry with -2, -3, … on collision within the same millisecond). overwrite updates the contents of an existing preset file in place, preserving its filename. rename rewrites only preset.displayName inside the existing file — no file move. delete is Files.delete.

8.3 BasicSignerOptions changes

Add two methods:

public void loadFromPreset(PropertyProvider store);
public void storeToPreset(PropertyProvider store);

These reuse the existing per-field load/store logic. The existing loadOptions() / storeOptions() — which operate on the main config — stay as-is, except they now take a PropertyProvider argument instead of reaching for the singleton.

9. UI design

The UI has two surfaces, with a clear split of concerns:

  • Toolbar combo — one-shot loader. Pick an entry, settings get populated, end of interaction.
  • Presets menu — the two mutating actions (save new / manage). Rename, delete, and in-place overwrite live inside the Manage dialog.

The design is deliberately stateless: there is no "active preset" tracked anywhere — not in the UI, not in config, not in memory. Load is fire-and-forget.

9.1 Presets combo box in the toolbar

The combo is placed in the existing main-window toolbar, alongside the Open / zoom / other controls.

[ ☰ File | Presets | Help ]
[ Toolbar:  [Open] [Zoom-] [Zoom+] ...   Preset: [ Load preset… ▾ ] ]
  • Combo contents come straight from PresetManager.getPresets() (ObservableList), sorted by display name.
  • Selecting a preset immediately loads itPresetManager.load(preset) runs on the selection-change event and calls signingVM.syncFromOptions(opts). No intermediate "Load" button, no confirmation prompt, no dirty-state detection. If the user has unsaved edits when they pick a preset, those edits are overwritten; this is the explicit tradeoff of fire-and-forget.
  • The combo is purely a launcher — it retains no "selected preset" state. After the load fires, the combo visually returns to its placeholder (Load preset…). This reinforces that there is no current preset, only current settings.
  • When the presets list is empty, the combo is disabled and shows No presets saved as its placeholder.

9.2 Presets menu

A new top-level menu between File and Help:

Presets
 ├─ Save current as new preset…
 └─ Manage presets…
  • Save current as new preset… opens a dialog for the display name (validated per §9.3) and writes a new preset-<epochMillis>.properties file.

  • Manage presets… opens a dialog listing all presets. Each row has three actions:

    • Rename — inline edit of the display name, validated per §9.3.
    • Overwrite with current settings — copies the live BasicSignerOptions into the selected preset file (preserving filename and createdAt). Confirmation prompt first, since this is destructive to the preset's previous contents.
    • Delete — confirmation, then Files.delete.

    No export/import. No reordering.

There is no Load submenu — the toolbar combo is the canonical load affordance. There is also no "Save" item separate from "Save as new" — without an active-preset concept, there is nothing to "save over," so the only two save-path operations are "create new" (menu) and "overwrite this specific one" (Manage dialog).

9.3 Display-name validation

Enforced in the Save / Save as / Rename dialogs, before PresetManager is called:

  • Trim leading/trailing whitespace.
  • Reject empty or whitespace-only.
  • Reject control characters (< 0x20, 0x7F).
  • Length cap at 60 chars — UI sanity, not a filesystem limit.
  • Reject if another preset already uses the same display name (case-insensitive).

None of this touches the filename — that remains preset-<epochMillis>.properties regardless.

10. i18n

New keys added to net.sf.jsignpdf.translations.messages (English as the source of truth):

  • jfx.gui.preset.label — "Preset:"
  • jfx.gui.preset.placeholder — "Load preset…"
  • jfx.gui.preset.placeholder.empty — "No presets saved"
  • jfx.gui.menu.presets, jfx.gui.menu.presets.saveAsNew, .manage
  • jfx.gui.preset.dialog.saveAsNew.title, .saveAsNew.prompt, .rename.title
  • jfx.gui.preset.dialog.delete.confirm — "Delete preset '{0}'?"
  • jfx.gui.preset.dialog.overwrite.confirm — "Overwrite preset '{0}' with current settings?"
  • jfx.gui.preset.validation.empty, .illegalChar, .tooLong, .duplicate
  • jfx.gui.preset.manage.title, .column.name, .column.created, .button.rename, .button.overwrite, .button.delete

11. Testing strategy

Unit:

  • ConfigLocationResolver with per-OS system-property override and env-var stubs: Linux with/without XDG_CONFIG_HOME, Windows with/without APPDATA, macOS, JSIGNPDF_CONFIG_DIR override, missing user.home.
  • Migration: legacy-present-no-new-dir → dir created and file copied, byte-for-byte; legacy-absent-new-dir-present → no-op; both-present → new dir wins, legacy untouched.
  • PresetManager: save → reload → display name round-trips; rename preserves filename and createdAt; overwrite preserves filename and createdAt but updates the rest; delete removes file; scan ignores files that lack preset.displayName.
  • Display-name validation: whitespace, control chars, /, \, length cap, case-insensitive uniqueness.
  • Filename collision on System.currentTimeMillis() within the same millisecond: retry with counter suffix (preset-<ts>-2.properties).

Integration:

  • End-to-end: open app with a legacy ~/.JSignPdf in a temp home, verify dir created and content migrated.
  • Save preset, restart, verify the preset file is still present in the toolbar combo (no active-preset restore — the combo starts at placeholder).
  • Preset file with only preset.displayName set (all-defaults preset) loads without error.

Manual / UI:

  • Toolbar combo: selecting a preset loads it directly and returns to the Load preset… placeholder.
  • Combo is disabled with No presets saved when the presets dir is empty, and becomes enabled as soon as the first preset is saved.
  • "Save current as new preset…" creates a visible new entry in the combo immediately.
  • Manage dialog: rename / overwrite / delete each reflect in the toolbar combo contents without needing a restart.
  • Overwrite and Delete both prompt for confirmation.

12. Open questions and future work

  • Export / import. Deferred. If requested, we'd add a file-chooser pair that copies .properties files to/from the presets dir, plus an optional "strip passwords" checkbox. Not in this PR.
  • CLI preset selection. Deferred. If requested, add a --preset <name> flag that loads a preset's keys before applying other CLI options. Out of scope for 3.0.0.
  • Per-preset log/state split. Strict XDG purists might want logs under $XDG_STATE_HOME/jsignpdf/ and caches under $XDG_CACHE_HOME/jsignpdf/. Not tackled here; the single <cfg> dir is enough for 3.0.0.