• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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