/**
* ConnectionWorker.java
*
* Copyright 2012 Niolex, Inc.
*
* Niolex licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.apache.niolex.commons.remote;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.net.Socket;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.niolex.commons.codec.StringUtil;
import org.apache.niolex.commons.reflect.FieldUtil;
import org.apache.niolex.commons.remote.Path.Type;
import org.apache.niolex.commons.util.SystemUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class wraps the connections, process client commands and return results.
*
* @author <a href="mailto:xiejiyun@gmail.com">Xie, Jiyun</a>
* @version 1.0.0
* @since 2012-7-25
*/
public class ConnectionWorker implements Runnable {
private static final Logger LOG = LoggerFactory.getLogger(ConnectionWorker.class);
private static final Map<String, Executer> COMMAND_MAP = new HashMap<String, Executer>();
private static final ThreadLocal<String> ENDL_HOLDER = new ThreadLocal<String>();
private static String AUTH_INFO = null;
// Add all executers here.
static {
addCommand("get", new Executer.Getter());
addCommand("list", new Executer.Lister());
addCommand("set", new Executer.Setter());
addCommand("invoke", new Executer.Invoker());
addCommand("mon", new Executer.InvoMonitor());
}
/**
* Add a custom command to worker.
*
* @param key the command key
* @param value the command executer
* @return the previous value associated with key, or null if there was no mapping for key.
*/
public static final Object addCommand(String key, Executer value) {
return COMMAND_MAP.put(key, value);
}
/**
* Set authentication info to worker.
*
* @param s the authentication string
*/
public static final void setAuthInfo(String s) {
AUTH_INFO = s;
}
/**
* Get the end line character for this current connection.
*
* @return The end line character
*/
public static String endl() {
String endl = ENDL_HOLDER.get();
return endl == null ? "\n" : endl;
}
// Scan the input stream.
private final Scanner scan;
// Write result to this output.
private final OutputStream out;
// The socket connection.
private final Socket sock;
// Current connection number.
private final AtomicInteger connNum;
// The bean map.
private final ConcurrentHashMap<String, Object> beanMap;
// Had we authenticated this connection?
private boolean hasAuthed = false;
/**
* The main Constructor, used to instantiate connection worker.
*
* @param socket the socket to work with
* @param map the global bean map
* @param connectionNumber the connection number counter
* @throws IOException if I/O related error occurred
*/
public ConnectionWorker(Socket socket, ConcurrentHashMap<String, Object> map, AtomicInteger connectionNumber)
throws IOException {
scan = new Scanner(socket.getInputStream(), "UTF-8");
out = socket.getOutputStream();
sock = socket;
connNum = connectionNumber;
beanMap = map;
LOG.info("Remote client [{}] connected.", socket.getRemoteSocketAddress());
}
/**
* The main work loop.
*
* Override super method
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
ENDL_HOLDER.set("\n");
try {
execute();
} catch (Exception e) {
LOG.debug("Error occurred when execute commands.", e);
} finally {
scan.close();
SystemUtil.close(out);
SystemUtil.close(sock);
connNum.decrementAndGet();
}
LOG.info("Remote client [{}] disconnected.", sock.getRemoteSocketAddress());
}
/**
* Execute the command here.
* The main work is to find the correct target object and execute the command on it.
*
* @throws IOException if I/O related error occurred
*/
@SuppressWarnings("incomplete-switch")
public void execute() throws IOException {
while (scan.hasNextLine()) {
final String line = scan.nextLine();
final String[] args = line.split("\\s+", 4);
// Empty line.
if (args.length == 0 || args[0].length() == 0) {
continue;
}
final String comm = args[0].toLowerCase();
Boolean b = commonProcess(comm, args);
if (b == null) break;
if (b == Boolean.TRUE) continue;
// Parse tree.
Path path = Path.parsePath(args[1]);
if (path.getType() == Type.INVALID) {
writeAndFlush(path.getName() + "^");
continue;
}
Object parent = beanMap.get(path.getName());
int pathIdx = 1;
// We need to break this while loop.
Outter:
while (parent != null && path != null) {
// Navigate into bean according to the path.
String name = path.getName();
try {
// For (pathIdx == 1) we already get parent from bean map.
if (pathIdx != 1) {
Field f = FieldUtil.getField(parent.getClass(), name);
f.setAccessible(true);
parent = f.get(parent);
}
} catch (Exception e) {
writeAndFlush("Invalid Path started at " + pathIdx + "." + name);
break;
}
switch(path.getType()) {
case ARRAY:
// Want to visit collection[array, list, set] here.
int idx = path.getIdx();
if (parent instanceof Collection<?>) {
Collection<? extends Object> os = (Collection<?>) parent;
if (os.size() <= idx) {
writeAndFlush("Invalid Path started at "
+ pathIdx + "." + name + " Array Out of Bound.");
break Outter;
}
Iterator<? extends Object> iter = os.iterator();
for (int i = 0; i < idx; ++i) {
iter.next();
}
parent = iter.next();
} else if (parent.getClass().isArray()) {
if (Array.getLength(parent) <= idx) {
writeAndFlush("Invalid Path started at " + pathIdx + "." + name + " Array Out of Bound.");
break Outter;
}
parent = Array.get(parent, idx);
} else {
writeAndFlush("Invalid Path started at " + pathIdx + "." + name + " Not Array.");
break Outter;
}
break;
case MAP:
// Want to visit map here.
if (parent instanceof Map<?, ?>) {
Map<? extends Object, ? extends Object> map = (Map<?, ?>) parent;
if (map.size() == 0) {
writeAndFlush("Map at " + pathIdx + "." + name + " Is Empty.");
break Outter;
}
Object key = map.keySet().iterator().next();
String realKey = path.getKey();
if (key instanceof String) {
parent = map.get(realKey);
} else if (key instanceof Integer) {
try {
idx = Integer.parseInt(realKey);
parent = map.get(idx);
} catch (Exception e) {
writeAndFlush("Invalid Map Key at " + pathIdx + "." + name);
break Outter;
}
} else if (key instanceof Long) {
try {
long lkey = Long.parseLong(realKey);
parent = map.get(lkey);
} catch (Exception e) {
writeAndFlush("Invalid Map Key at " + pathIdx + "." + name);
break Outter;
}
} else {
writeAndFlush("This Map Key Type " + key.getClass().getSimpleName() + " at "
+ pathIdx + "." + name + " Is Not Supported.");
break Outter;
}
} else {
writeAndFlush("Invalid Path started at " + pathIdx + "." + name + " Not Map.");
break Outter;
}
break;
}
++pathIdx;
path = path.next();
}
if (parent == null) {
writeAndFlush("Path Not Found.");
continue;
}
if (path != null) {
continue;
}
Executer ex = COMMAND_MAP.get(comm);
ex.execute(parent, out, args);
}
}
/**
* Process the common commands.
*
* @param comm the command name
* @param args the command arguments
* @return TRUE if command processed, FALSE if not, null if need exit
* @throws IOException if I/O related error occurred
*/
protected Boolean commonProcess(String comm, String[] args) throws IOException {
// Change End Of Line
if (comm.startsWith("win")) {
ENDL_HOLDER.set("\r\n");
writeAndFlush("End Line Changed.");
return Boolean.TRUE;
}
if (comm.startsWith("lin")) {
ENDL_HOLDER.set("\n");
writeAndFlush("End Line Changed.");
return Boolean.TRUE;
}
// Quit
if ("quit".equals(comm) || "exit".equals(comm)) {
writeAndFlush("Goodbye.");
return null;
}
// Auth
if ("auth".equals(comm)) {
if (AUTH_INFO == null || (args.length == 2 && AUTH_INFO.equals(args[1]))) {
writeAndFlush("Authenticate Success.");
hasAuthed = true;
} else {
hasAuthed = false;
writeAndFlush("Authenticate Failed.");
}
return Boolean.TRUE;
}
if (AUTH_INFO != null && !hasAuthed) {
writeAndFlush("Please authenticate.");
return Boolean.TRUE;
}
// Invalid command.
if (!COMMAND_MAP.containsKey(comm) || args.length < 2 || args[1].length() == 0) {
writeAndFlush("Invalid Command.");
return Boolean.TRUE;
}
return Boolean.FALSE;
}
/**
* Write the string to the output stream and automatically add an end line character at the
* end of the string, then flush the output stream.
*
* @param s the string to be written
* @throws IOException if I/O related error occurred
*/
protected void writeAndFlush(String s) throws IOException {
out.write(StringUtil.strToUtf8Byte(s + endl()));
out.flush();
}
}