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