--- a/src/java.management.rest/share/classes/com/oracle/jmx/remote/rest/http/MBeanResource.java Mon Dec 25 20:42:05 2017 +0530
+++ b/src/java.management.rest/share/classes/com/oracle/jmx/remote/rest/http/MBeanResource.java Wed Dec 27 14:44:48 2017 +0530
@@ -4,15 +4,24 @@
import com.oracle.jmx.remote.rest.json.JSONElement;
import com.oracle.jmx.remote.rest.json.JSONObject;
import com.oracle.jmx.remote.rest.json.JSONPrimitive;
+import com.oracle.jmx.remote.rest.json.parser.JSONParser;
+import com.oracle.jmx.remote.rest.json.parser.ParseException;
+import com.oracle.jmx.remote.rest.mapper.JSONDataException;
import com.oracle.jmx.remote.rest.mapper.JSONMapper;
import com.oracle.jmx.remote.rest.mapper.JSONMappingException;
import com.oracle.jmx.remote.rest.mapper.JSONMappingFactory;
import com.sun.net.httpserver.HttpExchange;
import javax.management.*;
+import javax.management.openmbean.OpenMBeanAttributeInfo;
+import javax.management.openmbean.OpenMBeanParameterInfo;
+import javax.management.openmbean.OpenType;
import javax.management.remote.rest.PlatformRestAdapter;
import java.io.IOException;
+import java.net.HttpURLConnection;
import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import static javax.management.MBeanOperationInfo.*;
@@ -20,6 +29,21 @@
private final ObjectName objectName;
private final MBeanServer mBeanServer;
+ private static final String pathPrefix = "^/?jmx/servers/[^/]+/mbeans/[^/]+";
+
+ private final static Map<String, Class<?>> primitiveToObject = new HashMap<>();
+
+ static {
+ primitiveToObject.put("int", Integer.TYPE);
+ primitiveToObject.put("long", Long.TYPE);
+ primitiveToObject.put("double", Double.TYPE);
+ primitiveToObject.put("float", Float.TYPE);
+ primitiveToObject.put("boolean", Boolean.TYPE);
+ primitiveToObject.put("char", Character.TYPE);
+ primitiveToObject.put("byte", Byte.TYPE);
+ primitiveToObject.put("void", Void.TYPE);
+ primitiveToObject.put("short", Short.TYPE);
+ }
public MBeanResource(MBeanServer mBeanServer, ObjectName objectName) {
this.mBeanServer = mBeanServer;
@@ -171,6 +195,29 @@
return jobj2;
}
+ private Map<String, Object> getAttributes(String[] attrs) throws IntrospectionException,
+ InstanceNotFoundException, ReflectionException, AttributeNotFoundException, MBeanException {
+ Map<String, Object> result = new LinkedHashMap<>();
+ if (attrs.length == 1) {
+ result.put(attrs[0], mBeanServer.getAttribute(objectName, attrs[0]));
+ } else {
+ AttributeList attrVals = mBeanServer.getAttributes(objectName, attrs);
+ if (attrVals.size() != attrs.length) {
+ List<String> missingAttrs = new ArrayList<>(Arrays.asList(attrs));
+ for (Attribute a : attrVals.asList()) {
+ missingAttrs.remove(a.getName());
+ result.put(a.getName(), a.getValue());
+ }
+ for (String attr : missingAttrs) {
+ result.put(attr, "< Error: No such attribute >");
+ }
+ } else {
+ attrVals.asList().forEach((a) -> result.put(a.getName(), a.getValue()));
+ }
+ }
+ return result;
+ }
+
private Map<String, Object> getAllAttributes() throws IntrospectionException,
InstanceNotFoundException, ReflectionException, AttributeNotFoundException, MBeanException {
Map<String, Object> result = new HashMap<>();
@@ -206,6 +253,166 @@
return result;
}
+ private Map<String, Object> setAttributes(JSONObject attrMap) throws IntrospectionException,
+ InstanceNotFoundException, ReflectionException, JSONDataException, ClassNotFoundException, MBeanException {
+ if (attrMap == null || attrMap.isEmpty()) {
+ throw new JSONDataException("Null arguments for set attribute");
+ }
+ Map<String, Object> result = new HashMap<>();
+ for (String attrName : attrMap.keySet()) {
+ MBeanInfo mBeanInfo = mBeanServer.getMBeanInfo(objectName);
+ MBeanAttributeInfo attrInfo = Arrays.stream(mBeanInfo.getAttributes()).filter(a -> a.getName().equals(attrName)).findFirst().orElse(null);
+ if (attrInfo == null) {
+ result.put(attrName, "<Attribute not found>");
+ } else if (!attrInfo.isWritable()) {
+ result.put(attrName, "<Attribute is read-only>");
+ } else {
+ JSONMapper mapper;
+ if (attrInfo instanceof OpenMBeanAttributeInfo) {
+ OpenType<?> type = ((OpenMBeanAttributeInfo) attrInfo).getOpenType();
+ mapper = JSONMappingFactory.INSTANCE.getTypeMapper(type);
+ } else {
+ Class<?> inputCls = primitiveToObject.get(attrInfo.getType());
+ try {
+ if (inputCls == null) {
+ inputCls = Class.forName(attrInfo.getType());
+ }
+ } catch (ClassNotFoundException | ClassCastException ex) {
+ throw new IllegalArgumentException("Invalid parameters : " + attrMap.get(attrName).toJsonString() + " cannot be mapped to : " + attrInfo.getType());
+ }
+ mapper = JSONMappingFactory.INSTANCE.getTypeMapper(inputCls);
+ }
+ try {
+ Object attrValue = mapper.toJavaObject(attrMap.get(attrName));
+ Attribute attrObj = new Attribute(attrName, attrValue);
+ mBeanServer.setAttribute(objectName, attrObj);
+ result.put(attrName, "success");
+ } catch (InvalidAttributeValueException | JSONDataException e) {
+ result.put(attrName, "<Invalid value for the attribute>");
+ } catch (AttributeNotFoundException e) {
+ result.put(attrName, "<Attribute not found>");
+ }
+ }
+ }
+ return result;
+ }
+
+ private Object mapJsonToType(JSONElement jsonElement, MBeanParameterInfo type) {
+ if (type instanceof OpenMBeanParameterInfo) {
+ OpenType<?> openType = ((OpenMBeanParameterInfo) type).getOpenType();
+ JSONMapper typeMapper = JSONMappingFactory.INSTANCE.getTypeMapper(openType);
+ try {
+ return typeMapper.toJavaObject(jsonElement);
+ } catch (JSONDataException ex) {
+ throw new IllegalArgumentException("Invalid JSON String : " + jsonElement.toJsonString() + " for arguments");
+ }
+ } else {
+ Class<?> inputCls = primitiveToObject.get(type.getType());
+ try {
+ if (inputCls == null) {
+ inputCls = Class.forName(type.getType());
+ }
+ } catch (ClassNotFoundException | ClassCastException ex) {
+ throw new IllegalArgumentException("Invalid parameters : " + jsonElement.toJsonString() + " cannot be mapped to : " + type.getType());
+ }
+ JSONMapper typeMapper = JSONMappingFactory.INSTANCE.getTypeMapper(inputCls);
+ if (typeMapper == null) {
+ throw new IllegalArgumentException("Invalid parameters : " + jsonElement.toJsonString() + " cannot be mapped to : " + type.getType());
+ }
+ try {
+ return typeMapper.toJavaObject(jsonElement);
+ } catch (JSONDataException ex) {
+ throw new IllegalArgumentException("Invalid JSON String : " + jsonElement.toJsonString() + " for arguments");
+ }
+ }
+ }
+
+ private Map<String, Object> getParameters(Map<String, JSONElement> jsonValues, Map<String, MBeanParameterInfo> typeMap) {
+ if (jsonValues.size() != typeMap.size()) {
+ throw new IllegalArgumentException("Invalid parameters : expected - " + typeMap.size() + " parameters, got - " + jsonValues.size());
+ }
+ if(!jsonValues.keySet().equals(typeMap.keySet())) {
+ throw new IllegalArgumentException("Invalid parameters - expected : " + Arrays.toString(typeMap.keySet().toArray()));
+ }
+ Map<String, Object> parameters = new LinkedHashMap<>();
+ if (typeMap.size() == 0 && jsonValues.isEmpty()) {
+ return parameters;
+ }
+ for (String name : typeMap.keySet()) {
+ MBeanParameterInfo type = typeMap.get(name);
+ JSONElement jsonVal = jsonValues.get(name);
+ Object obj = mapJsonToType(jsonVal, type);
+ parameters.put(name, obj);
+ }
+ return parameters;
+ }
+
+ private JSONElement execOperation(String opstr, JSONObject params)
+ throws MBeanException, IntrospectionException, ReflectionException, InstanceNotFoundException {
+ if (params == null) {
+ params = new JSONObject();
+ }
+ MBeanInfo mBeanInfo;
+ try {
+ mBeanInfo = mBeanServer.getMBeanInfo(objectName);
+ } catch (InstanceNotFoundException ex) {
+ throw new IllegalArgumentException("MBean does not exist");
+ }
+
+ MBeanOperationInfo[] opinfos = Arrays.stream(mBeanInfo.getOperations()).
+ filter(a -> a.getName().equals(opstr)).toArray(MBeanOperationInfo[]::new);
+
+ if (opinfos.length == 0) {
+ throw new IllegalArgumentException("Invalid Operation String");
+ }
+
+ String[] signature = null;
+ Object[] parameters = null;
+
+ if (opinfos.length == 1) {
+ MBeanParameterInfo[] sig = opinfos[0].getSignature();
+ Map<String, MBeanParameterInfo> typeMap = new LinkedHashMap<>();
+ Arrays.stream(sig).forEach(e -> typeMap.put(e.getName(), e));
+ parameters = getParameters(params, typeMap).values().toArray();
+ signature = Arrays.asList(sig).stream().map(a -> a.getType()).toArray(a -> new String[a]);
+ } else if (opinfos.length > 1) {
+ IllegalArgumentException exception = null;
+ for (MBeanOperationInfo opInfo : opinfos) {
+ MBeanParameterInfo[] sig = opInfo.getSignature();
+ try {
+ Map<String, MBeanParameterInfo> typeMap = new LinkedHashMap<>();
+ Arrays.stream(sig).forEach(e -> typeMap.put(e.getName(), e));
+ parameters = getParameters(params, typeMap).values().toArray();
+ signature = Arrays.asList(sig).stream().map(a -> a.getType()).toArray(a -> new String[a]);
+ exception = null;
+ break;
+ } catch (IllegalArgumentException ex) {
+ exception = ex;
+ }
+ }
+ if (exception != null) {
+ throw exception;
+ }
+ }
+
+ Object invoke = null;
+ try {
+ invoke = mBeanServer.invoke(objectName, opstr, parameters, signature);
+ if (invoke != null) {
+ JSONMapper typeMapper = JSONMappingFactory.INSTANCE.getTypeMapper(invoke);
+ if (typeMapper != null) {
+ return typeMapper.toJsonValue(invoke);
+ } else {
+ return new JSONPrimitive("<Unable to map result to JSON>");
+ }
+ } else {
+ return new JSONPrimitive("void");
+ }
+ } catch (JSONMappingException e) {
+ return new JSONPrimitive("<Unable to map result to JSON>");
+ }
+ }
+
private HttpResponse doMBeanInfo(HttpExchange exchange) {
try {
JSONObject mBeanInfo = getMBeanInfo(mBeanServer, objectName);
@@ -222,8 +429,11 @@
@Override
public void handle(HttpExchange exchange) throws IOException {
String path = exchange.getRequestURI().getPath();
- if (path.matches("^\\/?jmx\\/servers\\/[a-zA-Z0-9\\-\\.]+\\/mbeans\\/[^\\/]+\\/?$") ||
- path.matches("^\\/?jmx\\/servers\\/[a-zA-Z0-9\\-\\.]+\\/mbeans\\/[^\\/]+\\/info$")) {
+ if (path.matches(pathPrefix + "/?$")) {
+ RestResource.super.handle(exchange);
+ } else if (path.matches(pathPrefix + "/info$") && exchange.getRequestMethod().equalsIgnoreCase("GET")) {
+ RestResource.super.handle(exchange);
+ } else if (path.matches(pathPrefix + "/[^/]+/?$") && exchange.getRequestMethod().equalsIgnoreCase("POST")) {
RestResource.super.handle(exchange);
} else {
HttpUtil.sendResponse(exchange, new HttpResponse(404, "Not found"));
@@ -232,7 +442,7 @@
@Override
public HttpResponse doGet(HttpExchange exchange) {
- if(exchange.getRequestURI().getPath().endsWith("info")) {
+ if (exchange.getRequestURI().getPath().endsWith("info")) {
return doMBeanInfo(exchange);
}
String path = PlatformRestAdapter.getAuthority() + exchange.getRequestURI().getPath().replaceAll("\\/$", "");
@@ -241,7 +451,7 @@
try {
Map<String, Object> allAttributes = getAllAttributes();
Map<String, String> _links = new LinkedHashMap<>();
- _links.put("info",HttpUtil.escapeUrl(info));
+ _links.put("info", HttpUtil.escapeUrl(info));
MBeanOperationInfo[] opInfo = mBeanServer.getMBeanInfo(objectName).getOperations();
JSONArray jarr = new JSONArray();
@@ -263,14 +473,14 @@
}
JSONMapper typeMapper = JSONMappingFactory.INSTANCE.getTypeMapper(allAttributes);
- if(typeMapper != null) {
+ if (typeMapper != null) {
JSONElement jsonElement1 = typeMapper.toJsonValue(allAttributes);
JSONElement jsonElement2 = typeMapper.toJsonValue(_links);
JSONObject jobj = new JSONObject();
- jobj.put("attributes",jsonElement1);
- jobj.put("operations",jarr);
- jobj.put("_links",jsonElement2);
+ jobj.put("attributes", jsonElement1);
+ jobj.put("operations", jarr);
+ jobj.put("_links", jsonElement2);
return new HttpResponse(200, jobj.toJsonString());
} else {
return new HttpResponse(HttpResponse.SERVER_ERROR, "Unable to find JSONMapper");
@@ -289,13 +499,194 @@
}
}
+ /*
+ HTTP POST for this MBean's URL allows setting of attributes and execution of operations.
+ POST request body can follow one of the below formats
+ 1. { name : value}
+ Set a single attribute
+ 2. { name1 : value1, name2 : value2 }
+ Sets multiple attributes
+ 3. {attributes : {read : [name]} , {write : {name : value}}, operations : {op_name : {param_name:name, param_value:value}}}
+ This bulk operation request sets multiple attributes and executes multiple
+ operations on the MBean.
+ */
@Override
- public HttpResponse doPut(HttpExchange exchange) {
- return null;
+ public HttpResponse doPost(HttpExchange exchange) {
+ String path = exchange.getRequestURI().getPath();
+ String reqBody = null;
+ try {
+ if (path.matches(pathPrefix + "/?$")) { // POST to current URL
+ reqBody = HttpUtil.readRequestBody(exchange);
+ if (reqBody == null || reqBody.isEmpty()) { // No Parameters
+ return HttpResponse.BAD_REQUEST;
+ }
+
+ JSONParser parser = new JSONParser(reqBody);
+ JSONElement jsonElement = parser.parse();
+ if (!(jsonElement instanceof JSONObject)) {
+ return new HttpResponse(HttpResponse.BAD_REQUEST,
+ "Invalid parameters : [" + reqBody + "]");
+ }
+
+ JSONObject jsonObject = (JSONObject) jsonElement;
+
+ if (jsonObject.keySet().contains("attributes") | jsonObject.keySet().contains("operations")) {
+ return handleBulkRequest(exchange, jsonObject);
+ } else {
+ Map<String, Object> stringObjectMap = setAttributes(jsonObject);
+ JSONMapper typeMapper = JSONMappingFactory.INSTANCE.getTypeMapper(stringObjectMap);
+ if (typeMapper != null) {
+ return new HttpResponse(HttpURLConnection.HTTP_OK, typeMapper.toJsonValue(stringObjectMap).toJsonString());
+ } else {
+ return new HttpResponse(HttpResponse.SERVER_ERROR, "Unable to find JSON Mapper");
+ }
+ }
+ } else if (path.matches(pathPrefix + "/[^/]+/?$")) { // POST to MBeanOperation
+ Matcher matcher = Pattern.compile(pathPrefix + "/").matcher(path);
+ String operation;
+ if (matcher.find()) {
+ String ss = path.substring(matcher.end());
+ operation = ss;
+ } else {
+ return HttpResponse.BAD_REQUEST;
+ }
+
+ reqBody = HttpUtil.readRequestBody(exchange);
+ JSONElement result;
+ if (reqBody == null || reqBody.isEmpty()) { // No Parameters
+ result = execOperation(operation, null);
+ } else {
+ JSONParser parser = new JSONParser(reqBody);
+ JSONElement jsonElement = parser.parse();
+ if (!(jsonElement instanceof JSONObject)) {
+ return new HttpResponse(HttpResponse.BAD_REQUEST,
+ "Invalid parameters : [" + reqBody + "] for operation - " + operation);
+ }
+ result = execOperation(operation, (JSONObject) jsonElement);
+ }
+ return new HttpResponse(HttpURLConnection.HTTP_OK, result.toJsonString());
+ } else {
+ return HttpResponse.REQUEST_NOT_FOUND;
+ }
+ } catch (InstanceNotFoundException e) {
+ // Should never happen
+ } catch (ClassNotFoundException | JSONDataException | ParseException e) {
+ return new HttpResponse(HttpURLConnection.HTTP_BAD_REQUEST, "Invalid JSON : " + reqBody, e.getMessage());
+ } catch (IntrospectionException | JSONMappingException | MBeanException | ReflectionException | IOException e) {
+ return new HttpResponse(HttpResponse.SERVER_ERROR, HttpResponse.getErrorMessage(e));
+ } catch (IllegalArgumentException e) {
+ return new HttpResponse(HttpResponse.BAD_REQUEST, e.getMessage());
+ } catch (Exception e) {
+ return new HttpResponse(HttpResponse.SERVER_ERROR, HttpResponse.getErrorMessage(e));
+ }
+ return HttpResponse.REQUEST_NOT_FOUND;
+ }
+
+ private HttpResponse handleBulkRequest(HttpExchange exchange, JSONObject reqObject) {
+ try {
+ JSONObject result = new JSONObject();
+
+ do {
+ // Handle attributes
+ JSONElement element = reqObject.get("attributes");
+ if (element != null && !(element instanceof JSONObject)) // input validation
+ break;
+ if (element != null && element instanceof JSONObject) {
+ JSONObject attrInfo = (JSONObject) element;
+ JSONObject attrNode = new JSONObject();
+ // atleast one of get/set must be present
+ if (attrInfo.get("get") == null && attrInfo.get("set") == null)
+ break;
+ // Read attributes
+ JSONElement read = attrInfo.get("get");
+ if (read != null && !(read instanceof JSONArray))
+ break;
+ if (read != null && read instanceof JSONArray) {
+ JSONArray jattrs = (JSONArray) read;
+ String[] attributes = getStrings(jattrs);
+ Map<String, Object> attrRead = getAttributes(attributes);
+ JSONMapper typeMapper = JSONMappingFactory.INSTANCE.getTypeMapper(attrRead);
+ JSONElement jAttrRead = typeMapper.toJsonValue(attrRead);
+ attrNode.put("get", jAttrRead);
+ }
+
+ // Write attributes
+ JSONElement write = attrInfo.get("set");
+
+ if (write != null && !(write instanceof JSONObject))
+ break;
+
+ if (write != null && write instanceof JSONObject) {
+ JSONObject jattrs = (JSONObject) write;
+ Map<String, Object> attrMap = setAttributes(jattrs);
+ JSONMapper typeMapper = JSONMappingFactory.INSTANCE.getTypeMapper(attrMap);
+ JSONElement jAttrRead = typeMapper.toJsonValue(attrMap);
+ attrNode.put("set", jAttrRead);
+ }
+ result.put("attributes", attrNode);
+ }
+
+ // Execute operations
+ element = reqObject.get("operations");
+ if (element != null) {
+ JSONArray operationList;
+ if (element instanceof JSONPrimitive // Single no-arg operation
+ || element instanceof JSONObject) { // single/mulitple operations
+ operationList = new JSONArray();
+ operationList.add(element);
+ } else if (element instanceof JSONArray) { // List of no-arg/with-arg operation
+ operationList = (JSONArray) element;
+ } else {
+ // Should never happen
+ return HttpResponse.BAD_REQUEST;
+ }
+ JSONObject opResult = new JSONObject();
+ for (JSONElement elem : operationList) {
+ if (elem instanceof JSONPrimitive
+ && ((JSONPrimitive) elem).getValue() instanceof String) { // no-arg operation
+ String opName = (String) ((JSONPrimitive) elem).getValue();
+ JSONElement obj = execOperation(opName, null);
+ opResult.put(opName, obj);
+ } else if (elem instanceof JSONObject) {
+ Set<String> opNames = ((JSONObject) element).keySet();
+ for (String opName : opNames) {
+ JSONElement obj = execOperation(opName, (JSONObject) ((JSONObject) element).get(opName));
+ opResult.put(opName, obj);
+ }
+ }
+ }
+ result.put("operations", opResult);
+ }
+ return new HttpResponse(HttpURLConnection.HTTP_OK, result.toJsonString());
+ } while (false);
+ throw new JSONDataException("Invalid request body : " + reqObject.toJsonString());
+ } catch (InstanceNotFoundException e) {
+ // Should never happen
+ } catch (ClassNotFoundException | JSONDataException e) {
+ return new HttpResponse(HttpResponse.BAD_REQUEST, "Invalid JSON : " + e.getMessage());
+ } catch (IntrospectionException | JSONMappingException | MBeanException | ReflectionException e) {
+ return new HttpResponse(HttpResponse.SERVER_ERROR, HttpResponse.getErrorMessage(e));
+ } catch (IllegalArgumentException e) {
+ return new HttpResponse(HttpResponse.BAD_REQUEST, e.getMessage());
+ } catch (Exception e) {
+ return new HttpResponse(HttpResponse.SERVER_ERROR, HttpResponse.getErrorMessage(e));
+ }
+ return HttpResponse.REQUEST_NOT_FOUND;
+ }
+
+ private String[] getStrings(JSONArray jsonArray) throws JSONDataException {
+ List<String> attributes = new ArrayList<>();
+ for (JSONElement element : jsonArray) {
+ if (element instanceof JSONPrimitive && ((JSONPrimitive) element).getValue() instanceof String) {
+ JSONPrimitive val = (JSONPrimitive) element;
+ attributes.add((String) val.getValue());
+ } else throw new JSONDataException("Expecting String, got " + element.toJsonString());
+ }
+ return attributes.toArray(new String[0]);
}
@Override
- public HttpResponse doPost(HttpExchange exchange) {
+ public HttpResponse doPut(HttpExchange exchange) {
return null;
}