public class AddPeer extends FcpMessage {

	/**
	 * Creates a new “AddPeer” request.
	 */
	private AddPeer() {
		super("AddPeer");
	}

	/**
	 * Creates a new “AddPeer” request that reads the noderef of the peer from
	 * the given file.
	 *
	 * @param file
	 *            The file to read the noderef from
	 */
	public AddPeer(String file) {
		this();
		setField("File", file);
	}

	public AddPeer(String identifier, String file) {
		this(file);
		setField("Identifier", identifier);
	}

	/**
	 * Creates a new “AddPeer” request that reads the noderef of the peer from
	 * the given URL.
	 *
	 * @param url
	 *            The URL to read the noderef from
	 */
	public AddPeer(URL url) {
		this();
		setField("URL", String.valueOf(url));
	}

	public AddPeer(String identifier, URL url) {
		this(url);
		setField("Identifier", identifier);
	}

	/**
	 * Creates a new “AddPeer” request that adds the peer given by the noderef.
	 *
	 * @param nodeRef
	 *            The noderef of the peer
	 */
	public AddPeer(NodeRef nodeRef) {
		this();
		setNodeRef(nodeRef);
	}

	public AddPeer(String identifier, NodeRef nodeRef) {
		this(nodeRef);
		setField("Identifier", identifier);
	}

	//
	// PRIVATE METHODS
	//

	/**
	 * Sets the noderef of the peer to add.
	 *
	 * @param nodeRef
	 *            The noderef of the peer
	 */
	private void setNodeRef(NodeRef nodeRef) {
		setField("lastGoodVersion", String.valueOf(nodeRef.getLastGoodVersion()));
		setField("opennet", String.valueOf(nodeRef.isOpennet()));
		setField("identity", nodeRef.getIdentity());
		setField("myName", nodeRef.getMyName());
		setField("location", String.valueOf(nodeRef.getLocation()));
		setField("testnet", String.valueOf(nodeRef.isTestnet()));
		setField("version", String.valueOf(nodeRef.getVersion()));
		setField("physical.udp", nodeRef.getPhysicalUDP());
		setField("ark.pubURI", nodeRef.getARK().getPublicURI());
		setField("ark.number", String.valueOf(nodeRef.getARK().getNumber()));
		setField("dsaPubKey.y", nodeRef.getDSAPublicKey());
		setField("dsaGroup.g", nodeRef.getDSAGroup().getBase());
		setField("dsaGroup.p", nodeRef.getDSAGroup().getPrime());
		setField("dsaGroup.q", nodeRef.getDSAGroup().getSubprime());
		setField("auth.negTypes", FcpUtils.encodeMultiIntegerField(nodeRef.getNegotiationTypes()));
		setField("sig", nodeRef.getSignature());
	}

} public class AllData extends BaseMessage implements Identifiable {

	/** The payload. */
	private InputStream payloadInputStream;

	/**
	 * Creates an “AllData” message that wraps the received message.
	 *
	 * @param receivedMessage
	 *            The received message
	 * @param payloadInputStream
	 *            The payload
	 */
	public AllData(FcpMessage receivedMessage, InputStream payloadInputStream) {
		super(receivedMessage);
		this.payloadInputStream = payloadInputStream;
	}

	/**
	 * Returns the identifier of the request.
	 *
	 * @return The identifier of the request
	 */
	@Override
	public String getIdentifier() {
		return getField("Identifier");
	}

	/**
	 * Returns the length of the data.
	 *
	 * @return The length of the data, or <code>-1</code> if the length could
	 *         not be parsed
	 */
	public long getDataLength() {
		return FcpUtils.safeParseLong(getField("DataLength"));
	}

	/**
	 * Returns the startup time of the request.
	 *
	 * @return The startup time of the request (in milliseconds since Jan 1,
	 *         1970 UTC), or <code>-1</code> if the time could not be parsed
	 */
	public long getStartupTime() {
		return FcpUtils.safeParseLong(getField("StartupTime"));
	}

	/**
	 * Returns the completion time of the request.
	 *
	 * @return The completion time of the request (in milliseconds since Jan 1,
	 *         1970 UTC), or <code>-1</code> if the time could not be parsed
	 */
	public long getCompletionTime() {
		return FcpUtils.safeParseLong(getField("CompletionTime"));
	}

	/**
	 * Returns the payload input stream. You <strong>have</strong> consume the
	 * input stream before returning from the
	 * {@link FcpListener#receivedAllData(FcpConnection, AllData)} method!
	 *
	 * @return The payload
	 */
	public InputStream getPayloadInputStream() {
		return payloadInputStream;
	}

	/**
	 * Returns the content type of the found file.
	 *
	 * @return The content type
	 */
	public String getContentType() {
		return getField("Metadata.ContentType");
	}

} public class ClientGet extends FcpMessage {

	/**
	 * Creates a new “ClientGet” request.
	 *
	 * @param uri
	 *            The URI to get
	 * @param identifier
	 *            The identifier of the request
	 */
	public ClientGet(String uri, String identifier) {
		this(uri, identifier, ReturnType.direct);
	}

	/**
	 * Creates a new “ClientGet” request.
	 *
	 * @param uri
	 *            The URI to get
	 * @param identifier
	 *            The identifier of the request
	 * @param returnType
	 *            The return type of the request
	 */
	public ClientGet(String uri, String identifier, ReturnType returnType) {
		super("ClientGet");
		setField("URI", uri);
		setField("Identifier", identifier);
		setField("ReturnType", String.valueOf(returnType));
	}

	/**
	 * Sets whether the local data store should be ignored when searching for a
	 * key.
	 *
	 * @param ignoreDataStore
	 *            <code>true</code> to ignore the local data store,
	 *            <code>false</code> to include it
	 */
	public void setIgnoreDataStore(boolean ignoreDataStore) {
		setField("IgnoreDS", String.valueOf(ignoreDataStore));
	}

	/**
	 * Sets whether the search for the key should be restricted to the local
	 * data store only.
	 *
	 * @param dsOnly
	 *            <code>true</code> to restrict the search to the local data
	 *            store, <code>false</code> to search on other nodes, too
	 */
	public void setDataStoreOnly(boolean dsOnly) {
		setField("DSonly", String.valueOf(dsOnly));
	}

	/**
	 * Sets the verbosity of the request.
	 *
	 * @param verbosity
	 *            The verbosity of the request
	 */
	public void setVerbosity(Verbosity verbosity) {
		setField("Verbosity", String.valueOf(verbosity));
	}

	/**
	 * Sets the maximum size of the file to retrieve. If the file is larger
	 * than this size the request will fail!
	 *
	 * @param maxSize
	 *            The maximum size of the file to retrieve
	 */
	public void setMaxSize(long maxSize) {
		setField("MaxSize", String.valueOf(maxSize));
	}

	/**
	 * Sets the maximum size of temporary files created by the node. If a
	 * temporary file is larger than this size the request will fail!
	 *
	 * @param maxTempSize
	 *            The maximum size of temporary files
	 */
	public void setMaxTempSize(long maxTempSize) {
		setField("MaxTempSize", String.valueOf(maxTempSize));
	}

	/**
	 * The maximum number of retries in case a block can not be retrieved.
	 *
	 * @param maxRetries
	 *            The maximum number of retries for failed blocks,
	 *            <code>-1</code> to try forever
	 */
	public void setMaxRetries(int maxRetries) {
		setField("MaxRetries", String.valueOf(maxRetries));
	}

	/**
	 * Sets the priority of the request.
	 *
	 * @param priority
	 *            The priority of the request
	 */
	public void setPriority(Priority priority) {
		setField("PriorityClass", String.valueOf(priority));
	}

	public void setRealTimeFlag(boolean realTimeFlag) {
		setField("RealTimeFlag", String.valueOf(realTimeFlag));
	}

	/**
	 * Sets the persistence of the request.
	 *
	 * @param persistence
	 *            The persistence of the request
	 */
	public void setPersistence(Persistence persistence) {
		setField("Persistence", String.valueOf(persistence));
	}

	/**
	 * Sets the client token of the request.
	 *
	 * @param clientToken
	 *            The client token of the request
	 */
	public void setClientToken(String clientToken) {
		setField("ClientToken", clientToken);
	}

	/**
	 * Sets whether the request should be visible on the global queue.
	 *
	 * @param global
	 *            <code>true</code> to make the request visible on the global
	 *            queue, <code>false</code> for client-local queue only
	 */
	public void setGlobal(boolean global) {
		setField("Global", String.valueOf(global));
	}

	/**
	 * Sets whether to request the “binary blob” for a key.
	 *
	 * @param binaryBlob
	 *            <code>true</code> to request the binary blob,
	 *            <code>false</code> to get the “real thing”
	 */
	public void setBinaryBlob(boolean binaryBlob) {
		setField("BinaryBlob", String.valueOf(binaryBlob));
	}

	/**
	 * Sets whether to filter the fetched content.
	 *
	 * @param filterData
	 *            {@code true} to filter content, {@code false} otherwise
	 */
	public void setFilterData(boolean filterData) {
		setField("FilterData", String.valueOf(filterData));
	}

	/**
	 * Sets the allowed MIME types of the requested file. If the MIME type of
	 * the file does not match one of the given MIME types the request will
	 * fail!
	 *
	 * @param allowedMimeTypes
	 *            The allowed MIME types
	 */
	public void setAllowedMimeTypes(String... allowedMimeTypes) {
		setField("AllowedMIMETypes", FcpUtils.encodeMultiStringField(allowedMimeTypes));
	}

	/**
	 * Sets the filename to download the file to. You should only call this
	 * method if your return type is {@link ReturnType#disk}!
	 *
	 * @param filename
	 *            The filename to download the file to
	 */
	public void setFilename(String filename) {
		setField("Filename", filename);
	}

	/**
	 * Sets the name for the temporary file. You should only call this method
	 * if your return type is {@link ReturnType#disk}!
	 *
	 * @param tempFilename
	 *            The name of the temporary file
	 */
	public void setTempFilename(String tempFilename) {
		setField("TempFilename", tempFilename);
	}

} public class ClientHello extends FcpMessage {

	/**
	 * Creates a new “ClientHello” message with the given client name. The
	 * client name has to be unique to the node otherwise you will get a
	 * {@link CloseConnectionDuplicateClientName} response from the node!
	 *
	 * @param clientName
	 *            The unique client name
	 */
	public ClientHello(String clientName) {
		this(clientName, "2.0");
	}

	/**
	 * Creates a new “ClientHello” message with the given client name. The
	 * client name has to be unique to the node otherwise you will get a
	 * {@link CloseConnectionDuplicateClientName} response from the node! The
	 * expected FCP version is currently ignored by the node.
	 *
	 * @param clientName
	 *            The unique client name
	 * @param expectedVersion
	 *            The FCP version that the node is expected to talk
	 */
	public ClientHello(String clientName, String expectedVersion) {
		super("ClientHello");
		setField("Name", clientName);
		setField("ExpectedVersion", expectedVersion);
	}

} public class CloseConnectionDuplicateClientName extends BaseMessage {

	/**
	 * Creates a new CloseConnectionDuplicateClientName message that wraps the
	 * given message.
	 *
	 * @param receivedMessage
	 *            The received message
	 */
	public CloseConnectionDuplicateClientName(FcpMessage receivedMessage) {
		super(receivedMessage);
	}

} public class DataFound extends BaseMessage implements Identifiable {

	/**
	 * Creates a new “DataFound” message that wraps the received message.
	 *
	 * @param receivedMessage
	 *            The received message
	 */
	public DataFound(FcpMessage receivedMessage) {
		super(receivedMessage);
	}

	/**
	 * Returns whether the request is on the global queue.
	 *
	 * @return <code>true</code> if the request is on the global queue,
	 *         <code>false</code> if the request is on the client-local queue
	 */
	public boolean isGlobal() {
		return Boolean.valueOf(getField("Global"));
	}

	/**
	 * Returns the identifier of the request.
	 *
	 * @return The identifier of the request
	 */
	@Override
	public String getIdentifier() {
		return getField("Identifier");
	}

	/**
	 * Returns the content type of the data.
	 *
	 * @return The content type of the data
	 */
	public String getMetadataContentType() {
		return getField("Metadata.ContentType");
	}

	/**
	 * Returns the length of the data.
	 *
	 * @return The length of the data
	 */
	public long getDataLength() {
		return FcpUtils.safeParseLong(getField("DataLength"));
	}

} public class EndListPeerNotes extends BaseMessage {

	/**
	 * Creates a new “EndListPeerNotes” message that wraps the received
	 * message.
	 *
	 * @param fcpMessage
	 *            The received message
	 */
	public EndListPeerNotes(FcpMessage fcpMessage) {
		super(fcpMessage);
	}

} public class EndListPeers extends BaseMessage implements Identifiable {

	/**
	 * Creates a new “EndListPeers” message that wraps the received message.
	 *
	 * @param receivedMessage
	 *            The message that was received
	 */
	public EndListPeers(FcpMessage receivedMessage) {
		super(receivedMessage);
	}

	/**
	 * Returns the identifier of the request.
	 *
	 * @return The identifier of the request
	 */
	@Override
	public String getIdentifier() {
		return getField("Identifier");
	}

} public class EndListPersistentRequests extends BaseMessage {

	/**
	 * Creates a new “EndListPersistentRequests” message that wraps the
	 * received message.
	 *
	 * @param receivedMessage
	 *            The received message
	 */
	public EndListPersistentRequests(FcpMessage receivedMessage) {
		super(receivedMessage);
	}

} public class FCPPluginMessage extends FcpMessage {

	/**
	 * @deprecated Use {@link #FCPPluginMessage(String, String)} instead
	 */
	@Deprecated
	public FCPPluginMessage(String pluginClass) {
		super("FCPPluginMessage");
		setField("PluginName", pluginClass);
	}

	public FCPPluginMessage(String identifier, String pluginClass) {
		this(pluginClass);
		setField("Identifier", identifier);
	}

	/**
	 * @deprecated Use {@link #FCPPluginMessage(String, String)} instead
	 */
	@Deprecated
	public void setIdentifier(String identifier) {
		setField("Identifier", identifier);
	}

	/**
	 * Sets a custom parameter for the plugin.
	 *
	 * @param key
	 * 	The key of the parameter
	 * @param value
	 * 	The value of the parameter
	 */
	public void setParameter(String key, String value) {
		setField("Param." + key, value);
	}

	/**
	 * @deprecated Use {@link #setData(InputStream, long)} instead
	 */
	@Deprecated
	public void setDataLength(long dataLength) {
		setField("DataLength", String.valueOf(dataLength));
	}

	public void setData(InputStream payloadInputStream, long dataLength) {
		setPayloadInputStream(payloadInputStream);
		setDataLength(dataLength);
	}

} public class FCPPluginReply extends BaseMessage implements Identifiable {

	/** The payload input stream. */
	private final InputStream payloadInputStream;

	/**
	 * Creates a new “FCPPluginReply” message that wraps the received message.
	 *
	 * @param receivedMessage
	 *            The received message
	 * @param payloadInputStream
	 *            The optional input stream for the payload
	 */
	public FCPPluginReply(FcpMessage receivedMessage, InputStream payloadInputStream) {
		super(receivedMessage);
		this.payloadInputStream = payloadInputStream;
	}

	/**
	 * Returns the name of the plugin.
	 *
	 * @return The name of the plugin
	 */
	public String getPluginName() {
		return getField("PluginName");
	}

	/**
	 * Returns the identifier of the request.
	 *
	 * @return The identifier of the request
	 */
	@Override
	public String getIdentifier() {
		return getField("Identifier");
	}

	/**
	 * Returns the length of the optional payload.
	 *
	 * @return The length of the payload, or <code>-1</code> if there is no
	 *         payload or the length could not be parsed
	 */
	public long getDataLength() {
		return FcpUtils.safeParseLong(getField("DataLength"));
	}

	/**
	 * Returns a reply from the plugin.
	 *
	 * @param key
	 *            The name of the reply
	 * @return The value of the reply
	 */
	public String getReply(String key) {
		return getField("Replies." + key);
	}

	/**
	 * Returns all replies from the plugin. The plugin sends replies as normal
	 * message fields prefixed by “Replies.”. The keys of the returned map do
	 * not contain this prefix!
	 *
	 * @return All replies from the plugin
	 */
	public Map<String, String> getReplies() {
		Map<String, String> fields = getFields();
		Map<String, String> replies = new HashMap<String, String>();
		for (Entry<String, String> field : fields.entrySet()) {
			if (field.getKey().startsWith("Replies.")) {
				replies.put(field.getKey().substring(8), field.getValue());
			}
		}
		return replies;
	}

	/**
	 * Returns the optional payload.
	 *
	 * @return The payload of the reply, or <code>null</code> if there is no
	 *         payload
	 */
	public InputStream getPayloadInputStream() {
		return payloadInputStream;
	}

} public class FcpAdapter implements FcpListener {

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedNodeHello(FcpConnection fcpConnection, NodeHello nodeHello) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedCloseConnectionDuplicateClientName(FcpConnection fcpConnection, CloseConnectionDuplicateClientName closeConnectionDuplicateClientName) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedSSKKeypair(FcpConnection fcpConnection, SSKKeypair sskKeypair) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedPeer(FcpConnection fcpConnection, Peer peer) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedEndListPeers(FcpConnection fcpConnection, EndListPeers endListPeers) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedPeerNote(FcpConnection fcpConnection, PeerNote peerNote) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedEndListPeerNotes(FcpConnection fcpConnection, EndListPeerNotes endListPeerNotes) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedPeerRemoved(FcpConnection fcpConnection, PeerRemoved peerRemoved) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see FcpListener#receivedNodeData(FcpConnection, NodeData)
	 */
	@Override
	public void receivedNodeData(FcpConnection fcpConnection, NodeData nodeData) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see FcpListener#receivedTestDDAReply(FcpConnection, TestDDAReply)
	 */
	@Override
	public void receivedTestDDAReply(FcpConnection fcpConnection, TestDDAReply testDDAReply) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedTestDDAComplete(FcpConnection fcpConnection, TestDDAComplete testDDAComplete) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedPersistentGet(FcpConnection fcpConnection, PersistentGet persistentGet) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedPersistentPut(FcpConnection fcpConnection, PersistentPut persistentPut) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedEndListPersistentRequests(FcpConnection fcpConnection, EndListPersistentRequests endListPersistentRequests) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedURIGenerated(FcpConnection fcpConnection, URIGenerated uriGenerated) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedDataFound(FcpConnection fcpConnection, DataFound dataFound) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedAllData(FcpConnection fcpConnection, AllData allData) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedSimpleProgress(FcpConnection fcpConnection, SimpleProgress simpleProgress) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedStartedCompression(FcpConnection fcpConnection, StartedCompression startedCompression) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedFinishedCompression(FcpConnection fcpConnection, FinishedCompression finishedCompression) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedUnknownPeerNoteType(FcpConnection fcpConnection, UnknownPeerNoteType unknownPeerNoteType) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedUnknownNodeIdentifier(FcpConnection fcpConnection, UnknownNodeIdentifier unknownNodeIdentifier) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedConfigData(FcpConnection fcpConnection, ConfigData configData) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedGetFailed(FcpConnection fcpConnection, GetFailed getFailed) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedPutFailed(FcpConnection fcpConnection, PutFailed putFailed) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedIdentifierCollision(FcpConnection fcpConnection, IdentifierCollision identifierCollision) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedPersistentPutDir(FcpConnection fcpConnection, PersistentPutDir persistentPutDir) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedPersistentRequestRemoved(FcpConnection fcpConnection, PersistentRequestRemoved persistentRequestRemoved) {
		/* empty. */
	}

	@Override
	public void receivedSubscribedUSK(FcpConnection fcpConnection, SubscribedUSK subscribedUSK) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedSubscribedUSKUpdate(FcpConnection fcpConnection, SubscribedUSKUpdate subscribedUSKUpdate) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedPluginInfo(FcpConnection fcpConnection, PluginInfo pluginInfo) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedPluginRemoved(FcpConnection fcpConnection, PluginRemoved pluginRemoved) {
		/* empty. */
	}

	@Override
	public void receivedFCPPluginReply(FcpConnection fcpConnection, FCPPluginReply fcpPluginReply) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedPersistentRequestModified(FcpConnection fcpConnection, PersistentRequestModified persistentRequestModified) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedPutSuccessful(FcpConnection fcpConnection, PutSuccessful putSuccessful) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedPutFetchable(FcpConnection fcpConnection, PutFetchable putFetchable) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedProtocolError(FcpConnection fcpConnection, ProtocolError protocolError) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedSentFeed(FcpConnection source, SentFeed sentFeed) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedBookmarkFeed(FcpConnection fcpConnection, ReceivedBookmarkFeed receivedBookmarkFeed) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void receivedMessage(FcpConnection fcpConnection, FcpMessage fcpMessage) {
		/* empty. */
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void connectionClosed(FcpConnection fcpConnection, Throwable throwable) {
		/* empty. */
	}

} public class FcpConnection implements Closeable {

	/** Logger. */
	private static final Logger logger = Logger.getLogger(FcpConnection.class.getName());

	/** The default port for FCP v2. */
	public static final int DEFAULT_PORT = 9481;

	/** Listener management. */
	private final FcpListenerManager fcpListenerManager = new FcpListenerManager(this);

	/** The address of the node. */
	private final InetAddress address;

	/** The port number of the node’s FCP port. */
	private final int port;

	/** The remote socket. */
	private Socket remoteSocket;

	/** The input stream from the node. */
	private InputStream remoteInputStream;

	/** The output stream to the node. */
	private OutputStream remoteOutputStream;

	/** The connection handler. */
	private FcpConnectionHandler connectionHandler;

	/** Incoming message statistics. */
	private static final Map<String, Integer> incomingMessageStatistics = Collections.synchronizedMap(new HashMap<String, Integer>());

	/**
	 * Creates a new FCP connection to the freenet node running on localhost,
	 * using the default port.
	 *
	 * @throws UnknownHostException
	 *             if the hostname can not be resolved
	 */
	public FcpConnection() throws UnknownHostException {
		this(InetAddress.getLocalHost());
	}

	/**
	 * Creates a new FCP connection to the Freenet node running on the given
	 * host, listening on the default port.
	 *
	 * @param host
	 *            The hostname of the Freenet node
	 * @throws UnknownHostException
	 *             if <code>host</code> can not be resolved
	 */
	public FcpConnection(String host) throws UnknownHostException {
		this(host, DEFAULT_PORT);
	}

	/**
	 * Creates a new FCP connection to the Freenet node running on the given
	 * host, listening on the given port.
	 *
	 * @param host
	 *            The hostname of the Freenet node
	 * @param port
	 *            The port number of the node’s FCP port
	 * @throws UnknownHostException
	 *             if <code>host</code> can not be resolved
	 */
	public FcpConnection(String host, int port) throws UnknownHostException {
		this(InetAddress.getByName(host), port);
	}

	/**
	 * Creates a new FCP connection to the Freenet node running at the given
	 * address, listening on the default port.
	 *
	 * @param address
	 *            The address of the Freenet node
	 */
	public FcpConnection(InetAddress address) {
		this(address, DEFAULT_PORT);
	}

	/**
	 * Creates a new FCP connection to the Freenet node running at the given
	 * address, listening on the given port.
	 *
	 * @param address
	 *            The address of the Freenet node
	 * @param port
	 *            The port number of the node’s FCP port
	 */
	public FcpConnection(InetAddress address, int port) {
		this.address = address;
		this.port = port;
	}

	//
	// LISTENER MANAGEMENT
	//

	/**
	 * Adds the given listener to the list of listeners.
	 *
	 * @param fcpListener
	 *            The listener to add
	 */
	public void addFcpListener(FcpListener fcpListener) {
		fcpListenerManager.addListener(fcpListener);
	}

	/**
	 * Removes the given listener from the list of listeners.
	 *
	 * @param fcpListener
	 *            The listener to remove
	 */
	public void removeFcpListener(FcpListener fcpListener) {
		fcpListenerManager.removeListener(fcpListener);
	}

	public synchronized boolean isClosed() {
		return connectionHandler == null;
	}

	//
	// ACTIONS
	//

	/**
	 * Connects to the node.
	 *
	 * @throws IOException
	 *             if an I/O error occurs
	 * @throws IllegalStateException
	 *             if there is already a connection to the node
	 */
	public synchronized void connect() throws IOException, IllegalStateException {
		if (connectionHandler != null) {
			throw new IllegalStateException("already connected, disconnect first");
		}
		logger.info("connecting to " + address + ":" + port + "…");
		remoteSocket = new Socket(address, port);
		remoteInputStream = remoteSocket.getInputStream();
		remoteOutputStream = remoteSocket.getOutputStream();
		new Thread(connectionHandler = new FcpConnectionHandler(this, remoteInputStream)).start();
	}

	/**
	 * Disconnects from the node. If there is no connection to the node, this
	 * method does nothing.
	 *
	 * @deprecated Use {@link #close()} instead
	 */
	@Deprecated
	public synchronized void disconnect() {
		close();
	}

	/**
	 * Closes the connection. If there is no connection to the node, this
	 * method does nothing.
	 */
	@Override
	public void close() {
		handleDisconnect(null);
	}

	/**
	 * Sends the given FCP message.
	 *
	 * @param fcpMessage
	 *            The FCP message to send
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	public synchronized void sendMessage(FcpMessage fcpMessage) throws IOException {
		logger.fine("sending message: " + fcpMessage.getName());
		fcpMessage.write(remoteOutputStream);
	}

	//
	// PACKAGE-PRIVATE METHODS
	//

	/**
	 * Handles the given message, notifying listeners. This message should only
	 * be called by {@link FcpConnectionHandler}.
	 *
	 * @param fcpMessage
	 *            The received message
	 */
	void handleMessage(FcpMessage fcpMessage) throws IOException{
		logger.fine("received message: " + fcpMessage.getName());
		String messageName = fcpMessage.getName();
		countMessage(messageName);
		if ("SimpleProgress".equals(messageName)) {
			fcpListenerManager.fireReceivedSimpleProgress(new SimpleProgress(fcpMessage));
		} else if ("ProtocolError".equals(messageName)) {
			fcpListenerManager.fireReceivedProtocolError(new ProtocolError(fcpMessage));
		} else if ("PersistentGet".equals(messageName)) {
			fcpListenerManager.fireReceivedPersistentGet(new PersistentGet(fcpMessage));
		} else if ("PersistentPut".equals(messageName)) {
			fcpListenerManager.fireReceivedPersistentPut(new PersistentPut(fcpMessage));
		} else if ("PersistentPutDir".equals(messageName)) {
			fcpListenerManager.fireReceivedPersistentPutDir(new PersistentPutDir(fcpMessage));
		} else if ("URIGenerated".equals(messageName)) {
			fcpListenerManager.fireReceivedURIGenerated(new URIGenerated(fcpMessage));
		} else if ("EndListPersistentRequests".equals(messageName)) {
			fcpListenerManager.fireReceivedEndListPersistentRequests(new EndListPersistentRequests(fcpMessage));
		} else if ("Peer".equals(messageName)) {
			fcpListenerManager.fireReceivedPeer(new Peer(fcpMessage));
		} else if ("PeerNote".equals(messageName)) {
			fcpListenerManager.fireReceivedPeerNote(new PeerNote(fcpMessage));
		} else if ("StartedCompression".equals(messageName)) {
			fcpListenerManager.fireReceivedStartedCompression(new StartedCompression(fcpMessage));
		} else if ("FinishedCompression".equals(messageName)) {
			fcpListenerManager.fireReceivedFinishedCompression(new FinishedCompression(fcpMessage));
		} else if ("GetFailed".equals(messageName)) {
			fcpListenerManager.fireReceivedGetFailed(new GetFailed(fcpMessage));
		} else if ("PutFetchable".equals(messageName)) {
			fcpListenerManager.fireReceivedPutFetchable(new PutFetchable(fcpMessage));
		} else if ("PutSuccessful".equals(messageName)) {
			fcpListenerManager.fireReceivedPutSuccessful(new PutSuccessful(fcpMessage));
		} else if ("PutFailed".equals(messageName)) {
			fcpListenerManager.fireReceivedPutFailed(new PutFailed(fcpMessage));
		} else if ("DataFound".equals(messageName)) {
			fcpListenerManager.fireReceivedDataFound(new DataFound(fcpMessage));
		} else if ("SubscribedUSKUpdate".equals(messageName)) {
			fcpListenerManager.fireReceivedSubscribedUSKUpdate(new SubscribedUSKUpdate(fcpMessage));
		} else if ("SubscribedUSK".equals(messageName)) {
			fcpListenerManager.fireReceivedSubscribedUSK(new SubscribedUSK(fcpMessage));
		} else if ("IdentifierCollision".equals(messageName)) {
			fcpListenerManager.fireReceivedIdentifierCollision(new IdentifierCollision(fcpMessage));
		} else if ("AllData".equals(messageName)) {
			InputStream payloadInputStream = getInputStream(FcpUtils.safeParseLong(fcpMessage.getField("DataLength")));
			fcpListenerManager.fireReceivedAllData(new AllData(fcpMessage, payloadInputStream));
		} else if ("EndListPeerNotes".equals(messageName)) {
			fcpListenerManager.fireReceivedEndListPeerNotes(new EndListPeerNotes(fcpMessage));
		} else if ("EndListPeers".equals(messageName)) {
			fcpListenerManager.fireReceivedEndListPeers(new EndListPeers(fcpMessage));
		} else if ("SSKKeypair".equals(messageName)) {
			fcpListenerManager.fireReceivedSSKKeypair(new SSKKeypair(fcpMessage));
		} else if ("PeerRemoved".equals(messageName)) {
			fcpListenerManager.fireReceivedPeerRemoved(new PeerRemoved(fcpMessage));
		} else if ("PersistentRequestModified".equals(messageName)) {
			fcpListenerManager.fireReceivedPersistentRequestModified(new PersistentRequestModified(fcpMessage));
		} else if ("PersistentRequestRemoved".equals(messageName)) {
			fcpListenerManager.fireReceivedPersistentRequestRemoved(new PersistentRequestRemoved(fcpMessage));
		} else if ("UnknownPeerNoteType".equals(messageName)) {
			fcpListenerManager.fireReceivedUnknownPeerNoteType(new UnknownPeerNoteType(fcpMessage));
		} else if ("UnknownNodeIdentifier".equals(messageName)) {
			fcpListenerManager.fireReceivedUnknownNodeIdentifier(new UnknownNodeIdentifier(fcpMessage));
		} else if ("FCPPluginReply".equals(messageName)) {
			InputStream payloadInputStream = getInputStream(FcpUtils.safeParseLong(fcpMessage.getField("DataLength"), 0));
			fcpListenerManager.fireReceivedFCPPluginReply(new FCPPluginReply(fcpMessage, payloadInputStream));
		} else if ("PluginInfo".equals(messageName)) {
			fcpListenerManager.fireReceivedPluginInfo(new PluginInfo(fcpMessage));
		} else if ("PluginRemoved".equals(messageName)) {
			fcpListenerManager.fireReceivedPluginRemoved(new PluginRemoved(fcpMessage));
		} else if ("NodeData".equals(messageName)) {
			fcpListenerManager.fireReceivedNodeData(new NodeData(fcpMessage));
		} else if ("TestDDAReply".equals(messageName)) {
			fcpListenerManager.fireReceivedTestDDAReply(new TestDDAReply(fcpMessage));
		} else if ("TestDDAComplete".equals(messageName)) {
			fcpListenerManager.fireReceivedTestDDAComplete(new TestDDAComplete(fcpMessage));
		} else if ("ConfigData".equals(messageName)) {
			fcpListenerManager.fireReceivedConfigData(new ConfigData(fcpMessage));
		} else if ("NodeHello".equals(messageName)) {
			fcpListenerManager.fireReceivedNodeHello(new NodeHello(fcpMessage));
		} else if ("CloseConnectionDuplicateClientName".equals(messageName)) {
			fcpListenerManager.fireReceivedCloseConnectionDuplicateClientName(new CloseConnectionDuplicateClientName(fcpMessage));
		} else if ("SentFeed".equals(messageName)) {
			fcpListenerManager.fireSentFeed(new SentFeed(fcpMessage));
		} else if ("ReceivedBookmarkFeed".equals(messageName)) {
			fcpListenerManager.fireReceivedBookmarkFeed(new ReceivedBookmarkFeed(fcpMessage));
		} else {
			fcpListenerManager.fireMessageReceived(fcpMessage);
		}
	}

	/**
	 * Handles a disconnect from the node.
	 *
	 * @param throwable
	 *            The exception that caused the disconnect, or
	 *            <code>null</code> if there was no exception
	 */
	synchronized void handleDisconnect(Throwable throwable) {
		FcpUtils.close(remoteInputStream);
		FcpUtils.close(remoteOutputStream);
		FcpUtils.close(remoteSocket);
		if (connectionHandler != null) {
			connectionHandler.stop();
			connectionHandler = null;
			fcpListenerManager.fireConnectionClosed(throwable);
		}
	}

	//
	// PRIVATE METHODS
	//

	/**
	 * Incremets the counter in {@link #incomingMessageStatistics} by
	 * <cod>1</code> for the given message name.
	 *
	 * @param name
	 *            The name of the message to count
	 */
	private void countMessage(String name) {
		int oldValue = 0;
		if (incomingMessageStatistics.containsKey(name)) {
			oldValue = incomingMessageStatistics.get(name);
		}
		incomingMessageStatistics.put(name, oldValue + 1);
		logger.finest("count for " + name + ": " + (oldValue + 1));
	}

	private synchronized InputStream getInputStream(long dataLength) throws IOException {
		return new TempInputStream(remoteInputStream, dataLength);
	}

} public interface FcpListener extends EventListener {

	/**
	 * Notifies a listener that a “NodeHello” message was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param nodeHello
	 *            The “NodeHello” message
	 */
	public void receivedNodeHello(FcpConnection fcpConnection, NodeHello nodeHello);

	/**
	 * Notifies a listener that a “CloseConnectionDuplicateClientName” message
	 * was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param closeConnectionDuplicateClientName
	 *            The “CloseConnectionDuplicateClientName” message
	 */
	public void receivedCloseConnectionDuplicateClientName(FcpConnection fcpConnection, CloseConnectionDuplicateClientName closeConnectionDuplicateClientName);

	/**
	 * Notifies a listener that a “SSKKeypair” message was received.
	 *
	 * @param fcpConnection
	 *            The connection that received themessage
	 * @param sskKeypair
	 *            The “SSKKeypair” message
	 */
	public void receivedSSKKeypair(FcpConnection fcpConnection, SSKKeypair sskKeypair);

	/**
	 * Notifies a listener that a “Peer” message was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param peer
	 *            The “Peer” message
	 */
	public void receivedPeer(FcpConnection fcpConnection, Peer peer);

	/**
	 * Notifies a listener that an “EndListPeers” message was received.
	 *
	 * @param fcpConnection
	 *            The connection that recevied the message
	 * @param endListPeers
	 *            The “EndListPeers” message
	 */
	public void receivedEndListPeers(FcpConnection fcpConnection, EndListPeers endListPeers);

	/**
	 * Notifies a listener that a “PeerNote” message was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param peerNote
	 *            The “PeerNote” message
	 */
	public void receivedPeerNote(FcpConnection fcpConnection, PeerNote peerNote);

	/**
	 * Notifies a listener that an “EndListPeerNotes” message was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param endListPeerNotes
	 *            The “EndListPeerNotes” message
	 */
	public void receivedEndListPeerNotes(FcpConnection fcpConnection, EndListPeerNotes endListPeerNotes);

	/**
	 * Notifies a listener that a “PeerRemoved” message was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param peerRemoved
	 *            The “PeerRemoved” message
	 */
	public void receivedPeerRemoved(FcpConnection fcpConnection, PeerRemoved peerRemoved);

	/**
	 * Notifies a listener that a “NodeData” message was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param nodeData
	 *            The “NodeData” message
	 */
	public void receivedNodeData(FcpConnection fcpConnection, NodeData nodeData);

	/**
	 * Notifies a listener that a “TestDDAReply” message was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param testDDAReply
	 *            The “TestDDAReply” message
	 */
	public void receivedTestDDAReply(FcpConnection fcpConnection, TestDDAReply testDDAReply);

	/**
	 * Notifies a listener that a “TestDDAComplete” was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param testDDAComplete
	 *            The “TestDDAComplete” message
	 */
	public void receivedTestDDAComplete(FcpConnection fcpConnection, TestDDAComplete testDDAComplete);

	/**
	 * Notifies a listener that a “PersistentGet” was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param persistentGet
	 *            The “PersistentGet” message
	 */
	public void receivedPersistentGet(FcpConnection fcpConnection, PersistentGet persistentGet);

	/**
	 * Notifies a listener that a “PersistentPut” was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param persistentPut
	 *            The “PersistentPut” message
	 */
	public void receivedPersistentPut(FcpConnection fcpConnection, PersistentPut persistentPut);

	/**
	 * Notifies a listener that a “EndListPersistentRequests” was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param endListPersistentRequests
	 *            The “EndListPersistentRequests” message
	 */
	public void receivedEndListPersistentRequests(FcpConnection fcpConnection, EndListPersistentRequests endListPersistentRequests);

	/**
	 * Notifies a listener that a “URIGenerated” was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param uriGenerated
	 *            The “URIGenerated” message
	 */
	public void receivedURIGenerated(FcpConnection fcpConnection, URIGenerated uriGenerated);

	/**
	 * Notifies a listener that a “DataFound” was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param dataFound
	 *            The “DataFound” message
	 */
	public void receivedDataFound(FcpConnection fcpConnection, DataFound dataFound);

	/**
	 * Notifies a listener that an “AllData” was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param allData
	 *            The “AllData” message
	 */
	public void receivedAllData(FcpConnection fcpConnection, AllData allData);

	/**
	 * Notifies a listener that a “SimpleProgress” was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param simpleProgress
	 *            The “SimpleProgress” message
	 */
	public void receivedSimpleProgress(FcpConnection fcpConnection, SimpleProgress simpleProgress);

	/**
	 * Notifies a listener that a “StartedCompression” was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param startedCompression
	 *            The “StartedCompression” message
	 */
	public void receivedStartedCompression(FcpConnection fcpConnection, StartedCompression startedCompression);

	/**
	 * Notifies a listener that a “FinishedCompression” was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param finishedCompression
	 *            The “FinishedCompression” message
	 */
	public void receivedFinishedCompression(FcpConnection fcpConnection, FinishedCompression finishedCompression);

	/**
	 * Notifies a listener that an “UnknownPeerNoteType” was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param unknownPeerNoteType
	 *            The “UnknownPeerNoteType” message
	 */
	public void receivedUnknownPeerNoteType(FcpConnection fcpConnection, UnknownPeerNoteType unknownPeerNoteType);

	/**
	 * Notifies a listener that a “UnknownNodeIdentifier” message was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param unknownNodeIdentifier
	 *            The “UnknownNodeIdentifier” message
	 */
	public void receivedUnknownNodeIdentifier(FcpConnection fcpConnection, UnknownNodeIdentifier unknownNodeIdentifier);

	/**
	 * Notifies a listener that a “ConfigData” message was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param configData
	 *            The “ConfigData” message
	 */
	public void receivedConfigData(FcpConnection fcpConnection, ConfigData configData);

	/**
	 * Notifies a listener that a “GetFailed” message was recevied.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param getFailed
	 *            The “GetFailed” message
	 */
	public void receivedGetFailed(FcpConnection fcpConnection, GetFailed getFailed);

	/**
	 * Notifies a listener that a “PutFailed” message was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param putFailed
	 *            The “PutFailed” message
	 */
	public void receivedPutFailed(FcpConnection fcpConnection, PutFailed putFailed);

	/**
	 * Notifies a listener that an “IdentifierCollision” message was receied.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param identifierCollision
	 *            The “IdentifierCollision” message
	 */
	public void receivedIdentifierCollision(FcpConnection fcpConnection, IdentifierCollision identifierCollision);

	/**
	 * Notifies a listener that a “PersistentPutDir” message was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param persistentPutDir
	 *            The “PersistentPutDir” message
	 */
	public void receivedPersistentPutDir(FcpConnection fcpConnection, PersistentPutDir persistentPutDir);

	/**
	 * Notifies a listener that a “PersistentRequestRemoved” message was
	 * received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param persistentRequestRemoved
	 *            The “PersistentRequestRemoved” message
	 */
	public void receivedPersistentRequestRemoved(FcpConnection fcpConnection, PersistentRequestRemoved persistentRequestRemoved);

	void receivedSubscribedUSK(FcpConnection fcpConnection, SubscribedUSK subscribedUSK);

	/**
	 * Notifies a listener that a “SubscribedUSKUpdate” message was received.
	 *
	 * @param fcpConnection
	 *            The connection that recevied the message
	 * @param subscribedUSKUpdate
	 *            The “SubscribedUSKUpdate” message
	 */
	public void receivedSubscribedUSKUpdate(FcpConnection fcpConnection, SubscribedUSKUpdate subscribedUSKUpdate);

	/**
	 * Notifies a listener that a “PluginInfo” message was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param pluginInfo
	 *            The “PluginInfo” message
	 */
	public void receivedPluginInfo(FcpConnection fcpConnection, PluginInfo pluginInfo);

	void receivedPluginRemoved(FcpConnection fcpConnection, PluginRemoved pluginRemoved);

	/**
	 * Notifies a listener that an “FCPPluginReply“ message was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param fcpPluginReply
	 *            The “FCPPluginReply” message
	 */
	public void receivedFCPPluginReply(FcpConnection fcpConnection, FCPPluginReply fcpPluginReply);

	/**
	 * Notifies a listener that a “PersistentRequestModified” message was
	 * received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param persistentRequestModified
	 *            The “PersistentRequestModified” message
	 */
	public void receivedPersistentRequestModified(FcpConnection fcpConnection, PersistentRequestModified persistentRequestModified);

	/**
	 * Notifies a listener that a “PutSuccessful” message was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param putSuccessful
	 *            The “PutSuccessful” message
	 */
	public void receivedPutSuccessful(FcpConnection fcpConnection, PutSuccessful putSuccessful);

	/**
	 * Notifies a listener that a “PutFetchable” message was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param putFetchable
	 *            The “PutFetchable” message
	 */
	public void receivedPutFetchable(FcpConnection fcpConnection, PutFetchable putFetchable);

	/**
	 * Notifies a listener that a feed was sent to a peer.
	 *
	 * @param source
	 *            The connection that received the message
	 * @param sentFeed
	 *            The “SentFeed” message
	 */
	public void receivedSentFeed(FcpConnection source, SentFeed sentFeed);

	/**
	 * Notifies a listener that a bookmark was updated.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param receivedBookmarkFeed
	 *            The “ReceivedBookmarkFeed” message
	 */
	public void receivedBookmarkFeed(FcpConnection fcpConnection, ReceivedBookmarkFeed receivedBookmarkFeed);

	/**
	 * Notifies a listener that a “ProtocolError” was received.
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param protocolError
	 *            The “ProtocolError” message
	 */
	public void receivedProtocolError(FcpConnection fcpConnection, ProtocolError protocolError);

	/**
	 * Notifies a listener that a message has been received. This method is
	 * only called if {@link FcpConnection#handleMessage(FcpMessage)} does not
	 * recognize the message. Should that ever happen, please file a bug
	 * report!
	 *
	 * @param fcpConnection
	 *            The connection that received the message
	 * @param fcpMessage
	 *            The message that was received
	 */
	public void receivedMessage(FcpConnection fcpConnection, FcpMessage fcpMessage);

	/**
	 * Notifies a listener that a connection was closed. A closed connection
	 * can be reestablished by calling {@link FcpConnection#connect()} on the
	 * same object again.
	 *
	 * @param fcpConnection
	 *            The connection that was closed.
	 * @param throwable
	 *            The exception that caused the disconnect, or
	 *            <code>null</code> if there was no exception
	 */
	public void connectionClosed(FcpConnection fcpConnection, Throwable throwable);

} public class GenerateSSK extends FcpMessage {

	/**
	 * Creates a new “GenerateSSK” message.
	 */
	public GenerateSSK() {
		this(FcpUtils.getUniqueIdentifier());
	}

	/**
	 * Creates a new “GenerateSSK” message with the given client identifier.
	 *
	 * @param clientIdentifier
	 *            The client identifier
	 */
	public GenerateSSK(String clientIdentifier) {
		super("GenerateSSK");
		setField("Identifier", clientIdentifier);
	}

} public class GetFailed extends BaseMessage implements Identifiable {

	/**
	 * Creates a new “GetFailed” message that wraps the received message.
	 *
	 * @param receivedMessage
	 *            The received message
	 */
	public GetFailed(FcpMessage receivedMessage) {
		super(receivedMessage);
	}

	/**
	 * Returns the code of the error.
	 *
	 * @return The code of the error, or <code>-1</code> if the error code
	 *         could not be parsed
	 */
	public int getCode() {
		return FcpUtils.safeParseInt(getField("Code"));
	}

	/**
	 * Returns the identifier of the request.
	 *
	 * @return The identifier of the request
	 */
	@Override
	public String getIdentifier() {
		return getField("Identifier");
	}

	/**
	 * Returns whether the request is on the global queue.
	 *
	 * @return <code>true</code> if the request is on the global queue,
	 *         <code>false</code> if it is on the client-local queue
	 */
	public boolean isGlobal() {
		return Boolean.valueOf(getField("Global"));
	}

	/**
	 * Returns the description of the error code.
	 *
	 * @return The description of the error code
	 */
	public String getCodeDescription() {
		return getField("CodeDescription");
	}

	/**
	 * Returns the extra description of the error.
	 *
	 * @return The extra description of the error
	 */
	public String getExtraDescription() {
		return getField("ExtraDescription");
	}

	/**
	 * Returns the short description of the error.
	 *
	 * @return The short description of the error
	 */
	public String getShortCodeDescription() {
		return getField("ShortCodeDescription");
	}

	/**
	 * Returns the expected data length, if already knows.
	 *
	 * @return The expected data length, or <code>-1</code> if the length could
	 *         not be parsed
	 */
	public long getExpectedDataLength() {
		return FcpUtils.safeParseLong(getField("ExpectedDataLength"));
	}

	/**
	 * Returns the expected content type of the request.
	 *
	 * @return The expected content type
	 */
	public String getExpectedMetadataContentType() {
		return getField("ExpectedMetadata.ContentType");
	}

	/**
	 * Returns whether the expected values (see
	 * {@link #getExpectedDataLength()} and
	 * {@link #getExpectedMetadataContentType()}) have already been finalized
	 * and can be trusted. If the values have not been finalized that can
	 * change over time.
	 *
	 * @return <code>true</code> if the expected values have already been
	 *         finalized, <code>false</code> otherwise
	 */
	public boolean isFinalizedExpected() {
		return Boolean.valueOf(getField("FinalizedExpected"));
	}

	/**
	 * Returns the URI the request is redirected to (in case of a request for a
	 * USK). This is returned so that client applications know that the URI of
	 * the key has updated.
	 *
	 * @return The URI the request was redirected to
	 */
	public String getRedirectURI() {
		return getField("RedirectURI");
	}

	/**
	 * Returns whether the request failed fatally. If a request fails fatally
	 * it can never complete, even with inifinite retries.
	 *
	 * @return <code>true</code> if the request failed fatally,
	 *         <code>false</code> otherwise
	 */
	public boolean isFatal() {
		return Boolean.valueOf(getField("Fatal"));
	}

	/**
	 * Returns a list of complex error codes with the message. Use
	 * {@link #getComplexErrorDescription(int)} and
	 * {@link #getComplexErrorCount(int)} to get details.
	 *
	 * @return A list of complex error codes
	 */
	public int[] getComplexErrorCodes() {
		Map<String, String> allFields = getFields();
		List<Integer> errorCodeList = new ArrayList<Integer>();
		for (Entry<String, String> field : allFields.entrySet()) {
			String fieldKey = field.getKey();
			if (fieldKey.startsWith("Errors.")) {
				int nextDot = fieldKey.indexOf('.', 7);
				if (nextDot > -1) {
					int errorCode = FcpUtils.safeParseInt(fieldKey.substring(7, nextDot));
					if (errorCode != -1) {
						errorCodeList.add(errorCode);
					}
				}
			}
		}
		int[] errorCodes = new int[errorCodeList.size()];
		int errorIndex = 0;
		for (int errorCode : errorCodeList) {
			errorCodes[errorIndex++] = errorCode;
		}
		return errorCodes;
	}

	/**
	 * Returns the description of the complex error. You should only hand it
	 * error codes you got from {@link #getComplexErrorCodes()}!
	 *
	 * @param errorCode
	 *            The error code
	 * @return The description of the complex error
	 */
	public String getComplexErrorDescription(int errorCode) {
		return getField("Errors." + errorCode + ".Description");
	}

	/**
	 * Returns the count of the complex error. You should only hand it error
	 * codes you got from {@link #getComplexErrorCodes()}!
	 *
	 * @param errorCode
	 *            The error code
	 * @return The count of the complex error, or <code>-1</code> if the count
	 *         could not be parsed
	 */
	public int getComplexErrorCount(int errorCode) {
		return FcpUtils.safeParseInt(getField("Errors." + errorCode + ".Count"));
	}

} public class GetNode extends FcpMessage {

	/**
	 * Creates a “GetNode” command that returns the darknet noderef of the
	 * node.
	 */
	public GetNode() {
		this(null, null, null);
	}

	/**
	 * Creates a “GetNode” command that returns the request noderef of the
	 * node, including private and volatile data, if requested. If any of the
	 * Boolean parameters are <code>null</code> the parameter is ignored and
	 * the node’s default value is used.
	 *
	 * @param giveOpennetRef
	 *            <code>true</code> to request the opennet noderef,
	 *            <code>false</code> for darknet
	 * @param withPrivate
	 *            <code>true</code> to include private data in the noderef
	 * @param withVolatile
	 *            <code>true</code> to include volatile data in the noderef
	 */
	public GetNode(Boolean giveOpennetRef, Boolean withPrivate, Boolean withVolatile) {
		this(null, giveOpennetRef, withPrivate, withVolatile);
	}

	public GetNode(String identifier, Boolean giveOpennetRef, Boolean withPrivate, Boolean withVolatile) {
		super("GetNode");
		if (identifier != null) {
			setField("Identifier", identifier);
		}
		if (giveOpennetRef != null) {
			setField("GiveOpennetRef", String.valueOf(giveOpennetRef));
		}
		if (withPrivate != null) {
			setField("WithPrivate", String.valueOf(withPrivate));
		}
		if (withVolatile != null) {
			setField("WithVolatile", String.valueOf(withVolatile));
		}
	}

} public class ListPeerNotes extends FcpMessage {

	/**
	 * Creates a new “ListPeerNotes” request that lists all notes of the
	 * specified node.
	 *
	 * @param nodeIdentifier
	 *            The identifier of the node
	 */
	public ListPeerNotes(String nodeIdentifier) {
		super("ListPeerNotes");
		setField("NodeIdentifier", nodeIdentifier);
	}

	public ListPeerNotes(String identifier, String nodeIdentifier) {
		this(nodeIdentifier);
		setField("Identifier", identifier);
	}

} public class ListPeers extends FcpMessage {

	/**
	 * Creates a new “ListPeers” request that only includes basic data of the
	 * peers.
	 *
	 * @param identifier
	 *            The identifier of the request
	 */
	public ListPeers(String identifier) {
		this(identifier, false, false);
	}

	/**
	 * Creates a new “ListPeers” request that includes wanted data of the
	 * peers.
	 *
	 * @param identifier
	 *            The identifier of the request
	 * @param withMetadata
	 *            If <code>true</code> metadata of the peers is included in the
	 *            reply
	 * @param withVolatile
	 *            if <code>true</code> volatile data of the peers is included
	 *            in the reply
	 */
	public ListPeers(String identifier, boolean withMetadata, boolean withVolatile) {
		super("ListPeers");
		setField("Identifier", identifier);
		setField("WithMetadata", String.valueOf(withMetadata));
		setField("WithVolatile", String.valueOf(withVolatile));
	}

} public class ListPersistentRequests extends FcpMessage {

	/**
	 * Creates a new “ListPersistentRequests” command that lists all persistent
	 * requests in the current queue.
	 */
	public ListPersistentRequests() {
		super("ListPersistentRequests");
	}

} public class ModifyPeer extends FcpMessage {

	public ModifyPeer(String identifier, String nodeIdentifier) {
		super("ModifyPeer");
		setField("Identifier", identifier);
		setField("NodeIdentifier", nodeIdentifier);
	}

	@Deprecated
	public ModifyPeer(String nodeIdentifier, Boolean allowLocalAddresses, Boolean disabled, Boolean listenOnly) {
		super("ModifyPeer");
		setField("NodeIdentifier", nodeIdentifier);
		if (allowLocalAddresses != null) {
			setAllowLocalAddresses(allowLocalAddresses);
		}
		if (disabled != null) {
			setEnabled(!disabled);
		}
		if (listenOnly != null) {
			setListenOnly(listenOnly);
		}
	}

	public void setAllowLocalAddresses(boolean allowLocalAddresses) {
		setField("AllowLocalAddresses", String.valueOf(allowLocalAddresses));
	}

	public void setEnabled(boolean enabled) {
		setField("IsDisabled", String.valueOf(!enabled));
	}

	public void setListenOnly(boolean listenOnly) {
		setField("IsListenOnly", String.valueOf(listenOnly));
	}

	public void setBurstOnly(boolean burstOnly) {
		setField("IsBurstOnly", String.valueOf(burstOnly));
	}

	public void setIgnoreSource(boolean ignoreSource) {
		setField("IgnoreSourcePort", String.valueOf(ignoreSource));
	}

} public class ModifyPeerNote extends FcpMessage {

	private static final FreenetBase64 BASE64_ENCODER = new FreenetBase64();

	public ModifyPeerNote(String identifier, String nodeIdentifier) {
		super("ModifyPeerNote");
		setField("Identifier", identifier);
		setField("NodeIdentifier", nodeIdentifier);
	}

	@Deprecated
	public ModifyPeerNote(String nodeIdentifier, String noteText, int peerNoteType) {
		super("ModifyPeerNote");
		setField("NodeIdentifier", nodeIdentifier);
		setField("NoteText", noteText);
		setField("PeerNoteType", String.valueOf(peerNoteType));
	}

	public void setNoteText(String noteText) {
		setField("NoteText", BASE64_ENCODER.encode(noteText.getBytes(Charsets.UTF_8)));
	}

	public void setPeerNoteType(PeerNoteType peerNoteType) {
		setField("PeerNoteType", peerNoteType.toString());
	}

} public class NodeData extends BaseMessage {

	/** The noderef of the node. */
	private final NodeRef nodeRef;

	/**
	 * Creates a new “NodeData” message that wraps the received message.
	 *
	 * @param receivedMessage
	 *            The received message
	 */
	public NodeData(FcpMessage receivedMessage) {
		super(receivedMessage);
		nodeRef = new NodeRef(receivedMessage);
	}

	/**
	 * Returns the noderef of the node.
	 *
	 * @return The noderef of the node
	 */
	public NodeRef getNodeRef() {
		return nodeRef;
	}

	/**
	 * Returns the last good version, i.e. the oldest version the node will
	 * connect to.
	 *
	 * @return The last good version
	 */
	public Version getLastGoodVersion() {
		return nodeRef.getLastGoodVersion();
	}

	/**
	 * Returns the signature of the noderef.
	 *
	 * @return The signature of the noderef
	 */
	public String getSignature() {
		return nodeRef.getSignature();
	}

	/**
	 * Returns whether the noderef is the opennet noderef of the node
	 *
	 * @return <code>true</code> if the noderef is the opennet noderef of the
	 *         node, <code>false</code> otherwise
	 */
	public boolean isOpennet() {
		return nodeRef.isOpennet();
	}

	/**
	 * Returns the identity of the node
	 *
	 * @return The identity of the node
	 */
	public String getIdentity() {
		return nodeRef.getIdentity();
	}

	/**
	 * Returns the name of the node.
	 *
	 * @return The name of the node
	 */
	public String getMyName() {
		return nodeRef.getMyName();
	}

	/**
	 * Returns the version of the node.
	 *
	 * @return The version of the node
	 */
	public Version getVersion() {
		return nodeRef.getVersion();
	}

	/**
	 * Returns IP addresses and port number of the node.
	 *
	 * @return The IP addresses and port numbers of the node
	 */
	public String getPhysicalUDP() {
		return nodeRef.getPhysicalUDP();
	}

	/**
	 * Returns the ARK of the node.
	 *
	 * @return The ARK of the node
	 */
	public ARK getARK() {
		return nodeRef.getARK();
	}

	/**
	 * Returns the public key of the node.
	 *
	 * @return The public key of the node
	 */
	public String getDSAPublicKey() {
		return nodeRef.getDSAPublicKey();
	}

	/**
	 * Returns the private key of the node.
	 *
	 * @return The private key of the node
	 */
	public String getDSKPrivateKey() {
		return getField("dsaPrivKey.x");
	}

	/**
	 * Returns the DSA group of the node.
	 *
	 * @return The DSA group of the node
	 */
	public DSAGroup getDSAGroup() {
		return nodeRef.getDSAGroup();
	}

	/**
	 * Returns the negotiation types supported by the node.
	 *
	 * @return The node’s supported negotiation types
	 */
	public int[] getNegotiationTypes() {
		return nodeRef.getNegotiationTypes();
	}

	/**
	 * Returns one of the volatile fields from the message. The given field
	 * name is prepended with “volatile.” so if you want to get the value of
	 * the field with the name “volatile.freeJavaMemory” you only need to
	 * specify “freeJavaMemory”.
	 *
	 * @param field
	 *            The name of the field
	 * @return The value of the field, or <code>null</code> if there is no such
	 *         field
	 */
	public String getVolatile(String field) {
		return getField("volatile." + field);
	}

} public class NodeHello extends BaseMessage {

	/**
	 * Createa a new “NodeHello” message that wraps the received message.
	 *
	 * @param receivedMessage
	 *            The received FCP message
	 */
	public NodeHello(FcpMessage receivedMessage) {
		super(receivedMessage);
	}

	/**
	 * Returns the build of the node. This may not be a number but also a
	 * string like “@custom@” in case you built the node yourself.
	 *
	 * @return The build of the node
	 */
	public String getBuild() {
		return getField("Build");
	}

	/**
	 * Returns the build number of the node. This may not be a number but also
	 * a string like “@custom@” in case you built the node yourself.
	 *
	 * @return The build number of the node, or <code>-1</code> if the build
	 *         number could not be determined
	 */
	public int getBuildNumber() {
		return FcpUtils.safeParseInt(getBuild());
	}

	/**
	 * Returns the number of compression codecs.
	 *
	 * @return The number of compression codecs
	 */
	public String getCompressionCodecs() {
		return getField("CompressionCodecs");
	}

	/**
	 * Returns the number of compression codecs.
	 *
	 * @return The number of compression codecs, or <code>-1</code> if the
	 *         number of compression codecs could not be determined
	 */
	public int getCompressionCodecsNumber() {
		return FcpUtils.safeParseInt(getCompressionCodecs());
	}

	/**
	 * Returns the unique connection identifier.
	 *
	 * @return The connection identifier
	 */
	public String getConnectionIdentifier() {
		return getField("ConnectionIdentifier");
	}

	/**
	 * Returns the build of the external library file.
	 *
	 * @return The build of the external library file
	 */
	public String getExtBuild() {
		return getField("ExtBuild");
	}

	/**
	 * Returns the build number of the external library file.
	 *
	 * @return The build number of the external library file, or
	 *         <code>-1</code> if the build number could not be determined
	 */
	public int getExtBuildNumber() {
		return FcpUtils.safeParseInt(getExtBuild());
	}

	/**
	 * Returns the revision of the external library file.
	 *
	 * @return The revision of the external library file
	 */
	public String getExtRevision() {
		return getField("ExtRevision");
	}

	/**
	 * Returns the revision number of the external library file.
	 *
	 * @return The revision number of the external library file, or
	 *         <code>-1</code> if the revision number could not be determined
	 */
	public int getExtRevisionNumber() {
		return FcpUtils.safeParseInt(getExtRevision());
	}

	/**
	 * Returns the FCP version the node speaks.
	 *
	 * @return The FCP version the node speaks
	 */
	public String getFCPVersion() {
		return getField("FCPVersion");
	}

	/**
	 * Returns the make of the node, e.g. “Fred” (freenet reference
	 * implementation).
	 *
	 * @return The make of the node
	 */
	public String getNode() {
		return getField("Node");
	}

	/**
	 * Returns the language of the node as 2-letter code, e.g. “en” or “de”.
	 *
	 * @return The language of the node
	 */
	public String getNodeLanguage() {
		return getField("NodeLanguage");
	}

	/**
	 * Returns the revision of the node.
	 *
	 * @return The revision of the node
	 */
	public String getRevision() {
		return getField("Revision");
	}

	/**
	 * Returns the revision number of the node.
	 *
	 * @return The revision number of the node, or <code>-1</code> if the
	 *         revision number coult not be determined
	 */
	public int getRevisionNumber() {
		return FcpUtils.safeParseInt(getRevision());
	}

	/**
	 * Returns whether the node is currently is testnet mode.
	 *
	 * @return <code>true</code> if the node is currently in testnet mode,
	 *         <code>false</code> otherwise
	 */
	public boolean getTestnet() {
		return Boolean.valueOf(getField("Testnet"));
	}

	/**
	 * Returns the version of the node.
	 *
	 * @return The version of the node
	 */
	public String getVersion() {
		return getField("Version");
	}

} public class NodeRef {

	/** The identity of the node. */
	private String identity;

	/** Whether the node is an opennet peer. */
	private boolean opennet;

	/** The name of the node. */
	private String name;

	/** The location of the node. */
	private double location;

	/** The IP addresses and ports of the node. */
	private String physicalUDP;

	/** The ARK of the node. */
	private ARK ark;

	/** The public DSA key of the node. */
	private String dsaPublicKey;

	/** The DSA group of the node. */
	private DSAGroup dsaGroup;

	/** The node’s supported negotiation types. */
	private int[] negotiationTypes;

	/** The version of the node. */
	private Version version;

	/** The oldest version the node will connect to. */
	private Version lastGoodVersion;

	/** Whether the node is a testnet node. */
	private boolean testnet;

	/** The signature of the reference. */
	private String signature;

	/**
	 * Creates a new, empty noderef.
	 */
	public NodeRef() {
		/* intentionally left blank. */
	}

	/**
	 * Creates a new noderef that is initialized with fields from the given
	 * message.
	 *
	 * @param fromMessage
	 *            The message to get initial values for the noderef from
	 */
	public NodeRef(FcpMessage fromMessage) {
		identity = fromMessage.getField("identity");
		opennet = Boolean.valueOf(fromMessage.getField("opennet"));
		name = fromMessage.getField("myName");
		if (fromMessage.hasField("location")) {
			location = Double.valueOf(fromMessage.getField("location"));
		}
		physicalUDP = fromMessage.getField("physical.udp");
		ark = new ARK(fromMessage.getField("ark.pubURI"), fromMessage.getField("ark.privURI"), fromMessage.getField("ark.number"));
		dsaPublicKey = fromMessage.getField("dsaPubKey.y");
		dsaGroup = new DSAGroup(fromMessage.getField("dsaGroup.b"), fromMessage.getField("dsaGroup.p"), fromMessage.getField("dsaGroup.q"));
		negotiationTypes = FcpUtils.decodeMultiIntegerField(fromMessage.getField("auth.negTypes"));
		version = new Version(fromMessage.getField("version"));
		lastGoodVersion = new Version(fromMessage.getField("lastGoodVersion"));
		testnet = Boolean.valueOf(fromMessage.getField("testnet"));
		signature = fromMessage.getField("sig");
	}

	/**
	 * Returns the identity of the node.
	 *
	 * @return The identity of the node
	 */
	public String getIdentity() {
		return identity;
	}

	/**
	 * Sets the identity of the node.
	 *
	 * @param identity
	 *            The identity of the node
	 */
	public void setIdentity(String identity) {
		this.identity = identity;
	}

	/**
	 * Returns whether the node is an opennet peer.
	 *
	 * @return <code>true</code> if the node is an opennet peer,
	 *         <code>false</code> otherwise
	 */
	public boolean isOpennet() {
		return opennet;
	}

	/**
	 * Sets whether the node is an opennet peer.
	 *
	 * @param opennet
	 *            <code>true</code> if the node is an opennet peer,
	 *            <code>false</code> otherwise
	 */
	public void setOpennet(boolean opennet) {
		this.opennet = opennet;
	}

	/**
	 * Returns the name of the node. If the node is an opennet peer, it will
	 * not have a name!
	 *
	 * @return The name of the node, or <code>null</code> if the node is an
	 *         opennet peer
	 */
	public String getMyName() {
		return name;
	}

	/**
	 * Sets the name of the peer.
	 *
	 * @param name
	 *            The name of the peer
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * Returns the location of the node.
	 *
	 * @return The location of the node
	 */
	public double getLocation() {
		return location;
	}

	/**
	 * Sets the location of the node
	 *
	 * @param location
	 *            The location of the node
	 */
	public void setLocation(double location) {
		this.location = location;
	}

	/**
	 * Returns the IP addresses and port numbers of the node.
	 *
	 * @return The IP addresses and port numbers of the node
	 */
	public String getPhysicalUDP() {
		return physicalUDP;
	}

	/**
	 * Sets the IP addresses and port numbers of the node.
	 *
	 * @param physicalUDP
	 *            The IP addresses and port numbers of the node
	 */
	public void setPhysicalUDP(String physicalUDP) {
		this.physicalUDP = physicalUDP;
	}

	/**
	 * Returns the ARK of the node.
	 *
	 * @return The ARK of the node
	 */
	public ARK getARK() {
		return ark;
	}

	/**
	 * Sets the ARK of the node.
	 *
	 * @param ark
	 *            The ARK of the node
	 */
	public void setARK(ARK ark) {
		this.ark = ark;
	}

	/**
	 * Returns the public DSA key of the node.
	 *
	 * @return The public DSA key of the node
	 */
	public String getDSAPublicKey() {
		return dsaPublicKey;
	}

	/**
	 * Sets the public DSA key of the node.
	 *
	 * @param dsaPublicKey
	 *            The public DSA key of the node
	 */
	public void setDSAPublicKey(String dsaPublicKey) {
		this.dsaPublicKey = dsaPublicKey;
	}

	/**
	 * Returns the DSA group of the node.
	 *
	 * @return The DSA group of the node
	 */
	public DSAGroup getDSAGroup() {
		return dsaGroup;
	}

	/**
	 * Sets the DSA group of the node.
	 *
	 * @param dsaGroup
	 *            The DSA group of the node
	 */
	public void setDSAGroup(DSAGroup dsaGroup) {
		this.dsaGroup = dsaGroup;
	}

	/**
	 * Returns the negotiation types supported by the node.
	 *
	 * @return The node’s supported negotiation types
	 */
	public int[] getNegotiationTypes() {
		return negotiationTypes;
	}

	/**
	 * Sets the negotiation types supported by the node.
	 *
	 * @param negotiationTypes
	 *            The node’s supported negotiation types
	 */
	public void setNegotiationTypes(int[] negotiationTypes) {
		this.negotiationTypes = negotiationTypes;
	}

	/**
	 * Returns the version of the node.
	 *
	 * @return The version of the node
	 */
	public Version getVersion() {
		return version;
	}

	/**
	 * Sets the version of the node.
	 *
	 * @param version
	 *            The version of the node
	 */
	public void setVersion(Version version) {
		this.version = version;
	}

	/**
	 * Returns the last good version of the node.
	 *
	 * @return The oldest version the node will connect to
	 */
	public Version getLastGoodVersion() {
		return lastGoodVersion;
	}

	/**
	 * Sets the last good version of the node.
	 *
	 * @param lastGoodVersion
	 *            The oldest version the node will connect to
	 */
	public void setLastGoodVersion(Version lastGoodVersion) {
		this.lastGoodVersion = lastGoodVersion;
	}

	/**
	 * Returns whether the node is a testnet node.
	 *
	 * @return <code>true</code> if the node is a testnet node,
	 *         <code>false</code> otherwise
	 */
	public boolean isTestnet() {
		return testnet;
	}

	/**
	 * Sets whether this node is a testnet node.
	 *
	 * @param testnet
	 *            <code>true</code> if the node is a testnet node,
	 *            <code>false</code> otherwise
	 */
	public void setTestnet(boolean testnet) {
		this.testnet = testnet;
	}

	/**
	 * Returns the signature of the noderef.
	 *
	 * @return The signature of the noderef
	 */
	public String getSignature() {
		return signature;
	}

	/**
	 * Sets the signature of the noderef.
	 *
	 * @param signature
	 *            The signature of the noderef
	 */
	public void setSignature(String signature) {
		this.signature = signature;
	}

} public class Peer extends BaseMessage implements Identifiable {

	/**
	 * Creates a new “Peer” reply from the received message.
	 *
	 * @param receivedMessage
	 *            The received message
	 */
	public Peer(FcpMessage receivedMessage) {
		super(receivedMessage);
	}

	/**
	 * Returns a collection of fields as a node reference.
	 *
	 * @return The node reference contained within this message
	 */
	public NodeRef getNodeRef() {
		NodeRef nodeRef = new NodeRef();
		nodeRef.setARK(getARK());
		nodeRef.setDSAGroup(getDSAGroup());
		nodeRef.setDSAPublicKey(getDSAPublicKey());
		nodeRef.setIdentity(getIdentity());
		nodeRef.setLastGoodVersion(getLastGoodVersion());
		nodeRef.setLocation(getLocation());
		nodeRef.setName(getMyName());
		nodeRef.setNegotiationTypes(getNegotiationTypes());
		nodeRef.setOpennet(isOpennet());
		nodeRef.setPhysicalUDP(getPhysicalUDP());
		nodeRef.setVersion(getVersion());
		return nodeRef;
	}

	/**
	 * Returns the identifier of the request.
	 *
	 * @return The identifier of the request
	 */
	@Override
	public String getIdentifier() {
		return getField("Identifier");
	}

	/**
	 * Returns the “physical.udp” line from the message. It contains all IP
	 * addresses and port numbers of the peer.
	 *
	 * @return The IP addresses and port numbers of the peer
	 */
	public String getPhysicalUDP() {
		return getField("physical.udp");
	}

	/**
	 * Returns whether the listed peer is an opennet peer.
	 *
	 * @return <code>true</code> if the peer is an opennet peer,
	 *         <code>false</code> if the peer is a darknet peer
	 */
	public boolean isOpennet() {
		return Boolean.valueOf(getField("opennet"));
	}

	/**
	 * Returns whether this peer is a seed.
	 *
	 * @return <code>true</code> if the peer is a seed, <code>false</code>
	 *         otherwise
	 */
	public boolean isSeed() {
		return Boolean.valueOf(getField("seed"));
	}

	/**
	 * Returns the “y” part of the peer’s public DSA key.
	 *
	 * @return The public DSA key
	 */
	public String getDSAPublicKey() {
		return getField("dsaPubKey.y");
	}

	/**
	 * Returns the DSA group of the peer.
	 *
	 * @return The DSA group of the peer
	 */
	public DSAGroup getDSAGroup() {
		return new DSAGroup(getField("dsaGroup.g"), getField("dsaGroup.p"), getField("dsaGroup.q"));
	}

	/**
	 * Returns the last good version of the peer, i.e. the oldest version the
	 * peer will connect to.
	 *
	 * @return The last good version of the peer
	 */
	public Version getLastGoodVersion() {
		return new Version(getField("lastGoodVersion"));
	}

	/**
	 * Returns the ARK of the peer.
	 *
	 * @return The ARK of the peer
	 */
	public ARK getARK() {
		return new ARK(getField("ark.pubURI"), getField("ark.number"));
	}

	/**
	 * Returns the identity of the peer.
	 *
	 * @return The identity of the peer
	 */
	public String getIdentity() {
		return getField("identity");
	}

	/**
	 * Returns the name of the peer. If the peer is not a darknet peer it will
	 * have no name.
	 *
	 * @return The name of the peer, or <code>null</code> if the peer is an
	 *         opennet peer
	 */
	public String getMyName() {
		return getField("myName");
	}

	/**
	 * Returns the location of the peer.
	 *
	 * @return The location of the peer
	 * @throws NumberFormatException
	 *             if the field can not be parsed
	 */
	public double getLocation() throws NumberFormatException {
		return Double.valueOf(getField("location"));
	}

	/**
	 * Returns whether the peer is a testnet node.
	 *
	 * @return <code>true</code> if the peer is a testnet node,
	 *         <code>false</code> otherwise
	 */
	public boolean isTestnet() {
		return Boolean.valueOf("testnet");
	}

	/**
	 * Returns the version of the peer.
	 *
	 * @return The version of the peer
	 */
	public Version getVersion() {
		return new Version(getField("version"));
	}

	/**
	 * Returns the negotiation types the peer supports.
	 *
	 * @return The supported negotiation types
	 */
	public int[] getNegotiationTypes() {
		return FcpUtils.decodeMultiIntegerField(getField("auth.negTypes"));
	}

	/**
	 * Returns all volatile fields from the message.
	 *
	 * @return All volatile files
	 */
	public Map<String, String> getVolatileFields() {
		Map<String, String> volatileFields = new HashMap<String, String>();
		for (Entry<String, String> field : getFields().entrySet()) {
			if (field.getKey().startsWith("volatile.")) {
				volatileFields.put(field.getKey(), field.getValue());
			}
		}
		return Collections.unmodifiableMap(volatileFields);
	}

	/**
	 * Returns one of the volatile fields from the message. The given field
	 * name is prepended with “volatile.” so if you want to get the value of
	 * the field with the name “volatile.status” you only need to specify
	 * “status”.
	 *
	 * @param field
	 *            The name of the field
	 * @return The value of the field, or <code>null</code> if there is no such
	 *         field
	 */
	public String getVolatile(String field) {
		return getField("volatile." + field);
	}

	/**
	 * Returns all metadata fields from the message.
	 *
	 * @return All volatile files
	 */
	public Map<String, String> getMetadataFields() {
		Map<String, String> metadataFields = new HashMap<String, String>();
		for (Entry<String, String> field : getFields().entrySet()) {
			if (field.getKey().startsWith("metadata.")) {
				metadataFields.put(field.getKey(), field.getValue());
			}
		}
		return Collections.unmodifiableMap(metadataFields);
	}

	/**
	 * Returns one of the metadata fields from the message. The given field
	 * name is prepended with “metadata.” so if you want to get the value of
	 * the field with the name “metadata.timeLastRoutable” you only need to
	 * specify “timeLastRoutable”.
	 *
	 * @param field
	 *            The name of the field
	 * @return The value of the field, or <code>null</code> if there is no such
	 *         field
	 */
	public String getMetadata(String field) {
		return getField("metadata." + field);
	}

} public class PeerNote extends BaseMessage {

	private static final FreenetBase64 BASE64_DECODER = new FreenetBase64();

	/** The type for base64 encoded peer notes. */
	public static final int TYPE_PRIVATE_PEER_NOTE = 1;

	/**
	 * Creates a “PeerNote” message that wraps the recevied message.
	 *
	 * @param receivedMessage
	 *            The received message
	 */
	public PeerNote(FcpMessage receivedMessage) {
		super(receivedMessage);
	}

	/**
	 * Returns the identifier of the node this note belongs to.
	 *
	 * @return The note’s node’s identifier
	 */
	public String getNodeIdentifier() {
		return getField("NodeIdentifier");
	}

	/**
	 * Returns the base64-encoded note text.
	 *
	 * @return The note text
	 */
	public String getNoteText() {
		return new String(BASE64_DECODER.decode(getField("NoteText")), Charsets.UTF_8);
	}

	/**
	 * Returns the type of the peer note.
	 *
	 * @return The type of the peer note, or <code>-1</code> if the type can
	 *         not be parsed
	 */
	public int getPeerNoteType() {
		return FcpUtils.safeParseInt(getField("PeerNoteType"));
	}

} public class PeerRemoved extends BaseMessage {

	/**
	 * Creates a new “PeerRemoved” message that wraps the received message.
	 *
	 * @param receivedMessage
	 *            The received message
	 */
	public PeerRemoved(FcpMessage receivedMessage) {
		super(receivedMessage);
	}

	/**
	 * Returns the identity of the removed peer.
	 *
	 * @return The identity of the removed peer
	 */
	public String getIdentity() {
		return getField("Identity");
	}

	/**
	 * Returns the node identifier of the removed peer.
	 *
	 * @return The node identifier of the removed peer
	 */
	public String getNodeIdentifier() {
		return getField("NodeIdentifier");
	}

} public class PersistentGet extends BaseMessage implements Identifiable {

	/**
	 * Creates a new “PersistentGet” message that wraps the received message.
	 *
	 * @param receivedMessage
	 *            The received message
	 */
	public PersistentGet(FcpMessage receivedMessage) {
		super(receivedMessage);
	}

	/**
	 * Returns the identifier of the request.
	 *
	 * @return The identifier of the request
	 */
	@Override
	public String getIdentifier() {
		return getField("Identifier");
	}

	/**
	 * Returns the URI of the request.
	 *
	 * @return The URI of the request
	 */
	public String getURI() {
		return getField("URI");
	}

	/**
	 * Returns the verbosity of the request.
	 *
	 * @return The verbosity of the request
	 */
	public Verbosity getVerbosity() {
		return Verbosity.valueOf(getField("Verbosity"));
	}

	/**
	 * Returns the return type of the request.
	 *
	 * @return The return type of the request
	 */
	public ReturnType getReturnType() {
		try {
			return ReturnType.valueOf(getField("ReturnType"));
		} catch (IllegalArgumentException iae1) {
			return ReturnType.unknown;
		}
	}

	/**
	 * Returns the name of the file the data is downloaded to. This field will
	 * only be set if {@link #getReturnType()} is {@link ReturnType#disk}.
	 *
	 * @return The name of the file the data is downloaded to
	 */
	public String getFilename() {
		return getField("Filename");
	}

	/**
	 * Returns the name of the temporary file. This field will only be set if
	 * {@link #getReturnType()} is {@link ReturnType#disk}.
	 *
	 * @return The name of the temporary file
	 */
	public String getTempFilename() {
		return getField("TempFilename");
	}

	/**
	 * Returns the client token of the request.
	 *
	 * @return The client token of the request
	 */
	public String getClientToken() {
		return getField("ClientToken");
	}

	/**
	 * Returns the priority of the request.
	 *
	 * @return The priority of the request
	 */
	public Priority getPriority() {
		return Priority.values()[FcpUtils.safeParseInt(getField("PriorityClass"), Priority.unknown.ordinal())];
	}

	/**
	 * Returns the persistence of the request.
	 *
	 * @return The persistence of the request, or {@link Persistence#unknown}
	 *         if the persistence could not be parsed
	 */
	public Persistence getPersistence() {
		try {
			return Persistence.valueOf(getField("Persistence"));
		} catch (IllegalArgumentException iae1) {
			return Persistence.unknown;
		}
	}

	/**
	 * Returns whether this request is on the global queue or on the
	 * client-local queue.
	 *
	 * @return <code>true</code> if the request is on the global queue,
	 *         <code>false</code> if the request is on the client-local queue
	 */
	public boolean isGlobal() {
		return Boolean.valueOf(getField("Global"));
	}

	/**
	 * Returns the maximum number of retries for a failed block.
	 *
	 * @return The maximum number of retries for a failed block,
	 *         <code>-1</code> for endless retries, <code>-2</code> if the
	 *         number could not be parsed
	 */
	public int getMaxRetries() {
		return FcpUtils.safeParseInt(getField("MaxRetries"), -2);
	}

} public class PersistentPut extends BaseMessage implements Identifiable {

	/**
	 * Creates a new “PersistentPut” message that wraps the received message.
	 *
	 * @param receivedMessage
	 *            The received message
	 */
	public PersistentPut(FcpMessage receivedMessage) {
		super(receivedMessage);
	}

	/**
	 * Returns the client token of the request.
	 *
	 * @return The client token of the request
	 */
	public String getClientToken() {
		return getField("ClientToken");
	}

	/**
	 * Returns the data length of the request.
	 *
	 * @return The data length of the request, or <code>-1</code> if the length
	 *         could not be parsed
	 */
	public long getDataLength() {
		return FcpUtils.safeParseLong(getField("DataLength"));
	}

	/**
	 * Returns whether the request is on the global queue.
	 *
	 * @return <code>true</code> if the request is on the global queue,
	 *         <code>false</code> otherwise
	 */
	public boolean isGlobal() {
		return Boolean.valueOf(getField("Global"));
	}

	/**
	 * Returns the identifier of the request.
	 *
	 * @return The identifier of the request
	 */
	@Override
	public String getIdentifier() {
		return getField("Identifier");
	}

	/**
	 * Returns the maximum number of retries for failed blocks. When
	 * <code>-1</code> is returned each block is tried forever.
	 *
	 * @return The maximum number of retries for failed blocks, or
	 *         <code>-1</code> for unlimited retries, or <code>-2</code> if the
	 *         number of retries could not be parsed
	 */
	public int getMaxRetries() {
		return FcpUtils.safeParseInt(getField("MaxRetries"));
	}

	/**
	 * Returns the content type of the data.
	 *
	 * @return The content type
	 */
	public String getMetadataContentType() {
		return getField("Metadata.ContentType");
	}

	/**
	 * Returns the persistence of the request.
	 *
	 * @return The persistence of the request
	 */
	public Persistence getPersistence() {
		return Persistence.valueOf(getField("Persistence"));
	}

	/**
	 * Returns the priority of the request.
	 *
	 * @return The priority of the request, or {@link Priority#unknown} if the
	 *         priority could not be parsed
	 */
	public Priority getPriority() {
		return Priority.values()[FcpUtils.safeParseInt(getField("PriorityClass"), Priority.unknown.ordinal())];
	}

	/**
	 * Returns whether this request has started.
	 *
	 * @return <code>true</code> if the request has started, <code>false</code>
	 *         otherwise
	 */
	public boolean isStarted() {
		return Boolean.valueOf(getField("Started"));
	}

	/**
	 * Returns the target filename of the request.
	 *
	 * @return The target filename of the request
	 */
	public String getTargetFilename() {
		return getField("TargetFilename");
	}

	/**
	 * Returns the upload source of the request.
	 *
	 * @return The upload source of the request
	 */
	public UploadFrom getUploadFrom() {
		return UploadFrom.valueOf(getField("UploadFrom"));
	}

	/**
	 * Returns the target URI of the request.
	 *
	 * @return The target URI of the request
	 */
	public String getURI() {
		return getField("URI");
	}

	/**
	 * Returns the verbosity of the request.
	 *
	 * @return The verbosity of the request
	 */
	public Verbosity getVerbosity() {
		return Verbosity.valueOf(getField("Verbosity"));
	}

} public class ProtocolError extends BaseMessage {

	/**
	 * Creates a new “ProtocolError” message that wraps the received message.
	 *
	 * @param receivedMessage
	 *            The received message
	 */
	public ProtocolError(FcpMessage receivedMessage) {
		super(receivedMessage);
	}

	/**
	 * Returns whether the causing message had the “Global” flag set.
	 *
	 * @return <code>true</code> if the causing message had the “Global” flag
	 *         set
	 */
	public boolean isGlobal() {
		return Boolean.valueOf(getField("Global"));
	}

	/**
	 * Returns the error code.
	 *
	 * @return The error code, or <code>-1</code> if the error code could not
	 *         be parsed
	 */
	public int getCode() {
		return FcpUtils.safeParseInt(getField("Code"));
	}

	/**
	 * Returns the description of the error.
	 *
	 * @return The description of the error
	 */
	public String getCodeDescription() {
		return getField("CodeDescription");
	}

	/**
	 * Returns some extra description of the error.
	 *
	 * @return Extra description of the error, or <code>null</code> if there is
	 *         none
	 */
	public String getExtraDescription() {
		return getField("ExtraDescription");
	}

	/**
	 * Returns whether the connection to the node can stay open.
	 *
	 * @return <code>true</code> when the connection has to be closed,
	 *         <code>false</code> otherwise
	 */
	public boolean isFatal() {
		return Boolean.valueOf(getField("Fatal"));
	}

	/**
	 * The identifier of the causing request, if any.
	 *
	 * @return The identifier of the causing request
	 */
	public String getIdentifier() {
		return getField("Identifier");
	}

} public class RemovePeer extends FcpMessage {

	@Deprecated
	public RemovePeer(String nodeIdentifier) {
		super("RemovePeer");
		setField("NodeIdentifier", nodeIdentifier);
	}

	public RemovePeer(String identifier, String nodeIdentifier) {
		this(nodeIdentifier);
		setField("Identifier", identifier);
	}

} public class SSKKeypair extends BaseMessage implements Identifiable {

	/**
	 * Creates a new “SSKKeypair” message that wraps the received message.
	 *
	 * @param receivedMessage
	 *            The received message
	 */
	public SSKKeypair(FcpMessage receivedMessage) {
		super(receivedMessage);
	}

	/**
	 * Returns the identifier of the request.
	 *
	 * @return The identifier of the request
	 */
	@Override
	public String getIdentifier() {
		return getField("Identifier");
	}

	/**
	 * Returns the URI that must be used to insert data.
	 *
	 * @return The insert URI
	 */
	public String getInsertURI() {
		return getField("InsertURI");
	}

	/**
	 * Returns the URI that must be used to request data.
	 *
	 * @return The request URI
	 */
	public String getRequestURI() {
		return getField("RequestURI");
	}

} public class SimpleProgress extends BaseMessage implements Identifiable {

	/**
	 * Creates a new “SimpleProgress” message that wraps the received message.
	 *
	 * @param receivedMessage
	 *            The received message
	 */
	public SimpleProgress(FcpMessage receivedMessage) {
		super(receivedMessage);
	}

	/**
	 * Returns the total number of blocks. This number may increase as long as
	 * {@link #isFinalizedTotal()} returns <code>false</code>.
	 *
	 * @return The total number of blocks
	 */
	public int getTotal() {
		return FcpUtils.safeParseInt(getField("Total"));
	}

	/**
	 * Returns the number of blocks that are required to completet the request.
	 * This number might actually be lower than {@link #getTotal} because of
	 * redundancy information. This number may also increase as long as
	 * {@link #isFinalizedTotal()} returns <code>false</code>.
	 *
	 * @return The number of required blocks
	 */
	public int getRequired() {
		return FcpUtils.safeParseInt(getField("Required"));
	}

	/**
	 * Returns the number of blocks that have failed and run out of retries.
	 *
	 * @return The number of failed blocks
	 */
	public int getFailed() {
		return FcpUtils.safeParseInt(getField("Failed"));
	}

	/**
	 * Returns the number of fatally failed blocks. A block that failed fatally
	 * can never be completed, even with infinite retries.
	 *
	 * @return The number of fatally failed blocks
	 */
	public int getFatallyFailed() {
		return FcpUtils.safeParseInt(getField("FatallyFailed"));
	}

	/**
	 * Returns the number of blocks that have been successfully processed.
	 *
	 * @return The number of succeeded blocks
	 */
	public int getSucceeded() {
		return FcpUtils.safeParseInt(getField("Succeeded"));
	}

	/**
	 * Returns whether the total number of blocks (see {@link #getTotal()} has
	 * been finalized. Once the total number of blocks has been finalized for a
	 * request it will not change any more, and this method of every further
	 * SimpleProgress message will always return <code>true</code>.
	 *
	 * @return <code>true</code> if the number of total blocks has been
	 *         finalized, <code>false</code> otherwise
	 */
	public boolean isFinalizedTotal() {
		return Boolean.valueOf(getField("FinalizedTotal"));
	}

	public long getLastProgress() {
		return Long.valueOf(getField("LastProgress"));
	}

	public int getMinSuccessFetchBlocks() {
		return Integer.valueOf(getField("MinSuccessFetchBlocks"));
	}

	/**
	 * Returns the identifier of the request.
	 *
	 * @return The identifier of the request
	 */
	@Override
	public String getIdentifier() {
		return getField("Identifier");
	}

} public class WatchGlobal extends FcpMessage {

	/**
	 * Enables or disables watching the global queue.
	 *
	 * @param enabled
	 *            <code>true</code> to watch the global queue and the
	 *            client-local queue, <code>false</code> to watch only the
	 *            client-local queue
	 */
	public WatchGlobal(boolean enabled) {
		this(enabled, Verbosity.ALL);
	}

	/**
	 * Enables or disables watching the global queue, optionally masking out
	 * certain events.
	 *
	 * @param enabled
	 *            <code>true</code> to watch the global queue and the
	 *            client-local queue, <code>false</code> to watch only the
	 *            client-local queue
	 * @param verbosityMask
	 *            A verbosity mask that determines which events are received
	 */
	public WatchGlobal(boolean enabled, Verbosity verbosityMask) {
		super("WatchGlobal");
		setField("Enabled", String.valueOf(enabled));
		setField("VerbosityMask", String.valueOf(verbosityMask));
	}

}
import static com.google.common.collect.FluentIterable.from;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import net.pterodactylus.fcp.AddPeer;
import net.pterodactylus.fcp.AllData;
import net.pterodactylus.fcp.ClientGet;
import net.pterodactylus.fcp.ClientHello;
import net.pterodactylus.fcp.CloseConnectionDuplicateClientName;
import net.pterodactylus.fcp.DataFound;
import net.pterodactylus.fcp.EndListPeerNotes;
import net.pterodactylus.fcp.EndListPeers;
import net.pterodactylus.fcp.EndListPersistentRequests;
import net.pterodactylus.fcp.FCPPluginMessage;
import net.pterodactylus.fcp.FCPPluginReply;
import net.pterodactylus.fcp.FcpAdapter;
import net.pterodactylus.fcp.FcpConnection;
import net.pterodactylus.fcp.FcpListener;
import net.pterodactylus.fcp.GenerateSSK;
import net.pterodactylus.fcp.GetFailed;
import net.pterodactylus.fcp.GetNode;
import net.pterodactylus.fcp.ListPeerNotes;
import net.pterodactylus.fcp.ListPeers;
import net.pterodactylus.fcp.ListPersistentRequests;
import net.pterodactylus.fcp.ModifyPeer;
import net.pterodactylus.fcp.ModifyPeerNote;
import net.pterodactylus.fcp.NodeData;
import net.pterodactylus.fcp.NodeHello;
import net.pterodactylus.fcp.NodeRef;
import net.pterodactylus.fcp.Peer;
import net.pterodactylus.fcp.PeerNote;
import net.pterodactylus.fcp.PeerRemoved;
import net.pterodactylus.fcp.PersistentGet;
import net.pterodactylus.fcp.PersistentPut;
import net.pterodactylus.fcp.ProtocolError;
import net.pterodactylus.fcp.RemovePeer;
import net.pterodactylus.fcp.SSKKeypair;
import net.pterodactylus.fcp.SimpleProgress;
import net.pterodactylus.fcp.WatchGlobal;
import com.google.common.base.Predicate;
			 * {@inheritDoc}
			 *
			 * @see net.pterodactylus.fcp.FcpAdapter#receivedDataFound(net.pterodactylus.fcp.FcpConnection,
			 *      net.pterodactylus.fcp.DataFound)
			 */
			@Override
			public void receivedDataFound(FcpConnection fcpConnection, DataFound dataFound) {
				Request getRequest = requests.get(dataFound.getIdentifier());
				if (getRequest == null) {
					return;
				}
				getRequest.setComplete(true);
				getRequest.setLength(dataFound.getDataLength());
				getRequest.setContentType(dataFound.getMetadataContentType());
			}

			/**
			 * {@inheritDoc}
			 *
			 * @see net.pterodactylus.fcp.FcpAdapter#receivedGetFailed(net.pterodactylus.fcp.FcpConnection,
			 *      net.pterodactylus.fcp.GetFailed)
			 */
			@Override
			public void receivedGetFailed(FcpConnection fcpConnection, GetFailed getFailed) {
				Request getRequest = requests.get(getFailed.getIdentifier());
				if (getRequest == null) {
					return;
				}
				getRequest.setComplete(true);
				getRequest.setFailed(true);
				getRequest.setFatal(getFailed.isFatal());
				getRequest.setErrorCode(getFailed.getCode());
			}

			/**
			 * {@inheritDoc}
			 *
			 * @see net.pterodactylus.fcp.FcpAdapter#receivedPersistentPut(net.pterodactylus.fcp.FcpConnection,
			 *      net.pterodactylus.fcp.PersistentPut)
			 */
			@Override
			public void receivedPersistentPut(FcpConnection fcpConnection, PersistentPut persistentPut) {
				if (!persistentPut.isGlobal() || global) {
					PutRequest putRequest = new PutRequest(persistentPut);
					requests.put(persistentPut.getIdentifier(), putRequest);
				}
			}

			/**
			 * {@inheritDoc}
			 *
			 * @see net.pterodactylus.fcp.FcpAdapter#receivedSimpleProgress(net.pterodactylus.fcp.FcpConnection,
			 *      net.pterodactylus.fcp.SimpleProgress)
			 */
			@Override
			public void receivedSimpleProgress(FcpConnection fcpConnection, SimpleProgress simpleProgress) {
				Request request = requests.get(simpleProgress.getIdentifier());
				if (request == null) {
					return;
				}
				request.setTotalBlocks(simpleProgress.getTotal());
				request.setRequiredBlocks(simpleProgress.getRequired());
				request.setFailedBlocks(simpleProgress.getFailed());
				request.setFatallyFailedBlocks(simpleProgress.getFatallyFailed());
				request.setSucceededBlocks(simpleProgress.getSucceeded());
				request.setFinalizedTotal(simpleProgress.isFinalizedTotal());
			}

			/**
			 * {@inheritDoc}
			 */
			@Override
			public void receivedEndListPersistentRequests(FcpConnection fcpConnection, EndListPersistentRequests endListPersistentRequests) {
				completionLatch.countDown();
			}
		}.execute();
		return requests.values();
	}

	/**
	 * Sends a message to a plugin and waits for the response.
	 *
	 * @param pluginClass
	 *            The name of the plugin class
	 * @param parameters
	 *            The parameters for the plugin
	 * @return The responses from the plugin
	 * @throws FcpException
	 *             if an FCP error occurs
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	public Map<String, String> sendPluginMessage(String pluginClass, Map<String, String> parameters) throws IOException, FcpException {
		return sendPluginMessage(pluginClass, parameters, 0, null);
	}

	/**
	 * Sends a message to a plugin and waits for the response.
	 *
	 * @param pluginClass
	 *            The name of the plugin class
	 * @param parameters
	 *            The parameters for the plugin
	 * @param dataLength
	 *            The length of the optional data stream, or {@code 0} if there
	 *            is no optional data stream
	 * @param dataInputStream
	 *            The input stream for the payload, or {@code null} if there is
	 *            no payload
	 * @return The responses from the plugin
	 * @throws FcpException
	 *             if an FCP error occurs
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	public Map<String, String> sendPluginMessage(final String pluginClass, final Map<String, String> parameters, final long dataLength, final InputStream dataInputStream) throws IOException, FcpException {
		final Map<String, String> pluginReplies = Collections.synchronizedMap(new HashMap<String, String>());
		new ExtendedFcpAdapter() {

			@SuppressWarnings("synthetic-access")
			private final String identifier = createIdentifier("FCPPluginMessage");