blob: 8dbe02fad9a5534c2cad60586490fee488cbe2ae [file] [log] [blame]
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF 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.edgent.runtime.jsoncontrol;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import org.apache.edgent.execution.services.ControlService;
import org.apache.edgent.execution.services.Controls;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
/**
* Control service that accepts control instructions as JSON objects.
* <BR>
* A JSON object representing a control request can be passed
* to {@link #controlRequest(JsonObject)} to invoke a control
* operation (method) on a registered MBean.
*
* @see <a href="{@docRoot}/org/apache/edgent/runtime/jsoncontrol/package-summary.html">Control Request JSON</a>
*/
public class JsonControlService implements ControlService {
private static final Logger logger = LoggerFactory.getLogger(ControlService.class);
/**
* Key for the type of the control MBean in a JSON request.
* <BR>
* Value is {@value}.
*/
public static final String TYPE_KEY = "type";
/**
* Key for the alias of the control MBean in a JSON request.
* <BR>
* Value is {@value}. */
public static final String ALIAS_KEY = "alias";
/**
* Key for the operation name.
* <BR>
* Value is {@value}.
*/
public static final String OP_KEY = "op";
/**
* Key for the argument list.
* If no arguments are required then
* {@value} can be missing or an empty list.
* <BR>
* Value is {@value}.
*/
public static final String ARGS_KEY = "args";
private final Gson gson = new Gson();
private final Map<String, ControlMBean<?>> mbeans = new HashMap<>();
private static String getControlId(String type, String id, String alias) {
return type + ":" + (alias == null ? id : alias);
}
/**
* Handle a JSON control request.
*
* The control action is executed directly
* using the calling thread.
*
* @param request the request
* @return JSON response, JSON null if the request was not recognized.
* @throws Exception on failure
*/
public JsonElement controlRequest(JsonObject request) throws Exception {
if (request.has(OP_KEY))
return controlOperation(request);
return JsonNull.INSTANCE;
}
/**
* {@inheritDoc}
* <P>
* All control service MBeans must be valid according
* to {@link Controls#isControlServiceMBean(Class)}.
* </P>
*
* @see Controls#isControlServiceMBean(Class)
*/
@Override
public synchronized <T> String registerControl(String type, String id, String alias, Class<T> controlInterface,
T control) {
if (!Controls.isControlServiceMBean(controlInterface))
throw new IllegalArgumentException();
final String controlId = getControlId(type, id, alias);
if (mbeans.containsKey(controlId)) {
logger.error("Control id: {} already exists", controlId);
throw new IllegalStateException();
}
logger.trace("Register control id: {}", controlId);
mbeans.put(controlId, new ControlMBean<T>(controlInterface, control));
return controlId;
}
/**
* {@inheritDoc}
*/
@Override
public synchronized void unregister(String controlId) {
logger.trace("Unegister control id: {}", controlId);
mbeans.remove(controlId);
}
/**
* Handle a control operation.
* An operation maps to a {@code void} method.
* @param request Request to be executed.
* @return JSON boolean true if the request was executed, false if it was not.
* @throws Exception Exception executing the control instruction.
*/
private JsonElement controlOperation(JsonObject request) throws Exception {
final String type = request.get(TYPE_KEY).getAsString();
String alias = request.get(ALIAS_KEY).getAsString();
final String controlId = getControlId(type, null, alias);
logger.trace("Operation - control id: {}", controlId);
ControlMBean<?> mbean;
synchronized (this) {
mbean = mbeans.get(controlId);
}
if (mbean == null) {
logger.warn("Unable to find mbean for control id: {}", controlId);
return new JsonPrimitive(Boolean.FALSE);
}
String methodName = request.get(OP_KEY).getAsString();
logger.trace("Operation method - control id: {} method: {}", controlId, methodName);
int argumentCount = 0;
JsonArray args = null;
if (request.has(ARGS_KEY)) {
args = request.getAsJsonArray(ARGS_KEY);
argumentCount = args.size();
}
Method method = findMethod(mbean.getControlInterface(), methodName, argumentCount);
if (method == null) {
logger.warn("Unable to find method \"{}\" with {} args in {}", methodName, argumentCount, mbean.getControlInterface().getName());
return new JsonPrimitive(Boolean.FALSE);
}
logger.trace("Execute operation - control id: {} method: {}", controlId, methodName);
executeMethod(method, mbean.getControl(), getArguments(method, args));
logger.trace("Execute completed - control id: {} method: {}", controlId, methodName);
return new JsonPrimitive(Boolean.TRUE);
}
private Method findMethod(Class<?> controlInterface, String name, int argumentCount) {
Method[] methods = controlInterface.getDeclaredMethods();
for (Method method : methods) {
if (!Modifier.isPublic(method.getModifiers()))
continue;
if (name.equals(method.getName()) && method.getParameterTypes().length == argumentCount)
return method;
}
return null;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private Object[] getArguments(Method method, JsonArray args) {
final Class<?>[] paramTypes = method.getParameterTypes();
if (paramTypes.length == 0 || args == null || args.size() == 0)
return null;
assert paramTypes.length == args.size();
Object[] oargs = new Object[paramTypes.length];
for (int i = 0; i < oargs.length; i++) {
final Class<?> pt = paramTypes[i];
final JsonElement arg = args.get(i);
Object jarg;
if (String.class == pt) {
if (arg instanceof JsonObject)
jarg = gson.toJson(arg);
else
jarg = arg.getAsString();
}
else if (Integer.TYPE == pt)
jarg = arg.getAsInt();
else if (Long.TYPE == pt)
jarg = arg.getAsLong();
else if (Double.TYPE == pt)
jarg = arg.getAsDouble();
else if (Boolean.TYPE == pt)
jarg = arg.getAsBoolean();
else if (pt.isEnum())
jarg = Enum.valueOf((Class<Enum>) pt, arg.getAsString());
else
throw new UnsupportedOperationException(pt.getName());
oargs[i] = jarg;
}
return oargs;
}
private void executeMethod(Method method, Object control, Object[] arguments)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
method.invoke(control, arguments);
}
@Override
public <T> T getControl(String type, String alias, Class<T> controlInterface) {
String controlId = getControlId(type, null, alias);
ControlMBean<?> bean = getControlMBean(controlId, controlInterface);
return bean != null ? controlInterface.cast(bean.getControl()) : null;
}
@Override
public <T> String getControlId(String type, String alias, Class<T> controlInterface) {
String controlId = getControlId(type, null, alias);
return getControlMBean(controlId, controlInterface) != null ? controlId : null;
}
private synchronized <T> ControlMBean<?> getControlMBean(String controlId, Class<T> controlInterface) {
ControlMBean<?> bean = mbeans.get(controlId);
if (bean == null)
return null;
if (bean.getControlInterface() != controlInterface)
return null;
return bean;
}
}