blob: 9e3f8091ae9d02c622338e665dc57c9fc68b04f6 [file] [log] [blame]
/*
* Copyright (C) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.caliper;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
/**
* Ordinarily serialization should be done within the class that is being serialized. However,
* many of these classes are used by GWT, which dies when it sees Gson.
*/
public final class Json {
/**
* This Gson instance must be used when serializing a class that includes a Run as a member
* or as a member of a member (etc.), otherwise the Map<Scenario, ScenarioResult> will not
* be correctly serialized.
*/
private static final Gson GSON_INSTANCE =
new GsonBuilder()
.registerTypeAdapter(Date.class, new DateTypeAdapter())
.registerTypeAdapter(Run.class, new RunTypeAdapter())
.registerTypeAdapter(Measurement.class, new MeasurementDeserializer())
.create();
public static Gson getGsonInstance() {
return GSON_INSTANCE;
}
public static String measurementSetToJson(MeasurementSet measurementSet) {
return new Gson().toJson(measurementSet);
}
/**
* Attempts to extract a MeasurementSet from a string, assuming it is JSON. If this fails, it
* tries to extract it from the string assuming it is a space-separated list of double values.
*/
public static MeasurementSet measurementSetFromJson(String measurementSetJson) {
try {
return getGsonInstance().fromJson(measurementSetJson, MeasurementSet.class);
} catch (JsonParseException e) {
// might be an old MeasurementSet, so fall back on failure to the old, space separated
// serialization method.
try {
String[] measurementStrings = measurementSetJson.split("\\s+");
List<Measurement> measurements = new ArrayList<Measurement>();
for (String s : measurementStrings) {
measurements.add(
new Measurement(ImmutableMap.of("ns", 1, "us", 1000, "ms", 1000000, "s", 1000000000),
Double.valueOf(s), Double.valueOf(s)));
}
// seconds and variations is the default unit
return new MeasurementSet(measurements.toArray(new Measurement[measurements.size()]));
} catch (NumberFormatException ignore) {
throw new IllegalArgumentException("Not a measurement set: " + measurementSetJson);
}
}
}
public static MeasurementSet measurementSetFromJson(JsonObject measurementSetJson) {
return getGsonInstance().fromJson(measurementSetJson, MeasurementSet.class);
}
/**
* Backwards compatibility!
*/
private static class MeasurementDeserializer implements JsonDeserializer<Measurement> {
@Override public Measurement deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext context) throws JsonParseException {
JsonObject obj = jsonElement.getAsJsonObject();
if (obj.has("raw") && obj.has("processed")) {
return new Measurement(
context.<Map<String, Integer>>deserialize(obj.get("unitNames"),
new TypeToken<Map<String, Integer>>() {}.getType()),
context.<Double>deserialize(obj.get("raw"), Double.class),
context.<Double>deserialize(obj.get("processed"), Double.class));
}
if (obj.has("nanosPerRep") && obj.has("unitsPerRep") && obj.has("unitNames")) {
return new Measurement(
context.<Map<String, Integer>>deserialize(obj.get("unitNames"),
new TypeToken<Map<String, Integer>>() {}.getType()),
context.<Double>deserialize(obj.get("nanosPerRep"), Double.class),
context.<Double>deserialize(obj.get("unitsPerRep"), Double.class));
}
throw new JsonParseException(obj.toString());
}
}
/**
* This adapter is necessary because gson doesn't handle Maps more complex than Map<String, ...>
* in a useful way. For example, Map<Scenario, ScenarioResult>'s serialized version simply uses
* Scenario.toString() as the keys. This adapter stores this Map as lists of
* KeyValuePair<Scenario, ScenarioResult> instead, to preserve the Scenario objects on
* deserialization.
*/
private static class RunTypeAdapter implements JsonSerializer<Run>, JsonDeserializer<Run> {
@Override public Run deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext context) throws JsonParseException {
List<KeyValuePair<Scenario, ScenarioResult>> mapList = context.deserialize(
jsonElement.getAsJsonObject().get("measurements"),
new TypeToken<List<KeyValuePair<Scenario, ScenarioResult>>>() {}.getType());
Map<Scenario, ScenarioResult> measurements = new LinkedHashMap<Scenario, ScenarioResult>();
for (KeyValuePair<Scenario, ScenarioResult> entry : mapList) {
measurements.put(entry.getKey(), entry.getValue());
}
String benchmarkName =
context.deserialize(jsonElement.getAsJsonObject().get("benchmarkName"), String.class);
Date executedTimestamp = context.deserialize(
jsonElement.getAsJsonObject().get("executedTimestamp"), Date.class);
return new Run(measurements, benchmarkName, executedTimestamp);
}
@Override public JsonElement serialize(Run run, Type type, JsonSerializationContext context) {
JsonObject result = new JsonObject();
result.add("benchmarkName", context.serialize(run.getBenchmarkName()));
result.add("executedTimestamp", context.serialize(run.getExecutedTimestamp()));
List<KeyValuePair<Scenario, ScenarioResult>> mapList =
new ArrayList<KeyValuePair<Scenario, ScenarioResult>>();
for (Map.Entry<Scenario, ScenarioResult> entry : run.getMeasurements().entrySet()) {
mapList.add(new KeyValuePair<Scenario, ScenarioResult>(entry.getKey(), entry.getValue()));
}
result.add("measurements", context.serialize(mapList,
new TypeToken<List<KeyValuePair<Scenario, ScenarioResult>>>() {}.getType()));
return result;
}
}
private static class DateTypeAdapter implements JsonSerializer<Date>, JsonDeserializer<Date> {
private final DateFormat dateFormat;
private DateTypeAdapter() {
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
}
@Override public synchronized JsonElement serialize(Date date, Type type,
JsonSerializationContext jsonSerializationContext) {
return new JsonPrimitive(dateFormat.format(date));
}
@Override public synchronized Date deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext jsonDeserializationContext) {
String dateString = jsonElement.getAsString();
// first try to parse as an ISO 8601 date
try {
return dateFormat.parse(dateString);
} catch (ParseException ignored) {
}
// next, try a GSON-style locale-specific dates (for Caliper r282 and earlier)
try {
return DateFormat.getDateTimeInstance().parse(dateString);
} catch (ParseException ignored) {
}
throw new JsonParseException(dateString);
}
}
/**
* This is similar to the Map.Entry class, but is necessary since Entrys are not supported
* by gson.
*/
private static class KeyValuePair<K, V> {
private K k;
private V v;
KeyValuePair(K k, V v) {
this.k = k;
this.v = v;
}
public K getKey() {
return k;
}
public V getValue() {
return v;
}
@SuppressWarnings("unused")
private KeyValuePair() {} // for gson
}
private Json() {} // static class
}