Compare commits
2 commits
master
...
eric.readJ
Author | SHA1 | Date | |
---|---|---|---|
|
06412db708 | ||
|
dfef8dd426 |
6 changed files with 283 additions and 18 deletions
|
@ -0,0 +1,64 @@
|
|||
package com.squareup.moshi.recipes;
|
||||
|
||||
import com.squareup.moshi.FromJson;
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonQualifier;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import com.squareup.moshi.ToJson;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import okio.Buffer;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
final class ReadJsonString {
|
||||
@Retention(RUNTIME)
|
||||
@JsonQualifier
|
||||
public @interface JsonString {
|
||||
Object ADAPTER = new Object() {
|
||||
@FromJson @JsonString String fromJson(JsonReader reader) throws IOException {
|
||||
return reader.readJsonString().utf8();
|
||||
}
|
||||
|
||||
@ToJson void toJson(JsonWriter writer, @JsonString String value) throws IOException {
|
||||
writer.value(new Buffer().writeUtf8(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static final class Location {
|
||||
final String name;
|
||||
final @JsonString String metadata;
|
||||
|
||||
Location(String name, String metadata) {
|
||||
this.name = name;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
void run() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(JsonString.ADAPTER)
|
||||
.build();
|
||||
JsonAdapter<Location> locationAdapter = moshi.adapter(Location.class);
|
||||
String json = "{\n"
|
||||
+ " \"name\": \"Niagara Falls\",\n"
|
||||
+ " \"metadata\": {\n"
|
||||
+ " \"latitude\": 43.0962,\n"
|
||||
+ " \"longitude\": -79.0377\n"
|
||||
+ " }\n"
|
||||
+ "}";
|
||||
Location location = locationAdapter.fromJson(json);
|
||||
System.out.println(location.metadata);
|
||||
System.out.println(locationAdapter.toJson(new Location("Niagara Falls", "{\n"
|
||||
+ " \"latitude\": 43.0962,\n"
|
||||
+ " \"longitude\": -79.0377\n"
|
||||
+ " }")));
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
new ReadJsonString().run();
|
||||
}
|
||||
}
|
|
@ -423,6 +423,12 @@ public abstract class JsonReader implements Closeable {
|
|||
*/
|
||||
public abstract void skipValue() throws IOException;
|
||||
|
||||
/**
|
||||
* Reads the next JSON name or value without decoding it. White space and comments will be
|
||||
* removed.
|
||||
*/
|
||||
public abstract ByteString readJsonString() throws IOException;
|
||||
|
||||
/**
|
||||
* Returns the value of the next token, consuming it. The result may be a string, number, boolean,
|
||||
* null, map, or list, according to the JSON structure.
|
||||
|
|
|
@ -20,8 +20,10 @@ import java.io.IOException;
|
|||
import java.math.BigDecimal;
|
||||
import javax.annotation.Nullable;
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSink;
|
||||
import okio.BufferedSource;
|
||||
import okio.ByteString;
|
||||
import okio.Okio;
|
||||
|
||||
final class JsonUtf8Reader extends JsonReader {
|
||||
private static final long MIN_INCOMPLETE_INTEGER = Long.MIN_VALUE / 10;
|
||||
|
@ -65,6 +67,8 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
private static final int NUMBER_CHAR_EXP_SIGN = 6;
|
||||
private static final int NUMBER_CHAR_EXP_DIGIT = 7;
|
||||
|
||||
static final BufferedSink BLACKHOLE = Okio.buffer(Okio.blackhole());
|
||||
|
||||
/** The input JSON. */
|
||||
private final BufferedSource source;
|
||||
private final Buffer buffer;
|
||||
|
@ -228,13 +232,17 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
}
|
||||
|
||||
private int doPeek() throws IOException {
|
||||
return doPeek(BLACKHOLE);
|
||||
}
|
||||
|
||||
int doPeek(BufferedSink sink) throws IOException {
|
||||
int peekStack = scopes[stackSize - 1];
|
||||
if (peekStack == JsonScope.EMPTY_ARRAY) {
|
||||
scopes[stackSize - 1] = JsonScope.NONEMPTY_ARRAY;
|
||||
} else if (peekStack == JsonScope.NONEMPTY_ARRAY) {
|
||||
// Look for a comma before the next element.
|
||||
int c = nextNonWhitespace(true);
|
||||
buffer.readByte(); // consume ']' or ','.
|
||||
sink.write(buffer, 1); // consume ']' or ','.
|
||||
switch (c) {
|
||||
case ']':
|
||||
return peeked = PEEKED_END_ARRAY;
|
||||
|
@ -250,7 +258,7 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
// Look for a comma before the next element.
|
||||
if (peekStack == JsonScope.NONEMPTY_OBJECT) {
|
||||
int c = nextNonWhitespace(true);
|
||||
buffer.readByte(); // Consume '}' or ','.
|
||||
sink.write(buffer, 1); // Consume '}' or ','.
|
||||
switch (c) {
|
||||
case '}':
|
||||
return peeked = PEEKED_END_OBJECT;
|
||||
|
@ -265,15 +273,15 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
int c = nextNonWhitespace(true);
|
||||
switch (c) {
|
||||
case '"':
|
||||
buffer.readByte(); // consume the '\"'.
|
||||
sink.write(buffer, 1); // consume the '\"'.
|
||||
return peeked = PEEKED_DOUBLE_QUOTED_NAME;
|
||||
case '\'':
|
||||
buffer.readByte(); // consume the '\''.
|
||||
sink.write(buffer, 1); // consume the '\''.
|
||||
checkLenient();
|
||||
return peeked = PEEKED_SINGLE_QUOTED_NAME;
|
||||
case '}':
|
||||
if (peekStack != JsonScope.NONEMPTY_OBJECT) {
|
||||
buffer.readByte(); // consume the '}'.
|
||||
sink.write(buffer, 1); // consume the '}'.
|
||||
return peeked = PEEKED_END_OBJECT;
|
||||
} else {
|
||||
throw syntaxError("Expected name");
|
||||
|
@ -290,14 +298,14 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
scopes[stackSize - 1] = JsonScope.NONEMPTY_OBJECT;
|
||||
// Look for a colon before the value.
|
||||
int c = nextNonWhitespace(true);
|
||||
buffer.readByte(); // Consume ':'.
|
||||
sink.write(buffer, 1); // Consume ':'.
|
||||
switch (c) {
|
||||
case ':':
|
||||
break;
|
||||
case '=':
|
||||
checkLenient();
|
||||
if (source.request(1) && buffer.getByte(0) == '>') {
|
||||
buffer.readByte(); // Consume '>'.
|
||||
sink.write(buffer, 1); // Consume '>'.
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
@ -320,7 +328,7 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
switch (c) {
|
||||
case ']':
|
||||
if (peekStack == JsonScope.EMPTY_ARRAY) {
|
||||
buffer.readByte(); // Consume ']'.
|
||||
sink.write(buffer, 1); // Consume ']'.
|
||||
return peeked = PEEKED_END_ARRAY;
|
||||
}
|
||||
// fall-through to handle ",]"
|
||||
|
@ -335,16 +343,16 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
}
|
||||
case '\'':
|
||||
checkLenient();
|
||||
buffer.readByte(); // Consume '\''.
|
||||
sink.write(buffer, 1); // Consume '\''.
|
||||
return peeked = PEEKED_SINGLE_QUOTED;
|
||||
case '"':
|
||||
buffer.readByte(); // Consume '\"'.
|
||||
sink.write(buffer, 1); // Consume '\"'.
|
||||
return peeked = PEEKED_DOUBLE_QUOTED;
|
||||
case '[':
|
||||
buffer.readByte(); // Consume '['.
|
||||
sink.write(buffer, 1); // Consume '['.
|
||||
return peeked = PEEKED_BEGIN_ARRAY;
|
||||
case '{':
|
||||
buffer.readByte(); // Consume '{'.
|
||||
sink.write(buffer, 1); // Consume '{'.
|
||||
return peeked = PEEKED_BEGIN_OBJECT;
|
||||
default:
|
||||
}
|
||||
|
@ -857,23 +865,31 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
}
|
||||
|
||||
private void skipQuotedValue(ByteString runTerminator) throws IOException {
|
||||
readQuotedValue(runTerminator, BLACKHOLE);
|
||||
}
|
||||
|
||||
private void readQuotedValue(ByteString runTerminator, BufferedSink sink) throws IOException {
|
||||
while (true) {
|
||||
long index = source.indexOfElement(runTerminator);
|
||||
if (index == -1L) throw syntaxError("Unterminated string");
|
||||
|
||||
if (buffer.getByte(index) == '\\') {
|
||||
buffer.skip(index + 1);
|
||||
readEscapeCharacter();
|
||||
sink.write(buffer, index + 1);
|
||||
readEscapeCharacter(sink);
|
||||
} else {
|
||||
buffer.skip(index + 1);
|
||||
sink.write(buffer, index + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void skipUnquotedValue() throws IOException {
|
||||
readUnquotedValue(BLACKHOLE);
|
||||
}
|
||||
|
||||
private void readUnquotedValue(BufferedSink sink) throws IOException {
|
||||
long i = source.indexOfElement(UNQUOTED_STRING_TERMINALS);
|
||||
buffer.skip(i != -1L ? i : buffer.size());
|
||||
sink.write(buffer, i != -1L ? i : buffer.size());
|
||||
}
|
||||
|
||||
@Override public int nextInt() throws IOException {
|
||||
|
@ -978,6 +994,62 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
pathNames[stackSize - 1] = "null";
|
||||
}
|
||||
|
||||
@Override public ByteString readJsonString() throws IOException {
|
||||
Buffer buffer = new Buffer();
|
||||
readJsonValue(buffer);
|
||||
return buffer.readByteString();
|
||||
}
|
||||
|
||||
void readJsonValue(BufferedSink sink) throws IOException {
|
||||
int p = peeked;
|
||||
if (p == PEEKED_NONE) {
|
||||
p = doPeek();
|
||||
}
|
||||
if (p == PEEKED_BEGIN_ARRAY) {
|
||||
sink.writeUtf8("[");
|
||||
} else if (p == PEEKED_BEGIN_OBJECT) {
|
||||
sink.writeUtf8("{");
|
||||
} else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) {
|
||||
sink.writeUtf8("\"");
|
||||
} else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) {
|
||||
sink.writeUtf8("'");
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
do {
|
||||
p = peeked;
|
||||
if (p == PEEKED_NONE) {
|
||||
p = doPeek(sink);
|
||||
}
|
||||
|
||||
if (p == PEEKED_BEGIN_ARRAY) {
|
||||
pushScope(JsonScope.EMPTY_ARRAY);
|
||||
count++;
|
||||
} else if (p == PEEKED_BEGIN_OBJECT) {
|
||||
pushScope(JsonScope.EMPTY_OBJECT);
|
||||
count++;
|
||||
} else if (p == PEEKED_END_ARRAY) {
|
||||
stackSize--;
|
||||
count--;
|
||||
} else if (p == PEEKED_END_OBJECT) {
|
||||
stackSize--;
|
||||
count--;
|
||||
} else if (p == PEEKED_UNQUOTED_NAME || p == PEEKED_UNQUOTED) {
|
||||
readUnquotedValue(sink);
|
||||
} else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) {
|
||||
readQuotedValue(DOUBLE_QUOTE_OR_SLASH, sink);
|
||||
} else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) {
|
||||
readQuotedValue(SINGLE_QUOTE_OR_SLASH, sink);
|
||||
} else if (p == PEEKED_NUMBER) {
|
||||
sink.write(buffer, peekedNumberLength);
|
||||
}
|
||||
peeked = PEEKED_NONE;
|
||||
} while (count != 0);
|
||||
|
||||
pathIndices[stackSize - 1]++;
|
||||
pathNames[stackSize - 1] = "null";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next character in the stream that is neither whitespace nor a
|
||||
* part of a comment. When this returns, the returned character is always at
|
||||
|
@ -1088,11 +1160,16 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
* @throws IOException if any unicode escape sequences are malformed.
|
||||
*/
|
||||
private char readEscapeCharacter() throws IOException {
|
||||
return readEscapeCharacter(BLACKHOLE);
|
||||
}
|
||||
|
||||
private char readEscapeCharacter(BufferedSink sink) throws IOException {
|
||||
if (!source.request(1)) {
|
||||
throw syntaxError("Unterminated escape sequence");
|
||||
}
|
||||
|
||||
byte escaped = buffer.readByte();
|
||||
sink.writeByte(escaped);
|
||||
switch (escaped) {
|
||||
case 'u':
|
||||
if (!source.request(4)) {
|
||||
|
@ -1113,7 +1190,7 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
throw syntaxError("\\u" + buffer.readUtf8(4));
|
||||
}
|
||||
}
|
||||
buffer.skip(4);
|
||||
sink.write(buffer, 4);
|
||||
return result;
|
||||
|
||||
case 't':
|
||||
|
|
|
@ -22,6 +22,9 @@ import java.util.Iterator;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSink;
|
||||
import okio.ByteString;
|
||||
|
||||
import static com.squareup.moshi.JsonScope.CLOSED;
|
||||
|
||||
|
@ -310,7 +313,7 @@ final class JsonValueReader extends JsonReader {
|
|||
|
||||
if (skipped instanceof Map.Entry) {
|
||||
// We're skipping a name. Promote the map entry's value.
|
||||
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) stack[stackSize - 1];
|
||||
Map.Entry<String, ?> entry = (Map.Entry<String, ?>) stack[stackSize - 1];
|
||||
stack[stackSize - 1] = entry.getValue();
|
||||
} else if (stackSize > 0) {
|
||||
// We're skipping a value.
|
||||
|
@ -322,6 +325,36 @@ final class JsonValueReader extends JsonReader {
|
|||
return new JsonValueReader(this);
|
||||
}
|
||||
|
||||
@Override public ByteString readJsonString() throws IOException {
|
||||
Buffer buffer = new Buffer();
|
||||
readJsonValue(buffer);
|
||||
return buffer.readByteString();
|
||||
}
|
||||
|
||||
void readJsonValue(BufferedSink sink) throws IOException {
|
||||
if (failOnUnknown) {
|
||||
throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath());
|
||||
}
|
||||
|
||||
// If this element is in an object clear out the key.
|
||||
if (stackSize > 1) {
|
||||
pathNames[stackSize - 2] = "null";
|
||||
}
|
||||
|
||||
Object skipped = stackSize != 0 ? stack[stackSize - 1] : null;
|
||||
|
||||
if (skipped instanceof Map.Entry) {
|
||||
// We're skipping a name. Promote the map entry's value.
|
||||
Map.Entry<String, ?> entry = (Map.Entry<String, ?>) stack[stackSize - 1];
|
||||
JsonUtf8Writer.string(sink, entry.getKey());
|
||||
stack[stackSize - 1] = entry.getValue();
|
||||
} else if (stackSize > 0) {
|
||||
// We're skipping a value.
|
||||
JsonWriter.of(sink).writeJsonValue(stack[stackSize - 1]);
|
||||
remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Override void promoteNameToValue() throws IOException {
|
||||
if (hasNext()) {
|
||||
String name = nextName();
|
||||
|
|
|
@ -19,6 +19,8 @@ import java.io.Closeable;
|
|||
import java.io.Flushable;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import javax.annotation.Nullable;
|
||||
import okio.BufferedSink;
|
||||
|
@ -468,4 +470,37 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
|||
@CheckReturnValue public final String getPath() {
|
||||
return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices);
|
||||
}
|
||||
|
||||
JsonWriter writeJsonValue(@Nullable Object jsonValue) throws IOException {
|
||||
if (jsonValue == null) {
|
||||
return nullValue();
|
||||
}
|
||||
if (jsonValue instanceof Number) {
|
||||
return value((Number) jsonValue);
|
||||
}
|
||||
if (jsonValue instanceof String) {
|
||||
return value((String) jsonValue);
|
||||
}
|
||||
if (jsonValue instanceof Boolean) {
|
||||
return value((Boolean) jsonValue);
|
||||
}
|
||||
if (jsonValue instanceof Map) {
|
||||
beginObject();
|
||||
for (Map.Entry<String, Object> entry : ((Map<String, Object>) jsonValue).entrySet()) {
|
||||
name(entry.getKey());
|
||||
writeJsonValue(entry.getValue());
|
||||
}
|
||||
return endObject();
|
||||
}
|
||||
if (jsonValue instanceof List) {
|
||||
beginArray();
|
||||
List<Object> list = (List<Object>) jsonValue;
|
||||
for (int i = 0, size = list.size(); i < size; i++) {
|
||||
writeJsonValue(list.get(i));
|
||||
}
|
||||
return endArray();
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
"Expected a JSON value but found " + jsonValue + ", a " + jsonValue.getClass());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1147,4 +1147,54 @@ public final class JsonReaderTest {
|
|||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void readJsonStringObject() throws IOException {
|
||||
JsonReader reader = newReader("{\"pizzas\": [\"cheese\", \"pepperoni\"]}");
|
||||
reader.setLenient(true);
|
||||
assertEquals("{\"pizzas\":[\"cheese\",\"pepperoni\"]}", reader.readJsonString().utf8());
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
@Test public void readJsonStringArray() throws IOException {
|
||||
JsonReader reader = newReader("{\"pizzas\": [\"cheese\", \"pepperoni\"]}");
|
||||
reader.beginObject();
|
||||
reader.skipName();
|
||||
assertEquals("[\"cheese\",\"pepperoni\"]", reader.readJsonString().utf8());
|
||||
reader.endObject();
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
@Test public void readJsonStringName() throws IOException {
|
||||
JsonReader reader = newReader("{\"pizzas\": [\"cheese\", \"pepperoni\"]}");
|
||||
reader.beginObject();
|
||||
assertEquals("\"pizzas\"", reader.readJsonString().utf8());
|
||||
reader.skipValue();
|
||||
reader.endObject();
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
@Test public void readJsonStringString() throws IOException {
|
||||
JsonReader reader = newReader("{\"pizzas\": [\"cheese\", \"pepperoni\"]}");
|
||||
reader.beginObject();
|
||||
reader.skipName();
|
||||
reader.beginArray();
|
||||
reader.skipValue();
|
||||
assertEquals("\"pepperoni\"", reader.readJsonString().utf8());
|
||||
reader.endArray();
|
||||
reader.endObject();
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
@Test public void readJsonStringSkipsWhiteSpaceAndComments() throws IOException {
|
||||
JsonReader reader = newReader(" {\n"
|
||||
+ "\t\"pizzas\": [\n"
|
||||
+ "\t /* toppings */\n"
|
||||
+ "\t \"cheese\",\n"
|
||||
+ "\t \"pepperoni\" // the best one.\n"
|
||||
+ "\t]\n"
|
||||
+ "} ");
|
||||
reader.setLenient(true);
|
||||
assertEquals("{\"pizzas\":[\"cheese\",\"pepperoni\"]}", reader.readJsonString().utf8());
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue