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