package org.jolokia.handler.list; /* * Copyright 2009-2011 Roland Huss * * Licensed 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. */ import java.io.IOException; import java.util.*; import javax.management.*; import org.json.simple.JSONObject; /** * Tree of MBean meta data. This map is a container for one or more MBeanInfo meta data which can be obtained * via a <code>list</code> request. The full structure in its JSON representation looks like below. The amount * of data included can be fine tuned in two ways: * <ul> * <li>With a <code>maxDepth</code> parameter given at construction time, the size of the map can be restricted * (from top down)</li> * <li>A given path select onkly a partial information from the tree</li> * </ul> * Both limiting factors are taken care of when adding the information so that this map doesnt get unnecessarily * to large. * * <pre> * { * <domain> : * { * <prop list> : * { * "attr" : * { * <attr name> : * { * "type" : <attribute type>, * "desc" : <textual description of attribute>, * "rw" : true/false * }, * .... * }, * "op" : * { * <operation name> : * { * "args" : [ * { * "type" : <argument type> * "name" : <argument name> * "desc" : <textual description of argument> * }, * ..... * ], * "ret" : <return type>, * "desc" : <textual description of operation> * }, * ..... * }, * "not" : * { * "name" : <name>, * "desc" : <desc>, * "types" : [ <type1>, <type2> ] * } * }, * .... * }, * .... * } * </pre> * @author roland * @since 13.09.11 */ public class MBeanInfoData { // max depth for map to return private int maxDepth; // stack for an inner path private Stack<String> pathStack; // Map holding information private JSONObject infoMap; // Initialise updaters private static final Map<String,DataUpdater> UPDATERS = new HashMap<String, DataUpdater>(); // How to order keys in Object Names private boolean useCanonicalName; static { for (DataUpdater updater : new DataUpdater[] { new DescriptionDataUpdater(), new ClassNameDataUpdater(), new AttributeDataUpdater(), new OperationDataUpdater(), new NotificationDataUpdater() }) { UPDATERS.put(updater.getKey(),updater); } } /** * Constructor taking a max depth. The <em>max depth</em> specifies how deep the info tree should be build * up. The tree will be truncated if it gets larger than this value. A <em>path</em> (in form of a stack) * can be given, in which only a sub information is (sub-tree or leaf value) is stored * * @param pMaxDepth max depth * @param pPathStack the stack for restricting the information to add. The given stack will be cloned * and is left untouched. * @param pUseCanonicalName whether to use canonical name in listings */ public MBeanInfoData(int pMaxDepth, Stack<String> pPathStack, boolean pUseCanonicalName) { maxDepth = pMaxDepth; useCanonicalName = pUseCanonicalName; pathStack = pPathStack != null ? (Stack<String>) pPathStack.clone() : new Stack<String>(); infoMap = new JSONObject(); } /** * The first two levels of this map (tree) consist of the MBean's domain name and name properties, which are * independent of an MBean's meta data. If the max depth given at construction time is less or equals than 2 (and * no inner path into the map is given), then a client of this map does not need to query the MBeanServer for * MBeanInfo meta data. * <p></p> * This method checks this condition and returns true if this is the case. As side effect it will update this * map with the name part extracted from the given object name * * @param pName the objectname used for the first two levels * @return true if the object name has been added. */ public boolean handleFirstOrSecondLevel(ObjectName pName) { if (maxDepth == 1 && pathStack.size() == 0) { // Only add domain names with a dummy value if max depth is restricted to 1 // But only when used without path infoMap.put(pName.getDomain(), 1); return true; } else if (maxDepth == 2 && pathStack.size() == 0) { // Add domain an object name into the map, final value is a dummy value JSONObject mBeansMap = getOrCreateJSONObject(infoMap, pName.getDomain()); mBeansMap.put(getKeyPropertyString(pName),1); return true; } return false; } private String getKeyPropertyString(ObjectName pName) { return useCanonicalName ? pName.getCanonicalKeyPropertyListString() : pName.getKeyPropertyListString(); } /** * Add information about an MBean as obtained from an {@link MBeanInfo} descriptor. The information added * can be restricted by a given path (which has already be prepared as a stack). Also, a max depth as given in the * constructor restricts the size of the map from the top. * * @param mBeanInfo the MBean info * @param pName the object name of the MBean */ public void addMBeanInfo(MBeanInfo mBeanInfo, ObjectName pName) throws InstanceNotFoundException, IntrospectionException, ReflectionException, IOException { JSONObject mBeansMap = getOrCreateJSONObject(infoMap, pName.getDomain()); JSONObject mBeanMap = getOrCreateJSONObject(mBeansMap, getKeyPropertyString(pName)); // Trim down stack to get rid of domain/property list Stack<String> stack = truncatePathStack(2); if (stack.empty()) { addFullMBeanInfo(mBeanMap, mBeanInfo); } else { addPartialMBeanInfo(mBeanMap, mBeanInfo,stack); } // Trim if required if (mBeanMap.size() == 0) { mBeansMap.remove(getKeyPropertyString(pName)); if (mBeansMap.size() == 0) { infoMap.remove(pName.getDomain()); } } } /** * Add an exception which occurred during extraction of an {@link MBeanInfo} for * a certain {@link ObjectName} to this map. * * @param pName MBean name for which the error occurred * @param pExp exception occurred * @throws IOException if this method decides to rethrow the execption */ public void handleException(ObjectName pName, IOException pExp) throws IOException { // In case of a remote call, IOException can occur e.g. for // NonSerializableExceptions if (pathStack.size() == 0) { addException(pName, pExp); } else { // Happens for a deeper request, i.e with a path pointing directly into an MBean, // Hence we throw immediately an error here since there will be only this exception // and no extra info throw new IOException("IOException for MBean " + pName + " (" + pExp.getMessage() + ")",pExp); } } /** * Add an exception which occurred during extraction of an {@link MBeanInfo} for * a certain {@link ObjectName} to this map. * * @param pName MBean name for which the error occurred * @param pExp exception occurred * @throws IllegalStateException if this method decides to rethrow the exception */ public void handleException(ObjectName pName, IllegalStateException pExp) { // This happen happens for JBoss 7.1 in some cases. if (pathStack.size() == 0) { addException(pName, pExp); } else { throw new IllegalStateException("IllegalStateException for MBean " + pName + " (" + pExp.getMessage() + ")",pExp); } } /** * Add an exception which occurred during extraction of an {@link MBeanInfo} for * a certain {@link ObjectName} to this map. * * @param pName MBean name for which the error occurred * @param pExp exception occurred * @throws IllegalStateException if this method decides to rethrow the exception */ public void handleException(ObjectName pName, InstanceNotFoundException pExp) throws InstanceNotFoundException { // This happen happens for JBoss 7.1 in some cases (i.e. ResourceAdapterModule) if (pathStack.size() == 0) { addException(pName, pExp); } else { throw new InstanceNotFoundException("InstanceNotFoundException for MBean " + pName + " (" + pExp.getMessage() + ")"); } } // Add an exception to the info map private void addException(ObjectName pName, Exception pExp) { JSONObject mBeansMap = getOrCreateJSONObject(infoMap, pName.getDomain()); JSONObject mBeanMap = getOrCreateJSONObject(mBeansMap, getKeyPropertyString(pName)); mBeanMap.put(DataKeys.ERROR.getKey(), pExp.toString()); } /** * Extract either a sub tree or a leaf value. If a path is used, then adding MBeanInfos has added them * as if no path were given (i.e. in it original place in the tree) but leaves out other information * not included by the path. This method then moves up the part pointed to by the path to the top of the * tree hierarchy. It also takes into account the maximum depth of the tree and truncates below * * @return either a Map for a subtree or the leaf value as an object */ public Object truncate() { Object value = navigatePath(); if (maxDepth == 0) { return value; } if (! (value instanceof JSONObject)) { return value; } else { // Truncate all levels below return truncateJSONObject((JSONObject) value, maxDepth); } } // ===================================================================================================== private void addFullMBeanInfo(JSONObject pMBeanMap, MBeanInfo pMBeanInfo) { for (DataUpdater updater : UPDATERS.values()) { updater.update(pMBeanMap,pMBeanInfo,null); } } private void addPartialMBeanInfo(JSONObject pMBeanMap, MBeanInfo pMBeanInfo, Stack<String> pPathStack) { String what = pPathStack.empty() ? null : pPathStack.pop(); DataUpdater updater = UPDATERS.get(what); if (updater != null) { updater.update(pMBeanMap, pMBeanInfo, pPathStack); } else { throw new IllegalArgumentException("Illegal path element " + what); } } private JSONObject getOrCreateJSONObject(JSONObject pMap, String pKey) { JSONObject nMap = (JSONObject) pMap.get(pKey); if (nMap == null) { nMap = new JSONObject(); pMap.put(pKey, nMap); } return nMap; } private Object truncateJSONObject(JSONObject pValue, int pMaxDepth) { if (pMaxDepth == 0) { return 1; } JSONObject ret = new JSONObject(); Set<Map.Entry> entries = pValue.entrySet(); for (Map.Entry entry : entries) { Object value = entry.getValue(); Object key = entry.getKey(); if (value instanceof JSONObject) { ret.put(key, truncateJSONObject((JSONObject) value, pMaxDepth - 1)); } else { ret.put(key,value); } } return ret; } // Trim down the stack by some value or return an empty stack private Stack<String> truncatePathStack(int pLevel) { if (pathStack.size() < pLevel) { return new Stack<String>(); } else { // Trim of domain and MBean properties // pathStack gets cloned here since the processing will eat it up Stack<String> ret = (Stack<String>) pathStack.clone(); for (int i = 0;i < pLevel;i++) { ret.pop(); } return ret; } } // Navigate to sub map or leaf value private Object navigatePath() { int size = pathStack.size(); JSONObject innerMap = infoMap; while (size > 0) { Collection vals = innerMap.values(); if (vals.size() == 0) { return innerMap; } else if (vals.size() != 1) { throw new IllegalStateException("Internal: More than one key found when extracting with path: " + vals); } Object value = vals.iterator().next(); // End leaf, return it .... if (size == 1) { return value; } // Dive in deeper ... if (!(value instanceof JSONObject)) { throw new IllegalStateException("Internal: Value within path extraction must be a Map, not " + value.getClass()); } innerMap = (JSONObject) value; --size; } return innerMap; } }