|
1 // Copyright (c) 2009 The Chromium Authors. All rights reserved. |
|
2 // Use of this source code is governed by a BSD-style license that can be |
|
3 // found in the LICENSE file. |
|
4 |
|
5 package org.chromium.debug.core.util; |
|
6 |
|
7 import java.util.Collection; |
|
8 import java.util.Map; |
|
9 import java.util.SortedMap; |
|
10 |
|
11 import org.chromium.sdk.JsArray; |
|
12 import org.chromium.sdk.JsObject; |
|
13 import org.chromium.sdk.JsValue; |
|
14 import org.chromium.sdk.JsVariable; |
|
15 import org.chromium.sdk.JsValue.Type; |
|
16 |
|
17 /** |
|
18 * A converter of JsValues into human-readable strings used in various contexts. |
|
19 */ |
|
20 public class JsValueStringifier { |
|
21 |
|
22 /** |
|
23 * A configuration class |
|
24 */ |
|
25 public static class Config { |
|
26 |
|
27 /** |
|
28 * The maximum length of the text to render. If the value length exceeds |
|
29 * the limit, an ellipsis will be used to truncate the value. |
|
30 * The default is 80 characters. |
|
31 */ |
|
32 public int maxLength = 80; |
|
33 } |
|
34 |
|
35 private static final String ELLIPSIS = "..."; //$NON-NLS-1$ |
|
36 |
|
37 private static final String UNKNOWN_VALUE = "<null>"; //$NON-NLS-1$ |
|
38 |
|
39 private final Config config; |
|
40 |
|
41 |
|
42 /** |
|
43 * Constructs a visible string for the given {@code value} (without exposing |
|
44 * the value structure). Encloses JavaScript string values in double quotes. |
|
45 * |
|
46 * @param value to build a visible string for |
|
47 * @return {@code value.getValueString()} (enclosed in double quotes if |
|
48 * {@code value.getType() == TYPE_STRING}), or {@code null} if |
|
49 * {@code value==null} |
|
50 */ |
|
51 public static String toVisibleString(JsValue value) { |
|
52 return possiblyQuoteValueString(value); |
|
53 } |
|
54 |
|
55 private static String possiblyQuoteValueString(JsValue value) { |
|
56 if (value == null) { |
|
57 return UNKNOWN_VALUE; |
|
58 } |
|
59 String valueString = value.getValueString(); |
|
60 return value.getType() == JsValue.Type.TYPE_STRING |
|
61 ? "\"" + valueString + "\"" //$NON-NLS-1$ //$NON-NLS-2$ |
|
62 : valueString; |
|
63 } |
|
64 |
|
65 /** |
|
66 * Use the default config values. |
|
67 */ |
|
68 public JsValueStringifier() { |
|
69 this.config = new Config(); |
|
70 } |
|
71 |
|
72 /** |
|
73 * Use the specified {@code config} data. |
|
74 * @param config to use when rendering values. |
|
75 */ |
|
76 public JsValueStringifier(Config config) { |
|
77 this.config = config; |
|
78 } |
|
79 |
|
80 public String render(JsValue value) { |
|
81 if (value == null) { |
|
82 return UNKNOWN_VALUE; |
|
83 } |
|
84 StringBuilder output = new StringBuilder(); |
|
85 renderInternal(value, config.maxLength, true, output); |
|
86 return output.toString(); |
|
87 } |
|
88 |
|
89 /** |
|
90 * @param value to render |
|
91 * @param maxLength the maximum length of the {@code output} |
|
92 * @param descend whether to descend into the object contents |
|
93 * @param output to render into |
|
94 */ |
|
95 private void renderInternal(JsValue value, int maxLength, boolean descend, StringBuilder output) { |
|
96 if (!descend) { |
|
97 renderPrimitive(value, maxLength, output); |
|
98 return; |
|
99 } |
|
100 Type type = value.getType(); |
|
101 // TODO(apavlov): implement good stringification of other types? |
|
102 switch (type) { |
|
103 case TYPE_ARRAY: |
|
104 renderArray(value.asObject().asArray(), maxLength, output); |
|
105 break; |
|
106 case TYPE_OBJECT: |
|
107 renderObject(value.asObject(), maxLength, output); |
|
108 break; |
|
109 default: |
|
110 renderPrimitive(value, maxLength, output); |
|
111 break; |
|
112 } |
|
113 } |
|
114 |
|
115 private void renderPrimitive(JsValue value, int maxLength, StringBuilder output) { |
|
116 output.append(possiblyQuoteValueString(value)); |
|
117 truncate(output, maxLength, ELLIPSIS); |
|
118 } |
|
119 |
|
120 private void truncate(StringBuilder valueBuilder, int maxLength, String suffix) { |
|
121 int length = valueBuilder.length(); |
|
122 if (length > maxLength) { |
|
123 valueBuilder.setLength(maxLength); |
|
124 valueBuilder.replace( |
|
125 maxLength - suffix.length(), maxLength, suffix); |
|
126 } |
|
127 } |
|
128 |
|
129 private StringBuilder renderArray(JsArray value, int maxLength, StringBuilder output) { |
|
130 output.append('['); |
|
131 SortedMap<Integer, ? extends JsVariable> indexToElement = value.toSparseArray(); |
|
132 boolean isFirst = true; |
|
133 int maxLengthWithoutLastBracket = maxLength - 1; |
|
134 StringBuilder elementBuilder = new StringBuilder(); |
|
135 int entriesWritten = 0; |
|
136 for (Map.Entry<Integer, ? extends JsVariable> entry : indexToElement.entrySet()) { |
|
137 Integer index = entry.getKey(); |
|
138 JsVariable var = entry.getValue(); |
|
139 if (!isFirst) { |
|
140 output.append(','); |
|
141 } else { |
|
142 isFirst = false; |
|
143 } |
|
144 elementBuilder.setLength(0); |
|
145 elementBuilder.append(index).append('='); |
|
146 renderInternal(var.getValue(), maxLengthWithoutLastBracket /* essentially, no limit */, |
|
147 false, elementBuilder); |
|
148 if (output.length() + elementBuilder.length() >= maxLengthWithoutLastBracket) { |
|
149 // reached max length |
|
150 appendNMore(output, indexToElement.size() - entriesWritten); |
|
151 break; |
|
152 } else { |
|
153 output.append(elementBuilder.toString()); |
|
154 entriesWritten++; |
|
155 } |
|
156 } |
|
157 return output.append(']'); |
|
158 } |
|
159 |
|
160 private StringBuilder renderObject(JsObject value, int maxLength, StringBuilder output) { |
|
161 output.append('['); |
|
162 Collection<? extends JsVariable> properties = value.getProperties(); |
|
163 boolean isFirst = true; |
|
164 int maxLengthWithoutLastBracket = maxLength - 1; |
|
165 StringBuilder elementBuilder = new StringBuilder(); |
|
166 int entriesWritten = 0; |
|
167 for (JsVariable property : properties) { |
|
168 String name = property.getName(); |
|
169 if (!isFirst) { |
|
170 output.append(','); |
|
171 } else { |
|
172 isFirst = false; |
|
173 } |
|
174 elementBuilder.setLength(0); |
|
175 elementBuilder.append(name).append('='); |
|
176 renderInternal(property.getValue(), maxLengthWithoutLastBracket /* essentially, no limit */, |
|
177 false, elementBuilder); |
|
178 if (output.length() + elementBuilder.length() >= maxLengthWithoutLastBracket) { |
|
179 // reached max length |
|
180 appendNMore(output, properties.size() - entriesWritten); |
|
181 break; |
|
182 } else { |
|
183 output.append(elementBuilder.toString()); |
|
184 entriesWritten++; |
|
185 } |
|
186 } |
|
187 return output.append(']'); |
|
188 } |
|
189 |
|
190 private StringBuilder appendNMore(StringBuilder output, int n) { |
|
191 return output.append(" +").append(n).append(ELLIPSIS); //$NON-NLS-1$ |
|
192 } |
|
193 } |