Skip to content

Commit

Permalink
[java] Increased the max depth of new session payload (#12205)
Browse files Browse the repository at this point in the history
Increased the max depth of new session payload

Co-authored-by: Diego Molina <[email protected]>
  • Loading branch information
joerg1985 and diemol authored Jun 15, 2023
1 parent ab6e4f8 commit 265e2f4
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 49 deletions.
8 changes: 6 additions & 2 deletions java/src/org/openqa/selenium/json/Json.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ public class Json {
private final JsonTypeCoercer fromJson = new JsonTypeCoercer();

public String toJson(Object toConvert) {
return toJson(toConvert, JsonOutput.MAX_DEPTH);
}

public String toJson(Object toConvert, int maxDepth) {
try (Writer writer = new StringWriter();
JsonOutput jsonOutput = newOutput(writer)) {
jsonOutput.write(toConvert);
JsonOutput jsonOutput = newOutput(writer)) {
jsonOutput.write(toConvert, maxDepth);
return writer.toString();
} catch (IOException e) {
throw new JsonException(e);
Expand Down
94 changes: 49 additions & 45 deletions java/src/org/openqa/selenium/json/JsonOutput.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@

public class JsonOutput implements Closeable {
private static final Logger LOG = Logger.getLogger(JsonOutput.class.getName());
private static final int MAX_DEPTH = 10;
static final int MAX_DEPTH = 10;

private static final Predicate<Class<?>> GSON_ELEMENT;

Expand Down Expand Up @@ -100,7 +100,7 @@ public class JsonOutput implements Closeable {
ESCAPES = Collections.unmodifiableMap(builder);
}

private final Map<Predicate<Class<?>>, SafeBiConsumer<Object, Integer>> converters;
private final Map<Predicate<Class<?>>, DepthAwareConsumer> converters;
private final Appendable appendable;
private final Consumer<String> appender;
private Deque<Node> stack;
Expand All @@ -126,32 +126,32 @@ public class JsonOutput implements Closeable {

// Order matters, since we want to handle null values first to avoid exceptions, and then then
// common kinds of inputs next.
Map<Predicate<Class<?>>, SafeBiConsumer<Object, Integer>> builder = new LinkedHashMap<>();
builder.put(Objects::isNull, (obj, depth) -> append("null"));
builder.put(CharSequence.class::isAssignableFrom, (obj, depth) -> append(asString(obj)));
builder.put(Number.class::isAssignableFrom, (obj, depth) -> append(obj.toString()));
Map<Predicate<Class<?>>, DepthAwareConsumer> builder = new LinkedHashMap<>();
builder.put(Objects::isNull, (obj, maxDepth, depthRemaining) -> append("null"));
builder.put(CharSequence.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append(asString(obj)));
builder.put(Number.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append(obj.toString()));
builder.put(
Boolean.class::isAssignableFrom, (obj, depth) -> append((Boolean) obj ? "true" : "false"));
Boolean.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append((Boolean) obj ? "true" : "false"));
builder.put(
Date.class::isAssignableFrom,
(obj, depth) -> append(String.valueOf(MILLISECONDS.toSeconds(((Date) obj).getTime()))));
(obj, maxDepth, depthRemaining) -> append(String.valueOf(MILLISECONDS.toSeconds(((Date) obj).getTime()))));
builder.put(
Instant.class::isAssignableFrom,
(obj, depth) -> append(asString(DateTimeFormatter.ISO_INSTANT.format((Instant) obj))));
builder.put(Enum.class::isAssignableFrom, (obj, depth) -> append(asString(obj)));
(obj, maxDepth, depthRemaining) -> append(asString(DateTimeFormatter.ISO_INSTANT.format((Instant) obj))));
builder.put(Enum.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append(asString(obj)));
builder.put(
File.class::isAssignableFrom, (obj, depth) -> append(((File) obj).getAbsolutePath()));
builder.put(URI.class::isAssignableFrom, (obj, depth) -> append(asString((obj).toString())));
File.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append(((File) obj).getAbsolutePath()));
builder.put(URI.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append(asString((obj).toString())));
builder.put(
URL.class::isAssignableFrom,
(obj, depth) -> append(asString(((URL) obj).toExternalForm())));
builder.put(UUID.class::isAssignableFrom, (obj, depth) -> append(asString(obj.toString())));
(obj, maxDepth, depthRemaining) -> append(asString(((URL) obj).toExternalForm())));
builder.put(UUID.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append(asString(obj.toString())));
builder.put(
Level.class::isAssignableFrom,
(obj, depth) -> append(asString(LogLevelMapping.getName((Level) obj))));
(obj, maxDepth, depthRemaining) -> append(asString(LogLevelMapping.getName((Level) obj))));
builder.put(
GSON_ELEMENT,
(obj, depth) -> {
(obj, maxDepth, depthRemaining) -> {
LOG.log(
Level.WARNING,
"Attempt to convert JsonElement from GSON. This functionality is deprecated. "
Expand All @@ -162,36 +162,36 @@ public class JsonOutput implements Closeable {
// Special handling of asMap and toJson
builder.put(
cls -> getMethod(cls, "toJson") != null,
(obj, depth) -> convertUsingMethod("toJson", obj, depth));
(obj, maxDepth, depthRemaining) -> convertUsingMethod("toJson", obj, maxDepth, depthRemaining));
builder.put(
cls -> getMethod(cls, "asMap") != null,
(obj, depth) -> convertUsingMethod("asMap", obj, depth));
(obj, maxDepth, depthRemaining) -> convertUsingMethod("asMap", obj, maxDepth, depthRemaining));
builder.put(
cls -> getMethod(cls, "toMap") != null,
(obj, depth) -> convertUsingMethod("toMap", obj, depth));
(obj, maxDepth, depthRemaining) -> convertUsingMethod("toMap", obj, maxDepth, depthRemaining));

// And then the collection types
builder.put(
Collection.class::isAssignableFrom,
(obj, depth) -> {
if (depth < 1) {
(obj, maxDepth, depthRemaining) -> {
if (depthRemaining < 1) {
throw new JsonException(
"Reached the maximum depth of " + MAX_DEPTH + " while writing JSON");
"Reached the maximum depth of " + maxDepth + " while writing JSON");
}
beginArray();
((Collection<?>) obj)
.stream()
.filter(o -> (!(o instanceof Optional) || ((Optional<?>) o).isPresent()))
.forEach(o -> write(o, depth - 1));
.forEach(o -> write0(o, maxDepth, depthRemaining - 1));
endArray();
});

builder.put(
Map.class::isAssignableFrom,
(obj, depth) -> {
if (depth < 1) {
(obj, maxDepth, depthRemaining) -> {
if (depthRemaining < 1) {
throw new JsonException(
"Reached the maximum depth of " + MAX_DEPTH + " while writing JSON");
"Reached the maximum depth of " + maxDepth + " while writing JSON");
}
beginObject();
((Map<?, ?>) obj)
Expand All @@ -200,45 +200,45 @@ public class JsonOutput implements Closeable {
if (value instanceof Optional && !((Optional) value).isPresent()) {
return;
}
name(String.valueOf(key)).write(value, depth - 1);
name(String.valueOf(key)).write0(value, maxDepth, depthRemaining - 1);
});
endObject();
});
builder.put(
Class::isArray,
(obj, depth) -> {
if (depth < 1) {
(obj, maxDepth, depthRemaining) -> {
if (depthRemaining < 1) {
throw new JsonException(
"Reached the maximum depth of " + MAX_DEPTH + " while writing JSON");
"Reached the maximum depth of " + maxDepth + " while writing JSON");
}
beginArray();
Stream.of((Object[]) obj)
.filter(o -> (!(o instanceof Optional) || ((Optional<?>) o).isPresent()))
.forEach(o -> write(o, depth - 1));
.forEach(o -> write0(o, maxDepth, depthRemaining - 1));
endArray();
});

builder.put(
Optional.class::isAssignableFrom,
(obj, depth) -> {
(obj, maxDepth, depthRemaining) -> {
Optional<?> optional = (Optional<?>) obj;
if (!optional.isPresent()) {
append("null");
return;
}

write(optional.get(), depth);
write0(optional.get(), maxDepth, depthRemaining);
});

// Finally, attempt to convert as an object
builder.put(
cls -> true,
(obj, depth) -> {
if (depth < 1) {
(obj, maxDepth, depthRemaining) -> {
if (depthRemaining < 1) {
throw new JsonException(
"Reached the maximum depth of " + MAX_DEPTH + " while writing JSON");
"Reached the maximum depth of " + maxDepth + " while writing JSON");
}
mapObject(obj, depth - 1);
mapObject(obj, maxDepth, depthRemaining - 1);
});

this.converters = Collections.unmodifiableMap(builder);
Expand Down Expand Up @@ -313,13 +313,17 @@ public JsonOutput write(Object value) {
return write(value, MAX_DEPTH);
}

public JsonOutput write(Object input, int depthRemaining) {
public JsonOutput write(Object value, int maxDepth) {
return write0(value, maxDepth, maxDepth);
}

private JsonOutput write0(Object input, int maxDepth, 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))
.consume(input, depthRemaining);
.consume(input, maxDepth, depthRemaining);

return this;
}
Expand Down Expand Up @@ -381,7 +385,7 @@ private Method getMethod(Class<?> clazz, String methodName) {
}
}

private JsonOutput convertUsingMethod(String methodName, Object toConvert, int depth) {
private JsonOutput convertUsingMethod(String methodName, Object toConvert, int maxDepth, int depthRemaining) {
try {
Method method = getMethod(toConvert.getClass(), methodName);
if (method == null) {
Expand All @@ -390,13 +394,13 @@ private JsonOutput convertUsingMethod(String methodName, Object toConvert, int d
}
Object value = method.invoke(toConvert);

return write(value, depth);
return write0(value, maxDepth, depthRemaining);
} catch (ReflectiveOperationException e) {
throw new JsonException(e);
}
}

private void mapObject(Object toConvert, int depthRemaining) {
private void mapObject(Object toConvert, int maxDepth, int depthRemaining) {
if (toConvert instanceof Class) {
write(((Class<?>) toConvert).getName());
return;
Expand All @@ -420,7 +424,7 @@ private void mapObject(Object toConvert, int depthRemaining) {
Object value = pd.getReadMethod().apply(toConvert);
if (!Optional.empty().equals(value)) {
name(pd.getName());
write(value, depthRemaining - 1);
write0(value, maxDepth, depthRemaining - 1);
}
}
endObject();
Expand Down Expand Up @@ -479,7 +483,7 @@ public void write(String text) {
}

@FunctionalInterface
private interface SafeBiConsumer<T, U> {
void consume(T t, U u);
private interface DepthAwareConsumer {
void consume(Object object, int maxDepth, int depthRemaining);
}
}
2 changes: 1 addition & 1 deletion java/src/org/openqa/selenium/remote/NewSessionPayload.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public static NewSessionPayload create(Map<String, ?> source) {
Require.precondition(
source.containsKey("capabilities"), "New session payload must contain capabilities");

String json = new Json().toJson(Require.nonNull("Payload", source));
String json = new Json().toJson(Require.nonNull("Payload", source), 100);
return new NewSessionPayload(new StringReader(json));
}

Expand Down
31 changes: 30 additions & 1 deletion java/test/org/openqa/selenium/json/JsonOutputTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,7 @@ public String getCheese() {
}

@Test
void shouldRespectMaxDepth() {
void shouldRespectDefaultMaxDepth() {
StringBuilder builder = new StringBuilder();

JsonOutput jsonOutput = new Json().newOutput(builder);
Expand All @@ -738,6 +738,35 @@ void shouldRespectMaxDepth() {
assertThatExceptionOfType(JsonException.class).isThrownBy(() -> jsonOutput.write(finalValue));
}

@Test
void shouldRespectCustomHigherMaxDepth() {
shouldRespectMaxDepth(16);
}

@Test
void shouldRespectCustomLowerMaxDepth() {
shouldRespectMaxDepth(8);
}

void shouldRespectMaxDepth(int maxDepth) {
StringBuilder builder = new StringBuilder();

JsonOutput jsonOutput = new Json().newOutput(builder);
jsonOutput.beginArray();

Object value = emptyList();

for (int i = 0; i < maxDepth; i++) {
jsonOutput.write(value, maxDepth);

value = singletonList(value);
}

Object finalValue = value;

assertThatExceptionOfType(JsonException.class).isThrownBy(() -> jsonOutput.write(finalValue, maxDepth));
}

private String convert(Object toConvert) {
try (Writer writer = new StringWriter();
JsonOutput jsonOutput = new Json().newOutput(writer)) {
Expand Down

0 comments on commit 265e2f4

Please sign in to comment.