Home | History | Annotate | Download | only in nano
      1 // Protocol Buffers - Google's data interchange format
      2 // Copyright 2013 Google Inc.  All rights reserved.
      3 // http://code.google.com/p/protobuf/
      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 
     39 /**
     40  * Static helper methods for printing nano protos.
     41  *
     42  * @author flynn (at) google.com Andrew Flynn
     43  */
     44 public final class MessageNanoPrinter {
     45     // Do not allow instantiation
     46     private MessageNanoPrinter() {}
     47 
     48     private static final String INDENT = "  ";
     49     private static final int MAX_STRING_LEN = 200;
     50 
     51     /**
     52      * Returns an text representation of a MessageNano suitable for debugging. The returned string
     53      * is mostly compatible with Protocol Buffer's TextFormat (as provided by non-nano protocol
     54      * buffers) -- groups (which are deprecated) are output with an underscore name (e.g. foo_bar
     55      * instead of FooBar) and will thus not parse.
     56      *
     57      * <p>Employs Java reflection on the given object and recursively prints primitive fields,
     58      * groups, and messages.</p>
     59      */
     60     public static <T extends MessageNano> String print(T message) {
     61         if (message == null) {
     62             return "";
     63         }
     64 
     65         StringBuffer buf = new StringBuffer();
     66         try {
     67             print(null, message, new StringBuffer(), buf);
     68         } catch (IllegalAccessException e) {
     69             return "Error printing proto: " + e.getMessage();
     70         } catch (InvocationTargetException e) {
     71             return "Error printing proto: " + e.getMessage();
     72         }
     73         return buf.toString();
     74     }
     75 
     76     /**
     77      * Function that will print the given message/field into the StringBuffer.
     78      * Meant to be called recursively.
     79      *
     80      * @param identifier the identifier to use, or {@code null} if this is the root message to
     81      *        print.
     82      * @param object the value to print. May in fact be a primitive value or byte array and not a
     83      *        message.
     84      * @param indentBuf the indentation each line should begin with.
     85      * @param buf the output buffer.
     86      */
     87     private static void print(String identifier, Object object,
     88             StringBuffer indentBuf, StringBuffer buf) throws IllegalAccessException,
     89             InvocationTargetException {
     90         if (object == null) {
     91             // This can happen if...
     92             //   - we're about to print a message, String, or byte[], but it not present;
     93             //   - we're about to print a primitive, but "reftype" optional style is enabled, and
     94             //     the field is unset.
     95             // In both cases the appropriate behavior is to output nothing.
     96         } else if (object instanceof MessageNano) {  // Nano proto message
     97             int origIndentBufLength = indentBuf.length();
     98             if (identifier != null) {
     99                 buf.append(indentBuf).append(deCamelCaseify(identifier)).append(" <\n");
    100                 indentBuf.append(INDENT);
    101             }
    102             Class<?> clazz = object.getClass();
    103 
    104             // Proto fields follow one of two formats:
    105             //
    106             // 1) Public, non-static variables that do not begin or end with '_'
    107             // Find and print these using declared public fields
    108             for (Field field : clazz.getFields()) {
    109                 int modifiers = field.getModifiers();
    110                 String fieldName = field.getName();
    111                 if ("cachedSize".equals(fieldName)) {
    112                     // TODO(bduff): perhaps cachedSize should have a more obscure name.
    113                     continue;
    114                 }
    115 
    116                 if ((modifiers & Modifier.PUBLIC) == Modifier.PUBLIC
    117                         && (modifiers & Modifier.STATIC) != Modifier.STATIC
    118                         && !fieldName.startsWith("_")
    119                         && !fieldName.endsWith("_")) {
    120                     Class<?> fieldType = field.getType();
    121                     Object value = field.get(object);
    122 
    123                     if (fieldType.isArray()) {
    124                         Class<?> arrayType = fieldType.getComponentType();
    125 
    126                         // bytes is special since it's not repeated, but is represented by an array
    127                         if (arrayType == byte.class) {
    128                             print(fieldName, value, indentBuf, buf);
    129                         } else {
    130                             int len = value == null ? 0 : Array.getLength(value);
    131                             for (int i = 0; i < len; i++) {
    132                                 Object elem = Array.get(value, i);
    133                                 print(fieldName, elem, indentBuf, buf);
    134                             }
    135                         }
    136                     } else {
    137                         print(fieldName, value, indentBuf, buf);
    138                     }
    139                 }
    140             }
    141 
    142             // 2) Fields that are accessed via getter methods (when accessors
    143             //    mode is turned on)
    144             // Find and print these using getter methods.
    145             for (Method method : clazz.getMethods()) {
    146                 String name = method.getName();
    147                 // Check for the setter accessor method since getters and hazzers both have
    148                 // non-proto-field name collisions (hashCode() and getSerializedSize())
    149                 if (name.startsWith("set")) {
    150                     String subfieldName = name.substring(3);
    151 
    152                     Method hazzer = null;
    153                     try {
    154                         hazzer = clazz.getMethod("has" + subfieldName);
    155                     } catch (NoSuchMethodException e) {
    156                         continue;
    157                     }
    158                     // If hazzer does't exist or returns false, no need to continue
    159                     if (!(Boolean) hazzer.invoke(object)) {
    160                         continue;
    161                     }
    162 
    163                     Method getter = null;
    164                     try {
    165                         getter = clazz.getMethod("get" + subfieldName);
    166                     } catch (NoSuchMethodException e) {
    167                         continue;
    168                     }
    169 
    170                     print(subfieldName, getter.invoke(object), indentBuf, buf);
    171                 }
    172             }
    173             if (identifier != null) {
    174                 indentBuf.setLength(origIndentBufLength);
    175                 buf.append(indentBuf).append(">\n");
    176             }
    177         } else {
    178             // Non-null primitive value
    179             identifier = deCamelCaseify(identifier);
    180             buf.append(indentBuf).append(identifier).append(": ");
    181             if (object instanceof String) {
    182                 String stringMessage = sanitizeString((String) object);
    183                 buf.append("\"").append(stringMessage).append("\"");
    184             } else if (object instanceof byte[]) {
    185                 appendQuotedBytes((byte[]) object, buf);
    186             } else {
    187                 buf.append(object);
    188             }
    189             buf.append("\n");
    190         }
    191     }
    192 
    193     /**
    194      * Converts an identifier of the format "FieldName" into "field_name".
    195      */
    196     private static String deCamelCaseify(String identifier) {
    197         StringBuffer out = new StringBuffer();
    198         for (int i = 0; i < identifier.length(); i++) {
    199             char currentChar = identifier.charAt(i);
    200             if (i == 0) {
    201                 out.append(Character.toLowerCase(currentChar));
    202             } else if (Character.isUpperCase(currentChar)) {
    203                 out.append('_').append(Character.toLowerCase(currentChar));
    204             } else {
    205                 out.append(currentChar);
    206             }
    207         }
    208         return out.toString();
    209     }
    210 
    211     /**
    212      * Shortens and escapes the given string.
    213      */
    214     private static String sanitizeString(String str) {
    215         if (!str.startsWith("http") && str.length() > MAX_STRING_LEN) {
    216             // Trim non-URL strings.
    217             str = str.substring(0, MAX_STRING_LEN) + "[...]";
    218         }
    219         return escapeString(str);
    220     }
    221 
    222     /**
    223      * Escape everything except for low ASCII code points.
    224      */
    225     private static String escapeString(String str) {
    226         int strLen = str.length();
    227         StringBuilder b = new StringBuilder(strLen);
    228         for (int i = 0; i < strLen; i++) {
    229             char original = str.charAt(i);
    230             if (original >= ' ' && original <= '~' && original != '"' && original != '\'') {
    231                 b.append(original);
    232             } else {
    233                 b.append(String.format("\\u%04x", (int) original));
    234             }
    235         }
    236         return b.toString();
    237     }
    238 
    239     /**
    240      * Appends a quoted byte array to the provided {@code StringBuffer}.
    241      */
    242     private static void appendQuotedBytes(byte[] bytes, StringBuffer builder) {
    243         if (bytes == null) {
    244             builder.append("\"\"");
    245             return;
    246         }
    247 
    248         builder.append('"');
    249         for (int i = 0; i < bytes.length; ++i) {
    250             int ch = bytes[i] & 0xff;
    251             if (ch == '\\' || ch == '"') {
    252                 builder.append('\\').append((char) ch);
    253             } else if (ch >= 32 && ch < 127) {
    254                 builder.append((char) ch);
    255             } else {
    256                 builder.append(String.format("\\%03o", ch));
    257             }
    258         }
    259         builder.append('"');
    260     }
    261 }
    262