package org.basex.server;
import static org.basex.core.Text.*;
import static org.basex.util.Token.*;
import java.io.IOException;
import java.net.Socket;
import java.util.HashMap;
import org.basex.BaseXServer;
import org.basex.core.BaseXException;
import org.basex.core.Command;
import org.basex.core.CommandParser;
import org.basex.core.Context;
import org.basex.core.MainProp;
import org.basex.core.cmd.Add;
import org.basex.core.cmd.Close;
import org.basex.core.cmd.CreateDB;
import org.basex.core.cmd.Exit;
import org.basex.core.cmd.Replace;
import org.basex.core.cmd.Store;
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.query.QueryException;
import org.basex.util.Performance;
import org.basex.util.Util;
import org.basex.util.list.ByteList;
/**
* Server-side client session in the client-server architecture.
*
* @author BaseX Team 2005-12, BSD License
* @author Andreas Weiler
* @author Christian Gruen
*/
public final class ClientListener extends Thread {
/** Active queries. */
private final HashMap<String, QueryListener> queries =
new HashMap<String, QueryListener>();
/** Performance measurement. */
private final Performance perf = new Performance();
/** Database context. */
private final Context context;
/** Socket reference. */
private final Socket socket;
/** Server reference. */
private final BaseXServer server;
/** Log reference. */
private final Log log;
/** Socket for events. */
private Socket esocket;
/** Output for events. */
private PrintOutput eout;
/** Flag for active events. */
private boolean events;
/** Input stream. */
private BufferInput in;
/** Output stream. */
private PrintOutput out;
/** Current command. */
private Command command;
/** Query id counter. */
private int id;
/** Indicates if the server thread is running. */
private boolean running;
/** Timestamp of last interaction. */
public long last;
/**
* Constructor.
* @param s socket
* @param c database context
* @param l log reference
* @param srv server reference
*/
public ClientListener(final Socket s, final Context c, final Log l,
final BaseXServer srv) {
context = new Context(c, this);
socket = s;
log = l;
server = srv;
last = System.currentTimeMillis();
}
@Override
public void run() {
// initialize the session via cram-md5 authentication
try {
final String ts = Long.toString(System.nanoTime());
final byte[] address = socket.getInetAddress().getAddress();
// send {TIMESTAMP}0
out = PrintOutput.get(socket.getOutputStream());
out.print(ts);
send(true);
// evaluate login data
in = new BufferInput(socket.getInputStream());
// receive {USER}0{PASSWORD}0
final String us = in.readString();
final String pw = in.readString();
context.user = context.users.get(us);
running = context.user != null &&
md5(string(context.user.password) + ts).equals(pw);
// write log information
if(running) {
log.write(this, "LOGIN " + context.user.name, OK);
// send {OK}
send(true);
server.unblock(address);
context.add(this);
} else {
if(!us.isEmpty()) log.write(this, ACCESS_DENIED + COLS + us);
new ClientDelayer(server.block(address), this, server).start();
}
} catch(final IOException ex) {
Util.stack(ex);
log.write(ex.getMessage());
return;
}
if(!running) return;
// authentification done, start command loop
ServerCmd sc = null;
String cmd = null;
try {
while(running) {
command = null;
try {
final int b = in.read();
if(b == -1) {
// end of stream: exit session
quit();
break;
}
last = System.currentTimeMillis();
perf.getTime();
sc = ServerCmd.get(b);
cmd = null;
if(sc == ServerCmd.CREATE) {
create();
} else if(sc == ServerCmd.ADD) {
add();
} else if(sc == ServerCmd.WATCH) {
watch();
} else if(sc == ServerCmd.UNWATCH) {
unwatch();
} else if(sc == ServerCmd.REPLACE) {
replace();
} else if(sc == ServerCmd.STORE) {
store();
} else if(sc != ServerCmd.COMMAND) {
query(sc);
} else {
// database command
cmd = new ByteList().add(b).add(in.readBytes()).toString();
}
} catch(final IOException ex) {
// this exception may be thrown if a session is stopped
quit();
break;
}
if(sc != ServerCmd.COMMAND) continue;
// parse input and create command instance
try {
command = new CommandParser(cmd, context).parseSingle();
} catch(final QueryException ex) {
// log invalid command
final String msg = ex.getMessage();
log.write(this, cmd, ERROR_C + msg);
// send 0 to mark end of potential result
out.write(0);
// send {INFO}0
out.writeString(msg);
// send 1 to mark error
send(false);
continue;
}
// start timeout
command.startTimeout(context.mprop.num(MainProp.TIMEOUT));
log.write(this,
command.toString().replace('\r', ' ').replace('\n', ' '));
// execute command and send {RESULT}
boolean ok = true;
String info;
try {
command.execute(context, new EncodingOutput(out));
info = command.info();
} catch(final BaseXException ex) {
ok = false;
info = ex.getMessage();
if(info.startsWith(INTERRUPTED)) info = TIMEOUT_EXCEEDED;
}
// stop timeout
command.stopTimeout();
// send 0 to mark end of result
out.write(0);
// send info
info(info, ok);
// stop console
if(command instanceof Exit) {
command = null;
quit();
}
}
} catch(final IOException ex) {
log.write(this, sc == ServerCmd.COMMAND ? cmd : sc,
ERROR_C + ex.getMessage());
Util.debug(ex);
command = null;
quit();
}
command = null;
}
/**
* Exits the session.
*/
public synchronized void quit() {
running = false;
if(log != null) log.write(this, "LOGOUT " + context.user.name, OK);
// wait until running command was stopped
if(command != null) {
command.stop();
while(command != null) Performance.sleep(50);
}
context.delete(this);
try {
new Close().execute(context);
socket.close();
if(events) {
esocket.close();
// remove this session from all events in pool
for(final Sessions s : context.events.values()) s.remove(this);
}
} catch(final Exception ex) {
if(log != null) log.write(ex.getMessage());
Util.stack(ex);
}
}
/**
* Returns the context of this session.
* @return user reference
*/
public Context context() {
return context;
}
/**
* Registers the event socket.
* @param s socket
* @throws IOException I/O exception
*/
public synchronized void register(final Socket s) throws IOException {
esocket = s;
eout = PrintOutput.get(s.getOutputStream());
eout.write(0);
eout.flush();
}
/**
* Sends a notification to the client.
* @param name event name
* @param msg event message
* @throws IOException I/O exception
*/
public synchronized void notify(final byte[] name, final byte[] msg)
throws IOException {
last = System.currentTimeMillis();
eout.print(name);
eout.write(0);
eout.print(msg);
eout.write(0);
eout.flush();
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("[");
sb.append(socket.getInetAddress().getHostAddress());
sb.append(COL).append(socket.getPort()).append(']');
if(context.data() != null) sb.append(COLS).append(context.data().meta.name);
return sb.toString();
}
// PRIVATE METHODS ==========================================================
/**
* Returns error feedback.
* @param info error string
* @throws IOException I/O exception
*/
private void error(final String info) throws IOException {
info(info, false);
}
/**
* Returns user feedback.
* @param info information string
* @throws IOException I/O exception
*/
private void success(final String info) throws IOException {
info(info, true);
}
/**
* Returns user feedback.
* @param info information string
* @param ok success/error flag
* @throws IOException I/O exception
*/
private void info(final String info, final boolean ok) throws IOException {
// write feedback to log file
log.write(this, ok ? OK : ERROR_C + info, perf);
// send {MSG}0 and (0|1) as (success|error) flag
out.writeString(info);
send(ok);
}
/**
* Creates a database.
* @throws IOException I/O exception
*/
private void create() throws IOException {
execute(new CreateDB(in.readString()));
}
/**
* Adds a document to a database.
* @throws IOException I/O exception
*/
private void add() throws IOException {
execute(new Add(in.readString()));
}
/**
* Replace a document in a database.
* @throws IOException I/O exception
*/
private void replace() throws IOException {
execute(new Replace(in.readString()));
}
/**
* Stores raw data in a database.
* @throws IOException I/O exception
*/
private void store() throws IOException {
execute(new Store(in.readString()));
}
/**
* Executes the specified command.
* @param cmd command to be executed
* @throws IOException I/O exception
*/
private void execute(final Command cmd) throws IOException {
log.write(this, cmd + " [...]");
final DecodingInput di = new DecodingInput(in);
try {
cmd.setInput(di);
cmd.execute(context);
success(cmd.info());
} catch(final BaseXException ex) {
di.flush();
error(ex.getMessage());
}
}
/**
* Watches an event.
* @throws IOException I/O exception
*/
private void watch() throws IOException {
server.initEvents();
// initialize server-based event handling
if(!events) {
out.writeString(Integer.toString(context.mprop.num(MainProp.EVENTPORT)));
out.writeString(Long.toString(getId()));
out.flush();
events = true;
}
final String name = in.readString();
final Sessions s = context.events.get(name);
final boolean ok = s != null && !s.contains(this);
final String message;
if(ok) {
s.add(this);
message = WATCHING_EVENT_X;
} else if(s == null) {
message = EVENT_UNKNOWN_X;
} else {
message = EVENT_WATCHED_X;
}
info(Util.info(message, name), ok);
}
/**
* Unwatches an event.
* @throws IOException I/O exception
*/
private void unwatch() throws IOException {
final String name = in.readString();
final Sessions s = context.events.get(name);
final boolean ok = s != null && s.contains(this);
final String message;
if(ok) {
s.remove(this);
message = UNWATCHING_EVENT_X;
} else if(s == null) {
message = EVENT_UNKNOWN_X;
} else {
message = EVENT_NOT_WATCHED_X;
}
info(Util.info(message, name), ok);
out.flush();
}
/**
* Processes the query iterator.
* @param sc server command
* @throws IOException I/O exception
*/
private void query(final ServerCmd sc) throws IOException {
// iterator argument (query or identifier)
String arg = in.readString();
String err = null;
try {
final QueryListener qp;
if(sc == ServerCmd.QUERY) {
final String query = arg;
qp = new QueryListener(query, context);
arg = Integer.toString(id++);
queries.put(arg, qp);
// send {ID}0
out.writeString(arg);
// write log file
log.write(this, sc + "(" + arg + ')', query, OK, perf);
} else {
// find query process
qp = queries.get(arg);
// ID has already been removed
if(qp == null) {
if(sc != ServerCmd.CLOSE)
throw new IOException("Unknown Query ID: " + arg);
} else if(sc == ServerCmd.BIND) {
final String key = in.readString();
final String val = in.readString();
final String typ = in.readString();
qp.bind(key, val, typ);
log.write(this, sc + "(" + arg + ')', key, val, typ, OK, perf);
} else if(sc == ServerCmd.ITER) {
qp.execute(true, out, true);
} else if(sc == ServerCmd.EXEC) {
qp.execute(false, out, true);
} else if(sc == ServerCmd.INFO) {
out.print(qp.info());
} else if(sc == ServerCmd.OPTIONS) {
out.print(qp.options());
} else if(sc == ServerCmd.CLOSE) {
queries.remove(arg);
} else if(sc == ServerCmd.NEXT) {
throw new Exception("Protocol for query iteration is out-of-dated.");
}
// send 0 as end marker
out.write(0);
}
// send 0 as success flag
out.write(0);
// write log file (bind and execute have been logged before)
if(sc != ServerCmd.BIND) log.write(this, sc + "(" + arg + ')', OK, perf);
} catch(final Exception ex) {
// log exception (static or runtime)
err = ex.getMessage();
log.write(this, sc + "(" + arg + ')', ERROR_C + err);
queries.remove(arg);
}
if(err != null) {
// send 0 as end marker, 1 as error flag, and {MSG}0
out.write(0);
out.write(1);
out.writeString(err);
}
out.flush();
}
/**
* Sends a success flag to the client (0: true, 1: false).
* @param ok success flag
* @throws IOException I/O exception
*/
void send(final boolean ok) throws IOException {
out.write(ok ? 0 : 1);
out.flush();
}
}