package be.nikiroo.jvcard.remote;
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.ResourceBundle;
import be.nikiroo.jvcard.Card;
import be.nikiroo.jvcard.parsers.Format;
import be.nikiroo.jvcard.parsers.Parser;
import be.nikiroo.jvcard.remote.Command.Verb;
import be.nikiroo.jvcard.resources.Bundles;
import be.nikiroo.jvcard.tui.StringUtils;
/**
* This class implements a small server that can listen for requests to
* synchronise, get and put {@link Card}s.
*
*
* It is NOT secured in any way (it even is nice enough to give you a
* help message when you connect in raw mode via nc on how to use it), so do
* NOT enable such a server to be accessible from internet. This is not
* safe. Use a ssh/openssl tunnel or similar.
*
*
* @author niki
*
*/
public class Server implements Runnable {
private ServerSocket ss;
private int port;
private boolean stop;
private File dataDir;
private Object clientsLock = new Object();
private List clients = new LinkedList();
private Object cardsLock = new Object();
public static void main(String[] args) throws IOException {
Server server = new Server(4444);
server.run();
}
/**
* Create a new jVCard sercer on the given port.
*
* @param port
* the port to run on
*
* @throws IOException
* in case of IO error
*/
public Server(int port) throws IOException {
this.port = port;
ResourceBundle bundle = Bundles.getBundle("remote");
try {
String dir = bundle.getString("SERVER_DATA_PATH");
dataDir = new File(dir);
dataDir.mkdir();
if (!dataDir.exists()) {
throw new IOException("Cannot open or create data store at: "
+ dataDir);
}
} catch (Exception e) {
e.printStackTrace();
throw new IOException("Cannot open or create data store at: "
+ dataDir, e);
}
ss = new ServerSocket(port);
}
/**
* Stop the server. It may take some time before returning, but will only
* return when the server is actually stopped.
*/
public void stop() {
stop = true;
try {
SimpleSocket c = new SimpleSocket(new Socket((String) null, port),
"special STOP client");
c.open(true);
c.sendCommand(Verb.STOP);
c.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
if (clients.size() > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
if (clients.size() > 0) {
synchronized (clientsLock) {
for (SimpleSocket s : clients) {
System.err
.println("Forcefully closing client connection");
s.close();
}
clients.clear();
}
}
}
}
@Override
public void run() {
while (!stop) {
try {
final Socket s = ss.accept();
// TODO: thread pool?
new Thread(new Runnable() {
@Override
public void run() {
accept(new SimpleSocket(s, "[request]"));
}
}).start();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
/**
* Add a client to the current count.
*
* @return the client index number
*/
private void addClient(SimpleSocket s) {
synchronized (clientsLock) {
clients.add(s);
}
}
/**
* Remove a client from the current count.
*
* @param index
* the client index number
*/
private void removeClient(SimpleSocket s) {
synchronized (clientsLock) {
clients.remove(s);
}
}
/**
* Accept a client and process it.
*
* @param s
* the client to process
*/
private void accept(SimpleSocket s) {
addClient(s);
try {
s.open(false);
boolean clientStop = false;
while (!clientStop) {
Command cmd = s.receiveCommand();
Command.Verb verb = cmd.getVerb();
if (verb == null)
break;
System.out.println(s + " -> " + verb);
switch (verb) {
case STOP:
clientStop = true;
break;
case VERSION:
s.sendCommand(Verb.VERSION);
break;
case TIME:
s.sendLine(StringUtils.fromTime(new Date().getTime()));
break;
case GET:
synchronized (cardsLock) {
s.sendBlock(doGetCard(cmd.getParam()));
}
break;
case POST:
synchronized (cardsLock) {
doPostCard(cmd.getParam(), s.receiveBlock());
s.sendBlock();
break;
}
case LIST:
for (File file : dataDir.listFiles()) {
if (cmd.getParam() == null
|| cmd.getParam().length() == 0
|| file.getName().contains(cmd.getParam())) {
s.send(StringUtils.fromTime(file.lastModified())
+ " " + file.getName());
}
}
s.sendBlock();
break;
case HELP:
// TODO: i18n
s.send("The following commands are available:");
s.send("- TIME: get the server time");
s.send("- HELP: this help screen");
s.send("- LIST: list the available cards on this server");
s.send("- VERSION/GET/PUT/POST/DELETE/STOP: TODO");
s.sendBlock();
break;
default:
System.err
.println("Unsupported command received from a client connection, closing it: "
+ verb);
clientStop = true;
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
s.close();
}
removeClient(s);
}
/**
* Return the serialised {@link Card} (with timestamp).
*
* @param name
* the resource name to load
*
* @return the serialised data
*
* @throws IOException
* in case of error
*/
private List doGetCard(String name) throws IOException {
List lines = new LinkedList();
if (name != null && name.length() > 0) {
File vcf = new File(dataDir.getAbsolutePath() + File.separator
+ name);
if (vcf.exists()) {
Card card = new Card(vcf, Format.VCard21);
// timestamp:
lines.add(StringUtils.fromTime(card.getLastModified()));
// TODO: !!! fix this !!!
for (String line : card.toString(Format.VCard21).split("\r\n")) {
lines.add(line);
}
}
}
return lines;
}
/**
* Save the data to the new given resource.
*
* @param name
* the resource name to save
* @param data
* the data to save
*
* @throws IOException
* in case of error
*/
private void doPostCard(String name, List data) throws IOException {
if (name != null && name.length() > 0) {
File vcf = new File(dataDir.getAbsolutePath() + File.separator
+ name);
Card card = new Card(Parser.parse(data, Format.VCard21));
card.saveAs(vcf, Format.VCard21);
}
}
}