001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.camel.management;
018    
019    import java.lang.reflect.Method;
020    import java.lang.reflect.Proxy;
021    import java.util.LinkedHashMap;
022    import java.util.LinkedHashSet;
023    import java.util.Map;
024    import java.util.Set;
025    import javax.management.Descriptor;
026    import javax.management.IntrospectionException;
027    import javax.management.JMException;
028    import javax.management.modelmbean.ModelMBeanAttributeInfo;
029    import javax.management.modelmbean.ModelMBeanInfo;
030    import javax.management.modelmbean.ModelMBeanInfoSupport;
031    import javax.management.modelmbean.ModelMBeanNotificationInfo;
032    import javax.management.modelmbean.ModelMBeanOperationInfo;
033    
034    import org.apache.camel.CamelContext;
035    import org.apache.camel.Service;
036    import org.apache.camel.api.management.ManagedAttribute;
037    import org.apache.camel.api.management.ManagedNotification;
038    import org.apache.camel.api.management.ManagedNotifications;
039    import org.apache.camel.api.management.ManagedOperation;
040    import org.apache.camel.api.management.ManagedResource;
041    import org.apache.camel.util.IntrospectionSupport;
042    import org.apache.camel.util.LRUCache;
043    import org.apache.camel.util.LRUWeakCache;
044    import org.apache.camel.util.ObjectHelper;
045    import org.slf4j.Logger;
046    import org.slf4j.LoggerFactory;
047    
048    /**
049     * A Camel specific {@link javax.management.MBeanInfo} assembler that reads the
050     * details from the {@link ManagedResource}, {@link ManagedAttribute}, {@link ManagedOperation},
051     * {@link ManagedNotification}, and {@link ManagedNotifications} annotations.
052     */
053    public class MBeanInfoAssembler implements Service {
054    
055        private static final Logger LOG = LoggerFactory.getLogger(MBeanInfoAssembler.class);
056    
057        // use a cache to speedup gathering JMX MBeanInfo for known classes
058        // use a weak cache as we dont want the cache to keep around as it reference classes
059        // which could prevent classloader to unload classes if being referenced from this cache
060        private final LRUCache<Class<?>, MBeanAttributesAndOperations> cache = new LRUWeakCache<Class<?>, MBeanAttributesAndOperations>(1000);
061    
062        private final CamelContext camelContext;
063    
064        public MBeanInfoAssembler(CamelContext camelContext) {
065            this.camelContext = camelContext;
066        }
067    
068        @Override
069        public void start() throws Exception {
070            // noop
071        }
072    
073        @Override
074        public void stop() throws Exception {
075            if (LOG.isDebugEnabled()) {
076                LOG.debug("Clearing cache[size={}, hits={}, misses={}, evicted={}]", new Object[]{cache.size(), cache.getHits(), cache.getMisses(), cache.getEvicted()});
077            }
078            cache.clear();
079        }
080    
081        /**
082         * Structure to hold cached mbean attributes and operations for a given class.
083         */
084        private static final class MBeanAttributesAndOperations {
085            private Map<String, ManagedAttributeInfo> attributes;
086            private Set<ManagedOperationInfo> operations;
087        }
088    
089        /**
090         * Gets the {@link ModelMBeanInfo} for the given managed bean
091         *
092         * @param defaultManagedBean  the default managed bean
093         * @param customManagedBean   an optional custom managed bean
094         * @param objectName   the object name
095         * @return the model info, or <tt>null</tt> if not possible to create, for example due the managed bean is a proxy class
096         * @throws JMException is thrown if error creating the model info
097         */
098        public ModelMBeanInfo getMBeanInfo(Object defaultManagedBean, Object customManagedBean, String objectName) throws JMException {
099            // skip proxy classes
100            if (defaultManagedBean != null && Proxy.isProxyClass(defaultManagedBean.getClass())) {
101                LOG.trace("Skip creating ModelMBeanInfo due proxy class {}", defaultManagedBean.getClass());
102                return null;
103            }
104    
105            // maps and lists to contain information about attributes and operations
106            Map<String, ManagedAttributeInfo> attributes = new LinkedHashMap<String, ManagedAttributeInfo>();
107            Set<ManagedOperationInfo> operations = new LinkedHashSet<ManagedOperationInfo>();
108            Set<ModelMBeanAttributeInfo> mBeanAttributes = new LinkedHashSet<ModelMBeanAttributeInfo>();
109            Set<ModelMBeanOperationInfo> mBeanOperations = new LinkedHashSet<ModelMBeanOperationInfo>();
110            Set<ModelMBeanNotificationInfo> mBeanNotifications = new LinkedHashSet<ModelMBeanNotificationInfo>();
111    
112            // extract details from default managed bean
113            if (defaultManagedBean != null) {
114                extractAttributesAndOperations(defaultManagedBean.getClass(), attributes, operations);
115                extractMbeanAttributes(defaultManagedBean, attributes, mBeanAttributes, mBeanOperations);
116                extractMbeanOperations(defaultManagedBean, operations, mBeanOperations);
117                extractMbeanNotifications(defaultManagedBean, mBeanNotifications);
118            }
119    
120            // extract details from custom managed bean
121            if (customManagedBean != null) {
122                extractAttributesAndOperations(customManagedBean.getClass(), attributes, operations);
123                extractMbeanAttributes(customManagedBean, attributes, mBeanAttributes, mBeanOperations);
124                extractMbeanOperations(customManagedBean, operations, mBeanOperations);
125                extractMbeanNotifications(customManagedBean, mBeanNotifications);
126            }
127    
128            // create the ModelMBeanInfo
129            String name = getName(customManagedBean != null ? customManagedBean : defaultManagedBean, objectName);
130            String description = getDescription(customManagedBean != null ? customManagedBean : defaultManagedBean, objectName);
131            ModelMBeanAttributeInfo[] arrayAttributes = mBeanAttributes.toArray(new ModelMBeanAttributeInfo[mBeanAttributes.size()]);
132            ModelMBeanOperationInfo[] arrayOperations = mBeanOperations.toArray(new ModelMBeanOperationInfo[mBeanOperations.size()]);
133            ModelMBeanNotificationInfo[] arrayNotifications = mBeanNotifications.toArray(new ModelMBeanNotificationInfo[mBeanNotifications.size()]);
134    
135            ModelMBeanInfo info = new ModelMBeanInfoSupport(name, description, arrayAttributes, null, arrayOperations, arrayNotifications);
136            LOG.trace("Created ModelMBeanInfo {}", info);
137            return info;
138        }
139    
140        private void extractAttributesAndOperations(Class<?> managedClass, Map<String, ManagedAttributeInfo> attributes, Set<ManagedOperationInfo> operations) {
141            MBeanAttributesAndOperations cached = cache.get(managedClass);
142            if (cached == null) {
143                doExtractAttributesAndOperations(managedClass, attributes, operations);
144                cached = new MBeanAttributesAndOperations();
145                cached.attributes = new LinkedHashMap<String, ManagedAttributeInfo>(attributes);
146                cached.operations = new LinkedHashSet<ManagedOperationInfo>(operations);
147    
148                // clear before we re-add them
149                attributes.clear();
150                operations.clear();
151    
152                // add to cache
153                cache.put(managedClass, cached);
154            }
155    
156            attributes.putAll(cached.attributes);
157            operations.addAll(cached.operations);
158        }
159    
160        private void doExtractAttributesAndOperations(Class<?> managedClass, Map<String, ManagedAttributeInfo> attributes, Set<ManagedOperationInfo> operations) {
161            // extract the class
162            doDoExtractAttributesAndOperations(managedClass, attributes, operations);
163    
164            // and then any sub classes
165            if (managedClass.getSuperclass() != null) {
166                Class<?> clazz = managedClass.getSuperclass();
167                // skip any JDK classes
168                if (!clazz.getName().startsWith("java")) {
169                    LOG.trace("Extracting attributes and operations from sub class: {}", clazz);
170                    doExtractAttributesAndOperations(clazz, attributes, operations);
171                }
172            }
173    
174            // and then any additional interfaces (as interfaces can be annotated as well)
175            if (managedClass.getInterfaces() != null) {
176                for (Class<?> clazz : managedClass.getInterfaces()) {
177                    // recursive as there may be multiple interfaces
178                    if (clazz.getName().startsWith("java")) {
179                        // skip any JDK classes
180                        continue;
181                    }
182                    LOG.trace("Extracting attributes and operations from implemented interface: {}", clazz);
183                    doExtractAttributesAndOperations(clazz, attributes, operations);
184                }
185            }
186        }
187    
188        private void doDoExtractAttributesAndOperations(Class<?> managedClass, Map<String, ManagedAttributeInfo> attributes, Set<ManagedOperationInfo> operations) {
189            LOG.trace("Extracting attributes and operations from class: {}", managedClass);
190    
191            // introspect the class, and leverage the cache to have better performance
192            IntrospectionSupport.ClassInfo cache = IntrospectionSupport.cacheClass(managedClass);
193    
194            for (IntrospectionSupport.MethodInfo cacheInfo : cache.methods) {
195                // must be from declaring class
196                if (cacheInfo.method.getDeclaringClass() != managedClass) {
197                    continue;
198                }
199    
200                LOG.trace("Extracting attributes and operations from method: {}", cacheInfo.method);
201                ManagedAttribute ma = cacheInfo.method.getAnnotation(ManagedAttribute.class);
202                if (ma != null) {
203                    String key;
204                    String desc = ma.description();
205                    Method getter = null;
206                    Method setter = null;
207                    boolean mask = ma.mask();
208    
209                    if (cacheInfo.isGetter) {
210                        key = cacheInfo.getterOrSetterShorthandName;
211                        getter = cacheInfo.method;
212                    } else if (cacheInfo.isSetter) {
213                        key = cacheInfo.getterOrSetterShorthandName;
214                        setter = cacheInfo.method;
215                    } else {
216                        throw new IllegalArgumentException("@ManagedAttribute can only be used on Java bean methods, was: " + cacheInfo.method + " on bean: " + managedClass);
217                    }
218    
219                    // they key must be capitalized
220                    key = ObjectHelper.capitalize(key);
221    
222                    // lookup first
223                    ManagedAttributeInfo info = attributes.get(key);
224                    if (info == null) {
225                        info = new ManagedAttributeInfo(key, desc);
226                    }
227                    if (getter != null) {
228                        info.setGetter(getter);
229                    }
230                    if (setter != null) {
231                        info.setSetter(setter);
232                    }
233                    info.setMask(mask);
234    
235                    attributes.put(key, info);
236                }
237    
238                // operations
239                ManagedOperation mo = cacheInfo.method.getAnnotation(ManagedOperation.class);
240                if (mo != null) {
241                    String desc = mo.description();
242                    Method operation = cacheInfo.method;
243                    boolean mask = mo.mask();
244                    operations.add(new ManagedOperationInfo(desc, operation, mask));
245                }
246            }
247        }
248    
249        private void extractMbeanAttributes(Object managedBean, Map<String, ManagedAttributeInfo> attributes,
250                                            Set<ModelMBeanAttributeInfo> mBeanAttributes, Set<ModelMBeanOperationInfo> mBeanOperations) throws IntrospectionException {
251    
252            for (ManagedAttributeInfo info : attributes.values()) {
253                ModelMBeanAttributeInfo mbeanAttribute = new ModelMBeanAttributeInfo(info.getKey(), info.getDescription(), info.getGetter(), info.getSetter());
254    
255                // add missing attribute descriptors, this is needed to have attributes accessible
256                Descriptor desc = mbeanAttribute.getDescriptor();
257    
258                desc.setField("mask", info.isMask() ? "true" : "false");
259                if (info.getGetter() != null) {
260                    desc.setField("getMethod", info.getGetter().getName());
261                    // attribute must also be added as mbean operation
262                    ModelMBeanOperationInfo mbeanOperation = new ModelMBeanOperationInfo(info.getKey(), info.getGetter());
263                    Descriptor opDesc = mbeanOperation.getDescriptor();
264                    opDesc.setField("mask", info.isMask() ? "true" : "false");
265                    mbeanOperation.setDescriptor(opDesc);
266                    mBeanOperations.add(mbeanOperation);
267                }
268                if (info.getSetter() != null) {
269                    desc.setField("setMethod", info.getSetter().getName());
270                    // attribute must also be added as mbean operation
271                    ModelMBeanOperationInfo mbeanOperation = new ModelMBeanOperationInfo(info.getKey(), info.getSetter());
272                    mBeanOperations.add(mbeanOperation);
273                }
274                mbeanAttribute.setDescriptor(desc);
275    
276                mBeanAttributes.add(mbeanAttribute);
277                LOG.trace("Assembled attribute: {}", mbeanAttribute);
278            }
279        }
280    
281        private void extractMbeanOperations(Object managedBean, Set<ManagedOperationInfo> operations, Set<ModelMBeanOperationInfo> mBeanOperations) {
282            for (ManagedOperationInfo info : operations) {
283                ModelMBeanOperationInfo mbean = new ModelMBeanOperationInfo(info.getDescription(), info.getOperation());
284                Descriptor opDesc = mbean.getDescriptor();
285                opDesc.setField("mask", info.isMask() ? "true" : "false");
286                mbean.setDescriptor(opDesc);
287                mBeanOperations.add(mbean);
288                LOG.trace("Assembled operation: {}", mbean);
289            }
290        }
291    
292        private void extractMbeanNotifications(Object managedBean, Set<ModelMBeanNotificationInfo> mBeanNotifications) {
293            ManagedNotifications notifications = managedBean.getClass().getAnnotation(ManagedNotifications.class);
294            if (notifications != null) {
295                for (ManagedNotification notification : notifications.value()) {
296                    ModelMBeanNotificationInfo info = new ModelMBeanNotificationInfo(notification.notificationTypes(), notification.name(), notification.description());
297                    mBeanNotifications.add(info);
298                    LOG.trace("Assembled notification: {}", info);
299                }
300            }
301        }
302    
303        private String getDescription(Object managedBean, String objectName) {
304            ManagedResource mr = ObjectHelper.getAnnotation(managedBean, ManagedResource.class);
305            return mr != null ? mr.description() : "";
306        }
307    
308        private String getName(Object managedBean, String objectName) {
309            return managedBean.getClass().getName();
310        }
311    
312        private static final class ManagedAttributeInfo {
313            private String key;
314            private String description;
315            private Method getter;
316            private Method setter;
317            private boolean mask;
318    
319            private ManagedAttributeInfo(String key, String description) {
320                this.key = key;
321                this.description = description;
322            }
323    
324            public String getKey() {
325                return key;
326            }
327    
328            public String getDescription() {
329                return description;
330            }
331    
332            public Method getGetter() {
333                return getter;
334            }
335    
336            public void setGetter(Method getter) {
337                this.getter = getter;
338            }
339    
340            public Method getSetter() {
341                return setter;
342            }
343    
344            public void setSetter(Method setter) {
345                this.setter = setter;
346            }
347    
348            public boolean isMask() {
349                return mask;
350            }
351    
352            public void setMask(boolean mask) {
353                this.mask = mask;
354            }
355    
356            @Override
357            public String toString() {
358                return "ManagedAttributeInfo: [" + key + " + getter: " + getter + ", setter: " + setter + "]";
359            }
360        }
361    
362        private static final class ManagedOperationInfo {
363            private final String description;
364            private final Method operation;
365            private final boolean mask;
366    
367            private ManagedOperationInfo(String description, Method operation, boolean mask) {
368                this.description = description;
369                this.operation = operation;
370                this.mask = mask;
371            }
372    
373            public String getDescription() {
374                return description;
375            }
376    
377            public Method getOperation() {
378                return operation;
379            }
380    
381            public boolean isMask() {
382                return mask;
383            }
384    
385            @Override
386            public String toString() {
387                return "ManagedOperationInfo: [" + operation + "]";
388            }
389        }
390    
391    }