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