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.security.InvalidParameterException;
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.Contact;
import be.nikiroo.jvcard.Data;
import be.nikiroo.jvcard.parsers.Format;
import be.nikiroo.jvcard.parsers.Vcard21Parser;
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 updateLock = 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() {
SimpleSocket ss = new SimpleSocket(s, "[request]");
addClient(ss);
try {
ss.open(false);
while (processCmd(ss))
;
} catch (IOException e) {
e.printStackTrace();
} finally {
ss.close();
}
removeClient(ss);
}
}).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);
}
}
/**
* Process a command.
*
* @param s
* the {@link SimpleSocket} from which to get the command to
* process
*
* @return TRUE if the client is ready for another command, FALSE when the
* client exited
*
* @throws IOException
* in case of IO error
*/
private boolean processCmd(SimpleSocket s) throws IOException {
Command cmd = s.receiveCommand();
Command.Verb verb = cmd.getVerb();
if (verb == null)
return false;
boolean clientContinue = true;
System.out.println(s + " -> " + verb);
switch (verb) {
case STOP:
clientContinue = false;
break;
case VERSION:
s.sendCommand(Verb.VERSION);
break;
case TIME:
s.sendLine(StringUtils.fromTime(new Date().getTime()));
break;
case GET_CARD:
synchronized (updateLock) {
s.sendBlock(doGetCard(cmd.getParam()));
}
break;
case POST_CARD:
synchronized (updateLock) {
s.sendLine(doPostCard(cmd.getParam(), s.receiveBlock()));
}
break;
case PUT_CARD:
synchronized (updateLock) {
File vcf = getFile(cmd.getParam());
if (vcf == null) {
System.err
.println("Fail to update a card, file not available: "
+ cmd.getParam());
clientContinue = false;
} else {
Card card = new Card(vcf, Format.VCard21);
try {
while (processContactCmd(s, card))
;
card.save();
} catch (InvalidParameterException e) {
System.err
.println("Unsupported command received from a client connection, closing it: "
+ verb + " (" + e.getMessage() + ")");
clientContinue = false;
}
}
}
break;
case DELETE_CARD:
// TODO
System.err
.println("Unsupported command received from a client connection, closing it: "
+ verb);
clientContinue = false;
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);
clientContinue = false;
break;
}
return clientContinue;
}
/**
* Process a *_CONTACT subcommand.
*
* @param s
* the {@link SimpleSocket} to process
* @param card
* the target {@link Card}
*
* @return TRUE if the client is ready for another command, FALSE when the
* client is done
*
* @throws IOException
* in case of IO error
*
* @throw InvalidParameterException in case of invalid subcommand
*/
private boolean processContactCmd(SimpleSocket s, Card card)
throws IOException {
Command cmd = s.receiveCommand();
Command.Verb verb = cmd.getVerb();
if (verb == null)
return false;
boolean clientContinue = true;
System.out.println(s + " -> " + verb);
switch (verb) {
case GET_CONTACT: {
Contact contact = card.getById(cmd.getParam());
if (contact != null)
s.sendBlock(Vcard21Parser.toStrings(contact, -1));
else
s.sendBlock();
break;
}
case POST_CONTACT: {
String uid = cmd.getParam();
Contact contact = card.getById(uid);
if (contact != null)
contact.delete();
List list = Vcard21Parser.parseContact(s.receiveBlock());
if (list.size() > 0) {
contact = list.get(0);
contact.getPreferredData("UID").setValue(uid);
card.add(contact);
}
break;
}
case PUT_CONTACT: {
String uid = cmd.getParam();
Contact contact = card.getById(uid);
if (contact == null) {
throw new InvalidParameterException(
"Cannot find contact to modify for UID: " + uid);
}
while (processDataCmd(s, contact))
;
break;
}
case DELETE_CONTACT: {
String uid = cmd.getParam();
Contact contact = card.getById(uid);
if (contact == null) {
throw new InvalidParameterException(
"Cannot find contact to delete for UID: " + uid);
}
contact.delete();
break;
}
case PUT_CARD: {
clientContinue = false;
break;
}
default: {
throw new InvalidParameterException("command invalid here");
}
}
return clientContinue;
}
/**
* Process a *_DATA subcommand.
*
* @param s
* the {@link SimpleSocket} to process
* @param card
* the target {@link Contact}
*
* @return TRUE if the client is ready for another command, FALSE when the
* client is done
*
* @throws IOException
* in case of IO error
*
* @throw InvalidParameterException in case of invalid subcommand
*/
private boolean processDataCmd(SimpleSocket s, Contact contact)
throws IOException {
Command cmd = s.receiveCommand();
Command.Verb verb = cmd.getVerb();
if (verb == null)
return false;
boolean clientContinue = true;
System.out.println(s + " -> " + verb);
switch (verb) {
case GET_DATA: {
Data data = contact.getById(cmd.getParam());
if (data != null)
s.sendBlock(Vcard21Parser.toStrings(data));
else
s.sendBlock();
break;
}
case POST_DATA: {
String cstate = cmd.getParam();
Data data = null;
for (Data d : contact) {
if (cstate.equals(d.getContentState()))
data = d;
}
if (data != null)
data.delete();
List list = Vcard21Parser.parseData(s.receiveBlock());
if (list.size() > 0) {
contact.add(list.get(0));
}
break;
}
case DELETE_DATA: {
String cstate = cmd.getParam();
Data data = null;
for (Data d : contact) {
if (cstate.equals(d.getContentState()))
data = d;
}
if (data == null) {
throw new InvalidParameterException(
"Cannot find data to delete for content state: "
+ cstate);
}
contact.delete();
break;
}
case PUT_CONTACT: {
clientContinue = false;
break;
}
default: {
throw new InvalidParameterException("command invalid here");
}
}
return clientContinue;
}
/**
* 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();
File vcf = getFile(name);
if (vcf != null && vcf.exists()) {
Card card = new Card(vcf, Format.VCard21);
// timestamp:
lines.add(StringUtils.fromTime(card.getLastModified()));
lines.addAll(Vcard21Parser.toStrings(card));
}
return lines;
}
/**
* Save the data to the new given resource.
*
* @param name
* the resource name to save
* @param data
* the data to save
*
* @return the date of last modification
*
* @throws IOException
* in case of error
*/
private String doPostCard(String name, List data)
throws IOException {
File vcf = getFile(name);
if (vcf != null) {
Card card = new Card(Vcard21Parser.parseContact(data));
card.saveAs(vcf, Format.VCard21);
return StringUtils.fromTime(vcf.lastModified());
}
return "";
}
/**
* Return the {@link File} corresponding to the given resource name.
*
* @param name
* the resource name
*
* @return the corresponding {@link File} or NULL if the name was NULL or
* empty
*/
private File getFile(String name) {
if (name != null && name.length() > 0) {
return new File(dataDir.getAbsolutePath() + File.separator + name);
}
return null;
}
}