1 // Protocol Buffers - Google's data interchange format 2 // Copyright 2008 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; 32 33 import java.lang.reflect.Method; 34 import java.lang.reflect.Modifier; 35 import java.util.HashMap; 36 import java.util.Iterator; 37 import java.util.List; 38 import java.util.Map; 39 import java.util.Set; 40 import java.util.TreeSet; 41 42 /** Helps generate {@link String} representations of {@link MessageLite} protos. */ 43 final class MessageLiteToString { 44 45 private static final String LIST_SUFFIX = "List"; 46 private static final String BUILDER_LIST_SUFFIX = "OrBuilderList"; 47 private static final String MAP_SUFFIX = "Map"; 48 private static final String BYTES_SUFFIX = "Bytes"; 49 50 /** 51 * Returns a {@link String} representation of the {@link MessageLite} object. The first line of 52 * the {@code String} representation representation includes a comment string to uniquely identify 53 * the object instance. This acts as an indicator that this should not be relied on for 54 * comparisons. 55 * 56 * <p>For use by generated code only. 57 */ toString(MessageLite messageLite, String commentString)58 static String toString(MessageLite messageLite, String commentString) { 59 StringBuilder buffer = new StringBuilder(); 60 buffer.append("# ").append(commentString); 61 reflectivePrintWithIndent(messageLite, buffer, 0); 62 return buffer.toString(); 63 } 64 65 /** 66 * Reflectively prints the {@link MessageLite} to the buffer at given {@code indent} level. 67 * 68 * @param buffer the buffer to write to 69 * @param indent the number of spaces to indent the proto by 70 */ reflectivePrintWithIndent( MessageLite messageLite, StringBuilder buffer, int indent)71 private static void reflectivePrintWithIndent( 72 MessageLite messageLite, StringBuilder buffer, int indent) { 73 // Build a map of method name to method. We're looking for methods like getFoo(), hasFoo(), 74 // getFooList() and getFooMap() which might be useful for building an object's string 75 // representation. 76 Map<String, Method> nameToNoArgMethod = new HashMap<String, Method>(); 77 Map<String, Method> nameToMethod = new HashMap<String, Method>(); 78 Set<String> getters = new TreeSet<String>(); 79 for (Method method : messageLite.getClass().getDeclaredMethods()) { 80 nameToMethod.put(method.getName(), method); 81 if (method.getParameterTypes().length == 0) { 82 nameToNoArgMethod.put(method.getName(), method); 83 84 if (method.getName().startsWith("get")) { 85 getters.add(method.getName()); 86 } 87 } 88 } 89 90 for (String getter : getters) { 91 String suffix = getter.replaceFirst("get", ""); 92 if (suffix.endsWith(LIST_SUFFIX) 93 && !suffix.endsWith(BUILDER_LIST_SUFFIX) 94 // Sometimes people have fields named 'list' that aren't repeated. 95 && !suffix.equals(LIST_SUFFIX)) { 96 String camelCase = 97 suffix.substring(0, 1).toLowerCase() 98 + suffix.substring(1, suffix.length() - LIST_SUFFIX.length()); 99 // Try to reflectively get the value and toString() the field as if it were repeated. This 100 // only works if the method names have not been proguarded out or renamed. 101 Method listMethod = nameToNoArgMethod.get(getter); 102 if (listMethod != null && listMethod.getReturnType().equals(List.class)) { 103 printField( 104 buffer, 105 indent, 106 camelCaseToSnakeCase(camelCase), 107 GeneratedMessageLite.invokeOrDie(listMethod, messageLite)); 108 continue; 109 } 110 } 111 if (suffix.endsWith(MAP_SUFFIX) 112 // Sometimes people have fields named 'map' that aren't maps. 113 && !suffix.equals(MAP_SUFFIX)) { 114 String camelCase = 115 suffix.substring(0, 1).toLowerCase() 116 + suffix.substring(1, suffix.length() - MAP_SUFFIX.length()); 117 // Try to reflectively get the value and toString() the field as if it were a map. This only 118 // works if the method names have not been proguarded out or renamed. 119 Method mapMethod = nameToNoArgMethod.get(getter); 120 if (mapMethod != null 121 && mapMethod.getReturnType().equals(Map.class) 122 // Skip the deprecated getter method with no prefix "Map" when the field name ends with 123 // "map". 124 && !mapMethod.isAnnotationPresent(Deprecated.class) 125 // Skip the internal mutable getter method. 126 && Modifier.isPublic(mapMethod.getModifiers())) { 127 printField( 128 buffer, 129 indent, 130 camelCaseToSnakeCase(camelCase), 131 GeneratedMessageLite.invokeOrDie(mapMethod, messageLite)); 132 continue; 133 } 134 } 135 136 Method setter = nameToMethod.get("set" + suffix); 137 if (setter == null) { 138 continue; 139 } 140 if (suffix.endsWith(BYTES_SUFFIX) 141 && nameToNoArgMethod.containsKey( 142 "get" + suffix.substring(0, suffix.length() - "Bytes".length()))) { 143 // Heuristic to skip bytes based accessors for string fields. 144 continue; 145 } 146 147 String camelCase = suffix.substring(0, 1).toLowerCase() + suffix.substring(1); 148 149 // Try to reflectively get the value and toString() the field as if it were optional. This 150 // only works if the method names have not been proguarded out or renamed. 151 Method getMethod = nameToNoArgMethod.get("get" + suffix); 152 Method hasMethod = nameToNoArgMethod.get("has" + suffix); 153 // TODO(dweis): Fix proto3 semantics. 154 if (getMethod != null) { 155 Object value = GeneratedMessageLite.invokeOrDie(getMethod, messageLite); 156 final boolean hasValue = 157 hasMethod == null 158 ? !isDefaultValue(value) 159 : (Boolean) GeneratedMessageLite.invokeOrDie(hasMethod, messageLite); 160 // TODO(dweis): This doesn't stop printing oneof case twice: value and enum style. 161 if (hasValue) { 162 printField(buffer, indent, camelCaseToSnakeCase(camelCase), value); 163 } 164 continue; 165 } 166 } 167 168 if (messageLite instanceof GeneratedMessageLite.ExtendableMessage) { 169 Iterator<Map.Entry<GeneratedMessageLite.ExtensionDescriptor, Object>> iter = 170 ((GeneratedMessageLite.ExtendableMessage<?, ?>) messageLite).extensions.iterator(); 171 while (iter.hasNext()) { 172 Map.Entry<GeneratedMessageLite.ExtensionDescriptor, Object> entry = iter.next(); 173 printField(buffer, indent, "[" + entry.getKey().getNumber() + "]", entry.getValue()); 174 } 175 } 176 177 if (((GeneratedMessageLite<?, ?>) messageLite).unknownFields != null) { 178 ((GeneratedMessageLite<?, ?>) messageLite).unknownFields.printWithIndent(buffer, indent); 179 } 180 } 181 isDefaultValue(Object o)182 private static boolean isDefaultValue(Object o) { 183 if (o instanceof Boolean) { 184 return !((Boolean) o); 185 } 186 if (o instanceof Integer) { 187 return ((Integer) o) == 0; 188 } 189 if (o instanceof Float) { 190 return ((Float) o) == 0f; 191 } 192 if (o instanceof Double) { 193 return ((Double) o) == 0d; 194 } 195 if (o instanceof String) { 196 return o.equals(""); 197 } 198 if (o instanceof ByteString) { 199 return o.equals(ByteString.EMPTY); 200 } 201 if (o instanceof MessageLite) { // Can happen in oneofs. 202 return o == ((MessageLite) o).getDefaultInstanceForType(); 203 } 204 if (o instanceof java.lang.Enum<?>) { // Catches oneof enums. 205 return ((java.lang.Enum<?>) o).ordinal() == 0; 206 } 207 208 return false; 209 } 210 211 /** 212 * Formats a text proto field. 213 * 214 * <p>For use by generated code only. 215 * 216 * @param buffer the buffer to write to 217 * @param indent the number of spaces the proto should be indented by 218 * @param name the field name (in lower underscore case) 219 * @param object the object value of the field 220 */ printField(StringBuilder buffer, int indent, String name, Object object)221 static final void printField(StringBuilder buffer, int indent, String name, Object object) { 222 if (object instanceof List<?>) { 223 List<?> list = (List<?>) object; 224 for (Object entry : list) { 225 printField(buffer, indent, name, entry); 226 } 227 return; 228 } 229 if (object instanceof Map<?, ?>) { 230 Map<?, ?> map = (Map<?, ?>) object; 231 for (Map.Entry<?, ?> entry : map.entrySet()) { 232 printField(buffer, indent, name, entry); 233 } 234 return; 235 } 236 237 buffer.append('\n'); 238 for (int i = 0; i < indent; i++) { 239 buffer.append(' '); 240 } 241 buffer.append(name); 242 243 if (object instanceof String) { 244 buffer.append(": \"").append(TextFormatEscaper.escapeText((String) object)).append('"'); 245 } else if (object instanceof ByteString) { 246 buffer.append(": \"").append(TextFormatEscaper.escapeBytes((ByteString) object)).append('"'); 247 } else if (object instanceof GeneratedMessageLite) { 248 buffer.append(" {"); 249 reflectivePrintWithIndent((GeneratedMessageLite<?, ?>) object, buffer, indent + 2); 250 buffer.append("\n"); 251 for (int i = 0; i < indent; i++) { 252 buffer.append(' '); 253 } 254 buffer.append("}"); 255 } else if (object instanceof Map.Entry<?, ?>) { 256 buffer.append(" {"); 257 Map.Entry<?, ?> entry = (Map.Entry<?, ?>) object; 258 printField(buffer, indent + 2, "key", entry.getKey()); 259 printField(buffer, indent + 2, "value", entry.getValue()); 260 buffer.append("\n"); 261 for (int i = 0; i < indent; i++) { 262 buffer.append(' '); 263 } 264 buffer.append("}"); 265 } else { 266 buffer.append(": ").append(object.toString()); 267 } 268 } 269 camelCaseToSnakeCase(String camelCase)270 private static final String camelCaseToSnakeCase(String camelCase) { 271 StringBuilder builder = new StringBuilder(); 272 for (int i = 0; i < camelCase.length(); i++) { 273 char ch = camelCase.charAt(i); 274 if (Character.isUpperCase(ch)) { 275 builder.append("_"); 276 } 277 builder.append(Character.toLowerCase(ch)); 278 } 279 return builder.toString(); 280 } 281 } 282