package be.nikiroo.jvcard.remote;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
/**
* A client or server connection, that will allow you to connect to, send and
* receive data to/from a jVCard remote server.
*
*
*
* @author niki
*/
public class SimpleSocket {
/**
* An {@link Appendable} that can be used to send data over a
* {@link SimpleSocket}. You must close it to send the end of block element.
*
* @author niki
*
*/
public class BlockAppendable implements Appendable, Closeable {
private SimpleSocket ss;
/**
* Create a new {@link BlockAppendable} for the given
* {@link SimpleSocket}.
*
* @param ss
* the {@link SimpleSocket}
*/
public BlockAppendable(SimpleSocket ss) {
this.ss = ss;
}
@Override
public Appendable append(CharSequence csq) throws IOException {
ss.send(csq);
return this;
}
@Override
public Appendable append(char c) throws IOException {
ss.send("" + c);
return this;
}
@Override
public Appendable append(CharSequence csq, int start, int end)
throws IOException {
ss.send(csq.subSequence(start, end));
return this;
}
@Override
public void close() throws IOException {
ss.sendBlock();
}
}
/**
* The current version of the network protocol.
*/
static public final int CURRENT_VERSION = 1;
/**
* The end of block marker.
*
* An end of block marker needs to be on a line on itself to be valid, and
* will denote the end of a block of data.
*/
static private String EOB = ".";
private Socket s;
private PrintWriter out;
private BufferedReader in;
private int version; // version of the OTHER end, not this one (this one is
// CURRENT_VERSION obviously)
private String label; // can be used for debugging
/**
* Create a new {@link SimpleSocket} with the given {@link Socket}.
*
* @param s
* the {@link Socket}
*/
public SimpleSocket(Socket s, String label) {
this.s = s;
this.label = label;
}
/**
* Return the label of this {@link SimpleSocket}. This is mainly used for
* debugging purposes or user display if any. It is optional.
*
* @return the label
*/
public String getLabel() {
if (label == null)
return "[no name]";
return label;
}
/**
* Open the {@link SimpleSocket} for reading/writing and negotiates the
* version.
*
* Note that you MUST call {@link SimpleSocket#close()} when you are
* done to release the acquired resources.
*
* @param client
* TRUE for clients, FALSE for servers (server speaks first)
*
* @throws IOException
* in case of IO error
*/
public void open(boolean client) throws IOException {
out = new PrintWriter(s.getOutputStream(), false);
in = new BufferedReader(new InputStreamReader(s.getInputStream()));
if (client) {
version = new CommandInstance(receiveLine(), -1).getVersion();
sendLine(new CommandInstance(Command.VERSION, CURRENT_VERSION)
.toString());
} else {
send(new CommandInstance(Command.VERSION, CURRENT_VERSION)
.toString());
// TODO: i18n
send("[Some help info here]");
send("you need to reply with your VERSION + end of block");
send("please send HELP in a full block or help");
sendBlock();
version = new CommandInstance(receiveLine(), -1).getVersion();
}
}
/**
* Close the connection and release acquired resources.
*
* @return TRUE if everything was closed properly, FALSE if the connection
* was broken (in all cases, resources are released)
*/
public boolean close() {
boolean broken = false;
try {
sendBlock();
broken = out.checkError();
} catch (IOException e) {
broken = true;
}
try {
s.close();
} catch (IOException e) {
e.printStackTrace();
broken = true;
try {
in.close();
} catch (IOException ee) {
}
out.close();
}
s = null;
in = null;
out = null;
return !broken;
}
/**
* Sends lines to the remote server. Do NOT sends the end-of-block
* marker.
*
* @param data
* the data to send
*
* @throws IOException
* in case of IO error
*/
protected void send(CharSequence data) throws IOException {
if (data != null) {
out.append(data);
}
out.append("\n");
if (out.checkError())
throw new IOException();
}
/**
* Sends an end-of-block marker to the remote server.
*
* @throws IOException
* in case of IO error
*/
public void sendBlock() throws IOException {
sendBlock((List) null);
}
/**
* Sends commands to the remote server, then sends an end-of-block marker.
*
* @param data
* the data to send
*
* @throws IOException
* in case of IO error
*/
public void sendLine(String data) throws IOException {
sendBlock(Arrays.asList(new String[] { data }));
}
/**
* Sends commands to the remote server, then sends an end-of-block marker.
*
* @param data
* the data to send
*
* @throws IOException
* in case of IO error
*/
public void sendBlock(List data) throws IOException {
if (data != null) {
for (String dataLine : data) {
send(dataLine);
}
}
send(EOB);
}
/**
* Sends commands to the remote server, then sends an end-of-block marker.
*
* @param command
* the {@link Command} to send
*
* @throws IOException
* in case of IO error
*/
public void sendCommand(Command command) throws IOException {
sendCommand(command, null);
}
/**
* Sends commands to the remote server, then sends an end-of-block marker.
*
* @param command
* the data to send
*
* @param param
* the parameter for this command if any
*
* @throws IOException
* in case of IO error
*/
public void sendCommand(Command command, String param) throws IOException {
sendLine(new CommandInstance(command, param, CURRENT_VERSION)
.toString());
}
/**
* Create a new {@link Appendable} that can be used to send data on this
* {@link SimpleSocket}. When you are done, just call
* {@link BlockAppendable#close()}.
*
* @return the {@link Appendable}
*/
public BlockAppendable createBlockAppendable() {
return new BlockAppendable(this);
}
/**
* Read a line from the remote server.
*
* Do NOT read until the end-of-block marker, and can return said
* block without conversion.
*
* @return the read line
*
* @throws IOException
* in case of IO error
*/
protected String receive() throws IOException {
String line = in.readLine();
return line;
}
/**
* Read lines from the remote server until the end-of-block ("\0\n") marker
* is detected.
*
* @return the read lines without the end marker, or NULL if nothing more to
* read
*
* @throws IOException
* in case of IO error
*/
public List receiveBlock() throws IOException {
List result = new LinkedList();
String line = receive();
for (; line != null && !line.equals(EOB); line = receive()) {
result.add(line);
}
if (line == null)
return null;
return result;
}
/**
* Read a line from the remote server then read until the next end-of-block
* marker.
*
* @return the parsed line, or NULL if nothing more to read
*
* @throws IOException
* in case of IO error
*/
public String receiveLine() throws IOException {
List lines = receiveBlock();
if (lines.size() > 0)
return lines.get(0);
return null;
}
/**
* Read a line from the remote server and convert it to a
* {@link CommandInstance}, then read until the next end-of-block marker.
*
* @return the parsed {@link CommandInstance}
*
* @throws IOException
* in case of IO error
*/
public CommandInstance receiveCommand() throws IOException {
String line = receive();
CommandInstance cmd = new CommandInstance(line, version);
receiveBlock();
return cmd;
}
@Override
public String toString() {
String source = "[not connected]";
InetAddress iadr = s.getInetAddress();
if (iadr != null)
source = iadr.getHostName();
return getLabel() + " (" + source + ")";
}
}