From 1073af21305360bd33903c533cdac57e9f936294 Mon Sep 17 00:00:00 2001 From: Yuuta Liang Date: Tue, 28 Nov 2023 18:19:39 -0800 Subject: Move TUI and GUI into separate packages Signed-off-by: Yuuta Liang --- src/main/ui/gui/MainUI.java | 696 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 696 insertions(+) create mode 100644 src/main/ui/gui/MainUI.java (limited to 'src/main/ui/gui/MainUI.java') diff --git a/src/main/ui/gui/MainUI.java b/src/main/ui/gui/MainUI.java new file mode 100644 index 0000000..9fa65ef --- /dev/null +++ b/src/main/ui/gui/MainUI.java @@ -0,0 +1,696 @@ +package ui.gui; + +import model.GroupObserver; +import model.ObservedData; +import model.Observer; +import model.asn1.exceptions.InvalidCAException; +import model.asn1.exceptions.InvalidDBException; +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import model.ca.AuditLogEntry; +import model.ca.CertificationAuthority; +import model.ca.Template; +import model.csr.CertificationRequest; +import model.pki.cert.Certificate; +import model.pki.crl.CertificateList; +import model.pki.crl.RevokedCertificate; +import persistence.Decoder; +import persistence.FS; +import ui.Utils; +import ui.gui.widgets.*; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.security.interfaces.RSAPublicKey; + +import static java.awt.GridBagConstraints.*; +import static ui.gui.widgets.UIUtils.*; + +/** + * The main GUI. + * +------------------------------++------------------------------++------------------------------+ + * | JCA X || JCA X || JCA X | + * +------------------------------++------------------------------++------------------------------+ + * |Load Save CSR||Load Save Sign Revoke Export||Load Save New Ena Dis Del| + * +----+-------+-----------+-----++----+-------+----------+------++----+-------+-----------------+ + * | CA | Certs | Templates | || CA | Certs | Templates| || CA | Certs | Templates | + * +----+-------+-----------+-----++-+--+-----+-+----+-----+------++-+--+---+---+-----+-----------+ + * | Welcome to JCA || | Serial | Subj | Bef | To || | Name | Subj | Validity | + * | |+-+--------+------+-----+------++-+------+---------+-----------+ + * | Private key: Generate || (Issued Certs) || (All Templates) | + * | CA certificate: Install CSR || || | + * +------------------------------++------------------------------++------------------------------+ + * +-----+----------+-------------++-----+----------+-------------++-----+----------+-------------+ + * |Time | Operator | Action ||Time | Operator | Action ||Time | Operator | Action | + * +-----+----------+-------------++-----+----------+-------------++-----+----------+-------------+ + * | (Audit Logs) || (Audit Logs) || (Audit Logs) | + * +------------------------------++------------------------------++------------------------------+ + * | Ready: (Last operation) || Unsaved: (Last operation) || Ready: (Last Operation) | + * +------------------------------++------------------------------++------------------------------+ + */ +public class MainUI extends JFrame { + /** + * Default db file (./data/ca.json) + */ + private static final Path PATH_DEFAULT = Path.of("data", "ca.json"); + + /** + * The root panel (Box layout). + */ + private final JPanel rootPanel = new JPanel(new BoxLayout(rootPane, BoxLayout.PAGE_AXIS)); + + /** + * Common toolbar buttons + */ + private final JButton buttonToolbarLoad = btn("Load", "open.png", this::onLoad); + private final JButton buttonToolbarSave = btn("Save", "saveall.png", this::onSave); + + /** + * Toolbar that switches with the tab. + */ + private final JPanel panelContextAwareToolbar = new JPanel(new CardLayout(0, 0)); + + /** + * Tab root. + */ + private final JTabbedPane tabbedPane = new JTabbedPane(); + + /** + * CA tab + */ + private final JLabel labelCACertificate = new JLabel(); + private final JLabel labelPrivateKey = new JLabel(); + private final JButton buttonGenPrivKey = btn("Generate", 'G', this::onGeneratePrivateKey); + private final JButton buttonInstallCA = btn("Install", 'I', this::onInstallCA); + private final JButton buttonGenCSR = btn("CSR", this::onSignCSR); + private final QRPanel panelQR = new QRPanel(256); + private final JToolBar toolbarCA = new JToolBar(); + private final JButton buttonCAToolbarCRL = btn("CRL", "publisher.png", this::onCRL); + + /** + * Certs tab + */ + private JPanel panelCertsTab; + private final JTable tableCerts = new JTable(); + private final CertTableModel modelCerts = new CertTableModel(); + private final JToolBar toolbarCerts = new JToolBar(); + private final JButton buttonCertsToolbarNew = btn("Sign", "new.png", this::onIssue); + private final JButton buttonCertsToolbarRevoke = btn("Revoke", "deletetest.png", this::onRevokeCert); + private final JButton buttonCertsToolbarExport = btn("Export", "export.png", this::onExportCert); + + /** + * Templates tab + */ + private JPanel panelTmpTab; + private final JTable tableTemplates = new JTable(); + private final TemplateTableModel modelTemplates = new TemplateTableModel(); + private final JToolBar toolbarTemplates = new JToolBar(); + private final JButton buttonTemplatesToolbarNew = btn("New", "new.png", this::onNewTemplate); + private final JButton buttonTemplatesToolbarEnable = btn("Enable", "enable.png", this::onEnableTemplate); + private final JButton buttonTemplatesToolbarDisable = btn("Disable", "disable.png", this::onDisableTemplate); + private final JButton buttonTemplatesToolbarDelete = btn("Delete", "deletetest.png", this::onDeleteTemplate); + + /** + * Logs region + */ + private final JPanel panelLogs; + private final JTable tableAuditLogs = new JTable(); + private final LogTableModel modelAuditLogs = new LogTableModel(); + + /** + * Status region + */ + private final JLabel labelStatus = new JLabel(); + private ObservedData unsaved = new ObservedData<>(false, this::acceptUnsaved); + + /** + * CA and observers + */ + private CertificationAuthority ca; + + private final GroupObserver obs = new GroupObserver(); + + /** + * EFFECTS: Setup the CA and GUI. + */ + public MainUI() { + setTitle("JCA"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + setContentPane(rootPanel); + rootPanel.setLayout(new BoxLayout(rootPanel, BoxLayout.PAGE_AXIS)); + + rootPanel.add(setupToolbar()); + final JSplitPane splitPane = new JSplitPane(0); + rootPanel.add(splitPane); + splitPane.setLeftComponent(setupTabs()); + rootPanel.getRootPane().setDefaultButton(buttonGenPrivKey); + + splitPane.setRightComponent(panelLogs = defView(scrTbl(tableAuditLogs), "No audit logs")); + panelLogs.setPreferredSize(new Dimension(panelLogs.getPreferredSize().width, + panelLogs.getPreferredSize().height / 2)); + panelLogs.setBackground(new Color(-1)); + + rootPanel.add(labelStatus); + labelStatus.setBorder(BorderFactory.createEmptyBorder(4, 8, 8, 8)); + + ca = new CertificationAuthority(); + + tableAuditLogs.setModel(modelAuditLogs); + tableCerts.setModel(modelCerts); + tableTemplates.setModel(modelTemplates); + + tabbedPane.addChangeListener(this::onChangeTab); + setupObservers(); + + setCA(ca); + } + + // -----BEGIN HELPER METHODS----- + + /** + * EFFECTS: Rewind the CA and refresh all pages. + * MODIFIES: ca (observer), this + */ + private void setCA(CertificationAuthority ca) { + this.ca = ca; + ca.registerObserver(obs); + + modelAuditLogs.setPtrData(ca.getLogs()); + modelCerts.setPtrData(ca.getSigned()); + modelCerts.setPtrRevokedData(ca.getRevoked()); + modelTemplates.setPtrData(ca.getTemplates()); + + renderRefresh(); + } + + /** + * EFFECTS: Set the QR code data to either null (no public key) or PEM-encoded PKCS#1 public key. + */ + private void renderQRPublicKey() { + if (ca.getPublicKey() == null) { + panelQR.setData(null); + return; + } + panelQR.setData(Utils.toPEM(Utils.byteToByte(ca.getPublicKey().getEncoded()), "PUBLIC KEY")); + } + + // -----END HELPER METHODS----- + + // -----BEGIN DATA OBSERVERS----- + + /** + * EFFECTS: Setup observers + * MODIFIES: this + */ + private void setupObservers() { + obs.register(AuditLogEntry.class, this::acceptAuditLog); + obs.register(Certificate.class, this::acceptCertificate); + obs.register(RSAPublicKey.class, this::acceptPrivateKey); + obs.register(Template.class, this::acceptTemplate); + } + + /** + * EFFECTS: Handle new audit log, hide default text, notify the model + * MODIFIES: this + * REQUIRES: direction == ADD, i >= 0 + */ + private void acceptAuditLog(AuditLogEntry auditLogEntry, int direction, int i) { + setContentVisible(panelLogs, true); + modelAuditLogs.fireTableRowsInserted(ca.getLogs().size() - 1, ca.getLogs().size() - 1); + acceptUnsaved(unsaved.get(), Observer.DIRECTION_CHANGE, Observer.INDEX_NOT_IN_LIST); + } + + /** + * EFFECTS: Handle CA cert or new issued cert, notify CA page or model accordingly, hide certs default text + * MODIFIES: this + * REQUIRES: direction == CHANGE or ADD, i >= 0 or -1 + */ + private void acceptCertificate(Certificate cert, int direction, int i) { + if (i == Observer.INDEX_NOT_IN_LIST) { + renderCAPage(); + } else { + setContentVisible(panelCertsTab, true); + modelCerts.fireTableRowsInserted(i, i); + } + } + + /** + * EFFECTS: Handle added, changed, or deleted template; notify model, show / hide default text. + * MODIFIES: this + * REQUIRES: i >= 0. + */ + private void acceptTemplate(Template template, int direction, int i) { + setContentVisible(panelTmpTab, !ca.getTemplates().isEmpty()); + switch (direction) { + case Observer.DIRECTION_ADD: + modelTemplates.fireTableRowsInserted(i, i); + break; + case Observer.DIRECTION_CHANGE: + modelTemplates.fireTableRowsUpdated(i, i); + break; + case Observer.DIRECTION_REMOVE: + modelTemplates.fireTableRowsDeleted(i, i); + break; + } + } + + /** + * EFFECTS: Handle added private key. Change buttons / labels accordingly. + * MODIFIES: this + */ + private void acceptPrivateKey(RSAPublicKey pubKey, int direction, int i) { + renderCAPage(); + } + + /** + * EFFECTS: Handle status label change, set status to unsaved / saved + latest action. + * MODIFIES: this + */ + private void acceptUnsaved(Boolean unsaved, int direction, int i) { + labelStatus.setText(unsaved ? "Unsaved" : "Ready"); + if (!ca.getLogs().isEmpty()) { + labelStatus.setText(labelStatus.getText() + ": " + ca.getLogs().get(ca.getLogs().size() - 1).getAction()); + } + } + + // -----END DATA OBSERVERS---- + + // -----BEGIN RENDERERS----- + + /** + * EFFECTS: Setup the toolbar. + * MODIFIES: this + */ + private JToolBar setupToolbar() { + final JToolBar toolBar = new JToolBar(); + toolBar.setAlignmentX(Component.LEFT_ALIGNMENT); + toolBar.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8)); + + toolBar.add(buttonToolbarLoad); + toolBar.add(buttonToolbarSave); + + toolBar.add(panelContextAwareToolbar); + + panelContextAwareToolbar.add(toolbarTemplates, "CardToolbarTemplates"); + + toolbarTemplates.add(Box.createHorizontalGlue()); + toolbarTemplates.add(buttonTemplatesToolbarNew); + toolbarTemplates.add(buttonTemplatesToolbarEnable); + toolbarTemplates.add(buttonTemplatesToolbarDisable); + toolbarTemplates.add(buttonTemplatesToolbarDelete); + + panelContextAwareToolbar.add(toolbarCerts, "CardToolbarCerts"); + toolbarCerts.add(Box.createHorizontalGlue()); + toolbarCerts.add(buttonCertsToolbarNew); + toolbarCerts.add(buttonCertsToolbarRevoke); + toolbarCerts.add(buttonCertsToolbarExport); + + panelContextAwareToolbar.add(toolbarCA, "CardToolbarCA"); + toolbarCA.add(Box.createHorizontalGlue()); + toolbarCA.add(buttonCAToolbarCRL); + return toolBar; + } + + /** + * EFFECTS: Setup the tabs + * MODIFIES: this + */ + private JTabbedPane setupTabs() { + tabbedPane.setAlignmentX(Component.LEFT_ALIGNMENT); + tabbedPane.setBorder(BorderFactory.createEmptyBorder(8, 8, 4, 8)); + + final JPanel panelTabCA = new JPanel(); + panelTabCA.setLayout(new GridBagLayout()); + tabbedPane.addTab("CA", panelTabCA); + + panelTabCA.add(new JLabel("Welcome to JCA"), new GCBuilder().anchor(WEST).insectTop(8).build()); + panelTabCA.add(labelPrivateKey, new GCBuilder().gridY(1).anchor(WEST).build()); + panelTabCA.add(labelCACertificate, new GCBuilder().gridY(2).anchor(WEST).build()); + panelTabCA.add(buttonGenPrivKey, new GCBuilder().gridXY(1, 1).fill(HORIZONTAL).build()); + panelTabCA.add(buttonInstallCA, new GCBuilder().gridXY(1, 2).fill(HORIZONTAL).build()); + panelTabCA.add(buttonGenCSR, new GCBuilder().gridXY(2, 2).fill(HORIZONTAL).build()); + panelTabCA.add(panelQR, new GCBuilder().gridY(3).fill(HORIZONTAL).build()); + panelTabCA.add(new JPanel(), new GCBuilder().gridXY(4, 4).expandXY().fill(BOTH).build()); + tabbedPane.addTab("Certs", panelCertsTab = defView(scrTbl(tableCerts), "No issued certs")); + tabbedPane.addTab("Templates", panelTmpTab = defView(scrTbl(tableTemplates), "No templates")); + + return tabbedPane; + } + + /** + * EFFECTS: Render public key and CA to the CA page and toolbar + * MODIFIES: this + */ + private void renderCAPage() { + renderQRPublicKey(); + if (ca.getPublicKey() == null) { + labelPrivateKey.setText("Private key not installed"); + } else { + labelPrivateKey.setText(String.format("%s key: %s ...", + ca.getPublicKey().getAlgorithm(), + ca.getPublicKey().getModulus().toString(16).substring(0, 16))); + } + if (ca.getCertificate() == null) { + labelCACertificate.setText("CA certificate not installed"); + } else { + labelCACertificate.setText(String.format("CA: %s
Issued by: %s", + ca.getCertificate().getCertificate().getSubject().toString(), + ca.getCertificate().getCertificate().getIssuer().toString())); + } + buttonGenPrivKey.setEnabled(ca.getPublicKey() == null); + buttonInstallCA.setEnabled(ca.getPublicKey() != null && ca.getCertificate() == null); + buttonGenCSR.setEnabled(ca.getPublicKey() != null && ca.getCertificate() == null); + buttonCertsToolbarNew.setEnabled(ca.getPublicKey() != null && ca.getCertificate() != null); + buttonCAToolbarCRL.setEnabled(ca.getPublicKey() != null && ca.getCertificate() != null); + } + + /** + * EFFECTS: Reset all GUI to initial state and render the CA again. + * MODIFIES: this + */ + private void renderRefresh() { + acceptUnsaved(false, Observer.DIRECTION_CHANGE, Observer.INDEX_NOT_IN_LIST); + onChangeTab(null); + renderCAPage(); + + modelAuditLogs.fireTableDataChanged(); + setContentVisible(panelLogs, !ca.getLogs().isEmpty()); + + modelCerts.fireTableDataChanged(); + setContentVisible(panelCertsTab, !ca.getSigned().isEmpty()); + + modelTemplates.fireTableDataChanged(); + setContentVisible(panelTmpTab, !ca.getTemplates().isEmpty()); + } + + // -----END RENDERERS----- + + // -----BEGIN ACTION LISTENERS----- + + /** + * EFFECTS: Switch toolbar according to tab change. + * MODIFIES: this + */ + private void onChangeTab(ChangeEvent ev) { + final CardLayout toolbarCardLayout = (CardLayout) panelContextAwareToolbar.getLayout(); + switch (tabbedPane.getSelectedIndex()) { + case 0: { + toolbarCerts.setEnabled(false); + toolbarTemplates.setEnabled(false); + toolbarCardLayout.show(panelContextAwareToolbar, "CardToolbarCA"); + break; + } + case 1: { + toolbarCerts.setEnabled(true); + toolbarTemplates.setEnabled(false); + toolbarCardLayout.show(panelContextAwareToolbar, "CardToolbarCerts"); + break; + } + case 2: { + toolbarCerts.setEnabled(false); + toolbarTemplates.setEnabled(true); + toolbarCardLayout.show(panelContextAwareToolbar, "CardToolbarTemplates"); + break; + } + } + } + + /** + * EFFECTS: Generate private key. + * MODIFIES: this + * REQUIRES: No private key / CA is installed. + */ + private void onGeneratePrivateKey(ActionEvent ev) { + try { + ca.generateKey(); + unsaved.set(true); + } catch (NoSuchAlgorithmException e) { + alert(rootPanel, "Generate private key", e); + } finally { + renderCAPage(); + } + } + + /** + * EFFECTS: Sign a CSR and save to disk (in binary form). + * MODIFIES: this + * REQUIRES: Proper private key installed, no CA. + */ + private void onSignCSR(ActionEvent ev) { + final Path p = chooseFile(rootPanel, "DER binary (*.csr)", "csr"); + if (p == null) { + return; + } + + try { + final OutputStream fd = Files.newOutputStream(p, + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.CREATE); + final CertificationRequest csr = ca.signCSR(); + fd.write(Utils.byteToByte(csr.encodeDER())); + fd.close(); + unsaved.set(true); + } catch (IOException | ParseException | NoSuchAlgorithmException | SignatureException | InvalidKeyException e) { + alert(rootPanel, "Sign certification request", e); + } + } + + /** + * EFFECTS: Pick a certificate and install. + * MODIFIES: this + * REQUIRES: Proper private key is installed, no CA. + */ + private void onInstallCA(ActionEvent ev) { + final Path p = chooseFile(rootPanel, "DER X.509 certificate (*.crt, *.pem)", "crt", "pem"); + if (p == null) { + return; + } + + try { + final Certificate crt = new Certificate(new BytesReader(UIUtils.openDERorPEM(p, "CERTIFICATE")), + false); + ca.installCertificate(crt); + unsaved.set(true); + } catch (IOException | ParseException | InvalidCAException e) { + alert(rootPanel, "Install CA", e); + } finally { + renderCAPage(); + } + } + + /** + * EFFECTS: Load the database and refresh. + * MODIFIES: this + */ + private void onLoad(ActionEvent ev) { + if (unsaved.get()) { + alert(rootPanel, "Load database from filesystem", + "Unable to load: current modifications are not saved."); + return; + } + + try { + setCA(Decoder.decodeCA(FS.read(PATH_DEFAULT))); + } catch (NoSuchAlgorithmException | InvalidDBException e) { + alert(rootPanel, "Load database from filesystem", e); + } + } + + /** + * EFFECTS: Save database. + * MODIFIES: this + */ + private void onSave(ActionEvent ev) { + try { + FS.write(PATH_DEFAULT, Decoder.encodeCA(this.ca)); + unsaved.set(false); + } catch (IOException e) { + alert(rootPanel, "Save database to filesystem", e); + } + } + + /** + * EFFECTS: Enable a template. + * MODIFIES: this + */ + private void onEnableTemplate(ActionEvent ev) { + if (tableTemplates.getSelectedRow() == -1) { + return; + } + + final Template t = ca.getTemplates().get(tableTemplates.getSelectedRow()); + if (t.isEnabled()) { + return; + } + ca.setTemplateEnable(t, true); + unsaved.set(true); + } + + /** + * EFFECTS: Disable a template. + * MODIFIES: this + */ + private void onDisableTemplate(ActionEvent ev) { + if (tableTemplates.getSelectedRow() == -1) { + return; + } + + final Template t = ca.getTemplates().get(tableTemplates.getSelectedRow()); + if (!t.isEnabled()) { + return; + } + ca.setTemplateEnable(t, false); + unsaved.set(true); + } + + /** + * EFFECTS: Delete a template. + * MODIFIES: this + */ + private void onDeleteTemplate(ActionEvent ev) { + if (tableTemplates.getSelectedRow() == -1) { + return; + } + + final Template t = ca.getTemplates().get(tableTemplates.getSelectedRow()); + ca.removeTemplate(t); + unsaved.set(true); + } + + /** + * EFFECTS: Save the selected cert as a DER binary. + * MODIFIES: this + */ + private void onExportCert(ActionEvent ev) { + if (tableCerts.getSelectedRow() == -1) { + return; + } + + final Certificate c = ca.getSigned().get(tableCerts.getSelectedRow()); + final Path p = chooseFile(rootPanel, "DER binary (*.crt)", "crt"); + if (p == null) { + return; + } + try { + final OutputStream fd = Files.newOutputStream(p, + StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + fd.write(Utils.byteToByte(c.encodeDER())); + fd.close(); + } catch (IOException e) { + alert(rootPanel, "Export certificate", e); + } + } + + /** + * EFFECTS: Show revocation dialog for the selected cert. Save enforced. + * MODIFIES: this + */ + private void onRevokeCert(ActionEvent ev) { + if (tableCerts.getSelectedRow() == -1) { + return; + } + + final Certificate c = ca.getSigned().get(tableCerts.getSelectedRow()); + if (ca.getRevoked().stream().anyMatch(r -> + r.getSerialNumber().getLong() == c.getCertificate().getSerialNumber().getLong())) { + return; + } + final RevokeDialog diag = new RevokeDialog(c); + diag.pack(); + diag.setLocationRelativeTo(this); + diag.setVisible(true); + + final RevokedCertificate res = diag.getRes(); + if (res == null) { + return; + } + + ca.revoke(res); + onSave(null); + } + + /** + * EFFECTS: Sign a CRL and save to disk. Save enforced. + * MODIFIES: this + * REQUIRES: Proper private key and CA cert installed. + */ + private void onCRL(ActionEvent ev) { + final Path p = chooseFile(rootPanel, "DER CRL (*.crl)", "crl"); + if (p == null) { + return; + } + try { + final OutputStream fd = Files.newOutputStream(p, + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.CREATE); + final CertificateList crl = ca.signCRL(); + fd.write(Utils.byteToByte(crl.encodeDER())); + fd.close(); + onSave(null); + } catch (IOException | SignatureException | InvalidKeyException | NoSuchAlgorithmException e) { + alert(rootPanel, "Sign CRL", e); + } + } + + /** + * EFFECTS: Pick a CSR, show issue dialog, sign cert. Save enforced. + * MODIFIES: this + * REQUIRES: Proper private key / CA is installed. + */ + private void onIssue(ActionEvent ev) { + if (ca.getTemplates().stream().noneMatch(Template::isEnabled)) { + alert(rootPanel, "Issue new certificate", "No enabled templates."); + return; + } + final Path p = chooseFile(rootPanel, "DER CSR (*.csr, *.pem)", "csr", "pem"); + if (p == null) { + return; + } + try { + CertificationRequest csr = new CertificationRequest(new BytesReader(UIUtils.openDERorPEM(p, + "CERTIFICATE REQUEST")), false); + final IssueDialog diag = new IssueDialog(csr, ca.getTemplates()); + diag.setLocationRelativeTo(this); + diag.setVisible(true); + final Template res = diag.getRes(); + if (res == null) { + return; + } + ca.signCert(csr.getCertificationRequestInfo(), res); + onSave(null); + } catch (IOException | ParseException | NoSuchAlgorithmException | SignatureException | InvalidKeyException e) { + alert(rootPanel, "Issue new certificate", e); + } + } + + /** + * EFFECTS: Show the new template dialog and add a template. + * MODIFIES: this + */ + private void onNewTemplate(ActionEvent ev) { + final TemplateEditDialog diag = new TemplateEditDialog(temp -> + ca.getTemplates().stream().anyMatch(t -> t.getName().equals(temp))); + diag.pack(); + diag.setLocationRelativeTo(this); + diag.setVisible(true); + final Template res = diag.getRes(); + if (res == null) { + return; + } + ca.addTemplate(res); + unsaved.set(true); + } +} -- cgit v1.2.3