diff --git a/README.md b/README.md index 7b45623..b8812ec 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# fortGnox ![Logo](https://raw.githubusercontent.com/KneeDeepInMud/fortGnox/master/src/main/resources/org/mockenhaupt/fortgnox/fortGnox48.png "fortGnox Logo") +# fortGnox ![Logo](https://raw.githubusercontent.com/KneeDeepInMud/fortGnox/master/src/main/resources/org/mockenhaupt/fortgnox/fortGnox48.png "fortGnox Logo") Java based password manager front-end on top of GPG @@ -10,7 +10,7 @@ Java based password manager front-end on top of GPG __fortGnox__ is a GUI password manager written in Java. It uses [GnuPG](https://gnupg.org/) as backend for all encryption related tasks. -![Screenshot](https://raw.githubusercontent.com/KneeDeepInMud/fortGnox/master/resources/fortGnox_Screenshot.png "fortGnox screenshot") +![Screenshot](https://raw.githubusercontent.com/KneeDeepInMud/fortGnox/master/resources/fortGnox_Screenshot.png "fortGnox screenshot") Motivation was the long year usage of command line password manager [pass](https://www.passwordstore.org/) which can be used in friendly coexistence together with __fortGnox__ on the same password store. @@ -35,7 +35,7 @@ Although __fortGnox__ was mainly developed under Linux, it is already used by pe ### Prerequisites - Install Java (e.g. https://jdk.java.net/archive/, https://learn.microsoft.com/de-de/java/openjdk/download) - Install GPG - - Windows - https://www.gpg4win.de/thanks-for-download.html + - Windows - https://www.gpg4win.de/thanks-for-download.html - *ux - use distribution dependent installer (yum, apt ...) - If not already done, create a GPG key pair: @@ -44,20 +44,20 @@ Although __fortGnox__ was mainly developed under Linux, it is already used by pe and follow the instructions. - Download __fortGnox__ - + - Either download the latest JAR file from https://github.com/KneeDeepInMud/fortGnox/releases or download and compile the code. - To start __fortGnox__ basically issue the command - java -jar fortgnox-0.0.1.jar + java -jar fortgnox-v1.0.5.jar - - If JAR files are not directly started on Windows when double clicked in the Windows-explorer, create a shortcut to `java.exe`, edit the shortcut target in the shortcut properies and append the argument `-jar fortgnox-0.0.1.jar` + - If JAR files are not directly started on Windows when double clicked in the Windows-explorer, create a shortcut to `java.exe`, edit the shortcut target in the shortcut properies and append the argument `-jar fortgnox-v1.0.5.jar` - Edit configuration in __fortGnox__ - After first start open the `Settings` dialog and select `GPG` tab. - Enter the path to the folder where you want to store the encrypted passwords files in field `Data directories` - Enter **your own** public GPG key in field `Default recipient`. The public keys can be listed via command: - + gpg -k --keyid-format short diff --git a/pom.xml b/pom.xml index a6fe473..ea74e96 100644 --- a/pom.xml +++ b/pom.xml @@ -1,20 +1,26 @@ - + 4.0.0 org.mockenhaupt fortgnox fortGnox - 1.0.2 + 1.1.0 jar scm:git:file://. + + com.formdev + flatlaf + 3.1.1 + org.apache.maven.plugins maven-jar-plugin - 3.2.2 + 3.3.0 junit @@ -25,18 +31,13 @@ org.junit.jupiter junit-jupiter - 5.8.2 + 5.9.3 test com.fasterxml.jackson.dataformat jackson-dataformat-yaml - 2.13.2 - - - com.fasterxml.jackson.core - jackson-databind - 2.13.2.2 + 2.15.1 @@ -53,7 +54,7 @@ de.perdian.maven.plugins macosappbundler-maven-plugin - 1.18.0 + 1.19.0 @@ -104,7 +105,7 @@ org.codehaus.mojo buildnumber-maven-plugin - 1.4 + 3.1.0 validate @@ -159,9 +160,11 @@ org.apache.maven.plugins maven-assembly-plugin + 3.6.0 package + make-assembly single @@ -179,13 +182,18 @@ + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.11.0 8 8 @@ -195,7 +203,7 @@ org.codehaus.mojo exec-maven-plugin - 1.1.1 + 3.1.0 some-execution @@ -215,7 +223,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.2 + 3.1.0 diff --git a/resources/fortGnox.icns b/resources/fortGnox.icns index 27420d9..da54cb6 100644 Binary files a/resources/fortGnox.icns and b/resources/fortGnox.icns differ diff --git a/src/main/java/org/mockenhaupt/fortgnox/EditWindow.java b/src/main/java/org/mockenhaupt/fortgnox/EditWindow.java index 05d457d..d0307c9 100644 --- a/src/main/java/org/mockenhaupt/fortgnox/EditWindow.java +++ b/src/main/java/org/mockenhaupt/fortgnox/EditWindow.java @@ -5,26 +5,7 @@ import org.mockenhaupt.fortgnox.swing.LAFChooser; import org.mockenhaupt.fortgnox.tags.TagsStore; -import javax.swing.AbstractAction; -import javax.swing.BorderFactory; -import javax.swing.DefaultComboBoxModel; -import javax.swing.GroupLayout; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JComponent; -import javax.swing.JDialog; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; -import javax.swing.JTextField; -import javax.swing.KeyStroke; -import javax.swing.SwingConstants; -import javax.swing.SwingUtilities; -import javax.swing.WindowConstants; +import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.UndoableEditEvent; @@ -34,21 +15,12 @@ import javax.swing.undo.CannotRedoException; import javax.swing.undo.CannotUndoException; import javax.swing.undo.UndoManager; -import java.awt.BorderLayout; -import java.awt.Container; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.MouseInfo; -import java.awt.Point; +import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStreamReader; +import java.io.*; import java.net.URL; import java.time.ZonedDateTime; import java.util.ArrayList; @@ -56,17 +28,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import static javax.swing.JOptionPane.OK_CANCEL_OPTION; -import static javax.swing.JOptionPane.OK_OPTION; -import static javax.swing.JOptionPane.WARNING_MESSAGE; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_ADD_CHANGED_DATE_TIME; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPG_DEFAULT_RID; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPG_POST_COMMAND; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPG_USE_ASCII; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_LOOK_AND_FEEL; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_NEW_TEMPLATE; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_SECRETDIRS; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_TEXTAREA_FONT_SIZE; +import static javax.swing.JOptionPane.*; +import static org.mockenhaupt.fortgnox.FgPreferences.*; public class EditWindow implements FgGPGProcess.EncrypionListener, PropertyChangeListener, @@ -162,7 +125,8 @@ public void setText (String text, String status, String filename) return; } - this.tagsEditPanel.setText(TagsStore.getTagsOfFile(filename, true)); + this.tagsText = TagsStore.getTagsOfFile(filename, true); + this.tagsEditPanel.setText(tagsText); textArea.setText(text); setSecretTextModified(false); @@ -184,8 +148,13 @@ public boolean isSecretTextModified () public void setSecretTextModified (boolean secretTextModified) { - this.saveButton.setEnabled(secretTextModified); this.secretTextModified = secretTextModified; + this.saveButton.setEnabled(isModified()); + } + + public boolean isModified () + { + return this.tagsModified || this.secretTextModified; } public boolean isTagsModified () @@ -196,11 +165,7 @@ public boolean isTagsModified () public void setTagsModified (boolean tagsModified) { this.tagsModified = tagsModified; - if (!tagsModified) - { - tagsText = ""; - } - this.saveButton.setEnabled(tagsModified); + this.saveButton.setEnabled(isModified()); } public Container getTextArea () @@ -292,21 +257,20 @@ private void update () groupLayout.setAutoCreateContainerGaps(true); // horizontal - GroupLayout gl = groupLayout; - gl.setHorizontalGroup(gl.createParallelGroup() - .addGroup(gl.createSequentialGroup() - .addGroup(gl.createParallelGroup() + groupLayout.setHorizontalGroup(groupLayout.createParallelGroup() + .addGroup(groupLayout.createSequentialGroup() + .addGroup(groupLayout.createParallelGroup() .addComponent(dirNameLabel) .addComponent(fileNameLabel)) - .addGroup(gl.createParallelGroup() + .addGroup(groupLayout.createParallelGroup() .addComponent(comboBoxDirectories) .addComponent(fileNameText))) .addComponent(fileNameResulting)); // vertical - gl.setVerticalGroup(gl.createSequentialGroup() - .addGroup(gl.createParallelGroup().addComponent(dirNameLabel).addComponent(comboBoxDirectories)) - .addGroup(gl.createParallelGroup().addComponent(fileNameLabel).addComponent(fileNameText)) + groupLayout.setVerticalGroup(groupLayout.createSequentialGroup() + .addGroup(groupLayout.createParallelGroup().addComponent(dirNameLabel).addComponent(comboBoxDirectories)) + .addGroup(groupLayout.createParallelGroup().addComponent(fileNameLabel).addComponent(fileNameText)) .addComponent(fileNameResulting)); JButton buttonCreate; @@ -384,7 +348,7 @@ private void init (JFrame parent) if (editPanel == null) { editPanel = new JPanel(); - URL url = this.getClass().getResource("fortGnox.png"); +// URL url = this.getClass().getResource("fortGnox.png"); textArea = new JTextArea(); Document doc = textArea.getDocument(); @@ -498,6 +462,7 @@ private String getNewFileTemplateText (String newFileName) throws IOException if (fname == null || fname.isEmpty()) { URL url = this.getClass().getResource("/org/mockenhaupt/fortgnox/template.txt"); + if (url == null) return ""; br = new BufferedReader(new InputStreamReader(url.openStream())); } else @@ -620,6 +585,11 @@ private Container commandToolbar () textFieldFilename = new JTextField(); textFieldFilename.setEnabled(false); + textFieldFilename.setVisible(false); + + JButton pbInsertTemplate = new JButton("Insert Template"); + pbInsertTemplate.setToolTipText("Insert template for password entry at cursor position"); + pbInsertTemplate.addActionListener((e) -> insertTemplateAtCaret()); textFieldRID = new JTextField(); // textFieldRID.setMinimumSize(new Dimension(200, 30)); @@ -635,6 +605,7 @@ private Container commandToolbar () .addComponent(saveButton) .addComponent(comboBoxDirectories, 100, 200, 300) .addComponent(textFieldFilename, 100, 100, 300) + .addComponent(pbInsertTemplate, 100, 100, 300) .addComponent(labelRID) .addComponent(textFieldRID, 20, 100, 200) .addComponent(cbSkipPost) @@ -648,6 +619,7 @@ private Container commandToolbar () .addComponent(saveButton) .addComponent(comboBoxDirectories) .addComponent(textFieldFilename) + .addComponent(pbInsertTemplate) .addComponent(labelRID) .addComponent(textFieldRID) .addComponent(cbSkipPost) @@ -662,7 +634,7 @@ private Container commandToolbar () private void cancelEditing () { - if (!secretTextModified || OK_OPTION == JOptionPane.showConfirmDialog(parentWindow, + if (!isModified() || OK_OPTION == JOptionPane.showConfirmDialog(parentWindow, "File is modified, close discarding changes?", "fortGnox Close Confirmation", OK_CANCEL_OPTION)) { @@ -711,19 +683,13 @@ private String getRecipient (File gpgFile) private void doEncrypt () { -// File file = new File(textFieldFilename.getText()); -// if (!file.exists()) -// { -// JOptionPane.showMessageDialog(editWindow, "file does not exist", "fortgnox WARNING", WARNING_MESSAGE); -// return; -// } - + boolean needTriggerPost = false; if (isTagsModified() && textFieldFilename.getText() != null) { try { TagsStore.saveTagsForFile(textFieldFilename.getText(), tagsText); - executePostCommand(); + needTriggerPost = true; } catch (IOException e) { @@ -731,22 +697,25 @@ private void doEncrypt () } } - if (!isSecretTextModified()) { -// cancelEditing(); - return; - } + if (isSecretTextModified()) { + String rid = textFieldRID.getText(); - String rid = textFieldRID.getText(); + if (rid == null || rid.isEmpty()) + { + JOptionPane.showMessageDialog(parentWindow, "Cannot determine recipient", "fortGnox WARNING", WARNING_MESSAGE); + return; + } - if (rid == null || rid.isEmpty()) - { - JOptionPane.showMessageDialog(parentWindow, "Cannot determine recipient", "fortGnox WARNING", WARNING_MESSAGE); - return; + this.recipientId = rid; + String textToEncrypt = insertChangeTag(textArea.getText()); + fgGPGProcess.encrypt(textFieldFilename.getText(), textToEncrypt, rid, EditWindow.this); + needTriggerPost = false; } - this.recipientId = rid; - String textToEncrypt = insertChangeTag( textArea.getText()); - fgGPGProcess.encrypt(textFieldFilename.getText(), textToEncrypt, rid, EditWindow.this); + if (needTriggerPost) + { + executePostCommand(); + } } @@ -767,7 +736,6 @@ protected static String insertChangeTag (String text) if (minner.matches()) { String newText = m.group(1) + "$Changed: " + nowString + "$" + m.group(3); - System.out.println("newText = " + newText); return newText; } return text; @@ -842,6 +810,9 @@ public void handleGpgEncryptResult (String out, String err, String filename, Obj editHandler.handleFinished(); } } + + // refresh in any case, even if no new file has been added, don't hurt too much + fgGPGProcess.rebuildSecretList(); }); } @@ -899,4 +870,22 @@ public void handlePasswordInsert (String password) setStatusText("Failure inserting password, " + e.toString()); } } + + + private void insertTemplateAtCaret () + { + try + { + File file = new File(textFieldFilename.getText()); + String basename = file.getName(); + String templateText = getNewFileTemplateText(basename); + textArea.getDocument().insertString(textArea.getCaretPosition(), templateText, null); + textArea.requestFocus(); + } + catch (BadLocationException | IOException e) + { + setStatusText("Failure inserting password, " + e.toString()); + } + } + } diff --git a/src/main/java/org/mockenhaupt/fortgnox/FgGPGProcess.java b/src/main/java/org/mockenhaupt/fortgnox/FgGPGProcess.java index 06fdbfc..046a0af 100644 --- a/src/main/java/org/mockenhaupt/fortgnox/FgGPGProcess.java +++ b/src/main/java/org/mockenhaupt/fortgnox/FgGPGProcess.java @@ -989,7 +989,7 @@ public String getShortFileName (String filename, String info, boolean abbrev) private HashMap abbrevCompleteFileMap = new HashMap<>(); - private void rebuildSecretList () + public void rebuildSecretList () { fileMap.clear(); @@ -1158,7 +1158,7 @@ public List getSecretdirs () } @Override - public void handleDirContentChanged (String directory, String entry, WatchEvent.Kind kind) + public void handleDirContentChanged (String directory, String entry, ChangeEvent kind) { if (entry.toLowerCase().endsWith("gpg") || entry.toLowerCase().endsWith("asc") || entry.toLowerCase().endsWith("yml")) { diff --git a/src/main/java/org/mockenhaupt/fortgnox/FgPreferences.java b/src/main/java/org/mockenhaupt/fortgnox/FgPreferences.java index 4a675a3..70d81db 100644 --- a/src/main/java/org/mockenhaupt/fortgnox/FgPreferences.java +++ b/src/main/java/org/mockenhaupt/fortgnox/FgPreferences.java @@ -3,6 +3,7 @@ public class FgPreferences { public static final String PREFERENCE_NODE = "org.mockenhaupt.fortgnox"; + public static final String PREFERENCE_NODE_TEST = "org.mockenhaupt.fortgnoxJUNIT"; protected static final String[] PREFERENCE_NODES_OLD = {"org.fmoc.fortgnox", "org.fmoc.jgpg"}; @@ -33,6 +34,8 @@ public class FgPreferences public static final String PREF_USE_SEARCH_TAGS = "use_search_tags"; public static final String PREF_SHOW_SEARCH_TAGS = "show_search_tags"; + public static final String PREF_TOC_GENERATION = "generate_table_of_contents"; + public static final String PREF_TOC_PREFIX = "toc_prefix"; public static final String PREF_FAVORITES = "favorites"; public static final String PREF_RESET_MASK_BUTTON_SECONDS = "reset_mask_button_seconds"; @@ -62,11 +65,30 @@ public class FgPreferences public static final String PREF_LOOK_AND_FEEL = "PREF_LOOK_AND_FEEL"; + public static final String PREF_EXCEPTION = "PREF_EXCEPTION"; + public static final String PREF_DIRECTORY_OBSERVER_INTERVAL = "PREF_DIRECTORY_OBSERVER_INTERVAL"; + public static PreferencesAccess get () { + if (isJUnitTest()) + { + return PreferencesAccess.getInstance(PREFERENCE_NODE_TEST); + } return PreferencesAccess.getInstance(PREFERENCE_NODE); } + public static boolean isJUnitTest () + { + for (StackTraceElement element : Thread.currentThread().getStackTrace()) + { + if (element.getClassName().startsWith("org.junit.")) + { + return true; + } + } + return false; + } + private FgPreferences() { } } diff --git a/src/main/java/org/mockenhaupt/fortgnox/MainFrame.java b/src/main/java/org/mockenhaupt/fortgnox/MainFrame.java index 487b76a..68bfe22 100644 --- a/src/main/java/org/mockenhaupt/fortgnox/MainFrame.java +++ b/src/main/java/org/mockenhaupt/fortgnox/MainFrame.java @@ -14,6 +14,7 @@ import org.apache.commons.io.FilenameUtils; import org.mockenhaupt.fortgnox.misc.FileUtils; import org.mockenhaupt.fortgnox.misc.History; +import org.mockenhaupt.fortgnox.misc.StringUtils; import org.mockenhaupt.fortgnox.swing.FgOptionsDialog; import org.mockenhaupt.fortgnox.swing.FgPanelTextArea; import org.mockenhaupt.fortgnox.swing.FgTextFilter; @@ -37,6 +38,7 @@ import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; +import java.util.prefs.Preferences; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -749,14 +751,17 @@ public void executePostCommand () JOptionPane.QUESTION_MESSAGE, null) == YES_OPTION) { + fgPanelTextArea.setStatusText("Executing post command '" + postCommand + "' after tags change"); gpgProcess.command(postCommand, "", this, (out, err, filename, clientData, exitCode) -> { if (exitCode != 0) { + fgPanelTextArea.setStatusText("Post command failure '" + out + "" + err + "'"); JOptionPane.showMessageDialog(MainFrame.this, "Output:" + out + " Error:" + err, "fortGnox POST", JOptionPane.ERROR_MESSAGE); } else { + fgPanelTextArea.setStatusText("Post command success '" + out + " " + err + "'"); JOptionPane.showMessageDialog(MainFrame.this, out + err, "fortGnox POST", JOptionPane.INFORMATION_MESSAGE); } }); @@ -777,9 +782,19 @@ public JPopupMenu getSecretsPopupMenu (boolean launchedFromEditor) boolean hasEntries = false; if (!launchedFromEditor) { + hasEntries = true; + + JMenuItem miRefreshList = new JMenuItem("Reload password list from disk (F5)"); + miRefreshList.addActionListener(actionEvent -> + { + gpgProcess.rebuildSecretList(); + }); + popupMenu.add(miRefreshList); + + if (favorites.containsKey(jList.getSelectedValue())) { - hasEntries = true; + popupMenu.add(new JSeparator()); JMenuItem miRemoveFavorites = new JMenuItem("Remove selected entry from favorites"); miRemoveFavorites.addActionListener(actionEvent -> { @@ -796,6 +811,10 @@ public JPopupMenu getSecretsPopupMenu (boolean launchedFromEditor) if (!editMode) compressFavorites(); }); popupMenu.add(miCompressFavs); + // -------- + JMenuItem miRemoveFavs = new JMenuItem("Remove all favorites"); + miRemoveFavs.addActionListener(getFortGnoxDeleteFavorites()); + popupMenu.add(miRemoveFavs); } @@ -805,6 +824,7 @@ public JPopupMenu getSecretsPopupMenu (boolean launchedFromEditor) boolean sortReverse = FgPreferences.get().getBoolean(PREF_SECRETDIR_SORTING); FgPreferences.get().put(PREF_SECRETDIR_SORTING, !sortReverse); }); + popupMenu.add(new JSeparator()); popupMenu.add(miToggleSort); } @@ -1038,9 +1058,28 @@ private void compressFavorites () private void handleForFavoritesList (String entry) { + final LinkedHashMap oldFavorites = new LinkedHashMap<>(favorites); + + Integer i = favorites.computeIfAbsent(entry, s -> 0); favorites.put(entry, ++i); + if (favoritesAsJson().length() >= Preferences.MAX_VALUE_LENGTH) + { +// if (JOptionPane.showConfirmDialog(MainFrame.this, +// "Favorites exceeded max storable size, compress favorites?\n" + +// "Chosing cancel will ignore current selection for favorites", +// "Compact Favorites", OK_CANCEL_OPTION) == OK_OPTION) +// { + compressFavorites(); +// } +// else +// { +// favorites.clear(); +// favorites.putAll(oldFavorites); +// return; +// } + } dbg(FAV, entry + " weight:" + i); @@ -1088,17 +1127,22 @@ private boolean filterFile (String fileName) Matcher m = pattern.matcher(fileName); if (m.matches()) { - if (prefUseSearchTags && TagsStore.matchesTag(fileName, fgTextFilter.getText())) - { - return true; - } + String needle = fgTextFilter.getText().trim(); + + // all tags concatenated w/o blanks + String tags = TagsStore.getTagsOfFile(fileName, true); + tags = tags.replaceAll("\\s", ""); + String baseName = m.group(1); - String filter = fgTextFilter.getText(); String name2 = baseName.toLowerCase().replace(".asc", ""); + + // simply concat tags and filename and look in all name2 = name2.toLowerCase().replace(".gpg", ""); - boolean ret = name2.contains(filter.toLowerCase()); - dbg(FILTER, fgTextFilter.getText() + (ret ? " match " : " nomatch ") + fileName); + name2 += tags; + boolean ret = StringUtils.andMatcher(name2, needle); + dbg(FILTER, needle + (ret ? " match " : " nomatch ") + fileName); return ret; + } else { @@ -1390,6 +1434,19 @@ private void handleListSelection () private void initComponents () { // ToolTipManager.sharedInstance().setInitialDelay(100); + KeyboardFocusManager.getCurrentKeyboardFocusManager() + .addKeyEventDispatcher(new KeyEventDispatcher() + { + @Override + public boolean dispatchKeyEvent(KeyEvent e) + { + if (e.getKeyCode() == KeyEvent.VK_F5 && e.getID() == KeyEvent.KEY_RELEASED) + { + gpgProcess.rebuildSecretList(); + } + return false; + } + }); jSplitPaneLR = new JSplitPane(); JPanel panelList = new JPanel(); @@ -1604,19 +1661,8 @@ public void actionPerformed (java.awt.event.ActionEvent evt) buttonClearFavorites.setFocusable(false); buttonClearFavorites.setHorizontalTextPosition(SwingConstants.CENTER); buttonClearFavorites.setVerticalTextPosition(SwingConstants.BOTTOM); - buttonClearFavorites.addActionListener(new java.awt.event.ActionListener() - { - public void actionPerformed (java.awt.event.ActionEvent evt) - { - if (OK_OPTION == JOptionPane.showConfirmDialog(MainFrame.this, "Really delete all favorites?", "fortGnox Delete Favorites", OK_CANCEL_OPTION)) - { - favorites.clear(); - FgPreferences.get().put(PREF_FAVORITES, favoritesAsJson()); - refreshFavorites(); - } - } - }); - jToolBarMainFunctions.add(buttonClearFavorites); + buttonClearFavorites.addActionListener(getFortGnoxDeleteFavorites()); +// XXXXX #30 jToolBarMainFunctions.add(buttonClearFavorites); // --------------------------------- @@ -1720,6 +1766,21 @@ public void actionPerformed (java.awt.event.ActionEvent evt) pack(); } + private ActionListener getFortGnoxDeleteFavorites () + { + return new ActionListener() + { + public void actionPerformed (ActionEvent evt) + { + if (OK_OPTION == JOptionPane.showConfirmDialog(MainFrame.this, "Really delete all favorites?", "fortGnox Delete Favorites", OK_CANCEL_OPTION)) + { + favorites.clear(); + FgPreferences.get().put(PREF_FAVORITES, favoritesAsJson()); + refreshFavorites(); + } + } + }; + } private void buttonExitActionPerformed (java.awt.event.ActionEvent evt) @@ -1925,6 +1986,18 @@ public void propertyChange (PropertyChangeEvent propertyChangeEvent) initSecretListFont(); } break; + case PREF_EXCEPTION: + String msg = propertyChangeEvent.getNewValue().toString(); + int len = msg.length(); + if (msg.length() > 100) { + msg = msg.substring(0, 100); + msg = String.format("Failed to store propery of length '%d', %s ....", len, msg); + } + + JOptionPane.showMessageDialog(MainFrame.this, + msg, propertyChangeEvent.getOldValue().toString(), + JOptionPane.ERROR_MESSAGE); + default: // nothing to do } diff --git a/src/main/java/org/mockenhaupt/fortgnox/PasswordGenerator.java b/src/main/java/org/mockenhaupt/fortgnox/PasswordGenerator.java index e52ce98..ea73fbb 100644 --- a/src/main/java/org/mockenhaupt/fortgnox/PasswordGenerator.java +++ b/src/main/java/org/mockenhaupt/fortgnox/PasswordGenerator.java @@ -4,16 +4,7 @@ import org.mockenhaupt.fortgnox.swing.JCheckBoxPersistent; import org.mockenhaupt.fortgnox.swing.LAFChooser; -import javax.swing.DefaultComboBoxModel; -import javax.swing.GroupLayout; -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JDialog; -import javax.swing.JFormattedTextField; -import javax.swing.JFrame; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.WindowConstants; +import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import java.awt.Dimension; @@ -22,19 +13,22 @@ import java.beans.PropertyChangeListener; import java.security.SecureRandom; import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; +import java.util.HashMap; import java.util.List; import java.util.Random; -import java.util.Set; +import java.util.function.Consumer; import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPG_PASS_CHARPOOL_DIGIT; import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPG_PASS_CHARPOOL_LOWER; import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPG_PASS_CHARPOOL_SPECIAL; import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPG_PASS_CHARPOOL_UPPER; + public class PasswordGenerator implements PropertyChangeListener { + public static int MAX_PASSWORD_LENGTH = 999; + public static int MIN_PASSWORD_LENGTH = 4; + private JDialog generatorWindow; private final JFrame parent; private List digits = new ArrayList<>(); @@ -43,7 +37,7 @@ public class PasswordGenerator implements PropertyChangeListener private List special = new ArrayList<>(); private final JComboBox comboBoxPasswords = new JComboBox<>(); - private final JFormattedTextField textFieldLength = new JFormattedTextField(); + JSpinner jSpinnerPasswordLength = new JSpinner(new SpinnerNumberModel(12, MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH, 1)); private final JCheckBoxPersistent cbUpper = new JCheckBoxPersistent(FgPreferences.PREF_GPG_PASS_UPPER, "Uppercase", () -> handleEnabled()); private final JCheckBoxPersistent cbLower = new JCheckBoxPersistent(FgPreferences.PREF_GPG_PASS_LOWER, "Lowercase", () -> handleEnabled()); private final JCheckBoxPersistent cbDigit = new JCheckBoxPersistent(FgPreferences.PREF_GPG_PASS_DIGITS, "Digits", () -> handleEnabled()); @@ -119,35 +113,29 @@ public JPanel getGeneratorPanel () cbSpecial.setMnemonic('e'); buttonCopy.setMnemonic('l'); - textFieldLength.setMinimumSize(new Dimension(40, 10)); - textFieldLength.setValue(FgPreferences.get().getPreference(FgPreferences.PREF_GPG_PASS_LENGTH, 18)); - textFieldLength.setFormatterFactory(new javax.swing.text.DefaultFormatterFactory(new javax.swing.text.NumberFormatter(new java.text.DecimalFormat("#0")))); - textFieldLength.getDocument().addDocumentListener(new DocumentListener() - { - void updatePreference () - { - FgPreferences.get().putPreference(FgPreferences.PREF_GPG_PASS_LENGTH, textFieldLength.getText()); - } - - @Override - public void insertUpdate (DocumentEvent documentEvent) - { - updatePreference(); - } + jSpinnerPasswordLength.getModel().setValue(FgPreferences.get().getPreference(FgPreferences.PREF_GPG_PASS_LENGTH, 18)); + jSpinnerPasswordLength.setEditor(new JSpinner.NumberEditor(jSpinnerPasswordLength,"#")); - @Override - public void removeUpdate (DocumentEvent documentEvent) + jSpinnerPasswordLength.addMouseWheelListener(e -> { + int sign = e.getWheelRotation(); + Integer val = (Integer) jSpinnerPasswordLength.getValue(); + int newVal = normalizePasswordLength(val - sign); + if (newVal != val) { - updatePreference(); + jSpinnerPasswordLength.setValue(newVal); } - - @Override - public void changedUpdate (DocumentEvent documentEvent) + }); + jSpinnerPasswordLength.getModel().addChangeListener(e -> { + Integer val = (Integer) jSpinnerPasswordLength.getValue(); + int newVal = normalizePasswordLength(val); + if (newVal != val) { - updatePreference(); + jSpinnerPasswordLength.setValue(newVal); } + FgPreferences.get().putPreference(FgPreferences.PREF_GPG_PASS_LENGTH, newVal); }); + handleEnabled(); gl.setHorizontalGroup( gl.createParallelGroup(GroupLayout.Alignment.TRAILING) @@ -157,7 +145,7 @@ public void changedUpdate (DocumentEvent documentEvent) .addComponent(cbDigit) .addComponent(cbSpecial) .addGap(10) - .addComponent(textFieldLength, 20, 40, 60) + .addComponent(jSpinnerPasswordLength, 60, 70, 80) .addComponent(buttonGenerate) ) .addGroup(gl.createSequentialGroup() @@ -176,7 +164,7 @@ public void changedUpdate (DocumentEvent documentEvent) .addComponent(cbLower) .addComponent(cbDigit) .addComponent(cbSpecial) - .addComponent(textFieldLength) + .addComponent(jSpinnerPasswordLength) .addComponent(buttonGenerate) ) .addGroup(gl.createParallelGroup() @@ -258,48 +246,83 @@ private void initDialog () } - private void generatePassword () + public void generatePassword (int len, + boolean digit, + boolean upper, + boolean lower, + boolean useSpecial, + Consumer handleError, + Consumer handlePass) { - Long len = Long.parseLong(textFieldLength.getText()); - - len = Math.abs(len); - len = Math.min(len, 256); - len = Math.max(len, 4); - - textFieldLength.setValue(len); + if (len < MIN_PASSWORD_LENGTH || len > MAX_PASSWORD_LENGTH) { + handleError.accept("Password length not supported"); + return; + } StringBuilder sb = new StringBuilder(); Random rnd = new SecureRandom(); List> pool = new ArrayList<>(); - if (cbDigit.isSelected()) pool.add(digits); - if (cbUpper.isSelected()) pool.add(uppercase); - if (cbLower.isSelected()) pool.add(lowercase); - if (cbSpecial.isSelected()) pool.add(special); + if (digit) pool.add(digits); + if (upper) pool.add(uppercase); + if (lower) pool.add(lowercase); + if (useSpecial) pool.add(special); if (pool.size() == 0) { return; } - while (sb.length() < len) + int poolIx = 0; + int poolMod = pool.size(); + HashMap passMap = new HashMap<>(); + while (passMap.size() < len) { - int poolIx = Math.abs(rnd.nextInt()) % pool.size(); - List cSet = pool.get(poolIx); - if (cSet.size() <= 0) - { - JOptionPane.showMessageDialog(parent, "Empty character set in preferences", "fortGnox WARNING", JOptionPane.ERROR_MESSAGE); - break; - } - else + List curPool = pool.get(poolIx); + + int curPoolIx = Math.abs(rnd.nextInt()) % curPool.size(); + Character passChar = curPool.get(curPoolIx); + int insertPos = Math.abs(rnd.nextInt()) % len; + + while (passMap.get(insertPos) != null) { - int cSetIx = Math.abs(rnd.nextInt()) % cSet.size(); - sb.append(cSet.get(cSetIx)); + insertPos++; + insertPos %= len; } + passMap.put(insertPos, passChar); + + poolIx++; + poolIx %= poolMod; } - addPassword(sb.toString()); + passMap.keySet().stream().sorted().forEach(pos -> sb.append(passMap.get(pos))); + handlePass.accept(sb.toString()); + } + + private void generatePassword () + { + Integer len = (Integer)jSpinnerPasswordLength.getValue(); + + len = normalizePasswordLength(len); + + jSpinnerPasswordLength.setValue(len); + + generatePassword(len, + cbDigit.isSelected(), + cbUpper.isSelected(), + cbLower.isSelected(), + cbSpecial.isSelected(), + errorMsg -> JOptionPane.showMessageDialog(parent, errorMsg, "fortGnox WARNING", JOptionPane.ERROR_MESSAGE), + password -> addPassword(password)); + } + + private static Integer normalizePasswordLength (int len) + { + len = Math.abs(len); + len = Math.min(len, MAX_PASSWORD_LENGTH); + len = Math.max(len, MIN_PASSWORD_LENGTH); + return len; } private void addPassword (String pass) @@ -363,4 +386,23 @@ private void handleEnabled () } + public List getDigits () + { + return digits; + } + + public List getUppercase () + { + return uppercase; + } + + public List getLowercase () + { + return lowercase; + } + + public List getSpecial () + { + return special; + } } diff --git a/src/main/java/org/mockenhaupt/fortgnox/PreferencesAccess.java b/src/main/java/org/mockenhaupt/fortgnox/PreferencesAccess.java index 50da8cf..b98dc85 100644 --- a/src/main/java/org/mockenhaupt/fortgnox/PreferencesAccess.java +++ b/src/main/java/org/mockenhaupt/fortgnox/PreferencesAccess.java @@ -9,6 +9,7 @@ import static javax.swing.JOptionPane.YES_OPTION; import static org.mockenhaupt.fortgnox.FgPreferences.PREFERENCE_NODES_OLD; +import static org.mockenhaupt.fortgnox.FgPreferences.PREF_EXCEPTION; public class PreferencesAccess { @@ -76,6 +77,11 @@ public void addPropertyChangeListener (PropertyChangeListener listener) propertyChangeSupport.addPropertyChangeListener(listener); } + public void removePropertyChangeListener (PropertyChangeListener listener) + { + propertyChangeSupport.removePropertyChangeListener(listener); + } + private PropertyChangeEvent getEvent (String name, Object oldVal, Object newVal) { return new PropertyChangeEvent(INSTANCE, name, oldVal, newVal); @@ -156,56 +162,62 @@ public PreferencesAccess put (String name, T value) public PreferencesAccess putPreference (String name, T value) { + try { - if (value instanceof String) - { - String invalid = null; - String oldVal = INSTANCE.preferences.get(name, invalid); - - String svalue = ((String) value).trim(); - INSTANCE.preferences.put(name, svalue); - if (oldVal == null || !oldVal.equals(svalue)) + if (value instanceof String) { - fireEvent(name, null, svalue); - } - } - else if (value instanceof Integer) - { - Integer invalid = Integer.MIN_VALUE; - Integer oldVal = INSTANCE.preferences.getInt(name, invalid); + String invalid = null; + String oldVal = INSTANCE.preferences.get(name, invalid); - INSTANCE.preferences.putInt(name, (Integer) value); - if (!oldVal.equals(value)) - { - fireEvent(name, null, value); + String svalue = ((String) value).trim(); + INSTANCE.preferences.put(name, svalue); + if (oldVal == null || !oldVal.equals(svalue)) + { + fireEvent(name, null, svalue); + } } - } - else if (value instanceof Float) - { - Float invalid = Float.MIN_VALUE; - Float oldVal = INSTANCE.preferences.getFloat(name, invalid); + else if (value instanceof Integer) + { + Integer invalid = Integer.MIN_VALUE; + Integer oldVal = INSTANCE.preferences.getInt(name, invalid); - INSTANCE.preferences.putFloat(name, (Float) value); - if (!oldVal.equals(value)) + INSTANCE.preferences.putInt(name, (Integer) value); + if (!oldVal.equals(value)) + { + fireEvent(name, null, value); + } + } + else if (value instanceof Float) { - fireEvent(name, null, value); + Float invalid = Float.MIN_VALUE; + Float oldVal = INSTANCE.preferences.getFloat(name, invalid); + + INSTANCE.preferences.putFloat(name, (Float) value); + if (!oldVal.equals(value)) + { + fireEvent(name, null, value); + } } - } - else if (value instanceof Boolean) - { - Boolean invalid = false; - Boolean oldVal = INSTANCE.preferences.getBoolean(name, invalid); + else if (value instanceof Boolean) + { + Boolean invalid = false; + Boolean oldVal = INSTANCE.preferences.getBoolean(name, invalid); - INSTANCE.preferences.putBoolean(name, (Boolean) value); - if (!oldVal.equals(value)) + INSTANCE.preferences.putBoolean(name, (Boolean) value); + if (!oldVal.equals(value)) + { + fireEvent(name, null, value); + } + } + else { - fireEvent(name, null, value); + throw new IllegalArgumentException("unsupported preference type " + value.getClass()); } + } catch (Exception ex) { + String err = ex.getLocalizedMessage(); + fireEvent(PREF_EXCEPTION, "Storing '" + name + "' failed", err); } - else - { - throw new IllegalArgumentException("unsupported preference type " + value.getClass()); - } + return INSTANCE; } diff --git a/src/main/java/org/mockenhaupt/fortgnox/misc/DirectoryWatcher.java b/src/main/java/org/mockenhaupt/fortgnox/misc/DirectoryWatcher.java index bbe44ec..243e4a9 100644 --- a/src/main/java/org/mockenhaupt/fortgnox/misc/DirectoryWatcher.java +++ b/src/main/java/org/mockenhaupt/fortgnox/misc/DirectoryWatcher.java @@ -1,115 +1,101 @@ package org.mockenhaupt.fortgnox.misc; +import org.apache.commons.io.monitor.FileAlterationListenerAdaptor; +import org.apache.commons.io.monitor.FileAlterationMonitor; +import org.apache.commons.io.monitor.FileAlterationObserver; import org.mockenhaupt.fortgnox.DebugWindow; +import org.mockenhaupt.fortgnox.FgPreferences; import java.io.File; -import java.nio.file.FileSystems; -import java.nio.file.Path; -import java.nio.file.WatchEvent; -import java.nio.file.WatchKey; -import java.nio.file.WatchService; import java.util.Arrays; -import java.util.concurrent.atomic.AtomicBoolean; -import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; -import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; -import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; -import static java.nio.file.StandardWatchEventKinds.OVERFLOW; import static org.mockenhaupt.fortgnox.DebugWindow.Category.DIR; public class DirectoryWatcher { - public DirectoryWatcher (IDirectoryWatcherHandler handler) + IDirectoryWatcherHandler handler; + private String directory; + private FileAlterationMonitor monitor = null; + + public DirectoryWatcher(IDirectoryWatcherHandler handler) { this.handler = handler; } - IDirectoryWatcherHandler handler; - AtomicBoolean active = new AtomicBoolean(true); - private Thread thread; - private String directory; - - public void init (String directory) + public void init(String directory_) { - this.directory = directory; + int interval = FgPreferences.get().get(FgPreferences.PREF_DIRECTORY_OBSERVER_INTERVAL, -1); + if (interval <= 0) { + return; + } + interval = Math.max(5000, interval); + + this.directory = directory_; + File monitoredDirectory = new File(directory_); + FileAlterationObserver observer = new FileAlterationObserver(monitoredDirectory); + observer.addListener(getFileAlterationListener()); try { - Path dir = new File(directory).toPath(); - - WatchService watcher = FileSystems.getDefault().newWatchService(); - dir.register(watcher, - ENTRY_CREATE, - ENTRY_DELETE, - ENTRY_MODIFY); - startWatcherThread(watcher); + monitor = new FileAlterationMonitor(interval, observer); + monitor.start(); } catch (Exception e) { - dbg("Error in init: " + e.toString()); - Arrays.stream(e.getStackTrace()).sequential().forEach(stackTraceElement -> dbg(stackTraceElement.toString())); - e.printStackTrace(); + handleListenerException(e); } } - public void stop () + private void handleListenerException(Exception e) throws RuntimeException { - dbg("Stopping watcher thread " + directory); - if (thread != null) - { - thread.interrupt(); - active.set(false); - } + dbg("Error in init: " + e.toString()); + Arrays.stream(e.getStackTrace()).sequential().forEach(stackTraceElement -> dbg(stackTraceElement.toString())); + e.printStackTrace(); + throw new RuntimeException(e); } - private void dbg (String text) + private FileAlterationListenerAdaptor getFileAlterationListener() { - DebugWindow.get().debug(DIR, text + " (" + directory + ")"); + return new FileAlterationListenerAdaptor() + { + @Override + public void onFileChange(File file) + { + handler.handleDirContentChanged(directory, file.getName(), IDirectoryWatcherHandler.ChangeEvent.FILE_CHANGE); + } + + @Override + public void onFileCreate(File file) + { + handler.handleDirContentChanged(directory, file.getName(), IDirectoryWatcherHandler.ChangeEvent.FILE_NEW); + } + + @Override + public void onFileDelete(File file) + { + handler.handleDirContentChanged(directory, file.getName(), IDirectoryWatcherHandler.ChangeEvent.FILE_DELETE); + } + }; } - private void startWatcherThread (WatchService watcher) + public void stop() { - - thread = new Thread(() -> + dbg("Stopping watcher thread " + directory); + if (monitor != null) { - dbg("watcher thread: Starting thread loop"); - while (active.get()) + try { - try - { - WatchKey key = watcher.take(); - for (WatchEvent event : key.pollEvents()) - { - WatchEvent.Kind kind = event.kind(); - dbg("watcher thread: " + kind.name() + " " + event.context()); - if (kind == OVERFLOW) - { - continue; - } - - if (handler != null) - { - handler.handleDirContentChanged(directory, event.context().toString(), kind); - } - boolean valid = key.reset(); - if (!valid) - { - break; - } - } - } - catch (InterruptedException e) - { - dbg("watcher thread interrupted, stopping"); - active.set(false); - } + monitor.stop(); } - dbg("regularly terminated watcher "); - - }, "DIR-" + directory); - - dbg("Starting watcher thread " + thread); - thread.start(); + catch (Exception e) + { + handleListenerException(e); + } + } } + private void dbg(String text) + { + DebugWindow.get().debug(DIR, text + " (" + directory + ")"); + } } diff --git a/src/main/java/org/mockenhaupt/fortgnox/misc/IDirectoryWatcherHandler.java b/src/main/java/org/mockenhaupt/fortgnox/misc/IDirectoryWatcherHandler.java index 75d58aa..678e19e 100644 --- a/src/main/java/org/mockenhaupt/fortgnox/misc/IDirectoryWatcherHandler.java +++ b/src/main/java/org/mockenhaupt/fortgnox/misc/IDirectoryWatcherHandler.java @@ -4,5 +4,10 @@ public interface IDirectoryWatcherHandler { - void handleDirContentChanged (String directory, String entry, WatchEvent.Kind kind); + public enum ChangeEvent { + FILE_NEW, + FILE_DELETE, + FILE_CHANGE + } + void handleDirContentChanged (String directory, String entry, ChangeEvent kind); } diff --git a/src/main/java/org/mockenhaupt/fortgnox/misc/StringUtils.java b/src/main/java/org/mockenhaupt/fortgnox/misc/StringUtils.java new file mode 100644 index 0000000..be10511 --- /dev/null +++ b/src/main/java/org/mockenhaupt/fortgnox/misc/StringUtils.java @@ -0,0 +1,36 @@ +package org.mockenhaupt.fortgnox.misc; + +import java.util.Arrays; +import java.util.List; + +public class StringUtils { + public static boolean andMatcher (String haystack, List needles) + { + if (haystack == null || needles == null) + { + return false; + } + long c = needles.stream().filter(needle -> haystack.toLowerCase().contains(needle.toLowerCase())).count(); + + return c == needles.size(); + } + + public static boolean andMatcher (String haystack, String needles) + { + if (haystack == null || needles == null) + { + return false; + } + + return andMatcher(haystack, Arrays.asList(needles.split("\\s+"))); + } + + + public static String trimEnd(String toTrim) + { + if (toTrim == null ) return toTrim; + + String trimmed = toTrim.replaceAll("\\s+$", ""); + return trimmed; + } +} diff --git a/src/main/java/org/mockenhaupt/fortgnox/swing/FgOptionsDialog.java b/src/main/java/org/mockenhaupt/fortgnox/swing/FgOptionsDialog.java index d6e8409..88d439a 100644 --- a/src/main/java/org/mockenhaupt/fortgnox/swing/FgOptionsDialog.java +++ b/src/main/java/org/mockenhaupt/fortgnox/swing/FgOptionsDialog.java @@ -13,78 +13,18 @@ import org.mockenhaupt.fortgnox.PreferencesAccess; import org.mockenhaupt.fortgnox.misc.PasswordCharacterPool; -import javax.swing.DefaultComboBoxModel; -import javax.swing.DefaultListCellRenderer; -import javax.swing.GroupLayout; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JFormattedTextField; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTabbedPane; -import javax.swing.JTextField; -import javax.swing.SwingUtilities; -import javax.swing.UIManager; -import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.MouseInfo; -import java.awt.Point; +import javax.swing.*; +import javax.swing.border.LineBorder; +import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; -import java.util.ArrayList; import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; import java.util.stream.Collectors; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_ADD_CHANGED_DATE_TIME; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_CHARSET; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_CLEAR_SECONDS; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_CLIP_SECONDS; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_FAVORITES_MIN_HIT_COUNT; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_FAVORITES_SHOW_COUNT; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_FILTER_FAVORITES; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPGCONF_COMMAND; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPG_COMMAND; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPG_DEFAULT_RID; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPG_HOMEDIR; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPG_PASS_CHARPOOL_DIGIT; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPG_PASS_CHARPOOL_LOWER; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPG_PASS_CHARPOOL_SPECIAL; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPG_PASS_CHARPOOL_UPPER; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPG_POST_COMMAND; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPG_RID_FILE; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_GPG_USE_ASCII; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_HISTORY_SIZE; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_NEW_TEMPLATE; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_SECRETLIST_FONT_SIZE; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_LOOK_AND_FEEL; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_MASK_FIRST_LINE; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_NUMBER_FAVORITES; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_PASSWORD_MASK_PATTERNS; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_PASSWORD_SECONDS; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_RESET_MASK_BUTTON_SECONDS; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_SECRETDIRS; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_SHOW_PASSWORD_SHORTCUT_BAR; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_SHOW_SEARCH_TAGS; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_SHOW_TB_BUTTON_TEXT; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_TEXTAREA_FONT_SIZE; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_URL_OPEN_COMMAND; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_USERNAME_MASK_PATTERNS; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_USE_FAVORITES; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_USE_GPG_AGENT; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_USE_PASS_DIALOG; -import static org.mockenhaupt.fortgnox.FgPreferences.PREF_USE_SEARCH_TAGS; +import static org.mockenhaupt.fortgnox.FgPreferences.*; /** * @@ -114,6 +54,9 @@ public class FgOptionsDialog extends javax.swing.JDialog private JCheckBoxPersistent jCheckBoxShowFavoritesCount; private JCheckBoxPersistent jCheckBoxUseSearchTags; private JCheckBoxPersistent jCheckBoxShowSearchTags; + private JPanel jPanelTocGeneration; + private JCheckBoxPersistent jCheckBoxTocGeneration; + private JTextField jTextfieldTocPrefix; private JCheckBoxPersistent jCheckBoxAddChangedDateTime; private javax.swing.JCheckBox jCheckBoxShowDebugWindow; private javax.swing.JFormattedTextField jFormattedTextPassClearTimeout; @@ -259,6 +202,7 @@ void initPreferences () PreferencesAccess pa = FgPreferences.get(); this.textFieldCharsUpper.setText(pa.get(PREF_GPG_PASS_CHARPOOL_UPPER, PasswordCharacterPool.getUppercase())); + this.jTextfieldTocPrefix.setText(pa.get(PREF_TOC_PREFIX, PasswordCharacterPool.getUppercase())); this.textFieldCharsLower.setText(pa.get(PREF_GPG_PASS_CHARPOOL_LOWER, PasswordCharacterPool.getLowercase())); this.textFieldDigits.setText(pa.get(PREF_GPG_PASS_CHARPOOL_DIGIT, PasswordCharacterPool.getDigits())); this.textFieldSpecial.setText(pa.get(PREF_GPG_PASS_CHARPOOL_SPECIAL, PasswordCharacterPool.getSpecial())); @@ -280,6 +224,7 @@ void initPreferences () this.jCheckBoxShowFavoritesCount.setSelected(pa.getBoolean(PREF_FAVORITES_SHOW_COUNT)); this.jCheckBoxUseSearchTags.setSelected(pa.getBoolean(PREF_USE_SEARCH_TAGS)); this.jCheckBoxShowSearchTags.setSelected(pa.getBoolean(PREF_SHOW_SEARCH_TAGS)); + this.jCheckBoxTocGeneration.setSelected(pa.getBoolean(PREF_TOC_GENERATION)); this.jCheckBoxAddChangedDateTime.setSelected(pa.getBoolean(PREF_ADD_CHANGED_DATE_TIME)); this.jFormattedTextareaClearTimeout.setText(String.format("%d", pa.getInt(PREF_CLEAR_SECONDS))); this.jFormattedTextPassClearTimeout.setText(String.format("%d", pa.getInt(PREF_PASSWORD_SECONDS))); @@ -407,7 +352,7 @@ public int getSize () @Override public String getElementAt (int index) { - return lookAndFeelInfos[index].getClassName(); + return lookAndFeelInfos[index].getName(); } }); @@ -452,6 +397,26 @@ private void initComponents() jCheckBoxShowFavoritesCount = new JCheckBoxPersistent(PREF_FAVORITES_SHOW_COUNT); jCheckBoxUseSearchTags = new JCheckBoxPersistent(PREF_USE_SEARCH_TAGS, true); jCheckBoxShowSearchTags = new JCheckBoxPersistent(PREF_SHOW_SEARCH_TAGS, true); + + // TOC preferences + jCheckBoxTocGeneration = new JCheckBoxPersistent(PREF_TOC_GENERATION, false); + jPanelTocGeneration = new JPanel(); + jPanelTocGeneration.setBorder(new LineBorder(Color.GRAY, 1)); + BoxLayout boxLayoutTocGeneration = new BoxLayout(jPanelTocGeneration, BoxLayout.Y_AXIS); + jPanelTocGeneration.setLayout(boxLayoutTocGeneration); + + JPanel panelTocCharacter = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 5)); + jTextfieldTocPrefix = new JTextField(""); + jTextfieldTocPrefix.setColumns(3); + panelTocCharacter.add(jTextfieldTocPrefix); + panelTocCharacter.add(new JLabel("Prefix for TOC entries (leave empty for default template syntax)")); + panelTocCharacter.setAlignmentX(0f); + + jCheckBoxTocGeneration.setAlignmentX(0f); + jPanelTocGeneration.add(jCheckBoxTocGeneration); + jPanelTocGeneration.add(panelTocCharacter); + // - + jCheckBoxAddChangedDateTime = new JCheckBoxPersistent(PREF_ADD_CHANGED_DATE_TIME, false); jButtonApply = new javax.swing.JButton(); jButtonSave = new javax.swing.JButton(); @@ -534,7 +499,8 @@ public void actionPerformed (ActionEvent actionEvent) jCheckBoxFilterFavorites.setText("Filter favorites in addition to passwords"); jCheckBoxShowFavoritesCount.setText("Show count of individual favorite"); jCheckBoxUseSearchTags.setText("Use additional search tags when filtering list of passwords"); - jCheckBoxShowSearchTags.setText("Show search text in the password file list (behind the password file) "); + jCheckBoxShowSearchTags.setText("Show search tags in the password file list (behind the password file)"); + jCheckBoxTocGeneration.setText("Show table of contents in decoded password file (does not change the file)"); jCheckBoxAddChangedDateTime.setText("Add changed mark to each edited file"); // show/hide debug window @@ -559,8 +525,8 @@ public void actionPerformed (ActionEvent e) } }); - gl.setAutoCreateGaps(true); - gl.setAutoCreateContainerGaps(true); + gl.setAutoCreateGaps(false); + gl.setAutoCreateContainerGaps(false); JLabel dummyLabel = new JLabel(""); gl.setHorizontalGroup( gl.createParallelGroup() @@ -608,7 +574,7 @@ public void actionPerformed (ActionEvent e) .addComponent(jCheckBoxFilterFavorites) .addComponent(jCheckBoxShowTbButtonText) .addComponent(jCheckBoxAddChangedDateTime) - .addComponent(dummyLabel) + .addComponent(jPanelTocGeneration) ) ) ); @@ -634,7 +600,8 @@ public void actionPerformed (ActionEvent e) .addGroup(gl.createParallelGroup().addComponent(jCheckBoxUseFavorites).addComponent(jCheckBoxFilterFavorites)) .addGroup(gl.createParallelGroup().addComponent(jCheckBoxShowFavoritesCount).addComponent(jCheckBoxShowTbButtonText)) .addGroup(gl.createParallelGroup().addComponent(jCheckBoxUseSearchTags).addComponent(jCheckBoxAddChangedDateTime)) - .addGroup(gl.createParallelGroup().addComponent(jCheckBoxShowSearchTags).addComponent(dummyLabel)) + .addGroup(gl.createParallelGroup().addComponent(jCheckBoxShowSearchTags).addComponent(jPanelTocGeneration)) +// .addGroup(gl.createParallelGroup().addComponent(jCheckBoxTocGeneration).addComponent(dummyLabel)) ); @@ -812,6 +779,7 @@ private void jButtonApplyActionPerformed (java.awt.event.ActionEvent evt) FgPreferences.get().put(PREF_FAVORITES_SHOW_COUNT, jCheckBoxShowFavoritesCount.isSelected()); FgPreferences.get().put(PREF_USE_SEARCH_TAGS, jCheckBoxUseSearchTags.isSelected()); FgPreferences.get().put(PREF_SHOW_SEARCH_TAGS, jCheckBoxShowSearchTags.isSelected()); + FgPreferences.get().put(PREF_TOC_GENERATION, jCheckBoxTocGeneration.isSelected()); FgPreferences.get().put(PREF_CHARSET, comboBoxCharset.getSelectedItem()); FgPreferences.get().put(PREF_GPG_RID_FILE, jTexfFieldGpgRIDFile.getText()); FgPreferences.get().put(PREF_GPG_DEFAULT_RID, jTexfFieldGpgDefaultRID.getText()); @@ -820,6 +788,7 @@ private void jButtonApplyActionPerformed (java.awt.event.ActionEvent evt) FgPreferences.get().put(PREF_NEW_TEMPLATE, jTextNewFileTemplate.getText()); FgPreferences.get().put(PREF_GPG_PASS_CHARPOOL_UPPER, textFieldCharsUpper.getText().toUpperCase()); + FgPreferences.get().put(PREF_TOC_PREFIX, jTextfieldTocPrefix.getText()); FgPreferences.get().put(PREF_GPG_PASS_CHARPOOL_LOWER, textFieldCharsLower.getText().toLowerCase()); FgPreferences.get().put(PREF_GPG_PASS_CHARPOOL_SPECIAL, textFieldSpecial.getText()); FgPreferences.get().put(PREF_GPG_PASS_CHARPOOL_DIGIT, textFieldDigits.getText()); diff --git a/src/main/java/org/mockenhaupt/fortgnox/swing/FgPanelTextArea.java b/src/main/java/org/mockenhaupt/fortgnox/swing/FgPanelTextArea.java index cbf424c..d0bfc97 100644 --- a/src/main/java/org/mockenhaupt/fortgnox/swing/FgPanelTextArea.java +++ b/src/main/java/org/mockenhaupt/fortgnox/swing/FgPanelTextArea.java @@ -3,36 +3,14 @@ import org.mockenhaupt.fortgnox.FgPreferences; import org.mockenhaupt.fortgnox.MainFrame; import org.mockenhaupt.fortgnox.misc.FileUtils; +import org.mockenhaupt.fortgnox.misc.StringUtils; import org.mockenhaupt.fortgnox.tags.TagsStore; -import javax.swing.BorderFactory; -import javax.swing.ImageIcon; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JLabel; -import javax.swing.JMenuItem; -import javax.swing.JPanel; -import javax.swing.JPopupMenu; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; -import javax.swing.JTextPane; -import javax.swing.JToggleButton; -import javax.swing.JToolBar; -import javax.swing.SwingConstants; -import javax.swing.SwingUtilities; +import javax.swing.*; import javax.swing.Timer; import javax.swing.event.HyperlinkEvent; -import javax.swing.text.BadLocationException; -import javax.swing.text.DefaultHighlighter; -import javax.swing.text.Document; -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; -import java.awt.Desktop; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.MouseInfo; -import java.awt.Point; +import javax.swing.text.*; +import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; @@ -47,15 +25,8 @@ import java.net.URISyntaxException; import java.net.URLDecoder; import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; +import java.util.*; import java.util.List; -import java.util.Map; -import java.util.Scanner; -import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -93,9 +64,14 @@ public class FgPanelTextArea extends JPanel implements PropertyChangeListener, F )); private static final List DEFAULT_USERNAME_PATTERNS = new ArrayList(Arrays.asList( - "account", "user", ".*id[:=]", "login", "Auftra", "benutzer", "Kunde", "telefon", ".*nummer", ".*kennung" + "account", "user", ".*id[:=]", "login", "Auftrag", "benutzer", "Kunde", "telefon", ".*nummer", ".*kennung" )); + private static final String TOC_HEADER_PREFIX_FILE = "file://"; + private static final String TOC_HEADER_SUFFIX = "tocheader_"; + private static final String TOC_HEADER_PREFIX = TOC_HEADER_PREFIX_FILE + TOC_HEADER_SUFFIX; + private static final String TOC_START = ""; + private static final String TOC_END = "-- "; enum LineMaskingOrder @@ -116,9 +92,12 @@ enum LineMaskingOrder private boolean prefClipboardToolbarVisible = true; private boolean prefMaskFirstLine = true; private int prefTextAreaFontSize = 14; + private boolean prefTocGeneration = false; + private String prefTocPrefix = ""; private final AtomicReference oldStatusText = new AtomicReference<>(""); // stores the text position of search hits final private List hitList = new ArrayList<>(); + SortedMap tocMap = new TreeMap<>(); private int caretPointer = -1; public static Color BACKGROUND = new java.awt.Color(62, 62, 62); @@ -126,7 +105,10 @@ enum LineMaskingOrder public static final String PASSWORD_PREFIX = "oghogoo3eaTheephe7:"; public static final String GPG_FILE_PASSWORD_PREFIX = "oghogoo3eaTheephe8:"; public static final String COLOR_LINK = "#9fbfff"; - public static final String COLOR_GPG_FILE = "#8DDE7EFF"; + private static final String COLOR_END_OF_TOC = "#999999"; + + public static final String COLOR_JUMP_TOP = "#8DDE7EFF"; + public static final String COLOR_GPG_FILE = COLOR_JUMP_TOP; public static final String COLOR_CLIPBOARD = "#fff8ac"; public static final String COLOR_EMAIL = COLOR_CLIPBOARD; public static final String COLOR_PASSWORD = "#ed8cc5"; @@ -231,10 +213,44 @@ public void setMaskUsernamePatterns (String maskUsernamePatterns) } + public static void scrollToLineOfCaretPosition(JTextComponent component, int pos) throws BadLocationException + { + component.setCaretPosition(pos); + Container container = SwingUtilities.getAncestorOfClass(JViewport.class, component); + + if (container == null) return; + + try + { + Rectangle r = component.modelToView(component.getCaretPosition()); + JViewport viewport = (JViewport)container; + // int extentHeight = viewport.getExtentSize().height; + // int viewHeight = viewport.getViewSize().height; + // int y = Math.max(0, r.y - ((extentHeight - r.height) / 2)); + // y = Math.min(y, viewHeight - extentHeight); + + viewport.setViewPosition(new Point(0, r.y)); + } + catch(BadLocationException ble) + { + throw ble; + } + } + private void initTextArea (MainFrame mainFrame) { this.mainFrame = mainFrame; this.textPane = new JTextPane(); + +// StyleSheet styleSheet = new StyleSheet(); +//// styleSheet.addRule("ol {padding: 0px;}"); +// HTMLEditorKit htmlEditorKit = new HTMLEditorKit(); +// htmlEditorKit.setStyleSheet(styleSheet); +// +// HTMLDocument htmlDocument = (HTMLDocument) htmlEditorKit.createDefaultDocument(); +// textPane.setDocument(htmlDocument); + + textPane.setBorder(BorderFactory.createLineBorder(BACKGROUND, 1)); textPane.setEditable(false); textPane.setContentType("text/html"); @@ -295,6 +311,14 @@ public void keyTyped(KeyEvent e) } }); + setupHyperLinkEvents(mainFrame); + } + + + + + private void setupHyperLinkEvents(MainFrame mainFrame) + { textPane.addHyperlinkListener(e -> { if (HyperlinkEvent.EventType.ACTIVATED.equals(e.getEventType())) @@ -303,40 +327,23 @@ public void keyTyped(KeyEvent e) { if (e.getURL() != null) { - if (isOpenUrls()) + String needle = e.getURL().getHost(); + + if (needle.startsWith(TOC_HEADER_SUFFIX)) + { + int offset = getDocumentText().indexOf(TOC_END); + needle = needle.replaceAll(TOC_HEADER_SUFFIX, ""); + + int pos = Integer.parseInt(needle); + scrollToLineOfCaretPosition(textPane, pos == 0 ? 0 : pos + offset); + } + else if (isOpenUrls()) { openUrlLink(e); } else { copyToClipboard(e); -// String text = e.getDescription().trim(); -// MainFrame.toClipboard(text, "\"" + text + "\"", false); -// -// JPopupMenu popupMenu = new JPopupMenu(); -// JMenuItem miCopy = new JMenuItem("Copy URL to Clipboard"); -// miCopy.addActionListener(a -> copyToClipboard(e)); -// JMenuItem miOpen = new JMenuItem("Open URL in Browser"); -// miOpen.addActionListener(a -> -// { -// try -// { -// openUrlLink(e); -// } -// catch (URISyntaxException uriSyntaxException) -// { -// uriSyntaxException.printStackTrace(); -// } -// catch (IOException ioException) -// { -// ioException.printStackTrace(); -// } -// }); -// popupMenu.add(miOpen); -// popupMenu.add(miCopy); -// Point pointer = MouseInfo.getPointerInfo().getLocation(); -// SwingUtilities.convertPointFromScreen(pointer, textPane); -// popupMenu.show(textPane, pointer.x, pointer.y); } } else @@ -377,9 +384,16 @@ else if (HyperlinkEvent.EventType.ENTERED.equals(e.getEventType())) oldStatusText.set(getStatusText()); } - if (e.getURL() != null && isOpenUrls()) + if (e.getURL() != null) { - setStatusText("Open \"" + desc + "\" in browser"); + String needle = e.getURL().getHost(); + if (needle.startsWith(TOC_HEADER_SUFFIX)) + { + if (needle.endsWith("_0")) setStatusText("Jump to top"); + else setStatusText("Jump to section '" + tocMap.get(e.getURL().toString()) + "'"); + } + else if (isOpenUrls()) setStatusText("Open \"" + desc + "\" in browser"); + else setStatusText("Copy \"" + desc + "\" to clipboard"); } else if (desc.startsWith(PASSWORD_PREFIX)) { @@ -587,7 +601,7 @@ public void actionPerformed (ActionEvent actionEvent) scrollPaneTextAreaError.setMaximumSize(new Dimension(Integer.MAX_VALUE, h * 4)); scrollPaneTextAreaError.setPreferredSize(scrollPaneTextAreaError.getMaximumSize()); - scrollPaneTextAreaError.setVisible(false); +// scrollPaneTextAreaError.setVisible(false); textAreaError.setBackground(BACKGROUND); textAreaError.setForeground(new Color(232, 228, 160)); @@ -758,6 +772,8 @@ private void loadPreferences () prefClipboardToolbarVisible = FgPreferences.get().get(FgPreferences.PREF_SHOW_PASSWORD_SHORTCUT_BAR, prefClipboardToolbarVisible); prefMaskFirstLine = FgPreferences.get().get(FgPreferences.PREF_MASK_FIRST_LINE, prefMaskFirstLine); prefTextAreaFontSize = FgPreferences.get().get(FgPreferences.PREF_TEXTAREA_FONT_SIZE, prefTextAreaFontSize); + prefTocGeneration = FgPreferences.get().get(FgPreferences.PREF_TOC_GENERATION, prefTocGeneration); + prefTocPrefix = FgPreferences.get().get(FgPreferences.PREF_TOC_PREFIX, prefTocPrefix); FgPreferences.get().get(PREF_RESET_MASK_BUTTON_SECONDS, 5); } @@ -897,7 +913,12 @@ private String getLink (String url) return getLink(url, url, COLOR_LINK); } - private String getLink (String href, String showRef, String color) + private String getLink (String url, String text) + { + return getLink(url, text, COLOR_LINK); + } + + private String getLink (String href, String text, String color) { if (isDetectUrls()) { @@ -907,13 +928,13 @@ private String getLink (String href, String showRef, String color) ";' href='"); sb.append(href); sb.append("'>"); - sb.append(showRef); + sb.append(text); sb.append(""); return sb.toString(); } else { - return showRef; + return text; } } @@ -971,16 +992,21 @@ private String getMaskedText () { Scanner scanner = new Scanner(this.plainText); String maskedText = ""; + String plainMaskedText = ""; int passwordCount = 1; int lineNr = 0; int blankCount = 0; resetClipboardCommands(); + int headerLine = -1; + int lastHeaderLine = -1; while (scanner.hasNextLine()) { - String line = scanner.nextLine(); + String line = StringUtils.trimEnd(scanner.nextLine()); + String plainLine = line; + // BLANK LINE COMPRESSION ================================== boolean isBlankLine = line.matches("^\\s*$"); if (isBlankLine) { @@ -995,6 +1021,7 @@ private String getMaskedText () blankCount = 0; } + lineNr++; boolean lineHandled = false; @@ -1125,6 +1152,7 @@ private String getMaskedText () String password = matcher.group(2).trim(); String mask = PASSWORD_MASK + passwordCount; line = matcher.replaceAll("$1" + getPasswordLink(password, mask)); + plainLine = matcher.replaceAll("$1" + mask); addClipboardCommand(passwordCount, password); passwordCount++; lineHandled = true; @@ -1155,6 +1183,21 @@ private String getMaskedText () } } + + // TOC GENERATION ================================== + if (!lineHandled && prefTocGeneration) + { + AtomicReference lineRef = new AtomicReference<>(line); + AtomicReference headerLineRef = new AtomicReference<>(headerLine); + AtomicReference lastHeaderLineRef = new AtomicReference<>(lastHeaderLine); + handleTocGenerationInText(lineRef, headerLineRef, lastHeaderLineRef, plainMaskedText, lineNr); + line = lineRef.get(); + headerLine = headerLineRef.get(); + lastHeaderLine = lastHeaderLineRef.get(); + } + + // line feed + plainMaskedText += plainLine; maskedText += "
" + line +"
"; } @@ -1166,10 +1209,109 @@ private String getMaskedText () setClipToolbarVisibility(clipToolbar.getComponentCount() > 0); - return maskedText; + return getToc() + maskedText; + } + + + private void handleTocGenerationInText(AtomicReference lineRef, + AtomicReference headerLineRef, + AtomicReference lastHeaderLineRef, + String plainMaskedText, + int lineNr) + { + String line = lineRef.get(); + int headerLine = headerLineRef.get(); + int lastHeaderLine = lastHeaderLineRef.get(); + + if (prefTocPrefix == null || prefTocPrefix.isEmpty()) + { + String regexp = "^=+$"; + Pattern pattern = Pattern.compile(regexp, Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(line); + if (matcher.matches())// && matcher.groupCount() >= 2) + { + headerLine = lineNr; + } + + String headerRegexp = "^(=*\\s*)(.+)$"; + Pattern headerPattern = Pattern.compile(headerRegexp, Pattern.CASE_INSENSITIVE); + Matcher headerMatcher = headerPattern.matcher(line); + if (headerMatcher.matches() && headerMatcher.groupCount() >= 2) + { + String headerText = headerMatcher.group(2); + boolean neverHit = lastHeaderLine <= 0; + if (lineNr == headerLine + 1 && (neverHit || lineNr > lastHeaderLine + 2)) + { + String key = TOC_HEADER_PREFIX + String.format("%05d", plainMaskedText.length()); + line = headerMatcher.group(1) + getJumpToHeader(key, headerText); + tocMap.put(key, headerText); + lastHeaderLine = lineNr; + } + } + } + else + { + String headerRegexp = "^" + prefTocPrefix + "\\s*(.+)$"; + Pattern headerPattern = Pattern.compile(headerRegexp); + Matcher headerMatcher = headerPattern.matcher(line); + if (headerMatcher.matches() && headerMatcher.groupCount() >= 1) + { + String headerText = headerMatcher.group(1); + String key = TOC_HEADER_PREFIX + String.format("%05d", plainMaskedText.length()); + line = getJumpToHeader(key, headerText); + tocMap.put(key, headerText); + } + } + + headerLineRef.set(headerLine); + lineRef.set(line); + lastHeaderLineRef.set(lastHeaderLine); + } + + + + + + private String getToc () + { + if (!prefTocGeneration || tocMap == null || tocMap.isEmpty()) + { + return ""; + } + + StringBuilder sb = new StringBuilder(); + + Iterator> tocIter = tocMap.entrySet().iterator(); + + sb.append(TOC_START); + sb.append("
    "); + while (tocIter.hasNext()) + { + Map.Entry tocEntry = tocIter.next(); + String text = tocEntry.getValue(); + if (text != null && !text.trim().isEmpty()) + { + sb.append("
  1. "); + sb.append(getLink(tocEntry.getKey(), text.trim(), COLOR_JUMP_TOP)); + sb.append("
  2. "); + } + } + sb.append("
"); + sb.append(""); + sb.append(TOC_END); + sb.append(""); + return sb.toString(); + } + + private String getJumpToHeader(String id, String text) + { + StringBuilder sb = new StringBuilder(); + sb.append(getLink(TOC_HEADER_PREFIX + "0", text.trim(), COLOR_JUMP_TOP)); + return sb.toString(); } + private boolean isPasswordMatch (String line) { if (getMaskPasswordPatterns() != null && !getMaskPasswordPatterns().isEmpty()) @@ -1210,6 +1352,7 @@ private void updateText () { resetClipboardCommands(); resetSearch(); + tocMap.clear(); Point p = scrollPaneTextArea.getViewport().getViewPosition(); @@ -1248,7 +1391,7 @@ public void setText (String text, String err, String shortFileName, String fullF { if (text != null) { - setLabelFileName(shortFileName + TagsStore.getTagsOfFile(fullFileName)); + setLabelFileName(shortFileName == null ? "" : shortFileName + TagsStore.getTagsOfFile(fullFileName)); this.plainText = text; updateText(); } @@ -1258,7 +1401,7 @@ public void setText (String text, String err, String shortFileName, String fullF errText = errText.replaceAll("\\n$", ""); errText = errText.replaceAll("\\r\\n$", ""); textAreaError.setText(errText); - scrollPaneTextAreaError.setVisible(err != null && !err.isEmpty()); +// scrollPaneTextAreaError.setVisible(err != null && !err.isEmpty()); } @@ -1308,6 +1451,15 @@ public void propertyChange (PropertyChangeEvent propertyChangeEvent) prefTextAreaFontSize = FgPreferences.get().get(FgPreferences.PREF_TEXTAREA_FONT_SIZE, prefTextAreaFontSize); SwingUtilities.invokeLater(() -> updateText()); break; + case FgPreferences.PREF_TOC_GENERATION: + prefTocGeneration = FgPreferences.get().get(FgPreferences.PREF_TOC_GENERATION, prefTocGeneration); + SwingUtilities.invokeLater(() -> updateText()); + break; + case FgPreferences.PREF_TOC_PREFIX: + prefTocPrefix = FgPreferences.get().get(FgPreferences.PREF_TOC_PREFIX, prefTocPrefix); + if (prefTocPrefix != null) prefTocPrefix = prefTocPrefix.trim(); + SwingUtilities.invokeLater(() -> updateText()); + break; } } @@ -1316,15 +1468,9 @@ public void handleTextFilterChanged (String filter) { mainFrame.startTimer(); - String documentText; - Document document = textPane.getDocument(); - try + String documentText = getDocumentText(); + if (documentText == null) { - documentText = document.getText(0, document.getLength()); - } - catch (BadLocationException e) - { - e.printStackTrace(); return; } @@ -1346,6 +1492,22 @@ public void handleTextFilterChanged (String filter) } } + private String getDocumentText() + { + String documentText; + Document document = textPane.getDocument(); + try + { + documentText = document.getText(0, document.getLength()); + } + catch (BadLocationException e) + { + e.printStackTrace(); + return null; + } + return documentText; + } + @Override public void requestFocus() { diff --git a/src/main/java/org/mockenhaupt/fortgnox/swing/LAFChooser.java b/src/main/java/org/mockenhaupt/fortgnox/swing/LAFChooser.java index d4ccf35..5242d8f 100644 --- a/src/main/java/org/mockenhaupt/fortgnox/swing/LAFChooser.java +++ b/src/main/java/org/mockenhaupt/fortgnox/swing/LAFChooser.java @@ -1,12 +1,18 @@ package org.mockenhaupt.fortgnox.swing; +import com.formdev.flatlaf.FlatDarculaLaf; +import com.formdev.flatlaf.FlatDarkLaf; +import com.formdev.flatlaf.FlatIntelliJLaf; +import com.formdev.flatlaf.FlatLightLaf; import org.mockenhaupt.fortgnox.FgPreferences; import org.mockenhaupt.fortgnox.MainFrame; +import org.yaml.snakeyaml.util.ArrayUtils; -import javax.swing.SwingUtilities; -import javax.swing.UIManager; -import javax.swing.UnsupportedLookAndFeelException; -import java.awt.Component; +import javax.swing.*; +import java.awt.*; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; @@ -43,7 +49,7 @@ public static LAFChooser get() public boolean set (String laf, Component c) { UIManager.LookAndFeelInfo o = Arrays.stream(lafs) - .filter(lookAndFeelInfo -> lookAndFeelInfo.getClassName().toLowerCase().contains(laf.toLowerCase())) + .filter(lookAndFeelInfo -> lookAndFeelInfo.getName().toLowerCase().contains(laf.toLowerCase())) .findFirst() .orElse(null); if (o != null) return set(o, c); @@ -90,9 +96,32 @@ public UIManager.LookAndFeelInfo[] getLafs () return lafs; } - private void init () - { - lafs = UIManager.getInstalledLookAndFeels(); + private void init() { + + ArrayList ail = new ArrayList(); + ail.addAll(ArrayUtils.toUnmodifiableList(UIManager.getInstalledLookAndFeels())); + + Class clazzes[] = {FlatDarkLaf.class, FlatDarculaLaf.class, FlatLightLaf.class, FlatIntelliJLaf.class}; + for (Class clazz : clazzes) { + Method method = null; + try { + method = clazz.getMethod("setup"); + if (method == null) continue; + + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + try { + Object o = method.invoke(null); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + UIManager.LookAndFeelInfo lafi = new UIManager.LookAndFeelInfo(clazz.getSimpleName(), clazz.getName()); + ail.add(lafi); + } + lafs = ail.toArray(new UIManager.LookAndFeelInfo[]{}); } diff --git a/src/main/resources/org/mockenhaupt/fortgnox/fortGnox.png b/src/main/resources/org/mockenhaupt/fortgnox/fortGnox.png index 5caea11..62c6d3e 100644 Binary files a/src/main/resources/org/mockenhaupt/fortgnox/fortGnox.png and b/src/main/resources/org/mockenhaupt/fortgnox/fortGnox.png differ diff --git a/src/main/resources/org/mockenhaupt/fortgnox/fortGnox.xcf b/src/main/resources/org/mockenhaupt/fortgnox/fortGnox.xcf index 8314033..bca9ef2 100644 Binary files a/src/main/resources/org/mockenhaupt/fortgnox/fortGnox.xcf and b/src/main/resources/org/mockenhaupt/fortgnox/fortGnox.xcf differ diff --git a/src/main/resources/org/mockenhaupt/fortgnox/fortGnox128.png b/src/main/resources/org/mockenhaupt/fortgnox/fortGnox128.png index 91481e5..cb3a2b9 100644 Binary files a/src/main/resources/org/mockenhaupt/fortgnox/fortGnox128.png and b/src/main/resources/org/mockenhaupt/fortgnox/fortGnox128.png differ diff --git a/src/main/resources/org/mockenhaupt/fortgnox/fortGnox48.png b/src/main/resources/org/mockenhaupt/fortgnox/fortGnox48.png index 9d866ab..da14f1b 100644 Binary files a/src/main/resources/org/mockenhaupt/fortgnox/fortGnox48.png and b/src/main/resources/org/mockenhaupt/fortgnox/fortGnox48.png differ diff --git a/src/main/resources/org/mockenhaupt/fortgnox/template.txt b/src/main/resources/org/mockenhaupt/fortgnox/template.txt index 97737f3..a5989f1 100644 --- a/src/main/resources/org/mockenhaupt/fortgnox/template.txt +++ b/src/main/resources/org/mockenhaupt/fortgnox/template.txt @@ -2,7 +2,7 @@ ======================================================== = $FILENAME = url: -======================================================== +-------------------------------------------------------- user: mail: pass: diff --git a/src/test/java/org/mockenhaupt/fortgnox/PasswordGeneratorTest.java b/src/test/java/org/mockenhaupt/fortgnox/PasswordGeneratorTest.java new file mode 100644 index 0000000..ec8e8c8 --- /dev/null +++ b/src/test/java/org/mockenhaupt/fortgnox/PasswordGeneratorTest.java @@ -0,0 +1,105 @@ +package org.mockenhaupt.fortgnox; + +import org.junit.Assert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +class PasswordGeneratorTest +{ + + enum CharClass + { + digit(1 << 0), + upper(1 << 1), + lower(1 << 2), + special(1 << 3); + + private final int value; + + CharClass (int i) + { + this.value = i; + } + + public int getValue () + { + return value; + } + } + + PasswordGenerator passwordGenerator; + + @BeforeEach + void setUp () + { + passwordGenerator = new PasswordGenerator(password -> + { + }); + + } + + @Test + void generatePassword () + { + int charClass = 0; + for (int len = 20; len > 3; --len) + for (charClass = 1; charClass <= 0xf; ++charClass) + { + boolean digit = (charClass & CharClass.digit.getValue()) != 0; + boolean upper = (charClass & CharClass.upper.getValue()) != 0; + boolean lower = (charClass & CharClass.lower.getValue()) != 0; + boolean special = (charClass & CharClass.special.getValue()) != 0; + + AtomicReference pass = new AtomicReference<>(""); + passwordGenerator.generatePassword(len, digit, upper, lower, special, s -> + { + }, p -> pass.set(p)); + +// System.out.print("charClass = " + charClass); +// System.out.print(", digit: " + (digit ? "T" : "F")); +// System.out.print(", lower: " + (upper ? "T" : "F")); +// System.out.print(", upper: " + (lower ? "T" : "F")); +// System.out.println(", special: " + (special ? "T" : "F") + " password: " + pass); + + checkPassWordComplete(pass.get(), len, digit, upper, lower, special); + + } + + } + + + void checkPassWordComplete (String pass, + long len, + boolean digit, + boolean upper, + boolean lower, + boolean useSpecial) + { + Assert.assertTrue("Password " + pass + " has not length " + len, pass.length() == len); + if (digit) checkPool(pass, passwordGenerator.getDigits()); + if (upper) checkPool(pass, passwordGenerator.getUppercase()); + if (lower) checkPool(pass, passwordGenerator.getLowercase()); + if (useSpecial) checkPool(pass, passwordGenerator.getSpecial()); + + } + + private void checkPool (String pass, List characterList) + { + boolean found = false; + for (Character poolChar : characterList) + { + Optional charFound = pass.chars().mapToObj(value -> (char) value).filter(character -> character == poolChar).findAny(); + if (charFound.isPresent()) + { + found = true; + break; + } + } + Assert.assertTrue("Password " + pass + " has none of " + characterList.toString(), found); + } + +} diff --git a/src/test/java/org/mockenhaupt/fortgnox/PreferencesAccessTest.java b/src/test/java/org/mockenhaupt/fortgnox/PreferencesAccessTest.java index d2248f5..39b0345 100644 --- a/src/test/java/org/mockenhaupt/fortgnox/PreferencesAccessTest.java +++ b/src/test/java/org/mockenhaupt/fortgnox/PreferencesAccessTest.java @@ -1,16 +1,23 @@ package org.mockenhaupt.fortgnox; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Iterator; +import java.util.Map; +import java.util.function.Consumer; import static org.junit.jupiter.api.Assertions.assertEquals; class PreferencesAccessTest { - static final String NODE = PreferencesAccessTest.class.getPackage().getName() + "JUNIT"; + static final String NODE = FgPreferences.PREFERENCE_NODE_TEST; @BeforeEach - void setUp () + public void setUp () { PreferencesAccess.UNIT_TEST = true; PreferencesAccess.getInstance(NODE).clear(); @@ -24,9 +31,9 @@ void getPreference () { PreferencesAccess pa = PreferencesAccess.getInstance(NODE); - pa.addPropertyChangeListener( - propertyChangeEvent -> assertEquals(expectedEvent, propertyChangeEvent.getNewValue()) - ); + PropertyChangeListener p = propertyChangeEvent -> assertEquals(expectedEvent, propertyChangeEvent.getNewValue()); + + pa.addPropertyChangeListener(p); String expected = "default_string"; expectedEvent = expected; @@ -54,5 +61,39 @@ void getPreference () assertEquals(pa, pa.putPreference("int", 6)); assertEquals(6, pa.getPreference("int", 666)); + pa.removePropertyChangeListener(p); + } + + @Test + void putLongProperty() { + PreferencesAccess pa = PreferencesAccess.getInstance(NODE); + + PropertyChangeListener p = propertyChangeEvent -> + assertEquals(FgPreferences.PREF_EXCEPTION, propertyChangeEvent.getPropertyName() + ); + + pa.addPropertyChangeListener(p); + + StringBuilder jsonBuilder = new StringBuilder(); + jsonBuilder.append("{"); + int max = 1024 * 3; + for (int i = 0 ; i < max; ++i) + { + String key = String.format("key_%05d", i); + String val = String.format("val_%05d", i); + jsonBuilder.append('"'); + jsonBuilder.append(key); + jsonBuilder.append('"'); + jsonBuilder.append(':'); + jsonBuilder.append(val); + if (i < max - 1) + { + jsonBuilder.append(','); + } + } + jsonBuilder.append("}"); + pa.put("BIGJSON", jsonBuilder.toString()); + pa.removePropertyChangeListener(p); + } -} \ No newline at end of file +} diff --git a/src/test/java/org/mockenhaupt/fortgnox/misc/StringUtilsTest.java b/src/test/java/org/mockenhaupt/fortgnox/misc/StringUtilsTest.java new file mode 100644 index 0000000..b8b06bc --- /dev/null +++ b/src/test/java/org/mockenhaupt/fortgnox/misc/StringUtilsTest.java @@ -0,0 +1,18 @@ +package org.mockenhaupt.fortgnox.misc; + +import org.junit.Assert; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class StringUtilsTest { + + @Test + void andMatcher() { + assertEquals(false, StringUtils.andMatcher(null, (String)null)); + assertEquals(true, StringUtils.andMatcher("hanse", "a")); + assertEquals(false, StringUtils.andMatcher("hanse", "x")); + assertEquals(true, StringUtils.andMatcher("hanse", "ANS")); + assertEquals(true, StringUtils.andMatcher("hanse", "s h a e s")); + } +}