// Copyright (c) 2009 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.debug.core.util; import java.util.Collection; import java.util.Map; import java.util.SortedMap; import org.chromium.sdk.JsArray; import org.chromium.sdk.JsObject; import org.chromium.sdk.JsValue; import org.chromium.sdk.JsVariable; import org.chromium.sdk.JsValue.Type; /** * A converter of JsValues into human-readable strings used in various contexts. */ public class JsValueStringifier { /** * A configuration class */ public static class Config { /** * The maximum length of the text to render. If the value length exceeds * the limit, an ellipsis will be used to truncate the value. * The default is 80 characters. */ public int maxLength = 80; } private static final String ELLIPSIS = "..."; //$NON-NLS-1$ private static final String UNKNOWN_VALUE = "<null>"; //$NON-NLS-1$ private final Config config; /** * Constructs a visible string for the given {@code value} (without exposing * the value structure). Encloses JavaScript string values in double quotes. * * @param value to build a visible string for * @return {@code value.getValueString()} (enclosed in double quotes if * {@code value.getType() == TYPE_STRING}), or {@code null} if * {@code value==null} */ public static String toVisibleString(JsValue value) { return possiblyQuoteValueString(value); } private static String possiblyQuoteValueString(JsValue value) { if (value == null) { return UNKNOWN_VALUE; } String valueString = value.getValueString(); return value.getType() == JsValue.Type.TYPE_STRING ? "\"" + valueString + "\"" //$NON-NLS-1$ //$NON-NLS-2$ : valueString; } /** * Use the default config values. */ public JsValueStringifier() { this.config = new Config(); } /** * Use the specified {@code config} data. * @param config to use when rendering values. */ public JsValueStringifier(Config config) { this.config = config; } public String render(JsValue value) { if (value == null) { return UNKNOWN_VALUE; } StringBuilder output = new StringBuilder(); renderInternal(value, config.maxLength, true, output); return output.toString(); } /** * @param value to render * @param maxLength the maximum length of the {@code output} * @param descend whether to descend into the object contents * @param output to render into */ private void renderInternal(JsValue value, int maxLength, boolean descend, StringBuilder output) { if (!descend) { renderPrimitive(value, maxLength, output); return; } Type type = value.getType(); // TODO(apavlov): implement good stringification of other types? switch (type) { case TYPE_ARRAY: renderArray(value.asObject().asArray(), maxLength, output); break; case TYPE_OBJECT: renderObject(value.asObject(), maxLength, output); break; default: renderPrimitive(value, maxLength, output); break; } } private void renderPrimitive(JsValue value, int maxLength, StringBuilder output) { output.append(possiblyQuoteValueString(value)); truncate(output, maxLength, ELLIPSIS); } private void truncate(StringBuilder valueBuilder, int maxLength, String suffix) { int length = valueBuilder.length(); if (length > maxLength) { valueBuilder.setLength(maxLength); valueBuilder.replace( maxLength - suffix.length(), maxLength, suffix); } } private StringBuilder renderArray(JsArray value, int maxLength, StringBuilder output) { output.append('['); SortedMap<Integer, ? extends JsVariable> indexToElement = value.toSparseArray(); boolean isFirst = true; int maxLengthWithoutLastBracket = maxLength - 1; StringBuilder elementBuilder = new StringBuilder(); int entriesWritten = 0; for (Map.Entry<Integer, ? extends JsVariable> entry : indexToElement.entrySet()) { Integer index = entry.getKey(); JsVariable var = entry.getValue(); if (!isFirst) { output.append(','); } else { isFirst = false; } elementBuilder.setLength(0); elementBuilder.append(index).append('='); renderInternal(var.getValue(), maxLengthWithoutLastBracket /* essentially, no limit */, false, elementBuilder); if (output.length() + elementBuilder.length() >= maxLengthWithoutLastBracket) { // reached max length appendNMore(output, indexToElement.size() - entriesWritten); break; } else { output.append(elementBuilder.toString()); entriesWritten++; } } return output.append(']'); } private StringBuilder renderObject(JsObject value, int maxLength, StringBuilder output) { output.append('['); Collection<? extends JsVariable> properties = value.getProperties(); boolean isFirst = true; int maxLengthWithoutLastBracket = maxLength - 1; StringBuilder elementBuilder = new StringBuilder(); int entriesWritten = 0; for (JsVariable property : properties) { String name = property.getName(); if (!isFirst) { output.append(','); } else { isFirst = false; } elementBuilder.setLength(0); elementBuilder.append(name).append('='); renderInternal(property.getValue(), maxLengthWithoutLastBracket /* essentially, no limit */, false, elementBuilder); if (output.length() + elementBuilder.length() >= maxLengthWithoutLastBracket) { // reached max length appendNMore(output, properties.size() - entriesWritten); break; } else { output.append(elementBuilder.toString()); entriesWritten++; } } return output.append(']'); } private StringBuilder appendNMore(StringBuilder output, int n) { return output.append(" +").append(n).append(ELLIPSIS); //$NON-NLS-1$ } }