/**
* GmailAssistant 1.1 (2008-03-16)
* Copyright 2008 Zach Scrivena
* zachscrivena@gmail.com
* http://gmailassistant.sourceforge.net/
*
* Notifier for multiple Gmail accounts.
*
* TERMS AND CONDITIONS:
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package gmailassistant;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.event.DocumentEvent;
import javax.swing.filechooser.FileNameExtensionFilter;
/**
* "Load Profile" form.
*/
class ProfileLoader
extends JFrame
{
/** parent GmailAssistant object */
private final GmailAssistant parent;
/** file chooser for saving profile file */
private final JFileChooser fileChooser;
/** selected profile files to be loaded */
private volatile File[] files = null;
/** index of the currently selected profile file to be loaded */
private int filesIndex = 0;
/**
* Constructor.
*
* @param parent
* parent GmailAssistant object
*/
ProfileLoader(
final GmailAssistant parent)
{
/*********************
* INITIALIZE FIELDS *
*********************/
this.parent = parent;
/* get current directory */
File currentDirectory = new File(".");
try
{
currentDirectory = currentDirectory.getCanonicalFile();
}
catch (Exception e)
{
/* ignore */
}
/* file chooser */
this.fileChooser = new JFileChooser(currentDirectory);
this.fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
this.fileChooser.setMultiSelectionEnabled(true);
this.fileChooser.addChoosableFileFilter(new FileNameExtensionFilter(
GmailAssistant.NAME + " Profile (*." + GmailAssistant.PROFILE_FILENAME_EXTENSION + ")",
GmailAssistant.PROFILE_FILENAME_EXTENSION));
/******************************
* INITIALIZE FORM COMPONENTS *
******************************/
initComponents();
/***************************
* CONFIGURE FORM SETTINGS *
***************************/
setTitle("Load Profiles - " + this.parent.getTitle());
addWindowListener(new WindowAdapter()
{
@Override
public void windowClosing(WindowEvent e)
{
ProfileLoader.this.cancelButton.doClick();
}
});
/* inherit "always on top" behavior of parent */
try
{
setAlwaysOnTop(this.parent.isAlwaysOnTop());
}
catch (Exception e)
{
/* ignore */
}
/* inherit program icon of parent */
final List<Image> icons = this.parent.getIconImages();
if (!icons.isEmpty())
{
setIconImage(icons.get(0));
}
/* field: "Profile Password" */
this.passwordField.getDocument().addDocumentListener(new DocumentListenerAdapter()
{
@Override
public void insertUpdate(DocumentEvent e)
{
checkForm();
}
@Override
public void removeUpdate(DocumentEvent e)
{
checkForm();
}
@Override
public void changedUpdate(DocumentEvent e)
{
checkForm();
}
});
/* button: "OK" */
this.okButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
proceedLoadProfile();
}
});
/* button: "Cancel" */
this.cancelButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
cancelLoadProfile();
}
});
/* add standard editing popup menu to text fields */
SwingManipulator.addStandardEditingPopupMenu(new JTextField[]
{
this.passwordField,
});
/* key binding: ENTER key */
this.scrollPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "ENTER_OK_BUTTON");
this.scrollPane.getActionMap().put("ENTER_OK_BUTTON", new AbstractAction()
{
public void actionPerformed(ActionEvent e)
{
ProfileLoader.this.okButton.doClick();
}
});
/* key binding: ESCAPE key */
this.scrollPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "ESCAPE_CANCEL_BUTTON");
this.scrollPane.getActionMap().put("ESCAPE_CANCEL_BUTTON", new AbstractAction()
{
public void actionPerformed(ActionEvent e)
{
ProfileLoader.this.cancelButton.doClick();
}
});
/* center form on the parent form */
setLocationRelativeTo(this.parent);
}
/**
* Check selections specified by the user.
* This method must run on the EDT.
*/
private void checkForm()
{
String error = null;
if (error != null)
{
this.loadError.setText("<html><font color='red'>" + error + "</font></html>");
this.okButton.setEnabled(false);
return;
}
this.loadError.setText(" ");
this.okButton.setEnabled(true);
}
/**
* Proceed to load currently selected profile file using the
* user-specified settings on the form.
* This method must run on the EDT.
*/
private void proceedLoadProfile()
{
this.okButton.setEnabled(false);
this.cancelButton.setEnabled(false);
if (this.files == null)
{
this.setVisible(false);
return;
}
File f;
synchronized (this.files)
{
if ((this.filesIndex >= 0) && (this.filesIndex < this.files.length))
{
f = this.files[this.filesIndex];
}
else
{
this.setVisible(false);
return;
}
}
this.loadError.setText("<html><font color='blue'>Loading profile data...</font></html>");
/* read encrypted profile data from binary file */
byte[] salt = new byte[Encryptor.SALT_LENGTH];
byte[] ciphertextBytes = new byte[0];
try
{
final FileInputStream fis = new FileInputStream(f);
final int length = (int) f.length();
if (length < Encryptor.SALT_LENGTH)
{
throw new Exception("Invalid profile file length.");
}
this.loadError.setText("<html><font color='blue'>Loading profile data: Reading file...</font></html>");
/* read salt */
Arrays.fill(salt, (byte) 0x00);
int offset = 0;
while (offset < Encryptor.SALT_LENGTH)
{
final int n = fis.read(salt, offset, Encryptor.SALT_LENGTH - offset);
if (n == -1)
{
break;
}
offset += n;
}
/* read ciphertext */
ciphertextBytes = new byte[length - Encryptor.SALT_LENGTH];
Arrays.fill(ciphertextBytes, (byte) 0x00);
offset = 0;
while (offset < (length - Encryptor.SALT_LENGTH))
{
final int n = fis.read(ciphertextBytes, offset, length - Encryptor.SALT_LENGTH - offset);
if (n == -1)
{
break;
}
offset += n;
}
fis.close();
}
catch (Exception e)
{
Arrays.fill(ciphertextBytes, (byte) 0x00);
Arrays.fill(salt, (byte) 0x00);
this.loadError.setText("<html><font color='red'>Failed to load profile data</font></html>");
SwingManipulator.showErrorDialog(this, "Load Profiles - " + GmailAssistant.NAME,
"Failed to read profile data from file \"" + f.getPath() + "\": " + e +
"\nPlease check that the file can be read from.");
this.okButton.setEnabled(true);
this.cancelButton.setEnabled(true);
this.passwordField.selectAll();
this.passwordField.requestFocus();
return;
}
/* get user-specified password */
final char[] password = SwingManipulator.getPasswordJPasswordField(this.passwordField);
/* decrypt profile data */
byte[] cleartextBytes = new byte[0];
String[] cleartext = null;
this.loadError.setText("<html><font color='blue'>Loading profile data: Decrypting file...</font></html>");
try
{
cleartextBytes = Encryptor.decrypt(salt, String.valueOf(password), ciphertextBytes);
Arrays.fill(password, '\0');
Arrays.fill(ciphertextBytes, (byte) 0x00);
Arrays.fill(salt, (byte) 0x00);
cleartext = Encryptor.byteArrayToStringArray(cleartextBytes, GmailAssistant.PROPERTY_DELIMITER);
Arrays.fill(cleartextBytes, (byte) 0x00);
}
catch (Exception e)
{
Arrays.fill(password, '\0');
Arrays.fill(ciphertextBytes, (byte) 0x00);
Arrays.fill(salt, (byte) 0x00);
Arrays.fill(cleartextBytes, (byte) 0x00);
this.loadError.setText("<html><font color='red'>Failed to load profile data</font></html>");
SwingManipulator.showErrorDialog(this, "Load Profiles - " + GmailAssistant.NAME,
"Failed to decrypt profile data. Please check that the provided password is correct.");
this.okButton.setEnabled(true);
this.cancelButton.setEnabled(true);
this.passwordField.selectAll();
this.passwordField.requestFocus();
return;
}
this.loadError.setText("<html><font color='blue'>Loading profile data: Parsing file...</font></html>");
final List<Map<Account.Property,Object>> allAccountProperties = new ArrayList<Map<Account.Property,Object>>();
Map<Account.Property,Object> accountProperties = null;
String username = null;
final List<String> errors = new ArrayList<String>();
/* parse each line in the profile file */
for (String s : cleartext)
{
if (s.isEmpty() || s.startsWith("#"))
{
/* ignore empty lines and comments */
}
else if (s.startsWith("[") && s.endsWith("]"))
{
/* start recording properties for a new account */
username = s.substring(1, s.length() - 1);
accountProperties = Account.getNewDefaultProperties();
Account.setProperty(accountProperties, Account.Property.USERNAME, Account.propertyStringToValue(Account.Property.USERNAME, username));
allAccountProperties.add(accountProperties);
}
else if (s.contains(":"))
{
if (accountProperties == null)
{
/* program property */
final String keyString = s.substring(0, s.indexOf(":")).trim();
final GmailAssistant.Property p = GmailAssistant.KEY_STRING_TO_PROPERTY.get(keyString);
if (p == null)
{
errors.add("Failed to recognize program property key \"" + keyString + "\" in \"" + s + "\".");
}
else
{
final String valString = s.substring(s.indexOf(":") + 1);
final Object val = GmailAssistant.propertyStringToValue(p, valString);
if (val == null)
{
errors.add("Failed to parse program property value \"" + valString + "\" in \"" + s + "\".");
}
else
{
this.parent.setProperty(p, val);
}
}
}
else
{
/* account property */
final String keyString = s.substring(0, s.indexOf(":")).trim();
final Account.Property p = Account.KEY_STRING_TO_PROPERTY.get(keyString);
if (p == null)
{
errors.add("Failed to recognize account property key \"" + keyString + "\" in \"" + s + "\".");
}
else
{
final String valString = s.substring(s.indexOf(":") + 1);
final Object val = Account.propertyStringToValue(p, valString);
if (val == null)
{
errors.add("Failed to parse account property value \"" + valString + "\" in \"" + s + "\".");
}
else
{
Account.setProperty(accountProperties, p, val);
}
}
}
}
}
this.loadError.setText("<html><font color='blue'>Loading profile data: Done!</font></html>");
/* report any errors */
if (!errors.isEmpty())
{
this.loadError.setText("<html><font color='blue'>Loading profile data: Done! (errors encountered)</font></html>");
final StringBuilder sb = new StringBuilder();
for (String s : errors)
{
sb.append("\n\n");
sb.append(s);
}
SwingManipulator.showErrorDialog(this, "Load Profiles - " + GmailAssistant.NAME,
GmailAssistant.NAME + " encountered the following " +
((errors.size() == 1) ? "error" : "errors") +
" when parsing profile file \"" + f.getPath() + "\":" +
sb.toString());
}
/* repopulate "Options" form */
if (this.parent.options != null)
{
this.parent.options.repopulateOptions();
}
/* add new accounts loaded from the file */
for (Map<Account.Property,Object> p : allAccountProperties)
{
this.parent.addNewAccount(p);
}
/* load the next profile file */
loadNextProfile();
}
/**
* Cancel loading of the currently selected profile file.
* This method must run on the EDT.
*/
private void cancelLoadProfile()
{
/* load the next profile file */
loadNextProfile();
}
/**
* Present a file chooser for the user to select the profile files for loading.
* This method must run on the EDT.
*/
void showForm()
{
final int val = this.fileChooser.showOpenDialog(this);
if (val == JFileChooser.APPROVE_OPTION)
{
this.files = this.fileChooser.getSelectedFiles();
synchronized (this.files)
{
this.filesIndex = -1;
}
loadNextProfile();
}
}
/**
* Load the specified profile files.
* This method must run on the EDT.
*
* @param files
* profile files to be loaded
*/
void loadProfiles(
final File[] files)
{
this.files = files;
synchronized (this.files)
{
this.filesIndex = -1;
}
loadNextProfile();
}
/**
* Load the next profile file that was selected.
* This method must run on the EDT.
*/
void loadNextProfile()
{
if (this.files == null)
{
this.setVisible(false);
return;
}
File f;
synchronized (this.files)
{
while (true)
{
this.filesIndex++;
if ((this.filesIndex >= 0) && (this.filesIndex < this.files.length))
{
f = this.files[this.filesIndex];
if (f.isFile())
{
break;
}
else
{
SwingManipulator.showErrorDialog(this, "Load Profiles - " + GmailAssistant.NAME,
"The file \"" + f.getPath() + "\" does not exist.");
}
}
else
{
this.setVisible(false);
return;
}
}
this.loadTitle.setText("Loading profile " +
((this.files.length > 1) ?
((this.filesIndex + 1) + " of " + this.files.length) : ""));
this.loadFilename.setText(f.getPath());
}
/* update and show form for user to specify profile password */
this.passwordField.setText("");
this.loadError.setText(" ");
this.okButton.setEnabled(true);
this.cancelButton.setEnabled(true);
this.passwordField.selectAll();
this.passwordField.requestFocus();
this.setVisible(true);
this.setExtendedState(JFrame.NORMAL);
this.toFront();
}
/***************************
* NETBEANS-GENERATED CODE *
***************************/
/** This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents()
{
buttonsPanel = new javax.swing.JPanel();
okButton = new javax.swing.JButton();
cancelButton = new javax.swing.JButton();
loadTitle = new javax.swing.JLabel();
loadFilename = new javax.swing.JLabel();
scrollPane = new javax.swing.JScrollPane();
panel = new javax.swing.JPanel();
loadPanel = new javax.swing.JPanel();
passwordLabel = new javax.swing.JLabel();
passwordField = new javax.swing.JPasswordField();
lock = new javax.swing.JLabel();
loadError = new javax.swing.JLabel();
setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE);
buttonsPanel.setLayout(new java.awt.GridLayout(1, 2));
okButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/gmailassistant/resources/tick.png"))); // NOI18N
okButton.setMnemonic('O');
okButton.setText("OK");
okButton.setToolTipText("Load profile data from the specified file");
buttonsPanel.add(okButton);
cancelButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/gmailassistant/resources/cross.png"))); // NOI18N
cancelButton.setMnemonic('C');
cancelButton.setText("Cancel");
cancelButton.setToolTipText("Cancel loading of profile data");
buttonsPanel.add(cancelButton);
loadTitle.setText("Loading profile");
loadTitle.setVerticalAlignment(javax.swing.SwingConstants.TOP);
loadFilename.setFont(new java.awt.Font("Tahoma", 1, 11));
loadFilename.setText("C:\\Path\\To\\File");
loadFilename.setVerticalAlignment(javax.swing.SwingConstants.TOP);
scrollPane.setBorder(null);
scrollPane.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
panel.setLayout(new javax.swing.BoxLayout(panel, javax.swing.BoxLayout.Y_AXIS));
loadPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(""));
passwordLabel.setDisplayedMnemonic('p');
passwordLabel.setLabelFor(passwordField);
passwordLabel.setText("Profile password:");
passwordLabel.setToolTipText("Password to be used for decrypting the profile data");
passwordField.setToolTipText("Password to be used for decrypting the profile data");
lock.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
lock.setIcon(new javax.swing.ImageIcon(getClass().getResource("/gmailassistant/resources/lock.png"))); // NOI18N
lock.setToolTipText("GmailAssistant encrypts your profile data using AES-128 encryption");
loadError.setText("<html><font color='red'>load error</font></html>");
loadError.setVerticalAlignment(javax.swing.SwingConstants.TOP);
javax.swing.GroupLayout loadPanelLayout = new javax.swing.GroupLayout(loadPanel);
loadPanel.setLayout(loadPanelLayout);
loadPanelLayout.setHorizontalGroup(
loadPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(loadPanelLayout.createSequentialGroup()
.addContainerGap()
.addGroup(loadPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(loadPanelLayout.createSequentialGroup()
.addComponent(passwordLabel)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(passwordField, javax.swing.GroupLayout.DEFAULT_SIZE, 165, Short.MAX_VALUE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(lock))
.addComponent(loadError, javax.swing.GroupLayout.DEFAULT_SIZE, 272, Short.MAX_VALUE))
.addContainerGap())
);
loadPanelLayout.setVerticalGroup(
loadPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(loadPanelLayout.createSequentialGroup()
.addContainerGap()
.addGroup(loadPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(loadPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(passwordLabel)
.addComponent(passwordField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
.addComponent(lock))
.addGap(18, 18, 18)
.addComponent(loadError, javax.swing.GroupLayout.DEFAULT_SIZE, 38, Short.MAX_VALUE))
);
panel.add(loadPanel);
scrollPane.setViewportView(panel);
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
.addComponent(scrollPane, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 320, Short.MAX_VALUE)
.addComponent(buttonsPanel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 320, Short.MAX_VALUE)
.addComponent(loadTitle, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 320, Short.MAX_VALUE)
.addComponent(loadFilename, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 320, Short.MAX_VALUE))
.addContainerGap())
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addContainerGap()
.addComponent(loadTitle)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(loadFilename)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
.addComponent(scrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 99, Short.MAX_VALUE)
.addPreferredGap(javax.swing.LayoutStyle<