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