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