Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,13 @@ public void setStage(Stage stage) {
public void initFromOptions(BasicSignerOptions opts) {
this.options = opts;
signingVM.syncFromOptions(opts);

// The output file name is session state: the persisted one names the document signed last time, so it is
// dropped (unlike the output directory and the suffix, which are settings) and every run starts derived.
opts.setOutFile(null);
signingVM.outBaseNameProperty().set(null);
resolveOutputFile();

// No document is loaded at startup, so the visible-signature toggle must
// start disabled. The persisted position coordinates on signingVM are
// preserved so we can auto-place at the last-known location once a
Expand Down Expand Up @@ -653,8 +660,7 @@ private void loadPreset(Preset preset) {
// Flush any pending edits from the VM so that "load" is clearly "replace current".
signingVM.syncToOptions(options);
presetManager.load(preset, options);
signingVM.syncFromOptions(options);
resolveOutputFile();
reloadFromOptionsKeepingOutputName();
// Move the on-screen rectangle to match the preset's position.
applySigningVMPositionToPlacement();
updateStatus(java.text.MessageFormat.format(
Expand Down Expand Up @@ -961,6 +967,16 @@ private boolean isDerivedBaseName(String baseName, String previousInFile) {
return baseName.equals(derived);
}

/**
* Reloads the signing view model from {@code options} after they were changed behind its back (a preset load, an
* owner-password retry) and recomposes the output path. The output file name is carried over rather than re-read,
* because {@code options} hold it as a path composed with the suffix and input they had before the change.
*/
private void reloadFromOptionsKeepingOutputName() {
signingVM.syncFromOptionsKeepingOutBaseName(options);
resolveOutputFile();
}

/**
* Resolves the effective output path from the two user-facing fields — the output directory ({@code outPath}) and the
* output file name ({@code outBaseName}) — plus the input file and suffix, and writes it to {@code outFile} (the value
Expand Down Expand Up @@ -1518,17 +1534,17 @@ private void openDocument(File file) {
saveViewStateToConfig();
try {
if (options == null) {
options = new BasicSignerOptions();
options.loadOptions();
signingVM.syncFromOptions(options);
BasicSignerOptions opts = new BasicSignerOptions();
opts.loadOptions();
initFromOptions(opts);
}

// Reset visible signature and placement from previous document
signingVM.visibleProperty().set(false);
placementVM.reset();

// A derived output name is refreshed for the new document; a name the user deliberately chose is kept, like
// the output directory. The two are told apart by comparing against the name the previous input derived —
// A derived output name is refreshed for the new document; a name the user deliberately chose is kept for
// the rest of the session. The two are told apart by comparing against the name the previous input derived —
// captured before setInFile below overwrites it. The path resolves via the document-file listener once
// documentVM is updated further down.
if (isDerivedBaseName(signingVM.outBaseNameProperty().get(), options.getInFile())) {
Expand Down Expand Up @@ -1599,7 +1615,7 @@ private int promptPasswordAndRetry(File file) {

options.setPdfOwnerPwd(password.get().toCharArray());
options.setAdvanced(true);
signingVM.syncFromOptions(options);
reloadFromOptionsKeepingOutputName();

try {
PdfExtraInfo extraInfo = new PdfExtraInfo(options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,20 @@ public void syncFromOptions(BasicSignerOptions opts) {
proxyPort.set(opts.getProxyPort());
}

/**
* Syncs from a BasicSignerOptions instance, keeping the output file name currently held by this ViewModel.
* <p>
* {@link #syncFromOptions(BasicSignerOptions)} has to tell a derived output name from a deliberately chosen one by
* comparing the stored path against the one the options derive — which misreads a derived name as chosen whenever
* the reload brings a different suffix or input file. Mid-session the ViewModel already knows which of the two it
* holds, so callers that reload live options (preset load, owner-password retry) keep that answer instead.
*/
public void syncFromOptionsKeepingOutBaseName(BasicSignerOptions opts) {
String currentBaseName = outBaseName.get();
syncFromOptions(opts);
outBaseName.set(currentBaseName);
}

/**
* Resets all ViewModel properties to their default values.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package net.sf.jsignpdf.fx.view;

import static org.junit.Assert.assertNull;

import java.lang.reflect.Field;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.layout.BorderPane;

import org.junit.BeforeClass;
import org.junit.Test;

import net.sf.jsignpdf.BasicSignerOptions;
import net.sf.jsignpdf.Constants;
import net.sf.jsignpdf.fx.MonocleAssumption;
import net.sf.jsignpdf.fx.viewmodel.SigningOptionsViewModel;

/**
* The output file name is session state. A name carried over from the previous run would otherwise be applied to the
* first document opened in this one, and - being read back as a deliberate choice - would then stop following the
* suffix for the rest of the session.
*/
public class MainWindowOutputNameTest {

@BeforeClass
public static void initFx() throws Exception {
MonocleAssumption.assumeUsable();
CountDownLatch latch = new CountDownLatch(1);
try {
Platform.startup(latch::countDown);
} catch (IllegalStateException e) {
latch.countDown();
}
latch.await(5, TimeUnit.SECONDS);
}

@Test
public void persistedOutputNameIsDroppedAtStartup() throws Exception {
AtomicReference<String> baseName = new AtomicReference<>();
AtomicReference<String> outFile = new AtomicReference<>();
AtomicReference<String> persisted = new AtomicReference<>();
runOnFxThread(() -> {
MainWindowController controller = loadMainWindow();

BasicSignerOptions opts = new BasicSignerOptions();
opts.setInFile("/docs/drawing.pdf");
opts.setOutSuffix("_EM");
opts.setOutFile("/docs/final.pdf");
controller.initFromOptions(opts);

SigningOptionsViewModel vm = signingViewModel(controller);
baseName.set(vm.outBaseNameProperty().get());
outFile.set(vm.outFileProperty().get());
persisted.set(opts.getOutFile());
});

assertNull("the persisted output name must not survive a restart", baseName.get());
assertNull("no document is open, so there is no output path yet", outFile.get());
assertNull("the options must not keep the stale path either", persisted.get());
}

private static MainWindowController loadMainWindow() {
try {
FXMLLoader loader = new FXMLLoader(
MainWindowOutputNameTest.class.getResource("/net/sf/jsignpdf/fx/view/MainWindow.fxml"),
ResourceBundle.getBundle(Constants.RESOURCE_BUNDLE_BASE, Locale.ENGLISH));
loader.<BorderPane>load();
return loader.getController();
} catch (Exception e) {
throw new IllegalStateException(e);
}
}

private static SigningOptionsViewModel signingViewModel(MainWindowController controller) {
try {
Field field = MainWindowController.class.getDeclaredField("signingVM");
field.setAccessible(true);
return (SigningOptionsViewModel) field.get(controller);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException(e);
}
}

private static void runOnFxThread(Runnable action) throws Exception {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<Throwable> error = new AtomicReference<>();
Platform.runLater(() -> {
try {
action.run();
} catch (Throwable t) {
error.set(t);
} finally {
latch.countDown();
}
});
latch.await(10, TimeUnit.SECONDS);
if (error.get() != null) {
throw new AssertionError(error.get());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -351,4 +351,60 @@ public void syncFromOptions_customOutputNameSurvives() {
assertEquals("A deliberately chosen name must survive the round trip",
"final.pdf", vm.outBaseNameProperty().get());
}

/**
* Loading a preset replaces the suffix, which leaves the output path in the options composed with the old one.
* A cleared name must stay cleared so it is recomposed with the new suffix instead of accumulating both.
*/
@Test
public void syncFromOptionsKeepingOutBaseName_clearedNameStaysCleared() {
BasicSignerOptions opts = new BasicSignerOptions();
opts.setInFile("/docs/drawing.pdf");
opts.setOutSuffix("_EM");
opts.setOutFile(opts.getOutFileX());

SigningOptionsViewModel vm = new SigningOptionsViewModel();
vm.syncFromOptions(opts);
opts.setOutSuffix("_DL");
vm.syncFromOptionsKeepingOutBaseName(opts);

assertNull("The loaded suffix must not turn a derived name into a chosen one",
vm.outBaseNameProperty().get());
}

/**
* The owner-password retry re-syncs while the options already name the new input but still hold the output path
* of the previous document.
*/
@Test
public void syncFromOptionsKeepingOutBaseName_staleOutputOfPreviousInputIsIgnored() {
BasicSignerOptions opts = new BasicSignerOptions();
opts.setInFile("/docs/drawing.pdf");
opts.setOutSuffix("_signed");
opts.setOutFile(opts.getOutFileX());

SigningOptionsViewModel vm = new SigningOptionsViewModel();
vm.syncFromOptions(opts);
opts.setInFile("/docs/encrypted.pdf");
vm.syncFromOptionsKeepingOutBaseName(opts);

assertNull("The previous document's output name must not become the new document's chosen name",
vm.outBaseNameProperty().get());
}

@Test
public void syncFromOptionsKeepingOutBaseName_chosenNameSurvives() {
BasicSignerOptions opts = new BasicSignerOptions();
opts.setInFile("/docs/drawing.pdf");
opts.setOutSuffix("_EM");
opts.setOutFile("/docs/final.pdf");

SigningOptionsViewModel vm = new SigningOptionsViewModel();
vm.syncFromOptions(opts);
opts.setOutSuffix("_DL");
vm.syncFromOptionsKeepingOutBaseName(opts);

assertEquals("A deliberately chosen name must survive a preset load",
"final.pdf", vm.outBaseNameProperty().get());
}
}
Loading