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.hadoop.hbase.util;
018
019import java.io.Closeable;
020import java.io.IOException;
021import java.io.OutputStreamWriter;
022import java.io.PrintWriter;
023import java.lang.management.ManagementFactory;
024import java.lang.reflect.Array;
025import java.nio.charset.StandardCharsets;
026import java.util.Iterator;
027import java.util.Set;
028import javax.management.AttributeNotFoundException;
029import javax.management.InstanceNotFoundException;
030import javax.management.IntrospectionException;
031import javax.management.MBeanAttributeInfo;
032import javax.management.MBeanException;
033import javax.management.MBeanInfo;
034import javax.management.MBeanServer;
035import javax.management.MalformedObjectNameException;
036import javax.management.ObjectName;
037import javax.management.ReflectionException;
038import javax.management.RuntimeErrorException;
039import javax.management.RuntimeMBeanException;
040import javax.management.openmbean.CompositeData;
041import javax.management.openmbean.CompositeType;
042import javax.management.openmbean.TabularData;
043import org.apache.yetus.audience.InterfaceAudience;
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
046
047import org.apache.hbase.thirdparty.com.google.gson.Gson;
048import org.apache.hbase.thirdparty.com.google.gson.stream.JsonWriter;
049
050/**
051 * Utility for doing JSON and MBeans.
052 */
053@InterfaceAudience.Private
054public class JSONBean {
055  private static final Logger LOG = LoggerFactory.getLogger(JSONBean.class);
056  private static final Gson GSON = GsonUtil.createGson().create();
057
058  /**
059   * Use dumping out mbeans as JSON.
060   */
061  public interface Writer extends Closeable {
062
063    void write(String key, String value) throws IOException;
064
065    int write(MBeanServer mBeanServer, ObjectName qry, String attribute, boolean description)
066        throws IOException;
067
068    void flush() throws IOException;
069  }
070
071  /**
072   * Notice that, closing the return {@link Writer} will not close the {@code writer} passed in, you
073   * still need to close the {@code writer} by yourself.
074   * <p/>
075   * This is because that, we can only finish the json after you call {@link Writer#close()}. So if
076   * we just close the {@code writer}, you can write nothing after finished the json.
077   */
078  public Writer open(final PrintWriter writer) throws IOException {
079    JsonWriter jsonWriter = GSON.newJsonWriter(new java.io.Writer() {
080
081      @Override
082      public void write(char[] cbuf, int off, int len) throws IOException {
083        writer.write(cbuf, off, len);
084      }
085
086      @Override
087      public void flush() throws IOException {
088        writer.flush();
089      }
090
091      @Override
092      public void close() throws IOException {
093        // do nothing
094      }
095    });
096    jsonWriter.setIndent("  ");
097    jsonWriter.beginObject();
098    return new Writer() {
099      @Override
100      public void flush() throws IOException {
101        jsonWriter.flush();
102      }
103
104      @Override
105      public void close() throws IOException {
106        jsonWriter.endObject();
107        jsonWriter.close();
108      }
109
110      @Override
111      public void write(String key, String value) throws IOException {
112        jsonWriter.name(key).value(value);
113      }
114
115      @Override
116      public int write(MBeanServer mBeanServer, ObjectName qry, String attribute,
117          boolean description) throws IOException {
118        return JSONBean.write(jsonWriter, mBeanServer, qry, attribute, description);
119      }
120    };
121  }
122
123  /**
124   * @return Return non-zero if failed to find bean. 0
125   */
126  private static int write(JsonWriter writer, MBeanServer mBeanServer, ObjectName qry,
127      String attribute, boolean description) throws IOException {
128    LOG.trace("Listing beans for " + qry);
129    Set<ObjectName> names = null;
130    names = mBeanServer.queryNames(qry, null);
131    writer.name("beans").beginArray();
132    Iterator<ObjectName> it = names.iterator();
133    while (it.hasNext()) {
134      ObjectName oname = it.next();
135      MBeanInfo minfo;
136      String code = "";
137      String descriptionStr = null;
138      Object attributeinfo = null;
139      try {
140        minfo = mBeanServer.getMBeanInfo(oname);
141        code = minfo.getClassName();
142        if (description) {
143          descriptionStr = minfo.getDescription();
144        }
145        String prs = "";
146        try {
147          if ("org.apache.commons.modeler.BaseModelMBean".equals(code)) {
148            prs = "modelerType";
149            code = (String) mBeanServer.getAttribute(oname, prs);
150          }
151          if (attribute != null) {
152            prs = attribute;
153            attributeinfo = mBeanServer.getAttribute(oname, prs);
154          }
155        } catch (RuntimeMBeanException e) {
156          // UnsupportedOperationExceptions happen in the normal course of business,
157          // so no need to log them as errors all the time.
158          if (e.getCause() instanceof UnsupportedOperationException) {
159            if (LOG.isTraceEnabled()) {
160              LOG.trace("Getting attribute " + prs + " of " + oname + " threw " + e);
161            }
162          } else {
163            LOG.error("Getting attribute " + prs + " of " + oname + " threw an exception", e);
164          }
165          return 0;
166        } catch (AttributeNotFoundException e) {
167          // If the modelerType attribute was not found, the class name is used
168          // instead.
169          LOG.error("getting attribute " + prs + " of " + oname + " threw an exception", e);
170        } catch (MBeanException e) {
171          // The code inside the attribute getter threw an exception so log it,
172          // and fall back on the class name
173          LOG.error("getting attribute " + prs + " of " + oname + " threw an exception", e);
174        } catch (RuntimeException e) {
175          // For some reason even with an MBeanException available to them
176          // Runtime exceptionscan still find their way through, so treat them
177          // the same as MBeanException
178          LOG.error("getting attribute " + prs + " of " + oname + " threw an exception", e);
179        } catch (ReflectionException e) {
180          // This happens when the code inside the JMX bean (setter?? from the
181          // java docs) threw an exception, so log it and fall back on the
182          // class name
183          LOG.error("getting attribute " + prs + " of " + oname + " threw an exception", e);
184        }
185      } catch (InstanceNotFoundException e) {
186        // Ignored for some reason the bean was not found so don't output it
187        continue;
188      } catch (IntrospectionException e) {
189        // This is an internal error, something odd happened with reflection so
190        // log it and don't output the bean.
191        LOG.error("Problem while trying to process JMX query: " + qry + " with MBean " + oname, e);
192        continue;
193      } catch (ReflectionException e) {
194        // This happens when the code inside the JMX bean threw an exception, so
195        // log it and don't output the bean.
196        LOG.error("Problem while trying to process JMX query: " + qry + " with MBean " + oname, e);
197        continue;
198      }
199      writer.beginObject();
200      writer.name("name").value(oname.toString());
201      if (description && descriptionStr != null && descriptionStr.length() > 0) {
202        writer.name("description").value(descriptionStr);
203      }
204      writer.name("modelerType").value(code);
205      if (attribute != null && attributeinfo == null) {
206        writer.name("result").value("ERROR");
207        writer.name("message").value("No attribute with name " + attribute + " was found.");
208        writer.endObject();
209        writer.endArray();
210        writer.close();
211        return -1;
212      }
213
214      if (attribute != null) {
215        writeAttribute(writer, attribute, descriptionStr, attributeinfo);
216      } else {
217        MBeanAttributeInfo[] attrs = minfo.getAttributes();
218        for (int i = 0; i < attrs.length; i++) {
219          writeAttribute(writer, mBeanServer, oname, description, attrs[i]);
220        }
221      }
222      writer.endObject();
223    }
224    writer.endArray();
225    return 0;
226  }
227
228  private static void writeAttribute(JsonWriter writer, MBeanServer mBeanServer, ObjectName oname,
229      boolean description, MBeanAttributeInfo attr) throws IOException {
230    if (!attr.isReadable()) {
231      return;
232    }
233    String attName = attr.getName();
234    if ("modelerType".equals(attName)) {
235      return;
236    }
237    if (attName.indexOf("=") >= 0 || attName.indexOf(":") >= 0 || attName.indexOf(" ") >= 0) {
238      return;
239    }
240    String descriptionStr = description ? attr.getDescription() : null;
241    Object value = null;
242    try {
243      value = mBeanServer.getAttribute(oname, attName);
244    } catch (RuntimeMBeanException e) {
245      // UnsupportedOperationExceptions happen in the normal course of business,
246      // so no need to log them as errors all the time.
247      if (e.getCause() instanceof UnsupportedOperationException) {
248        if (LOG.isTraceEnabled()) {
249          LOG.trace("Getting attribute " + attName + " of " + oname + " threw " + e);
250        }
251      } else {
252        LOG.error("getting attribute " + attName + " of " + oname + " threw an exception", e);
253      }
254      return;
255    } catch (RuntimeErrorException e) {
256      // RuntimeErrorException happens when an unexpected failure occurs in getAttribute
257      // for example https://issues.apache.org/jira/browse/DAEMON-120
258      LOG.debug("getting attribute " + attName + " of " + oname + " threw an exception", e);
259      return;
260    } catch (AttributeNotFoundException e) {
261      // Ignored the attribute was not found, which should never happen because the bean
262      // just told us that it has this attribute, but if this happens just don't output
263      // the attribute.
264      return;
265    } catch (MBeanException e) {
266      // The code inside the attribute getter threw an exception so log it, and
267      // skip outputting the attribute
268      LOG.error("getting attribute " + attName + " of " + oname + " threw an exception", e);
269      return;
270    } catch (RuntimeException e) {
271      // For some reason even with an MBeanException available to them Runtime exceptions
272      // can still find their way through, so treat them the same as MBeanException
273      LOG.error("getting attribute " + attName + " of " + oname + " threw an exception", e);
274      return;
275    } catch (ReflectionException e) {
276      // This happens when the code inside the JMX bean (setter?? from the java docs)
277      // threw an exception, so log it and skip outputting the attribute
278      LOG.error("getting attribute " + attName + " of " + oname + " threw an exception", e);
279      return;
280    } catch (InstanceNotFoundException e) {
281      // Ignored the mbean itself was not found, which should never happen because we
282      // just accessed it (perhaps something unregistered in-between) but if this
283      // happens just don't output the attribute.
284      return;
285    }
286
287    writeAttribute(writer, attName, descriptionStr, value);
288  }
289
290  private static void writeAttribute(JsonWriter writer, String attName, String descriptionStr,
291      Object value) throws IOException {
292    if (descriptionStr != null && descriptionStr.length() > 0 && !attName.equals(descriptionStr)) {
293      writer.name(attName);
294      writer.beginObject();
295      writer.name("description").value(descriptionStr);
296      writer.name("value");
297      writeObject(writer, value);
298      writer.endObject();
299    } else {
300      writer.name(attName);
301      writeObject(writer, value);
302    }
303  }
304
305  private static void writeObject(JsonWriter writer, Object value) throws IOException {
306    if (value == null) {
307      writer.nullValue();
308    } else {
309      Class<?> c = value.getClass();
310      if (c.isArray()) {
311        writer.beginArray();
312        int len = Array.getLength(value);
313        for (int j = 0; j < len; j++) {
314          Object item = Array.get(value, j);
315          writeObject(writer, item);
316        }
317        writer.endArray();
318      } else if (value instanceof Number) {
319        Number n = (Number) value;
320        if (Double.isFinite(n.doubleValue())) {
321          writer.value(n);
322        } else {
323          writer.value(n.toString());
324        }
325      } else if (value instanceof Boolean) {
326        Boolean b = (Boolean) value;
327        writer.value(b);
328      } else if (value instanceof CompositeData) {
329        CompositeData cds = (CompositeData) value;
330        CompositeType comp = cds.getCompositeType();
331        Set<String> keys = comp.keySet();
332        writer.beginObject();
333        for (String key : keys) {
334          writeAttribute(writer, key, null, cds.get(key));
335        }
336        writer.endObject();
337      } else if (value instanceof TabularData) {
338        TabularData tds = (TabularData) value;
339        writer.beginArray();
340        for (Object entry : tds.values()) {
341          writeObject(writer, entry);
342        }
343        writer.endArray();
344      } else {
345        writer.value(value.toString());
346      }
347    }
348  }
349
350  /**
351   * Dump out all registered mbeans as json on System.out.
352   */
353  public static void dumpAllBeans() throws IOException, MalformedObjectNameException {
354    try (PrintWriter writer =
355      new PrintWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8))) {
356      JSONBean dumper = new JSONBean();
357      try (JSONBean.Writer jsonBeanWriter = dumper.open(writer)) {
358        MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
359        jsonBeanWriter.write(mbeanServer, new ObjectName("*:*"), null, false);
360      }
361    }
362  }
363}