content: /**
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to You under the Apache License, Version 2.0
   * (the "License"); you may not use this file except in compliance with
   * the License.  You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package org.apache.activemq.broker.jmx;
  
  import java.io.IOException;
  import java.lang.management.ManagementFactory;
  import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.Method;
  import java.lang.reflect.Proxy;
  import java.rmi.NoSuchObjectException;
  import java.rmi.Remote;
  import java.rmi.RemoteException;
  import java.rmi.registry.LocateRegistry;
  import java.rmi.registry.Registry;
  import java.rmi.server.UnicastRemoteObject;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.concurrent.CountDownLatch;
  import java.util.concurrent.TimeUnit;
  import java.util.concurrent.atomic.AtomicBoolean;
  
  import javax.management.Attribute;
  import javax.management.InstanceNotFoundException;
  import javax.management.JMException;
  import javax.management.MBeanServer;
  import javax.management.MBeanServerFactory;
  import javax.management.MBeanServerInvocationHandler;
  import javax.management.MalformedObjectNameException;
  import javax.management.ObjectInstance;
  import javax.management.ObjectName;
  import javax.management.QueryExp;
  import javax.management.remote.JMXConnectorServer;
  import javax.management.remote.JMXServiceURL;
  import javax.management.remote.rmi.RMIConnectorServer;
  import javax.management.remote.rmi.RMIJRMPServerImpl;
  
  import org.apache.activemq.Service;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  import org.slf4j.MDC;
  
  import static java.lang.ClassLoader.getSystemClassLoader;
  
  /**
   * An abstraction over JMX MBean registration
   *
   * @org.apache.xbean.XBean
   *
   */
  public class ManagementContext implements Service {
  
      /**
       * Default activemq domain
       */
      public static final String DEFAULT_DOMAIN = "org.apache.activemq";
  
      /**
       * Default registry lookup name
       */
      public static final String DEFAULT_LOOKUP_NAME = "jmxrmi";
  
      static {
          String option = Boolean.FALSE.toString();
          try {
              option = System.getProperty("org.apache.activemq.broker.jmx.createConnector", "false");
          } catch (Exception ex) {
              // no-op
          }
  
          DEFAULT_CREATE_CONNECTOR = Boolean.parseBoolean(option);
      }
  
      public static final boolean DEFAULT_CREATE_CONNECTOR;
  
      private static final Logger LOG = LoggerFactory.getLogger(ManagementContext.class);
      private MBeanServer beanServer;
      private String jmxDomainName = DEFAULT_DOMAIN;
      private boolean useMBeanServer = true;
      private boolean createMBeanServer = true;
      private boolean locallyCreateMBeanServer;
      private boolean createConnector = DEFAULT_CREATE_CONNECTOR;
      private boolean findTigerMbeanServer = true;
      private String connectorHost = "localhost";
      private int connectorPort = 1099;
      private Map<String, ?> environment;
      private int rmiServerPort;
      private String connectorPath = "/jmxrmi";
      private String lookupName = DEFAULT_LOOKUP_NAME;
      private final AtomicBoolean started = new AtomicBoolean(false);
      private final CountDownLatch connectorStarted = new CountDownLatch(1);
      private JMXConnectorServer connectorServer;
      private ObjectName namingServiceObjectName;
      private Registry registry;
      private final Map<ObjectName, ObjectName> registeredMBeanNames = new ConcurrentHashMap<>();
      private boolean allowRemoteAddressInMBeanNames = true;
      private String brokerName;
      private String suppressMBean;
      private List<ObjectName> suppressMBeanList;
      private Remote serverStub;
      private RMIJRMPServerImpl server;
  
      public ManagementContext() {
          this(null);
      }
  
      public ManagementContext(MBeanServer server) {
          this.beanServer = server;
      }
  
      @Override
      public void start() throws Exception {
          // lets force the MBeanServer to be created if needed
          if (started.compareAndSet(false, true)) {
  
              populateMBeanSuppressionMap();
  
              // fallback and use localhost
              if (connectorHost == null) {
                  connectorHost = "localhost";
              }
  
              // force MBean server to be looked up, so we have it
              getMBeanServer();
  
              if (connectorServer != null) {
                  try {
                      if (getMBeanServer().isRegistered(namingServiceObjectName)) {
                          LOG.debug("Invoking start on MBean: {}", namingServiceObjectName);
                          getMBeanServer().invoke(namingServiceObjectName, "start", null, null);
                      }
                  } catch (Throwable t) {
                      LOG.debug("Error invoking start on MBean {}. This exception is ignored.", namingServiceObjectName, t);
                  }
  
                  Thread t = new Thread("JMX connector") {
                      @Override
                      public void run() {
                          // ensure we use MDC logging with the broker name, so people can see the logs if MDC was in use
                          if (brokerName != null) {
                              MDC.put("activemq.broker", brokerName);
                          }
                          try {
                              if (started.get() && server != null) {
                                  LOG.debug("Starting JMXConnectorServer...");
                                  try {
                                      // need to remove MDC as we must not inherit MDC in child threads causing leaks
                                      MDC.remove("activemq.broker");
                                      connectorServer.start();
                                      serverStub = server.toStub();
                                  } finally {
                                      if (brokerName != null) {
                                          MDC.put("activemq.broker", brokerName);
                                      }
                                      connectorStarted.countDown();
                                  }
                                  LOG.info("JMX consoles can connect to {}", connectorServer.getAddress());
                              }
                          } catch (IOException e) {
                              LOG.warn("Failed to start JMX connector {}. Will restart management to re-create JMX connector, trying to remedy this issue.", e.getMessage());
                              LOG.debug("Reason for failed JMX connector start", e);
                          } finally {
                              MDC.remove("activemq.broker");
                          }
                      }
                  };
                  t.setDaemon(true);
                  t.start();
              }
          }
      }
  
      private void populateMBeanSuppressionMap() throws Exception {
          if (suppressMBean != null) {
              suppressMBeanList = new LinkedList<>();
              for (String pair : suppressMBean.split(",")) {
                  suppressMBeanList.add(new ObjectName(jmxDomainName + ":*," + pair));
              }
          }
      }
  
      @Override
      public void stop() throws Exception {
          if (started.compareAndSet(true, false)) {
              MBeanServer mbeanServer = getMBeanServer();
  
              // unregister the mbeans we have registered
              if (mbeanServer != null) {
                  for (Map.Entry<ObjectName, ObjectName> entry : registeredMBeanNames.entrySet()) {
                      ObjectName actualName = entry.getValue();
                      if (actualName != null && beanServer.isRegistered(actualName)) {
                          LOG.debug("Unregistering MBean {}", actualName);
                          mbeanServer.unregisterMBean(actualName);
                      }
                  }
              }
              registeredMBeanNames.clear();
  
              JMXConnectorServer server = connectorServer;
              connectorServer = null;
              if (server != null) {
                  try {
                      if (connectorStarted.await(10, TimeUnit.SECONDS)) {
                          LOG.debug("Stopping jmx connector");
                          server.stop();
                      }
                  } catch (IOException e) {
                      LOG.warn("Failed to stop jmx connector: {}", e.getMessage());
                  }
                  // stop naming service MBean
                  try {
                      if (namingServiceObjectName != null && getMBeanServer().isRegistered(namingServiceObjectName)) {
                          LOG.debug("Stopping MBean {}", namingServiceObjectName);
                          getMBeanServer().invoke(namingServiceObjectName, "stop", null, null);
                          LOG.debug("Unregistering MBean {}", namingServiceObjectName);
                          getMBeanServer().unregisterMBean(namingServiceObjectName);
                      }
                  } catch (Throwable t) {
                      LOG.warn("Error stopping and unregistering MBean {} due to {}", namingServiceObjectName, t.getMessage());
                  }
                  namingServiceObjectName = null;
              }
  
              if (locallyCreateMBeanServer && beanServer != null) {
                  // check to see if the factory knows about this server
                  List<MBeanServer> list = MBeanServerFactory.findMBeanServer(null);
                  if (list != null && !list.isEmpty() && list.contains(beanServer)) {
                      LOG.debug("Releasing MBeanServer {}", beanServer);
                      MBeanServerFactory.releaseMBeanServer(beanServer);
                  }
              }
              beanServer = null;
          }
  
          // Un-export JMX RMI registry, if it was created
          if (registry != null) {
              try {
                  UnicastRemoteObject.unexportObject(registry, true);
                  LOG.debug("Unexported JMX RMI Registry");
              } catch (NoSuchObjectException e) {
                  LOG.debug("Error occurred while unexporting JMX RMI registry. This exception will be ignored.");
              }
  
              registry = null;
          }
      }
  
      /**
       * Gets the broker name this context is used by, may be <tt>null</tt>
       * if the broker name was not set.
       */
      public String getBrokerName() {
          return brokerName;
      }
  
      /**
       * Sets the broker name this context is being used by.
       */
      public void setBrokerName(String brokerName) {
          this.brokerName = brokerName;
      }
  
      /**
       * @return Returns the jmxDomainName.
       */
      public String getJmxDomainName() {
          return jmxDomainName;
      }
  
      /**
       * @param jmxDomainName The jmxDomainName to set.
       */
      public void setJmxDomainName(String jmxDomainName) {
          this.jmxDomainName = jmxDomainName;
      }
  
      /**
       * Get the MBeanServer
       *
       * @return the MBeanServer
       */
      public MBeanServer getMBeanServer() {
          if (this.beanServer == null) {
              this.beanServer = findMBeanServer();
          }
          return beanServer;
      }
  
      /**
       * Set the MBeanServer
       */
      public void setMBeanServer(MBeanServer beanServer) {
          this.beanServer = beanServer;
      }
  
      /**
       * @return Returns the useMBeanServer.
       */
      public boolean isUseMBeanServer() {
          return useMBeanServer;
      }
  
      /**
       * @param useMBeanServer The useMBeanServer to set.
       */
      public void setUseMBeanServer(boolean useMBeanServer) {
          this.useMBeanServer = useMBeanServer;
      }
  
      /**
       * @return Returns the createMBeanServer flag.
       */
      public boolean isCreateMBeanServer() {
          return createMBeanServer;
      }
  
      /**
       * @param enableJMX Set createMBeanServer.
       */
      public void setCreateMBeanServer(boolean enableJMX) {
          this.createMBeanServer = enableJMX;
      }
  
      public boolean isFindTigerMbeanServer() {
          return findTigerMbeanServer;
      }
  
      public boolean isConnectorStarted() {
          return connectorStarted.getCount() == 0 || (connectorServer != null && connectorServer.isActive());
      }
  
      /**
       * Enables/disables the searching for the Java 5 platform MBeanServer
       */
      public void setFindTigerMbeanServer(boolean findTigerMbeanServer) {
          this.findTigerMbeanServer = findTigerMbeanServer;
      }
  
      /**
       * Formulate and return the MBean ObjectName of a custom control MBean
       *
       * @return the JMX ObjectName of the MBean, or <code>null</code> if
       *         <code>customName</code> is invalid.
       */
      public ObjectName createCustomComponentMBeanName(String type, String name) {
          ObjectName result = null;
          String tmp = jmxDomainName + ":" + "type=" + sanitizeString(type) + ",name=" + sanitizeString(name);
          try {
              result = new ObjectName(tmp);
          } catch (MalformedObjectNameException e) {
              LOG.error("Couldn't create ObjectName from: {}, {}", type, name);
          }
          return result;
      }
  
      /**
       * The ':' and '/' characters are reserved in ObjectNames
       *
       * @return sanitized String
       */
      private static String sanitizeString(String in) {
          String result = null;
          if (in != null) {
              result = in.replace(':', '_');
              result = result.replace('/', '_');
              result = result.replace('\\', '_');
          }
          return result;
      }
  
      /**
       * Retrieve an System ObjectName
       */
      public static ObjectName getSystemObjectName(String domainName, String containerName, Class<?> theClass) throws MalformedObjectNameException, NullPointerException {
          String tmp = domainName + ":" + "type=" + theClass.getName() + ",name=" + getRelativeName(containerName, theClass);
          return new ObjectName(tmp);
      }
  
      private static String getRelativeName(String containerName, Class<?> theClass) {
          String name = theClass.getName();
          int index = name.lastIndexOf(".");
          if (index >= 0 && (index + 1) < name.length()) {
              name = name.substring(index + 1);
          }
          return containerName + "." + name;
      }
  
      public Object newProxyInstance(ObjectName objectName, Class<?> interfaceClass, boolean notificationBroadcaster){
          return MBeanServerInvocationHandler.newProxyInstance(getMBeanServer(), objectName, interfaceClass, notificationBroadcaster);
      }
  
      public Object getAttribute(ObjectName name, String attribute) throws Exception{
          return getMBeanServer().getAttribute(name, attribute);
      }
  
      public ObjectInstance registerMBean(Object bean, ObjectName name) throws Exception{
          ObjectInstance result = null;
          if (isAllowedToRegister(name)) {
              result = getMBeanServer().registerMBean(bean, name);
              this.registeredMBeanNames.put(name, result.getObjectName());
          }
          return result;
      }
  
      protected boolean isAllowedToRegister(ObjectName name) {
          boolean result = true;
          if (suppressMBean != null && suppressMBeanList != null) {
              for (ObjectName attr : suppressMBeanList) {
                  if (attr.apply(name)) {
                      result = false;
                      break;
                  }
              }
          }
          return result;
      }
  
      public Set<ObjectName> queryNames(ObjectName name, QueryExp query) throws Exception{
          if (name != null) {
              ObjectName actualName = this.registeredMBeanNames.get(name);
              if (actualName != null) {
                  return getMBeanServer().queryNames(actualName, query);
              }
          }
          return getMBeanServer().queryNames(name, query);
      }
  
      public ObjectInstance getObjectInstance(ObjectName name) throws InstanceNotFoundException {
          return getMBeanServer().getObjectInstance(name);
      }
  
      /**
       * Unregister an MBean
       */
      public void unregisterMBean(ObjectName name) throws JMException {
          ObjectName actualName = this.registeredMBeanNames.get(name);
          if (beanServer != null && actualName != null && beanServer.isRegistered(actualName) && this.registeredMBeanNames.remove(name) != null) {
              LOG.debug("Unregistering MBean {}", actualName);
              beanServer.unregisterMBean(actualName);
          }
      }
  
      protected synchronized MBeanServer findMBeanServer() {
          MBeanServer result = null;
  
          try {
              if (useMBeanServer) {
                  if (findTigerMbeanServer) {
                      result = findTigerMBeanServer();
                  }
                  if (result == null) {
                      // lets piggy back on another MBeanServer - we could be in an appserver!
                      List<MBeanServer> list = MBeanServerFactory.findMBeanServer(null);
                      if (list != null && list.size() > 0) {
                          result = list.get(0);
                      }
                  }
              }
              if (result == null && createMBeanServer) {
                  result = createMBeanServer();
              }
          } catch (NoClassDefFoundError e) {
              LOG.error("Could not load MBeanServer", e);
          } catch (Throwable e) {
              // probably don't have access to system properties
              LOG.error("Failed to initialize MBeanServer", e);
          }
          return result;
      }
  
      public MBeanServer findTigerMBeanServer() {
          String name = "java.lang.management.ManagementFactory";
          Class<?> type = loadClass(name, ManagementContext.class.getClassLoader());
          if (type != null) {
              try {
                  Method method = type.getMethod("getPlatformMBeanServer", new Class[0]);
                  if (method != null) {
                      Object answer = method.invoke(null, new Object[0]);
                      if (answer instanceof MBeanServer) {
                          if (createConnector) {
                              createConnector((MBeanServer)answer);
                          }
                          return (MBeanServer)answer;
                      } else {
                          LOG.warn("Could not cast: {} into an MBeanServer. There must be some classloader strangeness in town", answer);
                      }
                  } else {
                      LOG.warn("Method getPlatformMBeanServer() does not appear visible on type: {}", type.getName());
                  }
              } catch (Exception e) {
                  LOG.warn("Failed to call getPlatformMBeanServer() due to: ", e);
              }
          } else {
              LOG.trace("Class not found: {} so probably running on Java 1.4", name);
          }
          return null;
      }
  
      private static Class<?> loadClass(String name, ClassLoader loader) {
          try {
              return loader.loadClass(name);
          } catch (ClassNotFoundException e) {
              try {
                  return Thread.currentThread().getContextClassLoader().loadClass(name);
              } catch (ClassNotFoundException e1) {
                  return null;
              }
          }
      }
  
      /**
       * @return an MBeanServer instance
       */
      protected MBeanServer createMBeanServer() throws MalformedObjectNameException, IOException {
          MBeanServer mbeanServer = MBeanServerFactory.createMBeanServer(jmxDomainName);
          locallyCreateMBeanServer = true;
          if (createConnector) {
              createConnector(mbeanServer);
          }
          return mbeanServer;
      }
  
      private void createConnector(MBeanServer mbeanServer) throws IOException {
          // Create the NamingService, needed by JSR 160
          try {
              if (registry == null) {
                  LOG.debug("Creating RMIRegistry on port {}", connectorPort);
                  registry = jmxRegistry(connectorPort);
              }
  
              namingServiceObjectName = ObjectName.getInstance("naming:type=rmiregistry");
  
              // Do not use the createMBean as the mx4j jar may not be in the
              // same class loader than the server
              Class<?> cl = Class.forName("mx4j.tools.naming.NamingService");
              mbeanServer.registerMBean(cl.getDeclaredConstructor().newInstance(), namingServiceObjectName);
  
              // set the naming port
              Attribute attr = new Attribute("Port", connectorPort);
              mbeanServer.setAttribute(namingServiceObjectName, attr);
          } catch(ClassNotFoundException e) {
              LOG.debug("Probably not using JRE 1.4: {}", e.getLocalizedMessage());
          } catch (Throwable e) {
              LOG.debug("Failed to create local registry. This exception will be ignored.", e);
          }
  
          // Create the JMXConnectorServer
          String rmiServer = "";
          if (rmiServerPort != 0) {
              // This is handy to use if you have a firewall and need to force JMX to use fixed ports.
              rmiServer = ""+getConnectorHost()+":" + rmiServerPort;
          }
  
          server = new RMIJRMPServerImpl(connectorPort, null, null, environment);
  
          final String serviceURL = "service:jmx:rmi://" + rmiServer + "/jndi/rmi://" +getConnectorHost()+":" + connectorPort + connectorPath;
          final JMXServiceURL url = new JMXServiceURL(serviceURL);
  
          connectorServer = new RMIConnectorServer(url, environment, server, ManagementFactory.getPlatformMBeanServer());
          LOG.debug("Created JMXConnectorServer {}", connectorServer);
      }
  
      public String getConnectorPath() {
          return connectorPath;
      }
  
      public void setConnectorPath(String connectorPath) {
          this.connectorPath = connectorPath;
  
          if (connectorPath == null || connectorPath.length() == 0) {
              this.lookupName = DEFAULT_LOOKUP_NAME;
          } else {
              this.lookupName = connectorPath.replaceAll("^/+", "").replaceAll("/+$", "");
          }
      }
  
      public int getConnectorPort() {
          return connectorPort;
      }
  
      /**
       * @org.apache.xbean.Property propertyEditor="org.apache.activemq.util.MemoryIntPropertyEditor"
       */
      public void setConnectorPort(int connectorPort) {
          this.connectorPort = connectorPort;
      }
  
      public int getRmiServerPort() {
          return rmiServerPort;
      }
  
      /**
       * @org.apache.xbean.Property propertyEditor="org.apache.activemq.util.MemoryIntPropertyEditor"
       */
      public void setRmiServerPort(int rmiServerPort) {
          this.rmiServerPort = rmiServerPort;
      }
  
      public boolean isCreateConnector() {
          return createConnector;
      }
  
      /**
       * @org.apache.xbean.Property propertyEditor="org.apache.activemq.util.BooleanEditor"
       */
      public void setCreateConnector(boolean createConnector) {
          this.createConnector = createConnector;
      }
  
      /**
       * Get the connectorHost
       * @return the connectorHost
       */
      public String getConnectorHost() {
          return this.connectorHost;
      }
  
      /**
       * Set the connectorHost
       * @param connectorHost the connectorHost to set
       */
      public void setConnectorHost(String connectorHost) {
          this.connectorHost = connectorHost;
      }
  
      public Map<String, ?> getEnvironment() {
          return environment;
      }
  
      public void setEnvironment(Map<String, ?> environment) {
          this.environment = environment;
      }
  
      public boolean isAllowRemoteAddressInMBeanNames() {
          return allowRemoteAddressInMBeanNames;
      }
  
      public void setAllowRemoteAddressInMBeanNames(boolean allowRemoteAddressInMBeanNames) {
          this.allowRemoteAddressInMBeanNames = allowRemoteAddressInMBeanNames;
      }
  
      /**
       * Allow selective MBeans registration to be suppressed. Any Mbean ObjectName that matches any
       * of the supplied attribute values will not be registered with the MBeanServer.
       * eg: "endpoint=dynamicProducer,endpoint=Consumer" will suppress the registration of *all* dynamic producer and consumer mbeans.
       *
       * @param commaListOfAttributeKeyValuePairs  the comma separated list of attribute key=value pairs to match.
       */
      public void setSuppressMBean(String commaListOfAttributeKeyValuePairs) {
          this.suppressMBean = commaListOfAttributeKeyValuePairs;
      }
  
      public String getSuppressMBean() {
          return suppressMBean;
      }
  
      // do not use sun.rmi.registry.RegistryImpl! it is not always easily available
      private Registry jmxRegistry(final int port) throws RemoteException {
          final var loader = Thread.currentThread().getContextClassLoader();
          final var delegate = LocateRegistry.createRegistry(port);
          return Registry.class.cast(Proxy.newProxyInstance(
                  loader == null ? getSystemClassLoader() : loader,
                  new Class<?>[]{Registry.class}, (proxy, method, args) -> {
                      final var name = method.getName();
                      if ("lookup".equals(name) &&
                              method.getParameterCount() == 1 &&
                              method.getParameterTypes()[0] == String.class) {
                          return lookupName.equals(args[0]) ? serverStub : null;
                      }
                      switch (name) {
                          case "bind":
                          case "unbind":
                          case "rebind":
                              return null;
                          case "list":
                              return new String[] {lookupName};
                      }
                      try {
                          return method.invoke(delegate, args);
                      } catch (final InvocationTargetException ite) {
                          throw ite.getTargetException();
                      }
                  }));
      }
  }
  