package org.basex.server;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.basex.core.BaseXException;
import org.basex.core.Command;
import org.basex.core.Commands.Cmd;
import org.basex.core.Context;
import org.basex.core.MainProp;
import org.basex.io.in.BufferInput;
import org.basex.io.in.DecodingInput;
import org.basex.io.out.EncodingOutput;
import org.basex.io.out.PrintOutput;
import org.basex.util.Token;
/**
* This class offers methods to execute database commands via the
* client/server architecture. Commands are sent to the server instance over
* a socket connection:
* <ul>
* <li> A socket instance is created by the constructor.</li>
* <li> The {@link #execute} method sends database commands to the server.
* All strings are encoded as UTF8 and suffixed by a zero byte.</li>
* <li> If the command has been successfully executed,
* the result string is read.</li>
* <li> Next, the command info string is read.</li>
* <li> A last byte is next sent to indicate if command execution
* was successful (0) or not (1).</li>
* <li> {@link #close} closes the session by sending the {@link Cmd#EXIT}
* command to the server.</li>
* </ul>
*
* @author BaseX Team 2005-12, BSD License
* @author Christian Gruen
*/
public class ClientSession extends Session {
/** Event notifications. */
final Map<String, EventNotifier> notifiers =
Collections.synchronizedMap(new HashMap<String, EventNotifier>());
/** Server output (buffered). */
final PrintOutput sout;
/** Server input. */
final InputStream sin;
/** Socket reference. */
private final Socket socket;
/** Socket host name. */
private final String ehost;
/** Socket event reference. */
private Socket esocket;
/**
* Constructor, specifying login data.
* @param context database context
* @param user user name
* @param pass password
* @throws IOException I/O exception
*/
public ClientSession(final Context context, final String user,
final String pass) throws IOException {
this(context, user, pass, null);
}
/**
* Constructor, specifying login data and an output stream.
* @param context database context
* @param user user name
* @param pass password
* @param output client output; if set to {@code null}, results will
* be returned as strings.
* @throws IOException I/O exception
*/
public ClientSession(final Context context, final String user,
final String pass, final OutputStream output) throws IOException {
this(context.mprop.get(MainProp.HOST), context.mprop.num(MainProp.PORT),
user, pass, output);
}
/**
* Constructor, specifying the server host:port combination and login data.
* @param host server name
* @param port server port
* @param user user name
* @param pass password
* @throws IOException I/O exception
*/
public ClientSession(final String host, final int port,
final String user, final String pass) throws IOException {
this(host, port, user, pass, null);
}
/**
* Constructor, specifying the server host:port combination, login data and
* an output stream.
* @param host server name
* @param port server port
* @param user user name
* @param pass password
* @param output client output; if set to {@code null}, results will
* be returned as strings.
* @throws IOException I/O exception
*/
public ClientSession(final String host, final int port, final String user,
final String pass, final OutputStream output) throws IOException {
super(output);
ehost = host;
// 5 seconds timeout
socket = new Socket();
socket.connect(new InetSocketAddress(host, port), 5000);
sin = socket.getInputStream();
// receive timestamp
final BufferInput bi = new BufferInput(sin);
final String ts = bi.readString();
// send user name and hashed password/timestamp
sout = PrintOutput.get(socket.getOutputStream());
send(user);
send(Token.md5(Token.md5(pass) + ts));
sout.flush();
// receive success flag
if(!ok(bi)) throw new LoginException();
}
@Override
public void create(final String name, final InputStream input)
throws IOException {
send(ServerCmd.CREATE, input, name);
}
@Override
public void add(final String path, final InputStream input)
throws IOException {
send(ServerCmd.ADD, input, path);
}
@Override
public void replace(final String path, final InputStream input)
throws IOException {
send(ServerCmd.REPLACE, input, path);
}
@Override
public void store(final String path, final InputStream input)
throws IOException {
send(ServerCmd.STORE, input, path);
}
@Override
public ClientQuery query(final String query) throws IOException {
return new ClientQuery(query, this, out);
}
@Override
public synchronized void close() throws IOException {
if(esocket != null) esocket.close();
socket.close();
}
@Override
protected void execute(final String cmd, final OutputStream os)
throws IOException {
send(cmd);
sout.flush();
receive(os);
}
@Override
protected void execute(final Command cmd, final OutputStream os)
throws IOException {
execute(cmd.toString(), os);
}
/**
* Watches an event.
* @param name event name
* @param notifier event notification
* @throws IOException I/O exception
*/
public void watch(final String name, final EventNotifier notifier)
throws IOException {
sout.write(ServerCmd.WATCH.code);
if(esocket == null) {
sout.flush();
final BufferInput bi = new BufferInput(sin);
final int eport = Integer.parseInt(bi.readString());
// initialize event socket
esocket = new Socket();
esocket.connect(new InetSocketAddress(ehost, eport), 5000);
final OutputStream so = esocket.getOutputStream();
so.write(bi.readBytes());
so.write(0);
so.flush();
final InputStream is = esocket.getInputStream();
is.read();
listen(is);
}
send(name);
sout.flush();
receive(null);
notifiers.put(name, notifier);
}
/**
* Unwatches an event.
* @param name event name
* @throws IOException I/O exception
*/
public void unwatch(final String name) throws IOException {
sout.write(ServerCmd.UNWATCH.code);
send(name);
sout.flush();
receive(null);
notifiers.remove(name);
}
/**
* Starts the listener thread.
* @param in input stream
*/
private void listen(final InputStream in) {
new Thread() {
@Override
public void run() {
try {
while(true) {
final BufferInput bi = new BufferInput(in);
final EventNotifier n = notifiers.get(bi.readString());
final String l = bi.readString();
if(n != null) n.notify(l);
}
} catch(final IOException ex) {
// listener did not receive any more input
}
}
}.start();
}
/**
* Sends the specified stream to the server.
* @param input input stream
* @throws IOException I/O exception
*/
private void send(final InputStream input) throws IOException {
final EncodingOutput eo = new EncodingOutput(sout);
for(int b; (b = input.read()) != -1;) eo.write(b);
sout.write(0);
sout.flush();
receive(null);
}
/**
* Receives the info string.
* @param os output stream to send result to. If {@code null}, no result
* will be requested
* @throws IOException I/O exception
*/
private void receive(final OutputStream os) throws IOException {
final BufferInput bi = new BufferInput(sin);
if(os != null) receive(bi, os);
info = bi.readString();
if(!ok(bi)) throw new BaseXException(info);
}
/**
* Checks the next success flag.
* @param bi buffer input
* @return value of check
* @throws IOException I/O exception
*/
static boolean ok(final BufferInput bi) throws IOException {
return bi.read() == 0;
}
/**
* Sends the specified command, string arguments and input.
* @param cmd command
* @param input input stream
* @param strings string arguments
* @throws IOException I/O exception
*/
void send(final ServerCmd cmd, final InputStream input,
final String... strings) throws IOException {
sout.write(cmd.code);
for(final String s : strings) send(s);
send(input);
}
/**
* Retrieves data from the server.
* @param bi buffered server input
* @param os output stream
* @throws IOException I/O exception
*/
static void receive(final BufferInput bi, final OutputStream os)
throws IOException {
final DecodingInput di = new DecodingInput(bi);
for(int b; (b = di.read()) != -1;) os.write(b);
}
/**
* Sends a string to the server.
* @param s string to be sent
* @throws IOException I/O exception
*/
void send(final String s) throws IOException {
sout.write(Token.token(s));
sout.write(0);
}
}