|
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 } |