/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2008-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.internal.setup;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JRadioButton;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.JTextComponent;
import org.geotoolkit.internal.io.Installation;
import org.geotoolkit.nio.IOUtilities;
import org.geotoolkit.resources.Descriptions;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.resources.Vocabulary;
import static java.awt.GridBagConstraints.*;
/**
* The panel displaying a configuration form for the connection parameters to a database.
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 3.16
*
* @since 3.11 (derived from 3.00)
* @module
*/
@SuppressWarnings("serial")
abstract class DatabasePanel extends JComponent implements ActionListener {
/**
* {@link Installation#EPSG} or {@link Installation#COVERAGES} depending on
* the database we are configuring.
*/
private final Installation installation;
/**
* Button specifying whatever the embedded database should be used ("automatic")
* or an other database explicitly given by the user ("manual"). The "automatic"
* and "manual" modes are exclusive.
*/
private final JRadioButton isAutomatic, isManual;
/**
* The list of all components and their labels used for explicit configuration
* of the database connection. They will be enabled or disabled depending on
* which radio button is selected.
* <p>
* The first element shall be a {@link JComboBox} with a list of URLs.
*/
private final Field[] fields;
/**
* Describes a field (URL, username, password, etc.).
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.11
*
* @since 3.11
* @module
*/
final class Field implements ActionListener, DocumentListener {
final String propertyKey;
final JLabel label;
final JComponent component;
final String defaultValue;
private String originalValue;
private boolean isModified;
/**
* Creates a new {@code Field} instance with a label created from the given property key.
*/
Field(final String propertyKey, final short resourceKey, final Vocabulary resources,
final JComponent component, final String defaultValue)
{
this.propertyKey = propertyKey;
this.label = new JLabel(resources.getLabel(resourceKey));
this.component = component;
this.defaultValue = defaultValue;
if (component instanceof JComboBox) {
((JComboBox) component).addActionListener(this);
} else {
((JTextComponent) component).getDocument().addDocumentListener(this);
}
setOriginalValue(defaultValue);
}
/**
* Sets the original value. This is used in order to detect if the
* value edited by the user is different and needs to be saved.
*/
final void setOriginalValue(String value) {
if (value == null) {
value = "";
}
originalValue = value;
isModified = false;
}
/**
* Returns the {@linkplain #component} text. The component can be a field or a combo box.
*/
final Object getText() {
final JComponent c = component;
if (c instanceof JComboBox) {
return ((JComboBox) c).getSelectedItem();
}
return ((JTextComponent) c).getText();
}
/**
* Sets the {@linkplain #component} text. The component can be a field or a combo box.
*/
final void setText(final String value) {
final JComponent c = component;
if (c instanceof JComboBox) {
((JComboBox) c).setSelectedItem(value);
} else {
((JTextComponent) c).setText(value);
}
}
/**
* Invoked when the text changed. This method checks if the new user value is
* different than the original value, and update the "Apply" button state concequently.
*/
@Override
public void actionPerformed(ActionEvent e) {
Object value = getText();
if (value == null) {
value = "";
}
boolean mod = !value.equals(originalValue);
if (mod != isModified) {
isModified = mod;
if (mod) {
modificationCount++;
} else {
modificationCount--;
}
refresh();
}
}
@Override public void changedUpdate(DocumentEvent e) {actionPerformed(null);}
@Override public void insertUpdate(DocumentEvent e) {actionPerformed(null);}
@Override public void removeUpdate(DocumentEvent e) {actionPerformed(null);}
}
/**
* The amount of fields having a value different than the default value.
*/
int modificationCount;
/**
* {@code true} if the original file was defining the automatic mode.
* This field is always {@code false} if there is no radio buttons for
* the manual and automatic modes.
*
* @see #isAutomatic()
*/
private boolean isOriginalAutomatic;
/**
* {@code true} if the combo box contains an URL for an explicit database.
*/
private boolean hasExplicitURL;
/**
* The connection parameters as a map of properties, or {@code null} if they have not been
* loaded yet. If the embedded database is used instead of an explicit one, then this map
* is empty.
* <p>
* <strong>WARNING:</strong> contains the password. However it should not be a sensible
* password since it will be written uncrypted in a clear text file.
*/
private Properties settings;
/**
* Creates the panel.
*
* @param resources The {@code Vocabulary} instance for the locale to use.
* @param installation {@link Installation#EPSG} or {@link Installation#COVERAGES}
* depending on the database we are configuring.
* @param hasAutoChoice {@code true} for providing a choice between embedded and explicit database.
* @param fields The list of fields to provide.
*/
DatabasePanel(final Vocabulary resources, final Installation installation,
final boolean hasAutoChoice, final JButton applyButton)
{
setLayout(new GridBagLayout());
this.installation = installation;
setBorder(BorderFactory.createEmptyBorder(12, 12, 6, 12));
/*
* Layouts the radio buttons for choosing whatever the user want the
* automatic JavaDB embedded database or an explicit one.
*/
GridBagConstraints c = new GridBagConstraints();
c.insets.left=3; c.insets.right=3; c.anchor=FIRST_LINE_START;
c.gridx=0; c.gridwidth=REMAINDER;
if (hasAutoChoice) {
final Descriptions descriptions = Descriptions.getResources(resources.getLocale());
isAutomatic = new JRadioButton(descriptions.getString(Descriptions.Keys.UseEosDatabase_1, 0));
isManual = new JRadioButton(descriptions.getString(Descriptions.Keys.UseEosDatabase_1, 1));
isAutomatic.addActionListener(this);
isManual .addActionListener(this);
final ButtonGroup group = new ButtonGroup();
group.add(isAutomatic);
group.add(isManual);
c.gridy=0; add(isAutomatic, c);
c.gridy++; add(isManual, c);
} else {
c.gridy = -1;
isAutomatic = null;
isManual = null;
}
/*
* Layouts the fields for configuring explicitly the connection parameters.
*/
c.fill = BOTH;
c.gridwidth = 1;
fields = getFields(resources);
for (final Field field : fields) {
c.gridy++;
c.gridx=1; c.weightx=0; c.insets.left=30; add(field.label, c);
c.gridx=2; c.weightx=1; c.insets.left= 3; add(field.component, c);
}
applyButton.addActionListener(new ActionListener() {
@Override public void actionPerformed(final ActionEvent event) {
save();
}
});
addComponentListener(new LoadWhenShown());
}
/**
* Shall be invoked by subclasses for providing the database fields. This is invoked by
* the constructor only. We can not let subclasses define their own static method because
* the {@link Field} constructor needs access to {@code this}.
*/
abstract Field[] getFields(Vocabulary resources);
/**
* Returns {@code true} if this form has some fields which need to be saved.
*/
final boolean hasModifications() {
if (settings == null) {
return false;
}
final boolean isAutomatic = isAutomatic();
if (isAutomatic != isOriginalAutomatic) {
return true;
}
return !isAutomatic && modificationCount != 0;
}
/**
* Returns {@code true} if the "automatic" button radio is selected. This method returns
* always {@code false} if there is no automatic/manual button radio.
*/
private boolean isAutomatic() {
return isAutomatic != null && isAutomatic.isSelected();
}
/**
* Invoked when the user change the "automatic" or "manual" mode. This method will enable
* or disable the fields where are specified the connection parameters.
*/
@Override
public void actionPerformed(final ActionEvent event) {
final boolean manual = !isAutomatic();
for (final Field field : fields) {
field.label.setEnabled(manual);
field.component.setEnabled(manual);
final String value;
if (manual) {
value = settings.getProperty(field.propertyKey);
} else {
value = field.defaultValue;
}
field.setText(value);
}
final JComboBox<?> url = (JComboBox<?>) fields[0].component;
url.setSelectedIndex(manual || !hasExplicitURL ? 0 : 1);
refresh();
}
/**
* Refresh the state of the "Apply" button.
*/
final void refresh() {
((DatabasePanels) getParent().getParent()).refresh();
}
/**
* Loads the data only when first needed, which may never happen. We will load those
* data only once. One advantage of this deferred loading mechanism is to popup the
* error dialog box (if they was an I/O error) only if the user actually wanted to
* see this widget.
*/
private class LoadWhenShown extends ComponentAdapter {
@Override public void componentShown(final ComponentEvent e) {
removeComponentListener(this);
if (settings == null) {
load();
}
}
}
/**
* If the properties file has not yet been read, reads it now.
* Then, return its content (never {@code null}).
*/
final Properties getSettings() {
for (final ComponentListener listener : getComponentListeners()) {
if (listener instanceof LoadWhenShown) {
listener.componentShown(null);
break;
}
}
return settings;
}
/**
* Loads the settings from the {@value #CONFIGURATION_FILE} property file.
* The values will be stored in the GUI fields. This method is invoked only once.
*/
private void load() {
Properties settings;
try {
settings = installation.getDataSource();
} catch (IOException ex) {
error(Errors.Keys.CantReadFile_1, ex);
return;
}
final boolean manual = (settings != null);
if (!manual) {
settings = new Properties();
for (final Field field : fields) {
final String defaultValue = field.defaultValue;
if (defaultValue != null) {
settings.setProperty(field.propertyKey, defaultValue);
}
}
} else {
/*
* If there is an URL in the property file, add it at the beginning
* of the URL combo box.
*/
final Field urlField = fields[0];
final String value = settings.getProperty(urlField.propertyKey);
if (value != null) {
@SuppressWarnings("unchecked")
final JComboBox<String> url = (JComboBox<String>) urlField.component;
final DefaultComboBoxModel<String> model = (DefaultComboBoxModel<String>) url.getModel();
if (!value.equals(model.getElementAt(0))) {
model.insertElementAt(value, 0);
hasExplicitURL = true;
}
}
/*
* Remember the original values. This will be used in order to detect
* if a value has been edited, and consequently if we should enable or
* disable the "Apply" button.
*/
for (final Field field : fields) {
field.setOriginalValue(settings.getProperty(field.propertyKey));
}
}
this.settings = settings; // Set only on success.
final JRadioButton mode = manual ? isManual : isAutomatic;
if (mode != null) {
mode.setSelected(true);
isOriginalAutomatic = !manual;
}
actionPerformed(null); // Refresh the fields.
}
/**
* Saves the values from the GUI fields to the {@value #CONFIGURATION_FILE} file,
* or delete the file if the user selected the embedded database. This method is
* invoked every time the "Apply" button has been pushed.
*/
private void save() {
if (!hasModifications()) {
// Does not save the file if there is no modification, in order to preserve the
// file date and time information. Also in order to preserve any manual edition
// that the user could have done in the file.
return;
}
final Properties settings = this.settings;
if (isAutomatic()) {
final Path file = installation.directory(true).resolve(Installation.DATASOURCE_FILE);
try {
Files.delete(file);
} catch (IOException e) {
//ignore error
e.printStackTrace();
}
settings.clear();
if (isAutomatic != null) {
isOriginalAutomatic = true;
// Shall stay 'false' if there is no automatic/manual radio buttons.
}
} else {
for (final Field field : fields) {
setProperty(settings, field.propertyKey, field.getText());
}
if (!settings.isEmpty()) {
// Get the name to put in the property file comment line.
String name = installation.name();
for (final String id : DatabasePanels.CONNECTION_PANELS) {
if (id.equalsIgnoreCase(name)) {
name = id;
break;
}
}
try {
final Path file = installation.validDirectory(true).resolve(Installation.DATASOURCE_FILE);
IOUtilities.storeProperties(settings, file, "Connection parameters to the " + name + " database");
} catch (IOException ex) {
error(Errors.Keys.CantWriteFile_1, ex);
return;
}
}
isOriginalAutomatic = false;
}
// Remember the new field values only on success.
for (final Field field : fields) {
field.setOriginalValue(settings.getProperty(field.propertyKey));
}
modificationCount = 0;
refresh();
}
/**
* Adds the given (key, value) pair in the properties map,
* providing that the value is not null and non-empty.
*/
private static void setProperty(final Properties settings, final String key, final Object value) {
if (value != null) {
final String text = value.toString().trim();
if (!text.isEmpty()) {
settings.setProperty(key, text);
return;
}
}
settings.remove(key);
}
/**
* Displays an error message.
*
* @param key Whatever we are reading of writing the file, as a resource key.
*/
private void error(final short key, final IOException ex) {
JOptionPane.showMessageDialog(this, ex.getLocalizedMessage(),
Errors.format(key, Installation.DATASOURCE_FILE), JOptionPane.ERROR_MESSAGE);
}
}