Class ConfigPropertyEditor

java.lang.Object
org.bzdev.swing.ConfigPropertyEditor

public abstract class ConfigPropertyEditor extends Object
Property-editor class. This class supports configuration files that are represented as Java property files using the syntax described in the documentation for Properties.load(Reader). A separate class, ConfigPropUtilities, can be used to read files produced by property editors when editing is not needed.

The editor displays a window (whether a frame or dialog) containing a menubar, some buttons, and a table with two columns: one labeled Property and one labeled Value. The initial properties may have keys that cannot be edited, although their values can be edited. When this is the case, the background for this portion of the table will have a different color than the rest of the table. To insert a new row above a row, or to move or delete the row, the entire row must be selected. A table can also be configured to suppress some of the buttons located just above the table. Some keys in the table may consist of one or more dashes (minus signs). There are treated as spacers and are ignored when the properties are written or used outside of the editor.

Property values may be encrypted using GPG. The keys for these properties start with the substring "ebase64." and the values are stored in an encrypted form. GPG Agent should be configured so that decryption can be used conveniently. To use GPG Agent on Debian Linux systems, add the following lines to the .bashrc script:


     GPG_TTY=$(tty)
     export GPG_TTY
 

The file format is actually a stylized version of a standard Java properties file. The first additional constraint is that the first line, should be a comment (hence the initial '#') of the form


 #(M.T MEDIATYPE)
 
where MEDIATYPE is the media type assigned to the property file. This convention is used when ConfigPropertyEditor reads or writes a configuration file. In addition each line will be terminated with a carriage return followed by a line feed (the convention used in most RFCs for text-based formats). The first line must appear exactly as shown, with no leading or trailing white space, with a single space before the media type, and no spaces between the media type and the terminating closing parenthesis.

This format must use UTF-8 as its character set. While Java historically used ISO 8859-1 for a property file's character set, the methods Properties.load(Reader) and Properties.store(Writer,String) are used by this class, with the readers and writers configured to use the UTF-8 character set, and those methods, with UTF-8, are used by this class.

Keys are restricted to ones that consist of a sequence of tokens separated by periods, with each token starting with a letter followed by 0 or more letters, digits, and underscores. There are two special initial tokens:

  • base64. This token indicates that the value is base-64 encoded.
  • ebase64. This token indicates that the value is first base64 encoded and then encrypted using GPG or PGP. One use of this field is to store passwords (e.g., for a database). To encrypt, one will provide a list of recipients: when configuring a database, for instance, one can use this feature so that an administrator and a user can have access to the same password.
ConfigPropertyEditor will remove the encryption and encoding to allow editing of such fields. To decrypt, the user will have to enter a GPG passphrase. GPG Agent should be configured for this to work properly

Values that are not encoded or encrypted allow variable substitutions. the expression


   $(KEY)
 
will be replaced with the value of the key. The order of the keys does not matter, but references must not be circular. The sequence $$ will be replaced with $. It may be more convenient to use base-64 encoding, in which case variable substitution will not occur. Leading and trailing whitespace is preserved for keys whose first token is "base64" or "ebase64", but not otherwise. With any "base64" or "ebase64" token and its following delimited removed, each key must be unique.

To create an instance of this class for editing a configuration file for a specific application, a subclass has to be defined The constructor for such a subclass is expected to call the following methods (most are optional):

  • addIcon(java.awt.Image) or addIcon(java.lang.Class<?>,java.lang.String). These methods provide an icon to display when a configuration editor's window is iconified. Normally there are multiple icons, corresponding to different sizes required by a window manager.
  • addReservedKeys(String...). There may be some number of distinguished properties that are nominally expected to be present. This method will define a group of such keys. When called multiple times, each group will be separated from the others by a series of dashes in a table's first column.
  • addAltReservedKeys(String,String...). This method adds a set of reserved properties that will appear in a single row. The property names consist of a prefix, followed by a period, followed by a suffix. A combo box will allow one to choose the appropriate property. The values can use different renderers and editors.
  • setupCompleted(). This method must be called by the constructor, and indicates that all the reserved keys, and any default values associated with these, have been provided.
  • setDefaultProperty(String,String). Some properties have default values, typically in cases where the defaults are likely to be the ones the user needs. This method should be used to define what these defaults are.
  • addRE(String,TableCellRenderer,TableCellEditor). This method associates a table-cell renderer and editor with the final components of a key (components are separated by periods). One use is for configuring specialized renderers and editors for properties that provide colors.
  • changedPropertyClears(java.lang.String,java.lang.String...) This method indicates that when one property's value is changed, or the property is removed, other properties should have their values set to null. It is useful in cases where the change of one property indicates that the value of some other properties is almost certainly wrong. Generally these will reserved properties and should be listed in close proximity to each other.
The constructor may optionally call the following methods:
  • freezeRows(). This method prevents the user from adding, moving, or deleting rows. The user may, however, change a row's value or key (unless the row is a reserved row).
  • setInitialExtraRows(int). This method sets the number of blank table entries that appear after any predefined keys.
In addition a subclass must define the following methods:
  • errorTitle(). This contains the title used in dialog boxes associated with error messages.
  • configTitle(). This contains the title used in an ConfigPropertyEditor window (whether a frame or a dialog).
  • mediaType(). This contains the application-specific media type for a properties file, and appears in the first line of files saved by this methods in this class.
  • extensionFilterTitle(). This contains the title to use in a file-chooser dialog for the extension associated with an application's configuration file.
  • extension. This contains the file-name extension for an application's configuration file.

The following statements provide some examples of frequently used operations, assuming an instance named editor has been created.

  • Loading from a file:
    
              editor.loadFile(file);
            
  • Loading using a dialog:
    
                editor.showLoadDialog(component)
            
  • Opening the editor:
    
                  editor.edit(null, ConfigPropertyEditor.Mode.MODAL, null, true);
            
  • Getting decoded/decrypted properties:
    
                Properties config = editor.getDecodedProperties();
            
Finally, ConfigPropertyEditor is not a Swing or AWT component, although it uses such components, and its public methods do not have to be called on the AWT event dispatch thread. The rationale is that one use case is for providing a dialog box for configuring a program that otherwise runs as a command-line program and exits when done.
  • Constructor Details

    • ConfigPropertyEditor

      protected ConfigPropertyEditor()
      Constructor.
  • Method Details

    • getIconList

      public List<Image> getIconList()
      Obtain a list of images that can be used by a GUI when a window associated with this instance is closed.
      Returns:
      the images
    • errorTitle

      protected abstract String errorTitle()
      Get the title to use in modal dialog boxes reporting errors
      Returns:
      the title
    • configTitle

      protected abstract String configTitle()
      Get the title for the window containing this editor.
      Returns:
      the title
    • mediaType

      protected abstract String mediaType()
      Get the media type for the configuration file.
      Returns:
      the media type
    • setDefaultProperty

      protected void setDefaultProperty(String key, String value)
      Set the default value for a property. If the key starts with "base64.", this method will apply the Base-64 encoding. If the key starts with "ebase64.", the value must be first encrypted with GPG and then Base-64 encoded.
      Parameters:
      key - the property key
      value - the default value for the specified key
    • extensionFilterTitle

      protected abstract String extensionFilterTitle()
      Get the title for a file-chooser filter for the extension supported by this configuration property editor. This will appear as the title for a pull-down box that allows one to choose specific file extensions in an 'open' or 'save' dialog box
      Returns:
      the title
    • extension

      protected abstract String extension()
      Get the filename extension for configuration files.
      Returns:
      the filename extension
    • addIcon

      protected void addIcon(Image icon)
      Add an icon for use by a window system when this object is iconified.
      Parameters:
      icon - the icon
    • addIcon

      protected void addIcon(Class<?> clasz, String name)
      Add an icon for use by a window system when this object is iconified, given a class and resource. The name will be used to find a resource using the class loader for the specified class.

      To work well with Java modules, the resource should be in the same package as the class.

      Parameters:
      clasz - the class
      name - the name of a resource
    • setRecipients

      public void setRecipients(Component c)
      Set the list of recipients to use when values are encrypted. The list will be built interactively. A recipient is a string acceptable to the '-r' argument for GPG.
      Parameters:
      c - the component on which to center dialog boxes
    • setRecipients

      public void setRecipients(String[] list)
      Set the list of recipients to use when values are encrypted, given an array of recipients. A recipient is a string acceptable to the '-r' argument for GPG.
      Parameters:
      list - the recipient list; null to remove the recipients list
    • setRecipients

      public void setRecipients(List<String> list)
      Set the list of recipients to use when values are encrypted, given a list of recipients. A recipient is a string acceptable to the '-r' argument for GPG.
      Parameters:
      list - the recipient list; null to remove the recipients list
    • clearRecipients

      public void clearRecipients()
      Clear the recipients list. After this is called, and until either setRecipients(Component) or setRecipients(String[]) is called, the user will be asked for recipients each time a value is encrypted.
    • setSaveQuestion

      public void setSaveQuestion(boolean mode)
      Set whether or not a warning message about needing to save the state of the table should be shown.
      Parameters:
      mode - true if the user should be warned to save a modified table; false otherwise
    • save

      public void save(File f) throws IOException
      Save the configuration in a file.
      Parameters:
      f - the file
      Throws:
      IOException - if an IO error occurred
    • addReservedKeys

      protected void addReservedKeys(String... keys)
      Add a set of reserved keys.
      Parameters:
      keys - the keys
    • addAltReservedKeys

      protected void addAltReservedKeys(String prefix, String... suffixes) throws IllegalArgumentException
      Add a reserved-key entry where the key can be changed to use various suffixes. This will cause a spacer (a string of '-' characters where a key is expected) to be added after the entry, unless the prefix was already entered as a key by a call to addReservedKeys(String...) with that key given as the concatenation of the prefix, a period, and the first suffix. In this case, the call to addReservedKeys(String...) must immediately precede the call to this method or a sequence of calls to this method.
      Parameters:
      prefix - the start of a key
      suffixes - the possible suffixes following the prefix and separated from the prefix by a period.
      Throws:
      IllegalArgumentException - a key is already in use
    • setupCompleted

      protected void setupCompleted()
      Indicate that all reserved keywords have been added. If called multiple times, only the first call will have any effect.

      This should be called in a constructor.

    • showLoadDialog

      public void showLoadDialog(Component owner) throws IOException
      Load a file, chosen using a dialog, to set up this editor. This will be called before the editor's top-level window is created.
      Parameters:
      owner - the component on which any file-chooser dialog should be centered; null if there is none
      Throws:
      IOException - an IO error occurred
    • loadFile

      public void loadFile(File f) throws IOException
      Load a file to set up this editor. This will be called before the editor's top-level window is created.

      Calling this method directly will not result in the user being prompted to save changes if values are edited.

      Parameters:
      f - the file to open; null if no file should be loaded.
      Throws:
      IOException - an IO exception occurred
    • requestPassphrase

      public void requestPassphrase(Component owner)
      Request a GPG passphrase when one has not already been provided. This method will typically open a dialog box to request a GPG passphrase for decryption if the passphrase is not already known. However, if the argument is null, this method is not called from the event dispatch thread, and a system console exists, the system console will be used to obtain the passphrase.

      To use a dialog box when 'owner' is null and a console exists, use

      
       ConfigPropertyEditor cpe = ...;
       ...
       SwingUtilities.invokeAndWait(() -> {
           cpe.requestPassphrase(null);
       });
       

      NOTE: tests indicate that in Java, a system console exists only when both standard input and standard output are connected to a terminal.

      Parameters:
      owner - a component over which a dialog box should be displayed
    • requestPassphrase

      public void requestPassphrase(Component owner, boolean ignoreConsole)
      Request a GPG passphrase when one has not already been provided, optionally suppressing any use of the system console.. This method will typically open a dialog box to request a GPG passphrase for decryption if the passphrase is not already known. However, if the first argument is null, this method is not called from the event dispatch thread, and a system console exists, the system console will be used to obtain the passphrase unless the second argument true.

      To use a dialog box when 'owner' is null and a console exists, use

      
       ConfigPropertyEditor cpe = ...;
       ...
       SwingUtilities.invokeAndWait(() -> {
           cpe.requestPassphrase(null);
       });
       

      NOTE: tests indicate that in Java, a system console exists only when both standard input and standard output are connected to a terminal.

      Parameters:
      owner - a component over which a dialog box should be displayed
      ignoreConsole - true if a console should always be ignored; false otherwise
    • gpgPassphraseSupplier

      public static Supplier<char[]> gpgPassphraseSupplier(Component owner, boolean ignoreConsole)
      Create a supplier that can be used to ask for a GPG passphrase. The supplier will call requestGPGPassphrase(Component,boolean), which will typically open a dialog box to request a GPG passphrase. However, if the first argument is null, this method is not called from the event dispatch thread, and a system console exists, the system console will be used to obtain the passphrase (unless the second argument is true, in which case the current thread will block until a passphrase is provided via a dialog box or the dialog box is canceled).
      Parameters:
      owner - a component over which a dialog box should be displayed
      ignoreConsole - true if a console should always be ignored; false otherwise
      Returns:
      the supplier
      See Also:
    • requestGPGPassphrase

      public static char[] requestGPGPassphrase(Component owner, boolean ignoreConsole)
      Request a GPG passphrase. This method will typically open a dialog box to request a GPG passphrase. However, if the first argument is null, this method is not called from the event dispatch thread, and a system console exists, the system console will be used to obtain the passphrase (unless the second argument is true, in which case the current thread will block until a password is provided or the dialog box is canceled).

      To use a dialog box when 'owner' is null, the call is not from the event dispatch thread, and a console exists, use

      
       SwingUtilities.invokeAndWait(() -> {
           char[] passphrase = ConfigPropertyEditor
               .requestGPGPassphrase(null, true);
       });
       

      NOTE: tests indicate that in Java, a system console exists only when both standard input and standard output are connected to a terminal.

      Parameters:
      owner - a component over which a dialog box should be displayed
      ignoreConsole - true if a console should always be ignored; false otherwise
    • clearPassphrase

      public void clearPassphrase()
      Remove the current GPG passphrase. As a general rule, this method should be called as soon as a passphrase is no longer needed, or will not be needed for some time.
    • freezeRows

      protected void freezeRows()
      configure the table so that new rows cannot be added and rows cannot be moved or deleted.
    • setInitialExtraRows

      protected void setInitialExtraRows(int count)
      Set the number of blank rows at the end of the table. These rows are added after any rows containing reserved keys. The default is 10. Set to zero if only the reserved rows should be in the table.
      Parameters:
      count - the number of rows
    • addRE

      public void addRE(String tail, TableCellRenderer r, TableCellEditor e)
      Provide a custom table-cell renderer and/or editor for the value in the second column of some row. The key provided in column 1 of a row will be tested against the tail argument provided by this method. If there is an exact match for a key, the renderer or editor is used. Otherwise the key is replaced by the remainder of the key after its first period and the test is repeated until there is a match or the key can no longer by shorted. If there are multiple possible matches, the longest match is used. A match is based on the first argument, not the second or third.
      Parameters:
      tail - the last components of a key.
      r - the table cell renderer to use to display the value corresponding to a key; null if the normal choice is not overridden.
      e - the table cell editor to use to modify or create a value corresponding to a key; null if the normal choice is not overridden.
    • setHelpMenuItem

      public void setHelpMenuItem(JMenuItem helpMenuItem)
      Provide a menu item for displaying 'help' documentation. One should be cautious about using an instance of HelpMenuItem as the argument to this method due to this menu item's action opening a new window, which can be problematic with modal dialogs.

      If not called with a non-null argument, a Help menu will not be included.

      Parameters:
      helpMenuItem - the menuItem; null if there is not such a menu item
    • addConfigPropertyListener

      public void addConfigPropertyListener(ConfigPropertyEditor.ConfigPropertyListener l)
      Add a ConfigPropertyEditor.ConfigPropertyListener to this ConfigPropertyEditor. The listeners will be called when a monitored property's value has changed.
      Parameters:
      l - the listener to add
      See Also:
    • removeConfigPropertyListener

      public void removeConfigPropertyListener(ConfigPropertyEditor.ConfigPropertyListener l)
      Parameters:
      l - the listener to remove
    • monitorProperty

      protected void monitorProperty(String property) throws IllegalArgumentException
      Monitor a property. Registered listeners will be called when the property changes value.
      Parameters:
      property - the property to monitor
      Throws:
      IllegalArgumentException - the property's name starts with "ebase64.", indicating an encrypted value
      See Also:
    • getMonitoredPropertyValue

      protected String getMonitoredPropertyValue(String property) throws IllegalArgumentException
      Get the value for a monitored property.
      Parameters:
      property - a property that is being monitored
      Returns:
      the value for the specified property
      Throws:
      IllegalArgumentException - the key was not monitored
    • changedPropertyClears

      public void changedPropertyClears(String p, String... others)
      Indicate that if one property's value changes, various other properties should have their values set to null.

      This is useful in cases such as one property providing a file format and a second providing a file name that is required to have a particular extension.

      Parameters:
      p - a property
      others - a list of properties whose values should be set to null if property p changes
    • hasKey

      public boolean hasKey(String key)
      Test if a key exists.
      Parameters:
      key - the key
      Returns:
      true if the key exists; false otherwise
    • set

      public boolean set(Component owner, String key, String value)
      Set a key. The first argument is used when a key starts with "ebase64." as that indicates that GPG encryption will be used, in which case dialog boxes will be used to get the names of recipients. If a key already exists, its value will be overwritten.
      Parameters:
      owner - a component over which a dialog box may appear; null if a dialog box's location is not constrained
      key - the key
      value - the value for the key
      Returns:
      true if successful; false otherwise (e.g., GPG failed to encrypt, the key was missing, or the value was null)
    • prohibitEditing

      protected boolean prohibitEditing(int row, int col)
      Prevent editing of specific rows and columns. The default implementation returns false for any pair of arguments. If the value returned is dependent on a cell's row and column instead of its contents, the behavior of this class may be erratic unless those cells cannot be moved.

      This method can not override the prohibition on editing reserved keys or spacers.

      Parameters:
      row - the table row
      col - the table column
      Returns:
      true if editing is explicitly prohibited for the given row and column; false otherwise
    • edit

      public void edit(Component owner, ConfigPropertyEditor.Mode mode, Callable continuation, ConfigPropertyEditor.CloseMode quitCloseMode)
      Start the editor. This will cause a window to appear that will allow configuration parameters to be edited. Depending on the mode argument, the window may be a JFrame or a JDialog, and for a dialog, modal or modeless. One should call loadFile(File) if the parameters should be loaded from a known file or showLoadDialog(Component) if a dialog box should be used to select a file. Otherwise default values may be provided for some of the parameters.
      Parameters:
      owner - the component on which the editor should be centered; null if there is none
      mode - the window mode (ConfigPropertyEditor.Mode.JFRAME, ConfigPropertyEditor.Mode.MODAL, or ConfigPropertyEditor.Mode.MODELESS)
      continuation - a Callable that provides some code to run while or just after this editor's top-level window is closing.
      quitCloseMode - ConfigPropertyEditor.CloseMode.QUIT if the process should exit when the editor closes; ConfigPropertyEditor.CloseMode.CLOSE if the process should continue after the editor closes, or ConfigPropertyEditor.CloseMode.BOTH if both a Quit and a Close menu item should appear in the File menu.
    • getEncodedProperties

      public Properties getEncodedProperties()
      Get the encoded properties. Keys starting with 'base64.' will have their value Base-64 encoded and keys starting with 'ebase64.' will have their valued encrypted with GPG and then Base-64 encoded.
      Returns:
      the properties, encoded if necessary
    • getDecodedProperties

      public Properties getDecodedProperties()
      Get the decoded properties. Base-64 encoding will be removed for unencrypted properties and all string substitution will be performed before the results are returned. For keys starting with the component "base64", the first token and its following delimiter ("base64.") will be stripped from the key. Encrypted data is handled so as to allow the unencrypted data to be kept for as short a time as possible. For other keys, the value for a key will have parameter references replaced with the corresponding values.

      The Properties object returned by this method has been customized: for keys starting with "ebase64.", the method Properties.getProperty(String) will return the encrypted value, whereas the method get(Object) will return an Object that is actually an array of characters and that contains the decrypted data. For these keys, a new array will be returned each time Properties.get(Object) is called. This is somewhat atypical &emdash; normally Properties.getProperty(String) and Properties.get(Object) return the same object but with a different type. The rationale is that encrypted data is typically sensitive and should be removed as soon as it is no longer needed. You can overwrite a character array, but you cannot overwrite a string, which will persist until reclaimed by the garbage collector.

      NOTE: For encrypted values the method requestPassphrase(Component) should be called before a call to Properties.get(Object) if the dialog box is to be placed over a specific component.

      Returns:
      the properties
      See Also: