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