All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.openhft.chronicle.wire.JSONWire Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2016-2020 chronicle.software
 *
 *       https://chronicle.software
 *
 * 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 net.openhft.chronicle.wire;

import net.openhft.chronicle.bytes.*;
import net.openhft.chronicle.core.Jvm;
import net.openhft.chronicle.core.io.ClosedIllegalStateException;
import net.openhft.chronicle.core.io.IORuntimeException;
import net.openhft.chronicle.core.io.InvalidMarshallableException;
import net.openhft.chronicle.core.pool.ClassLookup;
import net.openhft.chronicle.core.threads.ThreadLocalHelper;
import net.openhft.chronicle.core.util.ClassNotFoundRuntimeException;
import net.openhft.chronicle.core.util.UnresolvedType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.ref.WeakReference;
import java.lang.reflect.Type;
import java.nio.BufferUnderflowException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Supplier;

import static net.openhft.chronicle.bytes.NativeBytes.nativeBytes;

/**
 * Represents the JSON wire format.
 * 

* This class provides functionality for managing JSON data in a wire format. * It currently provides a subset of functionalities similar to the YAML wire format. * The core capability of this class is to handle JSON data structures as {@code Bytes} * objects, allowing for efficient manipulation and parsing. */ @SuppressWarnings("this-escape") public class JSONWire extends TextWire { // The rest of null private static final @NotNull Bytes _ULL = Bytes.from("ull"); @Deprecated(/* to be removed in x.28 */) public static final @NotNull Bytes ULL = _ULL; // the rest of true private static final @NotNull Bytes _RUE = Bytes.from("rue"); // the rest of false private static final @NotNull Bytes _ALSE = Bytes.from("alse"); // Bytes for comma, commonly used as JSON separator. @SuppressWarnings("rawtypes") static final BytesStore COMMA = BytesStore.from(","); // A thread-local variable to store a reference to the stop characters tester for JSON parsing. static final ThreadLocal> STRICT_ESCAPED_END_OF_TEXT_JSON = new ThreadLocal<>(); // Supplier for stop character tester for strict JSON text that escapes specific characters. static final Supplier STRICT_END_OF_TEXT_JSON_ESCAPING = TextStopCharsTesters.STRICT_END_OF_TEXT_JSON::escaping; // Flag to determine whether to use types or not during parsing. boolean useTypes; private JSONValueOutFromStart valueOutFromStart; /** * Default constructor, initializes with elastic bytes allocated on heap. */ @SuppressWarnings("rawtypes") public JSONWire() { this(Bytes.allocateElasticOnHeap()); } /** * Constructs a JSONWire with the given bytes and a flag for using 8-bit. * * @param bytes The bytes to be used for initializing. * @param use8bit Flag indicating whether to use 8-bit representation. */ public JSONWire(@NotNull Bytes bytes, boolean use8bit) { super(bytes, use8bit); trimFirstCurly(false); } /** * Constructs a JSONWire with the given bytes. * * @param bytes The bytes to be used for initializing. */ @SuppressWarnings("rawtypes") public JSONWire(@NotNull Bytes bytes) { this(bytes, false); } /** * Static method to construct a JSONWire from a string representation of JSON. * * @param text The string containing JSON data. * @return A new instance of JSONWire. */ @NotNull public static JSONWire from(@NotNull String text) { return new JSONWire(Bytes.from(text)); } /** * Converts the content of the provided wire to a JSON string. * * @param wire The wire instance to be converted. * @return The string representation of the JSON content. * @throws InvalidMarshallableException If there's an error during conversion. */ public static String asText(@NotNull Wire wire) throws InvalidMarshallableException { long pos = wire.bytes().readPosition(); @NotNull JSONWire tw = new JSONWire(nativeBytes()); wire.copyTo(tw); wire.bytes().readPosition(pos); return tw.toString(); } /** * Determines if a given class is a wrapper type in Java. *

* This is useful for handling certain JSON conversion scenarios where * native types have wrapper counterparts, such as int and Integer. * * @param type The class to be checked. * @return {@code true} if the class is a Java wrapper type, otherwise {@code false}. */ static boolean isWrapper(Class type) { return type == Integer.class || type == Long.class || type == Float.class || type == Double.class || type == Short.class || type == Character.class || type == Byte.class || type == Boolean.class || type == Void.class; } @Override protected Class defaultKeyClass() { return String.class; } /** * Sets the flag to determine whether to use types during the JSON parsing or not. *

* This method is designed to follow the builder pattern, allowing it to be chained with other method calls on the {@code JSONWire} object. * * @param outputTypes A boolean value indicating whether to use types. * @return The current instance of the {@code JSONWire} class. */ public JSONWire useTypes(boolean outputTypes) { this.useTypes = outputTypes; return this; } /** * Gets the current setting for the use of types during JSON parsing. * * @return {@code true} if types are being used in the current instance, otherwise {@code false}. */ public boolean useTypes() { return useTypes; } @Override public @NotNull TextWire useTextDocuments() { readContext = new JSONReadDocumentContext(this); writeContext = trimFirstCurly() ? new TextWriteDocumentContext(this) : new JSONWriteDocumentContext(this); return this; } @NotNull @Override protected JSONValueOut createValueOut() { return new JSONValueOut(); } @NotNull @Override protected TextValueIn createValueIn() { return new JSONValueIn() { @Override public double float64() { consumePadding(); valueIn.skipType(); int sep = 0; switch (peekCode()) { case '[': case '{': Jvm.warn().on(getClass(), "Unable to read " + valueIn.objectBestEffort() + " as a double."); return 0; case '\'': case '"': sep = bytes.readUnsignedByte(); break; } boolean isNull; long l = bytes.readLimit(); try { bytes.readLimit(bytes.readPosition() + 4); isNull = "null".contentEquals(bytes); } finally { bytes.readLimit(l); } if (isNull) { bytes.readSkip("null".length()); consumePadding(); } final double v = isNull ? Double.NaN : bytes.parseDouble(); if (sep != 0) { int end = peekBack(); if (end != sep) throw new IORuntimeException("Expected " + (char) sep + " but was " + (char) end); } else { checkRewindDouble(); } return v; } @Override public void checkRewind() { int ch = peekBack(); if (ch == ':' || ch == '}' || ch == ']') bytes.readSkip(-1); // !='l' to handle 'null' in JSON wire else if (ch != 'l' && (ch > 'F' && (ch < 'a' || ch > 'f'))) { throw new IllegalArgumentException("Unexpected character in number '" + (char) ch + '\''); } } }; } @Override public void copyTo(@NotNull WireOut wire) throws InvalidMarshallableException { if (wire.getClass() == getClass()) { final Bytes bytes0 = bytes(); final long length = bytes0.readRemaining(); wire.bytes().write(this.bytes, bytes0.readPosition(), length); this.bytes.readSkip(length); return; } consumePadding(); trimCurlyBrackets(); while (bytes.readRemaining() > 1) { copyOne(wire, true, true); consumePadding(); } } private void trimCurlyBrackets() { // If the next byte is a closing curly bracket if (peekNextByte() == '}') { // Move past the closing curly bracket bytes.readSkip(1); // Consume any padding characters (e.g., whitespace) consumePadding(); // Loop backwards through the byte buffer, trimming whitespace or other padding characters while (peekPreviousByte() <= ' ') bytes.writeSkip(-1); // If the previous character is also a closing curly bracket, skip past it if (peekPreviousByte() == '}') bytes.writeSkip(-1); // TODO: Handle the case where an expected '}' character is missing (potential error situation) } } /** * Peeks the previous byte from the current read position without moving the read pointer. * * @return The byte value just before the current read position. */ private int peekPreviousByte() { // Return the byte just before the current read limit return bytes.peekUnsignedByte(bytes.readLimit() - 1); } /** * Copies one segment of data from this wire to the given wire output. * The segment copied depends on the first character encountered (e.g., '{' indicates a map). * This method understands JSON structural elements and translates them appropriately. * * @param wire The wire output to copy the data to. * @param expectKeyValues Flag indicating if the current position is inside a map structure. * @param topLevel Flag indicating if this is the topmost level of the copy operation. * @throws InvalidMarshallableException if there's a problem with copying the data. */ public void copyOne(@NotNull WireOut wire, boolean expectKeyValues, boolean topLevel) throws InvalidMarshallableException { consumePadding(); int ch = bytes.readUnsignedByte(); switch (ch) { case '\'': case '"': // Handle quoted values copyQuote(wire, ch, expectKeyValues, topLevel); if (expectKeyValues) { // For key-value pairs, consume any padding and expect a colon (:) separator consumePadding(); int ch2 = bytes.readUnsignedByte(); if (ch2 != ':') throw new IORuntimeException("Expected a ':' but got a '" + (char) ch); // Recursively copy the associated value after the colon copyOne(wire, false, false); } return; case '{': // Determine if this is a type prefix or a standard map, and copy accordingly if (isTypePrefix()) copyTypePrefix(wire); else copyMap(wire); return; case '[': // Handle sequences or arrays copySequence(wire); return; case '+': case '-': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': // Handle numeric values copyNumber(wire); return; case 'N': case 'n': // Special handling for the 'null' value if (compareRest(bytes, _ULL)) { wire.getValueOut().nu11(); return; } break; case 'f': case 'F': // Special handling for the 'false' value if (compareRest(bytes, _ALSE)) { wire.getValueOut().bool(false); return; } break; case 't': case 'T': // Special handling for the 'true' value if (compareRest(bytes, _RUE)) { wire.getValueOut().bool(true); return; } break; default: break; } // If the code reaches here, an unexpected character sequence was found bytes.readSkip(-1); throw new IORuntimeException("Unexpected chars '" + bytes.parse8bit(StopCharTesters.CONTROL_STOP) + "'"); } static boolean compareRest(@NotNull StreamingDataInput in, @NotNull Bytes s) throws BufferUnderflowException, ClosedIllegalStateException { if (s.length() > in.readRemaining()) return false; long position = in.readPosition(); for (int i = 0; i < s.length(); i++) { if (in.readUnsignedByte() != s.charAt(i)) { in.readPosition(position); return false; } } int ch = in.peekUnsignedByte(); if (Character.isLetterOrDigit(ch)) { in.readPosition(position); return false; } while (ch > 0 && ch <= ' ') { in.readSkip(1); ch = in.peekUnsignedByte(); } return true; } /** * Copies a type prefix from the input to the given wire output. * The type prefix is assumed to be a text value prefixed with '@'. This method will extract * the type prefix and pass it on to the wire output. * * @param wire The wire output to copy the type prefix to. * @throws InvalidMarshallableException if there's a problem with copying the data. */ private void copyTypePrefix(WireOut wire) throws InvalidMarshallableException { final StringBuilder sb = acquireStringBuilder(); // Extract the type literal getValueIn().text(sb); // Remove the '@' prefix from the type literal sb.deleteCharAt(0); wire.getValueOut().typePrefix(sb); // Consume any padding characters (e.g., whitespace) consumePadding(); int ch = bytes.readUnsignedByte(); if (ch != ':') throw new IORuntimeException("Expected a ':' after the type " + sb + " but got a " + (char) ch); // Recursively copy the associated value after the colon copyOne(wire, false, false); consumePadding(); int ch2 = bytes.readUnsignedByte(); if (ch2 != '}') throw new IORuntimeException("Expected a '}' after the type " + sb + " but got a " + (char) ch); } /** * Determines if the current position in the byte buffer represents a type prefix. * A type prefix is recognized by a leading '"' character followed by '@'. * * @return True if the current position indicates a type prefix, false otherwise. */ private boolean isTypePrefix() { final long rp = bytes.readPosition(); return bytes.peekUnsignedByte(rp) == '"' && bytes.peekUnsignedByte(rp + 1) == '@'; } /** * Copies a quoted string value from the input to the given wire output. * This method handles escaped characters within the quoted string. * * @param wire The wire output to copy the quoted string to. * @param ch The starting quote character (either single or double quote). * @param inMap Flag indicating if the current position is inside a map structure. * @param topLevel Flag indicating if this is the topmost level of the copy operation. * @throws InvalidMarshallableException if there's a problem with copying the data. */ private void copyQuote(WireOut wire, int ch, boolean inMap, boolean topLevel) throws InvalidMarshallableException { final StringBuilder sb = acquireStringBuilder(); // Extract the quoted text while (bytes.readRemaining() > 0) { int ch2 = bytes.readUnsignedByte(); if (ch2 == ch) break; sb.append((char) ch2); // If an escape character is found, append the following character as well if (ch2 == '\\') sb.append((char) bytes.readUnsignedByte()); } // Process any escaped characters within the text unescape(sb); // Determine how to write the text to the wire based on the provided flags if (topLevel) { wire.writeEvent(String.class, sb); } else if (inMap) { wire.write(sb); } else { wire.getValueOut().text(sb); } } /** * Copies a map structure from the input to the given wire output. * A map is assumed to be a set of key-value pairs enclosed in curly braces '{}'. * * @param wire The wire output to copy the map structure to. * @throws InvalidMarshallableException if there's a problem with copying the data. */ private void copyMap(WireOut wire) throws InvalidMarshallableException { wire.getValueOut().marshallable(out -> { consumePadding(); // Process each key-value pair within the map until the end is reached or the buffer is exhausted while (bytes.readRemaining() > 0) { final int ch = peekNextByte(); // If we've reached the end of the map, move past the closing brace and exit if (ch == '}') { bytes.readSkip(1); return; } // Process one key-value pair within the map copyOne(wire, true, false); // After processing a key-value pair, expect either a comma (next pair) or the end of the map expectComma('}'); } }); } /** * Consumes padding and expects either a comma (indicating another entry) or a given end character. * * @param end The expected end character (e.g., '}' for maps or ']' for sequences). */ private void expectComma(char end) { consumePadding(); final int ch = peekNextByte(); // If we've reached the expected end character, simply return if (ch == end) return; // If a comma is found, move past it and consume any subsequent padding if (ch == ',') { bytes.readSkip(1); consumePadding(); } else { throw new IORuntimeException("Expected a comma or '" + end + "' not a '" + (char) ch + "'"); } } /** * Copies a sequence structure from the input to the given wire output. * A sequence is assumed to be a list of values enclosed in square brackets '[]'. * * @param wire The wire output to copy the sequence to. */ private void copySequence(WireOut wire) { wire.getValueOut().sequence(out -> { // Consume any padding characters (e.g., whitespace) before the sequence content consumePadding(); // Process each value within the sequence until the end is reached or the buffer is almost exhausted while (bytes.readRemaining() > 1) { final int ch = peekNextByte(); // If we've reached the end of the sequence, move past the closing bracket and exit if (ch == ']') { bytes.readSkip(1); return; } // Process one value within the sequence copyOne(wire, false, false); // After processing a value, expect either a comma (next value) or the end of the sequence expectComma(']'); } }); } /** * Peeks at the next byte in the buffer without advancing the read position. * * @return The next byte from the current read position. */ private int peekNextByte() { return bytes.peekUnsignedByte(bytes.readPosition()); } /** * Copies a numeric value from the input buffer to the given wire output. * The method can handle both integer and decimal numbers. For binary wire outputs, * it can distinguish between the two and write the appropriate format. For textual wire outputs, * the number is written as is. * * @param wire The wire output to which the numeric value should be copied. */ private void copyNumber(WireOut wire) { // Move back one position to re-read the first character of the number bytes.readSkip(-1); long rp = bytes.readPosition(); boolean decimal = false; // Continuously read the buffer until a non-numeric character is encountered while (true) { int ch2 = peekNextByte(); switch (ch2) { case '+': case '-': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': bytes.readSkip(1); // If we're dealing with a binary wire format if (wire.isBinary()) { // Check if the character represents a decimal point decimal |= ch2 == '.'; } else { // For textual wire formats, simply append the character wire.bytes().append((char) ch2); } break; // If we encounter an end of structure character or any non-numeric character, stop parsing case '}': case ']': case ',': default: if (wire.isBinary()) { long rl = bytes.readLimit(); try { // Set the read position and limit to parse just the number bytes.readPositionRemaining(rp, bytes.readPosition() - rp); // If the number had a decimal point, treat it as a double, otherwise as a long if (decimal) wire.getValueOut().float64(bytes.parseDouble()); else wire.getValueOut().int64(bytes.parseLong()); } finally { bytes.readLimit(rl); } } else { // For textual wire outputs, append a comma after the number wire.getValueOut().elementSeparator(); } return; } } } @NotNull @Override protected Quotes needsQuotes(@NotNull CharSequence s) { for (int i = 0; i < s.length(); i++) { char ch = s.charAt(i); if (ch == '"' || ch < ' ' || ch == '\\') return Quotes.DOUBLE; } return Quotes.NONE; } @Override void escape(@NotNull CharSequence s) { bytes.writeUnsignedByte('"'); if (needsQuotes(s) == Quotes.NONE) { bytes.appendUtf8(s); } else { escape0(s, Quotes.DOUBLE); } bytes.writeUnsignedByte('"'); } /** * Escapes special characters in a CharSequence as per JSON String encoding standards detailed in RFC 7159, Section 7. * This ensures that the resulting string can be safely embedded within a JSON string while preserving its meaning. * See RFC 7159, Section 7 for more details. * * @param s The CharSequence to escape. * @param quotes Specifies the type of quotes used in the CharSequence and guides escaping. * @see RFC 7159, Section 7 */ protected void escape0(@NotNull CharSequence s, @NotNull Quotes quotes) { for (int i = 0; i < s.length(); i++) { char ch = s.charAt(i); // Switch on each character and apply the appropriate escape sequence switch (ch) { case '\b': // Backspace bytes.append("\\b"); break; case '\t': // Horizontal tab bytes.append("\\t"); break; case '\f': // Form feed bytes.append("\\f"); break; case '\n': // Line feed bytes.append("\\n"); break; case '\r': // Carriage return bytes.append("\\r"); break; case '"': // If the character is the same as the quote type, escape it if (ch == quotes.q) { bytes.writeUnsignedByte('\\').writeUnsignedByte(ch); } else { bytes.writeUnsignedByte(ch); } break; case '\\': // Backslash bytes.writeUnsignedByte('\\').writeUnsignedByte(ch); break; default: // For characters outside the ASCII range, or control characters below ASCII 32, use Unicode escape if (ch < ' ' || ch > 127) appendU4(ch); else bytes.append(ch); break; } } } @SuppressWarnings("rawtypes") @Override public ValueOut writeEvent(Class expectedType, Object eventKey) throws InvalidMarshallableException { return super.writeEvent(String.class, "" + eventKey); } @Override public void writeStartEvent() { } @NotNull @Override protected StringBuilder readField(@NotNull StringBuilder sb) { consumePadding(); int code = peekCode(); if (code == '}') { sb.setLength(0); return sb; } if (code == '{') { if (valueIn.stack.level > 0) throw new IORuntimeException("Expected field name, but got { at " + bytes.toDebugString(64)); valueIn.pushState(); bytes.readSkip(1); } return super.readField(sb); } @Override @NotNull protected StopCharsTester getStrictEscapingEndOfText() { StopCharsTester escaping = ThreadLocalHelper.getTL(STRICT_ESCAPED_END_OF_TEXT_JSON, STRICT_END_OF_TEXT_JSON_ESCAPING); // reset it. escaping.isStopChar(' ', ' '); return escaping; } class JSONReadDocumentContext extends TextReadDocumentContext { private int first; public JSONReadDocumentContext(@Nullable Wire wire) { super(wire); } @Override public void start() { first = bytes.peekUnsignedByte(); if (first == '{') { bytes.readSkip(1); long lastOffset = bytes.readLimit() - 1; if (bytes.peekUnsignedByte(lastOffset) == '}') bytes.readLimit(lastOffset); } super.start(); } @Override public void close() { if (first == '{') { consumePadding(); if (bytes.peekUnsignedByte() == '}') bytes.readSkip(1); } super.close(); } } /** * The JSONWriteDocumentContext class extends the TextWriteDocumentContext class. * It provides a specialized context for writing JSON data, adjusting writing positions * and handling JSON-specific syntax such as curly braces. * */ class JSONWriteDocumentContext extends TextWriteDocumentContext { // Position marker to track the start of a JSON object private long start; /** * Constructor for JSONWriteDocumentContext. * * @param wire The wire to be used for writing data */ public JSONWriteDocumentContext(Wire wire) { super(wire); } @Override public boolean isEmpty() { return wire().bytes().writePosition() == position + 1; } @Override public void start(boolean metaData) { int count = this.count; super.start(metaData); if (count == 0) { bytes.append('{'); start = bytes.writePosition(); } } @Override public void close() { super.close(); if (count == 0) { if (bytes.writePosition() == start) { bytes.writeSkip(-1); } else { bytes.append('}'); } } } } /** * The JSONValueOut class extends the YamlValueOut class. * It provides methods for adjusting and outputting values in JSON format. */ class JSONValueOut extends YamlValueOut { @SuppressWarnings("rawtypes") @NotNull @Override public TextWire typeLiteral(@NotNull BiConsumer> typeTranslator, Class type) { prependSeparator(); append("{\"@type\":\""); typeTranslator.accept(type, bytes); append("\"}"); elementSeparator(); return wireOut(); } @Override protected void trimWhiteSpace() { if (bytes.endsWith('\n') || bytes.endsWith(' ')) bytes.writeSkip(-1); } @Override protected void indent() { // No-op. } @NotNull @Override public String nullOut() { return "null"; } @NotNull @Override public JSONWire typeLiteral(@Nullable CharSequence type) { startBlock('{'); bytes.append("\"@type\":\"" + type + "\""); endBlock('}'); return (JSONWire) wireOut(); } @NotNull @Override public JSONValueOut typePrefix(@NotNull CharSequence typeName) { if (useTypes) { boolean nested = bytes.peekUnsignedByte(bytes.writePosition() - 1) == '{'; if (!nested) startBlock('{'); bytes.append("\"@"); bytes.append(applyAsAlias(classLookup, typeName)); bytes.append("\":"); if (nested) { if (valueOutFromStart == null) valueOutFromStart = new JSONValueOutFromStart(); return valueOutFromStart; } } return this; } private CharSequence applyAsAlias(ClassLookup classLookup, CharSequence typeName) { // TODO use classLookup.applyAsAlias(typeName); try { return classLookup.nameFor(classLookup.forName(typeName)); } catch (Exception e) { return typeName; } } @Override public void endTypePrefix() { super.endTypePrefix(); if (useTypes) { endBlock('}'); elementSeparator(); } } @Override public void elementSeparator() { sep = COMMA; } @Override protected void asTestQuoted(String s, Quotes quotes) { bytes.append('"'); escape0(s, quotes); bytes.append('"'); } @Override protected void popState() { } @Override protected void pushState() { leaf = true; } @Override protected void afterOpen() { sep = EMPTY; } @Override protected void afterClose() { } @Override protected void addNewLine(long pos) { } @Override protected void newLine() { } @Override protected void endField() { sep = COMMA; } @Override protected void fieldValueSeperator() { bytes.writeUnsignedByte(':'); } @Override public void writeComment(@NotNull CharSequence s) { } @Override protected String doubleToString(double d) { return Double.isNaN(d) ? "null" : super.doubleToString(d); } @Override protected String floatToString(float f) { return Float.isNaN(f) ? "null" : super.floatToString(f); } @NotNull @Override public JSONWire rawText(CharSequence value) { bytes.writeByte((byte) '\"'); super.rawText(value); bytes.writeByte((byte) '\"'); return JSONWire.this; } @Override public @NotNull JSONWire date(LocalDate localDate) { return (JSONWire) text(localDate.toString()); } @Override public @NotNull JSONWire dateTime(LocalDateTime localDateTime) { return (JSONWire) text(localDateTime.toString()); } @Override public @NotNull JSONWire object(@NotNull Class expectedType, V v) throws InvalidMarshallableException { return (JSONWire) (useTypes ? super.object(v) : super.object(expectedType, v)); } @Override public @NotNull JSONValueOut typePrefix(Class type) { if (type.isPrimitive() || isWrapper(type) || type.isEnum()) { // Do nothing because there are no other alternatives // and thus, the type is implicitly given in the declaration. return this; } else { return (JSONValueOut) super.typePrefix(type); } } @SuppressWarnings("unchecked") @Override public @NotNull JSONWire marshallable(@Nullable Map map, @NotNull Class kClass, @NotNull Class vClass, boolean leaf) throws InvalidMarshallableException { return (JSONWire) super.marshallable(map, (Class) String.class, vClass, leaf); } public @NotNull JSONWire time(final LocalTime localTime) { // Todo: fix quoted text return (JSONWire) super.time(localTime); /*return text(localTime.toString());*/ } } class JSONValueOutFromStart extends JSONValueOut { @Override public void endTypePrefix() { elementSeparator(); } } /** * The JSONValueIn class extends the TextValueIn class. * It provides specialized methods for interpreting values from JSON data, * ensuring proper handling of JSON-specific constructs like the "null" value. */ class JSONValueIn extends TextValueIn { @Nullable private Type consumeTypeLiteral(BiFunction unresolvedHandler) { long start = bytes.readPosition(); consumePadding(); StringBuilder sb = Wires.acquireStringBuilderScoped().get(); int code = readCode(); if (code != '{') { bytes.readPosition(start); return null; } consumePadding(); sb.setLength(0); text(sb); if (!"@type".contentEquals(sb)) { bytes.readPosition(start); return null; } consumePadding(); if (readCode() != ':') { bytes.readPosition(start); return null; } consumePadding(); sb.setLength(0); text(sb); String clazz = sb.toString().trim(); if (clazz.isEmpty()) { bytes.readPosition(start); return null; } consumePadding(); if (bytes.readRemaining() == 0 || bytes.readChar() != '}') { bytes.readPosition(start); return null; } consumePadding(); if (bytes.readRemaining() > 0 || peekCode() == ',') { bytes.readSkip(1); } try { return classLookup.forName(clazz); } catch (ClassNotFoundRuntimeException e1) { if (unresolvedHandler != null) unresolvedHandler.apply(clazz, e1.getCause()); return UnresolvedType.of(clazz); } } /** * Determines if the current value represents a JSON null value. * * @return True if the value is "null" in the JSON context; otherwise, False. * When true, it consumes the "null" and moves to the next token. * When false, no data is read, only peaked. */ @Override public boolean isNull() { consumePadding(); if (peekStringIgnoreCase("null")) { bytes.readSkip(4); // Skip to the next token, consuming any padding and/or a comma consumePadding(1); // discard the text after it. // text(acquireStringBuilder()); return true; } return false; } @Override public String text() { @Nullable String text = super.text(); return text == null || text.equals("null") ? null : text; } @Override protected boolean isASeparator(int nextChar) { return true; } @Override public @Nullable Object object() throws InvalidMarshallableException { return useTypes ? parseType() : super.object(); } @Override public @Nullable E object(@Nullable Class clazz) throws InvalidMarshallableException { return useTypes ? parseType(null, clazz, true) : super.object(null, clazz, true); } @Override public E object(@Nullable E using, @Nullable Class clazz) throws InvalidMarshallableException { return useTypes ? parseType(using, clazz, true) : super.object(using, clazz, true); } @Override public E object(@Nullable E using, @Nullable Class clazz, boolean bestEffort) throws InvalidMarshallableException { return useTypes ? parseType(using, clazz, bestEffort) : super.object(using, clazz, bestEffort); } @Override public Class typePrefix() { return super.typePrefix(); } @Override public Object typePrefixOrObject(Class tClass) { return super.typePrefixOrObject(tClass); } @Override public Type typeLiteral(BiFunction unresolvedHandler) { return consumeTypeLiteral(unresolvedHandler); } @Override public @Nullable Object marshallable(@NotNull Object object, @NotNull SerializationStrategy strategy) throws BufferUnderflowException, IORuntimeException, InvalidMarshallableException { return super.marshallable(object, strategy); } @Override public boolean isTyped() { // Either we use types for sure or we might use types... return useTypes || super.isTyped(); } /** * Parses the type of the object based on the data. If a type definition is present, * it will use that to determine the class of the object. Otherwise, it falls back * to the default parsing mechanism. * * @return The parsed object. * @throws InvalidMarshallableException If there's an issue with unmarshalling the data. */ private Object parseType() throws InvalidMarshallableException { if (!hasTypeDefinition()) { return super.object(); } else { final StringBuilder sb = acquireStringBuilder(); sb.setLength(0); consume('{'); this.wireIn().read(sb); final Class clazz = classLookup().forName(sb.subSequence(1, sb.length())); Object object = parseType(null, clazz, true); consume('}'); consumePadding(1); return object; } } private void consume(char c) { consumePadding(); if (bytes.peekUnsignedByte() == c) bytes.readByte(); } /** * Parses the type of the object based on the data and the given parameters. It will * either use the provided class or, if a type definition is present in the data, will * override with that. If the provided class or object instance is incompatible with the * type definition, it will throw a ClassCastException. * * @param using The object instance to use, or null if not provided. * @param clazz The class to parse the object as, or null if not provided. * @param bestEffort Indicates whether to give a best effort attempt to parse the object even if it's partially incorrect. * @return The parsed object. * @throws InvalidMarshallableException If there's an issue with unmarshalling the data. * @throws ClassCastException If there's a type mismatch between the provided class or instance and the type definition. */ private E parseType(@Nullable E using, @Nullable Class clazz, boolean bestEffort) throws InvalidMarshallableException { Type aClass = consumeTypeLiteral(null); if (aClass != null) return Jvm.uncheckedCast(aClass); if (!hasTypeDefinition()) { return super.object(using, clazz, bestEffort); } else { final StringBuilder sb = acquireStringBuilder(); sb.setLength(0); readTypeDefinition(sb); final Class overrideClass = Jvm.uncheckedCast(classLookup().forName(sb.subSequence(1, sb.length()))); if (clazz != null && !clazz.isAssignableFrom(overrideClass)) throw new ClassCastException("Unable to cast " + overrideClass.getName() + " to " + clazz.getName()); if (using != null && !overrideClass.isInstance(using)) throw new ClassCastException("Unable to reuse a " + using.getClass().getName() + " as a " + overrideClass.getName()); final E result = super.object(using, overrideClass, bestEffort); // remove the closing bracket from the type definition consumePadding(); final char endBracket = bytes.readChar(); assert endBracket == '}' : "Missing end bracket }, got " + endBracket + " from " + bytes; consumePadding(1); return result; } } /** * Checks if the next set of characters in the bytes stream represents a type definition. * A type definition is expected to start with the pattern {"@ after consuming any padding. * * @return true if a type definition is found, false otherwise. */ boolean hasTypeDefinition() { final long readPos = bytes.readPosition(); try { // Match {"@ with any padding in between consumePadding(); if (bytes.readChar() != '{') return false; consumePadding(); if (bytes.readChar() != '"') return false; consumePadding(); return bytes.readChar() == '@'; } finally { bytes.readPosition(readPos); } } /** * Reads the type definition from the bytes stream into the provided StringBuilder. * It assumes that the current position in the bytes stream is the start of the type * definition and consumes characters until it encounters a colon (":"). * * @param sb The StringBuilder to which the type definition will be appended. * @throws IORuntimeException If the expected opening bracket "{" is not found. */ void readTypeDefinition(StringBuilder sb) { consumePadding(); if (bytes.readChar() != '{') throw new IORuntimeException("Expected { but got " + bytes); consumePadding(); text(sb); consumePadding(); final char colon = bytes.readChar(); assert colon == ':' : "Expected : but got " + colon; } /** * Indicates whether types are being used in the current context or not. * * @return true if types are being used, false otherwise. */ public boolean useTypes() { return useTypes; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy