Skip to content

Commit f383b56

Browse files
authored
Improve TSA configuration UI (#348)
1 parent f42d931 commit f383b56

28 files changed

Lines changed: 332 additions & 104 deletions

jsignpdf/src/main/java/net/sf/jsignpdf/Constants.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public class Constants {
140140
PDF2IMAGE_LIBRARIES_DEFAULT);
141141

142142
public static final String DEFVAL_TSA_HASH_ALG = ConfigProvider.getInstance().getNotEmptyProperty("tsa.hashAlgorithm",
143-
"SHA-1");
143+
"SHA-256");
144144

145145
/**
146146
* Property name.

jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/MainWindowController.java

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
import javafx.fxml.FXML;
1010
import javafx.geometry.Insets;
1111
import javafx.scene.Node;
12+
import javafx.scene.control.Accordion;
1213
import javafx.scene.control.Alert;
1314
import javafx.scene.control.Button;
1415
import javafx.scene.control.ButtonBar;
1516
import javafx.scene.control.ButtonType;
1617
import javafx.scene.control.CheckMenuItem;
18+
import javafx.scene.control.TitledPane;
1719
import javafx.scene.control.ComboBox;
1820
import javafx.scene.control.Dialog;
1921
import javafx.scene.control.Label;
@@ -25,6 +27,7 @@
2527
import javafx.scene.control.ScrollPane;
2628
import javafx.scene.control.SplitPane;
2729
import javafx.scene.control.TextField;
30+
import javafx.scene.control.ToggleButton;
2831
import javafx.scene.input.DragEvent;
2932
import javafx.scene.input.Dragboard;
3033
import javafx.scene.input.KeyCode;
@@ -112,11 +115,15 @@ public class MainWindowController {
112115
@FXML private TextField txtPageNumber;
113116
@FXML private Label lblPageCount;
114117
@FXML private Button btnNextPage;
115-
@FXML private Button btnClearVisibleSig;
118+
@FXML private ToggleButton btnVisibleSig;
119+
@FXML private ToggleButton btnTsa;
116120
@FXML private Button btnSign;
117121

118122
// Content area
119123
@FXML private SplitPane splitPane;
124+
@FXML private Accordion sidePanelAccordion;
125+
@FXML private TitledPane tsaAccordionPane;
126+
@FXML private TitledPane encryptionAccordionPane;
120127
@FXML private ScrollPane scrollPane;
121128
@FXML private StackPane pdfArea;
122129
@FXML private Label lblDropHint;
@@ -236,12 +243,22 @@ private void initialize() {
236243
} else {
237244
autoPlaceVisibleSignature();
238245
}
239-
updateVisibleSigIndicators();
240246
updateSigStateBadge();
241247
});
242248

243-
// Bind visible-signature CheckMenuItem bidirectionally to the ViewModel
249+
// Bind the visible-signature controls (CheckMenuItem + toolbar toggle)
250+
// bidirectionally to the ViewModel so they mirror the side-panel checkbox.
244251
menuVisibleSig.selectedProperty().bindBidirectional(signingVM.visibleProperty());
252+
btnVisibleSig.selectedProperty().bindBidirectional(signingVM.visibleProperty());
253+
btnTsa.selectedProperty().bindBidirectional(signingVM.tsaEnabledProperty());
254+
255+
// When TSA is turned on but no URL is configured yet, jump the side-panel
256+
// accordion to the TSA section so the user can fill the required field.
257+
signingVM.tsaEnabledProperty().addListener((obs, was, on) -> {
258+
if (on && isBlank(signingVM.tsaUrlProperty().get())) {
259+
expandTsaPane();
260+
}
261+
});
245262

246263
// Status-bar badge: visible whenever a document is loaded. Its text and
247264
// colour swap based on whether visible signature is on or off.
@@ -256,7 +273,6 @@ private void initialize() {
256273
// Initial state for the visible-signature controls.
257274
// The badge's initial text and style come from FXML (correct for
258275
// visibleProperty=false on startup); listeners take over on state changes.
259-
updateVisibleSigIndicators();
260276
updateOutputPathLabel();
261277

262278
// Keep overlay sized to match the pdf page view
@@ -385,6 +401,7 @@ private void setDocumentControlsDisabled(boolean disabled) {
385401
txtPageNumber.setDisable(disabled);
386402
btnNextPage.setDisable(disabled);
387403
btnSign.setDisable(disabled);
404+
btnVisibleSig.setDisable(disabled);
388405
menuSign.setDisable(disabled);
389406
menuClose.setDisable(disabled);
390407
menuSaveAs.setDisable(disabled);
@@ -395,20 +412,6 @@ private void setDocumentControlsDisabled(boolean disabled) {
395412
if (signatureSettingsController != null) {
396413
signatureSettingsController.setVisibleSigCheckBoxDisabled(disabled);
397414
}
398-
// Visible-signature controls track both document state and current toggle
399-
updateVisibleSigIndicators();
400-
}
401-
402-
/**
403-
* Refreshes the enabled state of the "clear visible signature" toolbar button.
404-
* It is only meaningful when a document is loaded and the visible signature
405-
* is currently enabled.
406-
*/
407-
private void updateVisibleSigIndicators() {
408-
boolean canClear = documentVM.isDocumentLoaded() && signingVM.visibleProperty().get();
409-
if (btnClearVisibleSig != null) {
410-
btnClearVisibleSig.setDisable(!canClear);
411-
}
412415
}
413416

414417
/**
@@ -517,6 +520,22 @@ private void updateNavButtonState() {
517520
btnNextPage.setDisable(!documentVM.canGoNext());
518521
}
519522

523+
private void expandTsaPane() {
524+
if (sidePanelAccordion != null && tsaAccordionPane != null) {
525+
sidePanelAccordion.setExpandedPane(tsaAccordionPane);
526+
}
527+
}
528+
529+
private void expandEncryptionPane() {
530+
if (sidePanelAccordion != null && encryptionAccordionPane != null) {
531+
sidePanelAccordion.setExpandedPane(encryptionAccordionPane);
532+
}
533+
}
534+
535+
private static boolean isBlank(String s) {
536+
return s == null || s.trim().isEmpty();
537+
}
538+
520539
private void updateStatus(String message) {
521540
lblStatus.setText(message);
522541
}
@@ -653,11 +672,6 @@ private void onSaveAs() {
653672
}
654673
}
655674

656-
@FXML
657-
private void onClearVisibleSig() {
658-
signingVM.visibleProperty().set(false);
659-
}
660-
661675
@FXML
662676
private void onSign() {
663677
if (options == null || !documentVM.isDocumentLoaded()) {
@@ -669,13 +683,23 @@ private void onSign() {
669683

670684
// Validate encryption-dependent required fields before signing
671685
if (encryptionSettingsController != null && !encryptionSettingsController.isEncryptionConfigValid()) {
686+
expandEncryptionPane();
672687
String prefix = encryptionSettingsController.getValidationErrorKeyPrefix();
673688
showAlert(Alert.AlertType.WARNING,
674689
RES.get(prefix + ".title"),
675690
RES.get(prefix + ".text"));
676691
return;
677692
}
678693

694+
// Validate TSA: when enabled, the TSA server URL is mandatory.
695+
if (tsaSettingsController != null && !tsaSettingsController.isTsaConfigValid()) {
696+
expandTsaPane();
697+
showAlert(Alert.AlertType.WARNING,
698+
RES.get("jfx.gui.dialog.missingTsaUrl.title"),
699+
RES.get("jfx.gui.dialog.missingTsaUrl.text"));
700+
return;
701+
}
702+
679703
// Sync ViewModel to options
680704
signingVM.syncToOptions(options);
681705

@@ -732,9 +756,11 @@ private void onZoomOut() {
732756
private void onZoomFit() {
733757
if (!documentVM.isDocumentLoaded() || documentVM.getCurrentPageImage() == null) return;
734758
double imgWidth = documentVM.getCurrentPageImage().getWidth();
759+
double imgHeight = documentVM.getCurrentPageImage().getHeight();
735760
double viewWidth = scrollPane.getViewportBounds().getWidth();
736-
if (imgWidth > 0 && viewWidth > 0) {
737-
documentVM.setZoomLevel(viewWidth / imgWidth);
761+
double viewHeight = scrollPane.getViewportBounds().getHeight();
762+
if (imgWidth > 0 && imgHeight > 0 && viewWidth > 0 && viewHeight > 0) {
763+
documentVM.setZoomLevel(Math.min(viewWidth / imgWidth, viewHeight / imgHeight));
738764
}
739765
}
740766

jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/TsaSettingsController.java

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
import javafx.scene.layout.VBox;
1515
import javafx.stage.FileChooser;
1616
import net.sf.jsignpdf.fx.viewmodel.SigningOptionsViewModel;
17+
import net.sf.jsignpdf.types.HashAlgorithm;
1718
import net.sf.jsignpdf.types.ServerAuthentication;
19+
import net.sf.jsignpdf.utils.KeyStoreUtils;
1820

1921
import java.net.Proxy;
2022

@@ -23,16 +25,20 @@
2325
*/
2426
public class TsaSettingsController {
2527

28+
private static final String STYLE_VALIDATION_ERROR = "-fx-border-color: red; -fx-border-width: 1;";
29+
2630
@FXML private CheckBox chkTsaEnabled;
2731
@FXML private TextField txtTsaUrl;
2832
@FXML private ComboBox<ServerAuthentication> cmbTsaAuthn;
33+
@FXML private VBox tsaUserPane;
2934
@FXML private TextField txtTsaUser;
3035
@FXML private PasswordField txtTsaPassword;
31-
@FXML private TextField txtTsaCertFileType;
36+
@FXML private VBox tsaCertPane;
37+
@FXML private ComboBox<String> cmbTsaCertFileType;
3238
@FXML private TextField txtTsaCertFile;
3339
@FXML private PasswordField txtTsaCertFilePassword;
3440
@FXML private TextField txtTsaPolicy;
35-
@FXML private TextField txtTsaHashAlg;
41+
@FXML private ComboBox<HashAlgorithm> cmbTsaHashAlg;
3642
@FXML private VBox tsaDetailsPane;
3743

3844
@FXML private CheckBox chkOcspEnabled;
@@ -51,14 +57,28 @@ public class TsaSettingsController {
5157
@FXML
5258
private void initialize() {
5359
cmbTsaAuthn.setItems(FXCollections.observableArrayList(ServerAuthentication.values()));
60+
cmbTsaHashAlg.setItems(FXCollections.observableArrayList(HashAlgorithm.values()));
61+
cmbTsaCertFileType.setItems(FXCollections.observableArrayList(KeyStoreUtils.getKeyStores()));
5462
cmbProxyType.setItems(FXCollections.observableArrayList(Proxy.Type.values()));
5563

5664
// Toggle TSA details visibility
5765
tsaDetailsPane.managedProperty().bind(tsaDetailsPane.visibleProperty());
58-
chkTsaEnabled.selectedProperty().addListener((obs, o, n) ->
59-
tsaDetailsPane.setVisible(n));
66+
chkTsaEnabled.selectedProperty().addListener((obs, o, n) -> {
67+
tsaDetailsPane.setVisible(n);
68+
updateValidation();
69+
});
6070
tsaDetailsPane.setVisible(false);
6171

72+
// Live validation: TSA URL is required when TSA is enabled.
73+
txtTsaUrl.textProperty().addListener((obs, o, n) -> updateValidation());
74+
75+
// Toggle auth-dependent panes: show only the inputs relevant to the
76+
// selected authentication method (user/password vs certificate file).
77+
tsaUserPane.managedProperty().bind(tsaUserPane.visibleProperty());
78+
tsaCertPane.managedProperty().bind(tsaCertPane.visibleProperty());
79+
cmbTsaAuthn.valueProperty().addListener((obs, o, n) -> applyAuthVisibility(n));
80+
applyAuthVisibility(null);
81+
6282
// Toggle OCSP URL visibility
6383
chkOcspEnabled.selectedProperty().addListener((obs, o, n) -> {
6484
lblOcspServerUrl.setVisible(n);
@@ -104,11 +124,11 @@ private void bindToViewModel() {
104124
cmbTsaAuthn.valueProperty().bindBidirectional(viewModel.tsaServerAuthnProperty());
105125
txtTsaUser.textProperty().bindBidirectional(viewModel.tsaUserProperty());
106126
txtTsaPassword.textProperty().bindBidirectional(viewModel.tsaPasswordProperty());
107-
txtTsaCertFileType.textProperty().bindBidirectional(viewModel.tsaCertFileTypeProperty());
127+
cmbTsaCertFileType.valueProperty().bindBidirectional(viewModel.tsaCertFileTypeProperty());
108128
txtTsaCertFile.textProperty().bindBidirectional(viewModel.tsaCertFileProperty());
109129
txtTsaCertFilePassword.textProperty().bindBidirectional(viewModel.tsaCertFilePasswordProperty());
110130
txtTsaPolicy.textProperty().bindBidirectional(viewModel.tsaPolicyProperty());
111-
txtTsaHashAlg.textProperty().bindBidirectional(viewModel.tsaHashAlgProperty());
131+
cmbTsaHashAlg.valueProperty().bindBidirectional(viewModel.tsaHashAlgProperty());
112132

113133
chkOcspEnabled.selectedProperty().bindBidirectional(viewModel.ocspEnabledProperty());
114134
txtOcspServerUrl.textProperty().bindBidirectional(viewModel.ocspServerUrlProperty());
@@ -129,6 +149,8 @@ private void bindToViewModel() {
129149

130150
// Update visibility from initial loaded values
131151
tsaDetailsPane.setVisible(viewModel.tsaEnabledProperty().get());
152+
updateValidation();
153+
applyAuthVisibility(viewModel.tsaServerAuthnProperty().get());
132154
boolean ocspOn = viewModel.ocspEnabledProperty().get();
133155
lblOcspServerUrl.setVisible(ocspOn);
134156
txtOcspServerUrl.setVisible(ocspOn);
@@ -146,6 +168,32 @@ private void bindToViewModel() {
146168
txtProxyPort.setManaged(proxyOn);
147169
}
148170

171+
private void applyAuthVisibility(ServerAuthentication authn) {
172+
tsaUserPane.setVisible(authn == ServerAuthentication.PASSWORD);
173+
tsaCertPane.setVisible(authn == ServerAuthentication.CERTIFICATE);
174+
}
175+
176+
/**
177+
* Applies or clears a red-border style on the TSA URL field. The URL is
178+
* required whenever TSA is enabled.
179+
*/
180+
private void updateValidation() {
181+
boolean invalid = chkTsaEnabled.isSelected() && isBlank(txtTsaUrl.getText());
182+
txtTsaUrl.setStyle(invalid ? STYLE_VALIDATION_ERROR : null);
183+
}
184+
185+
/**
186+
* Returns true if TSA is either disabled or has a non-blank server URL.
187+
* Used by the main controller to gate Sign.
188+
*/
189+
public boolean isTsaConfigValid() {
190+
return !chkTsaEnabled.isSelected() || !isBlank(txtTsaUrl.getText());
191+
}
192+
193+
private static boolean isBlank(String s) {
194+
return s == null || s.trim().isEmpty();
195+
}
196+
149197
@FXML
150198
private void onBrowseTsaCertFile() {
151199
FileChooser fc = new FileChooser();

jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/SigningOptionsViewModel.java

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public class SigningOptionsViewModel {
9292
private final StringProperty tsaCertFile = new SimpleStringProperty();
9393
private final StringProperty tsaCertFilePassword = new SimpleStringProperty();
9494
private final StringProperty tsaPolicy = new SimpleStringProperty();
95-
private final StringProperty tsaHashAlg = new SimpleStringProperty();
95+
private final ObjectProperty<HashAlgorithm> tsaHashAlg = new SimpleObjectProperty<>();
9696

9797
// OCSP/CRL
9898
private final BooleanProperty ocspEnabled = new SimpleBooleanProperty(false);
@@ -168,7 +168,8 @@ public void syncToOptions(BasicSignerOptions opts) {
168168
opts.setTsaCertFile(tsaCertFile.get());
169169
opts.setTsaCertFilePwd(tsaCertFilePassword.get());
170170
opts.setTsaPolicy(tsaPolicy.get());
171-
opts.setTsaHashAlg(tsaHashAlg.get());
171+
HashAlgorithm tsaHa = tsaHashAlg.get();
172+
opts.setTsaHashAlg(tsaHa != null ? tsaHa.getAlgorithmName() : null);
172173

173174
// OCSP/CRL
174175
opts.setOcspEnabled(ocspEnabled.get());
@@ -234,11 +235,11 @@ public void syncFromOptions(BasicSignerOptions opts) {
234235
tsaServerAuthn.set(opts.getTsaServerAuthn());
235236
tsaUser.set(opts.getTsaUser());
236237
tsaPassword.set(opts.getTsaPasswd());
237-
tsaCertFileType.set(opts.getTsaCertFileType());
238+
tsaCertFileType.set(resolveTsaCertFileType(opts.getTsaCertFileType()));
238239
tsaCertFile.set(opts.getTsaCertFile());
239240
tsaCertFilePassword.set(opts.getTsaCertFilePwd());
240241
tsaPolicy.set(opts.getTsaPolicy());
241-
tsaHashAlg.set(opts.getTsaHashAlg());
242+
tsaHashAlg.set(resolveTsaHashAlg(opts.getTsaHashAlg()));
242243

243244
ocspEnabled.set(opts.isOcspEnabled());
244245
ocspServerUrl.set(opts.getOcspServerUrl());
@@ -307,11 +308,11 @@ public void resetToDefaults() {
307308
tsaServerAuthn.set(ServerAuthentication.NONE);
308309
tsaUser.set(null);
309310
tsaPassword.set(null);
310-
tsaCertFileType.set(null);
311+
tsaCertFileType.set(resolveTsaCertFileType(null));
311312
tsaCertFile.set(null);
312313
tsaCertFilePassword.set(null);
313314
tsaPolicy.set(null);
314-
tsaHashAlg.set(null);
315+
tsaHashAlg.set(resolveTsaHashAlg(null));
315316

316317
// OCSP/CRL
317318
ocspEnabled.set(false);
@@ -324,6 +325,32 @@ public void resetToDefaults() {
324325
proxyPort.set(Constants.DEFVAL_PROXY_PORT);
325326
}
326327

328+
/**
329+
* Defaults a blank TSA cert file type to PKCS12 — the TSA panel always
330+
* exposes a concrete keystore-type selection, matching the CLI default
331+
* (see {@link net.sf.jsignpdf.ssl.SSLInitializer}).
332+
*/
333+
private static String resolveTsaCertFileType(String stored) {
334+
if (stored == null || stored.trim().isEmpty()) {
335+
return "PKCS12";
336+
}
337+
return stored;
338+
}
339+
340+
/**
341+
* Resolves a persisted TSA hash algorithm name to the matching enum value.
342+
* Falls back to the configured default when the stored value is blank or
343+
* not recognised — the TSA panel always exposes a concrete selection.
344+
*/
345+
private static HashAlgorithm resolveTsaHashAlg(String stored) {
346+
HashAlgorithm ha = HashAlgorithm.fromAlgorithmName(stored);
347+
if (ha != null) {
348+
return ha;
349+
}
350+
HashAlgorithm fallback = HashAlgorithm.fromAlgorithmName(Constants.DEFVAL_TSA_HASH_ALG);
351+
return fallback != null ? fallback : HashAlgorithm.SHA256;
352+
}
353+
327354
private static char[] toCharArray(String s) {
328355
return s != null ? s.toCharArray() : null;
329356
}
@@ -382,7 +409,7 @@ private static String fromCharArray(char[] c) {
382409
public StringProperty tsaCertFileProperty() { return tsaCertFile; }
383410
public StringProperty tsaCertFilePasswordProperty() { return tsaCertFilePassword; }
384411
public StringProperty tsaPolicyProperty() { return tsaPolicy; }
385-
public StringProperty tsaHashAlgProperty() { return tsaHashAlg; }
412+
public ObjectProperty<HashAlgorithm> tsaHashAlgProperty() { return tsaHashAlg; }
386413
public BooleanProperty ocspEnabledProperty() { return ocspEnabled; }
387414
public StringProperty ocspServerUrlProperty() { return ocspServerUrl; }
388415
public BooleanProperty crlEnabledProperty() { return crlEnabled; }

0 commit comments

Comments
 (0)