Home | History | Annotate | Download | only in nano
      1 // Protocol Buffers - Google's data interchange format
      2 // Copyright 2013 Google Inc.  All rights reserved.
      3 // https://developers.google.com/protocol-buffers/
      4 //
      5 // Redistribution and use in source and binary forms, with or without
      6 // modification, are permitted provided that the following conditions are
      7 // met:
      8 //
      9 //     * Redistributions of source code must retain the above copyright
     10 // notice, this list of conditions and the following disclaimer.
     11 //     * Redistributions in binary form must reproduce the above
     12 // copyright notice, this list of conditions and the following disclaimer
     13 // in the documentation and/or other materials provided with the
     14 // distribution.
     15 //     * Neither the name of Google Inc. nor the names of its
     16 // contributors may be used to endorse or promote products derived from
     17 // this software without specific prior written permission.
     18 //
     19 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     20 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     21 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     22 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     23 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     24 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     25 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     26 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     27 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     28 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     29 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     30 
     31 package com.google.protobuf.nano;
     32 
     33 import java.lang.reflect.Array;
     34 import java.lang.reflect.Field;
     35 import java.lang.reflect.InvocationTargetException;
     36 import java.lang.reflect.Method;
     37 import java.lang.reflect.Modifier;
     38 import java.util.Map;
     39 
     40 /**
     41  * Static helper methods for printing nano protos.
     42  *
     43  * @author flynn (at) google.com Andrew Flynn
     44  */
     45 public final class MessageNanoPrinter {
     46     // Do not allow instantiation
     47     private MessageNanoPrinter() {}
     48 
     49     private static final String INDENT = "  ";
     50     private static final int MAX_STRING_LEN = 200;
     51 
     52     /**
     53      * Returns an text representation of a MessageNano suitable for debugging. The returned string
     54      * is mostly compatible with Protocol Buffer's TextFormat (as provided by non-nano protocol
     55      * buffers) -- groups (which are deprecated) are output with an underscore name (e.g. foo_bar
     56      * instead of FooBar) and will thus not parse.
     57      *
     58      * <p>Employs Java reflection on the given object and recursively prints primitive fields,
     59      * groups, and messages.</p>
     60      */
     61     public static <T extends MessageNano> String print(T message) {
     62         if (message == null) {
     63             return "";
     64         }
     65 
     66         StringBuffer buf = new StringBuffer();
     67         try {
     68             print(null, message, new StringBuffer(), buf);
     69         } catch (IllegalAccessException e) {
     70             return "Error printing proto: " + e.getMessage();
     71         } catch (InvocationTargetException e) {
     72             return "Error printing proto: " + e.getMessage();
     73         }
     74         return buf.toString();
     75     }
     76 
     77     /**
     78      * Function that will print the given message/field into the StringBuffer.
     79      * Meant to be called recursively.
     80      *
     81      * @param identifier the identifier to use, or {@code null} if this is the root message to
     82      *        print.
     83      * @param object the value to print. May in fact be a primitive value or byte array and not a
     84      *        message.
     85      * @param indentBuf the indentation each line should begin with.
     86      * @param buf the output buffer.
     87      */
     88     private static void print(String identifier, Object object,
     89             StringBuffer indentBuf, StringBuffer buf) throws IllegalAccessException,
     90             InvocationTargetException {
     91         if (object == null) {
     92             // This can happen if...
     93             //   - we're about to print a message, String, or byte[], but it not present;
     94             //   - we're about to print a primitive, but "reftype" optional style is enabled, and
     95             //     the field is unset.
     96             // In both cases the appropriate behavior is to output nothing.
     97         } else if (object instanceof MessageNano) {  // Nano proto message
     98             int origIndentBufLength = indentBuf.length();
     99             if (identifier != null) {
    100                 buf.append(indentBuf).append(deCamelCaseify(identifier)).append(" <\n");
    101                 indentBuf.append(INDENT);
    102             }
    103             Class<?> clazz = object.getClass();
    104 
    105             // Proto fields follow one of two formats:
    106             //
    107             // 1) Public, non-static variables that do not begin or end with '_'
    108             // Find and print these using declared public fields
    109             for (Field field : clazz.getFields()) {
    110                 int modifiers = field.getModifiers();
    111                 String fieldName = field.getName();
    112                 if ("cachedSize".equals(fieldName)) {
    113                     // TODO(bduff): perhaps cachedSize should have a more obscure name.
    114                     continue;
    115                 }
    116 
    117                 if ((modifiers & Modifier.PUBLIC) == Modifier.PUBLIC
    118                         && (modifiers & Modifier.STATIC) != Modifier.STATIC
    119                         && !fieldName.startsWith("_")
    120                         && !fieldName.endsWith("_")) {
    121                     Class<?> fieldType = field.getType();
    122                     Object value = field.get(object);
    123 
    124                     if (fieldType.isArray()) {
    125                         Class<?> arrayType = fieldType.getComponentType();
    126 
    127                         // bytes is special since it's not repeated, but is represented by an array
    128                         if (arrayType == byte.class) {
    129                             print(fieldName, value, indentBuf, buf);
    130                         } else {
    131                             int len = value == null ? 0 : Array.getLength(value);
    132                             for (int i = 0; i < len; i++) {
    133                                 Object elem = Array.get(value, i);
    134                                 print(fieldName, elem, indentBuf, buf);
    135                             }
    136                         }
    137                     } else {
    138                         print(fieldName, value, indentBuf, buf);
    139                     }
    140                 }
    141             }
    142 
    143             // 2) Fields that are accessed via getter methods (when accessors
    144             //    mode is turned on)
    145             // Find and print these using getter methods.
    146             for (Method method : clazz.getMethods()) {
    147                 String name = method.getName();
    148                 // Check for the setter accessor method since getters and hazzers both have
    149                 // non-proto-field name collisions (hashCode() and getSerializedSize())
    150                 if (name.startsWith("set")) {
    151                     String subfieldName = name.substring(3);
    152 
    153                     Method hazzer = null;
    154                     try {
    155                         hazzer = clazz.getMethod("has" + subfieldName);
    156                     } catch (NoSuchMethodException e) {
    157                         continue;
    158                     }
    159                     // If hazzer does't exist or returns false, no need to continue
    160                     if (!(Boolean) hazzer.invoke(object)) {
    161                         continue;
    162                     }
    163 
    164                     Method getter = null;
    165                     try {
    166                         getter = clazz.getMethod("get" + subfieldName);
    167                     } catch (NoSuchMethodException e) {
    168                         continue;
    169                     }
    170 
    171                     print(subfieldName, getter.invoke(object), indentBuf, buf);
    172                 }
    173             }
    174             if (identifier != null) {
    175                 indentBuf.setLength(origIndentBufLength);
    176                 buf.append(indentBuf).append(">\n");
    177             }
    178         } else if (object instanceof Map) {
    179           Map<?,?> map = (Map<?,?>) object;
    180           identifier = deCamelCaseify(identifier);
    181 
    182           for (Map.Entry<?,?> entry : map.entrySet()) {
    183             buf.append(indentBuf).append(identifier).append(" <\n");
    184             int origIndentBufLength = indentBuf.length();
    185             indentBuf.append(INDENT);
    186             print("key", entry.getKey(), indentBuf, buf);
    187             print("value", entry.getValue(), indentBuf, buf);
    188             indentBuf.setLength(origIndentBufLength);
    189             buf.append(indentBuf).append(">\n");
    190           }
    191         } else {
    192             // Non-null primitive value
    193             identifier = deCamelCaseify(identifier);
    194             buf.append(indentBuf).append(identifier).append(": ");
    195             if (object instanceof String) {
    196                 String stringMessage = sanitizeString((String) object);
    197                 buf.append("\"").append(stringMessage).append("\"");
    198             } else if (object instanceof byte[]) {
    199                 appendQuotedBytes((byte[]) object, buf);
    200             } else {
    201                 buf.append(object);
    202             }
    203             buf.append("\n");
    204         }
    205     }
    206 
    207     /**
    208      * Converts an identifier of the format "FieldName" into "field_name".
    209      */
    210     private static String deCamelCaseify(String identifier) {
    211         StringBuffer out = new StringBuffer();
    212         for (int i = 0; i < identifier.length(); i++) {
    213             char currentChar = identifier.charAt(i);
    214             if (i == 0) {
    215                 out.append(Character.toLowerCase(currentChar));
    216             } else if (Character.isUpperCase(currentChar)) {
    217                 out.append('_').append(Character.toLowerCase(currentChar));
    218             } else {
    219                 out.append(currentChar);
    220             }
    221         }
    222         return out.toString();
    223     }
    224 
    225     /**
    226      * Shortens and escapes the given string.
    227      */
    228     private static String sanitizeString(String str) {
    229         if (!str.startsWith("http") && str.length() > MAX_STRING_LEN) {
    230             // Trim non-URL strings.
    231             str = str.substring(0, MAX_STRING_LEN) + "[...]";
    232         }
    233         return escapeString(str);
    234     }
    235 
    236     /**
    237      * Escape everything except for low ASCII code points.
    238      */
    239     private static String escapeString(String str) {
    240         int strLen = str.length();
    241         StringBuilder b = new StringBuilder(strLen);
    242         for (int i = 0; i < strLen; i++) {
    243             char original = str.charAt(i);
    244             if (original >= ' ' && original <= '~' && original != '"' && original != '\'') {
    245                 b.append(original);
    246             } else {
    247                 b.append(String.format("\\u%04x", (int) original));
    248             }
    249         }
    250         return b.toString();
    251     }
    252 
    253     /**
    254      * Appends a quoted byte array to the provided {@code StringBuffer}.
    255      */
    256     private static void appendQuotedBytes(byte[] bytes, StringBuffer builder) {
    257         if (bytes == null) {
    258             builder.append("\"\"");
    259             return;
    260         }
    261 
    262         builder.append('"');
    263         for (int i = 0; i < bytes.length; ++i) {
    264             int ch = bytes[i] & 0xff;
    265             if (ch == '\\' || ch == '"') {
    266                 builder.append('\\').append((char) ch);
    267             } else if (ch >= 32 && ch < 127) {
    268                 builder.append((char) ch);
    269             } else {
    270                 builder.append(String.format("\\%03o", ch));
    271             }
    272         }
    273         builder.append('"');
    274     }
    275 }
    276