net.openhft.chronicle.wire.TextWire Maven / Gradle / Ivy
/*
* 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.bytes.ref.*;
import net.openhft.chronicle.bytes.util.Compression;
import net.openhft.chronicle.core.Jvm;
import net.openhft.chronicle.core.Maths;
import net.openhft.chronicle.core.io.*;
import net.openhft.chronicle.core.pool.ClassLookup;
import net.openhft.chronicle.core.scoped.ScopedResource;
import net.openhft.chronicle.core.threads.ThreadLocalHelper;
import net.openhft.chronicle.core.util.*;
import net.openhft.chronicle.core.values.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Type;
import java.nio.BufferUnderflowException;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeParseException;
import java.util.*;
import java.util.function.*;
import java.util.regex.Pattern;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static net.openhft.chronicle.bytes.NativeBytes.nativeBytes;
import static net.openhft.chronicle.wire.TextStopCharTesters.END_OF_TYPE;
import static net.openhft.chronicle.wire.Wires.*;
/**
* A representation of the YAML-based wire format. `TextWire` provides functionalities
* for reading and writing objects in a YAML-based format, and encapsulates various characteristics
* of the YAML text format.
*
* This class utilizes bit sets, thread locals, and regular expressions to efficiently handle
* the YAML formatting nuances.
*
*
Important: Some configurations and methods in this class are marked as deprecated
* and are slated for removal in future versions, suggesting that its behavior might evolve in future releases.
*/
@SuppressWarnings({"rawtypes", "unchecked", "this-escape"})
public class TextWire extends YamlWireOut {
// Constants representing specific textual constructs in YAML.
public static final BytesStore, ?> BINARY = BytesStore.from("!!binary");
public static final @NotNull Bytes TYPE_STR = Bytes.from("type ");
static final String SEQ_MAP = "!seqmap";
// A set of characters considered as "end characters" in this wire format.
static final BitSet END_CHARS = new BitSet();
// Thread locals for stop char testers that might need escaping in specific contexts.
// They are weakly referenced to avoid potential memory leaks in multithreaded environments.
static final ThreadLocal> ESCAPED_QUOTES = new ThreadLocal<>();//ThreadLocal.withInitial(StopCharTesters.QUOTES::escaping);
static final ThreadLocal> ESCAPED_SINGLE_QUOTES = new ThreadLocal<>();//ThreadLocal.withInitial(() -> StopCharTesters.SINGLE_QUOTES.escaping());
static final ThreadLocal> ESCAPED_END_OF_TEXT = new ThreadLocal<>();// ThreadLocal.withInitial(() -> TextStopCharsTesters.END_OF_TEXT.escaping());
static final ThreadLocal> STRICT_ESCAPED_END_OF_TEXT = new ThreadLocal<>();// ThreadLocal.withInitial(() -> TextStopCharsTesters.END_OF_TEXT.escaping());
static final Pattern REGX_PATTERN = Pattern.compile("\\.|\\$");
// Suppliers for various stop char testers.
static final Supplier QUOTES_ESCAPING = StopCharTesters.QUOTES::escaping;
static final Supplier SINGLE_QUOTES_ESCAPING = StopCharTesters.SINGLE_QUOTES::escaping;
static final Supplier END_OF_TEXT_ESCAPING = TextStopCharTesters.END_OF_TEXT::escaping;
static final Supplier STRICT_END_OF_TEXT_ESCAPING = TextStopCharsTesters.STRICT_END_OF_TEXT::escaping;
static final Supplier END_EVENT_NAME_ESCAPING = TextStopCharsTesters.END_EVENT_NAME::escaping;
// Metadata representation in bytes.
static final Bytes> META_DATA = Bytes.from("!!meta-data");
static {
IOTools.unmonitor(BINARY);
//for (char ch : "?%*&@`0123456789+- ',#:{}[]|>!\\".toCharArray())
//for (char ch : "?,#:{}[]|>\\^".toCharArray())
for (char ch : "#:}]".toCharArray())
END_CHARS.set(ch);
// Ensure the interner has loaded.
WireInternal.INTERNER.valueCount();
}
/**
* Input value parser specifically for text-based wire format.
*/
protected final TextValueIn valueIn = createValueIn();
/**
* Represents the start of the current line being processed in the wire format.
*/
protected long lineStart = 0;
/**
* Default value input utility.
*/
private DefaultValueIn defaultValueIn;
/**
* Context for writing documents in the wire format.
*/
protected WriteDocumentContext writeContext;
/**
* Context for reading documents from the wire format.
*/
protected ReadDocumentContext readContext;
/**
* Flag to determine if strict parsing rules are applied.
*/
private boolean strict = false;
/**
* Constructor to initialize the `TextWire` with a specific bytes representation
* and a flag to determine if 8-bit encoding is to be used.
*
* @param bytes Bytes representation.
* @param use8bit Flag to determine if 8-bit encoding is to be used.
*/
public TextWire(@NotNull Bytes> bytes, boolean use8bit) {
super(bytes, use8bit);
}
/**
* Constructor that initializes the `TextWire` with bytes representation
* with default 8-bit encoding turned off.
*
* @param bytes Bytes representation.
*/
public TextWire(@NotNull Bytes> bytes) {
this(bytes, false);
}
/**
* Factory method to create a `TextWire` from a file.
*
* @param name Name of the file.
* @return A new instance of `TextWire`.
* @throws IOException if any I/O error occurs.
*/
@NotNull
public static TextWire fromFile(String name) throws IOException {
return new TextWire(BytesUtil.readFile(name), true);
}
/**
* Factory method to create a `TextWire` from a string representation.
*
* @param text String representation of the wire format.
* @return A new instance of `TextWire`.
*/
@NotNull
public static TextWire from(@NotNull String text) {
return new TextWire(Bytes.from(text));
}
/**
* Converts any wire format into a text representation.
*
* @param wire The wire format to be converted.
* @return The text representation of the wire format.
*/
public static String asText(@NotNull Wire wire) {
NativeBytes bytes = nativeBytes();
ValidatableUtil.startValidateDisabled();
try {
long pos = wire.bytes().readPosition();
@NotNull Wire tw = WireType.TEXT.apply(bytes);
wire.copyTo(tw);
wire.bytes().readPosition(pos);
return tw.toString();
} finally {
ValidatableUtil.endValidateDisabled();
bytes.releaseLast();
}
}
// https://yaml.org/spec/1.2.2/#escaped-characters
/**
* Processes and unescapes the provided {@link CharSequence} containing escaped sequences.
* For instance, "\\n" is converted to a newline character, "\\t" to a tab, etc.
* This method modifies the given sequence directly and adjusts its length if needed.
*
* @param sb A {@link CharSequence} that is also an {@link Appendable}, containing potentially escaped sequences.
* This sequence will be modified directly.
*/
public static void unescape(@NotNull ACS sb) {
int end = 0;
int length = sb.length();
for (int i = 0; i < length; i++) {
char ch = sb.charAt(i);
// Check if the character is an escape character and if there's a character after it
if (ch == '\\' && i < length - 1) {
char ch3 = sb.charAt(++i);
// Handle different escaped characters
switch (ch3) {
case '0':
ch = 0;
break;
case 'a':
ch = 7;
break;
case 'b':
ch = '\b';
break;
case 't':
ch = '\t';
break;
case 'n':
ch = '\n';
break;
case 'v':
ch = 0xB;
break;
case 'f':
ch = 0xC;
break;
case 'r':
ch = '\r';
break;
case 'e':
ch = 0x1B;
break;
case 'N':
ch = 0x85;
break;
case '_':
ch = 0xA0;
break;
case 'L':
ch = 0x2028;
break;
case 'P':
ch = 0x2029;
break;
case 'x':
ch = (char)
(Character.getNumericValue(sb.charAt(++i)) * 16 +
Character.getNumericValue(sb.charAt(++i)));
break;
case 'u':
ch = (char)
(Character.getNumericValue(sb.charAt(++i)) * 4096 +
Character.getNumericValue(sb.charAt(++i)) * 256 +
Character.getNumericValue(sb.charAt(++i)) * 16 +
Character.getNumericValue(sb.charAt(++i)));
break;
default:
ch = ch3;
}
}
// Set the unescaped character into the sequence
AppendableUtil.setCharAt(sb, end++, ch);
}
// Validate the length consistency after unescaping
if (length != sb.length())
throw new IllegalStateException("Length changed from " + length + " to " + sb.length() + " for " + sb);
AppendableUtil.setLength(sb, end);
}
/**
* Acquires a {@link StopCharTester} instance related to escaping single quotes.
* This method utilizes thread-local storage to ensure that the returned instance is thread-safe.
*
* @return A {@link StopCharTester} instance specifically designed for escaping single quotes,
* or null if such an instance could not be acquired.
*/
@Nullable
static StopCharTester getEscapingSingleQuotes() {
// Fetch or create the StopCharTester from thread-local storage
StopCharTester sct = ThreadLocalHelper.getTL(ESCAPED_SINGLE_QUOTES, SINGLE_QUOTES_ESCAPING);
// Reset the StopCharTester instance
sct.isStopChar(' ');
return sct;
}
/**
* Loads an Object from a file
*
* @param filename the file-path containing the object
* @param the type of the object to load
* @return an instance of the object created from the data in the file
* @throws IOException if the file can not be found or read
*/
public static T load(String filename) throws IOException, InvalidMarshallableException {
return (T) TextWire.fromFile(filename).readObject();
}
@Override
public boolean isBinary() {
return false;
}
/**
* Retrieves the current strict mode setting for this TextWire instance.
*
* @return A boolean indicating whether strict mode is enabled (true) or disabled (false).
*/
public boolean strict() {
return strict;
}
/**
* Sets the strict mode for this TextWire instance.
* When strict mode is enabled, the instance may enforce stricter parsing or serialization rules.
*
* @param strict A boolean value to set the strict mode. True to enable, false to disable.
* @return The current TextWire instance, allowing for method chaining.
*/
public TextWire strict(boolean strict) {
this.strict = strict;
return this;
}
@Override
@NotNull
public T methodWriter(@NotNull Class tClass, Class>... additional) {
VanillaMethodWriterBuilder builder = new VanillaMethodWriterBuilder<>(tClass,
WireType.TEXT,
() -> newTextMethodWriterInvocationHandler(tClass));
for (Class> aClass : additional)
builder.addInterface(aClass);
useTextDocuments();
builder.marshallableOut(this);
return builder.build();
}
/**
* Creates a new textual method writer invocation handler based on provided interface(s).
* If any of the provided interfaces have a {@link Comment} annotation,
* the associated comment is written to the wire.
*
* @param interfaces One or more interfaces that the created handler should be aware of.
* @return A newly instantiated {@link TextMethodWriterInvocationHandler} for the provided interface(s).
*/
@NotNull
TextMethodWriterInvocationHandler newTextMethodWriterInvocationHandler(Class>... interfaces) {
for (Class> anInterface : interfaces) {
Comment c = Jvm.findAnnotation(anInterface, Comment.class);
if (c != null)
writeComment(c.value());
}
return new TextMethodWriterInvocationHandler(interfaces[0], this);
}
@Override
@NotNull
public MethodWriterBuilder methodWriterBuilder(@NotNull Class tClass) {
VanillaMethodWriterBuilder text = new VanillaMethodWriterBuilder<>(tClass,
WireType.TEXT,
() -> newTextMethodWriterInvocationHandler(tClass));
text.marshallableOut(this);
return text;
}
@Override
public @NotNull VanillaMethodReaderBuilder methodReaderBuilder() {
return super.methodReaderBuilder().wireType(WireType.TEXT);
}
@Override
public void classLookup(ClassLookup classLookup) {
this.classLookup = classLookup;
}
@Override
public ClassLookup classLookup() {
return classLookup;
}
@NotNull
@Override
public DocumentContext writingDocument(boolean metaData) {
if (writeContext == null)
useTextDocuments();
writeContext.start(metaData);
return writeContext;
}
@Override
public DocumentContext acquireWritingDocument(boolean metaData) {
if (writeContext != null && writeContext.isOpen() && writeContext.chainedElement())
return writeContext;
return writingDocument(metaData);
}
@NotNull
@Override
public DocumentContext readingDocument() {
initReadContext();
return readContext;
}
/**
* Initializes the read context for this TextWire instance.
* If the read context is not already set, the default behavior is to use binary documents.
*/
protected void initReadContext() {
if (readContext == null)
useBinaryDocuments();
readContext.start();
}
/**
* Configures this TextWire instance to use binary document contexts for reading and writing.
* This will replace the current read and write contexts with binary contexts.
*
* @return The current TextWire instance, allowing for method chaining.
*/
@NotNull
public TextWire useBinaryDocuments() {
readContext = new BinaryReadDocumentContext(this);
writeContext = new BinaryWriteDocumentContext(this);
return this;
}
/**
* Configures this TextWire instance to use textual document contexts for reading and writing.
* This will replace the current read and write contexts with textual contexts.
*
* @return The current TextWire instance, allowing for method chaining.
*/
@NotNull
public TextWire useTextDocuments() {
readContext = new TextReadDocumentContext(this);
writeContext = new TextWriteDocumentContext(this);
return this;
}
@NotNull
@Override
public DocumentContext readingDocument(long readLocation) {
final long readPosition = bytes().readPosition();
final long readLimit = bytes().readLimit();
bytes().readPosition(readLocation);
initReadContext();
readContext.closeReadLimit(readLimit);
readContext.closeReadPosition(readPosition);
return readContext;
}
/**
* Creates and returns a new instance of TextValueIn.
* This method is primarily intended for internal use to provide consistent access to TextValueIn instances.
*
* @return A new instance of TextValueIn.
*/
@NotNull
protected TextValueIn createValueIn() {
return new TextValueIn();
}
/**
* Converts the underlying bytes of this TextWire to its string representation.
* For large byte sequences, only the initial part of the data is returned followed by "..".
*
* @return A string representation of the TextWire's underlying bytes.
*/
public String toString() {
if (bytes.readRemaining() > (1024 * 1024)) {
final long l = bytes.readLimit();
try {
bytes.readLimit(bytes.readPosition() + (1024 * 1024));
return bytes + "..";
} finally {
bytes.readLimit(l);
}
} else
return bytes.toString();
}
/**
* Converts the underlying bytes of this TextWire to its ISO-8859-1 string representation.
*
* @return A string representation of the TextWire's underlying bytes in ISO-8859-1 encoding.
*/
public String to8bitString() {
return bytes.to8bitString();
}
/**
* Converts the underlying bytes of this TextWire to its UTF-8 string representation.
*
* @return A string representation of the TextWire's underlying bytes in UTF-8 encoding.
*/
public String toUtf8String() {
return bytes.toUtf8String();
}
@Override
public void copyTo(@NotNull WireOut wire) throws InvalidMarshallableException {
if (wire instanceof TextWire || wire instanceof YamlWire) {
final Bytes> bytes0 = bytes();
final long length = bytes0.readRemaining();
wire.bytes().write(this.bytes, bytes0.readPosition(), length);
this.bytes.readSkip(length);
} else {
// TODO: implement copying
throw new UnsupportedOperationException("Not implemented yet. Can only copy TextWire format to the same format not " + wire.getClass());
}
}
@Override
public long readEventNumber() {
final StringBuilder stringBuilder = acquireStringBuilder();
readField(stringBuilder);
try {
return StringUtils.parseInt(stringBuilder, 10);
} catch (NumberFormatException ignored) {
return Long.MIN_VALUE;
}
}
@NotNull
@Override
public ValueIn read() {
readField(acquireStringBuilder());
return valueIn;
}
/**
* Reads the field from the current position of the wire and appends it to the given StringBuilder.
* The method handles different encodings and escape sequences, and ensures correct parsing of field data.
*
* @param sb The StringBuilder to which the field value should be appended.
* @return The updated StringBuilder containing the field value.
*/
@NotNull
protected StringBuilder readField(@NotNull StringBuilder sb) {
consumePadding();
try {
int ch = peekCode();
// 10xx xxxx, 1111 xxxx
if (ch > 0x80 && ((ch & 0xC0) == 0x80 || (ch & 0xF0) == 0xF0)) {
throw new IllegalStateException("Attempting to read binary as TextWire ch=" + Integer.toHexString(ch));
}
if (ch < 0 || ch == '!' || ch == '[' || ch == '{') {
sb.setLength(0);
return sb;
}
if (ch == '?') {
bytes.readSkip(1);
consumePadding();
ch = peekCode();
}
if (ch == '"') {
bytes.readSkip(1);
parseUntil(sb, getEscapingQuotes());
consumePadding();
ch = readCode();
if (ch != ':')
throw new UnsupportedOperationException("Expected a : at " + bytes.toDebugString() + " was " + (char) ch);
} else if (ch == '\'') {
bytes.readSkip(1);
parseUntil(sb, getEscapingSingleQuotes());
consumePadding();
ch = readCode();
if (ch != ':')
throw new UnsupportedOperationException("Expected a : at " + bytes.toDebugString() + " was " + (char) ch);
} else if (ch < 0) {
sb.setLength(0);
return sb;
} else {
parseUntil(sb, getEscapingEndOfText());
trimTheEnd(sb);
}
unescape(sb);
} catch (BufferUnderflowException e) {
Jvm.debug().on(getClass(), e);
}
return sb;
}
/**
* Trims trailing whitespace from the end of the given StringBuilder.
* This utility method ensures that field values are read without trailing spaces.
*
* @param sb The StringBuilder to be trimmed.
*/
private void trimTheEnd(@NotNull StringBuilder sb) {
while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1)))
sb.setLength(sb.length() - 1);
}
@Nullable
@Override
public K readEvent(@NotNull Class expectedClass) throws InvalidMarshallableException {
consumePadding(0);
@NotNull StringBuilder sb = acquireStringBuilder();
try {
int ch = peekCode();
// 10xx xxxx, 1111 xxxx
if (ch > 0x80 && ((ch & 0xC0) == 0x80 || (ch & 0xF0) == 0xF0)) {
throw new IllegalStateException("Attempting to read binary as TextWire ch=" + Integer.toHexString(ch));
} else if (ch == '?') {
bytes.readSkip(1);
consumePadding();
@Nullable final K object;
// if we don't know what type of key we are looking for, and it is not being defined with !
// then we force it to be String as otherwise valueIn.object gets confused and gives us back a Map
int ch3 = peekCode();
if (ch3 != '!' && expectedClass == Object.class) {
object = (K) valueIn.objectWithInferredType0(null, SerializationStrategies.ANY_SCALAR, defaultKeyClass());
} else {
object = valueIn.object(expectedClass);
}
consumePadding();
int ch2 = readCode();
if (ch2 != ':')
throw new IllegalStateException("Unexpected character after field " + ch + " '" + (char) ch2 + "'");
return object;
} else if (ch == '[') {
return valueIn.object(expectedClass);
} else if (ch == '"' || ch == '\'') {
bytes.readSkip(1);
final StopCharTester escapingQuotes = ch == '"' ? getEscapingQuotes() : getEscapingSingleQuotes();
parseUntil(sb, escapingQuotes);
consumePadding(1);
ch = readCode();
if (ch != ':')
throw new UnsupportedOperationException("Expected a : at " + bytes.toDebugString());
} else if (ch < 0) {
sb.setLength(0);
return null;
} else {
parseUntil(sb, getEscapingEndOfText());
}
unescape(sb);
} catch (BufferUnderflowException e) {
Jvm.debug().on(getClass(), e);
}
// consumePadding();
return toExpected(expectedClass, sb);
}
/**
* Returns the default class to be used as a key.
* By default, this method returns the Object class.
*
* @return The default key class, which is {@link Object}.
*/
protected Class> defaultKeyClass() {
return Object.class;
}
/**
* Converts the provided StringBuilder's content to an instance of the expected class.
* The content of the StringBuilder is interned before the conversion.
*
* @param expectedClass The class to which the StringBuilder's content should be converted.
* @param sb The StringBuilder containing the data to be converted.
* @return An instance of the expected class, converted from the StringBuilder's content.
*/
@Nullable
private K toExpected(Class expectedClass, StringBuilder sb) {
return ObjectUtils.convertTo(expectedClass, WireInternal.INTERNER.intern(sb));
}
/**
* Retrieves the StopCharTester that determines the end of text with escaping.
* The tester is fetched from a thread-local storage and then reset.
*
* @return The StopCharTester for determining the end of text with escaping.
*/
@NotNull
protected StopCharTester getEscapingEndOfText() {
StopCharTester escaping = ThreadLocalHelper.getTL(ESCAPED_END_OF_TEXT, END_OF_TEXT_ESCAPING);
// reset it.
escaping.isStopChar(' ');
return escaping;
}
/**
* Retrieves the StopCharsTester that determines the end of text with strict escaping.
* The tester is fetched from thread-local storage and is then reset.
*
* @return The StopCharsTester for determining the end of text with strict escaping.
*/
@NotNull
protected StopCharsTester getStrictEscapingEndOfText() {
StopCharsTester escaping = ThreadLocalHelper.getTL(STRICT_ESCAPED_END_OF_TEXT, STRICT_END_OF_TEXT_ESCAPING);
// reset it.
escaping.isStopChar(' ', ' ');
return escaping;
}
@NotNull
protected StopCharsTester getEscapingEndEventName() {
StopCharsTester escaping = ThreadLocalHelper.getTL(STRICT_ESCAPED_END_OF_TEXT, END_EVENT_NAME_ESCAPING);
// Reset the stop characters tester to stop at space characters.
escaping.isStopChar(' ', ' ');
return escaping;
}
/**
* Retrieves the StopCharTester that determines the end of a quoted text section with escaping.
* The tester is fetched from thread-local storage and is then reset.
*
* @return The StopCharTester for determining the end of a quoted text section with escaping.
*/
@Nullable
protected StopCharTester getEscapingQuotes() {
StopCharTester sct = ThreadLocalHelper.getTL(ESCAPED_QUOTES, QUOTES_ESCAPING);
// reset it.
sct.isStopChar(' ');
return sct;
}
@Override
public void consumePadding() {
consumePadding(0);
}
@Override
@NotNull
public String readingPeekYaml() {
return "todo";
}
// TODO Move to valueIn
/**
* Consumes padding characters from the current reading position.
* Padding characters include spaces, tabs, new lines, commas, and comments. This method also
* handles skipping over any comments encountered during this process.
*
* @param commas The number of comma characters to consume. Once this count is reached, the method will return.
*/
public void consumePadding(int commas) {
for (; ; ) {
int codePoint = peekCode();
switch (codePoint) {
case '#':
// Handle comment lines.
readCode();
while (peekCode() == ' ')
readCode();
try (ScopedResource stlSb = Wires.acquireStringBuilderScoped()) {
final StringBuilder sb = stlSb.get();
for (int ch; notNewLine(ch = readCode()); )
sb.append((char) ch);
if (!valueIn.consumeAny)
commentListener.accept(sb);
}
this.lineStart = bytes.readPosition();
break;
case ',':
// Handle commas.
if (valueIn.isASeparator(peekCodeNext()) && commas-- <= 0)
return;
bytes.readSkip(1);
if (commas == 0)
return;
break;
case ' ':
case '\t':
// Consume spaces and tabs.
bytes.readSkip(1);
break;
case '\n':
case '\r':
// Handle new lines.
this.lineStart = bytes.readPosition() + 1;
bytes.readSkip(1);
break;
default:
return;
}
}
}
/**
* Checks if the given character code is not a newline character.
*
* @param readCode The character code to be checked.
* @return True if the code is not a newline character and not end-of-file, otherwise false.
*/
private boolean notNewLine(int readCode) {
return readCode >= 0 && readCode != '\r' && readCode != '\n';
}
/**
* Consumes the start of a document in the byte stream. The start is determined by
* the presence of three consecutive '-' characters followed by certain words
* (e.g., "!!data", "!!meta-data").
*/
protected void consumeDocumentStart() {
// Check if there are at least 4 bytes remaining to read.
if (bytes.readRemaining() > 4) {
long pos = bytes.readPosition();
// Look for the sequence of three '-' characters.
if (bytes.readByte(pos) == '-' && bytes.readByte(pos + 1) == '-' && bytes.readByte(pos + 2) == '-') {
bytes.readSkip(3);
consumeWhiteSpace();
pos = bytes.readPosition();
// Parse the next word in the byte stream.
@NotNull String word = bytes.parseUtf8(StopCharTesters.SPACE_STOP);
// Check the word against known document start words.
switch (word) {
case "!!data":
case "!!data-not-ready":
case "!!meta-data":
case "!!meta-data-not-ready":
break;
default:
bytes.readPosition(pos);
}
}
}
}
/**
* Peeks the next unsigned byte from the current read position without advancing the pointer.
*
* @return The next unsigned byte as an integer.
*/
int peekCode() {
return bytes.peekUnsignedByte();
}
/**
* Peeks the unsigned byte after the current read position without advancing the pointer.
*
* @return The unsigned byte after the current read position as an integer.
*/
int peekCodeNext() {
return bytes.peekUnsignedByte(bytes.readPosition() + 1);
}
/**
* returns {@code true} if the next string is {@code str}
*
* @param source string
* @return true if the strings are the same
*/
protected boolean peekStringIgnoreCase(@NotNull final String source) {
if (source.isEmpty())
return true;
if (bytes.readRemaining() < 1)
return false;
long pos = bytes.readPosition();
try {
for (int i = 0; i < source.length(); i++) {
if (Character.toLowerCase(source.charAt(i)) != Character.toLowerCase(bytes.readByte()))
return false;
}
} finally {
bytes.readPosition(pos);
}
return true;
}
/**
* Reads the next byte as an unsigned integer.
*
* @return The next byte if available or -1 if end-of-file.
*/
protected int readCode() {
if (bytes.readRemaining() < 1)
return -1;
return bytes.readUnsignedByte();
}
@NotNull
@Override
public ValueIn read(@NotNull WireKey key) {
return read(key.name(), key.code(), key.defaultValue());
}
/**
* Reads the value associated with a given key name, code, and provides a default value.
*
* @param keyName The name of the key.
* @param keyCode The code for the key.
* @param defaultValue The default value to return if the key isn't found.
* @return The value associated with the given key or the default value if not found.
*/
private ValueIn read(@NotNull CharSequence keyName, int keyCode, Object defaultValue) {
consumePadding();
ValueInState curr = valueIn.curr();
final StringBuilder stringBuilder = acquireStringBuilder();
// did we save the position last time
// so we could go back and parseOne an older field?
if (curr.savedPosition() > 0) {
bytes.readPosition(curr.savedPosition() - 1);
curr.savedPosition(0L);
}
// Iterate while bytes remain.
while (bytes.readRemaining() > 0) {
long position = bytes.readPosition();
// at the current position look for the field.
valueIn.consumeAny = true;
readField(stringBuilder);
valueIn.consumeAny = false;
// might have changed due to readField in JSONWire
curr = valueIn.curr();
// If the field matches the required key, return its value.
if (StringUtils.equalsCaseIgnore(stringBuilder, keyName))
return valueIn;
if (stringBuilder.length() == 0) {
if (curr.unexpectedSize() > 0)
break;
return valueIn;
}
// if no old field nor current field matches, set to default values.
// we may come back and set the field later if we find it.
curr.addUnexpected(position);
long toSkip = valueIn.readLengthMarshallable();
bytes.readSkip(toSkip);
consumePadding(1);
}
// Continuation of the read operation (possibly handles edge cases or fallbacks).
return read2(keyName, keyCode, defaultValue, curr, stringBuilder, keyName);
}
/**
* Attempts to read the value of a given key, continuing the read operation from the primary `read` method.
* If the current and old fields do not match the specified key, the default value is returned.
*
* @param keyName The name of the key for which the value needs to be read.
* @param keyCode The code for the key.
* @param defaultValue The default value to return if the key isn't found.
* @param curr The current state of the ValueIn.
* @param sb The StringBuilder used to capture the field name.
* @param name The name of the key (same as keyName, possibly added for clarity in some cases).
* @return The value associated with the key or the default value if the key is not found.
*/
protected ValueIn read2(CharSequence keyName, int keyCode, Object defaultValue, @NotNull ValueInState curr, @NotNull StringBuilder sb, @NotNull CharSequence name) {
final long position2 = bytes.readPosition();
// if not a match go back and look at old fields.
for (int i = 0; i < curr.unexpectedSize(); i++) {
bytes.readPosition(curr.unexpected(i));
valueIn.consumeAny = true;
readField(sb);
valueIn.consumeAny = false;
if (sb.length() == 0 || StringUtils.equalsCaseIgnore(sb, name)) {
// if an old field matches, remove it, save the current position
curr.removeUnexpected(i);
curr.savedPosition(position2 + 1);
return valueIn;
}
}
bytes.readPosition(position2);
// If no matching field is found, return the default value.
if (defaultValueIn == null)
defaultValueIn = new DefaultValueIn(this);
defaultValueIn.defaultValue = defaultValue;
return defaultValueIn;
}
@NotNull
@Override
public ValueIn read(@NotNull StringBuilder name) {
consumePadding();
readField(name);
return valueIn;
}
@NotNull
@Override
public ValueIn getValueIn() {
return valueIn;
}
@NotNull
@Override
public Wire readComment(@NotNull StringBuilder s) {
consumeWhiteSpace();
if (peekCode() == '#') {
bytes.readSkip(1);
consumeWhiteSpace();
bytes.parseUtf8(s, StopCharTesters.CONTROL_STOP);
}
return this;
}
/**
* Consumes and skips over white space characters from the current position in the byte stream.
*/
public void consumeWhiteSpace() {
while (Character.isWhitespace(peekCode()))
bytes.readSkip(1);
}
@Override
public void clear() {
bytes.clear();
valueIn.resetState();
valueOut.resetState();
}
@NotNull
@Override
public LongValue newLongReference() {
return new TextLongReference();
}
@NotNull
@Override
public BooleanValue newBooleanReference() {
return new TextBooleanReference();
}
@Override
public boolean useSelfDescribingMessage(@NotNull CommonMarshallable object) {
return true;
}
@NotNull
@Override
public IntValue newIntReference() {
return new TextIntReference();
}
@NotNull
@Override
public LongArrayValues newLongArrayReference() {
return new TextLongArrayReference();
}
@Override
public @NotNull IntArrayValues newIntArrayReference() {
return new TextIntArrayReference();
}
/**
* Parses a word from the current byte position until it encounters a space or stop character.
* The parsed word is then appended to the provided StringBuilder.
*
* @param sb The StringBuilder to which the parsed word will be appended.
*/
public void parseWord(@NotNull StringBuilder sb) {
parseUntil(sb, StopCharTesters.SPACE_STOP);
}
/**
* Parses characters from the current byte position until one of the specified stop characters
* in the tester is encountered. The parsed characters are then appended to the provided StringBuilder.
*
* @param sb The StringBuilder to which the parsed characters will be appended.
* @param testers A StopCharTester which determines which characters should stop the parsing.
*/
public void parseUntil(@NotNull StringBuilder sb, @NotNull StopCharTester testers) {
if (use8bit)
bytes.parse8bit(sb, testers);
else
bytes.parseUtf8(sb, testers);
}
/**
* Clears the StringBuilder and then parses characters from the current byte position
* until one of the specified stop characters in the tester is encountered.
* The parsed characters are then appended to the provided StringBuilder.
*
* @param sb The StringBuilder to which the parsed characters will be appended.
* @param testers A StopCharsTester which determines which characters should stop the parsing.
*/
public void parseUntil(@NotNull StringBuilder sb, @NotNull StopCharsTester testers) {
sb.setLength(0);
if (use8bit) {
AppendableUtil.read8bitAndAppend(bytes, sb, testers);
} else {
AppendableUtil.readUTFAndAppend(bytes, sb, testers);
}
}
/**
* Reads and returns an object from the current position in the bytes stream.
* Any padding and document start metadata are consumed before attempting to read the object.
*
* @return An instance of the read object, or null if the end of the stream is reached.
* @throws InvalidMarshallableException If the object could not be properly unmarshalled.
*/
@Nullable
public Object readObject() throws InvalidMarshallableException {
consumePadding();
consumeDocumentStart();
return getValueIn().object(Object.class);
}
/**
* Attempts to read an object from the current position in the bytes stream based on its
* detected type (e.g., list, map, typed object). The type of the object is inferred from
* the current and next code. The method also considers the given indentation for nested structures.
*
* @param indentation The current indentation level to handle nested objects.
* @return An instance of the read object, `NoObject.NO_OBJECT` if the object denotes
* an end of block or structure, or null if the end of the stream is reached.
* @throws InvalidMarshallableException If the object could not be properly unmarshalled.
*/
@Nullable
Object readObject(int indentation) throws InvalidMarshallableException {
consumePadding();
int code = peekCode();
int indentation2 = indentation();
if (indentation2 < indentation)
return NoObject.NO_OBJECT;
switch (code) {
case '-':
if (peekCodeNext() == '-')
return NoObject.NO_OBJECT;
return readList(indentation2, null);
case '[':
return readList();
case '{':
return valueIn.marshallableAsMap(Object.class, Object.class);
case '!':
return readTypedObject();
default:
return readMap(indentation2, null);
}
}
/**
* Determines the indentation of the current line by calculating the difference
* between the current read position and the start of the line.
*
* @return The amount of indentation in terms of the number of characters from
* the start of the line to the current read position.
*/
private int indentation() {
long pos = bytes.readPosition();
if (pos < lineStart) {
lineStart = pos;
return 0;
}
return Maths.toInt32(pos - lineStart);
}
/**
* Reads a typed object from the current position in the bytes stream. The type
* of the object is determined by the bytes' content.
*
* @return An instance of the read object.
* @throws InvalidMarshallableException If the object could not be properly unmarshalled.
*/
@Nullable
private Object readTypedObject() throws InvalidMarshallableException {
return valueIn.object(Object.class);
}
/**
* Attempts to read a list from the current position in the bytes stream. This method
* currently throws an UnsupportedOperationException, indicating that reading lists
* directly is not supported in this context.
*
* @return This method does not currently return a value due to the exception.
* @throws UnsupportedOperationException Always thrown since this method is not supported.
*/
@NotNull
private List readList() {
throw new UnsupportedOperationException();
}
/**
* Reads a list of objects from the current position in the bytes stream.
* The method consumes lines starting with '-' as list elements, and utilizes
* the provided indentation to understand nested structures.
*
* @param indentation The current indentation level to handle nested list items.
* @param elementType The expected type of elements within the list. Used when inferring
* the type of the read object.
* @return A list containing objects read from the bytes stream.
* @throws InvalidMarshallableException If any object within the list could not be properly unmarshalled.
*/
@NotNull
List readList(int indentation, Class> elementType) throws InvalidMarshallableException {
@NotNull List