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