feat(commons-serialize): port over gson streams as backend for new SerializeReader/SerializeWriter API in commons-serialize
This commit is contained in:
parent
108a370c51
commit
df78e10c6a
|
@ -7,7 +7,7 @@ plugins {
|
|||
dependencies {
|
||||
api(libs.gson)
|
||||
implementation(projects.commons)
|
||||
implementation(projects.commonsSerialize)
|
||||
api(projects.commonsSerialize)
|
||||
|
||||
testImplementation(libs.junit.jupiter.api)
|
||||
testRuntimeOnly(libs.junit.jupiter.engine)
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import io.gitlab.jfronny.scripts.*
|
||||
|
||||
plugins {
|
||||
commons.library
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.commons)
|
||||
api(projects.commonsSerialize)
|
||||
|
||||
testImplementation(libs.junit.jupiter.api)
|
||||
testImplementation(libs.google.truth)
|
||||
testRuntimeOnly(libs.junit.jupiter.engine)
|
||||
testRuntimeOnly(libs.junit.vintage)
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("maven") {
|
||||
groupId = "io.gitlab.jfronny"
|
||||
artifactId = "commons-serialize-json"
|
||||
|
||||
from(components["java"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.javadoc {
|
||||
linksOffline("https://maven.frohnmeyer-wds.de/javadoc/artifacts/io/gitlab/jfronny/commons/$version/raw", projects.commons)
|
||||
linksOffline("https://maven.frohnmeyer-wds.de/javadoc/artifacts/io/gitlab/jfronny/commons-serialize/$version/raw", projects.commonsSerialize)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,350 @@
|
|||
package io.gitlab.jfronny.commons.serialize.json;
|
||||
|
||||
import io.gitlab.jfronny.commons.serialize.StringEscapeUtil;
|
||||
import io.gitlab.jfronny.commons.serialize.stream.MalformedDataException;
|
||||
import io.gitlab.jfronny.commons.serialize.stream.SerializeWriter;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.*;
|
||||
|
||||
import static io.gitlab.jfronny.commons.serialize.json.impl.JsonScope.*;
|
||||
|
||||
public class JsonWriter extends SerializeWriter<IOException, JsonWriter> implements Closeable {
|
||||
private final Writer out;
|
||||
private int[] stack = new int[32];
|
||||
private int stackSize = 0;
|
||||
|
||||
{
|
||||
push(EMPTY_DOCUMENT);
|
||||
}
|
||||
|
||||
private String newline;
|
||||
private String indent;
|
||||
private String formattedColon;
|
||||
private String formattedComma;
|
||||
private boolean usesEmptyNewlineAndIndent;
|
||||
|
||||
private boolean omitQuotes = false;
|
||||
private String deferredName;
|
||||
private List<String> deferredComments = new LinkedList<>();
|
||||
|
||||
public JsonWriter(Writer out) {
|
||||
this.out = Objects.requireNonNull(out, "out == null");
|
||||
newline = indent = "";
|
||||
setIndent("");
|
||||
setNewline("");
|
||||
}
|
||||
|
||||
public JsonWriter setIndent(String indent) {
|
||||
if (indent == null || indent.isEmpty()) {
|
||||
this.indent = "";
|
||||
this.formattedColon = ":";
|
||||
this.formattedComma = ",";
|
||||
this.usesEmptyNewlineAndIndent = newline.isEmpty();
|
||||
} else {
|
||||
this.newline = "\n"; // if someone sets an indent, this is probably intended
|
||||
this.indent = indent;
|
||||
this.formattedColon = ": ";
|
||||
this.formattedComma = ",";
|
||||
this.usesEmptyNewlineAndIndent = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getIndent() {
|
||||
return indent;
|
||||
}
|
||||
|
||||
public JsonWriter setNewline(String newline) {
|
||||
if (newline == null || newline.isEmpty()) {
|
||||
this.newline = "";
|
||||
this.formattedComma = indent.isEmpty() ? "," : ", ";
|
||||
this.usesEmptyNewlineAndIndent = indent.isEmpty();
|
||||
} else {
|
||||
this.newline = newline;
|
||||
this.formattedComma = ",";
|
||||
this.usesEmptyNewlineAndIndent = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getNewline() {
|
||||
return newline;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter beginArray() throws IOException {
|
||||
writeDeferredName();
|
||||
return openScope(EMPTY_ARRAY, '[');
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter endArray() throws IOException {
|
||||
return closeScope(EMPTY_ARRAY, NONEMPTY_ARRAY, ']');
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter beginObject() throws IOException {
|
||||
writeDeferredName();
|
||||
return openScope(EMPTY_OBJECT, '{');
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter endObject() throws IOException {
|
||||
return closeScope(EMPTY_OBJECT, NONEMPTY_OBJECT, '}');
|
||||
}
|
||||
|
||||
private JsonWriter openScope(int empty, char openBracket) throws IOException {
|
||||
beforeValue();
|
||||
push(empty);
|
||||
out.write(openBracket);
|
||||
return this;
|
||||
}
|
||||
|
||||
private JsonWriter closeScope(int empty, int nonempty, char closeBracket) throws IOException {
|
||||
int context = peek();
|
||||
if (context != nonempty && context != empty) {
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
if (deferredName != null) {
|
||||
throw new IllegalStateException("Dangling name: " + deferredName);
|
||||
}
|
||||
|
||||
if (!deferredComments.isEmpty()) {
|
||||
newline();
|
||||
writeDeferredComment();
|
||||
context = nonempty;
|
||||
}
|
||||
|
||||
stackSize--;
|
||||
if (context == nonempty) {
|
||||
newline();
|
||||
}
|
||||
out.write(closeBracket);
|
||||
return this;
|
||||
}
|
||||
|
||||
private void push(int newTop) {
|
||||
if (stackSize == stack.length) {
|
||||
stack = Arrays.copyOf(stack, stackSize * 2);
|
||||
}
|
||||
stack[stackSize++] = newTop;
|
||||
}
|
||||
|
||||
/** Returns the value on the top of the stack. */
|
||||
private int peek() {
|
||||
if (stackSize == 0) {
|
||||
throw new IllegalStateException("JsonWriter is closed.");
|
||||
}
|
||||
return stack[stackSize - 1];
|
||||
}
|
||||
|
||||
/** Replace the value on the top of the stack with the given value. */
|
||||
private void replaceTop(int topOfStack) {
|
||||
stack[stackSize - 1] = topOfStack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter comment(String comment) throws IOException {
|
||||
if (!lenient) throw new MalformedDataException("Cannot write comment in non-lenient JsonWriter.");
|
||||
if (comment == null || comment.isBlank()) return this;
|
||||
String[] parts = comment.split("\n");
|
||||
Collections.addAll(deferredComments, parts);
|
||||
if (peek() == NONEMPTY_DOCUMENT) {
|
||||
newline();
|
||||
writeDeferredComment();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private void writeDeferredComment() throws IOException {
|
||||
if (!deferredComments.isEmpty()) {
|
||||
if (newline.isEmpty()) {
|
||||
out.append("/* ").append(String.join(" / ", deferredComments)).append(" */");
|
||||
} else {
|
||||
boolean first = true;
|
||||
for (String s : deferredComments) {
|
||||
if (!first) newline();
|
||||
first = false;
|
||||
out.append("// ").append(s);
|
||||
}
|
||||
}
|
||||
deferredComments.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter name(String name) throws IOException {
|
||||
Objects.requireNonNull(name, "name == null");
|
||||
if (deferredName != null) {
|
||||
throw new IllegalStateException("Already wrote a name, expecting a value.");
|
||||
}
|
||||
int context = peek();
|
||||
if (context != EMPTY_OBJECT && context != NONEMPTY_OBJECT) {
|
||||
throw new IllegalStateException("Please begin an object before writing a name.");
|
||||
}
|
||||
deferredName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
private void writeDeferredName() throws IOException {
|
||||
if (deferredName != null) {
|
||||
beforeName();
|
||||
if (omitQuotes && deferredName.matches("[a-zA-Z_$][\\w$]*")) {
|
||||
out.write(deferredName);
|
||||
}
|
||||
else {
|
||||
string(deferredName);
|
||||
}
|
||||
deferredName = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter value(String value) throws IOException {
|
||||
if (value == null) {
|
||||
return nullValue();
|
||||
}
|
||||
writeDeferredName();
|
||||
beforeValue();
|
||||
string(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonWriter literalValue(String value) throws IOException {
|
||||
if (value == null) {
|
||||
return nullValue();
|
||||
}
|
||||
writeDeferredName();
|
||||
beforeValue();
|
||||
out.append(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
if (stackSize == 0) {
|
||||
throw new IllegalStateException("JsonWriter is closed.");
|
||||
}
|
||||
out.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
out.close();
|
||||
|
||||
int size = stackSize;
|
||||
if (size > 1 || (size == 1 && stack[size - 1] != NONEMPTY_DOCUMENT)) {
|
||||
throw new IOException("Incomplete document");
|
||||
}
|
||||
stackSize = 0;
|
||||
}
|
||||
|
||||
private void string(String value) throws IOException {
|
||||
out.write('\"');
|
||||
int last = 0;
|
||||
int length = value.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
char c = value.charAt(i);
|
||||
String replacement = StringEscapeUtil.getReplacement(c);
|
||||
if (replacement == null) {
|
||||
continue;
|
||||
}
|
||||
if (last < i) {
|
||||
out.write(value, last, i - last);
|
||||
}
|
||||
out.write(replacement);
|
||||
last = i + 1;
|
||||
}
|
||||
if (last < length) {
|
||||
out.write(value, last, length - last);
|
||||
}
|
||||
out.write('\"');
|
||||
}
|
||||
|
||||
private void newline() throws IOException {
|
||||
if (usesEmptyNewlineAndIndent) {
|
||||
return;
|
||||
}
|
||||
|
||||
out.write(newline);
|
||||
for (int i = 1, size = stackSize; i < size; i++) {
|
||||
out.write(indent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts any necessary separators and whitespace before a name. Also adjusts the stack to expect
|
||||
* the name's value.
|
||||
*/
|
||||
private void beforeName() throws IOException {
|
||||
int context = peek();
|
||||
if (context == NONEMPTY_OBJECT) { // first in object
|
||||
out.write(formattedComma);
|
||||
} else if (context != EMPTY_OBJECT) { // not in an object!
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
newline();
|
||||
if (!deferredComments.isEmpty()) {
|
||||
writeDeferredComment();
|
||||
newline();
|
||||
}
|
||||
replaceTop(DANGLING_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts any necessary separators and whitespace before a literal value, inline array, or inline
|
||||
* object. Also adjusts the stack to expect either a closing bracket or another element.
|
||||
*/
|
||||
@SuppressWarnings("fallthrough")
|
||||
private void beforeValue() throws IOException {
|
||||
switch (peek()) {
|
||||
case NONEMPTY_DOCUMENT:
|
||||
if (!lenient) {
|
||||
throw new IllegalStateException("JSON must have only one top-level value.");
|
||||
}
|
||||
// fall-through
|
||||
case EMPTY_DOCUMENT: // first in document
|
||||
replaceTop(NONEMPTY_DOCUMENT);
|
||||
if (!deferredComments.isEmpty()) {
|
||||
writeDeferredComment();
|
||||
newline();
|
||||
}
|
||||
break;
|
||||
|
||||
case EMPTY_ARRAY: // first in array
|
||||
replaceTop(NONEMPTY_ARRAY);
|
||||
newline();
|
||||
if (!deferredComments.isEmpty()) {
|
||||
writeDeferredComment();
|
||||
newline();
|
||||
}
|
||||
break;
|
||||
|
||||
case NONEMPTY_ARRAY: // another in array
|
||||
out.append(formattedComma);
|
||||
newline();
|
||||
if (!deferredComments.isEmpty()) {
|
||||
writeDeferredComment();
|
||||
newline();
|
||||
}
|
||||
break;
|
||||
|
||||
case DANGLING_NAME: // value for name
|
||||
out.append(formattedColon);
|
||||
if (!deferredComments.isEmpty()) {
|
||||
newline();
|
||||
writeDeferredComment();
|
||||
newline();
|
||||
}
|
||||
replaceTop(NONEMPTY_OBJECT);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package io.gitlab.jfronny.commons.serialize.json.impl;
|
||||
|
||||
public class JsonScope {
|
||||
/** An array with no elements requires no separator before the next element. */
|
||||
public static final int EMPTY_ARRAY = 1;
|
||||
|
||||
/** An array with at least one value requires a separator before the next element. */
|
||||
public static final int NONEMPTY_ARRAY = 2;
|
||||
|
||||
/** An object with no name/value pairs requires no separator before the next element. */
|
||||
public static final int EMPTY_OBJECT = 3;
|
||||
|
||||
/** An object whose most recent element is a key. The next element must be a value. */
|
||||
public static final int DANGLING_NAME = 4;
|
||||
|
||||
/** An object with at least one name/value pair requires a separator before the next element. */
|
||||
public static final int NONEMPTY_OBJECT = 5;
|
||||
|
||||
/** No top-level value has been started yet. */
|
||||
public static final int EMPTY_DOCUMENT = 6;
|
||||
|
||||
/** A top-level value has already been started. */
|
||||
public static final int NONEMPTY_DOCUMENT = 7;
|
||||
|
||||
/** A document that's been closed and cannot be accessed. */
|
||||
public static final int CLOSED = 8;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module io.gitlab.jfronny.commons.serialize.json {
|
||||
requires io.gitlab.jfronny.commons;
|
||||
requires io.gitlab.jfronny.commons.serialize;
|
||||
requires static org.jetbrains.annotations;
|
||||
exports io.gitlab.jfronny.commons.serialize.json;
|
||||
}
|
|
@ -0,0 +1,429 @@
|
|||
/*
|
||||
* Copyright (C) 2014 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 io.gitlab.jfronny.commons.serialize.json.test;
|
||||
|
||||
import io.gitlab.jfronny.commons.serialize.json.JsonReader;
|
||||
import io.gitlab.jfronny.commons.serialize.stream.SerializeReader;
|
||||
import io.gitlab.jfronny.commons.serialize.stream.Token;
|
||||
import io.gitlab.jfronny.commons.serialize.stream.emulated.DataElementSerializer;
|
||||
import io.gitlab.jfronny.commons.serialize.stream.emulated.EmulatedReader;
|
||||
import io.gitlab.jfronny.commons.throwable.Try;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
@RunWith(Parameterized.class)
|
||||
public class JsonReaderPathTest {
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
public static List<Object[]> parameters() {
|
||||
return Arrays.asList(
|
||||
new Object[] {Factory.STRING_READER}, new Object[] {Factory.OBJECT_READER});
|
||||
}
|
||||
|
||||
@Parameterized.Parameter public Factory factory;
|
||||
|
||||
@Test
|
||||
public void path() throws IOException {
|
||||
SerializeReader<? extends IOException, ?> reader = factory.create("{\"a\":[2,true,false,null,\"b\",{\"c\":\"d\"},[3]]}");
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
reader.beginObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.");
|
||||
assertThat(reader.getPath()).isEqualTo("$.");
|
||||
String unused1 = reader.nextName();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a");
|
||||
reader.beginArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a[0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a[0]");
|
||||
int unused2 = reader.nextInt();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a[0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a[1]");
|
||||
boolean unused3 = reader.nextBoolean();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a[1]");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a[2]");
|
||||
boolean unused4 = reader.nextBoolean();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a[2]");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a[3]");
|
||||
reader.nextNull();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a[3]");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a[4]");
|
||||
String unused5 = reader.nextString();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a[4]");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a[5]");
|
||||
reader.beginObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a[5].");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a[5].");
|
||||
String unused6 = reader.nextName();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a[5].c");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a[5].c");
|
||||
String unused7 = reader.nextString();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a[5].c");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a[5].c");
|
||||
reader.endObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a[5]");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a[6]");
|
||||
reader.beginArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a[6][0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a[6][0]");
|
||||
int unused8 = reader.nextInt();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a[6][0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a[6][1]");
|
||||
reader.endArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a[6]");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a[7]");
|
||||
reader.endArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a");
|
||||
reader.endObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void objectPath() throws IOException {
|
||||
SerializeReader<? extends IOException, ?> reader = factory.create("{\"a\":1,\"b\":2}");
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
|
||||
Token unused1 = reader.peek();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
reader.beginObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.");
|
||||
assertThat(reader.getPath()).isEqualTo("$.");
|
||||
|
||||
Token unused2 = reader.peek();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.");
|
||||
assertThat(reader.getPath()).isEqualTo("$.");
|
||||
String unused3 = reader.nextName();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a");
|
||||
|
||||
Token unused4 = reader.peek();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a");
|
||||
int unused5 = reader.nextInt();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a");
|
||||
|
||||
Token unused6 = reader.peek();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a");
|
||||
String unused7 = reader.nextName();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.b");
|
||||
assertThat(reader.getPath()).isEqualTo("$.b");
|
||||
|
||||
Token unused8 = reader.peek();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.b");
|
||||
assertThat(reader.getPath()).isEqualTo("$.b");
|
||||
int unused9 = reader.nextInt();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.b");
|
||||
assertThat(reader.getPath()).isEqualTo("$.b");
|
||||
|
||||
Token unused10 = reader.peek();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.b");
|
||||
assertThat(reader.getPath()).isEqualTo("$.b");
|
||||
reader.endObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
|
||||
Token unused11 = reader.peek();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
Try.orThrow(reader::close);
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void arrayPath() throws IOException {
|
||||
SerializeReader<? extends IOException, ?> reader = factory.create("[1,2]");
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
|
||||
Token unused1 = reader.peek();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
reader.beginArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[0]");
|
||||
|
||||
Token unused2 = reader.peek();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[0]");
|
||||
int unused3 = reader.nextInt();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[1]");
|
||||
|
||||
Token unused4 = reader.peek();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[1]");
|
||||
int unused5 = reader.nextInt();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[1]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[2]");
|
||||
|
||||
Token unused6 = reader.peek();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[1]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[2]");
|
||||
reader.endArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
|
||||
Token unused7 = reader.peek();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
Try.orThrow(reader::close);
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleTopLevelValuesInOneDocument() throws IOException {
|
||||
assumeTrue(factory == Factory.STRING_READER);
|
||||
|
||||
SerializeReader<? extends IOException, ?> reader = factory.create("[][]");
|
||||
reader.setLenient(true);
|
||||
reader.beginArray();
|
||||
reader.endArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
reader.beginArray();
|
||||
reader.endArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skipArrayElements() throws IOException {
|
||||
SerializeReader<? extends IOException, ?> reader = factory.create("[1,2,3]");
|
||||
reader.beginArray();
|
||||
reader.skipValue();
|
||||
reader.skipValue();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[1]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[2]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skipArrayEnd() throws IOException {
|
||||
SerializeReader<? extends IOException, ?> reader = factory.create("[[],1]");
|
||||
reader.beginArray();
|
||||
reader.beginArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[0][0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[0][0]");
|
||||
assertThrows("Attempt to skip led outside its parent", IllegalStateException.class, reader::skipValue);
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[1]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skipObjectNames() throws IOException {
|
||||
SerializeReader<? extends IOException, ?> reader = factory.create("{\"a\":[]}");
|
||||
reader.beginObject();
|
||||
reader.skipValue();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.<skipped>");
|
||||
assertThat(reader.getPath()).isEqualTo("$.<skipped>");
|
||||
|
||||
reader.beginArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.<skipped>[0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$.<skipped>[0]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skipObjectValues() throws IOException {
|
||||
SerializeReader<? extends IOException, ?> reader = factory.create("{\"a\":1,\"b\":2}");
|
||||
reader.beginObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.");
|
||||
assertThat(reader.getPath()).isEqualTo("$.");
|
||||
String unused1 = reader.nextName();
|
||||
reader.skipValue();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a");
|
||||
String unused2 = reader.nextName();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.b");
|
||||
assertThat(reader.getPath()).isEqualTo("$.b");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skipObjectEnd() throws IOException {
|
||||
SerializeReader<? extends IOException, ?> reader = factory.create("{\"a\":{},\"b\":2}");
|
||||
reader.beginObject();
|
||||
String unused = reader.nextName();
|
||||
reader.beginObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a.");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a.");
|
||||
// skip end of object
|
||||
assertThat(reader.peek()).isEqualTo(Token.END_OBJECT);
|
||||
assertThrows("Attempt to skip led outside its parent", IllegalStateException.class, reader::skipValue);
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skipNestedStructures() throws IOException {
|
||||
SerializeReader<? extends IOException, ?> reader = factory.create("[[1,2,3],4]");
|
||||
reader.beginArray();
|
||||
reader.skipValue();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[1]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skipEndOfDocument() throws IOException {
|
||||
SerializeReader<? extends IOException, ?> reader = factory.create("[]");
|
||||
reader.beginArray();
|
||||
reader.endArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
assertThrows("Attempt to skip led outside the document", IllegalStateException.class, reader::skipValue);
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void arrayOfObjects() throws IOException {
|
||||
SerializeReader<? extends IOException, ?> reader = factory.create("[{},{},{}]");
|
||||
reader.beginArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[0]");
|
||||
reader.beginObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[0].");
|
||||
assertThat(reader.getPath()).isEqualTo("$[0].");
|
||||
reader.endObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[1]");
|
||||
reader.beginObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[1].");
|
||||
assertThat(reader.getPath()).isEqualTo("$[1].");
|
||||
reader.endObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[1]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[2]");
|
||||
reader.beginObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[2].");
|
||||
assertThat(reader.getPath()).isEqualTo("$[2].");
|
||||
reader.endObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[2]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[3]");
|
||||
reader.endArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void arrayOfArrays() throws IOException {
|
||||
SerializeReader<? extends IOException, ?> reader = factory.create("[[],[],[]]");
|
||||
reader.beginArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[0]");
|
||||
reader.beginArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[0][0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[0][0]");
|
||||
reader.endArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[1]");
|
||||
reader.beginArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[1][0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[1][0]");
|
||||
reader.endArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[1]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[2]");
|
||||
reader.beginArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[2][0]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[2][0]");
|
||||
reader.endArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$[2]");
|
||||
assertThat(reader.getPath()).isEqualTo("$[3]");
|
||||
reader.endArray();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void objectOfObjects() throws IOException {
|
||||
SerializeReader<? extends IOException, ?> reader = factory.create("{\"a\":{\"a1\":1,\"a2\":2},\"b\":{\"b1\":1}}");
|
||||
reader.beginObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.");
|
||||
assertThat(reader.getPath()).isEqualTo("$.");
|
||||
String unused1 = reader.nextName();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a");
|
||||
reader.beginObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a.");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a.");
|
||||
String unused2 = reader.nextName();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a.a1");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a.a1");
|
||||
int unused3 = reader.nextInt();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a.a1");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a.a1");
|
||||
String unused4 = reader.nextName();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a.a2");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a.a2");
|
||||
int unused5 = reader.nextInt();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a.a2");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a.a2");
|
||||
reader.endObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.a");
|
||||
assertThat(reader.getPath()).isEqualTo("$.a");
|
||||
String unused6 = reader.nextName();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.b");
|
||||
assertThat(reader.getPath()).isEqualTo("$.b");
|
||||
reader.beginObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.b.");
|
||||
assertThat(reader.getPath()).isEqualTo("$.b.");
|
||||
String unused7 = reader.nextName();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.b.b1");
|
||||
assertThat(reader.getPath()).isEqualTo("$.b.b1");
|
||||
int unused8 = reader.nextInt();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.b.b1");
|
||||
assertThat(reader.getPath()).isEqualTo("$.b.b1");
|
||||
reader.endObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$.b");
|
||||
assertThat(reader.getPath()).isEqualTo("$.b");
|
||||
reader.endObject();
|
||||
assertThat(reader.getPreviousPath()).isEqualTo("$");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
}
|
||||
|
||||
public enum Factory {
|
||||
STRING_READER {
|
||||
@Override
|
||||
public SerializeReader<? extends IOException, ?> create(String data) {
|
||||
return new JsonReader(new StringReader(data));
|
||||
}
|
||||
},
|
||||
OBJECT_READER {
|
||||
@Override
|
||||
public SerializeReader<? extends IOException, ?> create(String data) {
|
||||
JsonReader source = new JsonReader(new StringReader(data));
|
||||
return new EmulatedReader(Try.orThrow(() -> DataElementSerializer.deserialize(source)));
|
||||
}
|
||||
};
|
||||
|
||||
abstract SerializeReader<? extends IOException, ?> create(String data);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,9 +1,11 @@
|
|||
import io.gitlab.jfronny.scripts.*
|
||||
|
||||
plugins {
|
||||
commons.library
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.commons)
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("maven") {
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package io.gitlab.jfronny.commons.serialize;
|
||||
|
||||
/**
|
||||
* Utilities methods for escaping strings, extracted from gsons JsonWriter.
|
||||
* @author JFronny
|
||||
*/
|
||||
public class StringEscapeUtil {
|
||||
/*
|
||||
* From RFC 8259, "All Unicode characters may be placed within the
|
||||
* quotation marks except for the characters that must be escaped:
|
||||
* quotation mark, reverse solidus, and the control characters
|
||||
* (U+0000 through U+001F)."
|
||||
*
|
||||
* We also escape '\u2028' and '\u2029', which JavaScript interprets as
|
||||
* newline characters. This prevents eval() from failing with a syntax
|
||||
* error. http://code.google.com/p/google-gson/issues/detail?id=341
|
||||
*/
|
||||
private static final String[] REPLACEMENT_CHARS;
|
||||
|
||||
static {
|
||||
REPLACEMENT_CHARS = new String[128];
|
||||
for (int i = 0; i <= 0x1f; i++) {
|
||||
REPLACEMENT_CHARS[i] = String.format("\\u%04x", i);
|
||||
}
|
||||
REPLACEMENT_CHARS['"'] = "\\\"";
|
||||
REPLACEMENT_CHARS['\\'] = "\\\\";
|
||||
REPLACEMENT_CHARS['\t'] = "\\t";
|
||||
REPLACEMENT_CHARS['\b'] = "\\b";
|
||||
REPLACEMENT_CHARS['\n'] = "\\n";
|
||||
REPLACEMENT_CHARS['\r'] = "\\r";
|
||||
REPLACEMENT_CHARS['\f'] = "\\f";
|
||||
REPLACEMENT_CHARS['\0'] = "\\0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the replacement for the character, or null if the character does not need to be escaped.
|
||||
* @param c the character to escape
|
||||
* @return the replacement for the character, or null if the character does not need to be escaped
|
||||
*/
|
||||
public static String getReplacement(char c) {
|
||||
String replacement;
|
||||
if (c < 128) {
|
||||
replacement = REPLACEMENT_CHARS[c];
|
||||
if (replacement == null) {
|
||||
return null;
|
||||
}
|
||||
} else if (c == '\u2028') {
|
||||
replacement = "\\u2028";
|
||||
} else if (c == '\u2029') {
|
||||
replacement = "\\u2029";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return replacement;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package io.gitlab.jfronny.commons.serialize.stream;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class MalformedDataException extends IOException {
|
||||
public MalformedDataException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public MalformedDataException(String msg, Throwable throwable) {
|
||||
super(msg, throwable);
|
||||
}
|
||||
|
||||
public MalformedDataException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package io.gitlab.jfronny.commons.serialize.stream;
|
||||
|
||||
import io.gitlab.jfronny.commons.SamWithReceiver;
|
||||
|
||||
public abstract class SerializeReader<TEx extends Throwable, T extends SerializeReader<TEx, T>> implements AutoCloseable {
|
||||
protected boolean lenient = false;
|
||||
protected boolean serializeSpecialFloatingPointValues = false;
|
||||
|
||||
public boolean isLenient() {
|
||||
return lenient;
|
||||
}
|
||||
|
||||
public T setLenient(boolean lenient) {
|
||||
this.lenient = lenient;
|
||||
if (lenient) return setSerializeSpecialFloatingPointValues(true);
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
public boolean isSerializeSpecialFloatingPointValues() {
|
||||
return serializeSpecialFloatingPointValues;
|
||||
}
|
||||
|
||||
public T setSerializeSpecialFloatingPointValues(boolean serializeSpecialFloatingPointValues) {
|
||||
this.serializeSpecialFloatingPointValues = serializeSpecialFloatingPointValues;
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
public abstract T beginArray() throws TEx;
|
||||
public abstract T endArray() throws TEx;
|
||||
public <R> R array(SerializeReaderFunction<TEx, T, R> consumer) throws TEx {
|
||||
beginArray();
|
||||
var result = consumer.accept((T) this);
|
||||
endArray();
|
||||
return result;
|
||||
}
|
||||
public abstract T beginObject() throws TEx;
|
||||
public abstract T endObject() throws TEx;
|
||||
public <R> R object(SerializeReaderFunction<TEx, T, R> consumer) throws TEx {
|
||||
beginObject();
|
||||
var result = consumer.accept((T) this);
|
||||
endObject();
|
||||
return result;
|
||||
}
|
||||
|
||||
public abstract boolean hasNext() throws TEx;
|
||||
public abstract Token peek() throws TEx;
|
||||
|
||||
public abstract String nextName() throws TEx;
|
||||
public abstract String nextString() throws TEx;
|
||||
public abstract boolean nextBoolean() throws TEx;
|
||||
public abstract void nextNull() throws TEx;
|
||||
public double nextDouble() throws TEx {
|
||||
return nextNumber().doubleValue();
|
||||
}
|
||||
public long nextLong() throws TEx {
|
||||
return nextNumber().longValue();
|
||||
}
|
||||
public int nextInt() throws TEx {
|
||||
return nextNumber().intValue();
|
||||
}
|
||||
public abstract Number nextNumber() throws TEx;
|
||||
public abstract void skipValue() throws TEx;
|
||||
|
||||
public abstract String getPath();
|
||||
public abstract String getPreviousPath();
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + locationString();
|
||||
}
|
||||
|
||||
protected String locationString() {
|
||||
return " at path " + getPath();
|
||||
}
|
||||
|
||||
|
||||
@SamWithReceiver
|
||||
public interface SerializeReaderFunction<TEx extends Throwable, T extends SerializeReader<TEx, T>, R> {
|
||||
R accept(T reader) throws TEx;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
package io.gitlab.jfronny.commons.serialize.stream;
|
||||
|
||||
import io.gitlab.jfronny.commons.SamWithReceiver;
|
||||
|
||||
import java.io.Flushable;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public abstract class SerializeWriter<TEx extends Throwable, T extends SerializeWriter<TEx, T>> implements AutoCloseable, Flushable {
|
||||
private static final Pattern VALID_JSON_NUMBER_PATTERN =
|
||||
Pattern.compile("-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][-+]?[0-9]+)?");
|
||||
|
||||
protected boolean lenient = false;
|
||||
protected boolean serializeNulls = true;
|
||||
protected boolean serializeSpecialFloatingPointValues = false;
|
||||
|
||||
public boolean isLenient() {
|
||||
return lenient;
|
||||
}
|
||||
|
||||
public T setLenient(boolean lenient) {
|
||||
this.lenient = lenient;
|
||||
if (lenient) return setSerializeSpecialFloatingPointValues(true).setSerializeNulls(true);
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
public boolean isSerializeNulls() {
|
||||
return serializeNulls;
|
||||
}
|
||||
|
||||
public T setSerializeNulls(boolean serializeNulls) {
|
||||
this.serializeNulls = serializeNulls;
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
public boolean isSerializeSpecialFloatingPointValues() {
|
||||
return serializeSpecialFloatingPointValues;
|
||||
}
|
||||
|
||||
public T setSerializeSpecialFloatingPointValues(boolean serializeSpecialFloatingPointValues) {
|
||||
this.serializeSpecialFloatingPointValues = serializeSpecialFloatingPointValues;
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
public abstract T beginArray() throws TEx;
|
||||
public abstract T endArray() throws TEx;
|
||||
public T array(SerializeWriterConsumer<TEx, T> consumer) throws TEx {
|
||||
return consumer.accept(this.beginArray()).endObject();
|
||||
}
|
||||
public abstract T beginObject() throws TEx;
|
||||
public abstract T endObject() throws TEx;
|
||||
public T object(SerializeWriterConsumer<TEx, T> consumer) throws TEx {
|
||||
return consumer.accept(this.beginObject()).endObject();
|
||||
}
|
||||
|
||||
public abstract T comment(String comment) throws TEx;
|
||||
public abstract T name(String name) throws TEx;
|
||||
public T nullValue() throws TEx {
|
||||
if (serializeNulls) {
|
||||
return literalValue("null");
|
||||
} else {
|
||||
throw new IllegalArgumentException("Null values are not allowed");
|
||||
}
|
||||
}
|
||||
public abstract T value(String value) throws TEx;
|
||||
public T value(boolean value) throws TEx {
|
||||
return literalValue(value ? "true" : "false");
|
||||
}
|
||||
public T value(Boolean value) throws TEx {
|
||||
return value == null ? nullValue() : value(value.booleanValue());
|
||||
}
|
||||
public T value(float value) throws TEx {
|
||||
if (!serializeSpecialFloatingPointValues && (Float.isNaN(value) || Float.isInfinite(value)))
|
||||
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
|
||||
return literalValue(Float.toString(value));
|
||||
}
|
||||
public T value(Float value) throws TEx {
|
||||
return value == null ? nullValue() : value(value.floatValue());
|
||||
}
|
||||
public T value(double value) throws TEx {
|
||||
if (!serializeSpecialFloatingPointValues && (Double.isNaN(value) || Double.isInfinite(value)))
|
||||
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
|
||||
return literalValue(Double.toString(value));
|
||||
}
|
||||
public T value(Double value) throws TEx {
|
||||
return value == null ? nullValue() : value(value.doubleValue());
|
||||
}
|
||||
public T value(long value) throws TEx {
|
||||
return literalValue(Long.toString(value));
|
||||
}
|
||||
public T value(Long value) throws TEx {
|
||||
return value == null ? nullValue() : value(value.longValue());
|
||||
}
|
||||
public T value(Number value) throws TEx {
|
||||
if (value == null) return nullValue();
|
||||
String s = value.toString();
|
||||
if (s.equals("NaN") || s.equals("Infinity") || s.equals("-Infinity")) {
|
||||
if (!serializeSpecialFloatingPointValues) {
|
||||
throw new IllegalArgumentException("Numeric values must be finite, but was " + s);
|
||||
}
|
||||
} else {
|
||||
Class<? extends Number> numberClass = value.getClass();
|
||||
if (!isTrustedNumberClass(numberClass) && !VALID_JSON_NUMBER_PATTERN.matcher(s).matches()) {
|
||||
throw new IllegalArgumentException("String created by " + numberClass + " is not a valid number: " + s);
|
||||
}
|
||||
}
|
||||
return literalValue(s);
|
||||
}
|
||||
|
||||
private static boolean isTrustedNumberClass(Class<? extends Number> c) {
|
||||
return c == Integer.class
|
||||
|| c == Long.class
|
||||
|| c == Double.class
|
||||
|| c == Float.class
|
||||
|| c == Byte.class
|
||||
|| c == Short.class
|
||||
|| c == BigDecimal.class
|
||||
|| c == BigInteger.class
|
||||
|| c == AtomicInteger.class
|
||||
|| c == AtomicLong.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a literal value to the output without quoting or escaping.
|
||||
* This may not be supported by all implementations, if not supported an {@link UnsupportedOperationException} will be thrown.
|
||||
* @param value the literal value to write
|
||||
* @return this writer
|
||||
*/
|
||||
public abstract T literalValue(String value) throws TEx;
|
||||
|
||||
@SamWithReceiver
|
||||
public interface SerializeWriterConsumer<TEx extends Throwable, T extends SerializeWriter<TEx, T>> {
|
||||
T accept(T writer) throws TEx;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package io.gitlab.jfronny.commons.serialize.stream;
|
||||
|
||||
public enum Token {
|
||||
BEGIN_ARRAY, END_ARRAY, BEGIN_OBJECT, END_OBJECT, NAME, STRING, NUMBER, BOOLEAN, NULL, END_DOCUMENT
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package io.gitlab.jfronny.commons.serialize.stream.emulated;
|
||||
|
||||
import io.gitlab.jfronny.commons.data.LinkedTreeMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public sealed interface DataElement {
|
||||
record Null() implements DataElement {}
|
||||
sealed interface Primitive extends DataElement {
|
||||
java.lang.String asString();
|
||||
|
||||
record Boolean(boolean value) implements Primitive {
|
||||
@Override
|
||||
public java.lang.String asString() {
|
||||
return value ? "true" : "false";
|
||||
}
|
||||
}
|
||||
record Number(java.lang.Number value) implements Primitive {
|
||||
public Number {
|
||||
Objects.requireNonNull(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.lang.String asString() {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
record String(java.lang.String value) implements Primitive {
|
||||
@Override
|
||||
public java.lang.String asString() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
record Object(Map<String, DataElement> members) implements DataElement {
|
||||
public Object() {
|
||||
this(new LinkedTreeMap<>(false));
|
||||
}
|
||||
}
|
||||
record Array(List<DataElement> elements) implements DataElement {
|
||||
public Array() {
|
||||
this(new ArrayList<>());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package io.gitlab.jfronny.commons.serialize.stream.emulated;
|
||||
|
||||
import io.gitlab.jfronny.commons.serialize.stream.SerializeReader;
|
||||
import io.gitlab.jfronny.commons.serialize.stream.SerializeWriter;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class DataElementSerializer {
|
||||
public static <TEx extends Throwable, T extends SerializeWriter<TEx, T>> void serialize(DataElement element, T out) throws TEx {
|
||||
switch (element) {
|
||||
case DataElement.Array(var elements) -> out.array(b -> {
|
||||
for (DataElement e : elements) {
|
||||
serialize(e, b);
|
||||
}
|
||||
return b;
|
||||
});
|
||||
case DataElement.Null n -> out.nullValue();
|
||||
case DataElement.Object(var members) -> out.object(b -> {
|
||||
for (Map.Entry<String, DataElement> e : members.entrySet()) {
|
||||
b.name(e.getKey());
|
||||
serialize(e.getValue(), b);
|
||||
}
|
||||
return b;
|
||||
});
|
||||
case DataElement.Primitive p -> {
|
||||
switch (p) {
|
||||
case DataElement.Primitive.Boolean(var value) -> out.value(value);
|
||||
case DataElement.Primitive.Number(var value) -> out.value(value);
|
||||
case DataElement.Primitive.String(var value) -> out.value(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static <TEx extends Throwable, T extends SerializeReader<TEx, T>> DataElement deserialize(T in) throws TEx {
|
||||
return switch (in.peek()) {
|
||||
case STRING -> new DataElement.Primitive.String(in.nextString());
|
||||
case NUMBER -> new DataElement.Primitive.Number(in.nextNumber());
|
||||
case BOOLEAN -> new DataElement.Primitive.Boolean(in.nextBoolean());
|
||||
case NULL -> {
|
||||
in.nextNull();
|
||||
yield new DataElement.Null();
|
||||
}
|
||||
case BEGIN_ARRAY -> in.array(b -> {
|
||||
DataElement.Array array = new DataElement.Array();
|
||||
while (b.hasNext()) {
|
||||
array.elements().add(deserialize(b));
|
||||
}
|
||||
return array;
|
||||
});
|
||||
case BEGIN_OBJECT -> in.object(b -> {
|
||||
DataElement.Object object = new DataElement.Object();
|
||||
while (b.hasNext()) {
|
||||
object.members().put(b.nextName(), deserialize(b));
|
||||
}
|
||||
return object;
|
||||
});
|
||||
case END_ARRAY, END_OBJECT, END_DOCUMENT, NAME -> throw new IllegalStateException();
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,281 @@
|
|||
package io.gitlab.jfronny.commons.serialize.stream.emulated;
|
||||
|
||||
import io.gitlab.jfronny.commons.serialize.stream.MalformedDataException;
|
||||
import io.gitlab.jfronny.commons.serialize.stream.SerializeReader;
|
||||
import io.gitlab.jfronny.commons.serialize.stream.Token;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
public class EmulatedReader extends SerializeReader<MalformedDataException, EmulatedReader> {
|
||||
private static final Object SENTINEL_CLOSED = new Object();
|
||||
|
||||
/*
|
||||
* The nesting stack. Using a manual array rather than an ArrayList saves 20%.
|
||||
*/
|
||||
private Object[] stack = new Object[32];
|
||||
private int stackSize = 0;
|
||||
|
||||
/*
|
||||
* The path members. It corresponds directly to stack: At indices where the
|
||||
* stack contains an object (EMPTY_OBJECT, DANGLING_NAME or NONEMPTY_OBJECT),
|
||||
* pathNames contains the name at this scope. Where it contains an array
|
||||
* (EMPTY_ARRAY, NONEMPTY_ARRAY) pathIndices contains the current index in
|
||||
* that array. Otherwise the value is undefined, and we take advantage of that
|
||||
* by incrementing pathIndices when doing so isn't useful.
|
||||
*/
|
||||
private String[] pathNames = new String[32];
|
||||
private int[] pathIndices = new int[32];
|
||||
|
||||
public EmulatedReader(DataElement element) {
|
||||
push(element);
|
||||
}
|
||||
|
||||
private void push(Object newTop) {
|
||||
if (stackSize == stack.length) {
|
||||
int newLength = stackSize * 2;
|
||||
stack = Arrays.copyOf(stack, newLength);
|
||||
pathIndices = Arrays.copyOf(pathIndices, newLength);
|
||||
pathNames = Arrays.copyOf(pathNames, newLength);
|
||||
}
|
||||
stack[stackSize++] = newTop;
|
||||
}
|
||||
|
||||
private Object peekStack() {
|
||||
return stack[stackSize - 1];
|
||||
}
|
||||
|
||||
private Object popStack() {
|
||||
Object result = stack[--stackSize];
|
||||
stack[stackSize] = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
private void expect(Token expected) throws MalformedDataException {
|
||||
if (peek() != expected) {
|
||||
throw new IllegalStateException(
|
||||
"Expected " + expected + " but was " + peek() + locationString());
|
||||
}
|
||||
}
|
||||
|
||||
private String nextName(boolean skipName) throws MalformedDataException {
|
||||
expect(Token.NAME);
|
||||
Iterator<?> i = (Iterator<?>) peekStack();
|
||||
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) i.next();
|
||||
String result = (String) entry.getKey();
|
||||
pathNames[stackSize - 1] = skipName ? "<skipped>" : result;
|
||||
push(entry.getValue());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedReader beginArray() throws MalformedDataException {
|
||||
expect(Token.BEGIN_ARRAY);
|
||||
DataElement.Array array = (DataElement.Array) peekStack();
|
||||
push(array.elements().iterator());
|
||||
pathIndices[stackSize - 1] = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedReader endArray() throws MalformedDataException {
|
||||
expect(Token.END_ARRAY);
|
||||
popStack(); // empty iterator
|
||||
popStack(); // array
|
||||
if (stackSize > 0) {
|
||||
pathIndices[stackSize - 1]++;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedReader beginObject() throws MalformedDataException {
|
||||
expect(Token.BEGIN_OBJECT);
|
||||
DataElement.Object object = (DataElement.Object) peekStack();
|
||||
push(object.members().entrySet().iterator());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedReader endObject() throws MalformedDataException {
|
||||
expect(Token.END_OBJECT);
|
||||
pathNames[stackSize - 1] = null; // Free the last path name so that it can be garbage collected
|
||||
popStack(); // empty iterator
|
||||
popStack(); // object
|
||||
if (stackSize > 0) {
|
||||
pathIndices[stackSize - 1]++;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() throws MalformedDataException {
|
||||
Token token = peek();
|
||||
return token != Token.END_OBJECT
|
||||
&& token != Token.END_ARRAY
|
||||
&& token != Token.END_DOCUMENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Token peek() throws MalformedDataException {
|
||||
if (stackSize == 0) {
|
||||
return Token.END_DOCUMENT;
|
||||
}
|
||||
|
||||
Object o = peekStack();
|
||||
if (o instanceof Iterator) {
|
||||
boolean isObject = stack[stackSize - 2] instanceof DataElement.Object;
|
||||
Iterator<?> iterator = (Iterator<?>) o;
|
||||
if (iterator.hasNext()) {
|
||||
if (isObject) {
|
||||
return Token.NAME;
|
||||
} else {
|
||||
push(iterator.next());
|
||||
return peek();
|
||||
}
|
||||
} else {
|
||||
return isObject ? Token.END_OBJECT : Token.END_ARRAY;
|
||||
}
|
||||
} else if (o instanceof DataElement e) {
|
||||
return switch (e) {
|
||||
case DataElement.Object l -> Token.BEGIN_OBJECT;
|
||||
case DataElement.Array l -> Token.BEGIN_ARRAY;
|
||||
case DataElement.Primitive p -> switch (p) {
|
||||
case DataElement.Primitive.String s -> Token.STRING;
|
||||
case DataElement.Primitive.Boolean b -> Token.BOOLEAN;
|
||||
case DataElement.Primitive.Number n -> Token.NUMBER;
|
||||
};
|
||||
case DataElement.Null l -> Token.NULL;
|
||||
};
|
||||
} else if (o == SENTINEL_CLOSED) {
|
||||
throw new IllegalStateException("JsonReader is closed");
|
||||
} else {
|
||||
throw new MalformedDataException("Custom JsonElement subclass " + o.getClass().getName() + " is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nextName() throws MalformedDataException {
|
||||
return nextName(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nextString() throws MalformedDataException {
|
||||
Token token = peek();
|
||||
if (token != Token.STRING && token != Token.NUMBER) {
|
||||
throw new IllegalStateException(
|
||||
"Expected " + Token.STRING + " but was " + token + locationString());
|
||||
}
|
||||
String result = ((DataElement.Primitive) popStack()).asString();
|
||||
if (stackSize > 0) {
|
||||
pathIndices[stackSize - 1]++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean nextBoolean() throws MalformedDataException {
|
||||
expect(Token.BOOLEAN);
|
||||
if (!(popStack() instanceof DataElement.Primitive.Boolean result)) {
|
||||
throw new IllegalStateException("Expected a boolean");
|
||||
}
|
||||
if (stackSize > 0) {
|
||||
pathIndices[stackSize - 1]++;
|
||||
}
|
||||
return result.value();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nextNull() throws MalformedDataException {
|
||||
expect(Token.NULL);
|
||||
popStack();
|
||||
if (stackSize > 0) {
|
||||
pathIndices[stackSize - 1]++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number nextNumber() throws MalformedDataException {
|
||||
Token token = peek();
|
||||
if (token != Token.NUMBER && token != Token.STRING) {
|
||||
throw new IllegalStateException(
|
||||
"Expected " + Token.NUMBER + " but was " + token + locationString());
|
||||
}
|
||||
if (!(popStack() instanceof DataElement.Primitive.Number result)) {
|
||||
throw new IllegalStateException("Expected a number");
|
||||
}
|
||||
popStack();
|
||||
if (stackSize > 0) {
|
||||
pathIndices[stackSize - 1]++;
|
||||
}
|
||||
return result.value();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skipValue() throws MalformedDataException {
|
||||
Token peeked = peek();
|
||||
switch (peeked) {
|
||||
case NAME:
|
||||
@SuppressWarnings("unused")
|
||||
String unused = nextName(true);
|
||||
break;
|
||||
case END_ARRAY:
|
||||
endArray();
|
||||
throw new IllegalStateException("Attempt to skip led outside its parent");
|
||||
case END_OBJECT:
|
||||
endObject();
|
||||
throw new IllegalStateException("Attempt to skip led outside its parent");
|
||||
case END_DOCUMENT:
|
||||
throw new IllegalStateException("Attempt to skip led outside the document");
|
||||
default:
|
||||
popStack();
|
||||
if (stackSize > 0) {
|
||||
pathIndices[stackSize - 1]++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private String getPath(boolean usePreviousPath) {
|
||||
StringBuilder result = new StringBuilder().append('$');
|
||||
for (int i = 0; i < stackSize; i++) {
|
||||
if (stack[i] instanceof DataElement.Array) {
|
||||
if (++i < stackSize && stack[i] instanceof Iterator) {
|
||||
int pathIndex = pathIndices[i];
|
||||
// If index is last path element it points to next array element; have to decrement
|
||||
// `- 1` covers case where iterator for next element is on stack
|
||||
// `- 2` covers case where peek() already pushed next element onto stack
|
||||
if (usePreviousPath && pathIndex > 0 && (i == stackSize - 1 || i == stackSize - 2)) {
|
||||
pathIndex--;
|
||||
}
|
||||
result.append('[').append(pathIndex).append(']');
|
||||
}
|
||||
} else if (stack[i] instanceof DataElement.Object) {
|
||||
if (++i < stackSize && stack[i] instanceof Iterator) {
|
||||
result.append('.');
|
||||
if (pathNames[i] != null) {
|
||||
result.append(pathNames[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPath() {
|
||||
return getPath(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPreviousPath() {
|
||||
return getPath(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
stack = new Object[] {SENTINEL_CLOSED};
|
||||
stackSize = 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
package io.gitlab.jfronny.commons.serialize.stream.emulated;
|
||||
|
||||
import io.gitlab.jfronny.commons.serialize.stream.SerializeWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class EmulatedWriter extends SerializeWriter<RuntimeException, EmulatedWriter> {
|
||||
/** Added to the top of the stack when this writer is closed to cause following ops to fail. */
|
||||
private static final DataElement.Primitive.String SENTINEL_CLOSED = new DataElement.Primitive.String("closed");
|
||||
|
||||
/** The JsonElements and JsonArrays under modification, outermost to innermost. */
|
||||
private final List<DataElement> stack = new ArrayList<>();
|
||||
|
||||
/** The name for the next JSON object value. If non-null, the top of the stack is a JsonObject. */
|
||||
private String pendingName;
|
||||
|
||||
/** the JSON element constructed by this writer. */
|
||||
private DataElement product = new DataElement.Null();
|
||||
|
||||
public DataElement get() {
|
||||
if (!stack.isEmpty()) {
|
||||
throw new IllegalStateException("Expected one JSON element but was " + stack);
|
||||
}
|
||||
return product;
|
||||
}
|
||||
|
||||
private void put(DataElement value) {
|
||||
if (pendingName != null) {
|
||||
if (!(value instanceof DataElement.Null) || serializeNulls) {
|
||||
((DataElement.Object) stack.getLast()).members().put(pendingName, value);
|
||||
}
|
||||
pendingName = null;
|
||||
} else if (stack.isEmpty()) {
|
||||
product = value;
|
||||
} else {
|
||||
DataElement element = stack.getLast();
|
||||
if (element instanceof DataElement.Array array) {
|
||||
array.elements().add(value);
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedWriter beginArray() throws RuntimeException {
|
||||
DataElement.Array array = new DataElement.Array();
|
||||
put(array);
|
||||
stack.add(array);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedWriter endArray() throws RuntimeException {
|
||||
if (stack.isEmpty() || pendingName != null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
DataElement element = stack.getLast();
|
||||
if (element instanceof DataElement.Array) {
|
||||
stack.removeLast();
|
||||
return this;
|
||||
}
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedWriter beginObject() throws RuntimeException {
|
||||
DataElement.Object object = new DataElement.Object();
|
||||
put(object);
|
||||
stack.add(object);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedWriter endObject() throws RuntimeException {
|
||||
if (stack.isEmpty() || pendingName != null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
DataElement element = stack.getLast();
|
||||
if (element instanceof DataElement.Object) {
|
||||
stack.removeLast();
|
||||
return this;
|
||||
}
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedWriter comment(String comment) throws RuntimeException {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedWriter name(String name) throws RuntimeException {
|
||||
Objects.requireNonNull(name, "name == null");
|
||||
if (stack.isEmpty() || pendingName != null) {
|
||||
throw new IllegalStateException("Did not expect a name");
|
||||
}
|
||||
DataElement element = stack.getLast();
|
||||
if (element instanceof DataElement.Object) {
|
||||
pendingName = name;
|
||||
return this;
|
||||
}
|
||||
throw new IllegalStateException("Please begin an object before writing a name.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedWriter value(String value) throws RuntimeException {
|
||||
if (value == null) {
|
||||
return nullValue();
|
||||
}
|
||||
put(new DataElement.Primitive.String(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedWriter value(boolean value) throws RuntimeException {
|
||||
put(new DataElement.Primitive.Boolean(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedWriter value(float value) throws RuntimeException {
|
||||
if (!serializeSpecialFloatingPointValues && (Float.isNaN(value) || Float.isInfinite(value))) {
|
||||
throw new IllegalArgumentException("NaN and infinities are not permitted in this writer: " + value);
|
||||
}
|
||||
put(new DataElement.Primitive.Number(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedWriter value(double value) throws RuntimeException {
|
||||
if (!serializeSpecialFloatingPointValues && (Double.isNaN(value) || Double.isInfinite(value))) {
|
||||
throw new IllegalArgumentException("NaN and infinities are not permitted in this writer: " + value);
|
||||
}
|
||||
put(new DataElement.Primitive.Number(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedWriter value(long value) throws RuntimeException {
|
||||
put(new DataElement.Primitive.Number(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedWriter value(Number value) throws RuntimeException {
|
||||
if (value == null) {
|
||||
return nullValue();
|
||||
}
|
||||
|
||||
if (!isLenient()) {
|
||||
double d = value.doubleValue();
|
||||
if (Double.isNaN(d) || Double.isInfinite(d)) {
|
||||
throw new IllegalArgumentException("NaN and infinities are not permitted in this writer: " + value);
|
||||
}
|
||||
}
|
||||
|
||||
put(new DataElement.Primitive.Number(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedWriter nullValue() throws RuntimeException {
|
||||
put(new DataElement.Null());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmulatedWriter literalValue(String value) throws RuntimeException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
if (!stack.isEmpty()) {
|
||||
throw new IOException("Incomplete document");
|
||||
}
|
||||
stack.add(SENTINEL_CLOSED);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
module io.gitlab.jfronny.commons.serialize {
|
||||
requires static org.jetbrains.annotations;
|
||||
requires io.gitlab.jfronny.commons;
|
||||
exports io.gitlab.jfronny.commons.serialize;
|
||||
exports io.gitlab.jfronny.commons.serialize.stream;
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package io.gitlab.jfronny.commons.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Objects;
|
||||
|
||||
public class LazilyParsedNumber extends Number {
|
||||
private final String value;
|
||||
|
||||
public LazilyParsedNumber(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
private BigDecimal asBigDecimal() {
|
||||
if (value.length() > 10_000) {
|
||||
throw new NumberFormatException("Number string too large: " + value.substring(0, 30) + "...");
|
||||
}
|
||||
BigDecimal decimal = new BigDecimal(value);
|
||||
|
||||
// Cast to long to avoid issues with abs when value is Integer.MIN_VALUE
|
||||
if (Math.abs((long) decimal.scale()) >= 10_000) {
|
||||
throw new NumberFormatException("Number has unsupported scale: " + value);
|
||||
}
|
||||
return decimal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int intValue() {
|
||||
try {
|
||||
return Integer.parseInt(value);
|
||||
} catch (NumberFormatException e) {
|
||||
try {
|
||||
return (int) Long.parseLong(value);
|
||||
} catch (NumberFormatException nfe) {
|
||||
return asBigDecimal().intValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long longValue() {
|
||||
try {
|
||||
return Long.parseLong(value);
|
||||
} catch (NumberFormatException e) {
|
||||
return asBigDecimal().longValue();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float floatValue() {
|
||||
return Float.parseFloat(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double doubleValue() {
|
||||
return Double.parseDouble(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof LazilyParsedNumber that && Objects.equals(value, that.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(value);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,649 @@
|
|||
package io.gitlab.jfronny.commons.data;
|
||||
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* A map of comparable keys to values. Unlike {@code TreeMap}, this class uses insertion order for
|
||||
* iteration order. Comparison order is only used as an optimization for efficient insertion and
|
||||
* removal.
|
||||
*
|
||||
* <p>This implementation was derived from Android 4.1's TreeMap class.
|
||||
*/
|
||||
public class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Serializable {
|
||||
@SuppressWarnings({"unchecked", "rawtypes"}) // to avoid Comparable<Comparable<Comparable<...>>>
|
||||
private static final Comparator<Comparable> NATURAL_ORDER =
|
||||
new Comparator<Comparable>() {
|
||||
@Override
|
||||
public int compare(Comparable a, Comparable b) {
|
||||
return a.compareTo(b);
|
||||
}
|
||||
};
|
||||
|
||||
private final Comparator<? super K> comparator;
|
||||
private final boolean allowNullValues;
|
||||
Node<K, V> root;
|
||||
int size = 0;
|
||||
int modCount = 0;
|
||||
|
||||
// Used to preserve iteration order
|
||||
final Node<K, V> header;
|
||||
|
||||
/**
|
||||
* Create a natural order, empty tree map whose keys must be mutually comparable and non-null, and
|
||||
* whose values can be {@code null}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // unsafe! this assumes K is comparable
|
||||
public LinkedTreeMap() {
|
||||
this((Comparator<? super K>) NATURAL_ORDER, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a natural order, empty tree map whose keys must be mutually comparable and non-null.
|
||||
*
|
||||
* @param allowNullValues whether {@code null} is allowed as entry value
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // unsafe! this assumes K is comparable
|
||||
public LinkedTreeMap(boolean allowNullValues) {
|
||||
this((Comparator<? super K>) NATURAL_ORDER, allowNullValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tree map ordered by {@code comparator}. This map's keys may only be null if {@code
|
||||
* comparator} permits.
|
||||
*
|
||||
* @param comparator the comparator to order elements with, or {@code null} to use the natural
|
||||
* ordering.
|
||||
* @param allowNullValues whether {@code null} is allowed as entry value
|
||||
*/
|
||||
// unsafe! if comparator is null, this assumes K is comparable
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public LinkedTreeMap(Comparator<? super K> comparator, boolean allowNullValues) {
|
||||
this.comparator = comparator != null ? comparator : (Comparator) NATURAL_ORDER;
|
||||
this.allowNullValues = allowNullValues;
|
||||
this.header = new Node<>(allowNullValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V get(Object key) {
|
||||
Node<K, V> node = findByObject(key);
|
||||
return node != null ? node.value : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
return findByObject(key) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V put(K key, V value) {
|
||||
if (key == null) {
|
||||
throw new NullPointerException("key == null");
|
||||
}
|
||||
if (value == null && !allowNullValues) {
|
||||
throw new NullPointerException("value == null");
|
||||
}
|
||||
Node<K, V> created = find(key, true);
|
||||
V result = created.value;
|
||||
created.value = value;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
root = null;
|
||||
size = 0;
|
||||
modCount++;
|
||||
|
||||
// Clear iteration order
|
||||
Node<K, V> header = this.header;
|
||||
header.next = header.prev = header;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V remove(Object key) {
|
||||
Node<K, V> node = removeInternalByKey(key);
|
||||
return node != null ? node.value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the node at or adjacent to the given key, creating it if requested.
|
||||
*
|
||||
* @throws ClassCastException if {@code key} and the tree's keys aren't mutually comparable.
|
||||
*/
|
||||
Node<K, V> find(K key, boolean create) {
|
||||
Comparator<? super K> comparator = this.comparator;
|
||||
Node<K, V> nearest = root;
|
||||
int comparison = 0;
|
||||
|
||||
if (nearest != null) {
|
||||
// Micro-optimization: avoid polymorphic calls to Comparator.compare().
|
||||
@SuppressWarnings("unchecked") // Throws a ClassCastException below if there's trouble.
|
||||
Comparable<Object> comparableKey =
|
||||
(comparator == NATURAL_ORDER) ? (Comparable<Object>) key : null;
|
||||
|
||||
while (true) {
|
||||
comparison =
|
||||
(comparableKey != null)
|
||||
? comparableKey.compareTo(nearest.key)
|
||||
: comparator.compare(key, nearest.key);
|
||||
|
||||
// We found the requested key.
|
||||
if (comparison == 0) {
|
||||
return nearest;
|
||||
}
|
||||
|
||||
// If it exists, the key is in a subtree. Go deeper.
|
||||
Node<K, V> child = (comparison < 0) ? nearest.left : nearest.right;
|
||||
if (child == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
nearest = child;
|
||||
}
|
||||
}
|
||||
|
||||
// The key doesn't exist in this tree.
|
||||
if (!create) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create the node and add it to the tree or the table.
|
||||
Node<K, V> header = this.header;
|
||||
Node<K, V> created;
|
||||
if (nearest == null) {
|
||||
// Check that the value is comparable if we didn't do any comparisons.
|
||||
if (comparator == NATURAL_ORDER && !(key instanceof Comparable)) {
|
||||
throw new ClassCastException(key.getClass().getName() + " is not Comparable");
|
||||
}
|
||||
created = new Node<>(allowNullValues, nearest, key, header, header.prev);
|
||||
root = created;
|
||||
} else {
|
||||
created = new Node<>(allowNullValues, nearest, key, header, header.prev);
|
||||
if (comparison < 0) { // nearest.key is higher
|
||||
nearest.left = created;
|
||||
} else { // comparison > 0, nearest.key is lower
|
||||
nearest.right = created;
|
||||
}
|
||||
rebalance(nearest, true);
|
||||
}
|
||||
size++;
|
||||
modCount++;
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Node<K, V> findByObject(Object key) {
|
||||
try {
|
||||
return key != null ? find((K) key, false) : null;
|
||||
} catch (ClassCastException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this map's entry that has the same key and value as {@code entry}, or null if this map
|
||||
* has no such entry.
|
||||
*
|
||||
* <p>This method uses the comparator for key equality rather than {@code equals}. If this map's
|
||||
* comparator isn't consistent with equals (such as {@code String.CASE_INSENSITIVE_ORDER}), then
|
||||
* {@code remove()} and {@code contains()} will violate the collections API.
|
||||
*/
|
||||
Node<K, V> findByEntry(Entry<?, ?> entry) {
|
||||
Node<K, V> mine = findByObject(entry.getKey());
|
||||
boolean valuesEqual = mine != null && equal(mine.value, entry.getValue());
|
||||
return valuesEqual ? mine : null;
|
||||
}
|
||||
|
||||
private static boolean equal(Object a, Object b) {
|
||||
return Objects.equals(a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes {@code node} from this tree, rearranging the tree's structure as necessary.
|
||||
*
|
||||
* @param unlink true to also unlink this node from the iteration linked list.
|
||||
*/
|
||||
void removeInternal(Node<K, V> node, boolean unlink) {
|
||||
if (unlink) {
|
||||
node.prev.next = node.next;
|
||||
node.next.prev = node.prev;
|
||||
}
|
||||
|
||||
Node<K, V> left = node.left;
|
||||
Node<K, V> right = node.right;
|
||||
Node<K, V> originalParent = node.parent;
|
||||
if (left != null && right != null) {
|
||||
|
||||
/*
|
||||
* To remove a node with both left and right subtrees, move an
|
||||
* adjacent node from one of those subtrees into this node's place.
|
||||
*
|
||||
* Removing the adjacent node may change this node's subtrees. This
|
||||
* node may no longer have two subtrees once the adjacent node is
|
||||
* gone!
|
||||
*/
|
||||
|
||||
Node<K, V> adjacent = (left.height > right.height) ? left.last() : right.first();
|
||||
removeInternal(adjacent, false); // takes care of rebalance and size--
|
||||
|
||||
int leftHeight = 0;
|
||||
left = node.left;
|
||||
if (left != null) {
|
||||
leftHeight = left.height;
|
||||
adjacent.left = left;
|
||||
left.parent = adjacent;
|
||||
node.left = null;
|
||||
}
|
||||
|
||||
int rightHeight = 0;
|
||||
right = node.right;
|
||||
if (right != null) {
|
||||
rightHeight = right.height;
|
||||
adjacent.right = right;
|
||||
right.parent = adjacent;
|
||||
node.right = null;
|
||||
}
|
||||
|
||||
adjacent.height = Math.max(leftHeight, rightHeight) + 1;
|
||||
replaceInParent(node, adjacent);
|
||||
return;
|
||||
} else if (left != null) {
|
||||
replaceInParent(node, left);
|
||||
node.left = null;
|
||||
} else if (right != null) {
|
||||
replaceInParent(node, right);
|
||||
node.right = null;
|
||||
} else {
|
||||
replaceInParent(node, null);
|
||||
}
|
||||
|
||||
rebalance(originalParent, false);
|
||||
size--;
|
||||
modCount++;
|
||||
}
|
||||
|
||||
Node<K, V> removeInternalByKey(Object key) {
|
||||
Node<K, V> node = findByObject(key);
|
||||
if (node != null) {
|
||||
removeInternal(node, true);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
private void replaceInParent(Node<K, V> node, Node<K, V> replacement) {
|
||||
Node<K, V> parent = node.parent;
|
||||
node.parent = null;
|
||||
if (replacement != null) {
|
||||
replacement.parent = parent;
|
||||
}
|
||||
|
||||
if (parent != null) {
|
||||
if (parent.left == node) {
|
||||
parent.left = replacement;
|
||||
} else {
|
||||
assert parent.right == node;
|
||||
parent.right = replacement;
|
||||
}
|
||||
} else {
|
||||
root = replacement;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebalances the tree by making any AVL rotations necessary between the newly-unbalanced node and
|
||||
* the tree's root.
|
||||
*
|
||||
* @param insert true if the node was unbalanced by an insert; false if it was by a removal.
|
||||
*/
|
||||
private void rebalance(Node<K, V> unbalanced, boolean insert) {
|
||||
for (Node<K, V> node = unbalanced; node != null; node = node.parent) {
|
||||
Node<K, V> left = node.left;
|
||||
Node<K, V> right = node.right;
|
||||
int leftHeight = left != null ? left.height : 0;
|
||||
int rightHeight = right != null ? right.height : 0;
|
||||
|
||||
int delta = leftHeight - rightHeight;
|
||||
if (delta == -2) {
|
||||
Node<K, V> rightLeft = right.left;
|
||||
Node<K, V> rightRight = right.right;
|
||||
int rightRightHeight = rightRight != null ? rightRight.height : 0;
|
||||
int rightLeftHeight = rightLeft != null ? rightLeft.height : 0;
|
||||
|
||||
int rightDelta = rightLeftHeight - rightRightHeight;
|
||||
if (rightDelta == -1 || (rightDelta == 0 && !insert)) {
|
||||
rotateLeft(node); // AVL right right
|
||||
} else {
|
||||
assert (rightDelta == 1);
|
||||
rotateRight(right); // AVL right left
|
||||
rotateLeft(node);
|
||||
}
|
||||
if (insert) {
|
||||
break; // no further rotations will be necessary
|
||||
}
|
||||
|
||||
} else if (delta == 2) {
|
||||
Node<K, V> leftLeft = left.left;
|
||||
Node<K, V> leftRight = left.right;
|
||||
int leftRightHeight = leftRight != null ? leftRight.height : 0;
|
||||
int leftLeftHeight = leftLeft != null ? leftLeft.height : 0;
|
||||
|
||||
int leftDelta = leftLeftHeight - leftRightHeight;
|
||||
if (leftDelta == 1 || (leftDelta == 0 && !insert)) {
|
||||
rotateRight(node); // AVL left left
|
||||
} else {
|
||||
assert (leftDelta == -1);
|
||||
rotateLeft(left); // AVL left right
|
||||
rotateRight(node);
|
||||
}
|
||||
if (insert) {
|
||||
break; // no further rotations will be necessary
|
||||
}
|
||||
|
||||
} else if (delta == 0) {
|
||||
node.height = leftHeight + 1; // leftHeight == rightHeight
|
||||
if (insert) {
|
||||
break; // the insert caused balance, so rebalancing is done!
|
||||
}
|
||||
|
||||
} else {
|
||||
assert (delta == -1 || delta == 1);
|
||||
node.height = Math.max(leftHeight, rightHeight) + 1;
|
||||
if (!insert) {
|
||||
break; // the height hasn't changed, so rebalancing is done!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Rotates the subtree so that its root's right child is the new root. */
|
||||
private void rotateLeft(Node<K, V> root) {
|
||||
Node<K, V> left = root.left;
|
||||
Node<K, V> pivot = root.right;
|
||||
Node<K, V> pivotLeft = pivot.left;
|
||||
Node<K, V> pivotRight = pivot.right;
|
||||
|
||||
// move the pivot's left child to the root's right
|
||||
root.right = pivotLeft;
|
||||
if (pivotLeft != null) {
|
||||
pivotLeft.parent = root;
|
||||
}
|
||||
|
||||
replaceInParent(root, pivot);
|
||||
|
||||
// move the root to the pivot's left
|
||||
pivot.left = root;
|
||||
root.parent = pivot;
|
||||
|
||||
// fix heights
|
||||
root.height =
|
||||
Math.max(left != null ? left.height : 0, pivotLeft != null ? pivotLeft.height : 0) + 1;
|
||||
pivot.height = Math.max(root.height, pivotRight != null ? pivotRight.height : 0) + 1;
|
||||
}
|
||||
|
||||
/** Rotates the subtree so that its root's left child is the new root. */
|
||||
private void rotateRight(Node<K, V> root) {
|
||||
Node<K, V> pivot = root.left;
|
||||
Node<K, V> right = root.right;
|
||||
Node<K, V> pivotLeft = pivot.left;
|
||||
Node<K, V> pivotRight = pivot.right;
|
||||
|
||||
// move the pivot's right child to the root's left
|
||||
root.left = pivotRight;
|
||||
if (pivotRight != null) {
|
||||
pivotRight.parent = root;
|
||||
}
|
||||
|
||||
replaceInParent(root, pivot);
|
||||
|
||||
// move the root to the pivot's right
|
||||
pivot.right = root;
|
||||
root.parent = pivot;
|
||||
|
||||
// fixup heights
|
||||
root.height =
|
||||
Math.max(right != null ? right.height : 0, pivotRight != null ? pivotRight.height : 0) + 1;
|
||||
pivot.height = Math.max(root.height, pivotLeft != null ? pivotLeft.height : 0) + 1;
|
||||
}
|
||||
|
||||
private EntrySet entrySet;
|
||||
private KeySet keySet;
|
||||
|
||||
@Override
|
||||
public Set<Entry<K, V>> entrySet() {
|
||||
EntrySet result = entrySet;
|
||||
return result != null ? result : (entrySet = new EntrySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<K> keySet() {
|
||||
KeySet result = keySet;
|
||||
return result != null ? result : (keySet = new KeySet());
|
||||
}
|
||||
|
||||
static final class Node<K, V> implements Entry<K, V> {
|
||||
Node<K, V> parent;
|
||||
Node<K, V> left;
|
||||
Node<K, V> right;
|
||||
Node<K, V> next;
|
||||
Node<K, V> prev;
|
||||
final K key;
|
||||
final boolean allowNullValue;
|
||||
V value;
|
||||
int height;
|
||||
|
||||
/** Create the header entry */
|
||||
Node(boolean allowNullValue) {
|
||||
key = null;
|
||||
this.allowNullValue = allowNullValue;
|
||||
next = prev = this;
|
||||
}
|
||||
|
||||
/** Create a regular entry */
|
||||
Node(boolean allowNullValue, Node<K, V> parent, K key, Node<K, V> next, Node<K, V> prev) {
|
||||
this.parent = parent;
|
||||
this.key = key;
|
||||
this.allowNullValue = allowNullValue;
|
||||
this.height = 1;
|
||||
this.next = next;
|
||||
this.prev = prev;
|
||||
prev.next = this;
|
||||
next.prev = this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public K getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V setValue(V value) {
|
||||
if (value == null && !allowNullValue) {
|
||||
throw new NullPointerException("value == null");
|
||||
}
|
||||
V oldValue = this.value;
|
||||
this.value = value;
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o instanceof Entry) {
|
||||
Entry<?, ?> other = (Entry<?, ?>) o;
|
||||
return (key == null ? other.getKey() == null : key.equals(other.getKey()))
|
||||
&& (value == null ? other.getValue() == null : value.equals(other.getValue()));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return key + "=" + value;
|
||||
}
|
||||
|
||||
/** Returns the first node in this subtree. */
|
||||
public Node<K, V> first() {
|
||||
Node<K, V> node = this;
|
||||
Node<K, V> child = node.left;
|
||||
while (child != null) {
|
||||
node = child;
|
||||
child = node.left;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/** Returns the last node in this subtree. */
|
||||
public Node<K, V> last() {
|
||||
Node<K, V> node = this;
|
||||
Node<K, V> child = node.right;
|
||||
while (child != null) {
|
||||
node = child;
|
||||
child = node.right;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class LinkedTreeMapIterator<T> implements Iterator<T> {
|
||||
Node<K, V> next = header.next;
|
||||
Node<K, V> lastReturned = null;
|
||||
int expectedModCount = modCount;
|
||||
|
||||
LinkedTreeMapIterator() {}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
public final boolean hasNext() {
|
||||
return next != header;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
final Node<K, V> nextNode() {
|
||||
Node<K, V> e = next;
|
||||
if (e == header) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
if (modCount != expectedModCount) {
|
||||
throw new ConcurrentModificationException();
|
||||
}
|
||||
next = e.next;
|
||||
return lastReturned = e;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void remove() {
|
||||
if (lastReturned == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
removeInternal(lastReturned, true);
|
||||
lastReturned = null;
|
||||
expectedModCount = modCount;
|
||||
}
|
||||
}
|
||||
|
||||
class EntrySet extends AbstractSet<Entry<K, V>> {
|
||||
@Override
|
||||
public int size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Entry<K, V>> iterator() {
|
||||
return new LinkedTreeMapIterator<Entry<K, V>>() {
|
||||
@Override
|
||||
public Entry<K, V> next() {
|
||||
return nextNode();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object o) {
|
||||
return o instanceof Entry && findByEntry((Entry<?, ?>) o) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object o) {
|
||||
if (!(o instanceof Entry)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Node<K, V> node = findByEntry((Entry<?, ?>) o);
|
||||
if (node == null) {
|
||||
return false;
|
||||
}
|
||||
removeInternal(node, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
LinkedTreeMap.this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
final class KeySet extends AbstractSet<K> {
|
||||
@Override
|
||||
public int size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<K> iterator() {
|
||||
return new LinkedTreeMapIterator<K>() {
|
||||
@Override
|
||||
public K next() {
|
||||
return nextNode().key;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object o) {
|
||||
return containsKey(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object key) {
|
||||
return removeInternalByKey(key) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
LinkedTreeMap.this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If somebody is unlucky enough to have to serialize one of these, serialize it as a
|
||||
* LinkedHashMap so that they won't need Gson on the other side to deserialize it. Using
|
||||
* serialization defeats our DoS defence, so most apps shouldn't use it.
|
||||
*/
|
||||
private Object writeReplace() throws ObjectStreamException {
|
||||
return new LinkedHashMap<>(this);
|
||||
}
|
||||
|
||||
private void readObject(ObjectInputStream in) throws IOException {
|
||||
// Don't permit directly deserializing this class; writeReplace() should have written a
|
||||
// replacement
|
||||
throw new InvalidObjectException("Deserialization is unsupported");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
* Copyright (C) 2012 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 io.gitlab.jfronny.commons.test;
|
||||
|
||||
import io.gitlab.jfronny.commons.data.LinkedTreeMap;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public final class LinkedTreeMapTest {
|
||||
|
||||
@Test
|
||||
public void testIterationOrder() {
|
||||
LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
|
||||
map.put("a", "android");
|
||||
map.put("c", "cola");
|
||||
map.put("b", "bbq");
|
||||
assertIterationOrder(map.keySet(), "a", "c", "b");
|
||||
assertIterationOrder(map.values(), "android", "cola", "bbq");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveRootDoesNotDoubleUnlink() {
|
||||
LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
|
||||
map.put("a", "android");
|
||||
map.put("c", "cola");
|
||||
map.put("b", "bbq");
|
||||
Iterator<Entry<String, String>> it = map.entrySet().iterator();
|
||||
it.next();
|
||||
it.next();
|
||||
it.next();
|
||||
it.remove();
|
||||
assertIterationOrder(map.keySet(), "a", "c");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("ModifiedButNotUsed")
|
||||
public void testPutNullKeyFails() {
|
||||
LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
|
||||
try {
|
||||
map.put(null, "android");
|
||||
fail();
|
||||
} catch (NullPointerException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("ModifiedButNotUsed")
|
||||
public void testPutNonComparableKeyFails() {
|
||||
LinkedTreeMap<Object, String> map = new LinkedTreeMap<>();
|
||||
try {
|
||||
map.put(new Object(), "android");
|
||||
fail();
|
||||
} catch (ClassCastException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPutNullValue() {
|
||||
LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
|
||||
map.put("a", null);
|
||||
|
||||
assertEquals(1, map.size());
|
||||
assertTrue(map.containsKey("a"));
|
||||
assertTrue(map.containsValue(null));
|
||||
assertNull(map.get("a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPutNullValue_Forbidden() {
|
||||
LinkedTreeMap<String, String> map = new LinkedTreeMap<>(false);
|
||||
try {
|
||||
map.put("a", null);
|
||||
fail();
|
||||
} catch (NullPointerException e) {
|
||||
assertEquals("value == null", e.getMessage());
|
||||
}
|
||||
assertEquals(0, map.size());
|
||||
assertFalse(map.containsKey("a"));
|
||||
assertFalse(map.containsValue(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEntrySetValueNull() {
|
||||
LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
|
||||
map.put("a", "1");
|
||||
assertEquals("1", map.get("a"));
|
||||
Entry<String, String> entry = map.entrySet().iterator().next();
|
||||
assertEquals("a", entry.getKey());
|
||||
assertEquals("1", entry.getValue());
|
||||
entry.setValue(null);
|
||||
assertNull(entry.getValue());
|
||||
|
||||
assertTrue(map.containsKey("a"));
|
||||
assertTrue(map.containsValue(null));
|
||||
assertNull(map.get("a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEntrySetValueNull_Forbidden() {
|
||||
LinkedTreeMap<String, String> map = new LinkedTreeMap<>(false);
|
||||
map.put("a", "1");
|
||||
Entry<String, String> entry = map.entrySet().iterator().next();
|
||||
try {
|
||||
entry.setValue(null);
|
||||
fail();
|
||||
} catch (NullPointerException e) {
|
||||
assertEquals("value == null", e.getMessage());
|
||||
}
|
||||
assertEquals("1", entry.getValue());
|
||||
assertEquals("1", map.get("a"));
|
||||
assertFalse(map.containsValue(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContainsNonComparableKeyReturnsFalse() {
|
||||
LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
|
||||
map.put("a", "android");
|
||||
assertFalse(map.containsKey(new Object()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContainsNullKeyIsAlwaysFalse() {
|
||||
LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
|
||||
assertFalse(map.containsKey(null));
|
||||
map.put("a", "android");
|
||||
assertFalse(map.containsKey(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPutOverrides() throws Exception {
|
||||
LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
|
||||
assertNull(map.put("d", "donut"));
|
||||
assertNull(map.put("e", "eclair"));
|
||||
assertNull(map.put("f", "froyo"));
|
||||
assertEquals(3, map.size());
|
||||
|
||||
assertEquals("donut", map.get("d"));
|
||||
assertEquals("donut", map.put("d", "done"));
|
||||
assertEquals(3, map.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyStringValues() {
|
||||
LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
|
||||
map.put("a", "");
|
||||
assertTrue(map.containsKey("a"));
|
||||
assertEquals("", map.get("a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLargeSetOfRandomKeys() {
|
||||
Random random = new Random(1367593214724L);
|
||||
LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
|
||||
String[] keys = new String[1000];
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
keys[i] = Integer.toString(random.nextInt(), 36) + "-" + i;
|
||||
map.put(keys[i], "" + i);
|
||||
}
|
||||
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
String key = keys[i];
|
||||
assertTrue(map.containsKey(key));
|
||||
assertEquals("" + i, map.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClear() {
|
||||
LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
|
||||
map.put("a", "android");
|
||||
map.put("c", "cola");
|
||||
map.put("b", "bbq");
|
||||
map.clear();
|
||||
assertIterationOrder(map.keySet());
|
||||
assertEquals(0, map.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEqualsAndHashCode() throws Exception {
|
||||
LinkedTreeMap<String, Integer> map1 = new LinkedTreeMap<>();
|
||||
map1.put("A", 1);
|
||||
map1.put("B", 2);
|
||||
map1.put("C", 3);
|
||||
map1.put("D", 4);
|
||||
|
||||
LinkedTreeMap<String, Integer> map2 = new LinkedTreeMap<>();
|
||||
map2.put("C", 3);
|
||||
map2.put("B", 2);
|
||||
map2.put("D", 4);
|
||||
map2.put("A", 1);
|
||||
|
||||
assertEquals(map1, map2);
|
||||
assertEquals(map1.hashCode(), map2.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJavaSerialization() throws IOException, ClassNotFoundException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ObjectOutputStream objOut = new ObjectOutputStream(out);
|
||||
Map<String, Integer> map = new LinkedTreeMap<>();
|
||||
map.put("a", 1);
|
||||
objOut.writeObject(map);
|
||||
objOut.close();
|
||||
|
||||
ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(out.toByteArray()));
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Integer> deserialized = (Map<String, Integer>) objIn.readObject();
|
||||
assertEquals(Collections.singletonMap("a", 1), deserialized);
|
||||
}
|
||||
|
||||
@SuppressWarnings("varargs")
|
||||
@SafeVarargs
|
||||
private static final <T> void assertIterationOrder(Iterable<T> actual, T... expected) {
|
||||
ArrayList<T> actualList = new ArrayList<>();
|
||||
for (T t : actual) {
|
||||
actualList.add(t);
|
||||
}
|
||||
assertEquals(Arrays.asList(expected), actualList);
|
||||
}
|
||||
}
|
|
@ -4,12 +4,15 @@ annotations = "24.1.0"
|
|||
gradle-kotlin-dsl = "4.3.0"
|
||||
jf-scripts = "1.5-SNAPSHOT"
|
||||
gson = "2.10.3-SNAPSHOT"
|
||||
google-truth = "1.4.2"
|
||||
|
||||
[libraries]
|
||||
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
|
||||
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
|
||||
junit-vintage = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit" }
|
||||
annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" }
|
||||
gson = { module = "io.gitlab.jfronny:gson", version.ref = "gson" }
|
||||
google-truth = { module = "com.google.truth:truth", version.ref = "google-truth" }
|
||||
|
||||
plugin-kotlin = { module = "org.gradle.kotlin:gradle-kotlin-dsl-plugins", version.ref = "gradle-kotlin-dsl"}
|
||||
plugin-convention = { module = "io.gitlab.jfronny:convention", version.ref="jf-scripts" }
|
|
@ -3,6 +3,7 @@ rootProject.name = "JfCommons"
|
|||
include("commons")
|
||||
include("commons-serialize")
|
||||
include("commons-serialize-gson")
|
||||
include("commons-serialize-json")
|
||||
include("commons-serialize-gson-dsl")
|
||||
include("commons-io")
|
||||
include("commons-logger")
|
||||
|
|
Loading…
Reference in New Issue