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