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