Merge branch 'mavenize' into next
authorDavid ‘Bombe’ Roden <bombe@freenetproject.org>
Tue, 28 Aug 2012 08:52:01 +0000 (10:52 +0200)
committerDavid ‘Bombe’ Roden <bombe@freenetproject.org>
Tue, 28 Aug 2012 08:52:01 +0000 (10:52 +0200)
14 files changed:
1  2 
src/main/java/de/todesbaum/jsite/application/KeyDialog.java
src/main/java/de/todesbaum/jsite/application/Project.java
src/main/java/de/todesbaum/jsite/application/ProjectInserter.java
src/main/java/de/todesbaum/jsite/application/UpdateChecker.java
src/main/java/de/todesbaum/jsite/gui/FileScanner.java
src/main/java/de/todesbaum/jsite/gui/NodeManagerPage.java
src/main/java/de/todesbaum/jsite/gui/PreferencesPage.java
src/main/java/de/todesbaum/jsite/gui/ProjectFilesPage.java
src/main/java/de/todesbaum/jsite/gui/ProjectInsertPage.java
src/main/java/de/todesbaum/jsite/gui/ProjectPage.java
src/main/java/de/todesbaum/jsite/i18n/I18nContainer.java
src/main/java/de/todesbaum/jsite/main/CLI.java
src/main/java/de/todesbaum/jsite/main/Main.java
src/main/java/de/todesbaum/jsite/main/Version.java

index 0000000,4b64f72..cd628e7
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,318 +1,322 @@@
+ /*
+  * jSite - KeyDialog.java - Copyright © 2010–2012 David Roden
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * 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, write to the Free Software
+  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+  */
+ package de.todesbaum.jsite.application;
+ import java.awt.BorderLayout;
+ import java.awt.Dimension;
+ import java.awt.FlowLayout;
+ import java.awt.GridBagConstraints;
+ import java.awt.GridBagLayout;
+ import java.awt.Insets;
+ import java.awt.Toolkit;
+ import java.awt.event.ActionEvent;
+ import java.awt.event.InputEvent;
+ import java.awt.event.KeyEvent;
+ import java.awt.event.WindowAdapter;
+ import java.awt.event.WindowEvent;
+ import java.io.IOException;
+ import java.text.MessageFormat;
+ import javax.swing.AbstractAction;
+ import javax.swing.Action;
+ import javax.swing.BorderFactory;
+ import javax.swing.JButton;
+ import javax.swing.JDialog;
+ import javax.swing.JFrame;
+ import javax.swing.JLabel;
+ import javax.swing.JOptionPane;
+ import javax.swing.JPanel;
+ import javax.swing.JSeparator;
+ import javax.swing.JTextField;
+ import javax.swing.KeyStroke;
+ import javax.swing.SwingConstants;
+ import de.todesbaum.jsite.i18n.I18n;
+ import de.todesbaum.jsite.i18n.I18nContainer;
+ /**
+  * A dialog that lets the user edit the private and public key for a project.
+  *
+  * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
+  */
+ public class KeyDialog extends JDialog {
+       /** Interface to the freenet node. */
+       private final Freenet7Interface freenetInterface;
+       /** The public key. */
+       private String publicKey;
+       /** The private key. */
+       private String privateKey;
+       /** The “OK” button’s action. */
+       private Action okAction;
+       /** The “Cancel” button’s action. */
+       private Action cancelAction;
+       /** The “Regenerate” button’s action. */
+       private Action generateAction;
+       /** The text field for the private key. */
+       private JTextField privateKeyTextField;
+       /** The text field for the public key. */
+       private JTextField publicKeyTextField;
+       /** Whether the dialog was cancelled. */
+       private boolean cancelled;
+       /**
+        * Creates a new key dialog.
+        *
+        * @param freenetInterface
+        *            Interface to the freenet node
+        * @param parent
+        *            The parent frame
+        */
+       public KeyDialog(Freenet7Interface freenetInterface, JFrame parent) {
+               super(parent, I18n.getMessage("jsite.key-dialog.title"), true);
+               this.freenetInterface = freenetInterface;
+               addWindowListener(new WindowAdapter() {
+                       @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void windowClosing(WindowEvent windowEvent) {
+                               actionCancel();
+                       }
+               });
+               initDialog();
+       }
+       //
+       // ACCESSORS
+       //
+       /**
+        * Returns whether the dialog was cancelled.
+        *
+        * @return {@code true} if the dialog was cancelled, {@code false} otherwise
+        */
+       public boolean wasCancelled() {
+               return cancelled;
+       }
+       /**
+        * Returns the public key.
+        *
+        * @return The public key
+        */
+       public String getPublicKey() {
+               return publicKey;
+       }
+       /**
+        * Sets the public key.
+        *
+        * @param publicKey
+        *            The public key
+        */
+       public void setPublicKey(String publicKey) {
+               this.publicKey = publicKey;
+               publicKeyTextField.setText(publicKey);
+               pack();
+       }
+       /**
+        * Returns the private key.
+        *
+        * @return The private key
+        */
+       public String getPrivateKey() {
+               return privateKey;
+       }
+       /**
+        * Sets the private key.
+        *
+        * @param privateKey
+        *            The private key
+        */
+       public void setPrivateKey(String privateKey) {
+               this.privateKey = privateKey;
+               privateKeyTextField.setText(privateKey);
+               pack();
+       }
+       //
+       // ACTIONS
+       //
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void pack() {
+               super.pack();
+               Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
+               setLocation((screenSize.width - getWidth()) / 2, (screenSize.height - getHeight()) / 2);
+       }
+       //
+       // PRIVATE METHODS
+       //
+       /**
+        * Creates all necessary actions.
+        */
+       private void createActions() {
+               okAction = new AbstractAction(I18n.getMessage("jsite.general.ok")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               actionOk();
+                       }
+               };
+               okAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.key-dialog.button.ok.tooltip"));
+               okAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_ENTER);
+               cancelAction = new AbstractAction(I18n.getMessage("jsite.general.cancel")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               actionCancel();
+                       }
+               };
+               cancelAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.key-dialog.button.cancel.tooltip"));
+               cancelAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_ESCAPE);
+               generateAction = new AbstractAction(I18n.getMessage("jsite.key-dialog.button.generate")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               actionGenerate();
+                       }
+               };
+               generateAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.key-dialog.button.generate.tooltip"));
+               generateAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.CTRL_DOWN_MASK));
+       }
+       /**
+        * Initializes the dialog and all its components.
+        */
+       private void initDialog() {
+               createActions();
+               JPanel dialogPanel = new JPanel(new BorderLayout(12, 12));
+               dialogPanel.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
+               JPanel contentPanel = new JPanel(new GridBagLayout());
+               dialogPanel.add(contentPanel, BorderLayout.CENTER);
+               final JLabel keysLabel = new JLabel(I18n.getMessage("jsite.key-dialog.label.keys"));
+               contentPanel.add(keysLabel, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
+               final JLabel privateKeyLabel = new JLabel(I18n.getMessage("jsite.key-dialog.label.private-key"));
+               contentPanel.add(privateKeyLabel, new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(12, 18, 0, 0), 0, 0));
+               privateKeyTextField = new JTextField();
+               contentPanel.add(privateKeyTextField, new GridBagConstraints(1, 1, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(6, 12, 0, 0), 0, 0));
+               final JLabel publicKeyLabel = new JLabel(I18n.getMessage("jsite.key-dialog.label.public-key"));
+               contentPanel.add(publicKeyLabel, new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 18, 0, 0), 0, 0));
+               publicKeyTextField = new JTextField();
+               contentPanel.add(publicKeyTextField, new GridBagConstraints(1, 2, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(6, 12, 0, 0), 0, 0));
+               final JLabel actionsLabel = new JLabel(I18n.getMessage("jsite.key-dialog.label.actions"));
+               contentPanel.add(actionsLabel, new GridBagConstraints(0, 3, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(12, 0, 0, 0), 0, 0));
+               JPanel actionButtonPanel = new JPanel(new FlowLayout(FlowLayout.LEADING, 12, 12));
+               actionButtonPanel.setBorder(BorderFactory.createEmptyBorder(-12, -12, -12, -12));
+               contentPanel.add(actionButtonPanel, new GridBagConstraints(0, 4, 2, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(12, 18, 0, 0), 0, 0));
+               actionButtonPanel.add(new JButton(generateAction));
+               JPanel separatorPanel = new JPanel(new BorderLayout(12, 12));
+               dialogPanel.add(separatorPanel, BorderLayout.PAGE_END);
+               separatorPanel.add(new JSeparator(SwingConstants.HORIZONTAL), BorderLayout.PAGE_START);
+               JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING, 12, 12));
+               buttonPanel.setBorder(BorderFactory.createEmptyBorder(-12, -12, -12, -12));
+               separatorPanel.add(buttonPanel, BorderLayout.CENTER);
+               buttonPanel.add(new JButton(okAction));
+               buttonPanel.add(new JButton(cancelAction));
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
++                      @Override
+                       public void run() {
+                               keysLabel.setText(I18n.getMessage("jsite.key-dialog.label.keys"));
+                               privateKeyLabel.setText(I18n.getMessage("jsite.key-dialog.label.private-key"));
+                               publicKeyLabel.setText(I18n.getMessage("jsite.key-dialog.label.public-key"));
+                               actionsLabel.setText(I18n.getMessage("jsite.key-dialog.label.actions"));
+                       }
+               });
+               getContentPane().add(dialogPanel, BorderLayout.CENTER);
+               pack();
+               setResizable(false);
+       }
+       //
+       // PRIVATE ACTIONS
+       //
+       /**
+        * Quits the dialog, accepting all changes.
+        */
+       private void actionOk() {
+               publicKey = publicKeyTextField.getText();
+               privateKey = privateKeyTextField.getText();
+               cancelled = false;
+               setVisible(false);
+       }
+       /**
+        * Quits the dialog, discarding all changes.
+        */
+       private void actionCancel() {
+               cancelled = true;
+               setVisible(false);
+       }
+       /**
+        * Generates a new key pair.
+        */
+       private void actionGenerate() {
+               if (JOptionPane.showConfirmDialog(this, I18n.getMessage("jsite.project.warning.generate-new-key"), null, JOptionPane.OK_CANCEL_OPTION) == JOptionPane.CANCEL_OPTION) {
+                       return;
+               }
+               String[] keyPair = null;
+               try {
+                       keyPair = freenetInterface.generateKeyPair();
+               } catch (IOException ioe1) {
+                       JOptionPane.showMessageDialog(this, MessageFormat.format(I18n.getMessage("jsite.project.keygen.io-error"), ioe1.getMessage()), null, JOptionPane.ERROR_MESSAGE);
+                       return;
+               }
+               publicKeyTextField.setText(keyPair[1].substring(keyPair[1].indexOf('@') + 1, keyPair[1].lastIndexOf('/')));
+               privateKeyTextField.setText(keyPair[0].substring(keyPair[0].indexOf('@') + 1, keyPair[0].lastIndexOf('/')));
+               pack();
+       }
+ }
index 0000000,33d8447..36fff55
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,441 +1,442 @@@
 -      private String shortenURI(String uri) {
+ /*
+  * jSite - Project.java - Copyright © 2006–2012 David Roden
+  *
+  * This program is free software; you can redistribute it and/or modify it under
+  * the terms of the GNU General Public License as published by the Free Software
+  * Foundation; either version 2 of the License, or (at your option) any later
+  * version.
+  *
+  * 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, write to the Free Software Foundation, Inc., 59 Temple
+  * Place - Suite 330, Boston, MA 02111-1307, USA.
+  */
+ package de.todesbaum.jsite.application;
+ import java.io.File;
+ import java.util.Collections;
+ import java.util.HashMap;
+ import java.util.Map;
+ import java.util.Map.Entry;
+ import net.pterodactylus.util.io.MimeTypes;
+ /**
+  * Container for project information.
+  *
+  * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
+  */
+ public class Project implements Comparable<Project> {
+       /** The name of the project. */
+       protected String name;
+       /** The description of the project. */
+       protected String description;
+       /** The insert URI of the project. */
+       protected String insertURI;
+       /** The request URI of the project. */
+       protected String requestURI;
+       /** The index file of the project. */
+       protected String indexFile;
+       /** The local path of the project. */
+       protected String localPath;
+       /** The remote path of the URI. */
+       protected String path;
+       /** The time of the last insertion. */
+       protected long lastInsertionTime;
+       /** The edition to insert to. */
+       protected int edition;
+       /** Whether to ignore hidden directory. */
+       private boolean ignoreHiddenFiles;
+       /** Options for files. */
+       protected Map<String, FileOption> fileOptions = new HashMap<String, FileOption>();
+       /**
+        * Empty constructor.
+        */
+       public Project() {
+               /* do nothing. */
+       }
+       /**
+        * Creates a new project from an existing one.
+        *
+        * @param project
+        *            The project to clone
+        */
+       public Project(Project project) {
+               name = project.name;
+               description = project.description;
+               insertURI = project.insertURI;
+               requestURI = project.requestURI;
+               path = project.path;
+               edition = project.edition;
+               localPath = project.localPath;
+               indexFile = project.indexFile;
+               lastInsertionTime = project.lastInsertionTime;
+               ignoreHiddenFiles = project.ignoreHiddenFiles;
+               fileOptions = new HashMap<String, FileOption>(project.fileOptions);
+       }
+       /**
+        * Returns the name of the project.
+        *
+        * @return The name of the project
+        */
+       public String getName() {
+               return name;
+       }
+       /**
+        * Sets the name of the project.
+        *
+        * @param name
+        *            The name of the project
+        */
+       public void setName(String name) {
+               this.name = name;
+       }
+       /**
+        * Returns the description of the project.
+        *
+        * @return The description of the project
+        */
+       public String getDescription() {
+               return description;
+       }
+       /**
+        * Sets the description of the project.
+        *
+        * @param description
+        *            The description of the project
+        */
+       public void setDescription(String description) {
+               this.description = description;
+       }
+       /**
+        * Returns the local path of the project.
+        *
+        * @return The local path of the project
+        */
+       public String getLocalPath() {
+               return localPath;
+       }
+       /**
+        * Sets the local path of the project.
+        *
+        * @param localPath
+        *            The local path of the project
+        */
+       public void setLocalPath(String localPath) {
+               this.localPath = localPath;
+       }
+       /**
+        * Returns the name of the index file of the project, relative to the
+        * project’s local path.
+        *
+        * @return The name of the index file of the project
+        */
+       public String getIndexFile() {
+               return indexFile;
+       }
+       /**
+        * Sets the name of the index file of the project, relative to the project’s
+        * local path.
+        *
+        * @param indexFile
+        *            The name of the index file of the project
+        */
+       public void setIndexFile(String indexFile) {
+               this.indexFile = indexFile;
+       }
+       /**
+        * Returns the time the project was last inserted, in milliseconds since the
+        * epoch.
+        *
+        * @return The time of the last insertion
+        */
+       public long getLastInsertionTime() {
+               return lastInsertionTime;
+       }
+       /**
+        * Sets the time the project was last inserted, in milliseconds since the
+        * last epoch.
+        *
+        * @param lastInserted
+        *            The time of the last insertion
+        */
+       public void setLastInsertionTime(long lastInserted) {
+               lastInsertionTime = lastInserted;
+       }
+       /**
+        * Returns the remote path of the project. The remote path is the path that
+        * directly follows the request URI of the project.
+        *
+        * @return The remote path of the project
+        */
+       public String getPath() {
+               return path;
+       }
+       /**
+        * Sets the remote path of the project. The remote path is the path that
+        * directly follows the request URI of the project.
+        *
+        * @param path
+        *            The remote path of the project
+        */
+       public void setPath(String path) {
+               this.path = path;
+       }
+       /**
+        * Returns the insert URI of the project.
+        *
+        * @return The insert URI of the project
+        */
+       public String getInsertURI() {
+               return insertURI;
+       }
+       /**
+        * Sets the insert URI of the project.
+        *
+        * @param insertURI
+        *            The insert URI of the project
+        */
+       public void setInsertURI(String insertURI) {
+               this.insertURI = shortenURI(insertURI);
+       }
+       /**
+        * Returns the request URI of the project.
+        *
+        * @return The request URI of the project
+        */
+       public String getRequestURI() {
+               return requestURI;
+       }
+       /**
+        * Sets the request URI of the project.
+        *
+        * @param requestURI
+        *            The request URI of the project
+        */
+       public void setRequestURI(String requestURI) {
+               this.requestURI = shortenURI(requestURI);
+       }
+       /**
+        * Returns whether hidden files are ignored, i.e. not inserted.
+        *
+        * @return {@code true} if hidden files are not inserted, {@code false}
+        *         otherwise
+        */
+       public boolean isIgnoreHiddenFiles() {
+               return ignoreHiddenFiles;
+       }
+       /**
+        * Sets whether hidden files are ignored, i.e. not inserted.
+        *
+        * @param ignoreHiddenFiles
+        *            {@code true} if hidden files are not inserted, {@code false}
+        *            otherwise
+        */
+       public void setIgnoreHiddenFiles(boolean ignoreHiddenFiles) {
+               this.ignoreHiddenFiles = ignoreHiddenFiles;
+       }
+       /**
+        * {@inheritDoc}
+        * <p>
+        * This method returns the name of the project.
+        */
+       @Override
+       public String toString() {
+               return name;
+       }
+       /**
+        * Shortens the given URI by removing scheme and key-type prefixes.
+        *
+        * @param uri
+        *            The URI to shorten
+        * @return The shortened URI
+        */
++      private static String shortenURI(String uri) {
+               String shortUri = uri;
+               if (shortUri.startsWith("freenet:")) {
+                       shortUri = shortUri.substring("freenet:".length());
+               }
+               if (shortUri.startsWith("SSK@")) {
+                       shortUri = shortUri.substring("SSK@".length());
+               }
+               if (shortUri.startsWith("USK@")) {
+                       shortUri = shortUri.substring("USK@".length());
+               }
+               if (shortUri.endsWith("/")) {
+                       shortUri = shortUri.substring(0, shortUri.length() - 1);
+               }
+               return shortUri;
+       }
+       /**
+        * Shortens the name of the given file by removing the local path of the
+        * project and leading file separators.
+        *
+        * @param file
+        *            The file whose name should be shortened
+        * @return The shortened name of the file
+        */
+       public String shortenFilename(File file) {
+               String filename = file.getPath();
+               if (filename.startsWith(localPath)) {
+                       filename = filename.substring(localPath.length());
+                       if (filename.startsWith(File.separator)) {
+                               filename = filename.substring(1);
+                       }
+               }
+               return filename;
+       }
+       /**
+        * Returns the options for the file with the given name. If the file does
+        * not yet have any options, a new set of default options is created and
+        * returned.
+        *
+        * @param filename
+        *            The name of the file, relative to the project root
+        * @return The options for the file
+        */
+       public FileOption getFileOption(String filename) {
+               FileOption fileOption = fileOptions.get(filename);
+               if (fileOption == null) {
+                       fileOption = new FileOption(MimeTypes.getMimeType(filename.substring(filename.lastIndexOf('.') + 1)));
+                       fileOptions.put(filename, fileOption);
+               }
+               return fileOption;
+       }
+       /**
+        * Sets options for a file.
+        *
+        * @param filename
+        *            The filename to set the options for, relative to the project
+        *            root
+        * @param fileOption
+        *            The options to set for the file, or <code>null</code> to
+        *            remove the options for the file
+        */
+       public void setFileOption(String filename, FileOption fileOption) {
+               if (fileOption != null) {
+                       fileOptions.put(filename, fileOption);
+               } else {
+                       fileOptions.remove(filename);
+               }
+       }
+       /**
+        * Returns all file options.
+        *
+        * @return All file options
+        */
+       public Map<String, FileOption> getFileOptions() {
+               return Collections.unmodifiableMap(fileOptions);
+       }
+       /**
+        * Sets all file options.
+        *
+        * @param fileOptions
+        *            The file options
+        */
+       public void setFileOptions(Map<String, FileOption> fileOptions) {
+               this.fileOptions.clear();
+               this.fileOptions.putAll(fileOptions);
+       }
+       /**
+        * {@inheritDoc}
+        * <p>
+        * Projects are compared by their name only.
+        */
++      @Override
+       public int compareTo(Project project) {
+               return name.compareToIgnoreCase(project.name);
+       }
+       /**
+        * Returns the edition of the project.
+        *
+        * @return The edition of the project
+        */
+       public int getEdition() {
+               return edition;
+       }
+       /**
+        * Sets the edition of the project.
+        *
+        * @param edition
+        *            The edition to set
+        */
+       public void setEdition(int edition) {
+               this.edition = edition;
+       }
+       /**
+        * Constructs the final request URI including the edition number.
+        *
+        * @param offset
+        *            The offset for the edition number
+        * @return The final request URI
+        */
+       public String getFinalRequestURI(int offset) {
+               return "USK@" + requestURI + "/" + path + "/" + (edition + offset) + "/";
+       }
+       /**
+        * Performs some post-processing on the project after it was inserted
+        * successfully. At the moment it copies the current hashes of all file
+        * options to the last insert hashes, updating the hashes for the next
+        * insert.
+        */
+       public void onSuccessfulInsert() {
+               for (Entry<String, FileOption> fileOptionEntry : fileOptions.entrySet()) {
+                       FileOption fileOption = fileOptionEntry.getValue();
+                       if ((fileOption.getCurrentHash() != null) && (fileOption.getCurrentHash().length() > 0) && (!fileOption.getCurrentHash().equals(fileOption.getLastInsertHash()) || fileOption.isForceInsert())) {
+                               fileOption.setLastInsertEdition(edition);
+                               fileOption.setLastInsertHash(fileOption.getCurrentHash());
+                               fileOption.setLastInsertFilename(fileOption.hasChangedName() ? fileOption.getChangedName() : fileOptionEntry.getKey());
+                       }
+                       fileOption.setForceInsert(false);
+               }
+       }
+ }
index 0000000,a9ea8d1..ed06533
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,694 +1,697 @@@
+ /*
+  * jSite - ProjectInserter.java - Copyright © 2006–2012 David Roden
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * 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, write to the Free Software
+  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+  */
+ package de.todesbaum.jsite.application;
+ import java.io.File;
+ import java.io.FileInputStream;
+ import java.io.IOException;
+ import java.io.InputStream;
+ import java.util.ArrayList;
+ import java.util.Arrays;
+ import java.util.HashSet;
+ import java.util.Iterator;
+ import java.util.List;
+ import java.util.Map;
+ import java.util.Map.Entry;
+ import java.util.Set;
+ import java.util.concurrent.CountDownLatch;
+ import java.util.logging.Level;
+ import java.util.logging.Logger;
+ import net.pterodactylus.util.io.StreamCopier.ProgressListener;
+ import de.todesbaum.jsite.gui.FileScanner;
+ import de.todesbaum.jsite.gui.FileScanner.ScannedFile;
+ import de.todesbaum.jsite.gui.FileScannerListener;
+ import de.todesbaum.util.freenet.fcp2.Client;
+ import de.todesbaum.util.freenet.fcp2.ClientPutComplexDir;
+ import de.todesbaum.util.freenet.fcp2.ClientPutDir.ManifestPutter;
+ import de.todesbaum.util.freenet.fcp2.Connection;
+ import de.todesbaum.util.freenet.fcp2.DirectFileEntry;
+ import de.todesbaum.util.freenet.fcp2.FileEntry;
+ import de.todesbaum.util.freenet.fcp2.Message;
+ import de.todesbaum.util.freenet.fcp2.PriorityClass;
+ import de.todesbaum.util.freenet.fcp2.RedirectFileEntry;
+ import de.todesbaum.util.freenet.fcp2.Verbosity;
+ /**
+  * Manages project inserts.
+  *
+  * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
+  */
+ public class ProjectInserter implements FileScannerListener, Runnable {
+       /** The logger. */
+       private static final Logger logger = Logger.getLogger(ProjectInserter.class.getName());
+       /** Random number for FCP instances. */
+       private static final int random = (int) (Math.random() * Integer.MAX_VALUE);
+       /** Counter for FCP connection identifier. */
+       private static int counter = 0;
+       /** The list of insert listeners. */
+       private List<InsertListener> insertListeners = new ArrayList<InsertListener>();
+       /** The freenet interface. */
+       protected Freenet7Interface freenetInterface;
+       /** The project to insert. */
+       protected Project project;
+       /** The file scanner. */
+       private FileScanner fileScanner;
+       /** Object used for synchronization. */
+       protected final Object lockObject = new Object();
+       /** The temp directory. */
+       private String tempDirectory;
+       /** The current connection. */
+       private Connection connection;
+       /** Whether the insert is cancelled. */
+       private volatile boolean cancelled = false;
+       /** Progress listener for payload transfers. */
+       private ProgressListener progressListener;
+       /** Whether to use “early encode.” */
+       private boolean useEarlyEncode;
+       /** The insert priority. */
+       private PriorityClass priority;
+       /** The manifest putter. */
+       private ManifestPutter manifestPutter;
+       /**
+        * Adds a listener to the list of registered listeners.
+        *
+        * @param insertListener
+        *            The listener to add
+        */
+       public void addInsertListener(InsertListener insertListener) {
+               insertListeners.add(insertListener);
+       }
+       /**
+        * Removes a listener from the list of registered listeners.
+        *
+        * @param insertListener
+        *            The listener to remove
+        */
+       public void removeInsertListener(InsertListener insertListener) {
+               insertListeners.remove(insertListener);
+       }
+       /**
+        * Notifies all listeners that the project insert has started.
+        *
+        * @see InsertListener#projectInsertStarted(Project)
+        */
+       protected void fireProjectInsertStarted() {
+               for (InsertListener insertListener : insertListeners) {
+                       insertListener.projectInsertStarted(project);
+               }
+       }
+       /**
+        * Notifies all listeners that the insert has generated a URI.
+        *
+        * @see InsertListener#projectURIGenerated(Project, String)
+        * @param uri
+        *            The generated URI
+        */
+       protected void fireProjectURIGenerated(String uri) {
+               for (InsertListener insertListener : insertListeners) {
+                       insertListener.projectURIGenerated(project, uri);
+               }
+       }
+       /**
+        * Notifies all listeners that the insert has made some progress.
+        *
+        * @see InsertListener#projectUploadFinished(Project)
+        */
+       protected void fireProjectUploadFinished() {
+               for (InsertListener insertListener : insertListeners) {
+                       insertListener.projectUploadFinished(project);
+               }
+       }
+       /**
+        * Notifies all listeners that the insert has made some progress.
+        *
+        * @see InsertListener#projectInsertProgress(Project, int, int, int, int,
+        *      boolean)
+        * @param succeeded
+        *            The number of succeeded blocks
+        * @param failed
+        *            The number of failed blocks
+        * @param fatal
+        *            The number of fatally failed blocks
+        * @param total
+        *            The total number of blocks
+        * @param finalized
+        *            <code>true</code> if the total number of blocks has already
+        *            been finalized, <code>false</code> otherwise
+        */
+       protected void fireProjectInsertProgress(int succeeded, int failed, int fatal, int total, boolean finalized) {
+               for (InsertListener insertListener : insertListeners) {
+                       insertListener.projectInsertProgress(project, succeeded, failed, fatal, total, finalized);
+               }
+       }
+       /**
+        * Notifies all listeners the project insert has finished.
+        *
+        * @see InsertListener#projectInsertFinished(Project, boolean, Throwable)
+        * @param success
+        *            <code>true</code> if the project was inserted successfully,
+        *            <code>false</code> if it failed
+        * @param cause
+        *            The cause of the failure, if any
+        */
+       protected void fireProjectInsertFinished(boolean success, Throwable cause) {
+               for (InsertListener insertListener : insertListeners) {
+                       insertListener.projectInsertFinished(project, success, cause);
+               }
+       }
+       /**
+        * Sets the project to insert.
+        *
+        * @param project
+        *            The project to insert
+        */
+       public void setProject(Project project) {
+               this.project = project;
+       }
+       /**
+        * Sets the freenet interface to use.
+        *
+        * @param freenetInterface
+        *            The freenet interface to use
+        */
+       public void setFreenetInterface(Freenet7Interface freenetInterface) {
+               this.freenetInterface = freenetInterface;
+       }
+       /**
+        * Sets the temp directory to use.
+        *
+        * @param tempDirectory
+        *            The temp directory to use, or {@code null} to use the system
+        *            default
+        */
+       public void setTempDirectory(String tempDirectory) {
+               this.tempDirectory = tempDirectory;
+       }
+       /**
+        * Sets whether to use the “early encode“ flag for the insert.
+        *
+        * @param useEarlyEncode
+        *            {@code true} to set the “early encode” flag for the insert,
+        *            {@code false} otherwise
+        */
+       public void setUseEarlyEncode(boolean useEarlyEncode) {
+               this.useEarlyEncode = useEarlyEncode;
+       }
+       /**
+        * Sets the insert priority.
+        *
+        * @param priority
+        *            The insert priority
+        */
+       public void setPriority(PriorityClass priority) {
+               this.priority = priority;
+       }
+       /**
+        * Sets the manifest putter to use for inserts.
+        *
+        * @param manifestPutter
+        *            The manifest putter to use
+        */
+       public void setManifestPutter(ManifestPutter manifestPutter) {
+               this.manifestPutter = manifestPutter;
+       }
+       /**
+        * Starts the insert.
+        *
+        * @param progressListener
+        *            Listener to notify on progress events
+        */
+       public void start(ProgressListener progressListener) {
+               cancelled = false;
+               this.progressListener = progressListener;
+               fileScanner = new FileScanner(project);
+               fileScanner.addFileScannerListener(this);
+               new Thread(fileScanner).start();
+       }
+       /**
+        * Stops the current insert.
+        */
+       public void stop() {
+               cancelled = true;
+               synchronized (lockObject) {
+                       if (connection != null) {
+                               connection.disconnect();
+                       }
+               }
+       }
+       /**
+        * Creates an input stream that delivers the given file, replacing edition
+        * tokens in the file’s content, if necessary.
+        *
+        * @param filename
+        *            The name of the file
+        * @param fileOption
+        *            The file options
+        * @param edition
+        *            The current edition
+        * @param length
+        *            An array containing a single long which is used to
+        *            <em>return</em> the final length of the file, after all
+        *            replacements
+        * @return The input stream for the file
+        * @throws IOException
+        *             if an I/O error occurs
+        */
+       private InputStream createFileInputStream(String filename, FileOption fileOption, int edition, long[] length) throws IOException {
+               File file = new File(project.getLocalPath(), filename);
+               length[0] = file.length();
+               return new FileInputStream(file);
+       }
+       /**
+        * Creates a file entry suitable for handing in to
+        * {@link ClientPutComplexDir#addFileEntry(FileEntry)}.
+        *
+        * @param file
+        *            The name and hash of the file to insert
+        * @param edition
+        *            The current edition
+        * @return A file entry for the given file
+        */
+       private FileEntry createFileEntry(ScannedFile file, int edition) {
+               FileEntry fileEntry = null;
+               String filename = file.getFilename();
+               FileOption fileOption = project.getFileOption(filename);
+               if (fileOption.isInsert()) {
+                       fileOption.setCurrentHash(file.getHash());
+                       /* check if file was modified. */
+                       if (!fileOption.isForceInsert() && file.getHash().equals(fileOption.getLastInsertHash())) {
+                               /* only insert a redirect. */
+                               logger.log(Level.FINE, String.format("Inserting redirect to edition %d for %s.", fileOption.getLastInsertEdition(), filename));
+                               return new RedirectFileEntry(fileOption.hasChangedName() ? fileOption.getChangedName() : filename, fileOption.getMimeType(), "SSK@" + project.getRequestURI() + "/" + project.getPath() + "-" + fileOption.getLastInsertEdition() + "/" + fileOption.getLastInsertFilename());
+                       }
+                       try {
+                               long[] fileLength = new long[1];
+                               InputStream fileEntryInputStream = createFileInputStream(filename, fileOption, edition, fileLength);
+                               fileEntry = new DirectFileEntry(fileOption.hasChangedName() ? fileOption.getChangedName() : filename, fileOption.getMimeType(), fileEntryInputStream, fileLength[0]);
+                       } catch (IOException ioe1) {
+                               /* ignore, null is returned. */
+                       }
+               } else {
+                       if (fileOption.isInsertRedirect()) {
+                               fileEntry = new RedirectFileEntry(fileOption.hasChangedName() ? fileOption.getChangedName() : filename, fileOption.getMimeType(), fileOption.getCustomKey());
+                       }
+               }
+               return fileEntry;
+       }
+       /**
+        * Validates the given project. The project will be checked for any invalid
+        * conditions, such as invalid insert or request keys, missing path names,
+        * missing default file, and so on.
+        *
+        * @param project
+        *            The project to check
+        * @return The encountered warnings and errors
+        */
+       public static CheckReport validateProject(Project project) {
+               CheckReport checkReport = new CheckReport();
+               if ((project.getLocalPath() == null) || (project.getLocalPath().trim().length() == 0)) {
+                       checkReport.addIssue("error.no-local-path", true);
+               }
+               if ((project.getPath() == null) || (project.getPath().trim().length() == 0)) {
+                       checkReport.addIssue("error.no-path", true);
+               }
+               if ((project.getIndexFile() == null) || (project.getIndexFile().length() == 0)) {
+                       checkReport.addIssue("warning.empty-index", false);
+               } else {
+                       File indexFile = new File(project.getLocalPath(), project.getIndexFile());
+                       if (!indexFile.exists()) {
+                               checkReport.addIssue("error.index-missing", true);
+                       }
+               }
+               String indexFile = project.getIndexFile();
+               boolean hasIndexFile = (indexFile != null) && (indexFile.length() > 0);
+               List<String> allowedIndexContentTypes = Arrays.asList("text/html", "application/xhtml+xml");
+               if (hasIndexFile && !allowedIndexContentTypes.contains(project.getFileOption(indexFile).getMimeType())) {
+                       checkReport.addIssue("warning.index-not-html", false);
+               }
+               Map<String, FileOption> fileOptions = project.getFileOptions();
+               Set<Entry<String, FileOption>> fileOptionEntries = fileOptions.entrySet();
+               boolean insert = fileOptionEntries.isEmpty();
+               for (Entry<String, FileOption> fileOptionEntry : fileOptionEntries) {
+                       String fileName = fileOptionEntry.getKey();
+                       FileOption fileOption = fileOptionEntry.getValue();
+                       insert |= fileOption.isInsert() || fileOption.isInsertRedirect();
+                       if (fileName.equals(project.getIndexFile()) && !fileOption.isInsert() && !fileOption.isInsertRedirect()) {
+                               checkReport.addIssue("error.index-not-inserted", true);
+                       }
+                       if (!fileOption.isInsert() && fileOption.isInsertRedirect() && ((fileOption.getCustomKey().length() == 0) || "CHK@".equals(fileOption.getCustomKey()))) {
+                               checkReport.addIssue("error.no-custom-key", true, fileName);
+                       }
+               }
+               if (!insert) {
+                       checkReport.addIssue("error.no-files-to-insert", true);
+               }
+               Set<String> fileNames = new HashSet<String>();
+               for (Entry<String, FileOption> fileOptionEntry : fileOptionEntries) {
+                       FileOption fileOption = fileOptionEntry.getValue();
+                       if (!fileOption.isInsert() && !fileOption.isInsertRedirect()) {
+                               logger.log(Level.FINEST, "Ignoring {0}.", fileOptionEntry.getKey());
+                               continue;
+                       }
+                       String fileName = fileOptionEntry.getKey();
+                       if (fileOption.hasChangedName()) {
+                               fileName = fileOption.getChangedName();
+                       }
+                       logger.log(Level.FINEST, "Adding “{0}” for {1}.", new Object[] { fileName, fileOptionEntry.getKey() });
+                       if (!fileNames.add(fileName)) {
+                               checkReport.addIssue("error.duplicate-file", true, fileName);
+                       }
+               }
+               long totalSize = 0;
+               FileScanner fileScanner = new FileScanner(project);
+               final CountDownLatch completionLatch = new CountDownLatch(1);
+               fileScanner.addFileScannerListener(new FileScannerListener() {
+                       @Override
+                       public void fileScannerFinished(FileScanner fileScanner) {
+                               completionLatch.countDown();
+                       }
+               });
+               new Thread(fileScanner).start();
+               while (completionLatch.getCount() > 0) {
+                       try {
+                               completionLatch.await();
+                       } catch (InterruptedException ie1) {
+                               /* TODO: logging */
+                       }
+               }
+               for (ScannedFile scannedFile : fileScanner.getFiles()) {
+                       String fileName = scannedFile.getFilename();
+                       FileOption fileOption = project.getFileOption(fileName);
+                       if ((fileOption != null) && !fileOption.isInsert()) {
+                               continue;
+                       }
+                       totalSize += new File(project.getLocalPath(), fileName).length();
+               }
+               if (totalSize > 2 * 1024 * 1024) {
+                       checkReport.addIssue("warning.site-larger-than-2-mib", false);
+               }
+               return checkReport;
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void run() {
+               fireProjectInsertStarted();
+               List<ScannedFile> files = fileScanner.getFiles();
+               /* create connection to node */
+               synchronized (lockObject) {
+                       connection = freenetInterface.getConnection("project-insert-" + random + counter++);
+               }
+               connection.setTempDirectory(tempDirectory);
+               boolean connected = false;
+               Throwable cause = null;
+               try {
+                       connected = connection.connect();
+               } catch (IOException e1) {
+                       cause = e1;
+               }
+               if (!connected || cancelled) {
+                       fireProjectInsertFinished(false, cancelled ? new AbortedException() : cause);
+                       return;
+               }
+               Client client = new Client(connection);
+               /* collect files */
+               int edition = project.getEdition();
+               String dirURI = "USK@" + project.getInsertURI() + "/" + project.getPath() + "/" + edition + "/";
+               ClientPutComplexDir putDir = new ClientPutComplexDir("dir-" + counter++, dirURI, tempDirectory);
+               if ((project.getIndexFile() != null) && (project.getIndexFile().length() > 0)) {
+                       putDir.setDefaultName(project.getIndexFile());
+               }
+               putDir.setVerbosity(Verbosity.ALL);
+               putDir.setMaxRetries(-1);
+               putDir.setEarlyEncode(useEarlyEncode);
+               putDir.setPriorityClass(priority);
+               putDir.setManifestPutter(manifestPutter);
+               for (ScannedFile file : files) {
+                       FileEntry fileEntry = createFileEntry(file, edition);
+                       if (fileEntry != null) {
+                               try {
+                                       putDir.addFileEntry(fileEntry);
+                               } catch (IOException ioe1) {
+                                       fireProjectInsertFinished(false, ioe1);
+                                       return;
+                               }
+                       }
+               }
+               /* start request */
+               try {
+                       client.execute(putDir, progressListener);
+                       fireProjectUploadFinished();
+               } catch (IOException ioe1) {
+                       fireProjectInsertFinished(false, ioe1);
+                       return;
+               }
+               /* parse progress and success messages */
+               String finalURI = null;
+               boolean success = false;
+               boolean finished = false;
+               boolean disconnected = false;
+               while (!finished && !cancelled) {
+                       Message message = client.readMessage();
+                       finished = (message == null) || (disconnected = client.isDisconnected());
+                       logger.log(Level.FINE, "Received message: " + message);
+                       if (!finished) {
+                               @SuppressWarnings("null")
+                               String messageName = message.getName();
+                               if ("URIGenerated".equals(messageName)) {
+                                       finalURI = message.get("URI");
+                                       fireProjectURIGenerated(finalURI);
+                               }
+                               if ("SimpleProgress".equals(messageName)) {
+                                       int total = Integer.parseInt(message.get("Total"));
+                                       int succeeded = Integer.parseInt(message.get("Succeeded"));
+                                       int fatal = Integer.parseInt(message.get("FatallyFailed"));
+                                       int failed = Integer.parseInt(message.get("Failed"));
+                                       boolean finalized = Boolean.parseBoolean(message.get("FinalizedTotal"));
+                                       fireProjectInsertProgress(succeeded, failed, fatal, total, finalized);
+                               }
+                               success |= "PutSuccessful".equals(messageName);
+                               finished = (success && (finalURI != null)) || "PutFailed".equals(messageName) || messageName.endsWith("Error");
+                       }
+               }
+               /* post-insert work */
+               if (success) {
+                       @SuppressWarnings("null")
+                       String editionPart = finalURI.substring(finalURI.lastIndexOf('/') + 1);
+                       int newEdition = Integer.parseInt(editionPart);
+                       project.setEdition(newEdition);
+                       project.setLastInsertionTime(System.currentTimeMillis());
+                       project.onSuccessfulInsert();
+               }
+               fireProjectInsertFinished(success, cancelled ? new AbortedException() : (disconnected ? new IOException("Connection terminated") : null));
+       }
+       //
+       // INTERFACE FileScannerListener
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void fileScannerFinished(FileScanner fileScanner) {
+               if (!fileScanner.isError()) {
+                       new Thread(this).start();
+               } else {
+                       fireProjectInsertFinished(false, null);
+               }
+               fileScanner.removeFileScannerListener(this);
+       }
+       /**
+        * Container class that collects all warnings and errors that occured during
+        * {@link ProjectInserter#validateProject(Project) project validation}.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public static class CheckReport implements Iterable<Issue> {
+               /** The issures that occured. */
+               private final List<Issue> issues = new ArrayList<Issue>();
+               /**
+                * Adds an issue.
+                *
+                * @param issue
+                *            The issue to add
+                */
+               public void addIssue(Issue issue) {
+                       issues.add(issue);
+               }
+               /**
+                * Creates an {@link Issue} from the given error key and fatality flag
+                * and {@link #addIssue(Issue) adds} it.
+                *
+                * @param errorKey
+                *            The error key
+                * @param fatal
+                *            {@code true} if the error is fatal, {@code false} if only
+                *            a warning should be generated
+                * @param parameters
+                *            Any additional parameters
+                */
+               public void addIssue(String errorKey, boolean fatal, String... parameters) {
+                       addIssue(new Issue(errorKey, fatal, parameters));
+               }
+               /**
+                * {@inheritDoc}
+                */
++              @Override
+               public Iterator<Issue> iterator() {
+                       return issues.iterator();
+               }
+               /**
+                * Returns whether this check report does not contain any errors.
+                *
+                * @return {@code true} if this check report does not contain any
+                *         errors, {@code false} if this check report does contain
+                *         errors
+                */
+               public boolean isEmpty() {
+                       return issues.isEmpty();
+               }
+               /**
+                * Returns the number of issues in this check report.
+                *
+                * @return The number of issues
+                */
+               public int size() {
+                       return issues.size();
+               }
+       }
+       /**
+        * Container class for a single issue. An issue contains an error key
+        * that describes the error, and a fatality flag that determines whether
+        * the insert has to be aborted (if the flag is {@code true}) or if it
+        * can still be performed and only a warning should be generated (if the
+        * flag is {@code false}).
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’
+        *         Roden</a>
+        */
+       public static class Issue {
+               /** The error key. */
+               private final String errorKey;
+               /** The fatality flag. */
+               private final boolean fatal;
+               /** Additional parameters. */
+               private String[] parameters;
+               /**
+                * Creates a new issue.
+                *
+                * @param errorKey
+                *            The error key
+                * @param fatal
+                *            The fatality flag
+                * @param parameters
+                *            Any additional parameters
+                */
+               protected Issue(String errorKey, boolean fatal, String... parameters) {
+                       this.errorKey = errorKey;
+                       this.fatal = fatal;
+                       this.parameters = parameters;
+               }
+               /**
+                * Returns the key of the encountered error.
+                *
+                * @return The error key
+                */
+               public String getErrorKey() {
+                       return errorKey;
+               }
+               /**
+                * Returns whether the issue is fatal and the insert has to be
+                * aborted. Otherwise only a warning should be shown.
+                *
+                * @return {@code true} if the insert needs to be aborted, {@code
+                *         false} otherwise
+                */
+               public boolean isFatal() {
+                       return fatal;
+               }
+               /**
+                * Returns any additional parameters.
+                *
+                * @return The additional parameters
+                */
+               public String[] getParameters() {
+                       return parameters;
+               }
+       }
+ }
index 0000000,1f55681..bd3b14f
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,287 +1,288 @@@
 -      private String constructUpdateKey(int edition) {
+ /*
+  * jSite - UpdateChecker.java - Copyright © 2008–2012 David Roden
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * 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, write to the Free Software
+  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+  */
+ package de.todesbaum.jsite.application;
+ import java.io.IOException;
+ import java.io.InputStream;
+ import java.util.ArrayList;
+ import java.util.List;
+ import java.util.Properties;
+ import java.util.logging.Level;
+ import java.util.logging.Logger;
+ import net.pterodactylus.util.io.Closer;
+ import de.todesbaum.jsite.main.Main;
+ import de.todesbaum.jsite.main.Version;
+ import de.todesbaum.util.freenet.fcp2.Client;
+ import de.todesbaum.util.freenet.fcp2.ClientGet;
+ import de.todesbaum.util.freenet.fcp2.Connection;
+ import de.todesbaum.util.freenet.fcp2.Message;
+ import de.todesbaum.util.freenet.fcp2.Persistence;
+ import de.todesbaum.util.freenet.fcp2.ReturnType;
+ import de.todesbaum.util.freenet.fcp2.Verbosity;
+ /**
+  * Checks for newer versions of jSite.
+  *
+  * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
+  */
+ public class UpdateChecker implements Runnable {
+       /** The logger. */
+       private static final Logger logger = Logger.getLogger(UpdateChecker.class.getName());
+       /** Counter for connection names. */
+       private static int counter = 0;
+       /** The edition for the update check URL. */
+       private static final int UPDATE_EDITION = 17;
+       /** The URL for update checks. */
+       private static final String UPDATE_KEY = "USK@e3myoFyp5avg6WYN16ImHri6J7Nj8980Fm~aQe4EX1U,QvbWT0ImE0TwLODTl7EoJx2NBnwDxTbLTE6zkB-eGPs,AQACAAE";
+       /** Object used for synchronization. */
+       private final Object syncObject = new Object();
+       /** Update listeners. */
+       private final List<UpdateListener> updateListeners = new ArrayList<UpdateListener>();
+       /** Whether the main thread should stop. */
+       private boolean shouldStop = false;
+       /** Current last found edition of update key. */
+       private int lastUpdateEdition = UPDATE_EDITION;
+       /** Last found version. */
+       private Version lastVersion = Main.getVersion();
+       /** The freenet interface. */
+       private final Freenet7Interface freenetInterface;
+       /**
+        * Creates a new update checker that uses the given frame as its parent and
+        * communications via the given freenet interface.
+        *
+        * @param freenetInterface
+        *            The freenet interface
+        */
+       public UpdateChecker(Freenet7Interface freenetInterface) {
+               this.freenetInterface = freenetInterface;
+       }
+       //
+       // EVENT LISTENER MANAGEMENT
+       //
+       /**
+        * Adds an update listener to the list of registered listeners.
+        *
+        * @param updateListener
+        *            The update listener to add
+        */
+       public void addUpdateListener(UpdateListener updateListener) {
+               updateListeners.add(updateListener);
+       }
+       /**
+        * Removes the given listener from the list of registered listeners.
+        *
+        * @param updateListener
+        *            The update listener to remove
+        */
+       public void removeUpdateListener(UpdateListener updateListener) {
+               updateListeners.remove(updateListener);
+       }
+       /**
+        * Notifies all listeners that a version was found.
+        *
+        * @param foundVersion
+        *            The version that was found
+        * @param versionTimestamp
+        *            The timestamp of the version
+        */
+       protected void fireUpdateFound(Version foundVersion, long versionTimestamp) {
+               for (UpdateListener updateListener : updateListeners) {
+                       updateListener.foundUpdateData(foundVersion, versionTimestamp);
+               }
+       }
+       //
+       // ACCESSORS
+       //
+       /**
+        * Returns the latest version that was found.
+        *
+        * @return The latest found version
+        */
+       public Version getLatestVersion() {
+               return lastVersion;
+       }
+       //
+       // ACTIONS
+       //
+       /**
+        * Starts the update checker.
+        */
+       public void start() {
+               new Thread(this).start();
+       }
+       /**
+        * Stops the update checker.
+        */
+       public void stop() {
+               synchronized (syncObject) {
+                       shouldStop = true;
+                       syncObject.notifyAll();
+               }
+       }
+       //
+       // PRIVATE METHODS
+       //
+       /**
+        * Returns whether the update checker should stop.
+        *
+        * @return <code>true</code> if the update checker should stop,
+        *         <code>false</code> otherwise
+        */
+       private boolean shouldStop() {
+               synchronized (syncObject) {
+                       return shouldStop;
+               }
+       }
+       /**
+        * Creates the URI of the update file for the given edition.
+        *
+        * @param edition
+        *            The edition number
+        * @return The URI for the update file for the given edition
+        */
++      private static String constructUpdateKey(int edition) {
+               return UPDATE_KEY + "/jSite/" + edition + "/jSite.properties";
+       }
+       //
+       // INTERFACE Runnable
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void run() {
+               Connection connection = freenetInterface.getConnection("jSite-" + ++counter + "-UpdateChecker");
+               try {
+                       connection.connect();
+               } catch (IOException e1) {
+                       e1.printStackTrace();
+               }
+               Client client = new Client(connection);
+               boolean checkNow = false;
+               int currentEdition = lastUpdateEdition;
+               while (!shouldStop()) {
+                       checkNow = false;
+                       logger.log(Level.FINE, "Trying " + constructUpdateKey(currentEdition));
+                       ClientGet clientGet = new ClientGet("get-update-key");
+                       clientGet.setUri(constructUpdateKey(currentEdition));
+                       clientGet.setPersistence(Persistence.CONNECTION);
+                       clientGet.setReturnType(ReturnType.direct);
+                       clientGet.setVerbosity(Verbosity.ALL);
+                       try {
+                               client.execute(clientGet);
+                               boolean stop = false;
+                               while (!stop) {
+                                       Message message = client.readMessage();
+                                       logger.log(Level.FINEST, "Received message: " + message);
+                                       if (message == null) {
+                                               break;
+                                       }
+                                       if ("GetFailed".equals(message.getName())) {
+                                               if ("27".equals(message.get("code"))) {
+                                                       String editionString = message.get("redirecturi").split("/")[2];
+                                                       int editionNumber = -1;
+                                                       try {
+                                                               editionNumber = Integer.parseInt(editionString);
+                                                       } catch (NumberFormatException nfe1) {
+                                                               /* ignore. */
+                                                       }
+                                                       if (editionNumber != -1) {
+                                                               logger.log(Level.INFO, "Found new edition " + editionNumber);
+                                                               currentEdition = editionNumber;
+                                                               lastUpdateEdition = editionNumber;
+                                                               checkNow = true;
+                                                               break;
+                                                       }
+                                               }
+                                       }
+                                       if ("AllData".equals(message.getName())) {
+                                               logger.log(Level.FINE, "Update data found.");
+                                               InputStream dataInputStream = null;
+                                               Properties properties = new Properties();
+                                               try {
+                                                       dataInputStream = message.getPayloadInputStream();
+                                                       properties.load(dataInputStream);
+                                               } finally {
+                                                       Closer.close(dataInputStream);
+                                               }
+                                               String foundVersionString = properties.getProperty("jSite.Version");
+                                               if (foundVersionString != null) {
+                                                       Version foundVersion = Version.parse(foundVersionString);
+                                                       if (foundVersion != null) {
+                                                               lastVersion = foundVersion;
+                                                               String versionTimestampString = properties.getProperty("jSite.Date");
+                                                               logger.log(Level.FINEST, "Version timestamp: " + versionTimestampString);
+                                                               long versionTimestamp = -1;
+                                                               try {
+                                                                       versionTimestamp = Long.parseLong(versionTimestampString);
+                                                               } catch (NumberFormatException nfe1) {
+                                                                       /* ignore. */
+                                                               }
+                                                               fireUpdateFound(foundVersion, versionTimestamp);
+                                                               stop = true;
+                                                               checkNow = true;
+                                                               ++currentEdition;
+                                                       }
+                                               }
+                                       }
+                               }
+                       } catch (IOException e) {
+                               logger.log(Level.INFO, "Got IOException: " + e.getMessage());
+                               e.printStackTrace();
+                       }
+                       if (!checkNow && !shouldStop()) {
+                               synchronized (syncObject) {
+                                       try {
+                                               syncObject.wait(15 * 60 * 1000);
+                                       } catch (InterruptedException ie1) {
+                                               /* ignore. */
+                                       }
+                               }
+                       }
+               }
+       }
+ }
index 0000000,aa07943..bd98ed0
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,344 +1,347 @@@
+ /*
+  * jSite - FileScanner.java - Copyright © 2006–2012 David Roden
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * 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, write to the Free Software
+  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+  */
+ package de.todesbaum.jsite.gui;
+ import java.io.File;
+ import java.io.FileFilter;
+ import java.io.FileInputStream;
+ import java.io.IOException;
+ import java.io.InputStream;
+ import java.io.OutputStream;
+ import java.security.DigestOutputStream;
+ import java.security.MessageDigest;
+ import java.security.NoSuchAlgorithmException;
+ import java.util.ArrayList;
+ import java.util.Collections;
+ import java.util.List;
+ import java.util.logging.Level;
+ import java.util.logging.Logger;
+ import net.pterodactylus.util.io.Closer;
+ import net.pterodactylus.util.io.StreamCopier;
+ import de.todesbaum.jsite.application.Project;
+ import de.todesbaum.jsite.i18n.I18n;
+ /**
+  * Scans the local path of a project anychronously and returns the list of found
+  * files as an event.
+  *
+  * @see Project#getLocalPath()
+  * @see FileScannerListener#fileScannerFinished(FileScanner)
+  * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
+  */
+ public class FileScanner implements Runnable {
+       /** The logger. */
+       private final static Logger logger = Logger.getLogger(FileScanner.class.getName());
+       /** The list of listeners. */
+       private final List<FileScannerListener> fileScannerListeners = new ArrayList<FileScannerListener>();
+       /** The project to scan. */
+       private final Project project;
+       /** The list of found files. */
+       private List<ScannedFile> files;
+       /** Wether there was an error. */
+       private boolean error = false;
+       /**
+        * Creates a new file scanner for the given project.
+        *
+        * @param project
+        *            The project whose files to scan
+        */
+       public FileScanner(Project project) {
+               this.project = project;
+       }
+       /**
+        * Adds the given listener to the list of listeners.
+        *
+        * @param fileScannerListener
+        *            The listener to add
+        */
+       public void addFileScannerListener(FileScannerListener fileScannerListener) {
+               fileScannerListeners.add(fileScannerListener);
+       }
+       /**
+        * Removes the given listener from the list of listeners.
+        *
+        * @param fileScannerListener
+        *            The listener to remove
+        */
+       public void removeFileScannerListener(FileScannerListener fileScannerListener) {
+               fileScannerListeners.remove(fileScannerListener);
+       }
+       /**
+        * Notifies all listeners that the file scan finished.
+        */
+       protected void fireFileScannerFinished() {
+               for (FileScannerListener fileScannerListener : new ArrayList<FileScannerListener>(fileScannerListeners)) {
+                       fileScannerListener.fileScannerFinished(this);
+               }
+       }
+       /**
+        * {@inheritDoc}
+        * <p>
+        * Scans all available files in the project’s local path and emits an event
+        * when finished.
+        *
+        * @see FileScannerListener#fileScannerFinished(FileScanner)
+        */
++      @Override
+       public void run() {
+               files = new ArrayList<ScannedFile>();
+               error = false;
+               try {
+                       scanFiles(new File(project.getLocalPath()), files);
+                       Collections.sort(files);
+               } catch (IOException ioe1) {
+                       error = true;
+               }
+               fireFileScannerFinished();
+       }
+       /**
+        * Returns whether there was an error scanning for files.
+        *
+        * @return <code>true</code> if there was an error, <code>false</code>
+        *         otherwise
+        */
+       public boolean isError() {
+               return error;
+       }
+       /**
+        * Returns the list of found files.
+        *
+        * @return The list of found files
+        */
+       public List<ScannedFile> getFiles() {
+               return files;
+       }
+       /**
+        * Recursively scans a directory and adds all found files to the given list.
+        *
+        * @param rootDir
+        *            The directory to scan
+        * @param fileList
+        *            The list to which to add the found files
+        * @throws IOException
+        *             if an I/O error occurs
+        */
+       private void scanFiles(File rootDir, List<ScannedFile> fileList) throws IOException {
+               File[] files = rootDir.listFiles(new FileFilter() {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public boolean accept(File file) {
+                               return !project.isIgnoreHiddenFiles() || !file.isHidden();
+                       }
+               });
+               if (files == null) {
+                       throw new IOException(I18n.getMessage("jsite.file-scanner.can-not-read-directory"));
+               }
+               for (File file : files) {
+                       if (file.isDirectory()) {
+                               scanFiles(file, fileList);
+                               continue;
+                       }
+                       String filename = project.shortenFilename(file).replace('\\', '/');
+                       String hash = hashFile(project.getLocalPath(), filename);
+                       fileList.add(new ScannedFile(filename, hash));
+               }
+       }
+       /**
+        * Hashes the given file.
+        *
+        * @param path
+        *            The path of the project
+        * @param filename
+        *            The name of the file, relative to the project path
+        * @return The hash of the file
+        */
+       @SuppressWarnings("synthetic-access")
+       private static String hashFile(String path, String filename) {
+               InputStream fileInputStream = null;
+               DigestOutputStream digestOutputStream = null;
+               File file = new File(path, filename);
+               try {
+                       fileInputStream = new FileInputStream(file);
+                       digestOutputStream = new DigestOutputStream(new NullOutputStream(), MessageDigest.getInstance("SHA-256"));
+                       StreamCopier.copy(fileInputStream, digestOutputStream, file.length());
+                       return toHex(digestOutputStream.getMessageDigest().digest());
+               } catch (NoSuchAlgorithmException nsae1) {
+                       logger.log(Level.WARNING, "Could not get SHA-256 digest!", nsae1);
+               } catch (IOException ioe1) {
+                       logger.log(Level.WARNING, "Could not read file!", ioe1);
+               } finally {
+                       Closer.close(digestOutputStream);
+                       Closer.close(fileInputStream);
+               }
+               return toHex(new byte[32]);
+       }
+       /**
+        * Converts the given byte array into a hexadecimal string.
+        *
+        * @param array
+        *            The array to convert
+        * @return The hexadecimal string
+        */
+       private static String toHex(byte[] array) {
+               StringBuilder hexString = new StringBuilder(array.length * 2);
+               for (byte b : array) {
+                       hexString.append("0123456789abcdef".charAt((b >>> 4) & 0x0f)).append("0123456789abcdef".charAt(b & 0xf));
+               }
+               return hexString.toString();
+       }
+       /**
+        * {@link OutputStream} that discards all written bytes.
+        *
+        * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
+        */
+       private static class NullOutputStream extends OutputStream {
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public void write(int b) {
+                       /* do nothing. */
+               }
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public void write(byte[] b) {
+                       /* do nothing. */
+               }
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public void write(byte[] b, int off, int len) {
+                       /* do nothing. */
+               }
+       }
+       /**
+        * Container for a scanned file, consisting of the name of the file and its
+        * hash.
+        *
+        * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
+        */
+       public static class ScannedFile implements Comparable<ScannedFile> {
+               /** The name of the file. */
+               private final String filename;
+               /** The hash of the file. */
+               private final String hash;
+               /**
+                * Creates a new scanned file.
+                *
+                * @param filename
+                *            The name of the file
+                * @param hash
+                *            The hash of the file
+                */
+               public ScannedFile(String filename, String hash) {
+                       this.filename = filename;
+                       this.hash = hash;
+               }
+               //
+               // ACCESSORS
+               //
+               /**
+                * Returns the name of the file.
+                *
+                * @return The name of the file
+                */
+               public String getFilename() {
+                       return filename;
+               }
+               /**
+                * Returns the hash of the file.
+                *
+                * @return The hash of the file
+                */
+               public String getHash() {
+                       return hash;
+               }
+               //
+               // OBJECT METHODS
+               //
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public int hashCode() {
+                       return filename.hashCode();
+               }
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public boolean equals(Object obj) {
+                       return filename.equals(obj);
+               }
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public String toString() {
+                       return filename;
+               }
+               //
+               // COMPARABLE METHODS
+               //
+               /**
+                * {@inheritDoc}
+                */
++              @Override
+               public int compareTo(ScannedFile scannedFile) {
+                       return filename.compareTo(scannedFile.filename);
+               }
+       }
+ }
index 0000000,8e44129..49c25bc
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,450 +1,460 @@@
+ /*
+  * jSite - NodeManagerPage.java - Copyright © 2006–2012 David Roden
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * 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, write to the Free Software
+  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+  */
+ package de.todesbaum.jsite.gui;
+ import java.awt.BorderLayout;
+ import java.awt.Dimension;
+ import java.awt.FlowLayout;
+ import java.awt.GridBagConstraints;
+ import java.awt.GridBagLayout;
+ import java.awt.Insets;
+ import java.awt.event.ActionEvent;
+ import java.awt.event.KeyEvent;
+ import java.util.ArrayList;
+ import java.util.List;
+ import javax.swing.AbstractAction;
+ import javax.swing.Action;
+ import javax.swing.DefaultListModel;
+ import javax.swing.JButton;
+ import javax.swing.JLabel;
+ import javax.swing.JList;
+ import javax.swing.JOptionPane;
+ import javax.swing.JPanel;
+ import javax.swing.JScrollPane;
+ import javax.swing.JSpinner;
+ import javax.swing.JTextField;
+ import javax.swing.ListSelectionModel;
+ import javax.swing.SpinnerNumberModel;
+ import javax.swing.border.EmptyBorder;
+ import javax.swing.event.ChangeEvent;
+ import javax.swing.event.ChangeListener;
+ import javax.swing.event.DocumentEvent;
+ import javax.swing.event.DocumentListener;
+ import javax.swing.event.ListSelectionEvent;
+ import javax.swing.event.ListSelectionListener;
+ import javax.swing.text.BadLocationException;
+ import javax.swing.text.Document;
+ import de.todesbaum.jsite.application.Node;
+ import de.todesbaum.jsite.i18n.I18n;
+ import de.todesbaum.jsite.i18n.I18nContainer;
+ import de.todesbaum.util.swing.TLabel;
+ import de.todesbaum.util.swing.TWizard;
+ import de.todesbaum.util.swing.TWizardPage;
+ /**
+  * Wizard page that lets the user edit his nodes.
+  *
+  * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
+  */
+ public class NodeManagerPage extends TWizardPage implements ListSelectionListener, DocumentListener, ChangeListener {
+       /** List of node manager listeners. */
+       private List<NodeManagerListener> nodeManagerListeners = new ArrayList<NodeManagerListener>();
+       /** The “add node” action. */
+       protected Action addNodeAction;
+       /** The “delete node” action. */
+       protected Action deleteNodeAction;
+       /** The node list model. */
+       private DefaultListModel nodeListModel;
+       /** The node list. */
+       private JList nodeList;
+       /** The node name textfield. */
+       private JTextField nodeNameTextField;
+       /** The node hostname textfield. */
+       private JTextField nodeHostnameTextField;
+       /** The spinner for the node port. */
+       private JSpinner nodePortSpinner;
+       /**
+        * Creates a new node manager wizard page.
+        *
+        * @param wizard
+        *            The wizard this page belongs to
+        */
+       public NodeManagerPage(final TWizard wizard) {
+               super(wizard);
+               pageInit();
+               setHeading(I18n.getMessage("jsite.node-manager.heading"));
+               setDescription(I18n.getMessage("jsite.node-manager.description"));
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
++                      @Override
+                       public void run() {
+                               setHeading(I18n.getMessage("jsite.node-manager.heading"));
+                               setDescription(I18n.getMessage("jsite.node-manager.description"));
+                       }
+               });
+       }
+       /**
+        * Adds a listener for node manager events.
+        *
+        * @param nodeManagerListener
+        *            The listener to add
+        */
+       public void addNodeManagerListener(NodeManagerListener nodeManagerListener) {
+               nodeManagerListeners.add(nodeManagerListener);
+       }
+       /**
+        * Removes a listener for node manager events.
+        *
+        * @param nodeManagerListener
+        *            The listener to remove
+        */
+       public void removeNodeManagerListener(NodeManagerListener nodeManagerListener) {
+               nodeManagerListeners.remove(nodeManagerListener);
+       }
+       /**
+        * Notifies all listeners that the node configuration has changed.
+        *
+        * @param nodes
+        *            The new list of nodes
+        */
+       protected void fireNodesUpdated(Node[] nodes) {
+               for (NodeManagerListener nodeManagerListener : nodeManagerListeners) {
+                       nodeManagerListener.nodesUpdated(nodes);
+               }
+       }
+       /**
+        * Notifies all listeners that a new node was selected.
+        *
+        * @param node
+        *            The newly selected node
+        */
+       protected void fireNodeSelected(Node node) {
+               for (NodeManagerListener nodeManagerListener : nodeManagerListeners) {
+                       nodeManagerListener.nodeSelected(node);
+               }
+       }
+       /**
+        * Creates all actions.
+        */
+       private void createActions() {
+               addNodeAction = new AbstractAction(I18n.getMessage("jsite.node-manager.add-node")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               addNode();
+                       }
+               };
+               deleteNodeAction = new AbstractAction(I18n.getMessage("jsite.node-manager.delete-node")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               deleteNode();
+                       }
+               };
+               deleteNodeAction.setEnabled(false);
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
++                      @Override
+                       public void run() {
+                               addNodeAction.putValue(Action.NAME, I18n.getMessage("jsite.node-manager.add-node"));
+                               deleteNodeAction.putValue(Action.NAME, I18n.getMessage("jsite.node-manager.delete-node"));
+                       }
+               });
+       }
+       /**
+        * Initializes the page and all components in it.
+        */
+       private void pageInit() {
+               createActions();
+               nodeListModel = new DefaultListModel();
+               nodeList = new JList(nodeListModel);
+               nodeList.setName("node-list");
+               nodeList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+               nodeList.addListSelectionListener(this);
+               nodeList.setPreferredSize(new Dimension(250, -1));
+               nodeNameTextField = new JTextField("");
+               nodeNameTextField.getDocument().putProperty("Name", "node-name");
+               nodeNameTextField.getDocument().addDocumentListener(this);
+               nodeNameTextField.setEnabled(false);
+               nodeHostnameTextField = new JTextField("localhost");
+               nodeHostnameTextField.getDocument().putProperty("Name", "node-hostname");
+               nodeHostnameTextField.getDocument().addDocumentListener(this);
+               nodeHostnameTextField.setEnabled(false);
+               nodePortSpinner = new JSpinner(new SpinnerNumberModel(9481, 1, 65535, 1));
+               nodePortSpinner.setName("node-port");
+               nodePortSpinner.addChangeListener(this);
+               nodePortSpinner.setEnabled(false);
+               JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEADING, 12, 12));
+               buttonPanel.setBorder(new EmptyBorder(-12, -12, -12, -12));
+               buttonPanel.add(new JButton(addNodeAction));
+               buttonPanel.add(new JButton(deleteNodeAction));
+               JPanel centerPanel = new JPanel(new BorderLayout());
+               JPanel nodeInformationPanel = new JPanel(new GridBagLayout());
+               centerPanel.add(nodeInformationPanel, BorderLayout.PAGE_START);
+               nodeInformationPanel.add(buttonPanel, new GridBagConstraints(0, 0, 2, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
+               final JLabel nodeInformationLabel = new JLabel("<html><b>" + I18n.getMessage("jsite.node-manager.node-information") + "</b></html>");
+               nodeInformationPanel.add(nodeInformationLabel, new GridBagConstraints(0, 1, 2, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(6, 0, 0, 0), 0, 0));
+               final TLabel nodeNameLabel = new TLabel(I18n.getMessage("jsite.node-manager.name") + ":", KeyEvent.VK_N, nodeNameTextField);
+               nodeInformationPanel.add(nodeNameLabel, new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 18, 0, 0), 0, 0));
+               nodeInformationPanel.add(nodeNameTextField, new GridBagConstraints(1, 2, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
+               final TLabel nodeHostnameLabel = new TLabel(I18n.getMessage("jsite.node-manager.hostname") + ":", KeyEvent.VK_H, nodeHostnameTextField);
+               nodeInformationPanel.add(nodeHostnameLabel, new GridBagConstraints(0, 3, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 18, 0, 0), 0, 0));
+               nodeInformationPanel.add(nodeHostnameTextField, new GridBagConstraints(1, 3, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
+               final TLabel nodePortLabel = new TLabel(I18n.getMessage("jsite.node-manager.port") + ":", KeyEvent.VK_P, nodePortSpinner);
+               nodeInformationPanel.add(nodePortLabel, new GridBagConstraints(0, 4, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 18, 0, 0), 0, 0));
+               nodeInformationPanel.add(nodePortSpinner, new GridBagConstraints(1, 4, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 6, 0, 0), 0, 0));
+               setLayout(new BorderLayout(12, 12));
+               add(new JScrollPane(nodeList), BorderLayout.LINE_START);
+               add(centerPanel, BorderLayout.CENTER);
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
++                      @Override
+                       public void run() {
+                               nodeInformationLabel.setText("<html><b>" + I18n.getMessage("jsite.node-manager.node-information") + "</b></html>");
+                               nodeNameLabel.setText(I18n.getMessage("jsite.node-manager.name") + ":");
+                               nodeHostnameLabel.setText(I18n.getMessage("jsite.node-manager.hostname") + ":");
+                               nodePortLabel.setText(I18n.getMessage("jsite.node-manager.port") + ":");
+                       }
+               });
+       }
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void pageAdded(TWizard wizard) {
+               this.wizard.setNextEnabled(nodeListModel.getSize() > 0);
+               this.wizard.setPreviousName(I18n.getMessage("jsite.wizard.previous"));
+               this.wizard.setNextName(I18n.getMessage("jsite.wizard.next"));
+               this.wizard.setQuitName(I18n.getMessage("jsite.wizard.quit"));
+       }
+       /**
+        * Sets the node list.
+        *
+        * @param nodes
+        *            The list of nodes
+        */
+       public void setNodes(Node[] nodes) {
+               nodeListModel.clear();
+               for (Node node : nodes) {
+                       nodeListModel.addElement(node);
+               }
+               nodeList.repaint();
+               fireNodesUpdated(nodes);
+       }
+       /**
+        * Returns the node list.
+        *
+        * @return The list of nodes
+        */
+       public Node[] getNodes() {
+               Node[] returnNodes = new Node[nodeListModel.getSize()];
+               for (int nodeIndex = 0, nodeCount = nodeListModel.getSize(); nodeIndex < nodeCount; nodeIndex++) {
+                       returnNodes[nodeIndex] = (Node) nodeListModel.get(nodeIndex);
+               }
+               return returnNodes;
+       }
+       /**
+        * Returns the currently selected node.
+        *
+        * @return The selected node, or <code>null</code> if no node is selected
+        */
+       private Node getSelectedNode() {
+               return (Node) nodeList.getSelectedValue();
+       }
+       /**
+        * Updates node name or hostname when the user types into the textfields.
+        *
+        * @see #insertUpdate(DocumentEvent)
+        * @see #removeUpdate(DocumentEvent)
+        * @see #changedUpdate(DocumentEvent)
+        * @see DocumentListener
+        * @param documentEvent
+        *            The document event
+        */
+       private void updateTextField(DocumentEvent documentEvent) {
+               Node node = getSelectedNode();
+               if (node == null) {
+                       return;
+               }
+               Document document = documentEvent.getDocument();
+               String documentText = null;
+               try {
+                       documentText = document.getText(0, document.getLength());
+               } catch (BadLocationException ble1) {
+                       /* ignore. */
+               }
+               if (documentText == null) {
+                       return;
+               }
+               String documentName = (String) document.getProperty("Name");
+               if ("node-name".equals(documentName)) {
+                       node.setName(documentText);
+                       nodeList.repaint();
+                       fireNodesUpdated(getNodes());
+               } else if ("node-hostname".equals(documentName)) {
+                       node.setHostname(documentText);
+                       nodeList.repaint();
+                       fireNodesUpdated(getNodes());
+               }
+       }
+       //
+       // ACTIONS
+       //
+       /**
+        * Adds a new node to the list of nodes.
+        */
+       private void addNode() {
+               Node node = new Node("localhost", 9481, I18n.getMessage("jsite.node-manager.new-node"));
+               nodeListModel.addElement(node);
+               deleteNodeAction.setEnabled(nodeListModel.size() > 1);
+               wizard.setNextEnabled(true);
+               fireNodesUpdated(getNodes());
+       }
+       /**
+        * Deletes the currently selected node from the list of nodes.
+        */
+       private void deleteNode() {
+               Node node = getSelectedNode();
+               if (node == null) {
+                       return;
+               }
+               if (JOptionPane.showConfirmDialog(wizard, I18n.getMessage("jsite.node-manager.delete-node.warning"), null, JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.CANCEL_OPTION) {
+                       return;
+               }
+               int nodeIndex = nodeListModel.indexOf(node);
+               nodeListModel.removeElement(node);
+               nodeList.repaint();
+               fireNodeSelected((Node) nodeListModel.get(Math.min(nodeIndex, nodeListModel.size() - 1)));
+               fireNodesUpdated(getNodes());
+               deleteNodeAction.setEnabled(nodeListModel.size() > 1);
+               wizard.setNextEnabled(nodeListModel.size() > 0);
+       }
+       //
+       // INTERFACE ListSelectionListener
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       @SuppressWarnings("null")
+       public void valueChanged(ListSelectionEvent e) {
+               Object source = e.getSource();
+               if (source instanceof JList) {
+                       JList sourceList = (JList) source;
+                       if ("node-list".equals(sourceList.getName())) {
+                               Node node = (Node) sourceList.getSelectedValue();
+                               boolean enabled = (node != null);
+                               nodeNameTextField.setEnabled(enabled);
+                               nodeHostnameTextField.setEnabled(enabled);
+                               nodePortSpinner.setEnabled(enabled);
+                               deleteNodeAction.setEnabled(enabled && (nodeListModel.size() > 1));
+                               if (enabled) {
+                                       nodeNameTextField.setText(node.getName());
+                                       nodeHostnameTextField.setText(node.getHostname());
+                                       nodePortSpinner.setValue(node.getPort());
+                               } else {
+                                       nodeNameTextField.setText("");
+                                       nodeHostnameTextField.setText("localhost");
+                                       nodePortSpinner.setValue(9481);
+                               }
+                       }
+               }
+       }
+       //
+       // INTERFACE DocumentListener
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void insertUpdate(DocumentEvent e) {
+               updateTextField(e);
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void removeUpdate(DocumentEvent e) {
+               updateTextField(e);
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void changedUpdate(DocumentEvent e) {
+               updateTextField(e);
+       }
+       //
+       // INTERFACE ChangeListener
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void stateChanged(ChangeEvent e) {
+               Object source = e.getSource();
+               Node selectedNode = getSelectedNode();
+               if (selectedNode == null) {
+                       return;
+               }
+               if (source instanceof JSpinner) {
+                       JSpinner sourceSpinner = (JSpinner) source;
+                       if ("node-port".equals(sourceSpinner.getName())) {
+                               selectedNode.setPort((Integer) sourceSpinner.getValue());
+                               fireNodeSelected(selectedNode);
+                               nodeList.repaint();
+                       }
+               }
+       }
+ }
index 0000000,5cf258a..914e74b
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,519 +1,530 @@@
+ /*
+  * jSite - PreferencesPage.java - Copyright © 2009–2012 David Roden
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * 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, write to the Free Software
+  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+  */
+ package de.todesbaum.jsite.gui;
+ import java.awt.BorderLayout;
+ import java.awt.GridBagConstraints;
+ import java.awt.GridBagLayout;
+ import java.awt.Insets;
+ import java.awt.event.ActionEvent;
+ import javax.swing.AbstractAction;
+ import javax.swing.Action;
+ import javax.swing.BorderFactory;
+ import javax.swing.ButtonGroup;
+ import javax.swing.JButton;
+ import javax.swing.JCheckBox;
+ import javax.swing.JComboBox;
+ import javax.swing.JFileChooser;
+ import javax.swing.JLabel;
+ import javax.swing.JPanel;
+ import javax.swing.JRadioButton;
+ import javax.swing.JTextField;
+ import de.todesbaum.jsite.i18n.I18n;
+ import de.todesbaum.jsite.i18n.I18nContainer;
+ import de.todesbaum.jsite.main.ConfigurationLocator.ConfigurationLocation;
+ import de.todesbaum.util.freenet.fcp2.ClientPutDir.ManifestPutter;
+ import de.todesbaum.util.freenet.fcp2.PriorityClass;
+ import de.todesbaum.util.swing.TWizard;
+ import de.todesbaum.util.swing.TWizardPage;
+ /**
+  * Page that shows some preferences that are valid for the complete application.
+  *
+  * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
+  */
+ public class PreferencesPage extends TWizardPage {
+       /** Select default temp directory action. */
+       private Action selectDefaultTempDirectoryAction;
+       /** Select custom temp directory action. */
+       private Action selectCustomTempDirectoryAction;
+       /** Action that chooses a new temp directory. */
+       private Action chooseTempDirectoryAction;
+       /** Action when selecting “next to JAR file.” */
+       private Action nextToJarFileAction;
+       /** Action when selecting “home directory.” */
+       private Action homeDirectoryAction;
+       /** Action when selecting “custom directory.” */
+       private Action customDirectoryAction;
+       /** Action when selecting “use early encode.” */
+       private Action useEarlyEncodeAction;
+       /** Action when a priority was selected. */
+       private Action priorityAction;
+       /** The text field containing the directory. */
+       private JTextField tempDirectoryTextField;
+       /** The temp directory. */
+       private String tempDirectory;
+       /** The configuration location. */
+       private ConfigurationLocation configurationLocation;
+       /** Whether to use “early encode.” */
+       private boolean useEarlyEncode;
+       /** The prioriy for inserts. */
+       private PriorityClass priority;
+       /** The “default” button. */
+       private JRadioButton defaultTempDirectory;
+       /** The “custom” button. */
+       private JRadioButton customTempDirectory;
+       /** The “next to JAR file” checkbox. */
+       private JRadioButton nextToJarFile;
+       /** The “home directory” checkbox. */
+       private JRadioButton homeDirectory;
+       /** The “custom directory” checkbox. */
+       private JRadioButton customDirectory;
+       /** The “use early encode” checkbox. */
+       private JCheckBox useEarlyEncodeCheckBox;
+       /** The insert priority select box. */
+       private JComboBox insertPriorityComboBox;
+       /** The manifest putter select box. */
+       private JComboBox manifestPutterComboBox;
+       /**
+        * Creates a new “preferences” page.
+        *
+        * @param wizard
+        *            The wizard this page belongs to
+        */
+       public PreferencesPage(TWizard wizard) {
+               super(wizard);
+               pageInit();
+               setHeading(I18n.getMessage("jsite.preferences.heading"));
+               setDescription(I18n.getMessage("jsite.preferences.description"));
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
+                       /**
+                        * {@inheritDoc}
+                        */
++                      @Override
+                       public void run() {
+                               setHeading(I18n.getMessage("jsite.preferences.heading"));
+                               setDescription(I18n.getMessage("jsite.preferences.description"));
+                       }
+               });
+       }
+       //
+       // ACCESSORS
+       //
+       /**
+        * Returns the temp directory.
+        *
+        * @return The temp directory, or {@code null} to use the default temp
+        *         directory
+        */
+       public String getTempDirectory() {
+               return tempDirectory;
+       }
+       /**
+        * Sets the temp directory.
+        *
+        * @param tempDirectory
+        *            The temp directory, or {@code null} to use the default temp
+        *            directory
+        */
+       public void setTempDirectory(String tempDirectory) {
+               this.tempDirectory = tempDirectory;
+               tempDirectoryTextField.setText((tempDirectory != null) ? tempDirectory : "");
+               if (tempDirectory != null) {
+                       customTempDirectory.setSelected(true);
+                       chooseTempDirectoryAction.setEnabled(true);
+               } else {
+                       defaultTempDirectory.setSelected(true);
+               }
+       }
+       /**
+        * Returns the configuration location.
+        *
+        * @return The configuration location
+        */
+       public ConfigurationLocation getConfigurationLocation() {
+               return configurationLocation;
+       }
+       /**
+        * Sets the configuration location.
+        *
+        * @param configurationLocation
+        *            The configuration location
+        */
+       public void setConfigurationLocation(ConfigurationLocation configurationLocation) {
+               this.configurationLocation = configurationLocation;
+               switch (configurationLocation) {
+               case NEXT_TO_JAR_FILE:
+                       nextToJarFile.setSelected(true);
+                       break;
+               case HOME_DIRECTORY:
+                       homeDirectory.setSelected(true);
+                       break;
+               case CUSTOM:
+                       customDirectory.setSelected(true);
+                       break;
+               }
+       }
+       /**
+        * Sets whether it is possible to select the “next to JAR file” option for
+        * the configuration location.
+        *
+        * @param nextToJarFile
+        *            {@code true} if the configuration file can be saved next to
+        *            the JAR file, {@code false} otherwise
+        */
+       public void setHasNextToJarConfiguration(boolean nextToJarFile) {
+               this.nextToJarFile.setEnabled(nextToJarFile);
+       }
+       /**
+        * Sets whether it is possible to select the “custom location” option for
+        * the configuration location.
+        *
+        * @param customDirectory
+        *            {@code true} if the configuration file can be saved to a
+        *            custom location, {@code false} otherwise
+        */
+       public void setHasCustomConfiguration(boolean customDirectory) {
+               this.customDirectory.setEnabled(customDirectory);
+       }
+       /**
+        * Returns whether to use the “early encode“ flag for the insert.
+        *
+        * @return {@code true} to set the “early encode” flag for the insert,
+        *         {@code false} otherwise
+        */
+       public boolean useEarlyEncode() {
+               return useEarlyEncode;
+       }
+       /**
+        * Sets whether to use the “early encode“ flag for the insert.
+        *
+        * @param useEarlyEncode
+        *            {@code true} to set the “early encode” flag for the insert,
+        *            {@code false} otherwise
+        */
+       public void setUseEarlyEncode(boolean useEarlyEncode) {
+               useEarlyEncodeCheckBox.setSelected(useEarlyEncode);
+       }
+       /**
+        * Returns the configured insert priority.
+        *
+        * @return The insert priority
+        */
+       public PriorityClass getPriority() {
+               return priority;
+       }
+       /**
+        * Sets the insert priority.
+        *
+        * @param priority
+        *            The insert priority
+        */
+       public void setPriority(PriorityClass priority) {
+               insertPriorityComboBox.setSelectedItem(priority);
+       }
+       /**
+        * Returns the selected manifest putter.
+        *
+        * @return The selected manifest putter
+        */
+       public ManifestPutter getManifestPutter() {
+               return (ManifestPutter) manifestPutterComboBox.getSelectedItem();
+       }
+       /**
+        * Sets the manifest putter.
+        *
+        * @param manifestPutter
+        *            The manifest putter
+        */
+       public void setManifestPutter(ManifestPutter manifestPutter) {
+               manifestPutterComboBox.setSelectedItem(manifestPutter);
+       }
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void pageAdded(TWizard wizard) {
+               super.pageAdded(wizard);
+               this.wizard.setPreviousName(I18n.getMessage("jsite.menu.nodes.manage-nodes"));
+               this.wizard.setNextName(I18n.getMessage("jsite.wizard.next"));
+               this.wizard.setQuitName(I18n.getMessage("jsite.wizard.quit"));
+               this.wizard.setNextEnabled(false);
+       }
+       //
+       // PRIVATE METHODS
+       //
+       /**
+        * Initializes this page.
+        */
+       private void pageInit() {
+               createActions();
+               setLayout(new BorderLayout(12, 12));
+               add(createPreferencesPanel(), BorderLayout.CENTER);
+       }
+       /**
+        * Creates all actions.
+        */
+       private void createActions() {
+               selectDefaultTempDirectoryAction = new AbstractAction(I18n.getMessage("jsite.preferences.temp-directory.default")) {
+                       /**
+                        * {@inheritDoc}
+                        */
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               selectDefaultTempDirectory();
+                       }
+               };
+               selectCustomTempDirectoryAction = new AbstractAction(I18n.getMessage("jsite.preferences.temp-directory.custom")) {
+                       /**
+                        * {@inheritDoc}
+                        */
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               selectCustomTempDirectory();
+                       }
+               };
+               chooseTempDirectoryAction = new AbstractAction(I18n.getMessage("jsite.preferences.temp-directory.choose")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent e) {
+                               chooseTempDirectory();
+                       }
+               };
+               nextToJarFileAction = new AbstractAction(I18n.getMessage("jsite.preferences.config-directory.jar")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionevent) {
+                               configurationLocation = ConfigurationLocation.NEXT_TO_JAR_FILE;
+                       }
+               };
+               homeDirectoryAction = new AbstractAction(I18n.getMessage("jsite.preferences.config-directory.home")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionevent) {
+                               configurationLocation = ConfigurationLocation.HOME_DIRECTORY;
+                       }
+               };
+               customDirectoryAction = new AbstractAction(I18n.getMessage("jsite.preferences.config-directory.custom")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               configurationLocation = ConfigurationLocation.CUSTOM;
+                       }
+               };
+               useEarlyEncodeAction = new AbstractAction(I18n.getMessage("jsite.preferences.insert-options.use-early-encode")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               useEarlyEncode = useEarlyEncodeCheckBox.isSelected();
+                       }
+               };
+               priorityAction = new AbstractAction(I18n.getMessage("jsite.preferences.insert-options.priority")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               priority = (PriorityClass) insertPriorityComboBox.getSelectedItem();
+                       }
+               };
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void run() {
+                               selectDefaultTempDirectoryAction.putValue(Action.NAME, I18n.getMessage("jsite.preferences.temp-directory.default"));
+                               selectCustomTempDirectoryAction.putValue(Action.NAME, I18n.getMessage("jsite.preferences.temp-directory.custom"));
+                               chooseTempDirectoryAction.putValue(Action.NAME, I18n.getMessage("jsite.preferences.temp-directory.choose"));
+                               nextToJarFileAction.putValue(Action.NAME, I18n.getMessage("jsite.preferences.config-directory.jar"));
+                               homeDirectoryAction.putValue(Action.NAME, I18n.getMessage("jsite.preferences.config-directory.home"));
+                               customDirectoryAction.putValue(Action.NAME, I18n.getMessage("jsite.preferences.config-directory.custom"));
+                               useEarlyEncodeAction.putValue(Action.NAME, I18n.getMessage("jsite.preferences.insert-options.use-early-encode"));
+                       }
+               });
+       }
+       /**
+        * Creates the panel containing all preferences.
+        *
+        * @return The preferences panel
+        */
+       private JPanel createPreferencesPanel() {
+               JPanel preferencesPanel = new JPanel(new GridBagLayout());
+               preferencesPanel.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
+               final JLabel tempDirectoryLabel = new JLabel("<html><b>" + I18n.getMessage("jsite.preferences.temp-directory") + "</b></html>");
+               preferencesPanel.add(tempDirectoryLabel, new GridBagConstraints(0, 0, 3, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
+               defaultTempDirectory = new JRadioButton(selectDefaultTempDirectoryAction);
+               preferencesPanel.add(defaultTempDirectory, new GridBagConstraints(0, 1, 3, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(6, 18, 0, 0), 0, 0));
+               customTempDirectory = new JRadioButton(selectCustomTempDirectoryAction);
+               preferencesPanel.add(customTempDirectory, new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(0, 18, 0, 0), 0, 0));
+               ButtonGroup tempDirectoryButtonGroup = new ButtonGroup();
+               defaultTempDirectory.getModel().setGroup(tempDirectoryButtonGroup);
+               customTempDirectory.getModel().setGroup(tempDirectoryButtonGroup);
+               tempDirectoryTextField = new JTextField();
+               tempDirectoryTextField.setEditable(false);
+               if (tempDirectory != null) {
+                       tempDirectoryTextField.setText(tempDirectory);
+                       customTempDirectory.setSelected(true);
+               } else {
+                       defaultTempDirectory.setSelected(true);
+               }
+               chooseTempDirectoryAction.setEnabled(tempDirectory != null);
+               preferencesPanel.add(tempDirectoryTextField, new GridBagConstraints(1, 2, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(0, 6, 0, 0), 0, 0));
+               JButton chooseButton = new JButton(chooseTempDirectoryAction);
+               preferencesPanel.add(chooseButton, new GridBagConstraints(2, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_END, GridBagConstraints.BOTH, new Insets(0, 6, 0, 0), 0, 0));
+               final JLabel configurationDirectoryLabel = new JLabel("<html><b>" + I18n.getMessage("jsite.preferences.config-directory") + "</b></html>");
+               preferencesPanel.add(configurationDirectoryLabel, new GridBagConstraints(0, 3, 3, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(12, 0, 0, 0), 0, 0));
+               nextToJarFile = new JRadioButton(nextToJarFileAction);
+               preferencesPanel.add(nextToJarFile, new GridBagConstraints(0, 4, 3, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(6, 18, 0, 0), 0, 0));
+               homeDirectory = new JRadioButton(homeDirectoryAction);
+               preferencesPanel.add(homeDirectory, new GridBagConstraints(0, 5, 3, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(0, 18, 0, 0), 0, 0));
+               customDirectory = new JRadioButton(customDirectoryAction);
+               preferencesPanel.add(customDirectory, new GridBagConstraints(0, 6, 3, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(0, 18, 0, 0), 0, 0));
+               ButtonGroup configurationDirectoryButtonGroup = new ButtonGroup();
+               configurationDirectoryButtonGroup.add(nextToJarFile);
+               configurationDirectoryButtonGroup.add(homeDirectory);
+               configurationDirectoryButtonGroup.add(customDirectory);
+               final JLabel insertOptionsLabel = new JLabel("<html><b>" + I18n.getMessage("jsite.preferences.insert-options") + "</b></html>");
+               preferencesPanel.add(insertOptionsLabel, new GridBagConstraints(0, 7, 3, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(12, 0, 0, 0), 0, 0));
+               useEarlyEncodeCheckBox = new JCheckBox(useEarlyEncodeAction);
+               preferencesPanel.add(useEarlyEncodeCheckBox, new GridBagConstraints(0, 8, 3, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 18, 0, 0), 0, 0));
+               final JLabel insertPriorityLabel = new JLabel(I18n.getMessage("jsite.preferences.insert-options.priority"));
+               preferencesPanel.add(insertPriorityLabel, new GridBagConstraints(0, 9, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 18, 0, 0), 0, 0));
+               insertPriorityComboBox = new JComboBox(new PriorityClass[] { PriorityClass.MINIMUM, PriorityClass.PREFETCH, PriorityClass.BULK, PriorityClass.UPDATABLE, PriorityClass.SEMI_INTERACTIVE, PriorityClass.INTERACTIVE, PriorityClass.MAXIMUM });
+               insertPriorityComboBox.setAction(priorityAction);
+               preferencesPanel.add(insertPriorityComboBox, new GridBagConstraints(1, 9, 2, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 18, 0, 0), 0, 0));
+               final JLabel manifestPutterLabel = new JLabel(I18n.getMessage("jsite.preferences.insert-options.manifest-putter"));
+               preferencesPanel.add(manifestPutterLabel, new GridBagConstraints(0, 10, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 18, 0, 0), 0, 0));
+               manifestPutterComboBox = new JComboBox(ManifestPutter.values());
+               preferencesPanel.add(manifestPutterComboBox, new GridBagConstraints(1, 10, 2, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 18, 0, 0), 0, 0));
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
+                       /**
+                        * {@inheritDoc}
+                        */
++                      @Override
+                       public void run() {
+                               tempDirectoryLabel.setText("<html><b>" + I18n.getMessage("jsite.preferences.temp-directory") + "</b></html>");
+                               configurationDirectoryLabel.setText("<html><b>" + I18n.getMessage("jsite.preferences.config-directory") + "</b></html>");
+                               insertOptionsLabel.setText("<html><b>" + I18n.getMessage("jsite.preferences.insert-options") + "</b></html>");
+                               insertPriorityLabel.setText(I18n.getMessage("jsite.preferences.insert-options.priority"));
+                               manifestPutterLabel.setText(I18n.getMessage("jsite.preferences.insert-options.manifest-putter"));
+                       }
+               });
+               return preferencesPanel;
+       }
+       /**
+        * Activates the default temp directory radio button.
+        */
+       private void selectDefaultTempDirectory() {
+               tempDirectoryTextField.setEnabled(false);
+               chooseTempDirectoryAction.setEnabled(false);
+               tempDirectory = null;
+       }
+       /**
+        * Activates the custom temp directory radio button.
+        */
+       private void selectCustomTempDirectory() {
+               tempDirectoryTextField.setEnabled(true);
+               chooseTempDirectoryAction.setEnabled(true);
+               if (tempDirectoryTextField.getText().length() == 0) {
+                       chooseTempDirectory();
+                       if (tempDirectoryTextField.getText().length() == 0) {
+                               defaultTempDirectory.setSelected(true);
+                       }
+               }
+       }
+       /**
+        * Lets the user choose a new temp directory.
+        */
+       private void chooseTempDirectory() {
+               JFileChooser fileChooser = new JFileChooser(tempDirectory);
+               fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+               int returnValue = fileChooser.showDialog(wizard, I18n.getMessage("jsite.preferences.temp-directory.choose.approve"));
+               if (returnValue == JFileChooser.CANCEL_OPTION) {
+                       return;
+               }
+               tempDirectory = fileChooser.getSelectedFile().getPath();
+               tempDirectoryTextField.setText(tempDirectory);
+       }
+ }
index 0000000,014a497..5b26661
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,577 +1,592 @@@
+ /*
+  * jSite - ProjectFilesPage.java - Copyright © 2006–2012 David Roden
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * 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, write to the Free Software
+  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+  */
+ package de.todesbaum.jsite.gui;
+ import java.awt.BorderLayout;
+ import java.awt.Dimension;
+ import java.awt.GridBagConstraints;
+ import java.awt.GridBagLayout;
+ import java.awt.Insets;
+ import java.awt.event.ActionEvent;
+ import java.awt.event.ActionListener;
+ import java.awt.event.KeyEvent;
+ import java.text.MessageFormat;
+ import java.util.HashSet;
+ import java.util.Iterator;
+ import java.util.List;
+ import java.util.Set;
+ import javax.swing.AbstractAction;
+ import javax.swing.Action;
+ import javax.swing.JButton;
+ import javax.swing.JCheckBox;
+ import javax.swing.JComboBox;
+ import javax.swing.JComponent;
+ import javax.swing.JLabel;
+ import javax.swing.JList;
+ import javax.swing.JOptionPane;
+ import javax.swing.JPanel;
+ import javax.swing.JScrollPane;
+ import javax.swing.JTextField;
+ import javax.swing.ListSelectionModel;
+ import javax.swing.SwingUtilities;
+ import javax.swing.event.DocumentEvent;
+ import javax.swing.event.DocumentListener;
+ import javax.swing.event.ListSelectionEvent;
+ import javax.swing.event.ListSelectionListener;
+ import javax.swing.text.BadLocationException;
+ import javax.swing.text.Document;
+ import net.pterodactylus.util.io.MimeTypes;
+ import de.todesbaum.jsite.application.FileOption;
+ import de.todesbaum.jsite.application.Project;
+ import de.todesbaum.jsite.gui.FileScanner.ScannedFile;
+ import de.todesbaum.jsite.i18n.I18n;
+ import de.todesbaum.jsite.i18n.I18nContainer;
+ import de.todesbaum.util.swing.TLabel;
+ import de.todesbaum.util.swing.TWizard;
+ import de.todesbaum.util.swing.TWizardPage;
+ /**
+  * Wizard page that lets the user manage the files of a project.
+  *
+  * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
+  */
+ public class ProjectFilesPage extends TWizardPage implements ActionListener, ListSelectionListener, DocumentListener, FileScannerListener {
+       /** The project. */
+       private Project project;
+       /** The “scan files” action. */
+       private Action scanAction;
+       /** The “ignore hidden files” checkbox. */
+       private JCheckBox ignoreHiddenFilesCheckBox;
+       /** The list of project files. */
+       private JList projectFileList;
+       /** The “default file” checkbox. */
+       private JCheckBox defaultFileCheckBox;
+       /** The “insert” checkbox. */
+       private JCheckBox fileOptionsInsertCheckBox;
+       /** The “force insert” checkbox. */
+       private JCheckBox fileOptionsForceInsertCheckBox;
+       /** The “insert redirect” checkbox. */
+       private JCheckBox fileOptionsInsertRedirectCheckBox;
+       /** The “custom key” textfield. */
+       private JTextField fileOptionsCustomKeyTextField;
+       /** The “rename” check box. */
+       private JCheckBox fileOptionsRenameCheckBox;
+       /** The “new name” text field. */
+       private JTextField fileOptionsRenameTextField;
+       /** The “mime type” combo box. */
+       private JComboBox fileOptionsMIMETypeComboBox;
+       /**
+        * Creates a new project file page.
+        *
+        * @param wizard
+        *            The wizard the page belongs to
+        */
+       public ProjectFilesPage(final TWizard wizard) {
+               super(wizard);
+               pageInit();
+       }
+       /**
+        * Initializes the page and all its actions and components.
+        */
+       private void pageInit() {
+               createActions();
+               setLayout(new BorderLayout(12, 12));
+               add(createProjectFilesPanel(), BorderLayout.CENTER);
+       }
+       /**
+        * Creates all actions.
+        */
+       private void createActions() {
+               scanAction = new AbstractAction(I18n.getMessage("jsite.project-files.action.rescan")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               actionScan();
+                       }
+               };
+               scanAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_S);
+               scanAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project-files.action.rescan.tooltip"));
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void run() {
+                               scanAction.putValue(Action.NAME, I18n.getMessage("jsite.project-files.action.rescan"));
+                               scanAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project-files.action.rescan.tooltip"));
+                       }
+               });
+       }
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void pageAdded(TWizard wizard) {
+               actionScan();
+               this.wizard.setPreviousName(I18n.getMessage("jsite.wizard.previous"));
+               this.wizard.setNextName(I18n.getMessage("jsite.project-files.insert-now"));
+               this.wizard.setQuitName(I18n.getMessage("jsite.wizard.quit"));
+       }
+       /**
+        * Creates the panel contains the project file list and options.
+        *
+        * @return The created panel
+        */
+       private JComponent createProjectFilesPanel() {
+               JPanel projectFilesPanel = new JPanel(new BorderLayout(12, 12));
+               projectFileList = new JList();
+               projectFileList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+               projectFileList.setMinimumSize(new Dimension(250, projectFileList.getPreferredSize().height));
+               projectFileList.addListSelectionListener(this);
+               projectFilesPanel.add(new JScrollPane(projectFileList), BorderLayout.CENTER);
+               JPanel fileOptionsAlignmentPanel = new JPanel(new BorderLayout(12, 12));
+               projectFilesPanel.add(fileOptionsAlignmentPanel, BorderLayout.PAGE_END);
+               JPanel fileOptionsPanel = new JPanel(new GridBagLayout());
+               fileOptionsAlignmentPanel.add(fileOptionsPanel, BorderLayout.PAGE_START);
+               ignoreHiddenFilesCheckBox = new JCheckBox(I18n.getMessage("jsite.project-files.ignore-hidden-files"));
+               ignoreHiddenFilesCheckBox.setToolTipText(I18n.getMessage("jsite.project-files.ignore-hidden-files.tooltip"));
+               ignoreHiddenFilesCheckBox.setName("ignore-hidden-files");
+               ignoreHiddenFilesCheckBox.addActionListener(this);
+               fileOptionsPanel.add(ignoreHiddenFilesCheckBox, new GridBagConstraints(0, 0, 5, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
+               fileOptionsPanel.add(new JButton(scanAction), new GridBagConstraints(0, 1, 5, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 0, 0, 0), 0, 0));
+               final JLabel fileOptionsLabel = new JLabel("<html><b>" + I18n.getMessage("jsite.project-files.file-options") + "</b></html>");
+               fileOptionsPanel.add(fileOptionsLabel, new GridBagConstraints(0, 2, 5, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 0, 0, 0), 0, 0));
+               defaultFileCheckBox = new JCheckBox(I18n.getMessage("jsite.project-files.default"));
+               defaultFileCheckBox.setToolTipText(I18n.getMessage("jsite.project-files.default.tooltip"));
+               defaultFileCheckBox.setName("default-file");
+               defaultFileCheckBox.addActionListener(this);
+               defaultFileCheckBox.setEnabled(false);
+               fileOptionsPanel.add(defaultFileCheckBox, new GridBagConstraints(0, 3, 5, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(6, 18, 0, 0), 0, 0));
+               fileOptionsInsertCheckBox = new JCheckBox(I18n.getMessage("jsite.project-files.insert"), true);
+               fileOptionsInsertCheckBox.setToolTipText(I18n.getMessage("jsite.project-files.insert.tooltip"));
+               fileOptionsInsertCheckBox.setName("insert");
+               fileOptionsInsertCheckBox.setMnemonic(KeyEvent.VK_I);
+               fileOptionsInsertCheckBox.addActionListener(this);
+               fileOptionsInsertCheckBox.setEnabled(false);
+               fileOptionsPanel.add(fileOptionsInsertCheckBox, new GridBagConstraints(0, 4, 5, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 18, 0, 0), 0, 0));
+               fileOptionsForceInsertCheckBox = new JCheckBox(I18n.getMessage("jsite.project-files.force-insert"));
+               fileOptionsForceInsertCheckBox.setToolTipText(I18n.getMessage("jsite.project-files.force-insert.tooltip"));
+               fileOptionsForceInsertCheckBox.setName("force-insert");
+               fileOptionsForceInsertCheckBox.setMnemonic(KeyEvent.VK_F);
+               fileOptionsForceInsertCheckBox.addActionListener(this);
+               fileOptionsForceInsertCheckBox.setEnabled(false);
+               fileOptionsPanel.add(fileOptionsForceInsertCheckBox, new GridBagConstraints(0, 5, 5, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 18, 0, 0), 0, 0));
+               fileOptionsCustomKeyTextField = new JTextField(45);
+               fileOptionsCustomKeyTextField.setToolTipText(I18n.getMessage("jsite.project-files.custom-key.tooltip"));
+               fileOptionsCustomKeyTextField.setEnabled(false);
+               fileOptionsCustomKeyTextField.getDocument().addDocumentListener(this);
+               fileOptionsInsertRedirectCheckBox = new JCheckBox(I18n.getMessage("jsite.project-files.insert-redirect"), false);
+               fileOptionsInsertRedirectCheckBox.setToolTipText(I18n.getMessage("jsite.project-files.insert-redirect.tooltip"));
+               fileOptionsInsertRedirectCheckBox.setName("insert-redirect");
+               fileOptionsInsertRedirectCheckBox.setMnemonic(KeyEvent.VK_R);
+               fileOptionsInsertRedirectCheckBox.addActionListener(this);
+               fileOptionsInsertRedirectCheckBox.setEnabled(false);
+               final TLabel customKeyLabel = new TLabel(I18n.getMessage("jsite.project-files.custom-key") + ":", KeyEvent.VK_K, fileOptionsCustomKeyTextField);
+               fileOptionsPanel.add(fileOptionsInsertRedirectCheckBox, new GridBagConstraints(0, 6, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 18, 0, 0), 0, 0));
+               fileOptionsPanel.add(customKeyLabel, new GridBagConstraints(1, 6, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 6, 0, 0), 0, 0));
+               fileOptionsPanel.add(fileOptionsCustomKeyTextField, new GridBagConstraints(2, 6, 3, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
+               fileOptionsRenameCheckBox = new JCheckBox(I18n.getMessage("jsite.project-files.rename"), false);
+               fileOptionsRenameCheckBox.setToolTipText(I18n.getMessage("jsite.project-files.rename.tooltip"));
+               fileOptionsRenameCheckBox.setName("rename");
+               fileOptionsRenameCheckBox.setMnemonic(KeyEvent.VK_N);
+               fileOptionsRenameCheckBox.addActionListener(this);
+               fileOptionsRenameCheckBox.setEnabled(false);
+               fileOptionsRenameTextField = new JTextField();
+               fileOptionsRenameTextField.setEnabled(false);
+               fileOptionsRenameTextField.getDocument().addDocumentListener(new DocumentListener() {
+                       @SuppressWarnings("synthetic-access")
+                       private void storeText(DocumentEvent documentEvent) {
+                               FileOption fileOption = getSelectedFile();
+                               if (fileOption == null) {
+                                       /* no file selected. */
+                                       return;
+                               }
+                               Document document = documentEvent.getDocument();
+                               int documentLength = document.getLength();
+                               try {
+                                       fileOption.setChangedName(document.getText(0, documentLength).trim());
+                               } catch (BadLocationException ble1) {
+                                       /* ignore, it should never happen. */
+                               }
+                       }
++                      @Override
+                       public void changedUpdate(DocumentEvent documentEvent) {
+                               storeText(documentEvent);
+                       }
++                      @Override
+                       public void insertUpdate(DocumentEvent documentEvent) {
+                               storeText(documentEvent);
+                       }
++                      @Override
+                       public void removeUpdate(DocumentEvent documentEvent) {
+                               storeText(documentEvent);
+                       }
+               });
+               fileOptionsPanel.add(fileOptionsRenameCheckBox, new GridBagConstraints(0, 7, 2, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 18, 0, 0), 0, 0));
+               fileOptionsPanel.add(fileOptionsRenameTextField, new GridBagConstraints(2, 7, 3, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
+               fileOptionsMIMETypeComboBox = new JComboBox(MimeTypes.getAllMimeTypes().toArray());
+               fileOptionsMIMETypeComboBox.setToolTipText(I18n.getMessage("jsite.project-files.mime-type.tooltip"));
+               fileOptionsMIMETypeComboBox.setName("project-files.mime-type");
+               fileOptionsMIMETypeComboBox.addActionListener(this);
+               fileOptionsMIMETypeComboBox.setEditable(true);
+               fileOptionsMIMETypeComboBox.setEnabled(false);
+               final TLabel mimeTypeLabel = new TLabel(I18n.getMessage("jsite.project-files.mime-type") + ":", KeyEvent.VK_M, fileOptionsMIMETypeComboBox);
+               fileOptionsPanel.add(mimeTypeLabel, new GridBagConstraints(0, 8, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 18, 0, 0), 0, 0));
+               fileOptionsPanel.add(fileOptionsMIMETypeComboBox, new GridBagConstraints(1, 8, 4, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void run() {
+                               ignoreHiddenFilesCheckBox.setText(I18n.getMessage("jsite.project-files.ignore-hidden-files"));
+                               ignoreHiddenFilesCheckBox.setToolTipText(I18n.getMessage("jsite.projet-files.ignore-hidden-files.tooltip"));
+                               fileOptionsLabel.setText("<html><b>" + I18n.getMessage("jsite.project-files.file-options") + "</b></html>");
+                               defaultFileCheckBox.setText(I18n.getMessage("jsite.project-files.default"));
+                               defaultFileCheckBox.setToolTipText(I18n.getMessage("jsite.project-files.default.tooltip"));
+                               fileOptionsInsertCheckBox.setText(I18n.getMessage("jsite.project-files.insert"));
+                               fileOptionsInsertCheckBox.setToolTipText(I18n.getMessage("jsite.project-files.insert.tooltip"));
+                               fileOptionsForceInsertCheckBox.setText(I18n.getMessage("jsite.project-files.force-insert"));
+                               fileOptionsForceInsertCheckBox.setToolTipText(I18n.getMessage("jsite.project-files.force-insert.tooltip"));
+                               fileOptionsInsertRedirectCheckBox.setText(I18n.getMessage("jsite.project-files.insert-redirect"));
+                               fileOptionsInsertRedirectCheckBox.setToolTipText(I18n.getMessage("jsite.project-files.insert-redirect.tooltip"));
+                               fileOptionsCustomKeyTextField.setToolTipText(I18n.getMessage("jsite.project-files.custom-key.tooltip"));
+                               customKeyLabel.setText(I18n.getMessage("jsite.project-files.custom-key") + ":");
+                               fileOptionsRenameCheckBox.setText("jsite.project-files.rename");
+                               fileOptionsRenameCheckBox.setToolTipText("jsite.project-files.rename.tooltip");
+                               fileOptionsMIMETypeComboBox.setToolTipText(I18n.getMessage("jsite.project-files.mime-type.tooltip"));
+                               mimeTypeLabel.setText(I18n.getMessage("jsite.project-files.mime-type") + ":");
+                       }
+               });
+               return projectFilesPanel;
+       }
+       /**
+        * Sets the project whose files to manage.
+        *
+        * @param project
+        *            The project whose files to manage
+        */
+       public void setProject(final Project project) {
+               this.project = project;
+               setHeading(MessageFormat.format(I18n.getMessage("jsite.project-files.heading"), project.getName()));
+               setDescription(I18n.getMessage("jsite.project-files.description"));
+               ignoreHiddenFilesCheckBox.setSelected(project.isIgnoreHiddenFiles());
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
++                      @Override
+                       public void run() {
+                               setHeading(MessageFormat.format(I18n.getMessage("jsite.project-files.heading"), project.getName()));
+                               setDescription(I18n.getMessage("jsite.project-files.description"));
+                       }
+               });
+       }
+       //
+       // ACTIONS
+       //
+       /**
+        * Rescans the project’s files.
+        */
+       private void actionScan() {
+               projectFileList.clearSelection();
+               projectFileList.setListData(new Object[0]);
+               wizard.setNextEnabled(false);
+               wizard.setPreviousEnabled(false);
+               wizard.setQuitEnabled(false);
+               FileScanner fileScanner = new FileScanner(project);
+               fileScanner.addFileScannerListener(this);
+               new Thread(fileScanner).start();
+       }
+       /**
+        * {@inheritDoc}
+        * <p>
+        * Updates the file list.
+        */
++      @Override
+       public void fileScannerFinished(FileScanner fileScanner) {
+               final boolean error = fileScanner.isError();
+               if (!error) {
+                       final List<ScannedFile> files = fileScanner.getFiles();
+                       SwingUtilities.invokeLater(new Runnable() {
++                              @Override
+                               @SuppressWarnings("synthetic-access")
+                               public void run() {
+                                       projectFileList.setListData(files.toArray());
+                                       projectFileList.clearSelection();
+                               }
+                       });
+                       Set<String> entriesToRemove = new HashSet<String>();
+                       Iterator<String> filenames = new HashSet<String>(project.getFileOptions().keySet()).iterator();
+                       while (filenames.hasNext()) {
+                               String filename = filenames.next();
+                               boolean found = false;
+                               for (ScannedFile scannedFile : files) {
+                                       if (scannedFile.getFilename().equals(filename)) {
+                                               found = true;
+                                               break;
+                                       }
+                               }
+                               if (!found) {
+                                       entriesToRemove.add(filename);
+                               }
+                       }
+                       for (String filename : entriesToRemove) {
+                               project.setFileOption(filename, null);
+                       }
+               } else {
+                       JOptionPane.showMessageDialog(wizard, I18n.getMessage("jsite.project-files.scan-error"), null, JOptionPane.ERROR_MESSAGE);
+               }
+               SwingUtilities.invokeLater(new Runnable() {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void run() {
+                               wizard.setPreviousEnabled(true);
+                               wizard.setNextEnabled(!error);
+                               wizard.setQuitEnabled(true);
+                       }
+               });
+       }
+       /**
+        * Returns the {@link FileOption file options} for the currently selected
+        * file.
+        *
+        * @return The {@link FileOption}s for the selected file, or {@code null} if
+        *         no file is selected
+        */
+       private FileOption getSelectedFile() {
+               ScannedFile scannedFile = (ScannedFile) projectFileList.getSelectedValue();
+               if (scannedFile == null) {
+                       return null;
+               }
+               return project.getFileOption(scannedFile.getFilename());
+       }
+       //
+       // INTERFACE ActionListener
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void actionPerformed(ActionEvent actionEvent) {
+               Object source = actionEvent.getSource();
+               if ((source instanceof JCheckBox) && ("ignore-hidden-files".equals(((JCheckBox) source).getName()))) {
+                       project.setIgnoreHiddenFiles(((JCheckBox) source).isSelected());
+                       actionScan();
+                       return;
+               }
+               ScannedFile scannedFile = (ScannedFile) projectFileList.getSelectedValue();
+               if (scannedFile == null) {
+                       return;
+               }
+               String filename = scannedFile.getFilename();
+               FileOption fileOption = project.getFileOption(filename);
+               if (source instanceof JCheckBox) {
+                       JCheckBox checkBox = (JCheckBox) source;
+                       if ("default-file".equals(checkBox.getName())) {
+                               if (checkBox.isSelected()) {
+                                       if (filename.indexOf('/') > -1) {
+                                               JOptionPane.showMessageDialog(wizard, I18n.getMessage("jsite.project-files.invalid-default-file"), null, JOptionPane.ERROR_MESSAGE);
+                                               checkBox.setSelected(false);
+                                       } else {
+                                               project.setIndexFile(filename);
+                                       }
+                               } else {
+                                       if (filename.equals(project.getIndexFile())) {
+                                               project.setIndexFile(null);
+                                       }
+                               }
+                       } else if ("insert".equals(checkBox.getName())) {
+                               boolean isInsert = checkBox.isSelected();
+                               fileOption.setInsert(isInsert);
+                               fileOptionsInsertRedirectCheckBox.setEnabled(!isInsert);
+                       } else if ("force-insert".equals(checkBox.getName())) {
+                               boolean isForceInsert = checkBox.isSelected();
+                               fileOption.setForceInsert(isForceInsert);
+                       } else if ("insert-redirect".equals(checkBox.getName())) {
+                               boolean isInsertRedirect = checkBox.isSelected();
+                               fileOption.setInsertRedirect(isInsertRedirect);
+                               fileOptionsCustomKeyTextField.setEnabled(isInsertRedirect);
+                       } else if ("rename".equals(checkBox.getName())) {
+                               boolean isRenamed = checkBox.isSelected();
+                               fileOptionsRenameTextField.setEnabled(isRenamed);
+                               fileOption.setChangedName(isRenamed ? fileOptionsRenameTextField.getText() : "");
+                       }
+               } else if (source instanceof JComboBox) {
+                       JComboBox comboBox = (JComboBox) source;
+                       if ("project-files.mime-type".equals(comboBox.getName())) {
+                               fileOption.setMimeType((String) comboBox.getSelectedItem());
+                       }
+               }
+       }
+       //
+       // INTERFACE ListSelectionListener
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       @SuppressWarnings("null")
+       public void valueChanged(ListSelectionEvent e) {
+               ScannedFile scannedFile = (ScannedFile) projectFileList.getSelectedValue();
+               boolean enabled = scannedFile != null;
+               String filename = (scannedFile == null) ? null : scannedFile.getFilename();
+               defaultFileCheckBox.setEnabled(enabled);
+               fileOptionsInsertCheckBox.setEnabled(enabled);
+               fileOptionsRenameCheckBox.setEnabled(enabled);
+               fileOptionsMIMETypeComboBox.setEnabled(enabled);
+               if (filename != null) {
+                       FileOption fileOption = project.getFileOption(filename);
+                       defaultFileCheckBox.setSelected(filename.equals(project.getIndexFile()));
+                       fileOptionsInsertCheckBox.setSelected(fileOption.isInsert());
+                       fileOptionsForceInsertCheckBox.setEnabled(scannedFile.getHash().equals(fileOption.getLastInsertHash()));
+                       fileOptionsForceInsertCheckBox.setSelected(fileOption.isForceInsert());
+                       fileOptionsInsertRedirectCheckBox.setEnabled(!fileOption.isInsert());
+                       fileOptionsInsertRedirectCheckBox.setSelected(fileOption.isInsertRedirect());
+                       fileOptionsCustomKeyTextField.setEnabled(fileOption.isInsertRedirect());
+                       fileOptionsCustomKeyTextField.setText(fileOption.getCustomKey());
+                       fileOptionsRenameCheckBox.setSelected(fileOption.hasChangedName());
+                       fileOptionsRenameTextField.setEnabled(fileOption.hasChangedName());
+                       fileOptionsRenameTextField.setText(fileOption.getChangedName());
+                       fileOptionsMIMETypeComboBox.getModel().setSelectedItem(fileOption.getMimeType());
+               } else {
+                       defaultFileCheckBox.setSelected(false);
+                       fileOptionsInsertCheckBox.setSelected(true);
+                       fileOptionsForceInsertCheckBox.setEnabled(false);
+                       fileOptionsForceInsertCheckBox.setSelected(false);
+                       fileOptionsInsertRedirectCheckBox.setEnabled(false);
+                       fileOptionsInsertRedirectCheckBox.setSelected(false);
+                       fileOptionsCustomKeyTextField.setEnabled(false);
+                       fileOptionsCustomKeyTextField.setText("CHK@");
+                       fileOptionsRenameCheckBox.setEnabled(false);
+                       fileOptionsRenameCheckBox.setSelected(false);
+                       fileOptionsRenameTextField.setEnabled(false);
+                       fileOptionsRenameTextField.setText("");
+                       fileOptionsMIMETypeComboBox.getModel().setSelectedItem(MimeTypes.DEFAULT_CONTENT_TYPE);
+               }
+       }
+       //
+       // INTERFACE DocumentListener
+       //
+       /**
+        * Updates the options of the currently selected file with the changes made
+        * in the “custom key” textfield.
+        *
+        * @param documentEvent
+        *            The document event to process
+        */
+       private void processDocumentUpdate(DocumentEvent documentEvent) {
+               ScannedFile scannedFile = (ScannedFile) projectFileList.getSelectedValue();
+               if (scannedFile == null) {
+                       return;
+               }
+               FileOption fileOption = project.getFileOption(scannedFile.getFilename());
+               Document document = documentEvent.getDocument();
+               try {
+                       String text = document.getText(0, document.getLength());
+                       fileOption.setCustomKey(text);
+               } catch (BadLocationException ble1) {
+                       /* ignore. */
+               }
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void changedUpdate(DocumentEvent documentEvent) {
+               processDocumentUpdate(documentEvent);
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void insertUpdate(DocumentEvent documentEvent) {
+               processDocumentUpdate(documentEvent);
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void removeUpdate(DocumentEvent documentEvent) {
+               processDocumentUpdate(documentEvent);
+       }
+ }
index 0000000,2a3e5e5..b73a49a
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,534 +1,552 @@@
 -      private String formatNumber(double number, int digits) {
+ /*
+  * jSite - ProjectInsertPage.java - Copyright © 2006–2012 David Roden
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * 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, write to the Free Software
+  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+  */
+ package de.todesbaum.jsite.gui;
+ import java.awt.BorderLayout;
+ import java.awt.Font;
+ import java.awt.GridBagConstraints;
+ import java.awt.GridBagLayout;
+ import java.awt.Insets;
+ import java.awt.Toolkit;
+ import java.awt.datatransfer.Clipboard;
+ import java.awt.datatransfer.ClipboardOwner;
+ import java.awt.datatransfer.StringSelection;
+ import java.awt.datatransfer.Transferable;
+ import java.awt.event.ActionEvent;
+ import java.awt.event.KeyEvent;
+ import java.text.DateFormat;
+ import java.text.MessageFormat;
+ import java.util.Date;
+ import java.util.logging.Level;
+ import java.util.logging.Logger;
+ import javax.swing.AbstractAction;
+ import javax.swing.Action;
+ import javax.swing.JButton;
+ import javax.swing.JComponent;
+ import javax.swing.JLabel;
+ import javax.swing.JOptionPane;
+ import javax.swing.JPanel;
+ import javax.swing.JProgressBar;
+ import javax.swing.JTextField;
+ import javax.swing.SwingUtilities;
+ import net.pterodactylus.util.io.StreamCopier.ProgressListener;
+ import de.todesbaum.jsite.application.AbortedException;
+ import de.todesbaum.jsite.application.Freenet7Interface;
+ import de.todesbaum.jsite.application.InsertListener;
+ import de.todesbaum.jsite.application.Project;
+ import de.todesbaum.jsite.application.ProjectInserter;
+ import de.todesbaum.jsite.i18n.I18n;
+ import de.todesbaum.jsite.i18n.I18nContainer;
+ import de.todesbaum.util.freenet.fcp2.ClientPutDir.ManifestPutter;
+ import de.todesbaum.util.freenet.fcp2.PriorityClass;
+ import de.todesbaum.util.swing.TWizard;
+ import de.todesbaum.util.swing.TWizardPage;
+ /**
+  * Wizard page that shows the progress of an insert.
+  *
+  * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
+  */
+ public class ProjectInsertPage extends TWizardPage implements InsertListener, ClipboardOwner {
+       /** The logger. */
+       private static final Logger logger = Logger.getLogger(ProjectInsertPage.class.getName());
+       /** The project inserter. */
+       private ProjectInserter projectInserter;
+       /** The “copy URI” action. */
+       private Action copyURIAction;
+       /** The “request URI” textfield. */
+       private JTextField requestURITextField;
+       /** The “start time” label. */
+       private JLabel startTimeLabel;
+       /** The progress bar. */
+       private JProgressBar progressBar;
+       /** The start time of the insert. */
+       private long startTime = 0;
+       /** The number of inserted blocks. */
+       private volatile int insertedBlocks;
+       /** Whether the “copy URI to clipboard” button was used. */
+       private boolean uriCopied;
+       /** Whether the insert is currently running. */
+       private volatile boolean running = false;
+       /**
+        * Creates a new progress insert wizard page.
+        *
+        * @param wizard
+        *            The wizard this page belongs to
+        */
+       public ProjectInsertPage(final TWizard wizard) {
+               super(wizard);
+               createActions();
+               pageInit();
+               setHeading(I18n.getMessage("jsite.insert.heading"));
+               setDescription(I18n.getMessage("jsite.insert.description"));
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
++                      @Override
+                       public void run() {
+                               setHeading(I18n.getMessage("jsite.insert.heading"));
+                               setDescription(I18n.getMessage("jsite.insert.description"));
+                       }
+               });
+               projectInserter = new ProjectInserter();
+               projectInserter.addInsertListener(this);
+       }
+       /**
+        * Creates all used actions.
+        */
+       private void createActions() {
+               copyURIAction = new AbstractAction(I18n.getMessage("jsite.project.action.copy-uri")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               actionCopyURI();
+                       }
+               };
+               copyURIAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project.action.copy-uri.tooltip"));
+               copyURIAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_U);
+               copyURIAction.setEnabled(false);
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void run() {
+                               copyURIAction.putValue(Action.NAME, I18n.getMessage("jsite.project.action.copy-uri"));
+                               copyURIAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project.action.copy-uri.tooltip"));
+                       }
+               });
+       }
+       /**
+        * Initializes the page.
+        */
+       private void pageInit() {
+               setLayout(new BorderLayout(12, 12));
+               add(createProjectInsertPanel(), BorderLayout.CENTER);
+       }
+       /**
+        * Creates the main panel.
+        *
+        * @return The main panel
+        */
+       private JComponent createProjectInsertPanel() {
+               JComponent projectInsertPanel = new JPanel(new GridBagLayout());
+               requestURITextField = new JTextField();
+               requestURITextField.setEditable(false);
+               startTimeLabel = new JLabel();
+               progressBar = new JProgressBar(0, 1);
+               progressBar.setStringPainted(true);
+               progressBar.setValue(0);
+               final JLabel projectInformationLabel = new JLabel("<html><b>" + I18n.getMessage("jsite.insert.project-information") + "</b></html>");
+               projectInsertPanel.add(projectInformationLabel, new GridBagConstraints(0, 0, 2, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
+               final JLabel requestURILabel = new JLabel(I18n.getMessage("jsite.insert.request-uri") + ":");
+               projectInsertPanel.add(requestURILabel, new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(6, 18, 0, 0), 0, 0));
+               projectInsertPanel.add(requestURITextField, new GridBagConstraints(1, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
+               final JLabel startTimeLeftLabel = new JLabel(I18n.getMessage("jsite.insert.start-time") + ":");
+               projectInsertPanel.add(startTimeLeftLabel, new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(6, 18, 0, 0), 0, 0));
+               projectInsertPanel.add(startTimeLabel, new GridBagConstraints(1, 2, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
+               final JLabel progressLabel = new JLabel(I18n.getMessage("jsite.insert.progress") + ":");
+               projectInsertPanel.add(progressLabel, new GridBagConstraints(0, 3, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(6, 18, 0, 0), 0, 0));
+               projectInsertPanel.add(progressBar, new GridBagConstraints(1, 3, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
+               projectInsertPanel.add(new JButton(copyURIAction), new GridBagConstraints(0, 4, 2, 1, 0.0, 0.0, GridBagConstraints.LINE_END, GridBagConstraints.NONE, new Insets(12, 18, 0, 0), 0, 0));
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void run() {
+                               projectInformationLabel.setText("<html><b>" + I18n.getMessage("jsite.insert.project-information") + "</b></html>");
+                               requestURILabel.setText(I18n.getMessage("jsite.insert.request-uri") + ":");
+                               startTimeLeftLabel.setText(I18n.getMessage("jsite.insert.start-time") + ":");
+                               if (startTime != 0) {
+                                       startTimeLabel.setText(DateFormat.getDateTimeInstance().format(new Date(startTime)));
+                               } else {
+                                       startTimeLabel.setText("");
+                               }
+                               progressLabel.setText(I18n.getMessage("jsite.insert.progress") + ":");
+                       }
+               });
+               return projectInsertPanel;
+       }
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void pageAdded(TWizard wizard) {
+               this.wizard.setPreviousName(I18n.getMessage("jsite.wizard.previous"));
+               this.wizard.setPreviousEnabled(false);
+               this.wizard.setNextName(I18n.getMessage("jsite.general.cancel"));
+               this.wizard.setQuitName(I18n.getMessage("jsite.wizard.quit"));
+       }
+       /**
+        * Starts the insert.
+        */
+       public void startInsert() {
+               running = true;
+               copyURIAction.setEnabled(false);
+               progressBar.setValue(0);
+               progressBar.setString(I18n.getMessage("jsite.insert.starting"));
+               progressBar.setFont(progressBar.getFont().deriveFont(Font.PLAIN));
+               projectInserter.start(new ProgressListener() {
++                      @Override
+                       public void onProgress(final long copied, final long length) {
+                               SwingUtilities.invokeLater(new Runnable() {
+                                       /**
+                                        * {@inheritDoc}
+                                        */
++                                      @Override
+                                       @SuppressWarnings("synthetic-access")
+                                       public void run() {
+                                               int divisor = 1;
+                                               while (((copied / divisor) > Integer.MAX_VALUE) || ((length / divisor) > Integer.MAX_VALUE)) {
+                                                       divisor *= 10;
+                                               }
+                                               progressBar.setMaximum((int) (length / divisor));
+                                               progressBar.setValue((int) (copied / divisor));
+                                               progressBar.setString("Uploaded: " + copied + " / " + length);
+                                       }
+                               });
+                       }
+               });
+       }
+       /**
+        * Stops the currently running insert.
+        */
+       public void stopInsert() {
+               if (running) {
+                       wizard.setNextEnabled(false);
+                       projectInserter.stop();
+               }
+       }
+       /**
+        * Returns whether the insert is currently running.
+        *
+        * @return {@code true} if the insert is currently running, {@code false}
+        *         otherwise
+        */
+       public boolean isRunning() {
+               return running;
+       }
+       /**
+        * Sets the project to insert.
+        *
+        * @param project
+        *            The project to insert
+        */
+       public void setProject(final Project project) {
+               projectInserter.setProject(project);
+               SwingUtilities.invokeLater(new Runnable() {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void run() {
+                               requestURITextField.setText(project.getFinalRequestURI(1));
+                       }
+               });
+       }
+       /**
+        * Sets the freenet interface to use.
+        *
+        * @param freenetInterface
+        *            The freenet interface to use
+        */
+       public void setFreenetInterface(Freenet7Interface freenetInterface) {
+               projectInserter.setFreenetInterface(freenetInterface);
+       }
+       /**
+        * Sets the project inserter’s temp directory.
+        *
+        * @see ProjectInserter#setTempDirectory(String)
+        * @param tempDirectory
+        *            The temp directory to use, or {@code null} to use the system
+        *            default
+        */
+       public void setTempDirectory(String tempDirectory) {
+               projectInserter.setTempDirectory(tempDirectory);
+       }
+       /**
+        * Returns whether the “copy URI to clipboard” button was used.
+        *
+        * @return {@code true} if an URI was copied to clipboard, {@code false}
+        *         otherwise
+        */
+       public boolean wasUriCopied() {
+               return uriCopied;
+       }
+       /**
+        * Sets whether to use the “early encode“ flag for the insert.
+        *
+        * @param useEarlyEncode
+        *            {@code true} to set the “early encode” flag for the insert,
+        *            {@code false} otherwise
+        */
+       public void setUseEarlyEncode(boolean useEarlyEncode) {
+               projectInserter.setUseEarlyEncode(useEarlyEncode);
+       }
+       /**
+        * Sets the insert priority.
+        *
+        * @param priority
+        *            The insert priority
+        */
+       public void setPriority(PriorityClass priority) {
+               projectInserter.setPriority(priority);
+       }
+       /**
+        * Sets the manifest putter to use for the insert.
+        *
+        * @see ProjectInserter#setManifestPutter(ManifestPutter)
+        * @param manifestPutter
+        *            The manifest putter
+        */
+       public void setManifestPutter(ManifestPutter manifestPutter) {
+               projectInserter.setManifestPutter(manifestPutter);
+       }
+       //
+       // INTERFACE InsertListener
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void projectInsertStarted(final Project project) {
+               SwingUtilities.invokeLater(new Runnable() {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void run() {
+                               startTimeLabel.setText(DateFormat.getDateTimeInstance().format(new Date()));
+                       }
+               });
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void projectUploadFinished(Project project) {
+               startTime = System.currentTimeMillis();
+               SwingUtilities.invokeLater(new Runnable() {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void run() {
+                               progressBar.setString(I18n.getMessage("jsite.insert.starting"));
+                               progressBar.setValue(0);
+                       }
+               });
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void projectURIGenerated(Project project, final String uri) {
+               SwingUtilities.invokeLater(new Runnable() {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void run() {
+                               copyURIAction.setEnabled(true);
+                               requestURITextField.setText(uri);
+                       }
+               });
+               logger.log(Level.FINEST, "Insert generated URI: " + uri);
+               int slash = uri.indexOf('/');
+               slash = uri.indexOf('/', slash + 1);
+               int secondSlash = uri.indexOf('/', slash + 1);
+               if (secondSlash == -1) {
+                       secondSlash = uri.length();
+               }
+               String editionNumber = uri.substring(slash + 1, secondSlash);
+               logger.log(Level.FINEST, "Extracted edition number: " + editionNumber);
+               int edition = -1;
+               try {
+                       edition = Integer.valueOf(editionNumber);
+               } catch (NumberFormatException nfe1) {
+                       /* ignore. */
+               }
+               logger.log(Level.FINEST, "Insert edition: " + edition + ", Project edition: " + project.getEdition());
+               if ((edition != -1) && (edition == project.getEdition())) {
+                       JOptionPane.showMessageDialog(this, I18n.getMessage("jsite.insert.reinserted-edition"), I18n.getMessage("jsite.insert.reinserted-edition.title"), JOptionPane.INFORMATION_MESSAGE);
+               }
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void projectInsertProgress(Project project, final int succeeded, final int failed, final int fatal, final int total, final boolean finalized) {
+               insertedBlocks = succeeded;
+               SwingUtilities.invokeLater(new Runnable() {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void run() {
+                               if (total == 0) {
+                                       return;
+                               }
+                               progressBar.setMaximum(total);
+                               progressBar.setValue(succeeded + failed + fatal);
+                               int progress = (succeeded + failed + fatal) * 100 / total;
+                               StringBuilder progressString = new StringBuilder();
+                               progressString.append(progress).append("% (");
+                               progressString.append(succeeded + failed + fatal).append('/').append(total);
+                               progressString.append(") (");
+                               progressString.append(getTransferRate());
+                               progressString.append(' ').append(I18n.getMessage("jsite.insert.k-per-s")).append(')');
+                               progressBar.setString(progressString.toString());
+                               if (finalized) {
+                                       progressBar.setFont(progressBar.getFont().deriveFont(Font.BOLD));
+                               }
+                       }
+               });
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void projectInsertFinished(Project project, boolean success, Throwable cause) {
+               running = false;
+               if (success) {
+                       String copyURILabel = I18n.getMessage("jsite.insert.okay-copy-uri");
+                       int selectedValue = JOptionPane.showOptionDialog(this, I18n.getMessage("jsite.insert.inserted"), I18n.getMessage("jsite.insert.done.title"), 0, JOptionPane.INFORMATION_MESSAGE, null, new Object[] { I18n.getMessage("jsite.general.ok"), copyURILabel }, copyURILabel);
+                       if (selectedValue == 1) {
+                               actionCopyURI();
+                       }
+               } else {
+                       if (cause == null) {
+                               JOptionPane.showMessageDialog(this, I18n.getMessage("jsite.insert.insert-failed"), I18n.getMessage("jsite.insert.insert-failed.title"), JOptionPane.ERROR_MESSAGE);
+                       } else {
+                               if (cause instanceof AbortedException) {
+                                       JOptionPane.showMessageDialog(this, I18n.getMessage("jsite.insert.insert-aborted"), I18n.getMessage("jsite.insert.insert-aborted.title"), JOptionPane.INFORMATION_MESSAGE);
+                               } else {
+                                       JOptionPane.showMessageDialog(this, MessageFormat.format(I18n.getMessage("jsite.insert.insert-failed-with-cause"), cause.getMessage()), I18n.getMessage("jsite.insert.insert-failed.title"), JOptionPane.ERROR_MESSAGE);
+                               }
+                       }
+               }
+               SwingUtilities.invokeLater(new Runnable() {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void run() {
+                               progressBar.setValue(progressBar.getMaximum());
+                               progressBar.setString(I18n.getMessage("jsite.insert.done") + " (" + getTransferRate() + " " + I18n.getMessage("jsite.insert.k-per-s") + ")");
+                               wizard.setNextName(I18n.getMessage("jsite.wizard.next"));
+                               wizard.setNextEnabled(true);
+                               wizard.setQuitEnabled(true);
+                       }
+               });
+       }
+       //
+       // ACTIONS
+       //
+       /**
+        * Copies the request URI of the project to the clipboard.
+        */
+       private void actionCopyURI() {
+               uriCopied = true;
+               Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+               clipboard.setContents(new StringSelection(requestURITextField.getText()), this);
+       }
+       /**
+        * Formats the given number so that it always has the the given number of
+        * fractional digits.
+        *
+        * @param number
+        *            The number to format
+        * @param digits
+        *            The number of fractional digits
+        * @return The formatted number
+        */
++      private static String formatNumber(double number, int digits) {
+               int multiplier = (int) Math.pow(10, digits);
+               String formattedNumber = String.valueOf((int) (number * multiplier) / (double) multiplier);
+               if (formattedNumber.indexOf('.') == -1) {
+                       formattedNumber += '.';
+                       for (int digit = 0; digit < digits; digit++) {
+                               formattedNumber += "0";
+                       }
+               }
+               return formattedNumber;
+       }
+       /**
+        * Returns the formatted transfer rate at this point.
+        *
+        * @return The formatted transfer rate
+        */
+       private String getTransferRate() {
+               return formatNumber(insertedBlocks * 32.0 / ((System.currentTimeMillis() - startTime) / 1000), 1);
+       }
+       //
+       // INTERFACE ClipboardOwner
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void lostOwnership(Clipboard clipboard, Transferable contents) {
+               /* ignore. */
+       }
+ }
index 0000000,9345dbf..a072423
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,724 +1,739 @@@
+ /*
+  * jSite - ProjectPage.java - Copyright © 2006–2012 David Roden
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * 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, write to the Free Software
+  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+  */
+ package de.todesbaum.jsite.gui;
+ import java.awt.BorderLayout;
+ import java.awt.Dimension;
+ import java.awt.FlowLayout;
+ import java.awt.GridBagConstraints;
+ import java.awt.GridBagLayout;
+ import java.awt.Insets;
+ import java.awt.Toolkit;
+ import java.awt.datatransfer.Clipboard;
+ import java.awt.datatransfer.ClipboardOwner;
+ import java.awt.datatransfer.StringSelection;
+ import java.awt.datatransfer.Transferable;
+ import java.awt.event.ActionEvent;
+ import java.awt.event.KeyEvent;
+ import java.io.IOException;
+ import java.text.MessageFormat;
+ import javax.swing.AbstractAction;
+ import javax.swing.Action;
+ import javax.swing.JButton;
+ import javax.swing.JComponent;
+ import javax.swing.JFileChooser;
+ import javax.swing.JLabel;
+ import javax.swing.JList;
+ import javax.swing.JOptionPane;
+ import javax.swing.JPanel;
+ import javax.swing.JScrollPane;
+ import javax.swing.JTextField;
+ import javax.swing.ListSelectionModel;
+ import javax.swing.border.EmptyBorder;
+ import javax.swing.event.DocumentEvent;
+ import javax.swing.event.DocumentListener;
+ import javax.swing.event.ListSelectionEvent;
+ import javax.swing.event.ListSelectionListener;
+ import javax.swing.text.AbstractDocument;
+ import javax.swing.text.AttributeSet;
+ import javax.swing.text.BadLocationException;
+ import javax.swing.text.Document;
+ import javax.swing.text.DocumentFilter;
+ import net.pterodactylus.util.swing.SortedListModel;
+ import de.todesbaum.jsite.application.Freenet7Interface;
+ import de.todesbaum.jsite.application.KeyDialog;
+ import de.todesbaum.jsite.application.Project;
+ import de.todesbaum.jsite.i18n.I18n;
+ import de.todesbaum.jsite.i18n.I18nContainer;
+ import de.todesbaum.util.swing.TLabel;
+ import de.todesbaum.util.swing.TWizard;
+ import de.todesbaum.util.swing.TWizardPage;
+ /**
+  * Wizard page that lets the user manage his projects and start inserts.
+  *
+  * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
+  */
+ public class ProjectPage extends TWizardPage implements ListSelectionListener, DocumentListener, ClipboardOwner {
+       /** The freenet interface. */
+       private Freenet7Interface freenetInterface;
+       /** The “browse” action. */
+       private Action projectLocalPathBrowseAction;
+       /** The “add project” action. */
+       private Action projectAddAction;
+       /** The “delete project” action. */
+       private Action projectDeleteAction;
+       /** The “clone project” action. */
+       private Action projectCloneAction;
+       /** The “manage keys” action. */
+       private Action projectManageKeysAction;
+       /** The “copy URI” action. */
+       private Action projectCopyURIAction;
+       /** The “reset edition” action. */
+       private Action projectResetEditionAction;
+       /** The file chooser. */
+       private JFileChooser pathChooser;
+       /** The project list model. */
+       private SortedListModel<Project> projectListModel;
+       /** The project list scroll pane. */
+       private JScrollPane projectScrollPane;
+       /** The project list. */
+       private JList projectList;
+       /** The project name textfield. */
+       private JTextField projectNameTextField;
+       /** The project description textfield. */
+       private JTextField projectDescriptionTextField;
+       /** The local path textfield. */
+       private JTextField projectLocalPathTextField;
+       /** The textfield for the complete URI. */
+       private JTextField projectCompleteUriTextField;
+       /** The project path textfield. */
+       private JTextField projectPathTextField;
+       /** Whether the “copy URI to clipboard” action was used. */
+       private boolean uriCopied;
+       /**
+        * Creates a new project page.
+        *
+        * @param wizard
+        *            The wizard this page belongs to
+        */
+       public ProjectPage(final TWizard wizard) {
+               super(wizard);
+               setLayout(new BorderLayout(12, 12));
+               dialogInit();
+               setHeading(I18n.getMessage("jsite.project.heading"));
+               setDescription(I18n.getMessage("jsite.project.description"));
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
++                      @Override
+                       public void run() {
+                               setHeading(I18n.getMessage("jsite.project.heading"));
+                               setDescription(I18n.getMessage("jsite.project.description"));
+                       }
+               });
+       }
+       /**
+        * Initializes the page.
+        */
+       private void dialogInit() {
+               createActions();
+               pathChooser = new JFileChooser();
+               projectListModel = new SortedListModel<Project>();
+               projectList = new JList(projectListModel);
+               projectList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+               projectList.addListSelectionListener(this);
+               add(projectScrollPane = new JScrollPane(projectList), BorderLayout.LINE_START);
+               projectScrollPane.setPreferredSize(new Dimension(150, projectList.getPreferredSize().height));
+               add(createInformationPanel(), BorderLayout.CENTER);
+       }
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void pageAdded(TWizard wizard) {
+               super.pageAdded(wizard);
+               projectList.clearSelection();
+               this.wizard.setPreviousName(I18n.getMessage("jsite.menu.nodes.manage-nodes"));
+               this.wizard.setNextName(I18n.getMessage("jsite.wizard.next"));
+               this.wizard.setQuitName(I18n.getMessage("jsite.wizard.quit"));
+               this.wizard.setNextEnabled(false);
+       }
+       /**
+        * Adds the given listener to the list of listeners.
+        *
+        * @param listener
+        *            The listener to add
+        */
+       public void addListSelectionListener(ListSelectionListener listener) {
+               projectList.addListSelectionListener(listener);
+       }
+       /**
+        * Removes the given listener from the list of listeners.
+        *
+        * @param listener
+        *            The listener to remove
+        */
+       public void removeListSelectionListener(ListSelectionListener listener) {
+               projectList.removeListSelectionListener(listener);
+       }
+       /**
+        * Creates all actions.
+        */
+       private void createActions() {
+               projectLocalPathBrowseAction = new AbstractAction(I18n.getMessage("jsite.project.action.browse")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               actionLocalPathBrowse();
+                       }
+               };
+               projectLocalPathBrowseAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project.action.browse.tooltip"));
+               projectLocalPathBrowseAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_B);
+               projectLocalPathBrowseAction.setEnabled(false);
+               projectAddAction = new AbstractAction(I18n.getMessage("jsite.project.action.add-project")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               actionAdd();
+                       }
+               };
+               projectAddAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project.action.add-project.tooltip"));
+               projectAddAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_A);
+               projectDeleteAction = new AbstractAction(I18n.getMessage("jsite.project.action.delete-project")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               actionDelete();
+                       }
+               };
+               projectDeleteAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project.action.delete-project.tooltip"));
+               projectDeleteAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_D);
+               projectDeleteAction.setEnabled(false);
+               projectCloneAction = new AbstractAction(I18n.getMessage("jsite.project.action.clone-project")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               actionClone();
+                       }
+               };
+               projectCloneAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project.action.clone-project.tooltip"));
+               projectCloneAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_L);
+               projectCloneAction.setEnabled(false);
+               projectCopyURIAction = new AbstractAction(I18n.getMessage("jsite.project.action.copy-uri")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               actionCopyURI();
+                       }
+               };
+               projectCopyURIAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project.action.copy-uri.tooltip"));
+               projectCopyURIAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_U);
+               projectCopyURIAction.setEnabled(false);
+               projectManageKeysAction = new AbstractAction(I18n.getMessage("jsite.project.action.manage-keys")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               actionManageKeys();
+                       }
+               };
+               projectManageKeysAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project.action.manage-keys.tooltip"));
+               projectManageKeysAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_M);
+               projectManageKeysAction.setEnabled(false);
+               projectResetEditionAction = new AbstractAction(I18n.getMessage("jsite.project.action.reset-edition")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               actionResetEdition();
+                       }
+               };
+               projectResetEditionAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project.action.reset-edition.tooltip"));
+               projectResetEditionAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_R);
+               projectResetEditionAction.setEnabled(false);
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void run() {
+                               projectLocalPathBrowseAction.putValue(Action.NAME, I18n.getMessage("jsite.project.action.browse"));
+                               projectLocalPathBrowseAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project.action.browse.tooltip"));
+                               projectAddAction.putValue(Action.NAME, I18n.getMessage("jsite.project.action.add-project"));
+                               projectAddAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project.action.add-project.tooltip"));
+                               projectDeleteAction.putValue(Action.NAME, I18n.getMessage("jsite.project.action.delete-project"));
+                               projectDeleteAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project.action.delete-project.tooltip"));
+                               projectCloneAction.putValue(Action.NAME, I18n.getMessage("jsite.project.action.clone-project"));
+                               projectCloneAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project.action.clone-project.tooltip"));
+                               projectCopyURIAction.putValue(Action.NAME, I18n.getMessage("jsite.project.action.copy-uri"));
+                               projectCopyURIAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project.action.copy-uri.tooltip"));
+                               projectManageKeysAction.putValue(Action.NAME, I18n.getMessage("jsite.project.action.manage-keys"));
+                               projectManageKeysAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project.action.manage-keys.tooltip"));
+                               projectResetEditionAction.putValue(Action.NAME, I18n.getMessage("jsite.project.action.reset-edition"));
+                               projectResetEditionAction.putValue(Action.SHORT_DESCRIPTION, I18n.getMessage("jsite.project.action.reset-edition.tooltip"));
+                               pathChooser.setApproveButtonText(I18n.getMessage("jsite.project.action.browse.choose"));
+                       }
+               });
+       }
+       /**
+        * Creates the information panel.
+        *
+        * @return The information panel
+        */
+       private JComponent createInformationPanel() {
+               JPanel informationPanel = new JPanel(new BorderLayout(12, 12));
+               JPanel informationTable = new JPanel(new GridBagLayout());
+               JPanel functionButtons = new JPanel(new FlowLayout(FlowLayout.LEADING, 12, 12));
+               functionButtons.setBorder(new EmptyBorder(-12, -12, -12, -12));
+               functionButtons.add(new JButton(projectAddAction));
+               functionButtons.add(new JButton(projectDeleteAction));
+               functionButtons.add(new JButton(projectCloneAction));
+               functionButtons.add(new JButton(projectManageKeysAction));
+               informationPanel.add(functionButtons, BorderLayout.PAGE_START);
+               informationPanel.add(informationTable, BorderLayout.CENTER);
+               final JLabel projectInformationLabel = new JLabel("<html><b>" + I18n.getMessage("jsite.project.project.information") + "</b></html>");
+               informationTable.add(projectInformationLabel, new GridBagConstraints(0, 0, 3, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
+               projectNameTextField = new JTextField();
+               projectNameTextField.getDocument().putProperty("name", "project.name");
+               projectNameTextField.getDocument().addDocumentListener(this);
+               projectNameTextField.setEnabled(false);
+               final TLabel projectNameLabel = new TLabel(I18n.getMessage("jsite.project.project.name") + ":", KeyEvent.VK_N, projectNameTextField);
+               informationTable.add(projectNameLabel, new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 18, 0, 0), 0, 0));
+               informationTable.add(projectNameTextField, new GridBagConstraints(1, 1, 2, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
+               projectDescriptionTextField = new JTextField();
+               projectDescriptionTextField.getDocument().putProperty("name", "project.description");
+               projectDescriptionTextField.getDocument().addDocumentListener(this);
+               projectDescriptionTextField.setEnabled(false);
+               final TLabel projectDescriptionLabel = new TLabel(I18n.getMessage("jsite.project.project.description") + ":", KeyEvent.VK_D, projectDescriptionTextField);
+               informationTable.add(projectDescriptionLabel, new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 18, 0, 0), 0, 0));
+               informationTable.add(projectDescriptionTextField, new GridBagConstraints(1, 2, 2, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
+               projectLocalPathTextField = new JTextField();
+               projectLocalPathTextField.getDocument().putProperty("name", "project.localpath");
+               projectLocalPathTextField.getDocument().addDocumentListener(this);
+               projectLocalPathTextField.setEnabled(false);
+               final TLabel projectLocalPathLabel = new TLabel(I18n.getMessage("jsite.project.project.local-path") + ":", KeyEvent.VK_L, projectLocalPathTextField);
+               informationTable.add(projectLocalPathLabel, new GridBagConstraints(0, 3, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 18, 0, 0), 0, 0));
+               informationTable.add(projectLocalPathTextField, new GridBagConstraints(1, 3, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
+               informationTable.add(new JButton(projectLocalPathBrowseAction), new GridBagConstraints(2, 3, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
+               final JLabel projectAddressLabel = new JLabel("<html><b>" + I18n.getMessage("jsite.project.project.address") + "</b></html>");
+               informationTable.add(projectAddressLabel, new GridBagConstraints(0, 4, 3, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(12, 0, 0, 0), 0, 0));
+               projectPathTextField = new JTextField();
+               projectPathTextField.getDocument().putProperty("name", "project.path");
+               projectPathTextField.getDocument().addDocumentListener(this);
+               ((AbstractDocument) projectPathTextField.getDocument()).setDocumentFilter(new DocumentFilter() {
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException {
+                               super.insertString(fb, offset, string.replaceAll("/", ""), attr);
+                               updateCompleteURI();
+                       }
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
+                               super.replace(fb, offset, length, text.replaceAll("/", ""), attrs);
+                               updateCompleteURI();
+                       }
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void remove(FilterBypass fb, int offset, int length) throws BadLocationException {
+                               super.remove(fb, offset, length);
+                               updateCompleteURI();
+                       }
+               });
+               projectPathTextField.setEnabled(false);
+               final TLabel projectPathLabel = new TLabel(I18n.getMessage("jsite.project.project.path") + ":", KeyEvent.VK_P, projectPathTextField);
+               informationTable.add(projectPathLabel, new GridBagConstraints(0, 5, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 18, 0, 0), 0, 0));
+               informationTable.add(projectPathTextField, new GridBagConstraints(1, 5, 2, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
+               projectCompleteUriTextField = new JTextField();
+               projectCompleteUriTextField.setEditable(false);
+               final TLabel projectUriLabel = new TLabel(I18n.getMessage("jsite.project.project.uri") + ":", KeyEvent.VK_U, projectCompleteUriTextField);
+               informationTable.add(projectUriLabel, new GridBagConstraints(0, 6, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(6, 18, 0, 0), 0, 0));
+               informationTable.add(projectCompleteUriTextField, new GridBagConstraints(1, 6, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
+               informationTable.add(new JButton(projectCopyURIAction), new GridBagConstraints(2, 6, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(6, 6, 0, 0), 0, 0));
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
++                      @Override
+                       public void run() {
+                               projectInformationLabel.setText("<html><b>" + I18n.getMessage("jsite.project.project.information") + "</b></html>");
+                               projectNameLabel.setText(I18n.getMessage("jsite.project.project.name") + ":");
+                               projectDescriptionLabel.setText(I18n.getMessage("jsite.project.project.description") + ":");
+                               projectLocalPathLabel.setText(I18n.getMessage("jsite.project.project.local-path") + ":");
+                               projectAddressLabel.setText("<html><b>" + I18n.getMessage("jsite.project.project.address") + "</b></html>");
+                               projectPathLabel.setText(I18n.getMessage("jsite.project.project.path") + ":");
+                               projectUriLabel.setText(I18n.getMessage("jsite.project.project.uri") + ":");
+                       }
+               });
+               return informationPanel;
+       }
+       /**
+        * Sets the project list.
+        *
+        * @param projects
+        *            The list of projects
+        */
+       public void setProjects(Project[] projects) {
+               projectListModel.clear();
+               for (Project project : projects) {
+                       projectListModel.add(project);
+               }
+       }
+       /**
+        * Returns the list of projects.
+        *
+        * @return The list of projects
+        */
+       public Project[] getProjects() {
+               return projectListModel.toArray(new Project[projectListModel.size()]);
+       }
+       /**
+        * Sets the freenet interface to use.
+        *
+        * @param freenetInterface
+        *            The freenetInterface to use
+        */
+       public void setFreenetInterface(Freenet7Interface freenetInterface) {
+               this.freenetInterface = freenetInterface;
+       }
+       /**
+        * Returns the currently selected project.
+        *
+        * @return The currently selected project
+        */
+       public Project getSelectedProject() {
+               return (Project) projectList.getSelectedValue();
+       }
+       /**
+        * Returns whether the “copy URI to clipboard” button was used.
+        *
+        * @return {@code true} if the “copy URI to clipboard” button was used,
+        *         {@code false} otherwise
+        */
+       public boolean wasUriCopied() {
+               return uriCopied;
+       }
+       /**
+        * Updates the currently selected project with changed information from a
+        * textfield.
+        *
+        * @param documentEvent
+        *            The document event to process
+        */
+       private void setTextField(DocumentEvent documentEvent) {
+               Document document = documentEvent.getDocument();
+               String propertyName = (String) document.getProperty("name");
+               Project project = (Project) projectList.getSelectedValue();
+               if (project == null) {
+                       return;
+               }
+               try {
+                       String text = document.getText(0, document.getLength()).trim();
+                       if ("project.name".equals(propertyName)) {
+                               project.setName(text);
+                               projectList.repaint();
+                       } else if ("project.description".equals(propertyName)) {
+                               project.setDescription(text);
+                       } else if ("project.localpath".equals(propertyName)) {
+                               project.setLocalPath(text);
+                       } else if ("project.privatekey".equals(propertyName)) {
+                               project.setInsertURI(text);
+                       } else if ("project.publickey".equals(propertyName)) {
+                               project.setRequestURI(text);
+                       } else if ("project.path".equals(propertyName)) {
+                               project.setPath(text);
+                       }
+               } catch (BadLocationException e) {
+                       /* ignore. */
+               }
+       }
+       //
+       // ACTIONS
+       //
+       /**
+        * Lets the user choose a local path for a project.
+        */
+       private void actionLocalPathBrowse() {
+               Project project = (Project) projectList.getSelectedValue();
+               if (project == null) {
+                       return;
+               }
+               pathChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+               if (pathChooser.showDialog(this, I18n.getMessage("jsite.project.action.browse.choose")) == JFileChooser.APPROVE_OPTION) {
+                       projectLocalPathTextField.setText(pathChooser.getSelectedFile().getPath());
+               }
+       }
+       /**
+        * Adds a new project.
+        */
+       private void actionAdd() {
+               String[] keyPair = null;
+               if (!freenetInterface.hasNode()) {
+                       JOptionPane.showMessageDialog(this, I18n.getMessage("jsite.project-files.no-node-selected"), null, JOptionPane.ERROR_MESSAGE);
+                       return;
+               }
+               try {
+                       keyPair = freenetInterface.generateKeyPair();
+               } catch (IOException ioe1) {
+                       JOptionPane.showMessageDialog(this, MessageFormat.format(I18n.getMessage("jsite.project.keygen.io-error"), ioe1.getMessage()), null, JOptionPane.ERROR_MESSAGE);
+                       return;
+               }
+               Project newProject = new Project();
+               newProject.setName(I18n.getMessage("jsite.project.new-project.name"));
+               newProject.setInsertURI(keyPair[0]);
+               newProject.setRequestURI(keyPair[1]);
+               newProject.setEdition(-1);
+               newProject.setPath("");
+               projectListModel.add(newProject);
+               projectScrollPane.revalidate();
+               projectScrollPane.repaint();
+               projectList.setSelectedIndex(projectListModel.indexOf(newProject));
+       }
+       /**
+        * Deletes the currently selected project.
+        */
+       private void actionDelete() {
+               int selectedIndex = projectList.getSelectedIndex();
+               if (selectedIndex > -1) {
+                       if (JOptionPane.showConfirmDialog(this, MessageFormat.format(I18n.getMessage("jsite.project.action.delete-project.confirm"), ((Project) projectList.getSelectedValue()).getName()), null, JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.OK_OPTION) {
+                               projectListModel.remove(selectedIndex);
+                               projectList.clearSelection();
+                               if (projectListModel.getSize() != 0) {
+                                       projectList.setSelectedIndex(Math.min(selectedIndex, projectListModel.getSize() - 1));
+                               }
+                       }
+               }
+       }
+       /**
+        * Clones the currently selected project.
+        */
+       private void actionClone() {
+               int selectedIndex = projectList.getSelectedIndex();
+               if (selectedIndex > -1) {
+                       Project newProject = new Project((Project) projectList.getSelectedValue());
+                       newProject.setName(MessageFormat.format(I18n.getMessage("jsite.project.action.clone-project.copy"), newProject.getName()));
+                       projectListModel.add(newProject);
+                       projectList.setSelectedIndex(projectListModel.indexOf(newProject));
+               }
+       }
+       /**
+        * Copies the request URI of the currently selected project to the
+        * clipboard.
+        */
+       private void actionCopyURI() {
+               int selectedIndex = projectList.getSelectedIndex();
+               if (selectedIndex > -1) {
+                       Project selectedProject = (Project) projectList.getSelectedValue();
+                       Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+                       clipboard.setContents(new StringSelection(selectedProject.getFinalRequestURI(0)), this);
+                       uriCopied = true;
+               }
+       }
+       /**
+        * Opens a {@link KeyDialog} and lets the user manipulate the keys of the
+        * project.
+        */
+       private void actionManageKeys() {
+               int selectedIndex = projectList.getSelectedIndex();
+               if (selectedIndex > -1) {
+                       Project selectedProject = (Project) projectList.getSelectedValue();
+                       KeyDialog keyDialog = new KeyDialog(freenetInterface, wizard);
+                       keyDialog.setPrivateKey(selectedProject.getInsertURI());
+                       keyDialog.setPublicKey(selectedProject.getRequestURI());
+                       keyDialog.setVisible(true);
+                       if (!keyDialog.wasCancelled()) {
+                               String originalPublicKey = selectedProject.getRequestURI();
+                               String originalPrivateKey = selectedProject.getInsertURI();
+                               selectedProject.setInsertURI(keyDialog.getPrivateKey());
+                               selectedProject.setRequestURI(keyDialog.getPublicKey());
+                               if (!originalPublicKey.equals(selectedProject.getRequestURI()) || !originalPrivateKey.equals(selectedProject.getInsertURI())) {
+                                       selectedProject.setEdition(-1);
+                               }
+                               updateCompleteURI();
+                       }
+               }
+       }
+       /**
+        * Resets the edition of the currently selected project.
+        */
+       private void actionResetEdition() {
+               if (JOptionPane.showConfirmDialog(this, I18n.getMessage("jsite.project.warning.reset-edition"), null, JOptionPane.OK_CANCEL_OPTION) == JOptionPane.CANCEL_OPTION) {
+                       return;
+               }
+               int selectedIndex = projectList.getSelectedIndex();
+               if (selectedIndex > -1) {
+                       Project selectedProject = (Project) projectList.getSelectedValue();
+                       selectedProject.setEdition(-1);
+                       updateCompleteURI();
+               }
+       }
+       /**
+        * Updates the complete URI text field.
+        */
+       private void updateCompleteURI() {
+               int selectedIndex = projectList.getSelectedIndex();
+               if (selectedIndex > -1) {
+                       Project selectedProject = (Project) projectList.getSelectedValue();
+                       projectCompleteUriTextField.setText(selectedProject.getFinalRequestURI(0));
+               }
+       }
+       //
+       // INTERFACE ListSelectionListener
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void valueChanged(ListSelectionEvent listSelectionEvent) {
+               int selectedRow = projectList.getSelectedIndex();
+               Project selectedProject = (Project) projectList.getSelectedValue();
+               projectNameTextField.setEnabled(selectedRow > -1);
+               projectDescriptionTextField.setEnabled(selectedRow > -1);
+               projectLocalPathTextField.setEnabled(selectedRow > -1);
+               projectPathTextField.setEnabled(selectedRow > -1);
+               projectLocalPathBrowseAction.setEnabled(selectedRow > -1);
+               projectDeleteAction.setEnabled(selectedRow > -1);
+               projectCloneAction.setEnabled(selectedRow > -1);
+               projectCopyURIAction.setEnabled(selectedRow > -1);
+               projectManageKeysAction.setEnabled(selectedRow > -1);
+               projectResetEditionAction.setEnabled(selectedRow > -1);
+               if (selectedRow > -1) {
+                       projectNameTextField.setText(selectedProject.getName());
+                       projectDescriptionTextField.setText(selectedProject.getDescription());
+                       projectLocalPathTextField.setText(selectedProject.getLocalPath());
+                       projectPathTextField.setText(selectedProject.getPath());
+                       projectCompleteUriTextField.setText("freenet:" + selectedProject.getFinalRequestURI(0));
+               } else {
+                       projectNameTextField.setText("");
+                       projectDescriptionTextField.setText("");
+                       projectLocalPathTextField.setText("");
+                       projectPathTextField.setText("");
+                       projectCompleteUriTextField.setText("");
+               }
+       }
+       //
+       // INTERFACE ChangeListener
+       //
+       //
+       // INTERFACE DocumentListener
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void insertUpdate(DocumentEvent documentEvent) {
+               setTextField(documentEvent);
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void removeUpdate(DocumentEvent documentEvent) {
+               setTextField(documentEvent);
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void changedUpdate(DocumentEvent documentEvent) {
+               setTextField(documentEvent);
+       }
+       //
+       // INTERFACE ClipboardOwner
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void lostOwnership(Clipboard clipboard, Transferable contents) {
+               /* ignore. */
+       }
+ }
index 0000000,da2e0c3..9683dd3
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,90 +1,91 @@@
+ /*
+  * jSite - I18nContainer.java - Copyright © 2007–2012 David Roden
+  *
+  * This program is free software; you can redistribute it and/or modify it under
+  * the terms of the GNU General Public License as published by the Free Software
+  * Foundation; either version 2 of the License, or (at your option) any later
+  * version.
+  *
+  * 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, write to the Free Software Foundation, Inc., 59 Temple
+  * Place - Suite 330, Boston, MA 02111-1307, USA.
+  */
+ package de.todesbaum.jsite.i18n;
+ import java.util.ArrayList;
+ import java.util.Collections;
+ import java.util.Iterator;
+ import java.util.List;
+ /**
+  * Container that collects {@link Runnable}s that change the texts of GUI
+  * components when the current locale has changed.
+  *
+  * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
+  */
+ public class I18nContainer implements Iterable<Runnable> {
+       /** The container singleton. */
+       private static final I18nContainer singleton = new I18nContainer();
+       /** The list of runnables that change texts. */
+       private final List<Runnable> i18nRunnables = Collections.synchronizedList(new ArrayList<Runnable>());
+       /**
+        * The list of runnables that change texts and run after
+        * {@link #i18nRunnables}.
+        */
+       private final List<Runnable> i18nPostRunnables = Collections.synchronizedList(new ArrayList<Runnable>());
+       /**
+        * Returns the singleton instance.
+        *
+        * @return The singleton instance
+        */
+       public static I18nContainer getInstance() {
+               return singleton;
+       }
+       /**
+        * Registers an i18n runnable that is run when the current locale has
+        * changed.
+        *
+        * @param i18nRunnable
+        *            The runnable to register
+        */
+       public void registerRunnable(Runnable i18nRunnable) {
+               i18nRunnables.add(i18nRunnable);
+       }
+       /**
+        * Registers a {@link Runnable} that changes texts when the current locale
+        * has changed and runs after {@link #i18nRunnables} have run.
+        *
+        * @param i18nPostRunnable
+        *            The runnable to register
+        */
+       public void registerPostRunnable(Runnable i18nPostRunnable) {
+               i18nPostRunnables.add(i18nPostRunnable);
+       }
+       /**
+        * {@inheritDoc}
+        * <p>
+        * Returns a combined list of {@link #i18nRunnables} and
+        * {@link #i18nPostRunnables}, in that order.
+        */
++      @Override
+       public Iterator<Runnable> iterator() {
+               List<Runnable> allRunnables = new ArrayList<Runnable>();
+               allRunnables.addAll(i18nRunnables);
+               allRunnables.addAll(i18nPostRunnables);
+               return allRunnables.iterator();
+       }
+ }
index 0000000,0cf9cc4..bbe76c7
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,297 +1,303 @@@
+ /*
+  * jSite - CLI.java - Copyright © 2006–2012 David Roden
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * 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, write to the Free Software
+  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+  */
+ package de.todesbaum.jsite.main;
+ import java.io.PrintWriter;
+ import net.pterodactylus.util.io.StreamCopier.ProgressListener;
+ import de.todesbaum.jsite.application.Freenet7Interface;
+ import de.todesbaum.jsite.application.InsertListener;
+ import de.todesbaum.jsite.application.Node;
+ import de.todesbaum.jsite.application.Project;
+ import de.todesbaum.jsite.application.ProjectInserter;
+ /**
+  * Command-line interface for jSite.
+  *
+  * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
+  */
+ public class CLI implements InsertListener {
+       /** Object used for synchronization. */
+       private Object lockObject = new Object();
+       /** Writer for the console. */
+       private PrintWriter outputWriter = new PrintWriter(System.out, true);
+       /** The freenet interface. */
+       private Freenet7Interface freenetInterface;
+       /** The project inserter. */
+       private ProjectInserter projectInserter = new ProjectInserter();
+       /** The list of nodes. */
+       private Node[] nodes;
+       /** The projects. */
+       private Project[] projects;
+       /** Whether the insert has finished. */
+       private boolean finished = false;
+       /** Whether the insert finished successfully. */
+       private boolean success;
+       /**
+        * Creates a new command-line interface.
+        *
+        * @param args
+        *            The command-line arguments
+        */
+       private CLI(String[] args) {
+               if ((args.length == 0) || args[0].equals("-h") || args[0].equals("--help")) {
+                       outputWriter.println("\nParameters:\n");
+                       outputWriter.println("  --config-file=<configuration file>");
+                       outputWriter.println("  --node=<node name>");
+                       outputWriter.println("  --project=<project name>");
+                       outputWriter.println("  --local-directory=<local directory>");
+                       outputWriter.println("  --path=<path>");
+                       outputWriter.println("  --edition=<edition>");
+                       outputWriter.println("\nA project gets inserted when a new project is loaded on the command line,");
+                       outputWriter.println("or when the command line is finished. --local-directory, --path, and --edition");
+                       outputWriter.println("override the parameters in the project.");
+                       return;
+               }
+               String configFile = System.getProperty("user.home") + "/.jSite/config7";
+               for (String argument : args) {
+                       String value = argument.substring(argument.indexOf('=') + 1).trim();
+                       if (argument.startsWith("--config-file=")) {
+                               configFile = value;
+                       }
+               }
+               ConfigurationLocator configurationLocator = new ConfigurationLocator();
+               if (configFile != null) {
+                       configurationLocator.setCustomLocation(configFile);
+               }
+               Configuration configuration = new Configuration(configurationLocator, configurationLocator.findPreferredLocation());
+               projectInserter.addInsertListener(this);
+               projects = configuration.getProjects();
+               Node node = configuration.getSelectedNode();
+               nodes = configuration.getNodes();
+               freenetInterface = new Freenet7Interface();
+               freenetInterface.setNode(node);
+               projectInserter.setFreenetInterface(freenetInterface);
+               Project currentProject = null;
+               for (String argument : args) {
+                       if (argument.startsWith("--config-file=")) {
+                               /* we already parsed this one. */
+                               continue;
+                       }
+                       String value = argument.substring(argument.indexOf('=') + 1).trim();
+                       if (argument.startsWith("--node=")) {
+                               Node newNode = getNode(value);
+                               if (newNode == null) {
+                                       outputWriter.println("Node \"" + value + "\" not found.");
+                                       return;
+                               }
+                               node = newNode;
+                               freenetInterface.setNode(node);
+                       } else if (argument.startsWith("--project=")) {
+                               if (currentProject != null) {
+                                       if (insertProject(currentProject)) {
+                                               outputWriter.println("Project \"" + currentProject.getName() + "\" successfully inserted.");
+                                       } else {
+                                               outputWriter.println("Project \"" + currentProject.getName() + "\" was not successfully inserted.");
+                                       }
+                                       currentProject = null;
+                               }
+                               currentProject = getProject(value);
+                               if (currentProject == null) {
+                                       outputWriter.println("Project \"" + value + "\" not found.");
+                               }
+                       } else if (argument.startsWith("--local-directory")) {
+                               if (currentProject == null) {
+                                       outputWriter.println("You can't specifiy --local-directory before --project.");
+                                       return;
+                               }
+                               currentProject.setLocalPath(value);
+                       } else if (argument.startsWith("--path=")) {
+                               if (currentProject == null) {
+                                       outputWriter.println("You can't specify --path before --project.");
+                                       return;
+                               }
+                               currentProject.setPath(value);
+                       } else if (argument.startsWith("--edition=")) {
+                               if (currentProject == null) {
+                                       outputWriter.println("You can't specify --edition before --project.");
+                                       return;
+                               }
+                               currentProject.setEdition(Integer.parseInt(value));
+                       } else {
+                               outputWriter.println("Unknown parameter: " + argument);
+                               return;
+                       }
+               }
+               int errorCode = 1;
+               if (currentProject != null) {
+                       if (insertProject(currentProject)) {
+                               outputWriter.println("Project \"" + currentProject.getName() + "\" successfully inserted.");
+                               errorCode = 0;
+                       } else {
+                               outputWriter.println("Project \"" + currentProject.getName() + "\" was not successfully inserted.");
+                       }
+               }
+               configuration.setProjects(projects);
+               configuration.save();
+               System.exit(errorCode);
+       }
+       /**
+        * Returns the project with the given name.
+        *
+        * @param name
+        *            The name of the project
+        * @return The project, or <code>null</code> if no project could be found
+        */
+       private Project getProject(String name) {
+               for (Project project : projects) {
+                       if (project.getName().equals(name)) {
+                               return project;
+                       }
+               }
+               return null;
+       }
+       /**
+        * Returns the node with the given name.
+        *
+        * @param name
+        *            The name of the node
+        * @return The node, or <code>null</code> if no node could be found
+        */
+       private Node getNode(String name) {
+               for (Node node : nodes) {
+                       if (node.getName().equals(name)) {
+                               return node;
+                       }
+               }
+               return null;
+       }
+       /**
+        * Inserts the given project.
+        *
+        * @param currentProject
+        *            The project to insert
+        * @return <code>true</code> if the insert finished successfully,
+        *         <code>false</code> otherwise
+        */
+       private boolean insertProject(Project currentProject) {
+               if (!freenetInterface.hasNode()) {
+                       outputWriter.println("Node is not running!");
+                       return false;
+               }
+               projectInserter.setProject(currentProject);
+               projectInserter.start(new ProgressListener() {
++                      @Override
+                       public void onProgress(long copied, long length) {
+                               System.out.print("Uploaded: " + copied + " / " + length + " bytes...\r");
+                       }
+               });
+               synchronized (lockObject) {
+                       while (!finished) {
+                               try {
+                                       lockObject.wait();
+                               } catch (InterruptedException e) {
+                                       /* ignore, we're in a loop. */
+                               }
+                       }
+               }
+               return success;
+       }
+       //
+       // INTERFACE InsertListener
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void projectInsertStarted(Project project) {
+               outputWriter.println("Starting Insert of project \"" + project.getName() + "\".");
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void projectUploadFinished(Project project) {
+               outputWriter.println("Project \"" + project.getName() + "\" has been uploaded, starting insert...");
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void projectURIGenerated(Project project, String uri) {
+               outputWriter.println("URI: " + uri);
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void projectInsertProgress(Project project, int succeeded, int failed, int fatal, int total, boolean finalized) {
+               outputWriter.println("Progress: " + succeeded + " done, " + failed + " failed, " + fatal + " fatal, " + total + " total" + (finalized ? " (finalized)" : "") + ", " + ((succeeded + failed + fatal) * 100 / total) + "%");
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void projectInsertFinished(Project project, boolean success, Throwable cause) {
+               outputWriter.println("Request URI: " + project.getFinalRequestURI(0));
+               finished = true;
+               this.success = success;
+               synchronized (lockObject) {
+                       lockObject.notify();
+               }
+       }
+       //
+       // MAIN
+       //
+       /**
+        * Creates a new command-line interface with the given arguments.
+        *
+        * @param args
+        *            The command-line arguments
+        */
+       public static void main(String[] args) {
+               new CLI(args);
+       }
+ }
index 0000000,38a0f1c..6621931
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,753 +1,768 @@@
 -      private Locale findSupportedLocale(Locale forLocale) {
+ /*
+  * jSite - Main.java - Copyright © 2006–2012 David Roden
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * 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, write to the Free Software
+  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+  */
+ package de.todesbaum.jsite.main;
+ import java.awt.Component;
+ import java.awt.event.ActionEvent;
+ import java.awt.event.ActionListener;
+ import java.io.IOException;
+ import java.text.MessageFormat;
+ import java.util.Date;
+ import java.util.HashMap;
+ import java.util.Locale;
+ import java.util.Map;
+ import java.util.logging.ConsoleHandler;
+ import java.util.logging.Handler;
+ import java.util.logging.Level;
+ import java.util.logging.Logger;
+ import javax.swing.AbstractAction;
+ import javax.swing.Action;
+ import javax.swing.ButtonGroup;
+ import javax.swing.Icon;
+ import javax.swing.JList;
+ import javax.swing.JMenu;
+ import javax.swing.JMenuBar;
+ import javax.swing.JMenuItem;
+ import javax.swing.JOptionPane;
+ import javax.swing.JPanel;
+ import javax.swing.JRadioButtonMenuItem;
+ import javax.swing.event.ListSelectionEvent;
+ import javax.swing.event.ListSelectionListener;
+ import net.pterodactylus.util.image.IconLoader;
+ import de.todesbaum.jsite.application.Freenet7Interface;
+ import de.todesbaum.jsite.application.Node;
+ import de.todesbaum.jsite.application.Project;
+ import de.todesbaum.jsite.application.ProjectInserter;
+ import de.todesbaum.jsite.application.ProjectInserter.CheckReport;
+ import de.todesbaum.jsite.application.ProjectInserter.Issue;
+ import de.todesbaum.jsite.application.UpdateChecker;
+ import de.todesbaum.jsite.application.UpdateListener;
+ import de.todesbaum.jsite.gui.NodeManagerListener;
+ import de.todesbaum.jsite.gui.NodeManagerPage;
+ import de.todesbaum.jsite.gui.PreferencesPage;
+ import de.todesbaum.jsite.gui.ProjectFilesPage;
+ import de.todesbaum.jsite.gui.ProjectInsertPage;
+ import de.todesbaum.jsite.gui.ProjectPage;
+ import de.todesbaum.jsite.i18n.I18n;
+ import de.todesbaum.jsite.i18n.I18nContainer;
+ import de.todesbaum.jsite.main.ConfigurationLocator.ConfigurationLocation;
+ import de.todesbaum.util.swing.TWizard;
+ import de.todesbaum.util.swing.TWizardPage;
+ import de.todesbaum.util.swing.WizardListener;
+ /**
+  * The main class that ties together everything.
+  *
+  * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
+  */
+ public class Main implements ActionListener, ListSelectionListener, WizardListener, NodeManagerListener, UpdateListener {
+       /** The logger. */
+       private static final Logger logger = Logger.getLogger(Main.class.getName());
+       /** The version. */
+       private static final Version VERSION = new Version(0, 10);
+       /** The configuration. */
+       private Configuration configuration;
+       /** The freenet interface. */
+       private Freenet7Interface freenetInterface = new Freenet7Interface();
+       /** The update checker. */
+       private final UpdateChecker updateChecker;
+       /** The jSite icon. */
+       private Icon jSiteIcon;
+       /**
+        * Enumeration for all possible pages.
+        *
+        * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
+        */
+       private static enum PageType {
+               /** The node manager page. */
+               PAGE_NODE_MANAGER,
+               /** The project page. */
+               PAGE_PROJECTS,
+               /** The project files page. */
+               PAGE_PROJECT_FILES,
+               /** The project insert page. */
+               PAGE_INSERT_PROJECT,
+               /** The preferences page. */
+               PAGE_PREFERENCES
+       }
+       /** The supported locales. */
+       private static final Locale[] SUPPORTED_LOCALES = new Locale[] { Locale.ENGLISH, Locale.GERMAN, Locale.FRENCH };
+       /** The actions that switch the language. */
+       private Map<Locale, Action> languageActions = new HashMap<Locale, Action>();
+       /** The “manage nodes” action. */
+       private Action manageNodeAction;
+       /** The “preferences” action. */
+       private Action optionsPreferencesAction;
+       /** The “check for updates” action. */
+       private Action checkForUpdatesAction;
+       /** The “about jSite” action. */
+       private Action aboutAction;
+       /** The wizard. */
+       private TWizard wizard;
+       /** The node menu. */
+       private JMenu nodeMenu;
+       /** The currently selected node. */
+       private Node selectedNode;
+       /** Mapping from page type to page. */
+       private final Map<PageType, TWizardPage> pages = new HashMap<PageType, TWizardPage>();
+       /** The original location of the configuration file. */
+       private ConfigurationLocation originalLocation;
+       /**
+        * Creates a new core with the default configuration file.
+        */
+       private Main() {
+               this(null);
+       }
+       /**
+        * Creates a new core with the given configuration from the given file.
+        *
+        * @param configFilename
+        *            The name of the configuration file
+        */
+       private Main(String configFilename) {
+               /* collect all possible configuration file locations. */
+               ConfigurationLocator configurationLocator = new ConfigurationLocator();
+               if (configFilename != null) {
+                       configurationLocator.setCustomLocation(configFilename);
+               }
+               originalLocation = configurationLocator.findPreferredLocation();
+               logger.log(Level.CONFIG, "Using configuration from " + originalLocation + ".");
+               configuration = new Configuration(configurationLocator, originalLocation);
+               Locale.setDefault(configuration.getLocale());
+               I18n.setLocale(configuration.getLocale());
+               wizard = new TWizard();
+               createActions();
+               wizard.setJMenuBar(createMenuBar());
+               wizard.setQuitName(I18n.getMessage("jsite.wizard.quit"));
+               wizard.setPreviousEnabled(false);
+               wizard.setNextEnabled(true);
+               wizard.addWizardListener(this);
+               jSiteIcon = IconLoader.loadIcon("/jsite-icon.png");
+               wizard.setIcon(jSiteIcon);
+               updateChecker = new UpdateChecker(freenetInterface);
+               updateChecker.addUpdateListener(this);
+               updateChecker.start();
+               initPages();
+               showPage(PageType.PAGE_PROJECTS);
+       }
+       /**
+        * Creates all actions.
+        */
+       private void createActions() {
+               for (final Locale locale : SUPPORTED_LOCALES) {
+                       languageActions.put(locale, new AbstractAction(I18n.getMessage("jsite.menu.language." + locale.getLanguage()), IconLoader.loadIcon("/flag-" + locale.getLanguage() + ".png")) {
++                              @Override
+                               @SuppressWarnings("synthetic-access")
+                               public void actionPerformed(ActionEvent actionEvent) {
+                                       switchLanguage(locale);
+                               }
+                       });
+               }
+               manageNodeAction = new AbstractAction(I18n.getMessage("jsite.menu.nodes.manage-nodes")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               showPage(PageType.PAGE_NODE_MANAGER);
+                               optionsPreferencesAction.setEnabled(true);
+                               wizard.setPreviousName(I18n.getMessage("jsite.wizard.previous"));
+                               wizard.setNextName(I18n.getMessage("jsite.wizard.next"));
+                       }
+               };
+               optionsPreferencesAction = new AbstractAction(I18n.getMessage("jsite.menu.options.preferences")) {
+                       /**
+                        * {@inheritDoc}
+                        */
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               optionsPreferences();
+                       }
+               };
+               checkForUpdatesAction = new AbstractAction(I18n.getMessage("jsite.menu.help.check-for-updates")) {
+                       /**
+                        * {@inheritDoc}
+                        */
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent actionEvent) {
+                               showLatestUpdate();
+                       }
+               };
+               aboutAction = new AbstractAction(I18n.getMessage("jsite.menu.help.about")) {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void actionPerformed(ActionEvent e) {
+                               JOptionPane.showMessageDialog(wizard, MessageFormat.format(I18n.getMessage("jsite.about.message"), getVersion().toString()), null, JOptionPane.INFORMATION_MESSAGE, jSiteIcon);
+                       }
+               };
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void run() {
+                               manageNodeAction.putValue(Action.NAME, I18n.getMessage("jsite.menu.nodes.manage-nodes"));
+                               optionsPreferencesAction.putValue(Action.NAME, I18n.getMessage("jsite.menu.options.preferences"));
+                               checkForUpdatesAction.putValue(Action.NAME, I18n.getMessage("jsite.menu.help.check-for-updates"));
+                               aboutAction.putValue(Action.NAME, I18n.getMessage("jsite.menu.help.about"));
+                       }
+               });
+       }
+       /**
+        * Creates the menu bar.
+        *
+        * @return The menu bar
+        */
+       private JMenuBar createMenuBar() {
+               JMenuBar menuBar = new JMenuBar();
+               final JMenu languageMenu = new JMenu(I18n.getMessage("jsite.menu.languages"));
+               menuBar.add(languageMenu);
+               ButtonGroup languageButtonGroup = new ButtonGroup();
+               for (Locale locale : SUPPORTED_LOCALES) {
+                       Action languageAction = languageActions.get(locale);
+                       JRadioButtonMenuItem menuItem = new JRadioButtonMenuItem(languageActions.get(locale));
+                       if (locale.equals(Locale.getDefault())) {
+                               menuItem.setSelected(true);
+                       }
+                       languageAction.putValue("menuItem", menuItem);
+                       languageButtonGroup.add(menuItem);
+                       languageMenu.add(menuItem);
+               }
+               nodeMenu = new JMenu(I18n.getMessage("jsite.menu.nodes"));
+               menuBar.add(nodeMenu);
+               selectedNode = configuration.getSelectedNode();
+               nodesUpdated(configuration.getNodes());
+               final JMenu optionsMenu = new JMenu(I18n.getMessage("jsite.menu.options"));
+               menuBar.add(optionsMenu);
+               optionsMenu.add(optionsPreferencesAction);
+               /* evil hack to right-align the help menu */
+               JPanel panel = new JPanel();
+               panel.setOpaque(false);
+               menuBar.add(panel);
+               final JMenu helpMenu = new JMenu(I18n.getMessage("jsite.menu.help"));
+               menuBar.add(helpMenu);
+               helpMenu.add(checkForUpdatesAction);
+               helpMenu.add(aboutAction);
+               I18nContainer.getInstance().registerRunnable(new Runnable() {
++                      @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void run() {
+                               languageMenu.setText(I18n.getMessage("jsite.menu.languages"));
+                               nodeMenu.setText(I18n.getMessage("jsite.menu.nodes"));
+                               optionsMenu.setText(I18n.getMessage("jsite.menu.options"));
+                               helpMenu.setText(I18n.getMessage("jsite.menu.help"));
+                               for (Map.Entry<Locale, Action> languageActionEntry : languageActions.entrySet()) {
+                                       languageActionEntry.getValue().putValue(Action.NAME, I18n.getMessage("jsite.menu.language." + languageActionEntry.getKey().getLanguage()));
+                               }
+                       }
+               });
+               return menuBar;
+       }
+       /**
+        * Initializes all pages.
+        */
+       private void initPages() {
+               NodeManagerPage nodeManagerPage = new NodeManagerPage(wizard);
+               nodeManagerPage.setName("page.node-manager");
+               nodeManagerPage.addNodeManagerListener(this);
+               nodeManagerPage.setNodes(configuration.getNodes());
+               pages.put(PageType.PAGE_NODE_MANAGER, nodeManagerPage);
+               ProjectPage projectPage = new ProjectPage(wizard);
+               projectPage.setName("page.project");
+               projectPage.setProjects(configuration.getProjects());
+               projectPage.setFreenetInterface(freenetInterface);
+               projectPage.addListSelectionListener(this);
+               pages.put(PageType.PAGE_PROJECTS, projectPage);
+               ProjectFilesPage projectFilesPage = new ProjectFilesPage(wizard);
+               projectFilesPage.setName("page.project.files");
+               pages.put(PageType.PAGE_PROJECT_FILES, projectFilesPage);
+               ProjectInsertPage projectInsertPage = new ProjectInsertPage(wizard);
+               projectInsertPage.setName("page.project.insert");
+               projectInsertPage.setFreenetInterface(freenetInterface);
+               pages.put(PageType.PAGE_INSERT_PROJECT, projectInsertPage);
+               PreferencesPage preferencesPage = new PreferencesPage(wizard);
+               preferencesPage.setName("page.preferences");
+               preferencesPage.setTempDirectory(configuration.getTempDirectory());
+               pages.put(PageType.PAGE_PREFERENCES, preferencesPage);
+       }
+       /**
+        * Shows the page with the given type.
+        *
+        * @param pageType
+        *            The page type to show
+        */
+       private void showPage(PageType pageType) {
+               wizard.setPreviousEnabled(pageType.ordinal() > 0);
+               wizard.setNextEnabled(pageType.ordinal() < (pages.size() - 1));
+               wizard.setPage(pages.get(pageType));
+               wizard.setTitle(pages.get(pageType).getHeading() + " - jSite");
+       }
+       /**
+        * Returns whether a configuration file would be overwritten when calling
+        * {@link #saveConfiguration()}.
+        *
+        * @return {@code true} if {@link #saveConfiguration()} would overwrite an
+        *         existing file, {@code false} otherwise
+        */
+       private boolean isOverwritingConfiguration() {
+               return configuration.getConfigurationLocator().hasFile(configuration.getConfigurationDirectory());
+       }
+       /**
+        * Saves the configuration.
+        *
+        * @return <code>true</code> if the configuration could be saved,
+        *         <code>false</code> otherwise
+        */
+       private boolean saveConfiguration() {
+               NodeManagerPage nodeManagerPage = (NodeManagerPage) pages.get(PageType.PAGE_NODE_MANAGER);
+               configuration.setNodes(nodeManagerPage.getNodes());
+               if (selectedNode != null) {
+                       configuration.setSelectedNode(selectedNode);
+               }
+               ProjectPage projectPage = (ProjectPage) pages.get(PageType.PAGE_PROJECTS);
+               configuration.setProjects(projectPage.getProjects());
+               PreferencesPage preferencesPage = (PreferencesPage) pages.get(PageType.PAGE_PREFERENCES);
+               configuration.setTempDirectory(preferencesPage.getTempDirectory());
+               return configuration.save();
+       }
+       /**
+        * Finds a supported locale for the given locale.
+        *
+        * @param forLocale
+        *            The locale to find a supported locale for
+        * @return The supported locale that was found, or the default locale if no
+        *         supported locale could be found
+        */
++      private static Locale findSupportedLocale(Locale forLocale) {
+               for (Locale locale : SUPPORTED_LOCALES) {
+                       if (locale.equals(forLocale)) {
+                               return locale;
+                       }
+               }
+               for (Locale locale : SUPPORTED_LOCALES) {
+                       if (locale.getCountry().equals(forLocale.getCountry()) && locale.getLanguage().equals(forLocale.getLanguage())) {
+                               return locale;
+                       }
+               }
+               for (Locale locale : SUPPORTED_LOCALES) {
+                       if (locale.getLanguage().equals(forLocale.getLanguage())) {
+                               return locale;
+                       }
+               }
+               return SUPPORTED_LOCALES[0];
+       }
+       /**
+        * Returns the version.
+        *
+        * @return The version
+        */
+       public static final Version getVersion() {
+               return VERSION;
+       }
+       //
+       // ACTIONS
+       //
+       /**
+        * Switches the language of the interface to the given locale.
+        *
+        * @param locale
+        *            The locale to switch to
+        */
+       private void switchLanguage(Locale locale) {
+               Locale supportedLocale = findSupportedLocale(locale);
+               Action languageAction = languageActions.get(supportedLocale);
+               JRadioButtonMenuItem menuItem = (JRadioButtonMenuItem) languageAction.getValue("menuItem");
+               menuItem.setSelected(true);
+               I18n.setLocale(supportedLocale);
+               for (Runnable i18nRunnable : I18nContainer.getInstance()) {
+                       try {
+                               i18nRunnable.run();
+                       } catch (Throwable t) {
+                               /* we probably shouldn't swallow this. */
+                       }
+               }
+               wizard.setPage(wizard.getPage());
+               configuration.setLocale(supportedLocale);
+       }
+       /**
+        * Shows a dialog with general preferences.
+        */
+       private void optionsPreferences() {
+               ((PreferencesPage) pages.get(PageType.PAGE_PREFERENCES)).setConfigurationLocation(configuration.getConfigurationDirectory());
+               ((PreferencesPage) pages.get(PageType.PAGE_PREFERENCES)).setHasNextToJarConfiguration(configuration.getConfigurationLocator().isValidLocation(ConfigurationLocation.NEXT_TO_JAR_FILE));
+               ((PreferencesPage) pages.get(PageType.PAGE_PREFERENCES)).setHasCustomConfiguration(configuration.getConfigurationLocator().isValidLocation(ConfigurationLocation.CUSTOM));
+               ((PreferencesPage) pages.get(PageType.PAGE_PREFERENCES)).setUseEarlyEncode(configuration.useEarlyEncode());
+               ((PreferencesPage) pages.get(PageType.PAGE_PREFERENCES)).setPriority(configuration.getPriority());
+               ((PreferencesPage) pages.get(PageType.PAGE_PREFERENCES)).setManifestPutter(configuration.getManifestPutter());
+               showPage(PageType.PAGE_PREFERENCES);
+               optionsPreferencesAction.setEnabled(false);
+               wizard.setNextEnabled(true);
+               wizard.setNextName(I18n.getMessage("jsite.wizard.next"));
+       }
+       /**
+        * Shows a dialog box that shows the last version that was found by the
+        * {@link UpdateChecker}.
+        */
+       private void showLatestUpdate() {
+               Version latestVersion = updateChecker.getLatestVersion();
+               int versionDifference = latestVersion.compareTo(VERSION);
+               if (versionDifference > 0) {
+                       JOptionPane.showMessageDialog(wizard, MessageFormat.format(I18n.getMessage("jsite.update-checker.latest-version.newer.message"), VERSION, latestVersion), I18n.getMessage("jsite.update-checker.latest-version.title"), JOptionPane.INFORMATION_MESSAGE);
+               } else if (versionDifference < 0) {
+                       JOptionPane.showMessageDialog(wizard, MessageFormat.format(I18n.getMessage("jsite.update-checker.latest-version.older.message"), VERSION, latestVersion), I18n.getMessage("jsite.update-checker.latest-version.title"), JOptionPane.INFORMATION_MESSAGE);
+               } else {
+                       JOptionPane.showMessageDialog(wizard, MessageFormat.format(I18n.getMessage("jsite.update-checker.latest-version.okay.message"), VERSION, latestVersion), I18n.getMessage("jsite.update-checker.latest-version.title"), JOptionPane.INFORMATION_MESSAGE);
+               }
+       }
+       //
+       // INTERFACE ListSelectionListener
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void valueChanged(ListSelectionEvent e) {
+               JList list = (JList) e.getSource();
+               int selectedRow = list.getSelectedIndex();
+               wizard.setNextEnabled(selectedRow > -1);
+       }
+       //
+       // INTERFACE WizardListener
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void wizardNextPressed(TWizard wizard) {
+               String pageName = wizard.getPage().getName();
+               if ("page.node-manager".equals(pageName)) {
+                       showPage(PageType.PAGE_PROJECTS);
+               } else if ("page.project".equals(pageName)) {
+                       ProjectPage projectPage = (ProjectPage) wizard.getPage();
+                       Project project = projectPage.getSelectedProject();
+                       if ((project.getLocalPath() == null) || (project.getLocalPath().trim().length() == 0)) {
+                               JOptionPane.showMessageDialog(wizard, I18n.getMessage("jsite.warning.no-local-path"), null, JOptionPane.ERROR_MESSAGE);
+                               return;
+                       }
+                       if ((project.getPath() == null) || (project.getPath().trim().length() == 0)) {
+                               JOptionPane.showMessageDialog(wizard, I18n.getMessage("jsite.warning.no-path"), null, JOptionPane.ERROR_MESSAGE);
+                               return;
+                       }
+                       ((ProjectFilesPage) pages.get(PageType.PAGE_PROJECT_FILES)).setProject(project);
+                       ((ProjectInsertPage) pages.get(PageType.PAGE_INSERT_PROJECT)).setProject(project);
+                       showPage(PageType.PAGE_PROJECT_FILES);
+               } else if ("page.project.files".equals(pageName)) {
+                       ProjectPage projectPage = (ProjectPage) pages.get(PageType.PAGE_PROJECTS);
+                       Project project = projectPage.getSelectedProject();
+                       if (selectedNode == null) {
+                               JOptionPane.showMessageDialog(wizard, I18n.getMessage("jsite.error.no-node-selected"), null, JOptionPane.ERROR_MESSAGE);
+                               return;
+                       }
+                       CheckReport checkReport = ProjectInserter.validateProject(project);
+                       for (Issue issue : checkReport) {
+                               if (issue.isFatal()) {
+                                       JOptionPane.showMessageDialog(wizard, MessageFormat.format(I18n.getMessage("jsite." + issue.getErrorKey()), (Object[]) issue.getParameters()), null, JOptionPane.ERROR_MESSAGE);
+                                       return;
+                               }
+                               if (JOptionPane.showConfirmDialog(wizard, MessageFormat.format(I18n.getMessage("jsite." + issue.getErrorKey()), (Object[]) issue.getParameters()), null, JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE) != JOptionPane.OK_OPTION) {
+                                       return;
+                               }
+                       }
+                       boolean nodeRunning = false;
+                       try {
+                               nodeRunning = freenetInterface.isNodePresent();
+                       } catch (IOException e) {
+                               /* ignore. */
+                       }
+                       if (!nodeRunning) {
+                               JOptionPane.showMessageDialog(wizard, I18n.getMessage("jsite.error.no-node-running"), null, JOptionPane.ERROR_MESSAGE);
+                               return;
+                       }
+                       configuration.save();
+                       showPage(PageType.PAGE_INSERT_PROJECT);
+                       ProjectInsertPage projectInsertPage = (ProjectInsertPage) pages.get(PageType.PAGE_INSERT_PROJECT);
+                       String tempDirectory = ((PreferencesPage) pages.get(PageType.PAGE_PREFERENCES)).getTempDirectory();
+                       projectInsertPage.setTempDirectory(tempDirectory);
+                       projectInsertPage.setUseEarlyEncode(configuration.useEarlyEncode());
+                       projectInsertPage.setPriority(configuration.getPriority());
+                       projectInsertPage.setManifestPutter(configuration.getManifestPutter());
+                       projectInsertPage.startInsert();
+                       nodeMenu.setEnabled(false);
+                       optionsPreferencesAction.setEnabled(false);
+               } else if ("page.project.insert".equals(pageName)) {
+                       ProjectInsertPage projectInsertPage = (ProjectInsertPage) pages.get(PageType.PAGE_INSERT_PROJECT);
+                       if (projectInsertPage.isRunning()) {
+                               projectInsertPage.stopInsert();
+                       } else {
+                               showPage(PageType.PAGE_PROJECTS);
+                               nodeMenu.setEnabled(true);
+                               optionsPreferencesAction.setEnabled(true);
+                       }
+               } else if ("page.preferences".equals(pageName)) {
+                       PreferencesPage preferencesPage = (PreferencesPage) pages.get(PageType.PAGE_PREFERENCES);
+                       showPage(PageType.PAGE_PROJECTS);
+                       optionsPreferencesAction.setEnabled(true);
+                       configuration.setUseEarlyEncode(preferencesPage.useEarlyEncode());
+                       configuration.setPriority(preferencesPage.getPriority());
+                       configuration.setManifestPutter(preferencesPage.getManifestPutter());
+                       configuration.setConfigurationLocation(preferencesPage.getConfigurationLocation());
+               }
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void wizardPreviousPressed(TWizard wizard) {
+               String pageName = wizard.getPage().getName();
+               if ("page.project".equals(pageName) || "page.preferences".equals(pageName)) {
+                       showPage(PageType.PAGE_NODE_MANAGER);
+                       optionsPreferencesAction.setEnabled(true);
+               } else if ("page.project.files".equals(pageName)) {
+                       showPage(PageType.PAGE_PROJECTS);
+               } else if ("page.project.insert".equals(pageName)) {
+                       showPage(PageType.PAGE_PROJECT_FILES);
+               }
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void wizardQuitPressed(TWizard wizard) {
+               if (((ProjectPage) pages.get(PageType.PAGE_PROJECTS)).wasUriCopied() || ((ProjectInsertPage) pages.get(PageType.PAGE_INSERT_PROJECT)).wasUriCopied()) {
+                       JOptionPane.showMessageDialog(wizard, I18n.getMessage("jsite.project.warning.use-clipboard-now"));
+               }
+               if (JOptionPane.showConfirmDialog(wizard, I18n.getMessage("jsite.quit.question"), I18n.getMessage("jsite.quit.question.title"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE) == JOptionPane.OK_OPTION) {
+                       if (isOverwritingConfiguration() && !originalLocation.equals(configuration.getConfigurationDirectory())) {
+                               int overwriteConfigurationAnswer = JOptionPane.showConfirmDialog(wizard, MessageFormat.format(I18n.getMessage("jsite.quit.overwrite-configuration"), configuration.getConfigurationLocator().getFile(configuration.getConfigurationDirectory())), I18n.getMessage("jsite.quit.overwrite-configuration.title"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
+                               if (overwriteConfigurationAnswer == JOptionPane.YES_OPTION) {
+                                       if (saveConfiguration()) {
+                                               System.exit(0);
+                                       }
+                               } else if (overwriteConfigurationAnswer == JOptionPane.CANCEL_OPTION) {
+                                       return;
+                               }
+                               if (overwriteConfigurationAnswer == JOptionPane.NO_OPTION) {
+                                       System.exit(0);
+                               }
+                       } else {
+                               if (saveConfiguration()) {
+                                       System.exit(0);
+                               }
+                       }
+                       if (JOptionPane.showConfirmDialog(wizard, I18n.getMessage("jsite.quit.config-not-saved"), null, JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.OK_OPTION) {
+                               System.exit(0);
+                       }
+               }
+       }
+       //
+       // INTERFACE NodeManagerListener
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void nodesUpdated(Node[] nodes) {
+               nodeMenu.removeAll();
+               ButtonGroup nodeButtonGroup = new ButtonGroup();
+               Node newSelectedNode = null;
+               for (Node node : nodes) {
+                       JRadioButtonMenuItem nodeMenuItem = new JRadioButtonMenuItem(node.getName());
+                       nodeMenuItem.putClientProperty("Node", node);
+                       nodeMenuItem.addActionListener(this);
+                       nodeButtonGroup.add(nodeMenuItem);
+                       if (node.equals(selectedNode)) {
+                               newSelectedNode = node;
+                               nodeMenuItem.setSelected(true);
+                       }
+                       nodeMenu.add(nodeMenuItem);
+               }
+               nodeMenu.addSeparator();
+               nodeMenu.add(manageNodeAction);
+               selectedNode = newSelectedNode;
+               freenetInterface.setNode(selectedNode);
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void nodeSelected(Node node) {
+               for (Component menuItem : nodeMenu.getMenuComponents()) {
+                       if (menuItem instanceof JMenuItem) {
+                               if (node.equals(((JMenuItem) menuItem).getClientProperty("Node"))) {
+                                       ((JMenuItem) menuItem).setSelected(true);
+                               }
+                       }
+               }
+               freenetInterface.setNode(node);
+               selectedNode = node;
+       }
+       //
+       // INTERFACE ActionListener
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void actionPerformed(ActionEvent e) {
+               Object source = e.getSource();
+               if (source instanceof JRadioButtonMenuItem) {
+                       JRadioButtonMenuItem menuItem = (JRadioButtonMenuItem) source;
+                       Node node = (Node) menuItem.getClientProperty("Node");
+                       selectedNode = node;
+                       freenetInterface.setNode(selectedNode);
+               }
+       }
+       //
+       // INTERFACE UpdateListener
+       //
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public void foundUpdateData(Version foundVersion, long versionTimestamp) {
+               logger.log(Level.FINEST, "Found version {0} from {1,date}.", new Object[] { foundVersion, versionTimestamp });
+               if (foundVersion.compareTo(VERSION) > 0) {
+                       JOptionPane.showMessageDialog(wizard, MessageFormat.format(I18n.getMessage("jsite.update-checker.found-version.message"), foundVersion.toString(), new Date(versionTimestamp)), I18n.getMessage("jsite.update-checker.found-version.title"), JOptionPane.INFORMATION_MESSAGE);
+               }
+       }
+       //
+       // MAIN METHOD
+       //
+       /**
+        * Main method that is called by the VM.
+        *
+        * @param args
+        *            The command-line arguments
+        */
+       public static void main(String[] args) {
+               /* initialize logger. */
+               Logger logger = Logger.getLogger("de.todesbaum");
+               Handler handler = new ConsoleHandler();
+               logger.addHandler(handler);
+               String configFilename = null;
+               boolean nextIsConfigFilename = false;
+               for (String argument : args) {
+                       if (nextIsConfigFilename) {
+                               configFilename = argument;
+                               nextIsConfigFilename = false;
+                       }
+                       if ("--help".equals(argument)) {
+                               printHelp();
+                               return;
+                       } else if ("--debug".equals(argument)) {
+                               logger.setLevel(Level.ALL);
+                               handler.setLevel(Level.ALL);
+                       } else if ("--config-file".equals(argument)) {
+                               nextIsConfigFilename = true;
+                       }
+               }
+               if (nextIsConfigFilename) {
+                       System.out.println("--config-file needs parameter!");
+                       return;
+               }
+               new Main(configFilename);
+       }
+       /**
+        * Prints a small syntax help.
+        */
+       private static void printHelp() {
+               System.out.println("--help\tshows this cruft");
+               System.out.println("--debug\tenables some debug output");
+               System.out.println("--config-file <file>\tuse specified configuration file");
+       }
+ }
index 0000000,977836c..373703c
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,113 +1,114 @@@
+ /*
+  * jSite - Version.java - Copyright © 2006–2012 David Roden
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * 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, write to the Free Software
+  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+  */
+ package de.todesbaum.jsite.main;
+ /**
+  * Container for version information.
+  *
+  * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
+  */
+ public class Version implements Comparable<Version> {
+       /** The components of the version information. */
+       private final int[] components;
+       /**
+        * Creates a new version container with the given components.
+        *
+        * @param components
+        *            The version components
+        */
+       public Version(int... components) {
+               this.components = new int[components.length];
+               System.arraycopy(components, 0, this.components, 0, components.length);
+       }
+       /**
+        * Returns the number of version components.
+        *
+        * @return The number of version components
+        */
+       public int size() {
+               return components.length;
+       }
+       /**
+        * Returns the version component with the given index.
+        *
+        * @param index
+        *            The index of the version component
+        * @return The version component
+        */
+       public int getComponent(int index) {
+               return components[index];
+       }
+       /**
+        * Parses a version from the given string.
+        *
+        * @param versionString
+        *            The version string to parse
+        * @return The parsed version, or <code>null</code> if the string could not
+        *         be parsed
+        */
+       public static Version parse(String versionString) {
+               String[] componentStrings = versionString.split("\\.");
+               int[] components = new int[componentStrings.length];
+               int index = -1;
+               for (String componentString : componentStrings) {
+                       try {
+                               components[++index] = Integer.parseInt(componentString);
+                       } catch (NumberFormatException nfe1) {
+                               return null;
+                       }
+               }
+               return new Version(components);
+       }
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String toString() {
+               StringBuilder versionString = new StringBuilder();
+               for (int component : components) {
+                       if (versionString.length() != 0) {
+                               versionString.append('.');
+                       }
+                       versionString.append(component);
+               }
+               return versionString.toString();
+       }
+       /**
+        * {@inheritDoc}
+        */
++      @Override
+       public int compareTo(Version version) {
+               int lessComponents = Math.min(components.length, version.components.length);
+               for (int index = 0; index < lessComponents; index++) {
+                       if (version.components[index] == components[index]) {
+                               continue;
+                       }
+                       return components[index] - version.components[index];
+               }
+               return components.length - version.components.length;
+       }
+ }