/*
* Hook.java
* Copyright (C) 2011,2012 Wannes De Smet
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.xenmaster.web;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import net.wgr.core.ReflectionUtils;
import net.wgr.server.web.handling.WebCommandHandler;
import net.wgr.utility.GlobalExecutorService;
import net.wgr.wcp.command.Command;
import net.wgr.wcp.command.CommandException;
import net.wgr.wcp.command.Result;
import net.wgr.wcp.connectivity.Connection;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.xenmaster.api.util.APIHook;
import org.xenmaster.api.util.APIUtil;
import org.xenmaster.api.util.CachingFacility;
import org.xenmaster.controller.BadAPICallException;
/**
*
* @created Oct 1, 2011
* @author double-u
*/
public class Hook extends WebCommandHandler {
protected ConcurrentHashMap<Integer, StoredValue> store;
protected Class clazz = null;
protected Object current = null;
protected String className = "", commandName;
protected Connection connection;
public Hook() {
super("xen");
store = new ConcurrentHashMap<>();
GlobalExecutorService.get().scheduleAtFixedRate(new Housekeeper(), 1, 5, TimeUnit.MINUTES);
}
@Override
public Object execute(Command cmd) {
// Cleanup
clazz = null;
current = null;
className = commandName = "";
connection = cmd.getConnection();
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(APICall.class, new APICallDecoder());
Gson gson = builder.create();
APICall apic = gson.fromJson(cmd.getData(), APICall.class);
return executeInstruction(cmd.getName(), apic.ref, apic.args);
}
protected <T> String createLocalObject(Class<T> clazz, Object[] args) throws Exception {
T obj = clazz.newInstance();
if (args == null || args.length < 1 || args[0] == null || !(args[0] instanceof Map)) {
throw new IllegalArgumentException("Illegal arguments map was given");
}
for (Map.Entry<String, Object> entry : ((Map<String, Object>) args[0]).entrySet()) {
String methodName = "set" + entry.getKey().toLowerCase();
boolean set = false;
for (Method m : clazz.getMethods()) {
if (!m.getName().toLowerCase().equals(methodName) || m.getParameterTypes().length != 1) {
continue;
}
m.invoke(obj, APIUtil.deserializeToTargetType(entry.getValue(), m.getParameterTypes()[0]));
set = true;
}
if (!set) {
Logger.getLogger(getClass()).debug("Given field was not able to be set: " + entry.getKey());
}
}
return storeLocalObject(obj);
}
protected void determineClass(String ref, int index, String[] values) throws Exception {
String s = values[index];
int refOpen = s.indexOf('[');
if (refOpen != -1) {
className = s.substring(0, refOpen);
clazz = Class.forName("org.xenmaster.api." + className);
} else if (index == values.length - 2) {
className += s;
clazz = Class.forName("org.xenmaster.api." + className);
} else {
className += s + '.';
}
if (refOpen != -1) {
ref = s.substring(refOpen + 1, s.indexOf(']'));
}
initClassInstance(ref, clazz);
}
protected void initClassInstance(String ref, Class clazz) {
if (clazz != null && ref == null && APIHook.class.isAssignableFrom(clazz)) {
try {
Constructor ctor = clazz.getConstructor(Connection.class);
current = ctor.newInstance(connection);
return;
} catch (Exception ex) {
Logger.getLogger(getClass()).error("Failed to init APIHook", ex);
}
}
// The reference may be an empty string, just not null
if (ref != null && clazz != null) {
if (ref.startsWith("LocalRef:")) {
Integer localRef = Integer.parseInt(ref.substring(ref.indexOf(":") + 1));
if (store.get(localRef) == null) {
current = new IllegalStateException("LocalRef has expired : " + localRef);
return;
}
current = store.get(localRef).value;
} else {
current = CachingFacility.get(ref, clazz);
}
}
}
protected Object findAndCallMethod(String ref, String s, Object[] args) throws Exception {
int open = s.indexOf('(');
String methodName = (open != -1 ? s.substring(0, open) : s);
// Caller requested new instance of object
if (methodName.equals("new")) {
return current;
}
if (open != -1) {
String argstr = s.substring(s.indexOf('(') + 1, s.indexOf(')'));
argstr = argstr.replace(", ", ",");
args = StringUtils.split(argstr, ',');
}
if (current != null) {
clazz = current.getClass();
}
ArrayList<Method> matches = new ArrayList<>();
// First find name matches
for (Method m : ReflectionUtils.getAllMethods(clazz)) {
if (m.getName().equals(methodName) && Modifier.isPublic(m.getModifiers())) {
matches.add(m);
}
}
// Then param count matches
for (ListIterator<Method> it = matches.listIterator(); it.hasNext();) {
Method m = it.next();
if ((m.getParameterTypes().length > 0 && args == null) || (args != null && m.getParameterTypes().length != args.length)) {
it.remove();
}
}
if (matches.size() > 0 && matches.size() < 2) {
parseAndExecuteMethod(matches.get(0), args);
} else if (matches.isEmpty()) {
if (methodName.equals("build")) {
return createLocalObject(clazz, args);
} else {
Logger.getLogger(getClass()).warn("Method not found " + s + " in " + commandName);
return new CommandException("Method " + methodName + " was not found", commandName);
}
} else {
// We cannot match based on type information as we use the type information to cast the parameters
return new CommandException("The function call was ambiguous with " + matches.size() + " matched methods", commandName);
}
return null;
}
protected void parseAndExecuteMethod(Method m, Object[] args) throws Exception {
Class<?>[] types = m.getParameterTypes();
// Check method signature
if ((types != null && types.length != 0) && ((types.length > 0 && args == null) || (types.length != args.length))) {
Logger.getLogger(getClass()).info("Hook call made with incorrect number of arguments: " + commandName);
current = new CommandException("Illegal number of arguments in " + m.getName() + " call", commandName);
} else if (args != null) {
for (int j = 0; j < types.length; j++) {
Class<?> type = types[j];
Object value = args[j];
if (value == null) {
throw new IllegalArgumentException("An argument for " + clazz.getSimpleName() + '.' + m.getName() + " was null");
} else if (value instanceof String) {
String str = value.toString();
if (str.startsWith("LocalRef:")) {
Integer localRef = Integer.parseInt(str.substring(str.indexOf(":") + 1));
if (!store.containsKey(localRef)) {
current = new CommandException("Local object reference does not exist", commandName);
}
args[j] = store.get(localRef).value;
} else {
args[j] = APIUtil.deserializeToTargetType(value, type);
}
} else {
args[j] = APIUtil.deserializeToTargetType(value, type);
}
}
}
try {
if (Modifier.isStatic(m.getModifiers())) {
current = m.invoke(null, args);
} else {
if (current == null) {
throw new IllegalArgumentException("Instance method called as a static method.");
}
m.setAccessible(true);
current = m.invoke(current, args);
}
} catch (InvocationTargetException ex) {
// If it has a cause, it will be parsed by the next handler
if (ex.getCause() == null) {
Logger.getLogger(getClass()).info("Failed to invoke method", ex);
current = new CommandException(ex, commandName);
} else {
Logger.getLogger(getClass()).info("Hook call threw Exception", ex.getCause());
if (ex.getCause() instanceof BadAPICallException) {
current = new DetailedCommandException(commandName, ((BadAPICallException) ex.getCause()));
} else {
current = new CommandException(ex.getCause(), commandName);
}
}
}
}
protected Object executeInstruction(String command, String ref, Object[] args) {
String[] split = StringUtils.split(command, '.');
commandName = command;
for (int i = 0; i < split.length; i++) {
String s = split[i];
try {
if (clazz == null) {
determineClass(ref, i, split);
} else if (APIHook.class.isAssignableFrom(clazz) && ref == null) {
// API hooks are responsable for their own handling
return ((APIHook) current).handle(command, args, this);
} else {
Object result = findAndCallMethod(ref, s, args);
if (result != null) {
return result;
}
}
} catch (Exception ex) {
Logger.getLogger(getClass()).error("Instruction failed " + s, ex);
return new CommandException(ex, commandName);
}
}
if (current == null) {
current = new Result(null, null, "EMPTY");
}
return current;
}
public String storeLocalObject(Object obj) {
int ref = store.size();
store.put(ref, new StoredValue(obj));
return "LocalRef:" + ref;
}
public String getReferenceForLocalObject(Object obj) {
for (Map.Entry<Integer, StoredValue> entry : store.entrySet()) {
if (entry.getValue().value.equals(obj)) {
return "LocalRef:" + entry.getKey();
}
}
return null;
}
public static class APICall {
public String ref;
public Object[] args;
}
public static class StoredValue {
public long lastAccess = System.currentTimeMillis();
public Object value;
public StoredValue(Object value) {
this.value = value;
}
}
protected class Housekeeper implements Runnable {
@Override
public void run() {
for (Iterator<Map.Entry<Integer, StoredValue>> it = store.entrySet().iterator(); it.hasNext();) {
Map.Entry<Integer, StoredValue> entry = it.next();
Logger.getLogger(getClass()).debug("Deleting stale object with index LocalRef:" + entry.getKey());
if (System.currentTimeMillis() - entry.getValue().lastAccess > 60000) {
it.remove();
}
}
}
}
}