src/jdk.management.rest/share/classes/jdk/internal/management/remote/rest/http/MBeanCollectionResource.java
branchjmx-rest-api
changeset 56026 bd531f08d7c7
child 56027 81372436b79e
equal deleted inserted replaced
56007:d6cbabcaf518 56026:bd531f08d7c7
       
     1 /*
       
     2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
       
     3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       
     4  *
       
     5  * This code is free software; you can redistribute it and/or modify it
       
     6  * under the terms of the GNU General Public License version 2 only, as
       
     7  * published by the Free Software Foundation.  Oracle designates this
       
     8  * particular file as subject to the "Classpath" exception as provided
       
     9  * by Oracle in the LICENSE file that accompanied this code.
       
    10  *
       
    11  * This code is distributed in the hope that it will be useful, but WITHOUT
       
    12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
       
    13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
       
    14  * version 2 for more details (a copy is included in the LICENSE file that
       
    15  * accompanied this code).
       
    16  *
       
    17  * You should have received a copy of the GNU General Public License version
       
    18  * 2 along with this work; if not, write to the Free Software Foundation,
       
    19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
       
    20  *
       
    21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
       
    22  * or visit www.oracle.com if you need additional information or have any
       
    23  * questions.
       
    24  */
       
    25 
       
    26 package jdk.internal.management.remote.rest.http;
       
    27 
       
    28 import jdk.internal.management.remote.rest.json.JSONArray;
       
    29 import jdk.internal.management.remote.rest.json.JSONElement;
       
    30 import jdk.internal.management.remote.rest.json.JSONObject;
       
    31 import jdk.internal.management.remote.rest.json.JSONPrimitive;
       
    32 import jdk.internal.management.remote.rest.json.parser.JSONParser;
       
    33 import jdk.internal.management.remote.rest.json.parser.ParseException;
       
    34 import jdk.internal.management.remote.rest.mapper.JSONMapper;
       
    35 import jdk.internal.management.remote.rest.mapper.JSONMappingException;
       
    36 import jdk.internal.management.remote.rest.mapper.JSONMappingFactory;
       
    37 import com.sun.net.httpserver.HttpExchange;
       
    38 
       
    39 import javax.management.*;
       
    40 import jdk.internal.management.remote.rest.PlatformRestAdapter;
       
    41 
       
    42 import java.io.IOException;
       
    43 import java.io.UnsupportedEncodingException;
       
    44 import java.net.URLDecoder;
       
    45 import java.nio.charset.StandardCharsets;
       
    46 import java.util.*;
       
    47 import java.util.concurrent.ConcurrentHashMap;
       
    48 import java.util.concurrent.CopyOnWriteArrayList;
       
    49 import java.util.regex.Matcher;
       
    50 import java.util.regex.Pattern;
       
    51 import java.util.stream.Collectors;
       
    52 
       
    53 public class MBeanCollectionResource implements RestResource, NotificationListener {
       
    54 
       
    55     private List<ObjectName> allowedMbeans;
       
    56     private final MBeanServer mBeanServer;
       
    57     private final Map<ObjectName, MBeanResource> mBeanResourceMap = new ConcurrentHashMap<>();
       
    58     private static final int pageSize = 10;
       
    59     private static final String pathPrefix = "^/?jmx/servers/[a-zA-Z0-9\\-\\.]+/mbeans";
       
    60 
       
    61     // Only MXBean or any other MBean that uses types
       
    62     // that have a valid mapper functions
       
    63     private boolean isMBeanAllowed(ObjectName objName) {
       
    64         try {
       
    65             MBeanInfo mInfo = mBeanServer.getMBeanInfo(objName);
       
    66 
       
    67             // Return true for MXbean
       
    68             Descriptor desc = mInfo.getDescriptor();
       
    69             String isMxBean = (String) desc.getFieldValue("mxbean");
       
    70             if (isMxBean != null && isMxBean.equalsIgnoreCase("true"))
       
    71                 return true;
       
    72 
       
    73             // Check attribute types
       
    74             MBeanAttributeInfo[] attrsInfo = mInfo.getAttributes();
       
    75             for (MBeanAttributeInfo attrInfo : attrsInfo) {
       
    76                 String type = attrInfo.getType();
       
    77                 if (!JSONMappingFactory.INSTANCE.isTypeMapped(type)) {
       
    78                     return false;
       
    79                 }
       
    80             }
       
    81 
       
    82             // Check operation parameters and return types
       
    83             MBeanOperationInfo[] operations = mInfo.getOperations();
       
    84             for (MBeanOperationInfo opInfo : operations) {
       
    85                 MBeanParameterInfo[] signature = opInfo.getSignature();
       
    86                 for (MBeanParameterInfo sig : signature) {
       
    87                     if (!JSONMappingFactory.INSTANCE.isTypeMapped(sig.getType())) {
       
    88                         return false;
       
    89                     }
       
    90                 }
       
    91                 if (!JSONMappingFactory.INSTANCE.isTypeMapped(opInfo.getReturnType())) {
       
    92                     return false;
       
    93                 }
       
    94             }
       
    95             return true;
       
    96         } catch (InstanceNotFoundException | IntrospectionException |
       
    97                 ReflectionException | ClassNotFoundException ex) {
       
    98             ex.printStackTrace();
       
    99             return false;
       
   100         }
       
   101     }
       
   102 
       
   103     private void introspectMBeanTypes(MBeanServer server) {
       
   104         if (allowedMbeans.isEmpty()) {
       
   105             Set<ObjectInstance> allMBeans = server.queryMBeans(null, null); // get all Mbeans
       
   106             allMBeans.stream().filter((objIns) -> (isMBeanAllowed(objIns.getObjectName())))
       
   107                     .forEachOrdered(objIns -> allowedMbeans.add(objIns.getObjectName()));
       
   108         }
       
   109     }
       
   110 
       
   111     @Override
       
   112     public void handleNotification(Notification notification, Object handback) {
       
   113         try {
       
   114             MBeanServerNotification mbs = (MBeanServerNotification) notification;
       
   115             if (MBeanServerNotification.REGISTRATION_NOTIFICATION.equals(mbs.getType())) {
       
   116                 ObjectName mBeanName = mbs.getMBeanName();
       
   117                 if (isMBeanAllowed(mBeanName)) {
       
   118                     allowedMbeans.add(mBeanName);
       
   119                 }
       
   120             } else if (MBeanServerNotification.UNREGISTRATION_NOTIFICATION.equals(mbs.getType())) {
       
   121                 if (allowedMbeans.contains(mbs.getMBeanName())) {
       
   122                     allowedMbeans.remove(mbs.getMBeanName());
       
   123                 }
       
   124             }
       
   125         } catch (Exception e) {
       
   126         }
       
   127     }
       
   128 
       
   129     public MBeanCollectionResource(MBeanServer mBeanServer) {
       
   130         this.mBeanServer = mBeanServer;
       
   131         allowedMbeans = new ArrayList<>();
       
   132         introspectMBeanTypes(mBeanServer);
       
   133         allowedMbeans = new CopyOnWriteArrayList<>(allowedMbeans);
       
   134 
       
   135         // Create a REST handler for each MBean
       
   136         allowedMbeans.forEach(objectName -> mBeanResourceMap.put(objectName,
       
   137                 new MBeanResource(mBeanServer, objectName)));
       
   138     }
       
   139 
       
   140     @Override
       
   141     public void handle(HttpExchange exchange) throws IOException {
       
   142         String path = URLDecoder.decode(exchange.getRequestURI().getPath(), StandardCharsets.UTF_8.name());
       
   143 
       
   144         if (path.matches(pathPrefix + "/?$")) {
       
   145             RestResource.super.handle(exchange);
       
   146         } else if (path.matches(pathPrefix + "/[^/]+/?.*")) {
       
   147             // Extract mbean name
       
   148             // Forward the request to its corresponding rest resource
       
   149             Pattern mbeans = Pattern.compile(pathPrefix + "/");
       
   150             Matcher matcher = mbeans.matcher(path);
       
   151 
       
   152             if (matcher.find()) {
       
   153                 String ss = path.substring(matcher.end());
       
   154                 String mBeanName = ss;
       
   155                 if (ss.indexOf('/') != -1) {
       
   156                     mBeanName = ss.substring(0, ss.indexOf('/'));
       
   157                 }
       
   158                 try {
       
   159                     MBeanResource mBeanResource = mBeanResourceMap.get(new ObjectName(mBeanName));
       
   160                     if (mBeanResource == null) {
       
   161                         HttpUtil.sendResponse(exchange, HttpResponse.REQUEST_NOT_FOUND);
       
   162                         return;
       
   163                     }
       
   164                     mBeanResource.handle(exchange);
       
   165                 } catch (MalformedObjectNameException e) {
       
   166                     HttpUtil.sendResponse(exchange, HttpResponse.BAD_REQUEST);
       
   167                 }
       
   168 
       
   169             }
       
   170         }
       
   171     }
       
   172 
       
   173     @Override
       
   174     public HttpResponse doGet(HttpExchange exchange) {
       
   175         try {
       
   176             final String path = PlatformRestAdapter.getDomain()
       
   177                     + URLDecoder.decode(exchange.getRequestURI().getPath(), StandardCharsets.UTF_8.displayName())
       
   178                     .replaceAll("/$", "");
       
   179             List<ObjectName> filteredMBeans = allowedMbeans;
       
   180             Map<String, String> queryMap = HttpUtil.getGetRequestQueryMap(exchange);
       
   181             String query = exchange.getRequestURI().getQuery();
       
   182             if (query != null && queryMap.isEmpty()) {
       
   183                 return new HttpResponse(HttpResponse.BAD_REQUEST,
       
   184                         "Invalid query params : Allowed query keys [objectname,page]");
       
   185             } else if (query != null && !queryMap.isEmpty()) {
       
   186                 Map<String, String> newMap = new HashMap<>(queryMap);
       
   187                 newMap.remove("objectname");
       
   188                 newMap.remove("page");
       
   189                 if (!newMap.isEmpty()) { // Invalid query params
       
   190                     return new HttpResponse(HttpResponse.BAD_REQUEST,
       
   191                             "Invalid query params : Allowed query keys [objectname,page]");
       
   192                 }
       
   193             }
       
   194             if (queryMap.containsKey("objectname")) {        // Filter based on ObjectName query
       
   195                 Set<ObjectName> queryMBeans = mBeanServer
       
   196                         .queryNames(new ObjectName(queryMap.get("objectname")), null);
       
   197                 queryMBeans.retainAll(allowedMbeans);   // Intersection of two lists
       
   198                 filteredMBeans = new ArrayList<>(queryMBeans);
       
   199             }
       
   200 
       
   201             JSONObject _links = HttpUtil.getPaginationLinks(exchange, filteredMBeans, pageSize);
       
   202             List<ObjectName> mbeanPage = HttpUtil.filterByPage(exchange, filteredMBeans, pageSize);
       
   203 
       
   204             List<Map<String, String>> items = new ArrayList<>(filteredMBeans.size());
       
   205             for (ObjectName objectName : mbeanPage) {
       
   206                 Map<String, String> item = new LinkedHashMap<>();
       
   207                 item.put("name", objectName.getCanonicalName());
       
   208                 MBeanResource mBeanResource = mBeanResourceMap.get(objectName);
       
   209                 try {
       
   210                     JSONObject mBeanInfo = mBeanResource.getMBeanInfo(mBeanServer, objectName);
       
   211                     JSONElement element = mBeanInfo.get("descriptor");
       
   212                     if (element != null) {
       
   213                         JSONElement element1 = ((JSONObject) element).get("interfaceClassName");
       
   214                         if (element1 != null) {
       
   215                             String intfName = (String) ((JSONPrimitive) element1).getValue();
       
   216                             item.put("interfaceClassName", intfName);
       
   217                         }
       
   218                     }
       
   219                     element = mBeanInfo.get("className");
       
   220                     if (element != null) {
       
   221                         String className = (String) ((JSONPrimitive) element).getValue();
       
   222                         item.put("className", className);
       
   223                     }
       
   224                     element = mBeanInfo.get("description");
       
   225                     if (element != null) {
       
   226                         String description = (String) ((JSONPrimitive) element).getValue();
       
   227                         item.put("description", description);
       
   228                     }
       
   229                     element = mBeanInfo.get("attributeInfo");
       
   230                     if (element != null) {
       
   231                         item.put("attributeCount", ((JSONArray) element).size() + "");
       
   232                     }
       
   233                     element = mBeanInfo.get("operationInfo");
       
   234                     if (element != null) {
       
   235                         item.put("operationCount", ((JSONArray) element).size() + "");
       
   236                     }
       
   237 
       
   238                 } catch (InstanceNotFoundException | IntrospectionException | ReflectionException e) {
       
   239                 }
       
   240 
       
   241                 String href = path + "/" + objectName.toString();
       
   242                 href = HttpUtil.escapeUrl(href);
       
   243                 item.put("href", href);
       
   244                 items.add(item);
       
   245                 String info = HttpUtil.escapeUrl(href + "/info");
       
   246                 item.put("info", info);
       
   247             }
       
   248 
       
   249             Map<String, String> properties = new HashMap<>();
       
   250 
       
   251             properties.put("mbeanCount", Integer.toString(filteredMBeans.size()));
       
   252 
       
   253             JSONMapper typeMapper1 = JSONMappingFactory.INSTANCE.getTypeMapper(items);
       
   254             JSONMapper typeMapper2 = JSONMappingFactory.INSTANCE.getTypeMapper(properties);
       
   255 
       
   256             JSONElement linkElem = typeMapper1.toJsonValue(items);
       
   257             JSONElement propElem = typeMapper2.toJsonValue(properties);
       
   258             JSONObject jobj = new JSONObject();
       
   259 
       
   260             jobj.putAll((JSONObject) propElem);
       
   261             jobj.put("mbeans", linkElem);
       
   262 
       
   263             if (_links != null && !_links.isEmpty()) {
       
   264                 jobj.put("_links", _links);
       
   265             }
       
   266             return new HttpResponse(jobj.toJsonString());
       
   267         } catch (JSONMappingException e) {
       
   268             return HttpResponse.SERVER_ERROR;
       
   269         } catch (UnsupportedEncodingException e) {
       
   270             return HttpResponse.BAD_REQUEST;
       
   271         } catch (MalformedObjectNameException e) {
       
   272             return new HttpResponse(HttpResponse.BAD_REQUEST, "Invalid query string");
       
   273         }
       
   274     }
       
   275 
       
   276     @Override
       
   277     public HttpResponse doPost(HttpExchange exchange) {
       
   278         try {
       
   279             String path = URLDecoder.decode(exchange.getRequestURI().getPath(),StandardCharsets.UTF_8.displayName());
       
   280             String reqBody = null;
       
   281             if (path.matches(pathPrefix + "/?$")) { // POST to current URL
       
   282                 reqBody = HttpUtil.readRequestBody(exchange);
       
   283                 if (reqBody == null || reqBody.isEmpty()) { // No Parameters
       
   284                     return HttpResponse.BAD_REQUEST;
       
   285                 }
       
   286 
       
   287                 JSONParser parser = new JSONParser(reqBody);
       
   288                 JSONElement jsonElement = parser.parse();
       
   289                 if (!(jsonElement instanceof JSONObject)) {
       
   290                     return new HttpResponse(HttpResponse.BAD_REQUEST,
       
   291                             "Invalid parameters : [" + reqBody + "]");
       
   292                 }
       
   293 
       
   294                 JSONObject jsonObject = (JSONObject) jsonElement;
       
   295                 JSONObject normalizedObject = new JSONObject(jsonObject);
       
   296 
       
   297                 // Normalize the input MBean names
       
   298                 for (String objectNameString : jsonObject.keySet()) {
       
   299                     if (!objectNameString.startsWith("?")) { // Ignore object name patterns
       
   300                         JSONElement element = jsonObject.get(objectNameString);
       
   301                         normalizedObject.remove(objectNameString);
       
   302                         normalizedObject.put(new ObjectName(objectNameString).getCanonicalName(), element);
       
   303                     }
       
   304                 }
       
   305 
       
   306                 jsonObject.clear();
       
   307                 jsonObject = normalizedObject;
       
   308 
       
   309                 Set<String> objectNamePatterns = jsonObject.keySet()
       
   310                         .stream()
       
   311                         .filter(a -> a.startsWith("?"))
       
   312                         .collect(Collectors.toSet());
       
   313 
       
   314                 if (!objectNamePatterns.isEmpty()) {
       
   315                     for (String pattern : objectNamePatterns) {
       
   316                         Set<ObjectName> queryMBeans = mBeanServer
       
   317                                 .queryNames(new ObjectName(pattern.substring(1)), null);
       
   318                         queryMBeans.retainAll(allowedMbeans);
       
   319                         JSONElement patternNode = jsonObject.get(pattern);
       
   320                         jsonObject.remove(pattern);
       
   321                         for (ObjectName queryMBean : queryMBeans) {
       
   322                             String name = queryMBean.getCanonicalName();
       
   323                             if (jsonObject.containsKey(name)) {
       
   324                                 JSONObject obj = new JSONObject();
       
   325                                 obj.put(name, patternNode);
       
   326                                 deepMerge(jsonObject, obj);
       
   327                             } else {
       
   328                                 jsonObject.put(name, patternNode);
       
   329                             }
       
   330                         }
       
   331                     }
       
   332                 }
       
   333 
       
   334                 JSONObject result = new JSONObject();
       
   335                 for (String mBeanName : jsonObject.keySet()) {
       
   336                     MBeanResource mBeanResource = mBeanResourceMap.get(new ObjectName(mBeanName));
       
   337                     if (mBeanResource != null) {
       
   338                         JSONElement element = jsonObject.get(mBeanName);
       
   339                         if (element instanceof JSONObject) {
       
   340                             JSONElement res = mBeanResource.handleBulkRequest
       
   341                                     ((JSONObject) element);
       
   342                             result.put(mBeanName, res);
       
   343                         } else {
       
   344                             result.put(mBeanName, "Invalid input");
       
   345                         }
       
   346                     } else {
       
   347                         result.put(mBeanName, "Invalid MBean");
       
   348                     }
       
   349                 }
       
   350                 return new HttpResponse(result.toJsonString());
       
   351             } else {
       
   352                 return HttpResponse.METHOD_NOT_ALLOWED;
       
   353             }
       
   354         } catch (ParseException e) {
       
   355             return new HttpResponse(HttpResponse.BAD_REQUEST, "Invalid JSON String for request body");
       
   356         } catch (IOException e) {
       
   357             return HttpResponse.BAD_REQUEST;
       
   358         } catch (MalformedObjectNameException e) {
       
   359             return new HttpResponse(HttpResponse.BAD_REQUEST, "Invalid query string");
       
   360         }
       
   361     }
       
   362 
       
   363     private JSONObject deepMerge(JSONObject jsonObject1, JSONObject jsonObject2) {
       
   364         for (String key : jsonObject2.keySet()) {
       
   365             if (jsonObject1.containsKey(key)) {
       
   366                 if (jsonObject2.get(key) instanceof JSONObject && jsonObject1.get(key) instanceof JSONObject) {
       
   367                     JSONObject jobj1 = (JSONObject) jsonObject1.get(key);
       
   368                     JSONObject jobj2 = (JSONObject) jsonObject2.get(key);
       
   369                     jsonObject1.put(key, deepMerge(jobj1, jobj2));
       
   370                 } else if (jsonObject2.get(key) instanceof JSONArray && jsonObject1.get(key) instanceof JSONArray) {
       
   371                     JSONArray array1 = (JSONArray) jsonObject1.get(key);
       
   372                     JSONArray array2 = (JSONArray) jsonObject2.get(key);
       
   373                     for (JSONElement each : array2) {
       
   374                         if (!array1.contains(each)) {
       
   375                             array1.add(each);
       
   376                         }
       
   377                     }
       
   378                 } else {
       
   379                     JSONArray array = new JSONArray();
       
   380                     array.add(jsonObject1.get(key));
       
   381                     array.add(jsonObject2.get(key));
       
   382                     jsonObject1.put(key, array);
       
   383                 }
       
   384             } else {
       
   385                 jsonObject1.put(key, jsonObject2.get(key));
       
   386             }
       
   387         }
       
   388         return jsonObject1;
       
   389     }
       
   390 }