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