aboutsummaryrefslogtreecommitdiff
path: root/caliper/src/main/java/com/google/caliper/Json.java
blob: 9e3f8091ae9d02c622338e665dc57c9fc68b04f6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/*
 * 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
}