// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you 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 org.openqa.selenium.json;

import com.google.common.io.CharStreams;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;

public class Json {
  final static Gson GSON = new GsonBuilder()
      .registerTypeAdapterFactory(ListAdapter.FACTORY)
      .registerTypeAdapterFactory(MapAdapter.FACTORY)
      .setLenient()
      .serializeNulls()
      .disableHtmlEscaping()
      .create();

  public static final Type LIST_OF_MAPS_TYPE = new TypeToken<List<Map<String, Object>>>() {}.getType();
  public static final Type MAP_TYPE = new TypeToken<Map<String, Object>>() {}.getType();
  public static final Type OBJECT_TYPE = new TypeToken<Object>() {}.getType();

  private final JsonToBeanConverter toBean = new JsonToBeanConverter();
  private final BeanToJsonConverter toJson = new BeanToJsonConverter();

  public String toJson(Object toConvert) {
    return toJson.convert(toConvert);
  }

  /**
   * @deprecated No replacement. Use raw java types instead.
   */
  @Deprecated
  public JsonElement toJsonElement(Object toConvert) {
    return toJson.convertObject(toConvert);
  }

  public <T> T toType(Object source, Class<T> typeOfT) {
    return toBean.convert(typeOfT, source);
  }

  public <T> T toType(Object source, Type typeOfT) {
    Class<?> type;
    if (typeOfT instanceof ParameterizedType) {
      type = (Class<?>) ((ParameterizedType) typeOfT).getRawType();
    } else if (typeOfT instanceof Class) {
      type = (Class<?>) typeOfT;
    } else {
      throw new IllegalArgumentException("Unable to convert type: " + typeOfT);
    }
    //noinspection unchecked
    return (T) toBean.convert(type, source);
  }

  private static Object readValue(JsonReader in, Gson gson) throws IOException {
    switch (in.peek()) {
      case BEGIN_ARRAY:
        return gson.fromJson(in, List.class);

      case BEGIN_OBJECT:
        return gson.fromJson(in, Map.class);

      case BOOLEAN:
      case NULL:
      case STRING:
        return gson.fromJson(in, Object.class);

      case NUMBER:
        String number = in.nextString();
        if (number.indexOf('.') != -1) {
          return Double.parseDouble(number);
        }
        return Long.parseLong(number);

      default:
        throw new JsonParseException("Unexpected type: " + in.peek());
    }
  }

  public JsonInput newInput(Reader from) throws UncheckedIOException {
    return new JsonInput(GSON, GSON.newJsonReader(from));
  }

  public JsonOutput newOutput(Appendable to) throws UncheckedIOException {
    try {
      return new JsonOutput(toJson, GSON.newJsonWriter(CharStreams.asWriter(to)));
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
  }

  private static class MapAdapter extends TypeAdapter<Map<?, ?>> {

    private final static TypeAdapterFactory FACTORY = new TypeAdapterFactory() {
      @SuppressWarnings("unchecked")
      @Override
      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        if (type.getRawType() == Map.class) {
          return (TypeAdapter<T>) new MapAdapter(gson);
        }
        return null;
      }
    };

    private final Gson gson;

    private MapAdapter(Gson gson) {
      this.gson = Objects.requireNonNull(gson);
    }

    @Override
    public Map<?, ?> read(JsonReader in) throws IOException {
      if (in.peek() == JsonToken.NULL) {
        in.nextNull();
        return null;
      }

      Map<String, Object> map = new TreeMap<>();
      in.beginObject();

      while (in.hasNext()) {
        String key = in.nextName();
        Object value = readValue(in, gson);

        map.put(key, value);
      }

      in.endObject();
      return map;
    }

    @Override
    public void write(JsonWriter out, Map<?, ?> value) throws IOException {
      // It's fine to use GSON's own default writer for this.
      out.beginObject();
      for (Map.Entry<?, ?> entry : value.entrySet()) {
        out.name(String.valueOf(entry.getKey()));
        @SuppressWarnings("unchecked")
        TypeAdapter<Object>
            adapter =
            (TypeAdapter<Object>) gson.getAdapter(entry.getValue().getClass());
        adapter.write(out, entry.getValue());
      }
      out.endObject();
    }
  }

  private static class ListAdapter extends TypeAdapter<List<?>> {

    private final static TypeAdapterFactory FACTORY = new TypeAdapterFactory() {
      @SuppressWarnings("unchecked")
      @Override
      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        if (type.getRawType() == List.class) {
          return (TypeAdapter<T>) new ListAdapter(gson);
        }
        return null;
      }
    };

    private final Gson gson;

    private ListAdapter(Gson gson) {
      this.gson = Objects.requireNonNull(gson);
    }

    @Override
    public List<?> read(JsonReader in) throws IOException {
      if (in.peek() == JsonToken.NULL) {
        in.nextNull();
        return null;
      }

      List<Object> list = new LinkedList<>();
      in.beginArray();

      while (in.hasNext()) {
        list.add(readValue(in, gson));
      }

      in.endArray();
      return list;
    }

    @Override
    public void write(JsonWriter out, List<?> value) throws IOException {
      out.beginArray();
      for (Object entry : value) {
        @SuppressWarnings("unchecked")
        TypeAdapter<Object> adapter = (TypeAdapter<Object>) gson.getAdapter(entry.getClass());
        adapter.write(out, entry);
      }
      out.endArray();
    }
  }
}

@@ -873,16 +873,60 @@
 treams;%0A
+import com.google.common.reflect.TypeToken;%0A
 import c
@@ -1144,50 +1144,8 @@
 ry;%0A
-import com.google.gson.reflect.TypeToken;%0A
 impo
@@ -4338,32 +4338,56 @@
 eate(Gson gson, 
+com.google.gson.reflect.
 TypeToken%3CT%3E typ
@@ -5890,16 +5890,40 @@
 n gson, 
+com.google.gson.reflect.
 TypeToke

Switch to use guava type token. Should be a no-op