001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.jmx.export.assembler;
018
019import java.beans.PropertyDescriptor;
020import java.lang.reflect.Method;
021import javax.management.Descriptor;
022import javax.management.MBeanParameterInfo;
023import javax.management.modelmbean.ModelMBeanNotificationInfo;
024
025import org.springframework.aop.support.AopUtils;
026import org.springframework.beans.BeanUtils;
027import org.springframework.beans.factory.InitializingBean;
028import org.springframework.jmx.export.metadata.InvalidMetadataException;
029import org.springframework.jmx.export.metadata.JmxAttributeSource;
030import org.springframework.jmx.export.metadata.JmxMetadataUtils;
031import org.springframework.jmx.export.metadata.ManagedAttribute;
032import org.springframework.jmx.export.metadata.ManagedMetric;
033import org.springframework.jmx.export.metadata.ManagedNotification;
034import org.springframework.jmx.export.metadata.ManagedOperation;
035import org.springframework.jmx.export.metadata.ManagedOperationParameter;
036import org.springframework.jmx.export.metadata.ManagedResource;
037import org.springframework.util.Assert;
038import org.springframework.util.ObjectUtils;
039import org.springframework.util.StringUtils;
040
041/**
042 * Implementation of the {@link MBeanInfoAssembler} interface that reads
043 * the management interface information from source level metadata.
044 *
045 * <p>Uses the {@link JmxAttributeSource} strategy interface, so that
046 * metadata can be read using any supported implementation. Out of the box,
047 * Spring provides an implementation based on annotations:
048 * {@code AnnotationJmxAttributeSource}.
049 *
050 * @author Rob Harrop
051 * @author Juergen Hoeller
052 * @author Jennifer Hickey
053 * @since 1.2
054 * @see #setAttributeSource
055 * @see org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource
056 */
057public class MetadataMBeanInfoAssembler extends AbstractReflectiveMBeanInfoAssembler
058                implements AutodetectCapableMBeanInfoAssembler, InitializingBean {
059
060        private JmxAttributeSource attributeSource;
061
062
063        /**
064         * Create a new {@code MetadataMBeanInfoAssembler} which needs to be
065         * configured through the {@link #setAttributeSource} method.
066         */
067        public MetadataMBeanInfoAssembler() {
068        }
069
070        /**
071         * Create a new {@code MetadataMBeanInfoAssembler} for the given
072         * {@code JmxAttributeSource}.
073         * @param attributeSource the JmxAttributeSource to use
074         */
075        public MetadataMBeanInfoAssembler(JmxAttributeSource attributeSource) {
076                Assert.notNull(attributeSource, "JmxAttributeSource must not be null");
077                this.attributeSource = attributeSource;
078        }
079
080
081        /**
082         * Set the {@code JmxAttributeSource} implementation to use for
083         * reading the metadata from the bean class.
084         * @see org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource
085         */
086        public void setAttributeSource(JmxAttributeSource attributeSource) {
087                Assert.notNull(attributeSource, "JmxAttributeSource must not be null");
088                this.attributeSource = attributeSource;
089        }
090
091        @Override
092        public void afterPropertiesSet() {
093                if (this.attributeSource == null) {
094                        throw new IllegalArgumentException("Property 'attributeSource' is required");
095                }
096        }
097
098
099        /**
100         * Throws an IllegalArgumentException if it encounters a JDK dynamic proxy.
101         * Metadata can only be read from target classes and CGLIB proxies!
102         */
103        @Override
104        protected void checkManagedBean(Object managedBean) throws IllegalArgumentException {
105                if (AopUtils.isJdkDynamicProxy(managedBean)) {
106                        throw new IllegalArgumentException(
107                                        "MetadataMBeanInfoAssembler does not support JDK dynamic proxies - " +
108                                        "export the target beans directly or use CGLIB proxies instead");
109                }
110        }
111
112        /**
113         * Used for autodetection of beans. Checks to see if the bean's class has a
114         * {@code ManagedResource} attribute. If so it will add it list of included beans.
115         * @param beanClass the class of the bean
116         * @param beanName the name of the bean in the bean factory
117         */
118        @Override
119        public boolean includeBean(Class<?> beanClass, String beanName) {
120                return (this.attributeSource.getManagedResource(getClassToExpose(beanClass)) != null);
121        }
122
123        /**
124         * Vote on the inclusion of an attribute accessor.
125         * @param method the accessor method
126         * @param beanKey the key associated with the MBean in the beans map
127         * @return whether the method has the appropriate metadata
128         */
129        @Override
130        protected boolean includeReadAttribute(Method method, String beanKey) {
131                return hasManagedAttribute(method) || hasManagedMetric(method);
132        }
133
134        /**
135         * Votes on the inclusion of an attribute mutator.
136         * @param method the mutator method
137         * @param beanKey the key associated with the MBean in the beans map
138         * @return whether the method has the appropriate metadata
139         */
140        @Override
141        protected boolean includeWriteAttribute(Method method, String beanKey) {
142                return hasManagedAttribute(method);
143        }
144
145        /**
146         * Votes on the inclusion of an operation.
147         * @param method the operation method
148         * @param beanKey the key associated with the MBean in the beans map
149         * @return whether the method has the appropriate metadata
150         */
151        @Override
152        protected boolean includeOperation(Method method, String beanKey) {
153                PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method);
154                if (pd != null) {
155                        if (hasManagedAttribute(method)) {
156                                return true;
157                        }
158                }
159                return hasManagedOperation(method);
160        }
161
162        /**
163         * Checks to see if the given Method has the {@code ManagedAttribute} attribute.
164         */
165        private boolean hasManagedAttribute(Method method) {
166                return (this.attributeSource.getManagedAttribute(method) != null);
167        }
168
169        /**
170         * Checks to see if the given Method has the {@code ManagedMetric} attribute.
171         */
172        private boolean hasManagedMetric(Method method) {
173                return (this.attributeSource.getManagedMetric(method) != null);
174        }
175
176        /**
177         * Checks to see if the given Method has the {@code ManagedOperation} attribute.
178         * @param method the method to check
179         */
180        private boolean hasManagedOperation(Method method) {
181                return (this.attributeSource.getManagedOperation(method) != null);
182        }
183
184
185        /**
186         * Reads managed resource description from the source level metadata.
187         * Returns an empty {@code String} if no description can be found.
188         */
189        @Override
190        protected String getDescription(Object managedBean, String beanKey) {
191                ManagedResource mr = this.attributeSource.getManagedResource(getClassToExpose(managedBean));
192                return (mr != null ? mr.getDescription() : "");
193        }
194
195        /**
196         * Creates a description for the attribute corresponding to this property
197         * descriptor. Attempts to create the description using metadata from either
198         * the getter or setter attributes, otherwise uses the property name.
199         */
200        @Override
201        protected String getAttributeDescription(PropertyDescriptor propertyDescriptor, String beanKey) {
202                Method readMethod = propertyDescriptor.getReadMethod();
203                Method writeMethod = propertyDescriptor.getWriteMethod();
204
205                ManagedAttribute getter =
206                                (readMethod != null ? this.attributeSource.getManagedAttribute(readMethod) : null);
207                ManagedAttribute setter =
208                                (writeMethod != null ? this.attributeSource.getManagedAttribute(writeMethod) : null);
209
210                if (getter != null && StringUtils.hasText(getter.getDescription())) {
211                        return getter.getDescription();
212                }
213                else if (setter != null && StringUtils.hasText(setter.getDescription())) {
214                        return setter.getDescription();
215                }
216
217                ManagedMetric metric = (readMethod != null ? this.attributeSource.getManagedMetric(readMethod) : null);
218                if (metric != null && StringUtils.hasText(metric.getDescription())) {
219                        return metric.getDescription();
220                }
221
222                return propertyDescriptor.getDisplayName();
223        }
224
225        /**
226         * Retrieves the description for the supplied {@code Method} from the
227         * metadata. Uses the method name is no description is present in the metadata.
228         */
229        @Override
230        protected String getOperationDescription(Method method, String beanKey) {
231                PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method);
232                if (pd != null) {
233                        ManagedAttribute ma = this.attributeSource.getManagedAttribute(method);
234                        if (ma != null && StringUtils.hasText(ma.getDescription())) {
235                                return ma.getDescription();
236                        }
237                        ManagedMetric metric = this.attributeSource.getManagedMetric(method);
238                        if (metric != null && StringUtils.hasText(metric.getDescription())) {
239                                return metric.getDescription();
240                        }
241                        return method.getName();
242                }
243                else {
244                        ManagedOperation mo = this.attributeSource.getManagedOperation(method);
245                        if (mo != null && StringUtils.hasText(mo.getDescription())) {
246                                return mo.getDescription();
247                        }
248                        return method.getName();
249                }
250        }
251
252        /**
253         * Reads {@code MBeanParameterInfo} from the {@code ManagedOperationParameter}
254         * attributes attached to a method. Returns an empty array of {@code MBeanParameterInfo}
255         * if no attributes are found.
256         */
257        @Override
258        protected MBeanParameterInfo[] getOperationParameters(Method method, String beanKey) {
259                ManagedOperationParameter[] params = this.attributeSource.getManagedOperationParameters(method);
260                if (ObjectUtils.isEmpty(params)) {
261                        return super.getOperationParameters(method, beanKey);
262                }
263
264                MBeanParameterInfo[] parameterInfo = new MBeanParameterInfo[params.length];
265                Class<?>[] methodParameters = method.getParameterTypes();
266                for (int i = 0; i < params.length; i++) {
267                        ManagedOperationParameter param = params[i];
268                        parameterInfo[i] =
269                                        new MBeanParameterInfo(param.getName(), methodParameters[i].getName(), param.getDescription());
270                }
271                return parameterInfo;
272        }
273
274        /**
275         * Reads the {@link ManagedNotification} metadata from the {@code Class} of the managed resource
276         * and generates and returns the corresponding {@link ModelMBeanNotificationInfo} metadata.
277         */
278        @Override
279        protected ModelMBeanNotificationInfo[] getNotificationInfo(Object managedBean, String beanKey) {
280                ManagedNotification[] notificationAttributes =
281                                this.attributeSource.getManagedNotifications(getClassToExpose(managedBean));
282                ModelMBeanNotificationInfo[] notificationInfos =
283                                new ModelMBeanNotificationInfo[notificationAttributes.length];
284
285                for (int i = 0; i < notificationAttributes.length; i++) {
286                        ManagedNotification attribute = notificationAttributes[i];
287                        notificationInfos[i] = JmxMetadataUtils.convertToModelMBeanNotificationInfo(attribute);
288                }
289
290                return notificationInfos;
291        }
292
293        /**
294         * Adds descriptor fields from the {@code ManagedResource} attribute
295         * to the MBean descriptor. Specifically, adds the {@code currencyTimeLimit},
296         * {@code persistPolicy}, {@code persistPeriod}, {@code persistLocation}
297         * and {@code persistName} descriptor fields if they are present in the metadata.
298         */
299        @Override
300        protected void populateMBeanDescriptor(Descriptor desc, Object managedBean, String beanKey) {
301                ManagedResource mr = this.attributeSource.getManagedResource(getClassToExpose(managedBean));
302                if (mr == null) {
303                        throw new InvalidMetadataException(
304                                        "No ManagedResource attribute found for class: " + getClassToExpose(managedBean));
305                }
306
307                applyCurrencyTimeLimit(desc, mr.getCurrencyTimeLimit());
308
309                if (mr.isLog()) {
310                        desc.setField(FIELD_LOG, "true");
311                }
312                if (StringUtils.hasLength(mr.getLogFile())) {
313                        desc.setField(FIELD_LOG_FILE, mr.getLogFile());
314                }
315
316                if (StringUtils.hasLength(mr.getPersistPolicy())) {
317                        desc.setField(FIELD_PERSIST_POLICY, mr.getPersistPolicy());
318                }
319                if (mr.getPersistPeriod() >= 0) {
320                        desc.setField(FIELD_PERSIST_PERIOD, Integer.toString(mr.getPersistPeriod()));
321                }
322                if (StringUtils.hasLength(mr.getPersistName())) {
323                        desc.setField(FIELD_PERSIST_NAME, mr.getPersistName());
324                }
325                if (StringUtils.hasLength(mr.getPersistLocation())) {
326                        desc.setField(FIELD_PERSIST_LOCATION, mr.getPersistLocation());
327                }
328        }
329
330        /**
331         * Adds descriptor fields from the {@code ManagedAttribute} attribute or the {@code ManagedMetric} attribute
332         * to the attribute descriptor.
333         */
334        @Override
335        protected void populateAttributeDescriptor(Descriptor desc, Method getter, Method setter, String beanKey) {
336                if (getter != null && hasManagedMetric(getter)) {
337                        populateMetricDescriptor(desc, this.attributeSource.getManagedMetric(getter));
338                }
339                else {
340                        ManagedAttribute gma =
341                                        (getter == null) ? ManagedAttribute.EMPTY : this.attributeSource.getManagedAttribute(getter);
342                        ManagedAttribute sma =
343                                        (setter == null) ? ManagedAttribute.EMPTY : this.attributeSource.getManagedAttribute(setter);
344                        populateAttributeDescriptor(desc,gma,sma);
345                }
346        }
347
348        private void populateAttributeDescriptor(Descriptor desc, ManagedAttribute gma, ManagedAttribute sma) {
349                applyCurrencyTimeLimit(desc, resolveIntDescriptor(gma.getCurrencyTimeLimit(), sma.getCurrencyTimeLimit()));
350
351                Object defaultValue = resolveObjectDescriptor(gma.getDefaultValue(), sma.getDefaultValue());
352                desc.setField(FIELD_DEFAULT, defaultValue);
353
354                String persistPolicy = resolveStringDescriptor(gma.getPersistPolicy(), sma.getPersistPolicy());
355                if (StringUtils.hasLength(persistPolicy)) {
356                        desc.setField(FIELD_PERSIST_POLICY, persistPolicy);
357                }
358                int persistPeriod = resolveIntDescriptor(gma.getPersistPeriod(), sma.getPersistPeriod());
359                if (persistPeriod >= 0) {
360                        desc.setField(FIELD_PERSIST_PERIOD, Integer.toString(persistPeriod));
361                }
362        }
363
364        private void populateMetricDescriptor(Descriptor desc, ManagedMetric metric) {
365                applyCurrencyTimeLimit(desc, metric.getCurrencyTimeLimit());
366
367                if (StringUtils.hasLength(metric.getPersistPolicy())) {
368                        desc.setField(FIELD_PERSIST_POLICY, metric.getPersistPolicy());
369                }
370                if (metric.getPersistPeriod() >= 0) {
371                        desc.setField(FIELD_PERSIST_PERIOD, Integer.toString(metric.getPersistPeriod()));
372                }
373
374                if (StringUtils.hasLength(metric.getDisplayName())) {
375                        desc.setField(FIELD_DISPLAY_NAME, metric.getDisplayName());
376                }
377
378                if (StringUtils.hasLength(metric.getUnit())) {
379                        desc.setField(FIELD_UNITS, metric.getUnit());
380                }
381
382                if (StringUtils.hasLength(metric.getCategory())) {
383                        desc.setField(FIELD_METRIC_CATEGORY, metric.getCategory());
384                }
385
386                desc.setField(FIELD_METRIC_TYPE, metric.getMetricType().toString());
387        }
388
389        /**
390         * Adds descriptor fields from the {@code ManagedAttribute} attribute
391         * to the attribute descriptor. Specifically, adds the {@code currencyTimeLimit}
392         * descriptor field if it is present in the metadata.
393         */
394        @Override
395        protected void populateOperationDescriptor(Descriptor desc, Method method, String beanKey) {
396                ManagedOperation mo = this.attributeSource.getManagedOperation(method);
397                if (mo != null) {
398                        applyCurrencyTimeLimit(desc, mo.getCurrencyTimeLimit());
399                }
400        }
401
402        /**
403         * Determines which of two {@code int} values should be used as the value
404         * for an attribute descriptor. In general, only the getter or the setter will
405         * be have a non-negative value so we use that value. In the event that both values
406         * are non-negative, we use the greater of the two. This method can be used to
407         * resolve any {@code int} valued descriptor where there are two possible values.
408         * @param getter the int value associated with the getter for this attribute
409         * @param setter the int associated with the setter for this attribute
410         */
411        private int resolveIntDescriptor(int getter, int setter) {
412                return (getter >= setter ? getter : setter);
413        }
414
415        /**
416         * Locates the value of a descriptor based on values attached
417         * to both the getter and setter methods. If both have values
418         * supplied then the value attached to the getter is preferred.
419         * @param getter the Object value associated with the get method
420         * @param setter the Object value associated with the set method
421         * @return the appropriate Object to use as the value for the descriptor
422         */
423        private Object resolveObjectDescriptor(Object getter, Object setter) {
424                return (getter != null ? getter : setter);
425        }
426
427        /**
428         * Locates the value of a descriptor based on values attached
429         * to both the getter and setter methods. If both have values
430         * supplied then the value attached to the getter is preferred.
431         * The supplied default value is used to check to see if the value
432         * associated with the getter has changed from the default.
433         * @param getter the String value associated with the get method
434         * @param setter the String value associated with the set method
435         * @return the appropriate String to use as the value for the descriptor
436         */
437        private String resolveStringDescriptor(String getter, String setter) {
438                return (StringUtils.hasLength(getter) ? getter : setter);
439        }
440
441}