blob: 14e35432124e31bc7b91e2ac1b39654b29290399 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016 Ericsson
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License 2.0 which
* accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.tracecompass.common.core.log;
import java.text.DecimalFormat;
import java.text.Format;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.jdt.annotation.Nullable;
/**
* Logger helper
*
* This is a logger helper, it will allow entry-exit analysis to be much easier.
*
* The events are saved in a JSON-like message in the phase of the event. It is
* an event type but with extra information associated to it. Typical types can
* be the following.
* <ul>
* <li>Durations
* <ul>
* <li><strong>B</strong>, Begin</li>
* <li><strong>E</strong>, End</li>
* <li><strong>X</strong>, Complete, this is an event with a duration field</li>
* <li><strong>i</strong>, Instant / Info</li>
* </ul>
* </li>
* <li>Asynchronous nested messages
* <ul>
* <li><strong>b</strong>, nested begin</li>
* <li><strong>n</strong>, nested info</li>
* <li><strong>e</strong>, nested end</li>
* </ul>
* </li>
* <li>Flows
* <ul>
* <li><strong>s</strong>, flow begin</li>
* <li><strong>t</strong>, flow step (info)</li>
* <li><strong>f</strong>, flow end</li>
* </ul>
* </li>
* <li>Object tracking
* <ul>
* <li><strong>N</Strong>, Object created</li>
* <li><strong>D</Strong>, Object destroyed</li>
* </ul>
* </li>
* <li>Mark Events - events that generate markers
* <ul>
* <li><strong>R</strong>, Marker event</li>
* </ul>
* </li>
* <li>CounterEvents - events that count items
* <ul>
* <li><strong>C</strong>, Counter event</li>
* </ul>
* </li>
* </ul>
* <p>
* To use <strong>durations</strong> and/or <strong>flows</strong>, see
* {@link ScopeLog} and {@link FlowScopeLog}. These 2 concepts are related.
* Durations would typically be used to instrument simple methods, while flows
* would be preferred if there are links to be made with other threads.
* <p>
* To use <strong>Asynchronous nested messages</strong>, see
* {@link #traceAsyncStart(Logger, Level, String, String, int, Object...)}, and
* {@link #traceAsyncEnd(Logger, Level, String, String, int, Object...)}
* <p>
* To use <strong>Object tracking</strong>, see
* {@link #traceObjectCreation(Logger, Level, Object)} and
* {@link #traceObjectDestruction(Logger, Level, Object)}
*
* The design philosophy of this class is very heavily inspired by the trace
* event format of Google. The full specification is available <a
* href=https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/edit?pli=1#>here</a>.
* <p>
*
* The main goals are clarity of output and simplicity for the developer.
* Performance is a nice to have, but is not the main concern of this helper. A
* minor performance impact compared to simply logging the events is to be
* expected.
*
* @author Matthew Khouzam
* @since 3.0
* @noinstantiate This class is not intended to be instantiated by clients. It
* is a helper class.
*/
public final class TraceCompassLogUtils {
private static final Format FORMAT = new DecimalFormat("#.###"); //$NON-NLS-1$
/*
* Field names
*/
private static final String ARGS = "args"; //$NON-NLS-1$
private static final String NAME = "name"; //$NON-NLS-1$
private static final String CATEGORY = "cat"; //$NON-NLS-1$
private static final String ID = "id"; //$NON-NLS-1$
private static final String TID = "tid"; //$NON-NLS-1$
private static final String PID = "pid"; //$NON-NLS-1$
private static final String TIMESTAMP = "ts"; //$NON-NLS-1$
private static final String PHASE = "ph"; //$NON-NLS-1$
private static final String ARGS_ERROR_MESSAGE = "Data should be in the form of key, value, key1, value1, ... TraceCompassScopeLog was supplied "; //$NON-NLS-1$
private static final AtomicInteger ID_GENERATOR = new AtomicInteger(0);
private TraceCompassLogUtils() {
// do nothing
}
/**
* Scope Logger helper. This will automatically log entry and exit of the
* scope. This scope log will be shown under any scope enclosing it, but
* will not be the source, or destination of any link to other scopes. If
* relations should be done with other scopes, the {@link FlowScopeLog}
* class is more appropriate.
*
* Usage:
*
* <pre>
* {@code usage of ScopeLog}
* try (ScopeLog linksLogger = new ScopeLog(LOGGER, Level.CONFIG, "Perform Query")) { //$NON-NLS-1$
* ss.updateAllReferences();
* dataStore.addAll(ss.query(ts, trace));
* }
* </pre>
* <p>
* will generate the following trace
*
* <pre>
* {@code trace output}
* INFO: {"ts":12345,"ph":"B",tid:1,"name:Perform Query"}
* INFO: {"ts":"12366,"ph":"E","tid":1}
* </pre>
*/
public static class ScopeLog implements AutoCloseable {
private final long fThreadId;
private final Logger fLogger;
private final Level fLevel;
private final Map<String, Object> fData = new HashMap<>();
/**
* Scope logger constructor
*
* @param log
* the JUL logger to log to
* @param level
* the log level see {@link Level}
* @param label
* The label of the event pair
* @param args
* Additional messages to pass for this scope, should be in
* pairs key, value, key2, value2.... typically arguments.
* Note that these arguments will be logged only at the
* beginning of the scope
*/
public ScopeLog(Logger log, Level level, String label, Object... args) {
long time = System.nanoTime();
fLogger = log;
fLevel = level;
fThreadId = Thread.currentThread().getId();
fLogger.log(fLevel, (() -> {
StringBuilder sb = new StringBuilder();
sb.append('{');
appendCommon(sb, 'B', time, fThreadId);
appendName(sb, label);
appendArgs(sb, args);
sb.append('}');
return sb.toString();
}));
}
/**
* Add a tag to the scope logger, will be written at the exit. This can
* save space on the trace by having a field appended to an event rather
* than writing a whole new event for a small chunk of data.
*
* If the timing information is important than it would be more
* appropriate to call
* {@link TraceCompassLogUtils#traceInstant(Logger, Level, String, Object...)}
*
* @param name
* the name of the field
* @param value
* The value of the field.
*/
public void addData(String name, Object value) {
fData.put(name, value);
}
@Override
public void close() {
long time = System.nanoTime();
fLogger.log(fLevel, (() -> {
StringBuilder sb = new StringBuilder();
sb.append('{');
appendCommon(sb, 'E', time, fThreadId);
return appendArgs(sb, fData).append('}').toString();
}));
}
}
/**
* Builder class for the {@link FlowScopeLog}. One can either set a category
* or a parent scope before building the flow scope log. If none is set, a
* default category called "null" will be used.
*
* @author Geneviève Bastien
*/
public static class FlowScopeLogBuilder {
private final Logger fLogger;
private final Level fLevel;
private final String fLabel;
private final Object[] fArgs;
private int fId = Integer.MIN_VALUE;
private @Nullable String fCategory = null;
private @Nullable FlowScopeLog fParent = null;
private boolean fHasParent = false;
/**
* Flow scope log builder constructor
*
* @param logger
* the JUL logger
* @param level
* the log level see {@link Level}
* @param label
* The label of the event pair
* @param args
* the messages to pass, should be in pairs key, value, key2,
* value2.... typically arguments
*/
public FlowScopeLogBuilder(Logger logger, Level level, String label, Object... args) {
fLogger = logger;
fLevel = level;
fLabel = label;
fArgs = args;
}
/**
* Set a category for the flow scope. When building the scope, an ID
* will be automatically generated.
*
* This method is mutually exclusive with
* {@link #setParentScope(FlowScopeLog)}. Calling both will throw an
* exception.
*
* @param category
* The category of this flow
* @return This builder
*/
public FlowScopeLogBuilder setCategory(String category) {
if (fParent != null) {
throw new IllegalStateException("FlowScopeLogBuilder: Cannot set a category if a parent has already been set"); //$NON-NLS-1$
}
fCategory = category;
return this;
}
/**
* Set a category and ID for the flow scope. When building the scope
*
* This method is mutually exclusive with
* {@link #setParentScope(FlowScopeLog)}. Calling both will throw an
* exception.
*
* @param category
* The category of this flow
* @param id
* The ID of this flow
* @return This builder
*/
public FlowScopeLogBuilder setCategoryAndId(String category, int id) {
if (fParent != null) {
throw new IllegalStateException("FlowScopeLogBuilder: Cannot set a category if a parent has already been set"); //$NON-NLS-1$
}
fCategory = category;
fId = id;
// Id is already set, so assume this scope has a parent, even if the
// parent object is not available
fHasParent = true;
return this;
}
/**
* Set a parent scope for the flow scope to build. The scope will have
* the same category and ID as the parent scope.
*
* This method is mutually exclusive with {@link #setCategory(String)}
* and {@link #setCategoryAndId(String, int)}. Calling both will throw
* an exception.
*
* @param parent
* The parent scope
* @return This builder
*/
public FlowScopeLogBuilder setParentScope(FlowScopeLog parent) {
if (fCategory != null) {
throw new IllegalStateException("FlowScopeLogBuilder: Cannot set a parent scope if a category has already been set"); //$NON-NLS-1$
}
fParent = parent;
return this;
}
/**
* Build the flow scope log
*
* @return The flow scope log
*/
public FlowScopeLog build() {
FlowScopeLog parent = fParent;
if (parent != null) {
// Has a parent scope, so step in flow
return new FlowScopeLog(fLogger, fLevel, fLabel, parent.fCategory, parent.fId, false, fArgs);
}
return new FlowScopeLog(fLogger, fLevel, fLabel, String.valueOf(fCategory), (fId == Integer.MIN_VALUE ? ID_GENERATOR.incrementAndGet() : fId), !fHasParent, fArgs);
}
}
/**
* Flow Scope Logger helper. It will automatically log entry and exit of the
* scope. It can be used with other flow scopes to follow the program flow
* across threads. To do so, these scopes save more data, so take more disk
* space. If there is no inter-process/thread communication to follow, the
* {@link ScopeLog} class would be more appropriate.
*
* Usage: this can be used to track asynchronous threads communication. This
* can be used in scatter-gather/map-reduce operations as well as threads
* that trigger a UI Thread operation.
*
* <pre>
* {@code usage of FlowScopeLog}
* try (FlowScopeLog linksLogger = new FlowScopeLog(LOGGER, Level.CONFIG, "Perform Query", "category", 0x100)) { //$NON-NLS-1$
* Display.asynchExec(()->{
* try(FlowScopeLog linksLogger2 = new FlowScopeLog(LOGGER, Level.CONFIG, "Update UI", "category", linksLogger.getId()) {
* linksLogger.step("updating ui");
* };
* linksLogger.step("forked thread");
* }
* </pre>
* <p>
* will generate the following trace (order not guaranteed)
*
* <pre>
* {@code trace output}
* INFO: {"ts":12345,"ph":"s",tid:1,"name":"Perform Query", "cat":"category", "id":256}
* INFO: {"ts":12346","ph":"t",tid:1,"name":"forked thread","cat":"category", "id":256}
* INFO: {"ts":"12366,"ph":"f","tid":1,"cat":"category", "id":256}
* INFO: {"ts":12400,"ph":"s",tid:0,"name":"Update UI","cat":"category", "id":256}
* INFO: {"ts":12416","ph":"t",tid:0,"name":"updating ui", "cat":"category", "id":256}
* INFO: {"ts":"12420,"ph":"f","tid":0,"cat":"category", "id":256}
* </pre>
*/
public static class FlowScopeLog implements AutoCloseable {
private final long fThreadId;
private final Logger fLogger;
private final Level fLevel;
private final int fId;
private final String fCategory;
private final Map<String, Object> fData = new HashMap<>();
/**
* Flow scope logger constructor
*
* @param log
* the JUL logger
* @param level
* the log level see {@link Level}
* @param label
* The label of the event pair
* @param category
* the category of the flow events
* @param id
* The id of the flow
* @param startFlow
* Whether this flow scope object is the start of a flow, or
* a step
* @param args
* the messages to pass, should be in pairs key, value, key2,
* value2.... typically arguments
*/
private FlowScopeLog(Logger log, Level level, String label, String category, int id, boolean startFlow, Object... args) {
long time = System.nanoTime();
fId = id;
fLogger = log;
fLevel = level;
fCategory = category;
fThreadId = Thread.currentThread().getId();
fLogger.log(fLevel, (() -> {
StringBuilder sb = new StringBuilder();
sb.append('{');
appendCommon(sb, 'B', time, fThreadId);
appendName(sb, label);
appendArgs(sb, args);
sb.append('}');
return sb.toString();
}));
// Add a flow event, either start or step in enclosing scope
fLogger.log(fLevel, (() -> {
StringBuilder sb = new StringBuilder();
sb.append('{');
appendCommon(sb, startFlow ? 's' : 't', time, fThreadId);
appendName(sb, label);
appendCategory(sb, category);
appendId(sb, fId);
appendArgs(sb, args);
sb.append('}');
return sb.toString();
}));
}
/**
* Flow step, it will add a stop point for an arrow
*
* @param label
* The label for this step
* @param args
* the arguments to log
*/
public void step(String label, Object... args) {
long time = System.nanoTime();
fLogger.log(fLevel, (() -> {
StringBuilder sb = new StringBuilder();
sb.append('{');
appendCommon(sb, 't', time, fThreadId);
appendName(sb, label);
appendCategory(sb, fCategory);
appendId(sb, fId);
appendArgs(sb, args);
sb.append('}');
return sb.toString();
}));
}
/**
* Add a tag to the scope logger, will be written at the exit. This can
* save space on the trace by having a field appended to an event rather
* than writing a whole new event for a small chunk of data.
*
*
* If the timing information is important, then it would be more
* appropriate to call {@link #step(String, Object...)}
*
* @param name
* the name of the field
* @param value
* The value of the field.
*/
public void addData(String name, Object value) {
fData.put(name, value);
}
/**
* Get the ID for this scope. The ID can be injected to other components
* that can use it for the scope loggers
*
* @return The ID of this scope
*/
public int getId() {
return fId;
}
@Override
public void close() {
long time = System.nanoTime();
fLogger.log(fLevel, (() -> {
StringBuilder sb = new StringBuilder();
sb.append('{');
appendCommon(sb, 'E', time, fThreadId);
appendArgs(sb, fData);
sb.append('}');
return sb.toString();
}));
}
}
/**
* Trace Object Creation, logs the beginning of an object's life cycle.
* Typically one can put this in the object's constructor. However if an
* object is mutable, it can be tracked through phases with this method,
* then the object can be re-used, however, the resulting analyses may yield
* erroneous data if precautions are not taken.
*
* For mutable objects, save the return value of the call. This will be
* passed to the destruction of the object and then it can be matched.
*
* @param logger
* The JUL logger
* @param level
* The {@link Level} of this event.
* @param item
* the Object to trace
* @return The unique ID of this object (there may be collisions)
*/
public static int traceObjectCreation(Logger logger, Level level, Object item) {
long time = System.nanoTime();
long threadId = Thread.currentThread().getId();
int identityHashCode = System.identityHashCode(item);
logger.log(level, () -> {
StringBuilder sb = new StringBuilder();
sb.append('{');
appendCommon(sb, 'N', time, threadId);
appendName(sb, item.getClass().getSimpleName());
appendId(sb, identityHashCode);
return sb.append('}').toString();
});
return identityHashCode;
}
/**
* Trace Object Destruction, logs the end of an object's life cycle.
* Typically one can put this in the object's Dispose(). However if an
* object is mutable, it can be tracked through phases with this method,
* then the object can be re-used, however, the resulting analyses may yield
* erroneous data if precautions are not taken.
*
* @param logger
* The JUL logger
* @param level
* The {@link Level} of this event.
* @param item
* the Object to trace
*/
public static void traceObjectDestruction(Logger logger, Level level, Object item) {
long time = System.nanoTime();
long threadId = Thread.currentThread().getId();
logger.log(level, () -> {
StringBuilder sb = new StringBuilder();
sb.append('{');
appendCommon(sb, 'D', time, threadId);
appendName(sb, item.getClass().getSimpleName());
appendId(sb, System.identityHashCode(item));
return sb.append('}').toString();
});
}
/**
* Trace Object Destruction, logs the end of an object's life cycle.
* Typically one can put this in the object's Dispose(). However if an
* object is mutable, it can be tracked through phases with this method,
* then the object can be re-used, however, the resulting analyses may be
*
* @param logger
* The JUL logger
* @param level
* The {@link Level} of this event.
* @param item
* the Object to trace
* @param uniqueId
* The unique ID
*/
public static void traceObjectDestruction(Logger logger, Level level, Object item, int uniqueId) {
long time = System.nanoTime();
long threadId = Thread.currentThread().getId();
logger.log(level, () -> {
StringBuilder sb = new StringBuilder();
sb.append('{');
appendCommon(sb, 'D', time, threadId);
appendName(sb, item.getClass().getSimpleName());
appendId(sb, uniqueId);
return sb.append('}').toString();
});
}
/**
* Asynchronous events are used to specify asynchronous operations, such as
* an asynchronous (or synchronous) draw, or a network operation. Call this
* method at the beginning of such an operation.
*
* @param logger
* The JUL logger
* @param level
* The {@link Level} of this event.
* @param name
* The name of the asynchronous message
* @param category
* the category of the asynchronous event
* @param id
* The unique ID of a transaction
* @param args
* Additional arguments to log
*/
public static void traceAsyncStart(Logger logger, Level level, @Nullable String name, @Nullable String category, int id, Object... args) {
long time = System.nanoTime();
long threadId = Thread.currentThread().getId();
logger.log(level, () -> {
StringBuilder sb = new StringBuilder();
sb.append('{');
appendCommon(sb, 'b', time, threadId);
appendName(sb, name);
appendCategory(sb, category);
appendId(sb, id);
return appendArgs(sb, args).append('}').toString();
});
}
/**
* Asynchronous events are used to specify asynchronous operations, such as
* an asynchronous (or synchronous) draw, or a network operation. Call this
* method to augment the asynchronous event with nested information.
*
* @param logger
* The JUL logger
* @param level
* The {@link Level} of this event.
* @param name
* The name of the asynchronous message
* @param category
* the category of the asynchronous event
* @param id
* The unique ID of a transaction
* @param args
* Additional arguments to log
*/
public static void traceAsyncNested(Logger logger, Level level, @Nullable String name, @Nullable String category, int id, Object... args) {
long time = System.nanoTime();
long threadId = Thread.currentThread().getId();
logger.log(level, () -> {
StringBuilder sb = new StringBuilder();
sb.append('{');
appendCommon(sb, 'n', time, threadId);
appendName(sb, name);
appendCategory(sb, category);
appendId(sb, id);
return appendArgs(sb, args).append('}').toString();
});
}
/**
* Asynchronous events are used to specify asynchronous operations, such as
* an asynchronous (or synchronous) draw, or a network operation. Call this
* method at the end of such an operation.
*
* @param logger
* The JUL logger
* @param level
* The {@link Level} of this event.
* @param name
* The name of the asynchronous message
* @param category
* the category of the asynchronous event
* @param id
* The unique ID of a transaction
* @param args
* Additional arguments to log
*/
public static void traceAsyncEnd(Logger logger, Level level, @Nullable String name, @Nullable String category, int id, Object... args) {
long time = System.nanoTime();
long threadId = Thread.currentThread().getId();
logger.log(level, () -> {
StringBuilder sb = new StringBuilder();
sb.append('{');
appendCommon(sb, 'e', time, threadId);
appendName(sb, name);
appendCategory(sb, category);
appendId(sb, id);
return appendArgs(sb, args).append('}').toString();
});
}
/**
* Instant events, created to indicate an item of interest has occurred,
* similar to a standard System.out.println() or a
* Java.util.Logger#log(Level). This one provides an event in a more
* structured way. This should be the method to call to save data that
* should have a zero duration, as it will ensure a log format that can then
* be parsed by a trace type.
*
* @param logger
* The JUL logger
* @param level
* The {@link Level} of this event.
* @param name
* The name of the asynchronous message
* @param args
* Additional arguments to log
*/
public static void traceInstant(Logger logger, Level level, String name, Object... args) {
long time = System.nanoTime();
long threadId = Thread.currentThread().getId();
logger.log(level, () -> {
StringBuilder sb = new StringBuilder();
sb.append('{');
appendCommon(sb, 'i', time, threadId);
appendName(sb, name);
return appendArgs(sb, args).append('}').toString();
});
}
/**
* The counter events can track a value or multiple values as they change
* over time.
*
* @param logger
* The Logger
* @param level
* The {@link Level} of this event.
* @param name
* The name of the asynchronous message
* @param args
* The counters to log in the format : "title", value
*/
public static void traceCounter(Logger logger, Level level, @Nullable String name, Object... args) {
long time = System.nanoTime();
long threadId = Thread.currentThread().getId();
logger.log(level, () -> {
StringBuilder sb = new StringBuilder();
sb.append('{');
appendCommon(sb, 'C', time, threadId);
appendName(sb, name);
return appendArgs(sb, args).append('}').toString();
});
}
/**
* The Marker events are events with a duration that define a region of
* interest. These regions can be displayed in views as Markers or other
* indicators.
*
* @param logger
* The Logger
* @param level
* The {@link Level} of this event.
* @param name
* The name of the marker message message
* @param duration
* How long the marker should last
* @param args
* The counters to log in the format : "title", value, note
* "color" and an rbga will be used
*/
public static void traceMarker(Logger logger, Level level, @Nullable String name, long duration, Object... args) {
long time = System.nanoTime();
long threadId = Thread.currentThread().getId();
logger.log(level, () -> {
StringBuilder sb = new StringBuilder();
sb.append('{');
appendCommon(sb, 'R', time, threadId);
appendName(sb, name);
sb.append(',');
writeObject(sb, "dur", duration); //$NON-NLS-1$
return appendArgs(sb, args).append('}').toString();
});
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
/*
* USE ME FIRST
*/
private static StringBuilder appendCommon(StringBuilder appendTo, char phase, long time, long threadId) {
writeObject(appendTo, TIMESTAMP, FORMAT.format((double) time / 1000)).append(','); // $NON-NLS-1$
writeObject(appendTo, PHASE, phase).append(',');
writeObject(appendTo, TID, threadId).append(',');
return writeObject(appendTo, PID, threadId); // $NON-NLS-1$
}
private static StringBuilder appendName(StringBuilder sb, @Nullable String name) {
if (name != null) {
sb.append(',');
writeObject(sb, NAME, name);
}
return sb;
}
private static StringBuilder appendCategory(StringBuilder sb, @Nullable String category) {
if (category != null) {
sb.append(',');
writeObject(sb, CATEGORY, category);
}
return sb;
}
private static StringBuilder appendId(StringBuilder sb, int id) {
return sb.append(',')
.append('"')
.append(ID)
.append("\":\"0x") //$NON-NLS-1$
.append(Integer.toHexString(id))
.append('"');
}
private static StringBuilder appendArgs(StringBuilder sb, Map<String, Object> args) {
if (!args.isEmpty()) {
sb.append(',')
.append('"')
.append(ARGS)
.append('"')
.append(':');
Object[] argsArray = new Object[2 * args.size()];
Iterator<Entry<String, Object>> entryIter = args.entrySet().iterator();
for (int i = 0; i < args.size(); i++) {
Entry<String, Object> entry = entryIter.next();
argsArray[i] = entry.getKey();
argsArray[i + 1] = entry.getValue();
}
getArgs(sb, argsArray);
}
return sb;
}
private static StringBuilder appendArgs(StringBuilder sb, Object... args) {
if (args.length > 0) {
sb.append(',')
.append('"')
.append(ARGS)
.append('"')
.append(':');
getArgs(sb, args);
}
return sb;
}
private static StringBuilder getArgs(StringBuilder appendTo, Object[] data) {
if (data.length == 0) {
return appendTo;
}
Set<String> tester = new HashSet<>();
appendTo.append('{');
if (data.length == 1) {
// not in contract, but let's assume here that people are still new
// at this
appendTo.append("\"msg\":\"").append(data[0]).append('"'); //$NON-NLS-1$
} else {
if (data.length % 2 != 0) {
throw new IllegalArgumentException(
ARGS_ERROR_MESSAGE + "an odd number of messages" + Arrays.asList(data).toString()); //$NON-NLS-1$
}
for (int i = 0; i < data.length - 1; i += 2) {
Object value = data[i + 1];
String keyVal = String.valueOf(data[i]);
if (tester.contains(keyVal)) {
throw new IllegalArgumentException(ARGS_ERROR_MESSAGE + "an duplicate field names : " + keyVal); //$NON-NLS-1$
}
tester.add(keyVal);
if (i > 0) {
appendTo.append(',');
}
writeObject(appendTo, keyVal, value);
}
}
return appendTo.append('}');
}
private static StringBuilder writeObject(StringBuilder appendTo, Object key, @Nullable Object value) {
appendTo.append('"').append(key).append('"').append(':');
if (value instanceof Number) {
appendTo.append(value);
} else {
appendTo.append('"').append(String.valueOf(value)).append('"');
}
return appendTo;
}
}