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

net.openhft.chronicle.wire.YamlWire 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.bytes.ref.*;
import net.openhft.chronicle.bytes.util.Compression;
import net.openhft.chronicle.core.Jvm;
import net.openhft.chronicle.core.io.IORuntimeException;
import net.openhft.chronicle.core.io.InvalidMarshallableException;
import net.openhft.chronicle.core.io.ValidatableUtil;
import net.openhft.chronicle.core.pool.ClassLookup;
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.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
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.time.temporal.TemporalAccessor;
import java.util.*;
import java.util.function.*;

import static java.nio.charset.StandardCharsets.ISO_8859_1;

/**
 * Represents a YAML-based wire format designed for efficient parsing and serialization of data.
 * The YamlWire class extends YamlWireOut and utilizes a custom tokenizer to convert YAML tokens into byte sequences.
 * It provides utility methods to read from and write to both byte buffers and files.
 */
@SuppressWarnings({"rawtypes", "unchecked", "this-escape"})
public class YamlWire extends YamlWireOut {

    // YAML-specific tag constants for representing special constructs.
    static final String SEQ_MAP = "!seqmap";
    static final String BINARY_TAG = "!binary";
    static final String DATA_TAG = "!data";
    static final String NULL_TAG = "!null";

    //for (char ch : "?%&*@`0123456789+- ',#:{}[]|>!\\".toCharArray())
    // Internal helper for reading text-based values.
    private final TextValueIn valueIn = createValueIn();

    // Custom tokenizer for parsing YAML tokens.
    private final YamlTokeniser yt;

    // Map to store reusable content anchors defined in the YAML.
    private final Map anchorValues = new HashMap<>();

    // Provides default values for reading.
    private DefaultValueIn defaultValueIn;

    // Context for writing out YAML documents.
    private WriteDocumentContext writeContext;

    // Context for reading in YAML documents.
    private ReadDocumentContext readContext;

    // Instance for re-reading or re-parsing scenarios.
    private YamlWire rereadWire;

    /**
     * Constructor that initializes the YamlWire with provided bytes and a flag indicating the use of 8-bit.
     *
     * @param bytes Bytes from which YamlWire is initialized
     * @param use8bit A boolean flag to indicate the use of 8-bit
     */
    public YamlWire(@NotNull Bytes bytes, boolean use8bit) {
        super(bytes, use8bit);
        yt = new YamlTokeniser(bytes);
        defaultValueIn = new DefaultValueIn(this);
    }

    /**
     * Constructor that initializes the YamlWire with provided bytes.
     * Defaults to not using 8-bit.
     *
     * @param bytes Bytes from which YamlWire is initialized
     */
    public YamlWire(@NotNull Bytes bytes) {
        this(bytes, false);
    }

    /**
     * Utility method to create a new YamlWire instance by reading bytes from a specified file.
     *
     * @param name Name of the file from which bytes are read
     * @return A new YamlWire instance initialized with bytes from the specified file
     * @throws IOException If there's an error in reading the file
     */
    @NotNull
    public static YamlWire fromFile(String name) throws IOException {
        return new YamlWire(BytesUtil.readFile(name), true);
    }

    /**
     * Utility method to create a new YamlWire instance from a given text string.
     *
     * @param text String from which the YamlWire instance is initialized
     * @return A new YamlWire instance initialized from the given string
     */
    @NotNull
    public static YamlWire from(@NotNull String text) {
        return new YamlWire(Bytes.from(text));
    }

    /**
     * Converts the content of a given {@link Wire} object into its string representation.
     *
     * @param wire The {@link Wire} object whose content needs to be converted to string.
     * @return The string representation of the wire's content.
     * @throws InvalidMarshallableException If the given wire's content cannot be marshalled.
     */
    public static String asText(@NotNull Wire wire) throws InvalidMarshallableException {
        long pos = wire.bytes().readPosition();
        @NotNull Wire tw = Wire.newYamlWireOnHeap();
        wire.copyTo(tw);
        wire.bytes().readPosition(pos);
        return tw.toString();
    }

    /**
     * Unescapes special characters in the provided Appendable based on the provided block quote character.
     * This method adheres to the YAML 1.2 specification for escaped characters
     * (see YAML Spec 1.2.2).
     *
     * @param sb The appendable containing characters to be unescaped.
     * @param blockQuote The block quote character that determines the escaping scheme (' or ").
     * @param  An appendable that also implements CharSequence interface.
     */
    private static  void unescape(@NotNull ACS sb, char blockQuote) {
        int end = 0;
        int length = sb.length();
        boolean skip = false;
        for (int i = 0; i < length; i++) {
            if (skip) {
                skip = false;
                continue;
            }

            char ch = sb.charAt(i);

            // Processing escaped characters for double quotes
            if (blockQuote == '\"' && ch == '\\' && i < length - 1) {
                char ch3 = sb.charAt(++i);
                switch (ch3) {
                    // Various cases for character unescaping based on YAML specification
                    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;

                    // For Unicode escapes
                    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;
                }
            }

            // Processing escaped characters for single quotes
            if (blockQuote == '\'' && ch == '\'' && i < length - 1) {
                char ch2 = sb.charAt(i + 1);
                if (ch2 == ch) {
                    skip = true;
                }
            }

            AppendableUtil.setCharAt(sb, end++, ch);
        }
        if (length != sb.length())
            throw new IllegalStateException("Length changed from " + length + " to " + sb.length() + " for " + sb);
        AppendableUtil.setLength(sb, end);
    }

    /**
     * Removes underscore ('_') characters from the given StringBuilder. This is useful for processing
     * YAML numbers which may use underscores as visual separators (e.g., 1_000_000 for one million).
     *
     * @param s The StringBuilder from which underscores should be removed.
     */
    static void removeUnderscore(@NotNull StringBuilder s) {
        int i = 0;
        for (int j = 0; j < s.length(); j++) {
            char ch = s.charAt(j);
            s.setCharAt(i, ch);
            if (ch != '_')
                i++;
        }
        s.setLength(i);
    }

    /**
     * Attempts to interpret the content of the given StringBuilder as a number (long, double) or
     * as a date/time, based on the YAML specification. If none of these interpretations is successful,
     * it returns the original string content.
     *
     * @param bq The block quote character (either ' or ") that initiated the string in YAML.
     * @param s The StringBuilder containing the string to be interpreted.
     * @return An Object which might be a Long, Double, Date, Time or the original String itself
     *         depending on successful interpretation.
     */
    @Nullable
    static Object readNumberOrTextFrom(char bq, final @Nullable StringBuilder s) {
        if (leaveUnparsed(bq, s))
            return s;

        StringBuilder sb = s;
        // YAML octal notation
        if (StringUtils.startsWith(s, "0o")) {
            sb = new StringBuilder(s);
            sb.deleteCharAt(1);
        }

        // Remove underscores if present, as they can be used in YAML as visual separators in numbers.
        if (s.indexOf("_") >= 0) {
            sb = new StringBuilder(s);
            removeUnderscore(sb);
        }

        String ss = sb.toString();

        // Attempt to parse as a long
        try {
            return Long.decode(ss);
        } catch (NumberFormatException fallback) {
            // Intentionally left blank to handle fallback
        }

        // Attempt to parse as a double
        try {
            return Double.parseDouble(ss);
        } catch (NumberFormatException fallback) {
            // Intentionally left blank to handle fallback
        }

        // Attempt to parse as a date or time
        try {
            return parseDateOrTime(s, ss);
        } catch (DateTimeParseException fallback) {
            // Intentionally left blank to handle fallback
        }

        // If none of the interpretations was successful, return the original string content
        return s;
    }

    /**
     * Determines if a given string representation should be left unparsed based on certain criteria.
     *
     * 

For instance, it checks: *

    *
  • If the string is null.
  • *
  • If a block quote character is present.
  • *
  • If the length of the string is out of a certain range.
  • *
  • If the first character of the string is not a number, decimal point, or a sign.
  • *
* * @param bq The block quote character (either ' or ") that initiated the string in YAML. * @param s The StringBuilder containing the string to check. * @return True if the string should be left unparsed, otherwise false. */ private static boolean leaveUnparsed(char bq, @Nullable StringBuilder s) { return s == null || bq != 0 || s.length() < 1 || s.length() > 40 || "0123456789.+-".indexOf(s.charAt(0)) < 0; } /** * Attempts to parse the provided string into a temporal accessor representing a date or time. * The method handles various formats for dates (LocalDate), times (LocalTime), and * date-times with timezone offsets (ZonedDateTime). * If none of these interpretations is successful, a DateTimeParseException is thrown. * * @param s The original StringBuilder containing the string to be parsed. * @param ss The string equivalent of the StringBuilder 's'. * @return A TemporalAccessor which could be a LocalTime, LocalDate or ZonedDateTime based on the format. * @throws DateTimeParseException If the string cannot be parsed into any known date or time format. */ private static TemporalAccessor parseDateOrTime(StringBuilder s, String ss) { if (s.length() == 7 && s.charAt(1) == ':') { return LocalTime.parse('0' + ss); } if (s.length() == 8 && s.charAt(2) == ':') { return LocalTime.parse(s); } if (s.length() == 10) { return LocalDate.parse(s); } if (s.length() >= 22) { return ZonedDateTime.parse(s); } throw new DateTimeParseException("Unable to parse date or time", s, 0); } @Override public boolean isBinary() { return false; } @Override public boolean hintReadInputOrder() { // TODO Fix YamlTextWireTest for false. return true; } @Override @NotNull public T methodWriter(@NotNull Class tClass, Class... additional) { VanillaMethodWriterBuilder builder = new VanillaMethodWriterBuilder<>(tClass, WireType.YAML, () -> newTextMethodWriterInvocationHandler(tClass)); for (Class aClass : additional) builder.addInterface(aClass); useTextDocuments(); builder.marshallableOut(this); return builder.build(); } /** * Creates a new instance of TextMethodWriterInvocationHandler based on the provided interfaces. * If any of the provided interfaces have a {@link Comment} annotation, a comment is written to the wire. * * @param interfaces The array of interfaces to be used with the TextMethodWriterInvocationHandler. * @return A new instance of TextMethodWriterInvocationHandler. */ @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 builder = new VanillaMethodWriterBuilder<>(tClass, WireType.YAML, () -> newTextMethodWriterInvocationHandler(tClass)); builder.marshallableOut(this); return builder; } @Override public @NotNull VanillaMethodReaderBuilder methodReaderBuilder() { return super.methodReaderBuilder().wireType(WireType.YAML); } @NotNull @Override public DocumentContext writingDocument(boolean metaData) { if (writeContext == null) useBinaryDocuments(); writeContext.start(metaData); return writeContext; } @Override public DocumentContext acquireWritingDocument(boolean metaData) { if (writeContext != null && writeContext.isOpen()) return writeContext; return writingDocument(metaData); } @NotNull @Override public DocumentContext readingDocument() { initReadContext(); return readContext; } /** * Initializes the reading context. This method ensures that the binary document context is used if * none is already set and prepares the YamlTokeniser for reading from the current position. */ protected void initReadContext() { if (readContext == null) useBinaryDocuments(); readContext.start(); yt.lineStart(bytes.readPosition()); } /** * Configures the YamlWire to use binary format for document reading and writing. * This involves setting up contexts that handle the binary format of documents. * * @return The current instance of YamlWire. */ @NotNull public YamlWire useBinaryDocuments() { readContext = new BinaryReadDocumentContext(this); writeContext = new BinaryWriteDocumentContext(this); return this; } /** * Configures the YamlWire to use text format for document reading and writing. * This involves setting up contexts that handle the text format of documents. * * @return The current instance of YamlWire. */ @NotNull public YamlWire 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 a new instance of {@link TextValueIn}. This method can be overridden * by subclasses to provide a custom implementation of TextValueIn. * * @return A new instance of TextValueIn. */ @NotNull protected TextValueIn createValueIn() { return new TextValueIn(); } /** * Converts the current YamlWire instance into a string representation. * If the bytes to be read exceed 1MB, a truncated version of the bytes * (limited to the first 1MB) is returned, followed by "..". * Otherwise, it returns the full string representation of the bytes. * * @return The string representation of the YamlWire instance. */ 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(); } @Override public void copyTo(@NotNull WireOut wire) throws InvalidMarshallableException { if (wire.getClass() == TextWire.class || wire.getClass() == YamlWire.class) { final Bytes bytes0 = bytes(); wire.bytes().write(this.bytes, yt.blockStart(), bytes0.readLimit() - yt.blockStart); this.bytes.readPosition(this.bytes.readLimit()); } else { try (DocumentContext dc = wire.writingDocument()) { while (!endOfDocument()) { copyOne(dc.wire(), true); yt.next(); } } } } /** * Copies a single element from the current YamlWire instance to the provided wire. * This is a recursive method that handles different YAML elements like mappings, * sequences, and primitive values based on the current token from the YamlTokeniser (yt). * * @param wire The target wire to copy to. * @param nested A flag indicating whether the current element is nested within another element. * @throws InvalidMarshallableException If there's a problem during the marshalling process. */ private void copyOne(WireOut wire, boolean nested) throws InvalidMarshallableException { ValueOut wireValueOut = wire.getValueOut(); switch (yt.current()) { case NONE: break; case COMMENT: wire.writeComment(yt.text()); break; case TAG: if (yt.current() == YamlToken.TAG && yt.isText(NULL_TAG)) { wireValueOut.nu11(); yt.next(); break; } ValueOut valueOut2 = wireValueOut.typePrefix(yt.text()); yt.next(); copyOne(wire, true); valueOut2.endTypePrefix(); break; case DIRECTIVE: break; case DOCUMENT_END: break; case DIRECTIVES_END: yt.next(); while (!endOfDocument()) { copyOne(wire, false); yt.next(); } break; case MAPPING_KEY: copyMappingKey(wire, nested); break; case MAPPING_END: return; case MAPPING_START: { if (nested) { yt.next(); wireValueOut.marshallable(w -> { while (yt.current() == YamlToken.MAPPING_KEY) { copyMappingKey(wire, true); yt.next(); } }); } break; } case SEQUENCE_END: break; case SEQUENCE_ENTRY: break; case SEQUENCE_START: { yt.next(); YamlWire yw = this; wireValueOut.sequence(w -> { while (yt.current() != YamlToken.SEQUENCE_END) { yw.copyOne(w.wireOut(), true); yw.yt.next(); } }); break; } case TEXT: Object o = valueIn.readNumberOrText(); if (o instanceof Long) wireValueOut.int64((long) o); else wireValueOut.object(o); break; case LITERAL: wireValueOut.text(yt.text()); break; case ANCHOR: break; case ALIAS: break; case RESERVED: break; case STREAM_END: break; case STREAM_START: break; } } /** * Determines if the current YAML structure has reached the end of its document. * It checks based on the current token from the YamlTokeniser. * * @return true if the current token represents the end of a document or the stream; false otherwise. */ private boolean endOfDocument() { // Check if there's nothing to read if (isEmpty()) return true; switch (yt.current()) { case STREAM_END: case STREAM_START: case DOCUMENT_END: case NONE: return true; default: return false; } } /** * Copies a mapping key from the current YamlWire instance to the provided wire. * This method advances through the tokens to handle nested keys and ensures * the key is correctly written to the target wire. * * @param wire The target wire to copy to. * @param nested A flag indicating whether the current element is nested within another element. * @throws InvalidMarshallableException If there's a problem during the marshalling process. */ private void copyMappingKey(WireOut wire, boolean nested) throws InvalidMarshallableException { // Move to the next token to identify the key structure yt.next(); // Skip consecutive MAPPING_KEY tokens, if any if (yt.current() == YamlToken.MAPPING_KEY) yt.next(); // Check if the current token is of TEXT type representing the key if (yt.current() == YamlToken.TEXT) { // Differentiate between nested and non-nested keys for writing to the wire if (nested) { wire.write(yt.text()); } else { wire.writeEvent(String.class, yt.text()); } // Move to the next token after copying the key yt.next(); } else { // Throw an exception if unable to determine the key structure throw new UnsupportedOperationException("Unable to copy key " + yt); } // Recursively handle the associated value for the key copyOne(wire, true); } @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()); switch (yt.current()) { case NONE: case MAPPING_END: return defaultValueIn; default: return valueIn; } } /** * Reads a field from the current YamlToken and appends its content to the provided StringBuilder. * * @param sb StringBuilder instance to which the field content will be appended. * @return The same StringBuilder instance with the appended content. */ @NotNull protected StringBuilder readField(@NotNull StringBuilder sb) { startEventIfTop(); // If the current token indicates a key in a map if (yt.current() == YamlToken.MAPPING_KEY) { yt.next(); // Ensure the key is textual if (yt.current() == YamlToken.TEXT) { String text = yt.text(); // Captures the key's text. Note: using sb here may modify its contents sb.setLength(0); // Reset the StringBuilder sb.append(text); // Append the key's text to the StringBuilder unescape(sb, yt.blockQuote()); // Handle any escape sequences within the key yt.next(); } else { throw new IllegalStateException(yt.toString()); } } else { sb.setLength(0); // Clear the StringBuilder if the current token isn't a MAPPING_KEY } return sb; } @SuppressWarnings("fallthrough") @Nullable @Override public K readEvent(@NotNull Class expectedClass) throws InvalidMarshallableException { startEventIfTop(); switch (yt.current()) { case MAPPING_START: yt.next(); assert yt.current() == YamlToken.MAPPING_KEY; // Deliberate fall-through case MAPPING_KEY: YamlToken next = yt.next(); if (next == YamlToken.MAPPING_KEY) { return readEvent(expectedClass); } K object = valueIn.object(expectedClass); if (object instanceof StringBuilder) return (K) object.toString(); return object; case NONE: return null; } throw new UnsupportedOperationException(yt.toString()); } @Override public boolean isNotEmptyAfterPadding() { consumePadding(); switch (yt.current()) { case MAPPING_END: case DOCUMENT_END: case SEQUENCE_END: case NONE: return false; default: return true; } } @Override @SuppressWarnings("fallthrough") public void consumePadding() { while (true) { switch (yt.current()) { case COMMENT: String text = yt.text(); commentListener.accept(text); // fall through case DIRECTIVE: case DIRECTIVES_END: yt.next(); break; default: return; } } } @Override @NotNull public String readingPeekYaml() { return "todo"; } @NotNull @Override public ValueIn read(@NotNull WireKey key) { return read(key.name().toString()); } @NotNull @Override public ValueIn read(String keyName) { startEventIfTop(); // check the keys we have already seen first. YamlKeys keys = yt.keys(); int count = keys.count(); if (count > 0) { long[] offsets = keys.offsets(); if (rereadWire == null) { initRereadWire(); } int indent = yt.topContext().indent; for (int i = 0; i < count; i++) { rereadWire.yt.topContext().indent = indent; long end = bytes.readPosition(); long offset = offsets[i]; rereadWire.bytes.readPositionRemaining(offset, end - offset); rereadWire.yt.rereadFrom(offset); YamlToken next = rereadWire.yt.next(); if (next == YamlToken.MAPPING_START) // indented rather than iin { } next = rereadWire.yt.next(); assert next == YamlToken.MAPPING_KEY : "next: " + next; if (rereadWire.checkForMatch(keyName)) { keys.removeIndex(i); return rereadWire.valueIn; } } // Next lines not covered by any tests } YamlTokeniser.YTContext yc = yt.topContext(); int minIndent = yc.indent; // go through remaining keys while (yt.current() == YamlToken.MAPPING_KEY) { long lastKeyPosition = yt.lineStart; if (checkForMatch(keyName)) return valueIn; if (!StringUtils.startsWith(sb, "-")) keys.push(lastKeyPosition); // Avoid consuming '}' but consume to next mapping key valueIn.consumeAny(minIndent >= 0 ? minIndent : Integer.MAX_VALUE); } return defaultValueIn; } /** * Initializes 'rereadWire', skipping any preliminary tokens to get to the main content. */ private void initRereadWire() { rereadWire = new YamlWire(bytes.bytesStore().bytesForRead()); YamlToken yamlToken; // Keep advancing until we pass headers/comments and reach the main content do { yamlToken = rereadWire.yt.next(); } while (yamlToken == YamlToken.STREAM_START || yamlToken == YamlToken.DIRECTIVES_END || yamlToken == YamlToken.COMMENT || yamlToken == YamlToken.MAPPING_START); } /** * Produces a dump of the current parsing context. Useful for debugging. * * @return A string representation of the current parsing context. */ public String dumpContext() { ValidatableUtil.startValidateDisabled(); try { Wire yw = Wire.newYamlWireOnHeap(); yw.getValueOut().list(yt.contexts, YamlTokeniser.YTContext.class); return yw.toString(); } finally { ValidatableUtil.endValidateDisabled(); // Ensure the validation check is restored afterward } } /** * Checks if the next token's text matches the given key name after handling any escape sequences. * * @param keyName The expected key name. * @return true if the next token's text matches the given key name; false otherwise. */ private boolean checkForMatch(@NotNull String keyName) { YamlToken next = yt.next(); // If the next token is textual if (next == YamlToken.TEXT) { sb.setLength(0); // Reset the StringBuilder sb.append(yt.text()); // Append the text of the next token to the StringBuilder unescape(sb, yt.blockQuote()); // Handle any escape sequences within the text yt.next(); } else { throw new IllegalStateException(next.toString()); } // Compare the processed string in the StringBuilder with the expected keyName return (sb.length() == 0 || StringUtils.isEqual(sb, keyName)); } @NotNull @Override public ValueIn read(@NotNull StringBuilder name) { startEventIfTop(); readField(name); return valueIn; } @NotNull @Override public ValueIn getValueIn() { return valueIn; } @NotNull @Override public Wire readComment(@NotNull StringBuilder s) { s.setLength(0); if (yt.current() == YamlToken.COMMENT) { s.append(yt.text()); yt.next(); } return this; } @Override public void clear() { reset(); } /** * Consumes and skips the start of a YAML document, e.g., '---'. * For specific keywords (like "!!data" and "!!meta-data"), the cursor position remains unchanged. */ protected void consumeDocumentStart() { // Check if there are more than 4 bytes left to read if (bytes.readRemaining() > 4) { long pos = bytes.readPosition(); // Check if the next three characters are '---' if (bytes.readByte(pos) == '-' && bytes.readByte(pos + 1) == '-' && bytes.readByte(pos + 2) == '-') bytes.readSkip(3); // Skip '---' pos = bytes.readPosition(); // Read a word until a space is encountered @NotNull String word = bytes.parseUtf8(StopCharTesters.SPACE_STOP); switch (word) { // If the word matches any of the special cases, do nothing case "!!data": case "!!data-not-ready": case "!!meta-data": case "!!meta-data-not-ready": break; default: bytes.readPosition(pos); // Reset the read position for other cases } } // Move to the next token if the current one is NONE if (yt.current() == YamlToken.NONE) yt.next(); } @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(); } /** * Reads a YAML map and deserializes it into a Java Map, converting values to the specified type. * * @param valueType The class type to which map values should be converted. * @return A Java Map representing the YAML map. */ @NotNull private Map readMap(Class valueType) { Map map = new LinkedHashMap(); if (yt.current() == YamlToken.MAPPING_START) { while (yt.next() == YamlToken.MAPPING_KEY) { if (yt.next() == YamlToken.TEXT) { String key = yt.text(); Object o; if (yt.next() == YamlToken.TEXT) { // Convert the text to the specified type o = ObjectUtils.convertTo(valueType, yt.text()); } else { throw new UnsupportedOperationException(yt.toString()); } map.put(key, o); } else { throw new UnsupportedOperationException(yt.toString()); } } } else { throw new UnsupportedOperationException(yt.toString()); } return map; } @Override public void startEvent() { consumePadding(); switch (yt.current()) { case MAPPING_START: yt.next(Integer.MAX_VALUE); return; case NONE: return; } throw new UnsupportedOperationException(yt.toString()); } /** * Starts a YAML event if at the top level. Consumes any padding. * If the context size is 3 and the current token is MAPPING_START, moves to the next token. */ void startEventIfTop() { consumePadding(); if (yt.contextSize() == 3) if (yt.current() == YamlToken.MAPPING_START) yt.next(); } @Override public boolean isEndEvent() { consumePadding(); YamlToken current = yt.current(); return current == YamlToken.MAPPING_END || current == YamlToken.NONE; } @Override public void endEvent() { YamlTokeniser.YTContext context = yt.topContext(); int minIndent = context.indent; switch (yt.current()) { case MAPPING_END: case DOCUMENT_END: case NONE: break; default: do { valueIn.consumeAny(minIndent); } while (yt.current() == YamlToken.COMMENT); break; } if (yt.current() == YamlToken.NONE) { yt.next(Integer.MIN_VALUE); } else { while (yt.current() == YamlToken.MAPPING_KEY) { valueIn.consumeAny(minIndent); } } if (yt.current() == YamlToken.MAPPING_END || yt.current() == YamlToken.DOCUMENT_END || yt.current() == YamlToken.NONE) { yt.next(Integer.MIN_VALUE); return; } throw new UnsupportedOperationException(yt.toString()); } /** * Resets the state of the YamlWire instance, clearing all buffers and contexts. */ public void reset() { // Reset reading and writing contexts if they exist if (readContext != null) readContext.reset(); if (writeContext != null) writeContext.reset(); // Clear the bytes buffer and internal StringBuilder bytes.clear(); sb.setLength(0); // Reset the YAML tokenizer and value states yt.reset(); valueIn.resetState(); valueOut.resetState(); // Clear the anchor values map anchorValues.clear(); } @Override public boolean hasMetaDataPrefix() { if (yt.current() == YamlToken.TAG && yt.isText("!meta-data")) { yt.next(); return true; } return false; } @Override public boolean readDocument(@Nullable ReadMarshallable metaDataConsumer, @Nullable ReadMarshallable dataConsumer) throws InvalidMarshallableException { valueIn.resetState(); return super.readDocument(metaDataConsumer, dataConsumer); } @Override public boolean readDocument(long position, @Nullable ReadMarshallable metaDataConsumer, @Nullable ReadMarshallable dataConsumer) throws InvalidMarshallableException { valueIn.resetState(); return super.readDocument(position, metaDataConsumer, dataConsumer); } /** * Implementation of the ValueIn interface for reading text-based values from YamlWire. */ class TextValueIn implements ValueIn { @Override public ClassLookup classLookup() { return YamlWire.this.classLookup(); } @Override public void resetState() { yt.reset(); anchorValues.clear(); } @Nullable @Override public String text() { @Nullable CharSequence cs = textTo0(acquireStringBuilder()); yt.next(); return cs == null ? null : WireInternal.INTERNER.intern(cs); } @Nullable @Override public StringBuilder textTo(@NotNull StringBuilder sb) { sb.setLength(0); @Nullable CharSequence cs = textTo0(sb); yt.next(); if (cs == null) return null; if (cs != sb) { sb.setLength(0); sb.append(cs); } return sb; } @Nullable @Override public Bytes textTo(@NotNull Bytes bytes) { bytes.clear(); if (yt.current() == YamlToken.TEXT) { bytes.append(yt.text()); yt.next(); } else if (yt.current() == YamlToken.TAG) { if (yt.isText(NULL_TAG)) { yt.next(); yt.next(); return null; } else if (yt.isText(BINARY_TAG)) { yt.next(); bytes.write((byte[]) decodeBinary(byte[].class)); } else { throw new UnsupportedOperationException(yt.toString()); } } else { throw new UnsupportedOperationException(yt.toString()); } return bytes; } @Override public E object(@Nullable E using, @Nullable Class clazz, boolean bestEffort) throws InvalidMarshallableException { YamlToken current = yt.current(); if (current == YamlToken.ALIAS) { String alias = yt.text(); Object o = anchorValues.get(alias); yt.next(); if (o == null) throw new IllegalStateException("Unknown alias " + alias + " with no corresponding anchor"); return (E) o; } else if (current == YamlToken.ANCHOR) { String alias = yt.text(); yt.next(); Object o = Wires.object0(this, using, clazz, bestEffort); // Overwriting of anchor values is permitted anchorValues.put(alias, o); return (E) o; } return Wires.object0(this, using, clazz, bestEffort); } @Override public BracketType getBracketType() { switch (yt.current()) { default: throw new UnsupportedOperationException(yt.toString()); case DIRECTIVES_END: case TAG: case COMMENT: yt.next(); return getBracketType(); case MAPPING_START: return BracketType.MAP; case SEQUENCE_START: return BracketType.SEQ; case NONE: case MAPPING_KEY: case SEQUENCE_ENTRY: case STREAM_START: case TEXT: case LITERAL: return BracketType.NONE; } } /** * Extracts the text from the current token and appends it to a StringBuilder. * Handles various YAML tokens like TEXT, LITERAL, and TAG. * @return StringBuilder containing the text. */ @Nullable StringBuilder textTo0(@NotNull StringBuilder a) { consumePadding(); // consume any padding // if the current token is a sequence entry, move to the next token if (yt.current() == YamlToken.SEQUENCE_ENTRY) yt.next(); switch (yt.current()) { // handle text or literal tokens case TEXT: case LITERAL: a.append(yt.text()); // append the text value // unescape the text value if needed if (yt.current() == YamlToken.TEXT) unescape(a, yt.blockQuote()); break; case ANCHOR: // Handle YAML anchors, which can be referred to later as aliases String alias = yt.text(); yt.next(); textTo0(sb); // Store the anchor for later reference anchorValues.put(alias, sb.toString()); break; case ALIAS: // Retrieve the actual object that an alias refers to alias = yt.text(); Object o = anchorValues.get(alias); if (o == null) throw new IllegalStateException("Unknown alias " + alias + " with no corresponding anchor"); yt.next(); sb.append(o); break; // handle tag tokens case TAG: // check for a NULL tag and move to the next token if (yt.isText(NULL_TAG)) { yt.next(); return null; } // check for a BINARY tag, decode and append its value if (yt.isText(BINARY_TAG)) { yt.next(); final byte[] arr = (byte[]) decodeBinary(byte[].class); for (byte b : arr) { a.append((char) b); } return a; } throw new UnsupportedOperationException(yt.toString()); } return a; } @NotNull @Override public WireIn bytesMatch(@NotNull BytesStore compareBytes, BooleanConsumer consumer) { throw new UnsupportedOperationException(yt.toString()); } @NotNull @Override public WireIn bytes(@NotNull BytesOut toBytes) { toBytes.clear(); return bytes(b -> toBytes.write((BytesStore) b)); } @Nullable @Override public WireIn bytesSet(@NotNull PointerBytesStore toBytes) { return bytes(bytes -> { long capacity = bytes.readRemaining(); Bytes bytes2 = Bytes.allocateDirect(capacity); bytes2.write((BytesStore) bytes); toBytes.set(bytes2.addressForRead(bytes2.start()), capacity); }); } @Override @NotNull public WireIn bytes(@NotNull ReadBytesMarshallable bytesConsumer) { consumePadding(); // TODO needs to be made much more efficient. @NotNull StringBuilder sb = acquireStringBuilder(); if (yt.current() == YamlToken.TAG) { bytes.readSkip(1); yt.text(sb); yt.next(); if (yt.current() != YamlToken.TEXT) throw new UnsupportedOperationException(yt.toString()); @Nullable byte[] uncompressed = Compression.uncompress(sb, yt, t -> { @NotNull StringBuilder sb2 = acquireStringBuilder(); t.text(sb2); return Base64.getDecoder().decode(sb2.toString()); }); if (uncompressed != null) { Bytes bytes = Bytes.wrapForRead(uncompressed); try { bytesConsumer.readMarshallable(bytes); } finally { bytes.releaseLast(); } } else if (StringUtils.isEqual(sb, NULL_TAG)) { bytesConsumer.readMarshallable(null); yt.next(); } else { throw new IORuntimeException("Unsupported type=" + sb); } } else { textTo(sb); Bytes bytes = Bytes.wrapForRead(sb.toString().getBytes(ISO_8859_1)); try { bytesConsumer.readMarshallable(bytes); } finally { bytes.releaseLast(); } } return YamlWire.this; } @Override public byte @Nullable [] bytes(byte[] using) { return (byte[]) objectWithInferredType(using, SerializationStrategies.ANY_OBJECT, byte[].class); } @NotNull @Override public WireIn wireIn() { return YamlWire.this; } @Override public long readLength() { return readLengthMarshallable(); } @NotNull @Override public WireIn skipValue() { consumeAny(yt.topContext().indent); return YamlWire.this; } /** * Reads the length of a marshallable. * @return The length of the marshallable. */ protected long readLengthMarshallable() { long start = bytes.readPosition(); try { consumeAny(yt.topContext().indent); return bytes.readPosition() - start; } finally { bytes.readPosition(start); } } /** * Consumes any token type based on its indentation level. * @param minIndent Minimum indentation level to consume. */ protected void consumeAny(int minIndent) { consumePadding(); int indent2 = Math.max(yt.topContext().indent, minIndent); switch (yt.current()) { case SEQUENCE_ENTRY: case TAG: yt.next(minIndent); consumeAny(minIndent); break; case MAPPING_START: consumeMap(indent2); break; case SEQUENCE_START: consumeSeq(indent2); break; case MAPPING_KEY: yt.next(minIndent); consumeAny(minIndent); if (yt.current() != YamlToken.MAPPING_KEY && yt.current() != YamlToken.MAPPING_END) consumeAny(minIndent); break; case SEQUENCE_END: yt.next(minIndent); break; case TEXT: yt.next(minIndent); break; case MAPPING_END: case STREAM_START: case DOCUMENT_END: case NONE: break; default: throw new UnsupportedOperationException(yt.toString()); } } /** * Consumes a sequence from the YAML token stream. * * @param minIndent The minimum indentation level to consider. */ private void consumeSeq(int minIndent) { assert yt.current() == YamlToken.SEQUENCE_START; yt.next(minIndent); while (true) { switch (yt.current()) { case SEQUENCE_ENTRY: yt.next(minIndent); consumeAny(minIndent); break; case SEQUENCE_END: yt.next(minIndent); return; default: throw new IllegalStateException(yt.toString()); } } } /** * Consumes a map from the YAML token stream. * * @param minIndent The minimum indentation level to consider. */ private void consumeMap(int minIndent) { yt.next(minIndent); // Move to the next token in the map. // While the current token signifies a key in the map: while (yt.current() == YamlToken.MAPPING_KEY) { yt.next(minIndent); // consume KEY consumeAny(minIndent); // consume the key consumeAny(minIndent); // consume the value } if (yt.current() == YamlToken.NONE) yt.next(Integer.MIN_VALUE); if (yt.current() == YamlToken.MAPPING_END) yt.next(minIndent); } @NotNull @Override public WireIn bool(T t, @NotNull ObjBooleanConsumer tFlag) { consumePadding(); final StringBuilder stringBuilder = acquireStringBuilder(); if (textTo(stringBuilder) == null) { tFlag.accept(t, null); return YamlWire.this; } Boolean flag = stringBuilder.length() == 0 ? null : StringUtils.isEqual(stringBuilder, "true"); tFlag.accept(t, flag); return YamlWire.this; } @NotNull @Override public WireIn int8(@NotNull T t, @NotNull ObjByteConsumer tb) { consumePadding(); tb.accept(t, (byte) getALong()); return YamlWire.this; } @NotNull @Override public WireIn uint8(@NotNull T t, @NotNull ObjShortConsumer ti) { consumePadding(); ti.accept(t, (short) getALong()); return YamlWire.this; } @NotNull @Override public WireIn int16(@NotNull T t, @NotNull ObjShortConsumer ti) { consumePadding(); ti.accept(t, (short) getALong()); return YamlWire.this; } @NotNull @Override public WireIn uint16(@NotNull T t, @NotNull ObjIntConsumer ti) { consumePadding(); ti.accept(t, (int) getALong()); return YamlWire.this; } @NotNull @Override public WireIn int32(@NotNull T t, @NotNull ObjIntConsumer ti) { consumePadding(); ti.accept(t, (int) getALong()); return YamlWire.this; } /** * Extracts a long value from the current YAML token. * * @return The parsed long value. * @throws UnsupportedOperationException If the current token is not of type TEXT. */ long getALong() { if (yt.current() == YamlToken.TEXT) { long l = yt.parseLong(); yt.next(); return l; } throw new UnsupportedOperationException(yt.toString()); } @NotNull @Override public WireIn uint32(@NotNull T t, @NotNull ObjLongConsumer tl) { consumePadding(); tl.accept(t, getALong()); return YamlWire.this; } @NotNull @Override public WireIn int64(@NotNull T t, @NotNull ObjLongConsumer tl) { consumePadding(); tl.accept(t, getALong()); return YamlWire.this; } @NotNull @Override public WireIn float32(@NotNull T t, @NotNull ObjFloatConsumer tf) { consumePadding(); tf.accept(t, (float) getADouble()); return YamlWire.this; } /** * Extracts a double value from the current YAML token. * * @return The parsed double value. * @throws UnsupportedOperationException If the current token is not of type TEXT. */ public double getADouble() { if (yt.current() == YamlToken.TEXT) { double v = yt.parseDouble(); yt.next(); return v; } else { throw new UnsupportedOperationException("yt:" + yt.current()); } } @NotNull @Override public WireIn float64(@NotNull T t, @NotNull ObjDoubleConsumer td) { consumePadding(); td.accept(t, getADouble()); return YamlWire.this; } @NotNull @Override public WireIn time(@NotNull T t, @NotNull BiConsumer setLocalTime) { consumePadding(); final StringBuilder stringBuilder = acquireStringBuilder(); textTo(stringBuilder); setLocalTime.accept(t, LocalTime.parse(WireInternal.INTERNER.intern(stringBuilder))); return YamlWire.this; } @NotNull @Override public WireIn zonedDateTime(@NotNull T t, @NotNull BiConsumer tZonedDateTime) { consumePadding(); final StringBuilder stringBuilder = acquireStringBuilder(); textTo(stringBuilder); tZonedDateTime.accept(t, ZonedDateTime.parse(WireInternal.INTERNER.intern(stringBuilder))); return YamlWire.this; } @NotNull @Override public WireIn date(@NotNull T t, @NotNull BiConsumer tLocalDate) { consumePadding(); final StringBuilder stringBuilder = acquireStringBuilder(); textTo(stringBuilder); tLocalDate.accept(t, LocalDate.parse(WireInternal.INTERNER.intern(stringBuilder))); return YamlWire.this; } @NotNull @Override public WireIn uuid(@NotNull T t, @NotNull BiConsumer tuuid) { consumePadding(); final StringBuilder stringBuilder = acquireStringBuilder(); textTo(stringBuilder); tuuid.accept(t, UUID.fromString(WireInternal.INTERNER.intern(stringBuilder))); return YamlWire.this; } @NotNull @Override public WireIn int64array(@Nullable LongArrayValues values, T t, @NotNull BiConsumer setter) { consumePadding(); if (!(values instanceof TextLongArrayReference)) { values = new TextLongArrayReference(); } @NotNull Byteable b = (Byteable) values; long length = TextLongArrayReference.peakLength(bytes, bytes.readPosition()); b.bytesStore(bytes, bytes.readPosition(), length); bytes.readSkip(length); setter.accept(t, values); return YamlWire.this; } @NotNull @Override public WireIn int64(@NotNull LongValue value) { throw new UnsupportedOperationException(yt.toString()); } @NotNull @Override public WireIn int32(@NotNull IntValue value) { throw new UnsupportedOperationException(yt.toString()); } @Override public WireIn bool(@NotNull final BooleanValue value) { throw new UnsupportedOperationException(yt.toString()); } @NotNull @Override public WireIn int64(@Nullable LongValue value, T t, @NotNull BiConsumer setter) { if (!(value instanceof TextLongReference)) { setter.accept(t, value = new TextLongReference()); } return int64(value); } @NotNull @Override public WireIn int32(@Nullable IntValue value, T t, @NotNull BiConsumer setter) { throw new UnsupportedOperationException(yt.toString()); } @Override public boolean sequence(@NotNull T t, @NotNull BiConsumer tReader) { consumePadding(); if (isNull()) { return false; } if (yt.current() == YamlToken.SEQUENCE_START) { int minIndent = yt.secondTopContext().indent; yt.next(Integer.MAX_VALUE); tReader.accept(t, YamlWire.this.valueIn); if (yt.current() == YamlToken.NONE) yt.next(minIndent); if (yt.current() == YamlToken.SEQUENCE_END) yt.next(minIndent); } else if (yt.current() == YamlToken.TEXT) { tReader.accept(t, YamlWire.this.valueIn); } else { throw new UnsupportedOperationException(yt.toString()); } return true; } /** * Reads a sequence of items from the YAML token stream and populates the provided list. * * @param list The list to populate with the sequence items. * @param buffer Temporary storage used during sequence processing. * @param bufferAdd Supplier function to add items to the buffer. * @param reader0 Reader to process the tokens. * @return true if the sequence was successfully read, false otherwise. * @throws InvalidMarshallableException If there's a problem with marshalling. */ public boolean sequence(List list, @NotNull List buffer, Supplier bufferAdd, Reader reader0) throws InvalidMarshallableException { // Delegate to the other `sequence` method without the reader. return sequence(list, buffer, bufferAdd); } @Override public boolean sequence(@NotNull List list, @NotNull List buffer, @NotNull Supplier bufferAdd) throws InvalidMarshallableException { consumePadding(); if (isNull()) { return false; } list.clear(); if (yt.current() == YamlToken.SEQUENCE_START) { int minIndent = yt.secondTopContext().indent; yt.next(Integer.MAX_VALUE); while (hasNextSequenceItem()) { if (buffer.size() <= list.size()) buffer.add(bufferAdd.get()); Object using = buffer.get(list.size()); list.add((T) valueIn.object(using, using.getClass())); } if (yt.current() == YamlToken.NONE) yt.next(minIndent); if (yt.current() == YamlToken.SEQUENCE_END) yt.next(minIndent); } else { throw new UnsupportedOperationException(yt.toString()); } return true; } @NotNull @Override public WireIn sequence(@NotNull T t, K kls, @NotNull TriConsumer tReader) throws InvalidMarshallableException { consumePadding(); assert yt.current() == YamlToken.SEQUENCE_START; yt.next(Integer.MIN_VALUE); while (true) { switch (yt.current()) { case TEXT: case SEQUENCE_ENTRY: tReader.accept(t, kls, YamlWire.this.valueIn); continue; case SEQUENCE_END: yt.next(Integer.MIN_VALUE); return YamlWire.this; default: throw new IllegalStateException(yt.toString()); } } } @Override public boolean hasNext() { if (yt.current() == YamlToken.DOCUMENT_END) yt.next(Integer.MIN_VALUE); consumePadding(); switch (yt.current()) { case SEQUENCE_END: case STREAM_END: case DOCUMENT_END: case MAPPING_END: case NONE: return false; default: return true; } } @Override public boolean hasNextSequenceItem() { consumePadding(); switch (yt.current()) { // Perhaps should be negative selection instead of positive case SEQUENCE_START: case SEQUENCE_ENTRY: // Allows scalar value to be converted into singleton array case TEXT: return true; } return false; } @Override public T applyToMarshallable(@NotNull Function marshallableReader) { throw new UnsupportedOperationException(yt.toString()); } @NotNull @Override public ValueIn typePrefix(T t, @NotNull BiConsumer ts) { consumePadding(); if (yt.current() == YamlToken.TAG) { ts.accept(t, yt.text()); yt.next(); } else { ts.accept(t, "java.lang.Object"); } return this; } @Override public Class typePrefix() { if (yt.current() != YamlToken.TAG) return null; final StringBuilder stringBuilder = acquireStringBuilder(); yt.text(stringBuilder); // Do not handle !!binary, do not resolve to BytesStore, do not consume tag if (BINARY_TAG.contentEquals(stringBuilder) || DATA_TAG.contains(stringBuilder)) return null; try { yt.next(); return classLookup().forName(stringBuilder); } catch (ClassNotFoundRuntimeException e) { Jvm.warn().on(getClass(), "Unable to find " + stringBuilder + " " + e); return null; } } @Override public Object typePrefixOrObject(Class tClass) { consumePadding(); switch (yt.current()) { case TAG: { Class type = typePrefix(); return type; } default: return null; /* case MAPPING_START: if (tClass == null || tClass == Object.class || tClass == Map.class) { return readMap(); } return marshallable(ObjectUtils.newInstance(tClass), SerializationStrategies.MARSHALLABLE); case SEQUENCE_START: if (tClass == null || tClass == Object.class || tClass == List.class) return readList(Object.class); if (tClass == Set.class) return readSet(tClass); break; case TEXT: return text(); */ } } @Override public boolean isTyped() { consumePadding(); int code = bytes.peekUnsignedByte(); return code == '!'; } @NotNull @Override public WireIn typeLiteralAsText(T t, @NotNull BiConsumer classNameConsumer) throws IORuntimeException, BufferUnderflowException { if (yt.current() != YamlToken.TAG) throw new UnsupportedOperationException(yt.toString()); if (!yt.isText("type")) throw new UnsupportedOperationException(yt.text()); if (yt.next() != YamlToken.TEXT) throw new UnsupportedOperationException(yt.toString()); StringBuilder stringBuilder = acquireStringBuilder(); textTo(stringBuilder); classNameConsumer.accept(t, stringBuilder); return YamlWire.this; } @Override public Type typeLiteral(BiFunction unresolvedHandler) { consumePadding(); if (yt.current() == YamlToken.TAG) { if (yt.text().equals("type")) { if (yt.next() == YamlToken.TEXT) { String text = yt.text(); yt.next(); try { return classLookup().forName(text); } catch (ClassNotFoundRuntimeException e) { return unresolvedHandler.apply(text, e.getCause()); } } } } throw new UnsupportedOperationException(yt.toString()); } @Nullable @Override public Object marshallable(@NotNull Object object, @NotNull SerializationStrategy strategy) throws BufferUnderflowException, IORuntimeException, InvalidMarshallableException { if (isNull()) { consumeAny(yt.topContext().indent); return null; } consumePadding(); if (yt.current() == YamlToken.SEQUENCE_ENTRY) { yt.next(); consumePadding(); } switch (yt.current()) { case TAG: Class clazz = typePrefix(); if (clazz != object.getClass()) object = ObjectUtils.newInstance(clazz); return marshallable(object, strategy); case SEQUENCE_START: Jvm.warn().on(getClass(), "Expected a {} but was blank for type " + object.getClass()); consumeAny(yt.secondTopContext().indent); return object; case MAPPING_START: wireIn().startEvent(); object = strategy.readUsing(null, object, this, BracketType.MAP); try { wireIn().endEvent(); } catch (UnsupportedOperationException uoe) { throw new IORuntimeException("Unterminated { while reading marshallable " + object + ",code='" + yt.current() + "', bytes=" + Bytes.toString(bytes, 1024) ); } return object; case TEXT: return ObjectUtils.convertTo(object.getClass(), text()); default: break; } throw new UnsupportedOperationException(yt.toString()); } /** * Creates an instance of the provided class and attempts to populate it from the YAML stream. * * @param clazz The class type to instantiate and populate. * @return The populated instance of the class. * @throws IORuntimeException If there's an error in the YAML format or during demarshalling. */ @NotNull public Demarshallable demarshallable(@NotNull Class clazz) { consumePadding(); switch (yt.current()) { case TAG: yt.next(); break; case MAPPING_START: break; default: throw new IORuntimeException("Unsupported type " + yt.current()); } final long len = readLengthMarshallable(); final long limit = bytes.readLimit(); final long position = bytes.readPosition(); final long newLimit = position - 1 + len; Demarshallable object; try { // Adjust read limits to ensure that we don't read past the current object in the stream. bytes.readLimit(newLimit); bytes.readSkip(1); // skip the opening curly brace '{' consumePadding(); object = Demarshallable.newInstance((Class) clazz, YamlWire.this); } finally { // Restore original read limits after processing. bytes.readLimit(limit); bytes.readPosition(newLimit); } consumePadding(); if (yt.current() != YamlToken.MAPPING_END) throw new IORuntimeException("Unterminated { while reading marshallable " + object + ",code='" + yt.current() + "', bytes=" + Bytes.toString(bytes, 1024) ); yt.next(); return object; } @Override @Nullable public T typedMarshallable() throws InvalidMarshallableException { return (T) objectWithInferredType(null, SerializationStrategies.ANY_NESTED, null); } /** * Reads a YAML map and populates a Java map with the given key and value types. * * @param kClass The class type of the keys. * @param vClass The class type of the values. * @param usingMap An optional pre-existing map to populate. * @return The populated map. * @throws InvalidMarshallableException If there's a problem with marshalling. * @throws IORuntimeException If there's an error in the YAML format or during parsing. */ @Nullable private Map map(@NotNull final Class kClass, @NotNull final Class vClass, @Nullable Map usingMap) throws InvalidMarshallableException { consumePadding(); // If a pre-existing map isn't provided, initialize one. Otherwise, clear it. if (usingMap == null) usingMap = new LinkedHashMap<>(); else usingMap.clear(); @NotNull StringBuilder sb = acquireStringBuilder(); switch (yt.current()) { case TAG: return typedMap(kClass, vClass, usingMap, sb); case MAPPING_START: return marshallableAsMap(kClass, vClass, usingMap); case SEQUENCE_START: return readAllAsMap(kClass, vClass, usingMap); default: throw new IORuntimeException("Unexpected code " + yt.current()); } } /** * Parses a typed map based on specific YAML tags. * * @param kClazz The class type for map keys. * @param vClass The class type for map values. * @param usingMap The map to populate based on the YAML data. * @param sb A StringBuilder instance used for temporary string operations. * @return Populated map from the YAML data or null. * @throws InvalidMarshallableException If there's a problem with marshalling. * @throws IORuntimeException If an unexpected YAML structure or tag is encountered. */ @Nullable private Map typedMap(@NotNull Class kClazz, @NotNull Class vClass, @NotNull Map usingMap, @NotNull StringBuilder sb) throws InvalidMarshallableException { // Read the next YAML token into the provided StringBuilder. yt.text(sb); yt.next(); if (NULL_TAG.contentEquals(sb)) { text(); return null; // Return null to indicate absence of a value. // If the current token indicates a sequence map... } else if (SEQ_MAP.contentEquals(sb)) { consumePadding(); // Verify that the next token is the start of a sequence. if (yt.current() != YamlToken.SEQUENCE_START) throw new IORuntimeException("Unsupported start of sequence : " + yt.current()); // Read and populate the provided map with key-value pairs from the YAML sequence. do { marshallable(r -> { @Nullable final K k = r.read(() -> "key") .object(kClazz); @Nullable final V v = r.read(() -> "value") .object(vClass); usingMap.put(k, v); }); } while (hasNextSequenceItem()); return usingMap; } else { // If the token isn't recognized, throw an exception. throw new IORuntimeException("Unsupported type :" + sb); } } @Override public boolean bool() { consumePadding(); final StringBuilder stringBuilder = acquireStringBuilder(); if (textTo(stringBuilder) == null) throw new NullPointerException("value is null"); if (ObjectUtils.isTrue(stringBuilder)) return true; if (ObjectUtils.isFalse(stringBuilder)) return false; Jvm.debug().on(getClass(), "Unable to parse '" + stringBuilder + "' as a boolean flag, assuming false"); return false; } @Override public byte int8() { long l = int64(); if (l > Byte.MAX_VALUE || l < Byte.MIN_VALUE) throw new IllegalStateException("value=" + l + ", is greater or less than Byte.MAX_VALUE/MIN_VALUE"); return (byte) l; } @Override public short int16() { long l = int64(); if (l > Short.MAX_VALUE || l < Short.MIN_VALUE) throw new IllegalStateException("value=" + l + ", is greater or less than Short.MAX_VALUE/MIN_VALUE"); return (short) l; } @Override public int int32() { long l = int64(); if (l > Integer.MAX_VALUE || l < Integer.MIN_VALUE) throw new IllegalStateException("value=" + l + ", is greater or less than Integer.MAX_VALUE/MIN_VALUE"); return (int) l; } @Override public int uint16() { long l = int64(); if (l > Integer.MAX_VALUE || l < 0) throw new IllegalStateException("value=" + l + ", is greater or less than Integer" + ".MAX_VALUE/ZERO"); return (int) l; } @Override public long int64() { consumePadding(); // Fix an issue in MethodReaderWithHistoryTest if (yt.current() == YamlToken.SEQUENCE_ENTRY) yt.next(); valueIn.skipType(); if (yt.current() != YamlToken.TEXT) { Jvm.warn().on(getClass(), "Unable to read " + valueIn.objectBestEffort() + " as a long."); return 0; } return getALong(); } @Override public double float64() { consumePadding(); // Fix an issue in MethodReaderWithHistoryTest if (yt.current() == YamlToken.SEQUENCE_ENTRY) yt.next(); valueIn.skipType(); if (yt.current() != YamlToken.TEXT) { Jvm.warn().on(getClass(), "Unable to read " + valueIn.objectBestEffort() + " as a double."); return 0; } return getADouble(); } /** * Skips over a YAML type declaration in the current stream. * If the current token indicates a YAML type (i.e., a TAG), the reading position is adjusted to skip over it. */ void skipType() { consumePadding(); if (yt.current() == YamlToken.TAG) { yt.next(); consumePadding(); } } @Override public float float32() { return (float) float64(); } /** * @return true if !!null "", if {@code true} reads the !!null "" up to the next STOP, if * {@code false} no data is read ( data is only peaked if {@code false} ) */ @Override public boolean isNull() { consumePadding(); if (yt.current() == YamlToken.TAG && yt.isText(NULL_TAG)) { consumeAny(0); return true; } return false; } @Override public Object objectWithInferredType(Object using, @NotNull SerializationStrategy strategy, Class type) throws InvalidMarshallableException { consumePadding(); if (yt.current() == YamlToken.SEQUENCE_ENTRY) yt.next(); @Nullable Object o = objectWithInferredType0(using, strategy, type); consumePadding(); return o; } /** * Reads an object from the YAML stream, inferring its type based on provided context and current token. * Depending on the encountered token, various internal methods are invoked to parse the object correctly. * If a YAML ANCHOR is encountered, it's mapped to the parsed object for potential future ALIAS references. * * @param using The object to potentially reuse when reading. Might be null. * @param strategy The serialization strategy to employ while reading the object. * @param type Expected type of the object to be read. If null, the method will attempt to infer the type. * @return The read object, possibly of the expected type. Might be null if the YAML token is NONE. * @throws InvalidMarshallableException if any error occurs while parsing or constructing the object. */ @SuppressWarnings("fallthrough") @Nullable Object objectWithInferredType0(Object using, @NotNull SerializationStrategy strategy, Class type) throws InvalidMarshallableException { boolean bestEffort = type != null; // Handle type declaration, if present if (yt.current() == YamlToken.TAG) { Class aClass = typePrefix(); if (type == null || type == Object.class || type.isInterface()) type = aClass; } switch (yt.current()) { case MAPPING_START: // Parse YAML mapping, considering the expected type if (type != null) { // Create new TreeMap if a sorted map is expected if (type == SortedMap.class && !(using instanceof SortedMap)) using = new TreeMap(); // Handle map-specific types if (type == Object.class || Map.class.isAssignableFrom(type) || using instanceof Map) return map(Object.class, Object.class, (Map) using); } return valueIn.object(using, type, bestEffort); case SEQUENCE_START: // Read a YAML sequence return readSequence(type); case TEXT: case LITERAL: // Parse text or numeric value Object o = valueIn.readNumberOrText(); yt.next(); if (o instanceof StringBuilder) o = o.toString(); if (type == Class.class) return classLookup.forName(o.toString()); return ObjectUtils.convertTo(type, o); case ANCHOR: // Handle YAML anchors, which can be referred to later as aliases String alias = yt.text(); yt.next(); o = valueIn.object(using, type); // Store the anchor for later reference anchorValues.put(alias, o); return o; case ALIAS: // Retrieve the actual object that an alias refers to alias = yt.text(); o = anchorValues.get(alias); if (o == null) throw new IllegalStateException("Unknown alias " + alias + " with no corresponding anchor"); yt.next(); return o; case NONE: // Handle cases with no data return null; case TAG: // Handle specific YAML tags final StringBuilder stringBuilder = acquireStringBuilder(); yt.text(stringBuilder); // Specific handling for binary data if (BINARY_TAG.contentEquals(stringBuilder)) { yt.next(); return decodeBinary(type); } // Intentional fall-through for other tags default: throw new UnsupportedOperationException("Cannot determine what to do with " + yt.current()); } } /** * Attempts to read either a number or a textual value from the YAML stream. * If the current token is a LITERAL, a StringBuilder containing the text will be returned. * Otherwise, the method tries to interpret the content as a number or textual data. * * @return An Object which might be a StringBuilder (for text) or a numeric representation. */ @Nullable protected Object readNumberOrText() { // Determine the kind of quote used for the YAML block char bq = yt.blockQuote(); // Extract the text content into a StringBuilder @Nullable StringBuilder s = textTo0(acquireStringBuilder()); if (yt.current() == YamlToken.LITERAL) return s; if (StringUtils.isEqual(s, "true")) return true; if (StringUtils.isEqual(s, "false")) return false; return readNumberOrTextFrom(bq, s); } /** * Reads a sequence (i.e., a list or array) from the YAML stream and interprets it * according to the provided class type. Depending on the class type, the method creates * the appropriate collection (TreeSet, LinkedHashSet, ArrayList, etc.). * * @param clazz The class type that determines how the sequence should be interpreted. * @return An object representing the read sequence, which might be a collection or an array. */ @NotNull private Object readSequence(Class clazz) { @NotNull Collection coll = clazz == SortedSet.class ? new TreeSet<>() : clazz == Set.class ? new LinkedHashSet<>() : new ArrayList<>(); @Nullable Class componentType = (clazz != null && clazz.isArray() && clazz.getComponentType().isPrimitive()) ? clazz.getComponentType() : null; // Read the YAML sequence into the determined collection type readCollection(componentType, coll); // If the expected type is an array, convert the collection into an array if (clazz != null && clazz.isArray()) { Object o = Array.newInstance(clazz.getComponentType(), coll.size()); if (clazz.getComponentType().isPrimitive()) { Iterator iter = coll.iterator(); for (int i = 0; i < coll.size(); i++) Array.set(o, i, iter.next()); return o; } return coll.toArray((Object[]) o); } return coll; } /** * Populates a provided collection with items from the current YAML sequence. * This method makes use of the 'sequence' method to iterate through the sequence * and extract each item, casting them to the specified class if provided. * * @param clazz The class type to which each item should be casted, can be null. * @param list The target collection to be populated. */ private void readCollection(@Nullable Class clazz, @NotNull Collection list) { sequence(list, (l, v) -> { while (v.hasNextSequenceItem()) { l.add(v.object(clazz)); } }); } /** * Decodes a Base64 encoded binary string from the YAML stream. * This method first reads the object from the YAML as a String, * then decodes the Base64 representation, and lastly tries to * convert the decoded byte array into the desired type. * * @param type The expected type of the resulting object after decoding. * @return The decoded object, potentially wrapped or converted according to the desired type. * @throws InvalidMarshallableException If there's an error during the deserialization. */ private Object decodeBinary(Class type) throws InvalidMarshallableException { Object o = objectWithInferredType(null, SerializationStrategies.ANY_SCALAR, String.class); byte[] decoded = Base64.getDecoder().decode(o == null ? "" : o.toString().replaceAll("\\s", "")); // Check if the desired type matches a BytesStore or is left unspecified if (type == null || BytesStore.class.isAssignableFrom(type)) return BytesStore.wrap(decoded); // Check if the desired type is a byte array if (type.isArray() && type.getComponentType().equals(Byte.TYPE)) return decoded; // Attempt to convert the byte array into other supported types, such as BitSet try { Method valueOf = type.getDeclaredMethod("valueOf", byte[].class); Jvm.setAccessible(valueOf); return valueOf.invoke(null, decoded); } catch (NoSuchMethodException e) { // ignored - method not found for conversion } catch (InvocationTargetException | IllegalAccessException e) { throw new IllegalStateException(e); } // If all conversion attempts failed, throw an exception throw new UnsupportedOperationException("Cannot determine how to deserialize " + type + " from binary data"); } @Override public String toString() { return YamlWire.this.toString(); } } @Override public boolean writingIsComplete() { return !writeContext.isNotComplete(); } @Override public void rollbackIfNotComplete() { writeContext.rollbackIfNotComplete(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy