From 53440282c33373cd2131a68ff71c085185c94bf9 Mon Sep 17 00:00:00 2001 From: Simon Stewart Date: Tue, 15 May 2018 15:33:04 +0100 Subject: [PATCH] Switch to our own classes for outputting JSON --- .../selenium/json/BeanToJsonConverter.java | 235 ----------- .../src/org/openqa/selenium/json/Json.java | 8 +- .../org/openqa/selenium/json/JsonOutput.java | 366 +++++++++++++++--- .../org/openqa/selenium/json/JsonType.java | 2 + .../openqa/selenium/json/JsonOutputTest.java | 71 +++- .../org/openqa/selenium/json/JsonTest.java | 14 +- .../remote/http/JsonHttpCommandCodecTest.java | 2 +- java/server/src/org/openqa/grid/BUCK | 1 + 8 files changed, 382 insertions(+), 317 deletions(-) delete mode 100644 java/client/src/org/openqa/selenium/json/BeanToJsonConverter.java diff --git a/java/client/src/org/openqa/selenium/json/BeanToJsonConverter.java b/java/client/src/org/openqa/selenium/json/BeanToJsonConverter.java deleted file mode 100644 index d83eba2435b81..0000000000000 --- a/java/client/src/org/openqa/selenium/json/BeanToJsonConverter.java +++ /dev/null @@ -1,235 +0,0 @@ -// 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 static java.util.concurrent.TimeUnit.MILLISECONDS; - -import com.google.common.collect.ImmutableMap; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonNull; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; - -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.logging.LogLevelMapping; -import org.openqa.selenium.remote.SessionId; - -import java.io.File; -import java.lang.reflect.Method; -import java.net.URL; -import java.util.Arrays; -import java.util.Collection; -import java.util.Date; -import java.util.Map; -import java.util.function.BiFunction; -import java.util.function.Predicate; -import java.util.logging.Level; -import java.util.stream.Collector; - -/** - * Utility class for converting between JSON and Java Objects. - */ -class BeanToJsonConverter { - - private static final int MAX_DEPTH = 5; - - private final Gson gson; - - private final Map>, BiFunction> converters; - - public BeanToJsonConverter() { - this(Json.GSON); - } - - public BeanToJsonConverter(Gson gson) { - this.gson = gson; - - this.converters = ImmutableMap.>, BiFunction>builder() - // Java types - - .put(Boolean.class::isAssignableFrom, (depth, o) -> new JsonPrimitive((Boolean) o)) - .put(CharSequence.class::isAssignableFrom, (depth, o) -> new JsonPrimitive(String.valueOf(o))) - .put(Date.class::isAssignableFrom, (depth, o) -> new JsonPrimitive(MILLISECONDS.toSeconds(((Date) o).getTime()))) - .put(Enum.class::isAssignableFrom, (depth, o) -> new JsonPrimitive(o.toString())) - .put(File.class::isAssignableFrom, (depth, o) -> new JsonPrimitive(((File) o).getAbsolutePath())) - .put(Number.class::isAssignableFrom, (depth, o) -> new JsonPrimitive((Number) o)) - .put(URL.class::isAssignableFrom, (depth, o) -> new JsonPrimitive(((URL) o).toExternalForm())) - - // *sigh* gson - .put(JsonElement.class::isAssignableFrom, (depth, o) -> (JsonElement) o) - - // Selenium classes - .put(Level.class::isAssignableFrom, (depth, o) -> new JsonPrimitive(LogLevelMapping.getName((Level) o))) - .put(SessionId.class::isAssignableFrom, (depth, o) -> { - JsonObject converted = new JsonObject(); - converted.addProperty("value", o.toString()); - return converted; - }) - - - // Special handling of asMap and toJson - .put( - cls -> getMethod(cls, "toJson") != null, - (depth, o) -> convertUsingMethod("toJson", o, depth)) - .put( - cls -> getMethod(cls, "asMap") != null, - (depth, o) -> convertUsingMethod("asMap", o, depth)) - .put( - cls -> getMethod(cls, "toMap") != null, - (depth, o) -> convertUsingMethod("toMap", o, depth)) - - // And then the collection types - .put( - Collection.class::isAssignableFrom, - (depth, o) -> ((Collection) o).stream() - .map(obj -> convertObject(obj, depth - 1)) - .collect(Collector.of(JsonArray::new, JsonArray::add, (l, r) -> { l.addAll(r); return l;}))) - .put( - Map.class::isAssignableFrom, - (depth, o) -> { - JsonObject converted = new JsonObject(); - ((Map) o).forEach( - (key, value) -> converted.add(String.valueOf(key), convertObject(value, depth - 1))); - return converted; - }) - .put( - Class::isArray, - (depth, o) -> { - JsonArray converted = new JsonArray(); - Arrays.stream(((Object[]) o)).forEach(value -> converted.add(convertObject(value, depth -1))); - return converted; - } - ) - - // Finally, attempt to convert as an object - .put(cls -> true, (depth, o) -> mapObject(o, depth - 1)) - .build(); - } - - /** - * Convert an object that may or may not be a JsonElement into its JSON string - * representation, handling the case where it is neither in a graceful way. - * - * @param object which needs conversion - * @return the JSON string representation of object - */ - public String convert(Object object) { - if (object == null) { - return null; - } - - try { - JsonElement json = convertObject(object, MAX_DEPTH); - return gson.toJson(json); - } catch (WebDriverException e) { - throw e; - } catch (Exception e) { - throw new WebDriverException("Unable to convert: " + object, e); - } - } - - /** - * Convert an object that may or may not be a JsonElement into its JSON object - * representation, handling the case where it is neither in a graceful way. - * - * @param object which needs conversion - * @return the JSON object representation of object - * @deprecated Use {@link #convert(Object)} and normal java types. - */ - @Deprecated - JsonElement convertObject(Object object) { - return convertObject(object, MAX_DEPTH); - } - - @SuppressWarnings("unchecked") - private JsonElement convertObject(Object toConvert, int maxDepth) { - if (toConvert == null) { - return JsonNull.INSTANCE; - } - - return converters.entrySet().stream() - .filter(entry -> entry.getKey().test(toConvert.getClass())) - .map(Map.Entry::getValue) - .findFirst() - .map(to -> to.apply(maxDepth, toConvert)) - .orElse(null); - } - - private Method getMethod(Class clazz, String methodName) { - try { - Method method = clazz.getMethod(methodName); - method.setAccessible(true); - return method; - } catch (NoSuchMethodException | SecurityException e) { - // fall through - } - - return null; - - } - - private JsonElement convertUsingMethod(String methodName, Object toConvert, int depth) { - try { - Method method = getMethod(toConvert.getClass(), methodName); - Object value = method.invoke(toConvert); - - return convertObject(value, depth); - } catch (ReflectiveOperationException e) { - throw new WebDriverException(e); - } - } - - private JsonElement mapObject(Object toConvert, int maxDepth) { - if (maxDepth < 1) { - return JsonNull.INSTANCE; - } - - // Raw object via reflection? Nope, not needed - JsonObject mapped = new JsonObject(); - for (SimplePropertyDescriptor pd : SimplePropertyDescriptor - .getPropertyDescriptors(toConvert.getClass())) { - if ("class".equals(pd.getName())) { - mapped.addProperty("class", toConvert.getClass().getName()); - continue; - } - - // Only include methods not on java.lang.Object to stop things being super-noisy - Method readMethod = pd.getReadMethod(); - if (readMethod == null || Object.class.equals(readMethod.getDeclaringClass())) { - continue; - } - - if (readMethod.getParameterTypes().length > 0) { - continue; - } - - readMethod.setAccessible(true); - - try { - Object result = readMethod.invoke(toConvert); - mapped.add(pd.getName(), convertObject(result, maxDepth - 1)); - } catch (ReflectiveOperationException e) { - throw new WebDriverException(e); - } - } - - return mapped; - } -} diff --git a/java/client/src/org/openqa/selenium/json/Json.java b/java/client/src/org/openqa/selenium/json/Json.java index 5ac2faab3a597..2403793164ccb 100644 --- a/java/client/src/org/openqa/selenium/json/Json.java +++ b/java/client/src/org/openqa/selenium/json/Json.java @@ -17,7 +17,6 @@ package org.openqa.selenium.json; -import com.google.common.io.CharStreams; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -44,7 +43,6 @@ public class Json { public static final Type OBJECT_TYPE = new TypeToken() {}.getType(); private final JsonTypeCoercer fromJson = new JsonTypeCoercer(); - private final BeanToJsonConverter toJson = new BeanToJsonConverter(); public String toJson(Object toConvert) { try (Writer writer = new StringWriter(); @@ -76,10 +74,6 @@ public JsonInput newInput(Reader from) throws UncheckedIOException { } public JsonOutput newOutput(Appendable to) throws UncheckedIOException { - try { - return new JsonOutput(toJson, GSON.newJsonWriter(CharStreams.asWriter(to))); - } catch (IOException e) { - throw new UncheckedIOException(e); - } + return new JsonOutput(to); } } diff --git a/java/client/src/org/openqa/selenium/json/JsonOutput.java b/java/client/src/org/openqa/selenium/json/JsonOutput.java index 6bfe63fee6968..4b8b51e0d7e0c 100644 --- a/java/client/src/org/openqa/selenium/json/JsonOutput.java +++ b/java/client/src/org/openqa/selenium/json/JsonOutput.java @@ -17,88 +17,350 @@ package org.openqa.selenium.json; -import com.google.gson.stream.JsonWriter; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.JsonElement; + +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.logging.LogLevelMapping; +import org.openqa.selenium.remote.SessionId; import java.io.Closeable; +import java.io.File; import java.io.IOException; -import java.io.UncheckedIOException; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Date; +import java.util.Deque; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; public class JsonOutput implements Closeable { - private final JsonWriter jsonWriter; - private final BeanToJsonConverter toJson; + private static final Logger LOG = Logger.getLogger(JsonOutput.class.getName()); + private static final int MAX_DEPTH = 5; + + private final Map>, SafeBiConsumer> converters; + private final Appendable appendable; + private final Consumer appender; + private Deque stack; + + JsonOutput(Appendable appendable) { + this.appendable = Objects.requireNonNull(appendable); + + this.appender = + str -> { + try { + appendable.append(str); + } catch (IOException e) { + throw new JsonException("Unable to write to underlying appendable", e); + } + }; + + this.stack = new ArrayDeque<>(); + this.stack.addFirst(new Empty()); + + // Order matters, since we want to handle null values first to avoid exceptions, and then then + // common kinds of inputs next. + this.converters = ImmutableMap.>, SafeBiConsumer>builder() + .put(Objects::isNull, (obj, depth) -> append("null")) + .put(CharSequence.class::isAssignableFrom, (obj, depth) -> append(asString(obj))) + .put(Number.class::isAssignableFrom, (obj, depth) -> append(obj.toString())) + .put(Boolean.class::isAssignableFrom, (obj, depth) -> append((Boolean) obj ? "true" : "false")) + .put(Date.class::isAssignableFrom, (obj, depth) -> append(String.valueOf(MILLISECONDS.toSeconds(((Date) obj).getTime())))) + .put(Enum.class::isAssignableFrom, (obj, depth) -> append(asString(obj))) + .put(File.class::isAssignableFrom, (obj, depth) -> append(((File) obj).getAbsolutePath())) + .put(URL.class::isAssignableFrom, (obj, depth) -> append(asString(((URL) obj).toExternalForm()))) + .put(Level.class::isAssignableFrom, (obj, depth) -> append(LogLevelMapping.getName((Level) obj))) + .put( + SessionId.class::isAssignableFrom, + (obj, depth) -> { + beginObject(); + name("value"); + write(obj.toString()); + endObject(); + }) + .put( + JsonElement.class::isAssignableFrom, + (obj, depth) -> { + LOG.log( + Level.WARNING, + "Attempt to convert JsonElement from GSON. This functionality is deprecated. " + + "Diagnostic stacktrace follows", + new JsonException("Stack trace to determine cause of warning")); + append(obj.toString()); + }) + // Special handling of asMap and toJson + .put( + cls -> getMethod(cls, "toJson") != null, + (obj, depth) -> convertUsingMethod("toJson", obj, depth)) + .put( + cls -> getMethod(cls, "asMap") != null, + (obj, depth) -> convertUsingMethod("asMap", obj, depth)) + .put( + cls -> getMethod(cls, "toMap") != null, + (obj, depth) -> convertUsingMethod("toMap", obj, depth)) + + // And then the collection types + .put( + Collection.class::isAssignableFrom, + (obj, depth) -> { + beginArray(); + ((Collection) obj).forEach(o -> write(o, depth - 1)); + endArray(); + }) + + .put( + Map.class::isAssignableFrom, + (obj, depth) -> { + beginObject(); + ((Map) obj).forEach( + (key, value) -> name(String.valueOf(key)).write(value, depth - 1)); + endObject(); + }) + .put( + Class::isArray, + (obj, depth) -> { + beginArray(); + Stream.of((Object[]) obj).forEach(o -> write(o, depth - 1)); + endArray(); + }) - JsonOutput(BeanToJsonConverter toJson, JsonWriter jsonWriter) { - this.jsonWriter = jsonWriter; - this.jsonWriter.setIndent(" "); - this.toJson = toJson; + // Finally, attempt to convert as an object + .put(cls -> true, (obj, depth) -> mapObject(obj, depth - 1)) + + .build(); } - @Override - public void close() throws IOException { - jsonWriter.close(); + public JsonOutput beginObject() { + stack.getFirst().write("{"); + stack.addFirst(new JsonObject()); + return this; } - public JsonOutput write(JsonInput input) { - try { - Object read = input.read(Json.OBJECT_TYPE); - jsonWriter.jsonValue(toJson.convert(read)); - return this; - } catch (IOException e) { - throw new UncheckedIOException(e); + public JsonOutput name(String name) { + if (!(stack.getFirst() instanceof JsonObject)) { + throw new JsonException("Attempt to write name, but not writing a json object: " + name); } + ((JsonObject) stack.getFirst()).name(name); + return this; } - public JsonOutput write(Object input) { - try { - jsonWriter.jsonValue(toJson.convert(input)); - return this; - } catch (IOException e) { - throw new UncheckedIOException(e); + public JsonOutput endObject() { + if (!(stack.getFirst() instanceof JsonObject)) { + throw new JsonException("Attempt to close a json object, but not writing a json object"); } + stack.removeFirst(); + appender.accept("}"); + return this; } - public JsonOutput beginObject() { - try { - jsonWriter.beginObject(); - return this; - } catch (IOException e) { - throw new UncheckedIOException(e); + public JsonOutput beginArray() { + append("["); + stack.addFirst(new JsonCollection()); + return this; + } + + public JsonOutput endArray() { + if (!(stack.getFirst() instanceof JsonCollection)) { + throw new JsonException("Attempt to close a json array, but not writing a json array"); } + stack.removeFirst(); + appender.accept("]"); + return this; } - public JsonOutput endObject() { - try { - jsonWriter.endObject(); - return this; - } catch (IOException e) { - throw new UncheckedIOException(e); + public JsonOutput write(Object value) { + return write(value, MAX_DEPTH); + } + + public JsonOutput write(Object input, int depthRemaining) { + converters.entrySet().stream() + .filter(entry -> entry.getKey().test(input == null ? null : input.getClass())) + .findFirst() + .map(Map.Entry::getValue) + .orElseThrow(() -> new JsonException("Unable to write " + input)) + .accept(input, depthRemaining); + + return this; + } + + public void close() { + if (appendable instanceof Closeable) { + try { + ((Closeable) appendable).close(); + } catch (IOException e) { + throw new JsonException(e); + } + } + + if (!(stack.getFirst() instanceof Empty)) { + throw new JsonException("Attempting to close incomplete json stream"); } } - public JsonOutput name(String name) { + private JsonOutput append(String text) { + stack.getFirst().write(text); + return this; + } + + private String asString(Object obj) { + // https://www.json.org has some helpful comments on characters to escape + StringBuilder toReturn = new StringBuilder("\""); + + String.valueOf(obj) + .chars() + .mapToObj( + i -> { + switch (i) { + case '"': return "\\\""; + case '\\': return "\\\\"; + case '\b': return "\\b"; + case '\f': return "\\f"; + case '\n': return "\\n"; + case '\r': return "\\r"; + case '\t': return "\\t"; + default: return "" + (char) i; + } + }) + .forEach(toReturn::append); + + toReturn.append('"'); + + return toReturn.toString(); + } + + private Method getMethod(Class clazz, String methodName) { try { - jsonWriter.name(name); - return this; - } catch (IOException e) { - throw new UncheckedIOException(e); + Method method = clazz.getMethod(methodName); + method.setAccessible(true); + return method; + } catch (NoSuchMethodException | SecurityException e) { + return null; } } - public JsonOutput beginArray() { + private JsonOutput convertUsingMethod(String methodName, Object toConvert, int depth) { try { - jsonWriter.beginArray(); - return this; - } catch (IOException e) { - throw new UncheckedIOException(e); + Method method = getMethod(toConvert.getClass(), methodName); + Object value = method.invoke(toConvert); + + return write(value, depth); + } catch (ReflectiveOperationException e) { + throw new JsonException(e); } } - public JsonOutput endArray() { - try { - jsonWriter.endArray(); - return this; - } catch (IOException e) { - throw new UncheckedIOException(e); + private void mapObject(Object toConvert, int maxDepth) { + if (maxDepth < 1) { + append("null"); + return; + } + + // Raw object via reflection? Nope, not needed + beginObject(); + for (SimplePropertyDescriptor pd : + SimplePropertyDescriptor.getPropertyDescriptors(toConvert.getClass())) { + + if ("class".equals(pd.getName())) { + name("class").write(toConvert.getClass().getName()); + continue; + } + + // Only include methods not on java.lang.Object to stop things being super-noisy + Method readMethod = pd.getReadMethod(); + if (readMethod == null || Object.class.equals(readMethod.getDeclaringClass())) { + continue; + } + + if (readMethod.getParameterTypes().length > 0) { + continue; + } + + readMethod.setAccessible(true); + + try { + Object result = readMethod.invoke(toConvert); + name(pd.getName()); + write(result, maxDepth - 1); + } catch (ReflectiveOperationException e) { + throw new WebDriverException(e); + } + } + endObject(); + } + + private class Node { + protected boolean isEmpty = true; + + public void write(String text) { + if (isEmpty) { + isEmpty = false; + } else { + appender.accept(", "); + } + + appender.accept(text); + } + } + + private class Empty extends Node { + + @Override + public void write(String text) { + if (!isEmpty) { + throw new JsonException("Only allowed to write one value to a json stream"); + } + + super.write(text); + } + } + + private class JsonCollection extends Node { + } + + private class JsonObject extends Node { + private boolean isNameNext = true; + + public void name(String name) { + if (!isNameNext) { + throw new JsonException("Unexpected attempt to set name of json object: " + name); + } + isNameNext = false; + super.write(asString(name)); + appender.accept(": "); + } + + @Override + public void write(String text) { + if (isNameNext) { + throw new JsonException("Unexpected attempt to write value before name: " + text); + } + isNameNext = true; + + appender.accept(text); + } + } + + @FunctionalInterface + private interface SafeBiConsumer extends BiConsumer { + void consume(T t, U u) throws IOException; + + @Override + default void accept(T t, U u) { + try { + consume(t, u); + } catch (IOException e) { + throw new JsonException(e); + } } } } diff --git a/java/client/src/org/openqa/selenium/json/JsonType.java b/java/client/src/org/openqa/selenium/json/JsonType.java index ae1dc0115be6a..847907315f517 100644 --- a/java/client/src/org/openqa/selenium/json/JsonType.java +++ b/java/client/src/org/openqa/selenium/json/JsonType.java @@ -23,7 +23,9 @@ public enum JsonType { NULL, NUMBER, START_MAP, + END_MAP, START_COLLECTION, + END_COLLECTION, STRING ; } diff --git a/java/client/test/org/openqa/selenium/json/JsonOutputTest.java b/java/client/test/org/openqa/selenium/json/JsonOutputTest.java index 8493f632736d7..3f2e799781e9f 100644 --- a/java/client/test/org/openqa/selenium/json/JsonOutputTest.java +++ b/java/client/test/org/openqa/selenium/json/JsonOutputTest.java @@ -24,10 +24,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.openqa.selenium.json.Json.MAP_TYPE; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedSet; import com.google.gson.Gson; @@ -159,11 +162,8 @@ public void testShouldConvertEnumsWithMethods() { @Test public void testNullAndAnEmptyStringAreEncodedDifferently() { - BeanToJsonConverter - converter = new BeanToJsonConverter(); - - String nullValue = converter.convert(null); - String emptyString = converter.convert(""); + String nullValue = convert(null); + String emptyString = convert(""); assertNotEquals(emptyString, nullValue); } @@ -290,13 +290,19 @@ public Set toJson() { @Test public void testShouldCallAsMapMethodIfPresent() { String json = convert(new Mappable1("a key", "a value")); - assertEquals("{\"a key\":\"a value\"}", json); + + Map value = new Json().toType(json, MAP_TYPE); + + assertEquals(ImmutableMap.of("a key", "a value"), value); } @Test public void testShouldCallToMapMethodIfPresent() { String json = convert(new Mappable2("a key", "a value")); - assertEquals("{\"a key\":\"a value\"}", json); + + Map value = new Json().toType(json, MAP_TYPE); + + assertEquals(ImmutableMap.of("a key", "a value"), value); } @Test @@ -323,16 +329,16 @@ private void verifyStackTraceInJson(String json, StackTraceElement[] stackTrace) for (StackTraceElement e : stackTrace) { if (e.getFileName() != null) { // Native methods may have null filenames - assertTrue("Filename not found", json.contains("\"fileName\":\"" + e.getFileName() + "\"")); + assertTrue("Filename not found", json.contains("\"fileName\": \"" + e.getFileName() + "\"")); } assertTrue("Line number not found", - json.contains("\"lineNumber\":" + e.getLineNumber() + "")); + json.contains("\"lineNumber\": " + e.getLineNumber() + "")); assertTrue("class not found.", - json.contains("\"class\":\"" + e.getClass().getName() + "\"")); + json.contains("\"class\": \"" + e.getClass().getName() + "\"")); assertTrue("class name not found", - json.contains("\"className\":\"" + e.getClassName() + "\"")); + json.contains("\"className\": \"" + e.getClassName() + "\"")); assertTrue("method name not found.", - json.contains("\"methodName\":\"" + e.getMethodName() + "\"")); + json.contains("\"methodName\": \"" + e.getMethodName() + "\"")); int posOfCurrStackTraceElement = json.indexOf(e.getMethodName()); assertTrue("Mismatch in order of stack trace elements.", @@ -345,8 +351,8 @@ public void testShouldBeAbleToConvertARuntimeException() { RuntimeException clientError = new RuntimeException("foo bar baz!"); StackTraceElement[] stackTrace = clientError.getStackTrace(); String json = convert(clientError); - assertTrue(json.contains("\"message\":\"foo bar baz!\"")); - assertTrue(json.contains("\"class\":\"java.lang.RuntimeException\"")); + assertTrue(json.contains("\"message\": \"foo bar baz!\"")); + assertTrue(json.contains("\"class\": \"java.lang.RuntimeException\"")); assertTrue(json.contains("\"stackTrace\"")); verifyStackTraceInJson(json, stackTrace); } @@ -445,7 +451,10 @@ public void testProperlyConvertsNulls() { Map frameId = new HashMap<>(); frameId.put("id", null); String payload = convert(frameId); - assertEquals("{\"id\":null}", payload); + + Map result = new Json().toType(payload, MAP_TYPE); + assertTrue(result.containsKey("id")); + assertNull(result.get("id")); } @Test @@ -547,6 +556,38 @@ public void shouldNotIncludePropertiesFromJavaLangObjectOtherThanClass() { .forEach(name -> assertFalse(name, converted.keySet().contains(name))); } + @Test + public void shouldAllowValuesToBeStreamedToACollection() throws IOException { + StringBuilder builder = new StringBuilder(); + + try (JsonOutput jsonOutput = new Json().newOutput(builder)) { + jsonOutput.beginArray() + .write("brie") + .write("peas") + .endArray(); + } + + assertEquals( + ImmutableList.of("brie", "peas"), + new Json().toType(builder.toString(), Object.class)); + } + + @Test + public void shouldAllowValuesToBeStreamedToAnObject() { + StringBuilder builder = new StringBuilder(); + + try (JsonOutput jsonOutput = new Json().newOutput(builder)) { + jsonOutput.beginObject() + .name("cheese").write("brie") + .name("vegetable").write("peas") + .endObject(); + } + + assertEquals( + ImmutableMap.of("cheese", "brie", "vegetable", "peas"), + new Json().toType(builder.toString(), MAP_TYPE)); + } + private String convert(Object toConvert) { try (Writer writer = new StringWriter(); JsonOutput jsonOutput = new Json().newOutput(writer)) { diff --git a/java/client/test/org/openqa/selenium/json/JsonTest.java b/java/client/test/org/openqa/selenium/json/JsonTest.java index acf141dc0f73d..550e51465bf77 100644 --- a/java/client/test/org/openqa/selenium/json/JsonTest.java +++ b/java/client/test/org/openqa/selenium/json/JsonTest.java @@ -227,7 +227,7 @@ public void testShouldProperlyFillInACapabilitiesObject() { DesiredCapabilities capabilities = new DesiredCapabilities("browser", CapabilityType.VERSION, Platform.ANY); capabilities.setJavascriptEnabled(true); - String text = new BeanToJsonConverter().convert(capabilities); + String text = new Json().toJson(capabilities); Capabilities readCapabilities = new Json().toType(text, DesiredCapabilities.class); @@ -286,7 +286,7 @@ public void testCanHandleValueBeingAnArray() { response.setValue(value); response.setStatus(1512); - String json = new BeanToJsonConverter().convert(response); + String json = new Json().toJson(response); Response converted = new Json().toType(json, Response.class); assertEquals("bar", response.getSessionId()); @@ -299,7 +299,7 @@ public void testShouldConvertObjectsInArraysToMaps() { Date date = new Date(); Cookie cookie = new Cookie("foo", "bar", "localhost", "/rooted", date, true, true); - String rawJson = new BeanToJsonConverter().convert(Collections.singletonList(cookie)); + String rawJson = new Json().toJson(Collections.singletonList(cookie)); List list = new Json().toType(rawJson, List.class); Object first = list.get(0); @@ -324,7 +324,7 @@ private void assertMapEntry(Map map, String key, Object expected) { @Test public void testShouldConvertAnArrayBackIntoAnArray() { Exception e = new Exception(); - String converted = new BeanToJsonConverter().convert(e); + String converted = new Json().toJson(e); Map reconstructed = new Json().toType(converted, Map.class); List trace = (List) reconstructed.get("stackTrace"); @@ -334,7 +334,7 @@ public void testShouldConvertAnArrayBackIntoAnArray() { @Test public void testShouldBeAbleToReconsituteASessionId() { - String json = new BeanToJsonConverter().convert(new SessionId("id")); + String json = new Json().toJson(new SessionId("id")); SessionId sessionId = new Json().toType(json, SessionId.class); assertEquals("id", sessionId.toString()); @@ -347,7 +347,7 @@ public void testShouldBeAbleToConvertACommand() { sessionId, DriverCommand.NEW_SESSION, ImmutableMap.of("food", "cheese")); - String raw = new BeanToJsonConverter().convert(original); + String raw = new Json().toJson(original); Command converted = new Json().toType(raw, Command.class); assertEquals(sessionId.toString(), converted.getSessionId().toString()); @@ -361,7 +361,7 @@ public void testShouldBeAbleToConvertACommand() { public void testShouldConvertCapabilitiesToAMapAndIncludeCustomValues() { Capabilities caps = new ImmutableCapabilities("furrfu", "fishy"); - String raw = new BeanToJsonConverter().convert(caps); + String raw = new Json().toJson(caps); Capabilities converted = new Json().toType(raw, Capabilities.class); assertEquals("fishy", converted.getCapability("furrfu")); diff --git a/java/client/test/org/openqa/selenium/remote/http/JsonHttpCommandCodecTest.java b/java/client/test/org/openqa/selenium/remote/http/JsonHttpCommandCodecTest.java index 042dc98cfe32b..e04ab0bb108d5 100644 --- a/java/client/test/org/openqa/selenium/remote/http/JsonHttpCommandCodecTest.java +++ b/java/client/test/org/openqa/selenium/remote/http/JsonHttpCommandCodecTest.java @@ -113,7 +113,7 @@ public void encodingAPostWithUrlParameters() { codec.defineCommand("foo", POST, "/foo/:bar/baz"); Command command = new Command(null, "foo", ImmutableMap.of("bar", "apples123")); - String encoding = "{\"bar\":\"apples123\"}"; + String encoding = "{\"bar\": \"apples123\"}"; HttpRequest request = codec.encode(command); assertThat(request.getMethod(), is(POST)); diff --git a/java/server/src/org/openqa/grid/BUCK b/java/server/src/org/openqa/grid/BUCK index cb621ab0f79b3..28db73403ea56 100644 --- a/java/server/src/org/openqa/grid/BUCK +++ b/java/server/src/org/openqa/grid/BUCK @@ -17,6 +17,7 @@ java_library(name = 'grid', ]), deps = [ '//java/client/src/org/openqa/selenium:selenium', + '//java/client/src/org/openqa/selenium/json:json', '//java/client/src/org/openqa/selenium/remote:remote', '//java/client/src/org/openqa/selenium/firefox:firefox', '//java/client/src/org/openqa/selenium/safari:safari',