001/*
002 * Copyright 2012-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 *      http://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.boot.actuate.endpoint.jmx;
018
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.Map;
022
023import javax.management.Attribute;
024import javax.management.AttributeList;
025import javax.management.AttributeNotFoundException;
026import javax.management.DynamicMBean;
027import javax.management.InvalidAttributeValueException;
028import javax.management.MBeanException;
029import javax.management.MBeanInfo;
030import javax.management.ReflectionException;
031
032import reactor.core.publisher.Mono;
033
034import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
035import org.springframework.boot.actuate.endpoint.InvocationContext;
036import org.springframework.boot.actuate.endpoint.SecurityContext;
037import org.springframework.util.Assert;
038import org.springframework.util.ClassUtils;
039
040/**
041 * Adapter to expose a {@link ExposableJmxEndpoint JMX endpoint} as a
042 * {@link DynamicMBean}.
043 *
044 * @author Stephane Nicoll
045 * @author Andy Wilkinson
046 * @author Phillip Webb
047 * @since 2.0.0
048 */
049public class EndpointMBean implements DynamicMBean {
050
051        private static final boolean REACTOR_PRESENT = ClassUtils.isPresent(
052                        "reactor.core.publisher.Mono", EndpointMBean.class.getClassLoader());
053
054        private final JmxOperationResponseMapper responseMapper;
055
056        private final ClassLoader classLoader;
057
058        private final ExposableJmxEndpoint endpoint;
059
060        private final MBeanInfo info;
061
062        private final Map<String, JmxOperation> operations;
063
064        EndpointMBean(JmxOperationResponseMapper responseMapper, ClassLoader classLoader,
065                        ExposableJmxEndpoint endpoint) {
066                Assert.notNull(responseMapper, "ResponseMapper must not be null");
067                Assert.notNull(endpoint, "Endpoint must not be null");
068                this.responseMapper = responseMapper;
069                this.classLoader = classLoader;
070                this.endpoint = endpoint;
071                this.info = new MBeanInfoFactory(responseMapper).getMBeanInfo(endpoint);
072                this.operations = getOperations(endpoint);
073        }
074
075        private Map<String, JmxOperation> getOperations(ExposableJmxEndpoint endpoint) {
076                Map<String, JmxOperation> operations = new HashMap<>();
077                endpoint.getOperations()
078                                .forEach((operation) -> operations.put(operation.getName(), operation));
079                return Collections.unmodifiableMap(operations);
080        }
081
082        @Override
083        public MBeanInfo getMBeanInfo() {
084                return this.info;
085        }
086
087        @Override
088        public Object invoke(String actionName, Object[] params, String[] signature)
089                        throws MBeanException, ReflectionException {
090                JmxOperation operation = this.operations.get(actionName);
091                if (operation == null) {
092                        String message = "Endpoint with id '" + this.endpoint.getEndpointId()
093                                        + "' has no operation named " + actionName;
094                        throw new ReflectionException(new IllegalArgumentException(message), message);
095                }
096                ClassLoader previousClassLoader = overrideThreadContextClassLoader(
097                                this.classLoader);
098                try {
099                        return invoke(operation, params);
100                }
101                finally {
102                        overrideThreadContextClassLoader(previousClassLoader);
103                }
104        }
105
106        private ClassLoader overrideThreadContextClassLoader(ClassLoader classLoader) {
107                if (classLoader != null) {
108                        try {
109                                return ClassUtils.overrideThreadContextClassLoader(classLoader);
110                        }
111                        catch (SecurityException ex) {
112                                // can't set class loader, ignore it and proceed
113                        }
114                }
115                return null;
116        }
117
118        private Object invoke(JmxOperation operation, Object[] params)
119                        throws MBeanException, ReflectionException {
120                try {
121                        String[] parameterNames = operation.getParameters().stream()
122                                        .map(JmxOperationParameter::getName).toArray(String[]::new);
123                        Map<String, Object> arguments = getArguments(parameterNames, params);
124                        InvocationContext context = new InvocationContext(SecurityContext.NONE,
125                                        arguments);
126                        Object result = operation.invoke(context);
127                        if (REACTOR_PRESENT) {
128                                result = ReactiveHandler.handle(result);
129                        }
130                        return this.responseMapper.mapResponse(result);
131                }
132                catch (InvalidEndpointRequestException ex) {
133                        throw new ReflectionException(new IllegalArgumentException(ex.getMessage()),
134                                        ex.getMessage());
135                }
136                catch (Exception ex) {
137                        throw new MBeanException(translateIfNecessary(ex), ex.getMessage());
138                }
139        }
140
141        private Exception translateIfNecessary(Exception exception) {
142                if (exception.getClass().getName().startsWith("java.")) {
143                        return exception;
144                }
145                return new IllegalStateException(exception.getMessage());
146        }
147
148        private Map<String, Object> getArguments(String[] parameterNames, Object[] params) {
149                Map<String, Object> arguments = new HashMap<>();
150                for (int i = 0; i < params.length; i++) {
151                        arguments.put(parameterNames[i], params[i]);
152                }
153                return arguments;
154        }
155
156        @Override
157        public Object getAttribute(String attribute)
158                        throws AttributeNotFoundException, MBeanException, ReflectionException {
159                throw new AttributeNotFoundException("EndpointMBeans do not support attributes");
160        }
161
162        @Override
163        public void setAttribute(Attribute attribute) throws AttributeNotFoundException,
164                        InvalidAttributeValueException, MBeanException, ReflectionException {
165                throw new AttributeNotFoundException("EndpointMBeans do not support attributes");
166        }
167
168        @Override
169        public AttributeList getAttributes(String[] attributes) {
170                return new AttributeList();
171        }
172
173        @Override
174        public AttributeList setAttributes(AttributeList attributes) {
175                return new AttributeList();
176        }
177
178        private static class ReactiveHandler {
179
180                public static Object handle(Object result) {
181                        if (result instanceof Mono) {
182                                return ((Mono<?>) result).block();
183                        }
184                        return result;
185                }
186
187        }
188
189}