public class AbortedException extends Exception {

	/* nothing here. */

} public class Freenet7Interface {

	/** Random number to differentiate several jSites. */
	private static final int number = (int) (Math.random() * Integer.MAX_VALUE);

	/** Counter. */
	private static int counter = 0;

	private final NodeSupplier nodeSupplier;
	private final ConnectionSupplier connectionSupplier;
	private final ClientSupplier clientSupplier;

	/** The node to connect to. */
	private Node node;

	/** The connection to the node. */
	private Connection connection;

	public Freenet7Interface() {
		this(new DefaultNodeSupplier(), new DefaultConnectionSupplier(), new DefaultClientSupplier());
	}

	Freenet7Interface(NodeSupplier nodeSupplier, ConnectionSupplier connectionSupplier, ClientSupplier clientSupplier) {
		this.nodeSupplier = nodeSupplier;
		this.connectionSupplier = connectionSupplier;
		this.clientSupplier = clientSupplier;
	}

	/**
	 * Sets hostname and port from the given node.
	 *
	 * @param node
	 *            The node to get the hostname and port from
	 */
	public void setNode(de.todesbaum.jsite.application.Node node) {
		if (node != null) {
			this.node = nodeSupplier.supply(node.getHostname(), node.getPort());
			connection = connectionSupplier.supply(node, "jSite-" + number + "-connection-" + counter++);
		} else {
			this.node = null;
			connection = null;
		}
	}

	/**
	 * Returns the node this interface is connecting to.
	 *
	 * @return The node
	 */
	public Node getNode() {
		return node;
	}

	/**
	 * Creates a new connection to the current node with the given identifier.
	 *
	 * @param identifier
	 *            The identifier of the connection
	 * @return The connection to the node
	 */
	public Connection getConnection(String identifier) {
		return connectionSupplier.supply(node, identifier);
	}

	/**
	 * Checks whether the current node is connected. If the node is not
	 * connected, a connection will be tried.
	 *
	 * @return <code>true</code> if the node is connected, <code>false</code>
	 *         otherwise
	 * @throws IOException
	 *             if an I/O error occurs communicating with the node
	 */
	public boolean isNodePresent() throws IOException {
		if (!connection.isConnected()) {
			return connection.connect();
		}
		return true;
	}

	/**
	 * Generates an SSK key pair.
	 *
	 * @return An array of strings, the first one being the generated private
	 *         (insert) URI and the second one being the generated public
	 *         (request) URI
	 * @throws IOException
	 *             if an I/O error occurs communicating with the node
	 */
	public String[] generateKeyPair() throws IOException {
		if (!isNodePresent()) {
			throw new IOException("Node is offline.");
		}
		GenerateSSK generateSSK = new GenerateSSK();
		Client client = clientSupplier.supply(connection, generateSSK);
		Message keypairMessage = client.readMessage();
		return new String[] { keypairMessage.get("InsertURI"), keypairMessage.get("RequestURI") };
	}

	/**
	 * Checks whether the interface has already been configured with a node.
	 *
	 * @return <code>true</code> if this interface already has a node set,
	 *         <code>false</code> otherwise
	 */
	public boolean hasNode() {
		return (node != null) && (connection != null);
	}

	public interface NodeSupplier {

		Node supply(String hostname, int port);

	}

	public static class DefaultNodeSupplier implements NodeSupplier {

		@Override
		public Node supply(String hostname, int port) {
			return new Node(hostname, port);
		}

	}

	public interface ConnectionSupplier {

		Connection supply(Node node, String identifier);

	}

	public static class DefaultConnectionSupplier implements ConnectionSupplier {

		@Override
		public Connection supply(Node node, String identifier) {
			return new Connection(node, identifier);
		}

	}

	public interface ClientSupplier {

		Client supply(Connection connection, Command command) throws IOException;

	}

	public static class DefaultClientSupplier implements ClientSupplier {

		@Override
		public Client supply(Connection connection, Command command) throws IOException {
			return new Client(connection, command);
		}

	}

} public interface InsertListener extends EventListener {

	/**
	 * Enumeration for the different error situations.
	 *
	 * @author David ‘Bombe’ Roden &lt;bombe@freenetproject.org&gt;
	 */
	public static enum ErrorType {

		/** The key does already exist. */
		KEY_COLLISION,

		/** The route to the key was not found. */
		ROUTE_NOT_FOUND,

		/** The data was not found. */
		DATA_NOT_FOUND,

		/** Error in the FCP communication. */
		FCP_ERROR,

		/** General error in the communication. */
		IO_ERROR
	}

	/**
	 * Notifies a listener that an insert has started.
	 *
	 * @param project
	 *            The project that is now being inserted
	 */
	public void projectInsertStarted(Project project);

	/**
	 * Notifies a listener that the upload of a project has finished and the
	 * inserting will start now.
	 *
	 * @param project
	 *            The project that has been uploaded
	 */
	public void projectUploadFinished(Project project);

	/**
	 * Notifies a listener that a project insert has generated a URI.
	 *
	 * @param project
	 *            The project being inserted
	 * @param uri
	 *            The generated URI
	 */
	public void projectURIGenerated(Project project, String uri);

	/**
	 * Notifies a listener that an insert has made some progress.
	 *
	 * @param project
	 *            The project being inserted
	 * @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 been
	 *            finalized, <code>false</code> otherwise
	 */
	public void projectInsertProgress(Project project, int succeeded, int failed, int fatal, int total, boolean finalized);

	/**
	 * Notifies a listener that a project insert has finished.
	 *
	 * @param project
	 *            The project being inserted
	 * @param success
	 *            <code>true</code> if the insert succeeded, <code>false</code>
	 *            otherwise
	 * @param cause
	 *            The cause of a failure, if any (may be <code>null</code>)
	 */
	public void projectInsertFinished(Project project, boolean success, Throwable cause);

} 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 always force inserts. */
	private boolean alwaysForceInserts;

	/** 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;
		alwaysForceInserts = project.alwaysForceInserts;
		ignoreHiddenFiles = project.ignoreHiddenFiles;
		for (Entry<String, FileOption> fileOption : fileOptions.entrySet()) {
			fileOptions.put(fileOption.getKey(), new FileOption(fileOption.getValue()));
		}
	}

	/**
	 * 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 files for this project should always be inserted, even
	 * when unchanged.
	 *
	 * @return {@code true} to always force inserts on this project,
	 *         {@code false} otherwise
	 */
	public boolean isAlwaysForceInsert() {
		return alwaysForceInserts;
	}

	/**
	 * Sets whether files for this project should always be inserted, even when
	 * unchanged.
	 *
	 * @param alwaysForceInsert
	 *            {@code true} to always force inserts on this project,
	 *            {@code false} otherwise
	 */
	public void setAlwaysForceInsert(boolean alwaysForceInsert) {
		this.alwaysForceInserts = alwaysForceInsert;
	}

	/**
	 * 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);
		String defaultMimeType = "application/octet-stream";
		if (fileOption == null) {
			List<String> suffixes = getSuffixes(filename);
			for (String suffix : suffixes) {
				String mimeType = MimeTypes.getMimeType(suffix);
				if (!mimeType.equals(defaultMimeType)) {
					defaultMimeType = mimeType;
					break;
				}
			}
			fileOption = new FileOption(defaultMimeType);
		}
		fileOptions.put(filename, fileOption);
		return fileOption;
	}

	private List<String> getSuffixes(String filename) {
		List<String> suffixes = new ArrayList<>();
		int dot = filename.lastIndexOf(".");
		while (dot > -1) {
			String suffix = filename.substring(dot + 1);
			suffixes.add(0, suffix);
			dot = filename.lastIndexOf(".", dot - 1);
		}
		return suffixes;
	}

	/**
	 * 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.getChangedName().orElse(fileOptionEntry.getKey()));
			}
			fileOption.setForceInsert(false);
		}
	}

} 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 final AtomicInteger counter = new AtomicInteger();

	private final ProjectInsertListeners projectInsertListeners = new ProjectInsertListeners();

	/** The freenet interface. */
	private Freenet7Interface freenetInterface;

	/** The project to insert. */
	private Project project;

	/** The file scanner. */
	private FileScanner fileScanner;

	/** Object used for synchronization. */
	private 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;

	/**
	 * Adds a listener to the list of registered listeners.
	 *
	 * @param insertListener
	 *            The listener to add
	 */
	public void addInsertListener(InsertListener insertListener) {
		projectInsertListeners.addInsertListener(insertListener);
	}

	/**
	 * 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;
	}

	/**
	 * 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, this);
		fileScanner.startInBackground();
	}

	/**
	 * Stops the current insert.
	 */
	public void stop() {
		cancelled = true;
		synchronized (lockObject) {
			if (connection != null) {
				connection.disconnect();
			}
		}
	}

	/**
	 * Creates a file entry suitable for handing in to
	 * {@link ClientPutComplexDir#addFileEntry(FileEntry)}.
	 *
	 * @param file
	 * 		The name and hash of the file to insert
	 * @return A file entry for the given file
	 */
	private Optional<FileEntry> createFileEntry(ScannedFile file) {
		String filename = file.getFilename();
		FileOption fileOption = project.getFileOption(filename);
		if (fileOption.isInsert()) {
			fileOption.setCurrentHash(file.getHash());
			/* check if file was modified. */
			if (!project.isAlwaysForceInsert() && !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 Optional.of(new RedirectFileEntry(fileOption.getChangedName().orElse(filename), fileOption.getMimeType(), "SSK@" + project.getRequestURI() + "/" + project.getPath() + "-" + fileOption.getLastInsertEdition() + "/" + fileOption.getLastInsertFilename()));
			}
			try {
				return Optional.of(createFileEntry(filename, fileOption.getChangedName(), fileOption.getMimeType()));
			} catch (IOException ioe1) {
				/* ignore, null is returned. */
			}
		} else {
			if (fileOption.isInsertRedirect()) {
				return Optional.of(new RedirectFileEntry(fileOption.getChangedName().orElse(filename), fileOption.getMimeType(), fileOption.getCustomKey()));
			}
		}
		return Optional.empty();
	}

	private FileEntry createFileEntry(String filename, Optional<String> changedName, String mimeType) throws FileNotFoundException {
		File physicalFile = new File(project.getLocalPath(), filename);
		InputStream fileEntryInputStream = new FileInputStream(physicalFile);
		return new DirectFileEntry(changedName.orElse(filename), mimeType, fileEntryInputStream, physicalFile.length());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void run() {
		projectInsertListeners.fireProjectInsertStarted(project);
		List<ScannedFile> files = fileScanner.getFiles();

		/* create connection to node */
		synchronized (lockObject) {
			connection = freenetInterface.getConnection("project-insert-" + random + counter.getAndIncrement());
		}
		connection.setTempDirectory(tempDirectory);
		boolean connected = false;
		Throwable cause = null;
		try {
			connected = connection.connect();
		} catch (IOException e1) {
			cause = e1;
		}

		if (!connected || cancelled) {
			projectInsertListeners.fireProjectInsertFinished(project, 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.getAndIncrement(), dirURI, tempDirectory);
		if ((project.getIndexFile() != null) && (project.getIndexFile().length() > 0)) {
			FileOption indexFileOption = project.getFileOption(project.getIndexFile());
			Optional<String> changedName = indexFileOption.getChangedName();
			if (changedName.isPresent()) {
				putDir.setDefaultName(changedName.get());
			} else {
				putDir.setDefaultName(project.getIndexFile());
			}
		}
		putDir.setVerbosity(Verbosity.ALL);
		putDir.setMaxRetries(-1);
		putDir.setEarlyEncode(useEarlyEncode);
		putDir.setPriorityClass(priority);
		for (ScannedFile file : files) {
			Optional<FileEntry> fileEntry = createFileEntry(file);
			if (fileEntry.isPresent()) {
				try {
					putDir.addFileEntry(fileEntry.get());
				} catch (IOException ioe1) {
					projectInsertListeners.fireProjectInsertFinished(project, false, ioe1);
					return;
				}
			}
		}

		/* start request */
		try {
			client.execute(putDir, progressListener);
			projectInsertListeners.fireProjectUploadFinished(project);
		} catch (IOException ioe1) {
			projectInsertListeners.fireProjectInsertFinished(project, 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");
					projectInsertListeners.fireProjectURIGenerated(project, 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"));
					projectInsertListeners.fireProjectInsertProgress(project, 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();
		}
		projectInsertListeners.fireProjectInsertFinished(project, success, cancelled ? new AbortedException() : (disconnected ? new IOException("Connection terminated") : null));
	}

	//
	// INTERFACE FileScannerListener
	//

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void fileScannerFinished(boolean error, Collection<ScannedFile> files) {
		if (!error) {
			new Thread(this).start();
		} else {
			projectInsertListeners.fireProjectInsertFinished(project, false, null);
		}
	}

} public class I18n {

	/** The default locale, English. */
	private static Locale defaultLocale = new Locale("en");

	/** The current locale. */
	private static Locale currentLocale;

	/**
	 * Returns the currently set locale.
	 *
	 * @return The current locale
	 */
	public static Locale getLocale() {
		if (currentLocale == null) {
			currentLocale = Locale.getDefault();
		}
		return currentLocale;
	}

	/**
	 * Sets the current locale.
	 *
	 * @param locale
	 *            The new current locale
	 */
	public static void setLocale(Locale locale) {
		currentLocale = locale;
		Locale.setDefault(locale);
	}

	/**
	 * Returns the resource bundle for the current locale.
	 *
	 * @return The resource bundle for the current locale
	 */
	public static ResourceBundle getResourceBundle() {
		return getResourceBundle(getLocale());
	}

	/**
	 * Returns the resource bundle for the given locale.
	 *
	 * @param locale
	 *            The locale to get the resource bundle for
	 * @return The resource bundle for the given locale
	 */
	public static ResourceBundle getResourceBundle(Locale locale) {
		return ResourceBundle.getBundle("de.todesbaum.jsite.i18n.jSite", locale);
	}

	/**
	 * Retrieves a translated text for the given i18n key. If the resource
	 * bundle for the current locale does not have a translation for the given
	 * key, the default locale is tried. If that fails, the key is returned.
	 *
	 * @param key
	 *            The key to get the translation for
	 * @return The translated value, or the key itself if not translation can be
	 *         found
	 */
	public static String getMessage(String key) {
		try {
			return getResourceBundle().getString(key);
		} catch (MissingResourceException mre1) {
			try {
				return getResourceBundle(defaultLocale).getString(key);
			} catch (MissingResourceException mre2) {
				return key;
			}
		}
	}

} 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();
	}

} public final class PriorityClass {

	/** Denotes <code>maximum</code> priority class. */
	public static final PriorityClass MAXIMUM = new PriorityClass("maximum", 0);

	/** Denotes <code>interactive</code> priority class. */
	public static final PriorityClass INTERACTIVE = new PriorityClass("interactive", 1);

	/** Denotes <code>semi-interactive</code> priority class. */
	public static final PriorityClass SEMI_INTERACTIVE = new PriorityClass("semiInteractive", 2);

	/** Denotes <code>updatable</code> priority class. */
	public static final PriorityClass UPDATABLE = new PriorityClass("updatable", 3);

	/** Denotes <code>bulk</code> priority class. */
	public static final PriorityClass BULK = new PriorityClass("bulk", 4);

	/** Denotes <code>prefetch</code> priority class. */
	public static final PriorityClass PREFETCH = new PriorityClass("prefetch", 5);

	/** Denotes <code>minimum</code> priority class. */
	public static final PriorityClass MINIMUM = new PriorityClass("minimum", 6);

	/** The name of the priority class. */
	private String name;

	/** The value of the priority class. */
	private int value;

	/**
	 * Creates a new priority class with the specified name and value.
	 *
	 * @param name
	 *            The name of the priority class
	 * @param value
	 *            The value of the priority class
	 */
	private PriorityClass(String name, int value) {
		this.name = name;
		this.value = value;
	}

	/**
	 * Returns the name of this priority class.
	 *
	 * @return The name of this priority class
	 */
	public String getName() {
		return name;
	}

	/**
	 * Returns the value of this priority class.
	 *
	 * @return The value of this priority class
	 */
	public int getValue() {
		return value;
	}

	//
	// STATIC METHODS
	//

	/**
	 * Returns the priority class with the given name, matched case-insensitive.
	 *
	 * @param value
	 *            The name of the priority
	 * @return The priority with the given name, or {@code null} if no priority
	 *         matches the given name
	 */
	public static PriorityClass valueOf(String value) {
		for (PriorityClass priorityClass : new PriorityClass[] { MINIMUM, PREFETCH, BULK, UPDATABLE, SEMI_INTERACTIVE, INTERACTIVE, MAXIMUM }) {
			if (priorityClass.getName().equalsIgnoreCase(value)) {
				return priorityClass;
			}
		}
		return null;
	}

	//
	// OBJECT METHODS
	//

	/**
	 * {@inheritDoc}
	 */
	@Override
	public String toString() {
		return name;
	}

} public class TWizard extends JFrame implements WindowListener {

	protected List<WizardListener> wizardListeners = new ArrayList<WizardListener>();

	private Action previousAction;
	private Action nextAction;
	private Action quitAction;
	private JLabel pageIcon;
	private JPanel pagePanel;
	private JLabel pageHeading;
	private JLabel pageDescription;

	@Override
	protected void frameInit() {
		super.frameInit();
		setResizable(false);
		addWindowListener(this);
		setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
		createActions();

		pageIcon = new JLabel();
		pageIcon.setVerticalAlignment(SwingConstants.TOP);
		pageHeading = new JLabel();
		pageHeading.setFont(pageHeading.getFont().deriveFont(pageHeading.getFont().getSize() * 2.0f).deriveFont(Font.BOLD));
		pageDescription = new JLabel();

		JPanel contentPane = new JPanel(new BorderLayout(12, 12));
		contentPane.setBorder(new EmptyBorder(12, 12, 12, 12));

		JPanel topPanel = new JPanel(new BorderLayout(12, 12));
		contentPane.add(topPanel, BorderLayout.PAGE_START);

		topPanel.add(pageIcon, BorderLayout.LINE_START);

		JPanel textPanel = new JPanel(new BorderLayout(12, 12));
		topPanel.add(textPanel, BorderLayout.CENTER);
		textPanel.add(pageHeading, BorderLayout.PAGE_START);
		textPanel.add(pageDescription, BorderLayout.CENTER);

		pagePanel = new JPanel(new BorderLayout(12, 12));
		contentPane.add(pagePanel, BorderLayout.CENTER);

		JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING, 12, 12));
		buttonPanel.setBorder(new EmptyBorder(-12, -12, -12, -12));
		buttonPanel.add(new JButton(previousAction));
		buttonPanel.add(new JButton(nextAction));
		buttonPanel.add(new JButton(quitAction));
		contentPane.add(buttonPanel, BorderLayout.PAGE_END);

		setContentPane(contentPane);
	}

	@Override
	public void pack() {
		super.pack();
		Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
		setLocation((screenSize.width - getWidth()) / 2, (screenSize.height - getHeight()) / 2);
		// System.out.println("resized to: " + getWidth() + "x" + getHeight());
	}

	private void createActions() {
		previousAction = new AbstractAction("Previous") {
			public void actionPerformed(ActionEvent actionEvent) {
				actionPrevious();
			}
		};

		nextAction = new AbstractAction("Next") {
			public void actionPerformed(ActionEvent actionEvent) {
				actionNext();
			}
		};

		quitAction = new AbstractAction("Quit") {
			public void actionPerformed(ActionEvent actionEvent) {
				actionQuit();
			}
		};
	}

	public void addWizardListener(WizardListener wizardListener) {
		wizardListeners.add(wizardListener);
	}

	public void removeWizardListener(WizardListener wizardListener) {
		wizardListeners.remove(wizardListener);
	}

	protected void fireWizardPreviousPressed() {
		for (WizardListener wizardListener: wizardListeners) {
			wizardListener.wizardPreviousPressed(this);
		}
	}

	protected void fireWizardNextPressed() {
		for (WizardListener wizardListener: wizardListeners) {
			wizardListener.wizardNextPressed(this);
		}
	}

	protected void fireWizardQuitPressed() {
		for (WizardListener wizardListener: wizardListeners) {
			wizardListener.wizardQuitPressed(this);
		}
	}

	public void setIcon(Icon icon) {
		pageIcon.setIcon(icon);
	}

	public void setPage(TWizardPage page) {
		setVisible(false);
		pageHeading.setText(page.getHeading());
		pageDescription.setText(page.getDescription());
		if (pagePanel.getComponentCount() > 0) {
			if (pagePanel.getComponent(0) instanceof TWizardPage) {
				((TWizardPage) pagePanel.getComponent(0)).pageDeleted(this);
			}
		}
		pagePanel.removeAll();
		pagePanel.add(page, BorderLayout.CENTER);
		page.pageAdded(this);
		pack();
		setTitle(page.getHeading());
		setVisible(true);
	}

	public TWizardPage getPage() {
		return (TWizardPage) pagePanel.getComponent(0);
	}

	public void setPreviousEnabled(boolean previousEnabled) {
		previousAction.setEnabled(previousEnabled);
	}

	public void setPreviousName(String previousName) {
		previousAction.putValue(Action.NAME, previousName);
	}

	public void setNextEnabled(boolean nextEnabled) {
		nextAction.setEnabled(nextEnabled);
	}

	public void setNextName(String nextName) {
		nextAction.putValue(Action.NAME, nextName);
	}

	public void setQuitEnabled(boolean quitEnabled) {
		quitAction.setEnabled(quitEnabled);
	}

	public void setQuitName(String quitName) {
		quitAction.putValue(Action.NAME, quitName);
	}

	protected void actionPrevious() {
		fireWizardPreviousPressed();
	}

	protected void actionNext() {
		fireWizardNextPressed();
	}

	protected void actionQuit() {
		fireWizardQuitPressed();
	}

	//
	// INTERFACE WindowListener
	//

	/**
	 * {@inheritDoc}
	 */
	public void windowOpened(WindowEvent e) {
	}

	/**
	 * {@inheritDoc}
	 */
	public void windowClosing(WindowEvent e) {
		fireWizardQuitPressed();
	}

	/**
	 * {@inheritDoc}
	 */
	public void windowClosed(WindowEvent e) {
	}

	/**
	 * {@inheritDoc}
	 */
	public void windowIconified(WindowEvent e) {
	}

	/**
	 * {@inheritDoc}
	 */
	public void windowDeiconified(WindowEvent e) {
	}

	/**
	 * {@inheritDoc}
	 */
	public void windowActivated(WindowEvent e) {
	}

	/**
	 * {@inheritDoc}
	 */
	public void windowDeactivated(WindowEvent e) {
	}

} public class TWizardPage extends JPanel {

	protected final TWizard wizard;
	protected String heading;
	protected String description;

	public TWizardPage(final TWizard wizard) {
		this.wizard = wizard;
	}

	public TWizardPage(final TWizard wizard, String heading) {
		this.wizard = wizard;
		this.heading = heading;
	}

	public TWizardPage(final TWizard wizard, String heading, String description) {
		this(wizard, heading);
		this.description = description;
	}

	/**
	 * @return Returns the description.
	 */
	public String getDescription() {
		return description;
	}

	/**
	 * @param description
	 *            The description to set.
	 */
	public void setDescription(String description) {
		this.description = description;
	}

	/**
	 * @return Returns the heading.
	 */
	public String getHeading() {
		return heading;
	}

	/**
	 * @param heading
	 *            The heading to set.
	 */
	public void setHeading(String heading) {
		this.heading = heading;
	}

	public void pageAdded(TWizard wizard) {
	}

	public void pageDeleted(TWizard wizard) {
	}

}
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.PriorityClass;
import de.todesbaum.util.swing.TWizard;
import de.todesbaum.util.swing.TWizardPage;

	//
	// 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) {