You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/02/28 16:26:07 UTC
(juneau) branch master updated: YAML support
This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new c0fa710662 YAML support
c0fa710662 is described below
commit c0fa710662a6f936e65415713994436ec05a8605
Author: James Bognar <ja...@salesforce.com>
AuthorDate: Sat Feb 28 11:26:03 2026 -0500
YAML support
---
RELEASE-NOTES.txt | 32 +-
TODO.md | 8 +-
.../src/main/java/org/apache/juneau/MediaType.java | 2 +
.../java/org/apache/juneau/marshaller/Yaml.java | 227 +++
.../java/org/apache/juneau/yaml/YamlParser.java | 634 +++++++++
.../org/apache/juneau/yaml/YamlParserSession.java | 1456 ++++++++++++++++++++
.../org/apache/juneau/yaml/YamlSerializer.java | 907 ++++++++++++
.../apache/juneau/yaml/YamlSerializerSession.java | 453 ++++++
.../java/org/apache/juneau/yaml/YamlWriter.java | 409 ++++++
.../apache/juneau/yaml/annotation/YamlConfig.java | 78 ++
.../yaml/annotation/YamlConfigAnnotation.java | 72 +
.../juneau/yaml/annotation/package-info.java | 20 +
.../java/org/apache/juneau/yaml/package-info.java | 20 +
.../juneau/rest/config/BasicUniversalConfig.java | 7 +-
.../apache/juneau/rest/config/BasicYamlConfig.java | 74 +
.../org/apache/juneau/ComboRoundTripTest_Base.java | 66 +
.../org/apache/juneau/ComboRoundTrip_Tester.java | 12 +-
.../juneau/a/rttests/RoundTripDateTime_Test.java | 5 +
.../juneau/a/rttests/RoundTripTest_Base.java | 5 +
.../apache/juneau/yaml/YamlMarshaller_Test.java | 73 +
.../org/apache/juneau/yaml/YamlParser_Test.java | 144 ++
.../java/org/apache/juneau/yaml/Yaml_Test.java | 119 ++
22 files changed, 4801 insertions(+), 22 deletions(-)
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index c231a807f3..d8060c3256 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -26,7 +26,6 @@ Release Notes - Juneau - Version 9.2.1 - YYYY-MM-DD
* Bump Bump org.apache.maven.plugins:maven-compiler-plugin from 3.14.1 to 3.15.0 #314.
* Bump Bump jetty.version from 12.1.5 to 12.1.6 #312.
-<<<<<<< Updated upstream
** New Features - Direct Iterator/Iterable/Stream/Enumeration Serialization
* Iterators, Iterables (non-Collection), Enumerations, and java.util.stream.Streams
@@ -37,21 +36,30 @@ Release Notes - Juneau - Version 9.2.1 - YYYY-MM-DD
since the format requires knowing the size or inspecting elements upfront.
* New ClassMeta methods: isIterator(), isIterable(), isStream(), isStreamable().
* New ClassMeta.Category enum values: ITERATOR, ITERABLE, STREAM.
-=======
-** New Features - Direct Iterator/Iterable/Stream Serialization
-
- * Iterators, Iterables (non-Collection), Enumerations, and java.util.stream.Streams
- are now serialized directly as arrays without requiring intermediate List materialization.
- * For text-based serializers (JSON, XML, UON), elements are written lazily one at a
- time for improved memory efficiency with large datasets.
- * For binary/structured serializers (MsgPack, HTML, CSV), elements are collected to a
- List internally since the format requires knowing the size or inspecting elements upfront.
- * New ClassMeta methods: isIterator(), isIterable(), isStream(), isStreamable().
->>>>>>> Stashed changes
* New SerializerSession methods: forEachStreamableEntry(), toListFromStreamable().
* IteratorSwap and EnumerationSwap classes have been removed; Iterator, Enumeration, Iterable,
and Stream types are now handled natively by all serializers.
+** New Features - Built-in Date/Time and Duration Serialization
+
+ * Date, time, and duration types are now serialized and parsed natively as ISO 8601
+ strings by all serializers and parsers without requiring ObjectSwap registration.
+ * Supported types:
+ - All java.time.temporal.Temporal subtypes (Instant, ZonedDateTime, OffsetDateTime,
+ LocalDateTime, LocalDate, LocalTime, OffsetTime, Year, YearMonth)
+ - java.util.Calendar and java.util.Date
+ - javax.xml.datatype.XMLGregorianCalendar
+ - java.time.Duration (serialized as ISO 8601 duration strings, e.g. "PT1H30M")
+ * New ClassMeta.Category enum value: DURATION.
+ * New ClassMeta method: isDuration().
+ * New utility class: Iso8601Utils - centralizes ISO 8601 formatting and parsing.
+ * Default date/time swaps removed from DefaultSwaps; existing swap classes
+ (TemporalCalendarSwap, TemporalDateSwap, CalendarSwap, DateSwap, TemporalSwap)
+ remain available as override mechanisms for customizing output format.
+ * BeanSession.convertToMemberType() and SerializerSession.toString() updated to
+ use ISO 8601 formatting for date/time/Duration types.
+ * All parser sessions updated with date/time/Duration dispatch in parseAnything().
+
Release Notes - Juneau - Version 9.2.0 - 2025-12-30
** Changes
diff --git a/TODO.md b/TODO.md
index 4981320347..a3557e6056 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,18 +1,12 @@
# TODO
-- Find all places where we have system properties defined for juneau and convert them to use the new Settings class.
- Document "juneau.enableVerboseExceptions" setting.
- Change field names that start with underscore so that they end with underscore (e.g. "_enum"->"enum_")
- Find places where we defined fields and methods as _foobar and convert them to foobar_
- Investigate navlinks URL generation issue: Either "request:?Accept=text/json&plainText=true" should be supported, or "request:/?Accept=text/json&plainText=true" should not append '/' to the request URL. Currently, "request:/?Accept=..." generates URLs like "http://localhost:5000/rest/db/request:?Accept=..." which is incorrect.
-- Determine if it's possible to add a "short" field to @Schema for AI purposes.
-- JsonSchemaParser should be able to produce JsonSchema beans.
-- JsonSchemaGenerator should return JsonSchema beans.
-- Create full-fledged CSV serializer/parser support.
-- Add YAML serializer/parser support.
- Update REST server API to use new BeanStore2.
- ClassInfo should have a findGetter(String propertyName) convenience method.
- Make sure @Beanp("*") works on plain fields.
-- Add schema validation to beans during parsing.
+
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MediaType.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MediaType.java
index 076b7709e6..b311b7d836 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MediaType.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MediaType.java
@@ -66,6 +66,8 @@ public class MediaType implements Comparable<MediaType> {
public static final MediaType URLENCODING = of("application/x-www-form-urlencoded");
/** Reusable predefined media type for XML */
public static final MediaType XML = of("text/xml");
+ /** Reusable predefined media type for YAML */
+ public static final MediaType YAML = of("application/yaml");
/** Reusable predefined media type for SOAP XML */
public static final MediaType XMLSOAP = of("text/xml+soap");
/** Reusable predefined media type for RDF XML */
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Yaml.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Yaml.java
new file mode 100644
index 0000000000..4df7a24ebe
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Yaml.java
@@ -0,0 +1,227 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.juneau.marshaller;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.nio.charset.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.yaml.*;
+
+/**
+ * A pairing of a {@link YamlSerializer} and {@link YamlParser} into a single class with convenience read/write methods.
+ *
+ * <p>
+ * The general idea is to combine a single serializer and parser inside a simplified API for reading and writing POJOs.
+ *
+ * <h5 class='figure'>Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Using instance.</jc>
+ * Yaml <jv>yaml</jv> = <jk>new</jk> Yaml();
+ * MyPojo <jv>myPojo</jv> = <jv>yaml</jv>.read(<jv>string</jv>, MyPojo.<jk>class</jk>);
+ * String <jv>string</jv> = <jv>yaml</jv>.write(<jv>myPojo</jv>);
+ * </p>
+ * <p class='bjava'>
+ * <jc>// Using DEFAULT instance.</jc>
+ * MyPojo <jv>myPojo</jv> = Yaml.<jsf>DEFAULT</jsf>.read(<jv>string</jv>, MyPojo.<jk>class</jk>);
+ * String <jv>string</jv> = Yaml.<jsf>DEFAULT</jsf>.write(<jv>myPojo</jv>);
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Marshallers">Marshallers</a>
+ * </ul>
+ */
+public class Yaml extends CharMarshaller {
+
+ /**
+ * Default reusable instance.
+ */
+ public static final Yaml DEFAULT = new Yaml();
+
+ /**
+ * Serializes a Java object to a YAML string.
+ *
+ * <p>
+ * A shortcut for calling <c><jsf>DEFAULT</jsf>.write(<jv>object</jv>)</c>.
+ *
+ * @param object The object to serialize.
+ * @return
+ * The serialized object.
+ * @throws SerializeException If a problem occurred trying to convert the output.
+ */
+ public static String of(Object object) throws SerializeException {
+ return DEFAULT.write(object);
+ }
+
+ /**
+ * Serializes a Java object to a YAML output.
+ *
+ * <p>
+ * A shortcut for calling <c><jsf>DEFAULT</jsf>.write(<jv>output</jv>)</c>.
+ *
+ * @param object The object to serialize.
+ * @param output
+ * The output object.
+ * <br>Can be any of the following types:
+ * <ul>
+ * <li>{@link Writer}
+ * <li>{@link OutputStream} - Output will be written as UTF-8 encoded stream.
+ * <li>{@link File} - Output will be written as system-default encoded stream.
+ * <li>{@link StringBuilder} - Output will be written to the specified string builder.
+ * </ul>
+ * @return The output object.
+ * @throws SerializeException If a problem occurred trying to convert the output.
+ * @throws IOException Thrown by underlying stream.
+ */
+ public static Object of(Object object, Object output) throws SerializeException, IOException {
+ DEFAULT.write(object, output);
+ return output;
+ }
+
+ /**
+ * Parses a YAML input object to the specified Java type.
+ *
+ * <p>
+ * A shortcut for calling <c><jsf>DEFAULT</jsf>.read(<jv>input</jv>, <jv>type</jv>)</c>.
+ *
+ * @param <T> The class type of the object being created.
+ * @param input
+ * The input.
+ * <br>Can be any of the following types:
+ * <ul>
+ * <li><jk>null</jk>
+ * <li>{@link Reader}
+ * <li>{@link CharSequence}
+ * <li>{@link InputStream} containing UTF-8 encoded text (or charset defined by
+ * {@link org.apache.juneau.parser.ReaderParser.Builder#streamCharset(Charset)} property value).
+ * <li><code><jk>byte</jk>[]</code> containing UTF-8 encoded text (or charset defined by
+ * {@link org.apache.juneau.parser.ReaderParser.Builder#streamCharset(Charset)} property value).
+ * <li>{@link File} containing system encoded text (or charset defined by
+ * {@link org.apache.juneau.parser.ReaderParser.Builder#fileCharset(Charset)} property value).
+ * </ul>
+ * @param type The object type to create.
+ * @return The parsed object.
+ * @throws ParseException Malformed input encountered.
+ * @throws IOException Thrown by underlying stream.
+ */
+ public static <T> T to(Object input, Class<T> type) throws ParseException, IOException {
+ return DEFAULT.read(input, type);
+ }
+
+ /**
+ * Parses a YAML input object to the specified Java type.
+ *
+ * <p>
+ * A shortcut for calling <c><jsf>DEFAULT</jsf>.read(<jv>input</jv>, <jv>type</jv>, <jv>args</jv>)</c>.
+ *
+ * @param <T> The class type of the object to create.
+ * @param input
+ * The input.
+ * <br>Can be any of the following types:
+ * <ul>
+ * <li><jk>null</jk>
+ * <li>{@link Reader}
+ * <li>{@link CharSequence}
+ * <li>{@link InputStream} containing UTF-8 encoded text (or charset defined by
+ * {@link org.apache.juneau.parser.ReaderParser.Builder#streamCharset(Charset)} property value).
+ * <li><code><jk>byte</jk>[]</code> containing UTF-8 encoded text (or charset defined by
+ * {@link org.apache.juneau.parser.ReaderParser.Builder#streamCharset(Charset)} property value).
+ * <li>{@link File} containing system encoded text (or charset defined by
+ * {@link org.apache.juneau.parser.ReaderParser.Builder#fileCharset(Charset)} property value).
+ * </ul>
+ * @param type
+ * The object type to create.
+ * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
+ * @param args
+ * The type arguments of the class if it's a collection or map.
+ * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
+ * <br>Ignored if the main type is not a map or collection.
+ * @return The parsed object.
+ * @throws ParseException Malformed input encountered.
+ * @throws IOException Thrown by underlying stream.
+ * @see BeanSession#getClassMeta(Type,Type...) for argument syntax for maps and collections.
+ */
+ public static <T> T to(Object input, Type type, Type...args) throws ParseException, IOException {
+ return DEFAULT.read(input, type, args);
+ }
+
+ /**
+ * Parses a YAML input string to the specified type.
+ *
+ * <p>
+ * A shortcut for calling <c><jsf>DEFAULT</jsf>.read(<jv>input</jv>, <jv>type</jv>)</c>.
+ *
+ * @param <T> The class type of the object being created.
+ * @param input The input.
+ * @param type The object type to create.
+ * @return The parsed object.
+ * @throws ParseException Malformed input encountered.
+ */
+ public static <T> T to(String input, Class<T> type) throws ParseException {
+ return DEFAULT.read(input, type);
+ }
+
+ /**
+ * Parses a YAML input string to the specified Java type.
+ *
+ * <p>
+ * A shortcut for calling <c><jsf>DEFAULT</jsf>.read(<jv>input</jv>, <jv>type</jv>, <jv>args</jv>)</c>.
+ *
+ * @param <T> The class type of the object to create.
+ * @param input The input.
+ * @param type
+ * The object type to create.
+ * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
+ * @param args
+ * The type arguments of the class if it's a collection or map.
+ * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
+ * <br>Ignored if the main type is not a map or collection.
+ * @return The parsed object.
+ * @throws ParseException Malformed input encountered.
+ * @see BeanSession#getClassMeta(Type,Type...) for argument syntax for maps and collections.
+ */
+ public static <T> T to(String input, Type type, Type...args) throws ParseException {
+ return DEFAULT.read(input, type, args);
+ }
+
+ /**
+ * Constructor.
+ *
+ * <p>
+ * Uses {@link YamlSerializer#DEFAULT} and {@link YamlParser#DEFAULT}.
+ */
+ public Yaml() {
+ this(YamlSerializer.DEFAULT, YamlParser.DEFAULT);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param s
+ * The serializer to use for serializing output.
+ * <br>Must not be <jk>null</jk>.
+ * @param p
+ * The parser to use for parsing input.
+ * <br>Must not be <jk>null</jk>.
+ */
+ public Yaml(YamlSerializer s, YamlParser p) {
+ super(s, p);
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParser.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParser.java
new file mode 100644
index 0000000000..626d2552d8
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParser.java
@@ -0,0 +1,634 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.juneau.yaml;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import java.lang.annotation.*;
+import java.nio.charset.*;
+import java.util.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.apache.juneau.commons.collections.*;
+import org.apache.juneau.commons.function.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.parser.*;
+
+/**
+ * Parses YAML text into a POJO model.
+ *
+ * <h5 class='topic'>Media types</h5>
+ * <p>
+ * Handles <c>Content-Type</c> types: <bc>application/yaml, text/yaml</bc>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>This class is thread safe and reusable.
+ * </ul>
+ */
+@SuppressWarnings({
+ "java:S110", // Inheritance depth acceptable
+ "java:S115" // Constants use UPPER_snakeCase convention
+})
+public class YamlParser extends ReaderParser {
+
+ private static final String ARG_copyFrom = "copyFrom";
+
+ /**
+ * Builder class.
+ */
+ public static class Builder extends ReaderParser.Builder {
+
+ private static final Cache<HashKey,YamlParser> CACHE = Cache.of(HashKey.class, YamlParser.class).build();
+
+ /**
+ * Constructor, default settings.
+ */
+ protected Builder() {
+ consumes("application/yaml,text/yaml");
+ }
+
+ /**
+ * Copy constructor.
+ *
+ * @param copyFrom The builder to copy from.
+ * <br>Cannot be <jk>null</jk>.
+ */
+ protected Builder(Builder copyFrom) {
+ super(assertArgNotNull(ARG_copyFrom, copyFrom));
+ }
+
+ /**
+ * Copy constructor.
+ *
+ * @param copyFrom The bean to copy from.
+ * <br>Cannot be <jk>null</jk>.
+ */
+ protected Builder(YamlParser copyFrom) {
+ super(assertArgNotNull(ARG_copyFrom, copyFrom));
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder annotations(Annotation...values) {
+ super.annotations(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder apply(AnnotationWorkList work) {
+ super.apply(work);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder applyAnnotations(Class<?>...from) {
+ super.applyAnnotations(from);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder applyAnnotations(Object...from) {
+ super.applyAnnotations(from);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder autoCloseStreams() {
+ super.autoCloseStreams();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder autoCloseStreams(boolean value) {
+ super.autoCloseStreams(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanClassVisibility(Visibility value) {
+ super.beanClassVisibility(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanConstructorVisibility(Visibility value) {
+ super.beanConstructorVisibility(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanContext(BeanContext value) {
+ super.beanContext(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanContext(BeanContext.Builder value) {
+ super.beanContext(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanDictionary(java.lang.Class<?>...values) {
+ super.beanDictionary(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanFieldVisibility(Visibility value) {
+ super.beanFieldVisibility(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanInterceptor(Class<?> on, Class<? extends org.apache.juneau.swap.BeanInterceptor<?>> value) {
+ super.beanInterceptor(on, value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanMapPutReturnsOldValue() {
+ super.beanMapPutReturnsOldValue();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanMethodVisibility(Visibility value) {
+ super.beanMethodVisibility(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanProperties(Class<?> beanClass, String properties) {
+ super.beanProperties(beanClass, properties);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanProperties(Map<String,Object> values) {
+ super.beanProperties(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanProperties(String beanClassName, String properties) {
+ super.beanProperties(beanClassName, properties);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesExcludes(Class<?> beanClass, String properties) {
+ super.beanPropertiesExcludes(beanClass, properties);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesExcludes(Map<String,Object> values) {
+ super.beanPropertiesExcludes(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesExcludes(String beanClassName, String properties) {
+ super.beanPropertiesExcludes(beanClassName, properties);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesReadOnly(Class<?> beanClass, String properties) {
+ super.beanPropertiesReadOnly(beanClass, properties);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesReadOnly(Map<String,Object> values) {
+ super.beanPropertiesReadOnly(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesReadOnly(String beanClassName, String properties) {
+ super.beanPropertiesReadOnly(beanClassName, properties);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesWriteOnly(Class<?> beanClass, String properties) {
+ super.beanPropertiesWriteOnly(beanClass, properties);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesWriteOnly(Map<String,Object> values) {
+ super.beanPropertiesWriteOnly(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesWriteOnly(String beanClassName, String properties) {
+ super.beanPropertiesWriteOnly(beanClassName, properties);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beansRequireDefaultConstructor() {
+ super.beansRequireDefaultConstructor();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beansRequireSerializable() {
+ super.beansRequireSerializable();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beansRequireSettersForGetters() {
+ super.beansRequireSettersForGetters();
+ return this;
+ }
+
+ @Override /* Overridden from Context.Builder */
+ public YamlParser build() {
+ return cache(CACHE).build(YamlParser.class);
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder cache(Cache<HashKey,? extends org.apache.juneau.Context> value) {
+ super.cache(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder consumes(String value) {
+ super.consumes(value);
+ return this;
+ }
+
+ @Override /* Overridden from Context.Builder */
+ public Builder copy() {
+ return new Builder(this);
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder debug() {
+ super.debug();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder debug(boolean value) {
+ super.debug(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder debugOutputLines(int value) {
+ super.debugOutputLines(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder dictionaryOn(Class<?> on, java.lang.Class<?>...values) {
+ super.dictionaryOn(on, values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder disableBeansRequireSomeProperties() {
+ super.disableBeansRequireSomeProperties();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder disableIgnoreMissingSetters() {
+ super.disableIgnoreMissingSetters();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder disableIgnoreTransientFields() {
+ super.disableIgnoreTransientFields();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder disableIgnoreUnknownNullBeanProperties() {
+ super.disableIgnoreUnknownNullBeanProperties();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder disableInterfaceProxies() {
+ super.disableInterfaceProxies();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public <T> Builder example(Class<T> pojoClass, String json) {
+ super.example(pojoClass, json);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public <T> Builder example(Class<T> pojoClass, T o) {
+ super.example(pojoClass, o);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder fileCharset(Charset value) {
+ super.fileCharset(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder findFluentSetters() {
+ super.findFluentSetters();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder findFluentSetters(Class<?> on) {
+ super.findFluentSetters(on);
+ return this;
+ }
+
+ @Override /* Overridden from Context.Builder */
+ public HashKey hashKey() {
+ return HashKey.of(super.hashKey());
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder ignoreInvocationExceptionsOnGetters() {
+ super.ignoreInvocationExceptionsOnGetters();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder ignoreInvocationExceptionsOnSetters() {
+ super.ignoreInvocationExceptionsOnSetters();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder ignoreUnknownBeanProperties() {
+ super.ignoreUnknownBeanProperties();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder ignoreUnknownEnumValues() {
+ super.ignoreUnknownEnumValues();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder impl(Context value) {
+ super.impl(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder implClass(Class<?> interfaceClass, Class<?> implClass) {
+ super.implClass(interfaceClass, implClass);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder implClasses(Map<Class<?>,Class<?>> values) {
+ super.implClasses(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder interfaceClass(Class<?> on, Class<?> value) {
+ super.interfaceClass(on, value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder interfaces(java.lang.Class<?>...value) {
+ super.interfaces(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder listener(Class<? extends org.apache.juneau.parser.ParserListener> value) {
+ super.listener(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder locale(Locale value) {
+ super.locale(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder mediaType(MediaType value) {
+ super.mediaType(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder notBeanClasses(java.lang.Class<?>...values) {
+ super.notBeanClasses(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder notBeanPackages(String...values) {
+ super.notBeanPackages(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder propertyNamer(Class<?> on, Class<? extends org.apache.juneau.PropertyNamer> value) {
+ super.propertyNamer(on, value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder propertyNamer(Class<? extends org.apache.juneau.PropertyNamer> value) {
+ super.propertyNamer(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder sortProperties() {
+ super.sortProperties();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder sortProperties(java.lang.Class<?>...on) {
+ super.sortProperties(on);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder stopClass(Class<?> on, Class<?> value) {
+ super.stopClass(on, value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder streamCharset(Charset value) {
+ super.streamCharset(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder strict() {
+ super.strict();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder strict(boolean value) {
+ super.strict(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public <T,S> Builder swap(Class<T> normalClass, Class<S> swappedClass, ThrowingFunction<T,S> swapFunction) {
+ super.swap(normalClass, swappedClass, swapFunction);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public <T,S> Builder swap(Class<T> normalClass, Class<S> swappedClass, ThrowingFunction<T,S> swapFunction, ThrowingFunction<S,T> unswapFunction) {
+ super.swap(normalClass, swappedClass, swapFunction, unswapFunction);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder swaps(Class<?>...values) {
+ super.swaps(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder swaps(Object...values) {
+ super.swaps(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder timeZone(TimeZone value) {
+ super.timeZone(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder trimStrings() {
+ super.trimStrings();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder trimStrings(boolean value) {
+ super.trimStrings(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder type(Class<? extends org.apache.juneau.Context> value) {
+ super.type(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder typeName(Class<?> on, String value) {
+ super.typeName(on, value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder typePropertyName(Class<?> on, String value) {
+ super.typePropertyName(on, value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder typePropertyName(String value) {
+ super.typePropertyName(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder unbuffered() {
+ super.unbuffered();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder unbuffered(boolean value) {
+ super.unbuffered(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder useEnumNames() {
+ super.useEnumNames();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder useJavaBeanIntrospector() {
+ super.useJavaBeanIntrospector();
+ return this;
+ }
+ }
+
+ /** Default parser, all default settings.*/
+ public static final YamlParser DEFAULT = new YamlParser(create());
+
+ /**
+ * Creates a new builder for this object.
+ *
+ * @return A new builder.
+ */
+ public static Builder create() {
+ return new Builder();
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param builder The builder for this object.
+ */
+ public YamlParser(Builder builder) {
+ super(builder);
+ }
+
+ @Override /* Overridden from Context */
+ public Builder copy() {
+ return new Builder(this);
+ }
+
+ @Override /* Overridden from Context */
+ public YamlParserSession.Builder createSession() {
+ return YamlParserSession.create(this);
+ }
+
+ @Override /* Overridden from Context */
+ public YamlParserSession getSession() { return createSession().build(); }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParserSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParserSession.java
new file mode 100644
index 0000000000..10633d0133
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParserSession.java
@@ -0,0 +1,1456 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.juneau.yaml;
+
+import static org.apache.juneau.commons.lang.StateEnum.*;
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.commons.utils.StringUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.nio.charset.*;
+import java.util.*;
+import java.util.function.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.apache.juneau.commons.lang.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.commons.utils.*;
+import org.apache.juneau.httppart.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.swap.*;
+import org.apache.juneau.utils.Iso8601Utils;
+
+/**
+ * Session object that lives for the duration of a single use of {@link YamlParser}.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='warn'>This class is not thread safe and is typically discarded after one use.
+ * </ul>
+ */
+@SuppressWarnings({
+ "java:S125", // State-machine and parse-path comments are documentation, not commented-out code
+ "unchecked",
+ "rawtypes",
+ "resource"
+})
+public class YamlParserSession extends ReaderParserSession {
+
+ /**
+ * Builder class.
+ */
+ public static class Builder extends ReaderParserSession.Builder {
+
+ private YamlParser ctx;
+
+ /**
+ * Constructor
+ *
+ * @param ctx The context creating this session.
+ */
+ protected Builder(YamlParser ctx) {
+ super(ctx);
+ this.ctx = ctx;
+ }
+
+ @Override /* Overridden from Builder */
+ public <T> Builder apply(Class<T> type, Consumer<T> apply) {
+ super.apply(type, apply);
+ return this;
+ }
+
+ @Override
+ public YamlParserSession build() {
+ return new YamlParserSession(this);
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder debug(Boolean value) {
+ super.debug(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder fileCharset(Charset value) {
+ super.fileCharset(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder javaMethod(Method value) {
+ super.javaMethod(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder locale(Locale value) {
+ super.locale(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder mediaType(MediaType value) {
+ super.mediaType(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder mediaTypeDefault(MediaType value) {
+ super.mediaTypeDefault(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder outer(Object value) {
+ super.outer(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder properties(Map<String,Object> value) {
+ super.properties(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder property(String key, Object value) {
+ super.property(key, value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder schema(HttpPartSchema value) {
+ super.schema(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder schemaDefault(HttpPartSchema value) {
+ super.schemaDefault(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder streamCharset(Charset value) {
+ super.streamCharset(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder timeZone(TimeZone value) {
+ super.timeZone(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder timeZoneDefault(TimeZone value) {
+ super.timeZoneDefault(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder unmodifiable() {
+ super.unmodifiable();
+ return this;
+ }
+ }
+
+ /**
+ * Creates a new builder for this object.
+ *
+ * @param ctx The context creating this session.
+ * @return A new builder.
+ */
+ public static Builder create(YamlParser ctx) {
+ return new Builder(ctx);
+ }
+
+ private final YamlParser ctx;
+
+ /**
+ * Constructor.
+ *
+ * @param builder The builder for this object.
+ */
+ protected YamlParserSession(Builder builder) {
+ super(builder);
+ ctx = builder.ctx;
+ }
+
+ @Override /* Overridden from ParserSession */
+ protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
+ try (var r = pipe.getParserReader()) {
+ if (r == null)
+ return null;
+ return parseAnything(type, r, getOuter(), null);
+ }
+ }
+
+ @Override /* Overridden from ReaderParserSession */
+ protected <E> Collection<E> doParseIntoCollection(ParserPipe pipe, Collection<E> c, Type elementType) throws IOException, ParseException, ExecutableException {
+ try (var r = pipe.getParserReader()) {
+ skipWhitespaceAndComments(r);
+ int ch = r.peek();
+ if (ch == '[') {
+ parseFlowSequence(r, c, getClassMeta(elementType), null);
+ } else {
+ parseBlockSequence(r, c, getClassMeta(elementType), null, 0);
+ }
+ return c;
+ }
+ }
+
+ @Override /* Overridden from ReaderParserSession */
+ protected <K,V> Map<K,V> doParseIntoMap(ParserPipe pipe, Map<K,V> m, Type keyType, Type valueType) throws IOException, ParseException, ExecutableException {
+ try (var r = pipe.getParserReader()) {
+ skipWhitespaceAndComments(r);
+ int ch = r.peek();
+ if (ch == '{') {
+ parseFlowMapping(r, m, (ClassMeta<K>)getClassMeta(keyType), (ClassMeta<V>)getClassMeta(valueType), null);
+ } else {
+ parseBlockMapping(r, m, (ClassMeta<K>)getClassMeta(keyType), (ClassMeta<V>)getClassMeta(valueType), null, 0);
+ }
+ return m;
+ }
+ }
+
+ @SuppressWarnings({
+ "java:S3776" // Cognitive complexity acceptable for parser dispatch
+ })
+ private <T> T parseAnything(ClassMeta<?> eType, ParserReader r, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
+
+ if (eType == null)
+ eType = object();
+ var swap = (ObjectSwap<T,Object>)eType.getSwap(this);
+ var builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this);
+ ClassMeta<?> sType = null;
+ if (nn(builder))
+ sType = builder.getBuilderClassMeta(this);
+ else if (nn(swap))
+ sType = swap.getSwapClassMeta(this);
+ else
+ sType = eType;
+
+ if (sType.isOptional())
+ return (T)opt(parseAnything(eType.getElementType(), r, outer, pMeta));
+
+ setCurrentClass(sType);
+
+ Object o = null;
+
+ skipWhitespaceAndComments(r);
+ skipDocumentMarker(r);
+ skipWhitespaceAndComments(r);
+
+ int contentColumn = r.getPosition().getColumn();
+ int c = r.peek();
+ if (c == -1) {
+ // Let o be null.
+ } else if (c == '{') {
+ if (sType.isObject()) {
+ var m2 = new JsonMap(this);
+ parseFlowMapping(r, m2, string(), object(), pMeta);
+ o = cast(m2, pMeta, eType);
+ } else if (sType.isMap()) {
+ Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType));
+ o = parseFlowMapping(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
+ } else if (sType.isCollection()) {
+ var m = new JsonMap(this);
+ parseFlowMapping(r, m, string(), object(), pMeta);
+ o = cast(m, pMeta, eType);
+ } else if (nn(builder)) {
+ var m = toBeanMap(builder.create(this, eType));
+ o = builder.build(this, parseIntoBeanMap(r, m).getBean(), eType);
+ } else if (sType.canCreateNewBean(outer)) {
+ var m = newBeanMap(outer, sType.inner());
+ o = parseIntoBeanMap(r, m).getBean();
+ } else if (sType.isArray() || sType.isArgs()) {
+ var m = new JsonMap(this);
+ parseFlowMapping(r, m, string(), object(), pMeta);
+ o = cast(m, pMeta, eType);
+ } else {
+ Map m = new JsonMap(this);
+ parseFlowMapping(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
+ if (m.containsKey(getBeanTypePropertyName(eType)))
+ o = cast((JsonMap)m, pMeta, eType);
+ else if (nn(sType.getProxyInvocationHandler()))
+ o = newBeanMap(outer, sType.inner()).load(m).getBean();
+ else
+ throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", cn(sType), sType.getNotABeanReason());
+ }
+ } else if (c == '[') {
+ if (sType.isObject()) {
+ o = parseFlowSequence(r, new JsonList(this), object(), pMeta);
+ } else if (sType.isCollection()) {
+ Collection l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance() : new JsonList(this));
+ o = parseFlowSequence(r, l, sType, pMeta);
+ } else if (sType.isArray() || sType.isArgs()) {
+ var l = (ArrayList)parseFlowSequence(r, list(), sType, pMeta);
+ o = toArray(sType, l);
+ } else {
+ throw new ParseException(this, "Unrecognized syntax for class type ''{0}'', starting character ''{1}''", sType, (char)c);
+ }
+ } else if (c == '\'') {
+ String s = parseSingleQuotedString(r);
+ o = handleQuotedScalar(s, r, sType, eType, builder, outer, pMeta, contentColumn > 0 ? contentColumn - 1 : 0);
+ } else if (c == '"') {
+ String s = parseDoubleQuotedString(r);
+ o = handleQuotedScalar(s, r, sType, eType, builder, outer, pMeta, contentColumn > 0 ? contentColumn - 1 : 0);
+ } else if (c == '-') {
+ int c2 = peekSecondChar(r);
+ if (c2 == ' ' || c2 == '\n' || c2 == '\r') {
+ if (sType.isObject()) {
+ o = parseBlockSequence(r, new JsonList(this), object(), pMeta, 0);
+ } else if (sType.isCollection()) {
+ Collection l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance() : new JsonList(this));
+ o = parseBlockSequence(r, l, sType, pMeta, 0);
+ } else if (sType.isArray() || sType.isArgs()) {
+ var l = (ArrayList)parseBlockSequence(r, list(), sType, pMeta, 0);
+ o = toArray(sType, l);
+ } else {
+ throw new ParseException(this, "Unrecognized syntax for class type ''{0}'', starting character ''{1}''", sType, (char)c);
+ }
+ } else {
+ String s = parsePlainScalar(r, 0);
+ o = convertToType(s, sType, eType, outer, pMeta);
+ }
+ } else if (c == '|' || c == '>') {
+ String s = parseBlockScalar(r, (char)c);
+ o = convertToType(s, sType, eType, outer, pMeta);
+ } else if (c == '~') {
+ r.read(); // consume '~'
+ o = null;
+ } else {
+ String s = parsePlainScalar(r, 0);
+ o = handlePlainScalar(s, r, sType, eType, builder, outer, pMeta, contentColumn > 0 ? contentColumn - 1 : 0);
+ }
+
+ if (nn(swap) && nn(o))
+ o = unswap(swap, o, eType);
+
+ if (nn(outer))
+ setParent(eType, o, outer);
+
+ return (T)o;
+ }
+
+ @SuppressWarnings("rawtypes")
+ private <T> Object handleQuotedScalar(String s, ParserReader r, ClassMeta<?> sType, ClassMeta<?> eType, BuilderSwap<T,Object> builder, Object outer, BeanPropertyMeta pMeta, int keyIndent) throws IOException, ParseException, ExecutableException {
+ if (looksLikeMappingKey(s, r)) {
+ r.read(); // consume ':'
+ int cp = r.peek();
+ if (cp == ' ')
+ r.read();
+
+ if (sType.isObject()) {
+ var m2 = new JsonMap(this);
+ Object value = parseAnything(object(), r, m2, pMeta);
+ String ts = trim(s);
+ setName(object(), value, ts);
+ m2.put(ts, value);
+ parseBlockMappingRemainder(r, m2, string(), object(), pMeta, keyIndent);
+ return cast(m2, pMeta, eType);
+ } else if (sType.isMap()) {
+ Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType));
+ Object value = parseAnything(sType.getValueType(), r, m, pMeta);
+ Object key = convertAttrToType(m, trim(s), sType.getKeyType());
+ setName(sType.getValueType(), value, key);
+ m.put(key, value);
+ parseBlockMappingRemainder(r, m, sType.getKeyType(), sType.getValueType(), pMeta, keyIndent);
+ return m;
+ } else if (nn(builder)) {
+ var m = toBeanMap(builder.create(this, eType));
+ parseBeanProperty(r, m, s);
+ parseIntoBeanMapBlockRemainder(r, m, keyIndent);
+ return builder.build(this, m.getBean(), eType);
+ } else if (sType.canCreateNewBean(outer)) {
+ var m = newBeanMap(outer, sType.inner());
+ parseBeanProperty(r, m, s);
+ parseIntoBeanMapBlockRemainder(r, m, keyIndent);
+ return m.getBean();
+ } else {
+ var m2 = new JsonMap(this);
+ Object value = parseAnything(object(), r, m2, pMeta);
+ m2.put(s, value);
+ parseBlockMappingRemainder(r, m2, string(), object(), pMeta, keyIndent);
+ if (m2.containsKey(getBeanTypePropertyName(eType)))
+ return cast(m2, pMeta, eType);
+ if (nn(sType.getProxyInvocationHandler()))
+ return newBeanMap(outer, sType.inner()).load(m2).getBean();
+ throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", cn(sType), sType.getNotABeanReason());
+ }
+ }
+ if (sType.isObject())
+ return trim(s);
+ return convertToType(s, sType, eType, outer, pMeta);
+ }
+
+ @SuppressWarnings("rawtypes")
+ private <T> Object handlePlainScalar(String s, ParserReader r, ClassMeta<?> sType, ClassMeta<?> eType, BuilderSwap<T,Object> builder, Object outer, BeanPropertyMeta pMeta, int keyIndent) throws IOException, ParseException, ExecutableException {
+ if (s.isEmpty())
+ return null;
+
+ if ("null".equals(s))
+ return null;
+
+ if ("true".equals(s) || "false".equals(s)) {
+ if (sType.isObject() || sType.isBoolean())
+ return Boolean.valueOf(s);
+ return convertToType(s, sType, eType, outer, pMeta);
+ }
+
+ if (looksLikeMappingKey(s, r)) {
+ r.read(); // consume ':'
+ int cp = r.peek();
+ if (cp == ' ')
+ r.read(); // consume space after ':'
+
+ if (sType.isObject()) {
+ var m2 = new JsonMap(this);
+ Object value = parseAnything(object(), r, m2, pMeta);
+ String ts = trim(s);
+ setName(object(), value, ts);
+ m2.put(ts, value);
+ parseBlockMappingRemainder(r, m2, string(), object(), pMeta, keyIndent);
+ return cast(m2, pMeta, eType);
+ } else if (sType.isMap()) {
+ Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType));
+ Object value = parseAnything(sType.getValueType(), r, m, pMeta);
+ Object key = convertAttrToType(m, trim(s), sType.getKeyType());
+ setName(sType.getValueType(), value, key);
+ m.put(key, value);
+ parseBlockMappingRemainder(r, m, sType.getKeyType(), sType.getValueType(), pMeta, keyIndent);
+ return m;
+ } else if (nn(builder)) {
+ var m = toBeanMap(builder.create(this, eType));
+ parseBeanProperty(r, m, s);
+ parseIntoBeanMapBlockRemainder(r, m, keyIndent);
+ return builder.build(this, m.getBean(), eType);
+ } else if (sType.canCreateNewBean(outer)) {
+ var m = newBeanMap(outer, sType.inner());
+ parseBeanProperty(r, m, s);
+ parseIntoBeanMapBlockRemainder(r, m, keyIndent);
+ return m.getBean();
+ } else {
+ var m2 = new JsonMap(this);
+ Object value = parseAnything(object(), r, m2, pMeta);
+ m2.put(s, value);
+ parseBlockMappingRemainder(r, m2, string(), object(), pMeta, keyIndent);
+ if (m2.containsKey(getBeanTypePropertyName(eType)))
+ return cast(m2, pMeta, eType);
+ if (nn(sType.getProxyInvocationHandler()))
+ return newBeanMap(outer, sType.inner()).load(m2).getBean();
+ throw new ParseException(this, "Class ''{0}'' could not be instantiated. Reason: ''{1}''", cn(sType), sType.getNotABeanReason());
+ }
+ }
+
+ return convertToType(s, sType, eType, outer, pMeta);
+ }
+
+ private Object convertToType(String s, ClassMeta<?> sType, ClassMeta<?> eType, Object outer, BeanPropertyMeta pMeta) throws ParseException {
+ if (sType.isObject()) {
+ return resolveScalarType(trim(s));
+ } else if (sType.isBoolean()) {
+ return bool(s);
+ } else if (sType.isCharSequence()) {
+ return trim(s);
+ } else if (sType.isChar()) {
+ return parseCharacter(s);
+ } else if (sType.isNumber()) {
+ return StringUtils.parseNumber(s, (Class<? extends Number>)sType.inner());
+ } else if (sType.isDateOrCalendarOrTemporal() || sType.isDuration()) {
+ return Iso8601Utils.parse(s, sType, getTimeZone());
+ } else if (sType.canCreateNewInstanceFromString(outer)) {
+ return sType.newInstanceFromString(outer, s);
+ } else {
+ throw new ParseException(this, "Unrecognized syntax for class type ''{0}'', value ''{1}''", sType, s);
+ }
+ }
+
+ private boolean looksLikeMappingKey(String s, ParserReader r) throws IOException {
+ int c = r.peek();
+ if (c != ':')
+ return false;
+ // Peek past ':' to make sure it's followed by space, newline, or EOF (not part of value)
+ return true;
+ }
+
+ private <K,V> void parseBlockMappingRemainder(ParserReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta, int parentIndent) throws IOException, ParseException, ExecutableException {
+ parseBlockMapping(r, m, keyType, valueType, pMeta, parentIndent);
+ }
+
+ private <T> void parseIntoBeanMapBlockRemainder(ParserReader r, BeanMap<T> m, int parentIndent) throws IOException, ParseException, ExecutableException {
+ parseIntoBeanMapBlock(r, m, parentIndent);
+ }
+
+ private <T> void parseBeanProperty(ParserReader r, BeanMap<T> m, String currAttr) throws IOException, ParseException, ExecutableException {
+ var pm = m.getPropertyMeta(currAttr);
+ setCurrentProperty(pm);
+ if (pm == null) {
+ onUnknownProperty(currAttr, m, parseAnything(object(), r, m.getBean(false), null));
+ } else {
+ var cm = pm.getClassMeta();
+ Object value = parseAnything(cm, r, m.getBean(false), pm);
+ setName(cm, value, currAttr);
+ try {
+ pm.set(m, currAttr, value);
+ } catch (BeanRuntimeException e) {
+ onBeanSetterException(pm, e);
+ throw e;
+ }
+ }
+ setCurrentProperty(null);
+ }
+
+ private boolean isNullBlockValue(ParserReader r, int blockIndent) throws IOException {
+ int c = r.peek();
+ if (c == -1)
+ return true;
+ if (c != '\n' && c != '\r')
+ return false;
+ int charsRead = 0;
+ while (true) {
+ c = r.read();
+ if (c == -1) {
+ for (int j = 0; j < charsRead; j++) r.unread();
+ return true;
+ }
+ charsRead++;
+ if (c != '\n' && c != '\r' && c != ' ' && c != '\t')
+ break;
+ }
+ int nextColumn = r.getPosition().getColumn() - 1;
+ for (int j = 0; j < charsRead; j++) r.unread();
+ return nextColumn <= blockIndent;
+ }
+
+ private int peekSecondChar(ParserReader r) throws IOException {
+ r.read();
+ int c2 = r.peek();
+ r.unread();
+ return c2;
+ }
+
+ // ==========================================
+ // Flow mapping: { key: value, ... }
+ // ==========================================
+ @SuppressWarnings({
+ "java:S1168",
+ "java:S135",
+ "java:S2583",
+ "java:S3776"
+ })
+ private <K,V> Map<K,V> parseFlowMapping(ParserReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
+
+ if (keyType == null)
+ keyType = (ClassMeta<K>)string();
+
+ // S1: Looking for outer {
+ // S2: Looking for key start or }
+ // S3: Found key, looking for :
+ // S4: Found :, looking for value
+ // S5: Looking for , or }
+ // S6: Found , looking for key start
+
+ var state = S1;
+ String currKey = null;
+ int c = 0;
+ while (c != -1) {
+ c = r.read();
+ if (state == S1) {
+ if (c == '{')
+ state = S2;
+ else if (isWhitespace(c))
+ continue;
+ else
+ break;
+ } else if (state == S2) {
+ if (c == '}') {
+ return m;
+ } else if (isWhitespace(c)) {
+ continue;
+ } else {
+ r.unread();
+ currKey = parseFlowMappingKey(r);
+ state = S3;
+ }
+ } else if (state == S3) {
+ if (c == ':')
+ state = S4;
+ else if (isWhitespace(c))
+ continue;
+ } else if (state == S4) {
+ if (isWhitespace(c)) {
+ continue;
+ } else {
+ r.unread();
+ K key = convertAttrToType(m, currKey, keyType);
+ V value = parseAnything(valueType, r, m, pMeta);
+ setName(valueType, value, key);
+ m.put(key, value);
+ state = S5;
+ }
+ } else if (state == S5) {
+ if (c == ',') {
+ state = S6;
+ } else if (isWhitespace(c)) {
+ continue;
+ } else if (c == '}') {
+ return m;
+ } else {
+ break;
+ }
+ } else if (state == S6) {
+ if (c == '}') {
+ break;
+ } else if (isWhitespace(c)) {
+ continue;
+ } else {
+ r.unread();
+ currKey = parseFlowMappingKey(r);
+ state = S3;
+ }
+ }
+ }
+ if (state == S1)
+ throw new ParseException(this, "Expected '{' at beginning of YAML flow mapping.");
+ if (state == S2)
+ throw new ParseException(this, "Could not find key in YAML flow mapping.");
+ if (state == S3)
+ throw new ParseException(this, "Could not find ':' following key in YAML flow mapping.");
+ if (state == S4)
+ throw new ParseException(this, "Expected value in YAML flow mapping.");
+ if (state == S5)
+ throw new ParseException(this, "Could not find '}' marking end of YAML flow mapping.");
+ if (state == S6)
+ throw new ParseException(this, "Unexpected '}' found in YAML flow mapping.");
+
+ return null; // Unreachable.
+ }
+
+ private String parseFlowMappingKey(ParserReader r) throws IOException, ParseException {
+ int c = r.peek();
+ if (c == '\'' )
+ return parseSingleQuotedString(r);
+ if (c == '"')
+ return parseDoubleQuotedString(r);
+ return parsePlainFlowKey(r);
+ }
+
+ private String parsePlainFlowKey(ParserReader r) throws IOException {
+ var sb = new StringBuilder();
+ int c;
+ while ((c = r.read()) != -1) {
+ if (c == ':' || c == ',' || c == '}' || c == '{' || c == '[' || c == ']') {
+ r.unread();
+ break;
+ }
+ sb.append((char)c);
+ }
+ return sb.toString().trim();
+ }
+
+ // ==========================================
+ // Flow sequence: [ value, ... ]
+ // ==========================================
+ @SuppressWarnings({
+ "java:S1168",
+ "java:S135",
+ "java:S2583",
+ "java:S3776"
+ })
+ private <E> Collection<E> parseFlowSequence(ParserReader r, Collection<E> l, ClassMeta<?> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
+
+ // S1: Looking for outermost [
+ // S2: Looking for value or ]
+ // S3: Looking for , or ]
+ // S4: Looking for value
+
+ int argIndex = 0;
+
+ var state = S1;
+ int c = 0;
+ while (c != -1) {
+ c = r.read();
+ if (state == S1) {
+ if (c == '[')
+ state = S2;
+ else if (isWhitespace(c))
+ continue;
+ else
+ break;
+ } else if (state == S2) {
+ if (c == ']') {
+ return l;
+ } else if (isWhitespace(c)) {
+ continue;
+ } else if (c != -1) {
+ r.unread();
+ l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r, l, pMeta));
+ state = S3;
+ }
+ } else if (state == S3) {
+ if (c == ',') {
+ state = S4;
+ } else if (isWhitespace(c)) {
+ continue;
+ } else if (c == ']') {
+ return l;
+ } else {
+ break;
+ }
+ } else if (state == S4) {
+ if (isWhitespace(c)) {
+ continue;
+ } else if (c == ']') {
+ break;
+ } else if (c != -1) {
+ r.unread();
+ l.add((E)parseAnything(type.isArgs() ? type.getArg(argIndex++) : type.getElementType(), r, l, pMeta));
+ state = S3;
+ }
+ }
+ }
+ if (state == S1)
+ throw new ParseException(this, "Expected '[' at beginning of YAML flow sequence.");
+ if (state == S2)
+ throw new ParseException(this, "Expected value or ']' in YAML flow sequence.");
+ if (state == S3)
+ throw new ParseException(this, "Expected ',' or ']' in YAML flow sequence.");
+ if (state == S4)
+ throw new ParseException(this, "Unexpected trailing comma in YAML flow sequence.");
+
+ return null; // Unreachable.
+ }
+
+ // ==========================================
+ // Block mapping
+ // ==========================================
+ private <K,V> Map<K,V> parseBlockMapping(ParserReader r, Map<K,V> m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta, int parentIndent) throws IOException, ParseException, ExecutableException {
+
+ if (keyType == null)
+ keyType = (ClassMeta<K>)string();
+
+ int blockIndent = -1;
+
+ while (true) {
+ int consumed = skipBlanksAndCountIndent(r);
+ int lineIndent = r.getPosition().getColumn();
+ int c = r.peek();
+ if (c == -1)
+ break;
+
+ if (blockIndent == -1) {
+ if (lineIndent <= parentIndent && parentIndent > 0) {
+ unreadSpaces(r, consumed);
+ break;
+ }
+ blockIndent = lineIndent;
+ }
+
+ if (lineIndent < blockIndent) {
+ unreadSpaces(r, consumed);
+ break;
+ }
+
+ c = r.peek();
+ if (c == '#') {
+ skipToEndOfLine(r);
+ continue;
+ }
+
+ String keyStr = trim(parseBlockMappingKey(r));
+
+ c = r.read(); // Should be ':'
+ if (c != ':')
+ throw new ParseException(this, "Expected ':' after key in YAML block mapping, found ''{0}''", (char)c);
+
+ c = r.peek();
+ if (c == ' ')
+ r.read();
+ K key = convertAttrToType(m, keyStr, keyType);
+ V value = (V)parseAnything(valueType, r, m, pMeta);
+ setName(valueType, value, key);
+ m.put(key, value);
+ }
+
+ return m;
+ }
+
+ private int skipBlanksAndCountIndent(ParserReader r) throws IOException {
+ int indent = 0;
+ int c;
+ while ((c = r.read()) != -1) {
+ if (c == ' ') {
+ indent++;
+ } else if (c == '\n' || c == '\r') {
+ indent = 0;
+ } else if (c == '\t') {
+ indent++;
+ } else {
+ r.unread();
+ break;
+ }
+ }
+ return indent;
+ }
+
+ private void unreadSpaces(ParserReader r, int count) throws IOException {
+ for (int i = 0; i < count; i++)
+ r.unread();
+ }
+
+ private String parseBlockMappingKey(ParserReader r) throws IOException, ParseException {
+ int c = r.peek();
+ if (c == '\'')
+ return parseSingleQuotedString(r);
+ if (c == '"')
+ return parseDoubleQuotedString(r);
+ var sb = new StringBuilder();
+ while ((c = r.read()) != -1) {
+ if (c == ':') {
+ int next = r.peek();
+ if (next == ' ' || next == '\n' || next == '\r' || next == -1) {
+ r.unread();
+ break;
+ }
+ sb.append((char)c);
+ } else if (c == '\n' || c == '\r') {
+ r.unread();
+ break;
+ } else {
+ sb.append((char)c);
+ }
+ }
+ String key = sb.toString().trim();
+ if ("null".equals(key) || "Null".equals(key) || "NULL".equals(key) || "~".equals(key))
+ return null;
+ return key;
+ }
+
+ // ==========================================
+ // Block mapping for beans
+ // ==========================================
+ @SuppressWarnings({
+ "java:S3776"
+ })
+ private <T> BeanMap<T> parseIntoBeanMap(ParserReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException {
+ int c = r.peek();
+ if (c == '{') {
+ return parseIntoBeanMapFlow(r, m);
+ }
+ return parseIntoBeanMapBlock(r, m, 0);
+ }
+
+ @SuppressWarnings({
+ "java:S1168",
+ "java:S2583",
+ "java:S3776"
+ })
+ private <T> BeanMap<T> parseIntoBeanMapFlow(ParserReader r, BeanMap<T> m) throws IOException, ParseException, ExecutableException {
+
+ // S1: Looking for outer {
+ // S2: Looking for attrName start.
+ // S3: Found attrName end, looking for :.
+ // S4: Found :, looking for valStart.
+ // S5: Looking for , or }
+
+ var state = S1;
+ var currAttr = "";
+ int c = 0;
+ mark();
+ try {
+ while (c != -1) {
+ c = r.read();
+ if (state == S1) {
+ if (c == '{')
+ state = S2;
+ else if (isWhitespace(c))
+ continue;
+ else
+ break;
+ } else if (state == S2) {
+ if (c == '}') {
+ return m;
+ } else if (isWhitespace(c)) {
+ continue;
+ } else {
+ r.unread();
+ mark();
+ currAttr = parseFlowMappingKey(r);
+ state = S3;
+ }
+ } else if (state == S3) {
+ if (c == ':')
+ state = S4;
+ else if (isWhitespace(c))
+ continue;
+ } else if (state == S4) {
+ if (isWhitespace(c)) {
+ continue;
+ } else {
+ if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
+ var pm = m.getPropertyMeta(currAttr);
+ setCurrentProperty(pm);
+ if (pm == null) {
+ onUnknownProperty(currAttr, m, parseAnything(object(), r.unread(), m.getBean(false), null));
+ unmark();
+ } else {
+ unmark();
+ var cm = pm.getClassMeta();
+ Object value = parseAnything(cm, r.unread(), m.getBean(false), pm);
+ setName(cm, value, currAttr);
+ try {
+ pm.set(m, currAttr, value);
+ } catch (BeanRuntimeException e) {
+ onBeanSetterException(pm, e);
+ throw e;
+ }
+ }
+ setCurrentProperty(null);
+ }
+ state = S5;
+ }
+ } else if (state == S5) {
+ if (c == ',')
+ state = S2;
+ else if (isWhitespace(c))
+ continue;
+ else if (c == '}') {
+ return m;
+ }
+ }
+ }
+ if (state == S1)
+ throw new ParseException(this, "Expected '{' at beginning of YAML flow mapping.");
+ if (state == S2)
+ throw new ParseException(this, "Could not find attribute name in YAML flow mapping.");
+ if (state == S3)
+ throw new ParseException(this, "Could not find ':' following attribute name in YAML flow mapping.");
+ if (state == S4)
+ throw new ParseException(this, "Expected value in YAML flow mapping.");
+ if (state == S5)
+ throw new ParseException(this, "Could not find '}' marking end of YAML flow mapping.");
+ } finally {
+ unmark();
+ }
+
+ return null; // Unreachable.
+ }
+
+ @SuppressWarnings({
+ "java:S3776"
+ })
+ private <T> BeanMap<T> parseIntoBeanMapBlock(ParserReader r, BeanMap<T> m, int parentIndent) throws IOException, ParseException, ExecutableException {
+
+ int blockIndent = -1;
+
+ mark();
+ try {
+ while (true) {
+ int consumed = skipBlanksAndCountIndent(r);
+ int lineIndent = r.getPosition().getColumn();
+ int c = r.peek();
+ if (c == -1)
+ break;
+
+ if (blockIndent == -1) {
+ if (lineIndent <= parentIndent && parentIndent > 0) {
+ unreadSpaces(r, consumed);
+ break;
+ }
+ blockIndent = lineIndent;
+ }
+
+ if (lineIndent < blockIndent) {
+ unreadSpaces(r, consumed);
+ break;
+ }
+
+ c = r.peek();
+ if (c == '#') {
+ skipToEndOfLine(r);
+ continue;
+ }
+
+ String currAttr = trim(parseBlockMappingKey(r));
+
+ c = r.read(); // Should be ':'
+ if (c != ':')
+ throw new ParseException(this, "Expected ':' after attribute name in YAML block mapping, found ''{0}''", (char)c);
+
+ if (! currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
+ var pm = m.getPropertyMeta(currAttr);
+ setCurrentProperty(pm);
+ if (pm == null) {
+ c = r.peek();
+ if (c == ' ')
+ r.read();
+ Object v = isNullBlockValue(r, blockIndent) ? null : parseAnything(object(), r, m.getBean(false), null);
+ onUnknownProperty(currAttr, m, v);
+ unmark();
+ } else {
+ unmark();
+ var cm = pm.getClassMeta();
+ c = r.peek();
+ if (c == ' ')
+ r.read();
+ Object value = isNullBlockValue(r, blockIndent) ? null : parseAnything(cm, r, m.getBean(false), pm);
+ setName(cm, value, currAttr);
+ try {
+ pm.set(m, currAttr, value);
+ } catch (BeanRuntimeException e) {
+ onBeanSetterException(pm, e);
+ throw e;
+ }
+ }
+ setCurrentProperty(null);
+ }
+ mark();
+ }
+ } finally {
+ unmark();
+ }
+
+ return m;
+ }
+
+ // ==========================================
+ // Block sequence: - value\n- value\n...
+ // ==========================================
+ private <E> Collection<E> parseBlockSequence(ParserReader r, Collection<E> l, ClassMeta<?> type, BeanPropertyMeta pMeta, int parentIndent) throws IOException, ParseException, ExecutableException {
+
+ int blockIndent = -1;
+ int argIndex = 0;
+
+ while (true) {
+ int consumed = skipBlanksAndCountIndent(r);
+ int lineIndent = r.getPosition().getColumn();
+ int c = r.peek();
+ if (c == -1)
+ break;
+
+ if (blockIndent == -1) {
+ if (lineIndent <= parentIndent && parentIndent > 0) {
+ unreadSpaces(r, consumed);
+ break;
+ }
+ blockIndent = lineIndent;
+ }
+
+ if (lineIndent < blockIndent) {
+ unreadSpaces(r, consumed);
+ break;
+ }
+
+ c = r.peek();
+ if (c == '#') {
+ skipToEndOfLine(r);
+ continue;
+ }
+ if (c != '-') {
+ unreadSpaces(r, lineIndent);
+ break;
+ }
+
+ r.read(); // consume '-'
+ c = r.peek();
+ if (c != ' ' && c != '\n' && c != '\r') {
+ unreadSpaces(r, lineIndent);
+ break;
+ }
+
+ if (c == ' ')
+ r.read(); // consume space after '-'
+
+ ClassMeta<?> elementType = type.isArgs() ? type.getArg(argIndex++) : type.getElementType();
+ l.add((E)parseAnything(elementType, r, l, pMeta));
+ }
+
+ return l;
+ }
+
+ // ==========================================
+ // Single-quoted string: 'hello ''world'''
+ // ==========================================
+ private String parseSingleQuotedString(ParserReader r) throws IOException, ParseException {
+ int c = r.read(); // consume opening '
+ if (c != '\'')
+ throw new ParseException(this, "Expected single quote to start string.");
+
+ var sb = new StringBuilder();
+ while ((c = r.read()) != -1) {
+ if (c == '\'') {
+ int next = r.peek();
+ if (next == '\'') {
+ r.read(); // consume escaped '
+ sb.append('\'');
+ } else {
+ return sb.toString();
+ }
+ } else {
+ sb.append((char)c);
+ }
+ }
+ throw new ParseException(this, "Could not find end of single-quoted YAML string.");
+ }
+
+ // ==========================================
+ // Double-quoted string: "hello \"world\"\n"
+ // ==========================================
+ @SuppressWarnings({
+ "java:S3776"
+ })
+ private String parseDoubleQuotedString(ParserReader r) throws IOException, ParseException {
+ int c = r.read(); // consume opening "
+ if (c != '"')
+ throw new ParseException(this, "Expected double quote to start string.");
+
+ var sb = new StringBuilder();
+ while ((c = r.read()) != -1) {
+ if (c == '\\') {
+ c = r.read();
+ // @formatter:off
+ switch (c) {
+ case '\\': sb.append('\\'); break;
+ case '"': sb.append('"'); break;
+ case 'n': sb.append('\n'); break;
+ case 't': sb.append('\t'); break;
+ case 'r': sb.append('\r'); break;
+ case '0': sb.append('\0'); break;
+ case 'a': sb.append('\u0007'); break;
+ case 'b': sb.append('\b'); break;
+ case 'f': sb.append('\f'); break;
+ case 'e': sb.append('\u001b'); break;
+ case '/': sb.append('/'); break;
+ case 'x': {
+ String hex = r.read(2);
+ try {
+ sb.append((char)Integer.parseInt(hex, 16));
+ } catch (@SuppressWarnings("unused") NumberFormatException e) {
+ throw new ParseException(this, "Invalid \\x escape sequence in YAML string.");
+ }
+ break;
+ }
+ case 'u': {
+ String hex = r.read(4);
+ try {
+ sb.append((char)Integer.parseInt(hex, 16));
+ } catch (@SuppressWarnings("unused") NumberFormatException e) {
+ throw new ParseException(this, "Invalid \\u escape sequence in YAML string.");
+ }
+ break;
+ }
+ default:
+ throw new ParseException(this, "Invalid escape sequence '\\{0}' in YAML string.", (char)c);
+ }
+ // @formatter:on
+ } else if (c == '"') {
+ return sb.toString();
+ } else {
+ sb.append((char)c);
+ }
+ }
+ throw new ParseException(this, "Could not find end of double-quoted YAML string.");
+ }
+
+ // ==========================================
+ // Plain scalar (unquoted)
+ // ==========================================
+ private String parsePlainScalar(ParserReader r, int indent) throws IOException {
+ var sb = new StringBuilder();
+ int c;
+ while ((c = r.read()) != -1) {
+ if (c == '\n' || c == '\r') {
+ r.unread();
+ break;
+ }
+ if (c == ':') {
+ int next = r.peek();
+ if (next == ' ' || next == '\n' || next == '\r' || next == -1) {
+ r.unread(); // unread ':'
+ break;
+ }
+ sb.append((char)c);
+ } else if (c == ' ') {
+ int next = r.peek();
+ if (next == '#') {
+ // rest of line is comment
+ r.unread();
+ break;
+ }
+ sb.append((char)c);
+ } else if (c == ',' || c == '}' || c == ']') {
+ r.unread();
+ break;
+ } else if (c == '#' && sb.isEmpty()) {
+ r.unread();
+ break;
+ } else {
+ sb.append((char)c);
+ }
+ }
+ String result = sb.toString();
+ // Trim trailing whitespace
+ int end = result.length();
+ while (end > 0 && result.charAt(end - 1) == ' ')
+ end--;
+ return result.substring(0, end);
+ }
+
+ // ==========================================
+ // Block scalar: | or >
+ // ==========================================
+ @SuppressWarnings({
+ "java:S3776"
+ })
+ private String parseBlockScalar(ParserReader r, char indicator) throws IOException, ParseException {
+ r.read(); // consume '|' or '>'
+
+ // Parse indicator line for chomping
+ char chomping = ' '; // default = clip
+ int c;
+ while ((c = r.read()) != -1) {
+ if (c == '-') {
+ chomping = '-'; // strip
+ } else if (c == '+') {
+ chomping = '+'; // keep
+ } else if (c == '\n' || c == '\r') {
+ if (c == '\r') {
+ int next = r.peek();
+ if (next == '\n')
+ r.read();
+ }
+ break;
+ } else if (c >= '1' && c <= '9') {
+ // explicit indent indicator, ignore for now (auto-detect below)
+ } else if (c == ' ' || c == '\t') {
+ continue;
+ } else {
+ throw new ParseException(this, "Unexpected character ''{0}'' in YAML block scalar indicator.", (char)c);
+ }
+ }
+
+ // Read the block content
+ int blockIndent = -1;
+ var lines = new ArrayList<String>();
+ int trailingNewlines = 0;
+
+ while (true) {
+ // Count leading spaces
+ int spaces = 0;
+ while ((c = r.read()) != -1) {
+ if (c == ' ')
+ spaces++;
+ else
+ break;
+ }
+
+ if (c == -1) {
+ break;
+ }
+
+ if (c == '\n' || c == '\r') {
+ // Empty line
+ trailingNewlines++;
+ lines.add("");
+ if (c == '\r' && r.peek() == '\n')
+ r.read();
+ continue;
+ }
+
+ if (blockIndent == -1) {
+ blockIndent = spaces;
+ }
+
+ if (spaces < blockIndent) {
+ // End of block, push back everything
+ r.unread(); // push back non-space char
+ for (int i = 0; i < spaces; i++)
+ r.unread();
+ break;
+ }
+
+ // Read rest of line
+ var lineSb = new StringBuilder();
+ // Include extra spaces beyond blockIndent
+ for (int i = blockIndent; i < spaces; i++)
+ lineSb.append(' ');
+ lineSb.append((char)c);
+ while ((c = r.read()) != -1) {
+ if (c == '\n' || c == '\r') {
+ if (c == '\r' && r.peek() == '\n')
+ r.read();
+ break;
+ }
+ lineSb.append((char)c);
+ }
+
+ // Account for any blank lines that preceded this content line
+ for (int i = 0; i < trailingNewlines; i++)
+ lines.add("");
+ trailingNewlines = 0;
+ lines.add(lineSb.toString());
+
+ if (c == -1)
+ break;
+ }
+
+ // Build the result based on indicator and chomping
+ var sb = new StringBuilder();
+ for (int i = 0; i < lines.size(); i++) {
+ String line = lines.get(i);
+ if (indicator == '|') {
+ // Literal: preserve newlines
+ if (i > 0)
+ sb.append('\n');
+ sb.append(line);
+ } else {
+ // Folded: single newlines -> space, multiple newlines preserved
+ if (i > 0) {
+ if (line.isEmpty() || lines.get(i - 1).isEmpty()) {
+ sb.append('\n');
+ } else {
+ sb.append(' ');
+ }
+ }
+ sb.append(line);
+ }
+ }
+
+ // Apply chomping
+ if (chomping == '-') {
+ // strip: no trailing newline
+ } else if (chomping == '+') {
+ // keep: preserve all trailing newlines
+ sb.append('\n');
+ for (int i = 0; i < trailingNewlines; i++)
+ sb.append('\n');
+ } else {
+ // clip (default): single trailing newline
+ sb.append('\n');
+ }
+
+ return sb.toString();
+ }
+
+ // ==========================================
+ // Helper methods
+ // ==========================================
+
+ private void skipWhitespaceAndComments(ParserReader r) throws IOException {
+ int c;
+ while ((c = r.read()) != -1) {
+ if (c == '#') {
+ // Skip to end of line
+ while ((c = r.read()) != -1) {
+ if (c == '\n' || c == '\r')
+ break;
+ }
+ } else if (!isWhitespace(c)) {
+ r.unread();
+ return;
+ }
+ }
+ }
+
+ private void skipDocumentMarker(ParserReader r) throws IOException {
+ int c = r.peek();
+ if (c == '-') {
+ r.read();
+ int c2 = r.peek();
+ if (c2 == '-') {
+ r.read();
+ int c3 = r.peek();
+ if (c3 == '-') {
+ r.read();
+ // '---' found, skip rest of line
+ skipToEndOfLine(r);
+ return;
+ }
+ r.unread(); // unread second '-'
+ }
+ r.unread(); // unread first '-'
+ } else if (c == '.') {
+ r.read();
+ int c2 = r.peek();
+ if (c2 == '.') {
+ r.read();
+ int c3 = r.peek();
+ if (c3 == '.') {
+ r.read();
+ // '...' found, skip rest of line
+ skipToEndOfLine(r);
+ return;
+ }
+ r.unread();
+ }
+ r.unread();
+ }
+ }
+
+ private void skipToEndOfLine(ParserReader r) throws IOException {
+ int c;
+ while ((c = r.read()) != -1) {
+ if (c == '\n' || c == '\r')
+ return;
+ }
+ }
+
+ private boolean isWhitespace(int c) {
+ return c == ' ' || c == '\t' || c == '\n' || c == '\r';
+ }
+
+ private Object resolveScalarType(String s) {
+ if (s == null || "null".equals(s) || "~".equals(s) || s.isEmpty())
+ return null;
+ if ("true".equals(s) || "True".equals(s) || "TRUE".equals(s))
+ return Boolean.TRUE;
+ if ("false".equals(s) || "False".equals(s) || "FALSE".equals(s))
+ return Boolean.FALSE;
+
+ // Try parsing as number
+ if (!s.isEmpty()) {
+ char first = s.charAt(0);
+ if (first == '-' || first == '+' || (first >= '0' && first <= '9') || first == '.') {
+ try {
+ if (s.contains(".") || s.contains("e") || s.contains("E")) {
+ Double d = Double.parseDouble(s);
+ if (!d.isInfinite() && !d.isNaN())
+ return d;
+ } else {
+ try {
+ return Integer.parseInt(s);
+ } catch (@SuppressWarnings("unused") NumberFormatException e2) {
+ try {
+ return Long.parseLong(s);
+ } catch (@SuppressWarnings("unused") NumberFormatException e3) {
+ // Fall through to string
+ }
+ }
+ }
+ } catch (@SuppressWarnings("unused") NumberFormatException e) {
+ // Fall through to string
+ }
+ }
+ }
+
+ return s;
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializer.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializer.java
new file mode 100644
index 0000000000..b6c95ac26e
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializer.java
@@ -0,0 +1,907 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.juneau.yaml;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import java.lang.annotation.*;
+import java.nio.charset.*;
+import java.util.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.collections.*;
+import org.apache.juneau.commons.function.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * Serializes POJO models to YAML.
+ *
+ * <h5 class='topic'>Media types</h5>
+ * <p>
+ * Handles <c>Accept</c> types: <bc>application/yaml, text/yaml</bc>
+ * <p>
+ * Produces <c>Content-Type</c> types: <bc>application/yaml</bc>
+ *
+ * <h5 class='topic'>Description</h5>
+ * <p>
+ * The conversion is as follows...
+ * <ul class='spaced-list'>
+ * <li>
+ * Maps (e.g. {@link HashMap HashMaps}, {@link TreeMap TreeMaps}) are converted to YAML mappings.
+ * <li>
+ * Collections (e.g. {@link HashSet HashSets}, {@link LinkedList LinkedLists}) and Java arrays are converted to
+ * YAML sequences.
+ * <li>
+ * {@link String Strings} are converted to YAML scalars.
+ * <li>
+ * {@link Number Numbers} (e.g. {@link Integer}, {@link Long}, {@link Double}) are converted to YAML numbers.
+ * <li>
+ * {@link Boolean Booleans} are converted to YAML booleans.
+ * <li>
+ * {@code nulls} are converted to YAML nulls.
+ * <li>
+ * {@code arrays} are converted to YAML sequences.
+ * <li>
+ * {@code beans} are converted to YAML mappings.
+ * </ul>
+ *
+ * <p>
+ * The types above are considered "YAML-primitive" object types.
+ * Any non-YAML-primitive object types are transformed into YAML-primitive object types through
+ * {@link org.apache.juneau.swap.ObjectSwap ObjectSwaps} associated through the
+ * {@link org.apache.juneau.BeanContext.Builder#swaps(Class...)} method.
+ * Several default transforms are provided for transforming Dates, Enums, Iterators, etc...
+ *
+ * <p>
+ * This serializer provides several serialization options.
+ * Typically, one of the predefined DEFAULT serializers will be sufficient.
+ * However, custom serializers can be constructed to fine-tune behavior.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Use one of the default serializers to serialize a POJO</jc>
+ * String <jv>yaml</jv> = YamlSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>someObject</jv>);
+ *
+ * <jc>// Create a custom serializer</jc>
+ * YamlSerializer <jv>serializer</jv> = YamlSerializer.<jsm>create</jsm>().build();
+ *
+ * <jc>// Clone an existing serializer and modify it to use whitespace</jc>
+ * <jv>serializer</jv> = YamlSerializer.<jsf>DEFAULT</jsf>.copy().ws().build();
+ *
+ * <jc>// Serialize a POJO to YAML</jc>
+ * String <jv>yaml</jv> = <jv>serializer</jv>.serialize(<jv>someObject</jv>);
+ * </p>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>This class is thread safe and reusable.
+ * </ul>
+ */
+@SuppressWarnings({
+ "java:S110", // Inheritance depth acceptable for this class hierarchy
+ "java:S115", // Constants use UPPER_snakeCase naming convention
+})
+public class YamlSerializer extends WriterSerializer {
+
+ private static final String PROP_addBeanTypesYaml = "addBeanTypesYaml";
+
+ private static final String ARG_copyFrom = "copyFrom";
+
+ /**
+ * Builder class.
+ */
+ public static class Builder extends WriterSerializer.Builder {
+
+ private static final Cache<HashKey,YamlSerializer> CACHE = Cache.of(HashKey.class, YamlSerializer.class).build();
+
+ private boolean addBeanTypesYaml;
+
+ /**
+ * Constructor, default settings.
+ */
+ protected Builder() {
+ produces("application/yaml");
+ accept("application/yaml,text/yaml");
+ addBeanTypesYaml = env("YamlSerializer.addBeanTypes", false);
+ }
+
+ /**
+ * Copy constructor.
+ *
+ * @param copyFrom The builder to copy from.
+ * <br>Cannot be <jk>null</jk>.
+ */
+ protected Builder(Builder copyFrom) {
+ super(assertArgNotNull(ARG_copyFrom, copyFrom));
+ addBeanTypesYaml = copyFrom.addBeanTypesYaml;
+ }
+
+ /**
+ * Copy constructor.
+ *
+ * @param copyFrom The bean to copy from.
+ * <br>Cannot be <jk>null</jk>.
+ */
+ protected Builder(YamlSerializer copyFrom) {
+ super(assertArgNotNull(ARG_copyFrom, copyFrom));
+ addBeanTypesYaml = copyFrom.addBeanTypesYaml;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder accept(String value) {
+ super.accept(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder addBeanTypes() {
+ super.addBeanTypes();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder addBeanTypes(boolean value) {
+ super.addBeanTypes(value);
+ return this;
+ }
+
+ /**
+ * Add <js>"_type"</js> properties when needed.
+ *
+ * <p>
+ * If <jk>true</jk>, then <js>"_type"</js> properties will be added to beans if their type cannot be inferred
+ * through reflection.
+ *
+ * <p>
+ * When present, this value overrides the {@link org.apache.juneau.serializer.Serializer.Builder#addBeanTypes()} setting and is
+ * provided to customize the behavior of specific serializers in a {@link SerializerSet}.
+ *
+ * @return This object.
+ */
+ public Builder addBeanTypesYaml() {
+ return addBeanTypesYaml(true);
+ }
+
+ /**
+ * Same as {@link #addBeanTypesYaml()} but allows you to explicitly specify the value.
+ *
+ * @param value The value for this setting.
+ * @return This object.
+ */
+ public Builder addBeanTypesYaml(boolean value) {
+ addBeanTypesYaml = value;
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder addRootType() {
+ super.addRootType();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder addRootType(boolean value) {
+ super.addRootType(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder annotations(Annotation...values) {
+ super.annotations(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder apply(AnnotationWorkList work) {
+ super.apply(work);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder applyAnnotations(Class<?>...from) {
+ super.applyAnnotations(from);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder applyAnnotations(Object...from) {
+ super.applyAnnotations(from);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanClassVisibility(Visibility value) {
+ super.beanClassVisibility(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanConstructorVisibility(Visibility value) {
+ super.beanConstructorVisibility(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanContext(BeanContext value) {
+ super.beanContext(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanContext(BeanContext.Builder value) {
+ super.beanContext(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanDictionary(java.lang.Class<?>...values) {
+ super.beanDictionary(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanFieldVisibility(Visibility value) {
+ super.beanFieldVisibility(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanInterceptor(Class<?> on, Class<? extends org.apache.juneau.swap.BeanInterceptor<?>> value) {
+ super.beanInterceptor(on, value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanMapPutReturnsOldValue() {
+ super.beanMapPutReturnsOldValue();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanMethodVisibility(Visibility value) {
+ super.beanMethodVisibility(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanProperties(Class<?> beanClass, String properties) {
+ super.beanProperties(beanClass, properties);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanProperties(Map<String,Object> values) {
+ super.beanProperties(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanProperties(String beanClassName, String properties) {
+ super.beanProperties(beanClassName, properties);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesExcludes(Class<?> beanClass, String properties) {
+ super.beanPropertiesExcludes(beanClass, properties);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesExcludes(Map<String,Object> values) {
+ super.beanPropertiesExcludes(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesExcludes(String beanClassName, String properties) {
+ super.beanPropertiesExcludes(beanClassName, properties);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesReadOnly(Class<?> beanClass, String properties) {
+ super.beanPropertiesReadOnly(beanClass, properties);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesReadOnly(Map<String,Object> values) {
+ super.beanPropertiesReadOnly(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesReadOnly(String beanClassName, String properties) {
+ super.beanPropertiesReadOnly(beanClassName, properties);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesWriteOnly(Class<?> beanClass, String properties) {
+ super.beanPropertiesWriteOnly(beanClass, properties);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesWriteOnly(Map<String,Object> values) {
+ super.beanPropertiesWriteOnly(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beanPropertiesWriteOnly(String beanClassName, String properties) {
+ super.beanPropertiesWriteOnly(beanClassName, properties);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beansRequireDefaultConstructor() {
+ super.beansRequireDefaultConstructor();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beansRequireSerializable() {
+ super.beansRequireSerializable();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder beansRequireSettersForGetters() {
+ super.beansRequireSettersForGetters();
+ return this;
+ }
+
+ @Override /* Overridden from Context.Builder */
+ public YamlSerializer build() {
+ return cache(CACHE).build(YamlSerializer.class);
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder cache(Cache<HashKey,? extends org.apache.juneau.Context> value) {
+ super.cache(value);
+ return this;
+ }
+
+ @Override /* Overridden from Context.Builder */
+ public Builder copy() {
+ return new Builder(this);
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder debug() {
+ super.debug();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder debug(boolean value) {
+ super.debug(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder detectRecursions() {
+ super.detectRecursions();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder detectRecursions(boolean value) {
+ super.detectRecursions(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder dictionaryOn(Class<?> on, java.lang.Class<?>...values) {
+ super.dictionaryOn(on, values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder disableBeansRequireSomeProperties() {
+ super.disableBeansRequireSomeProperties();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder disableIgnoreMissingSetters() {
+ super.disableIgnoreMissingSetters();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder disableIgnoreTransientFields() {
+ super.disableIgnoreTransientFields();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder disableIgnoreUnknownNullBeanProperties() {
+ super.disableIgnoreUnknownNullBeanProperties();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder disableInterfaceProxies() {
+ super.disableInterfaceProxies();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public <T> Builder example(Class<T> pojoClass, String json) {
+ super.example(pojoClass, json);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public <T> Builder example(Class<T> pojoClass, T o) {
+ super.example(pojoClass, o);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder fileCharset(Charset value) {
+ super.fileCharset(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder findFluentSetters() {
+ super.findFluentSetters();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder findFluentSetters(Class<?> on) {
+ super.findFluentSetters(on);
+ return this;
+ }
+
+ @Override /* Overridden from Context.Builder */
+ public HashKey hashKey() {
+ return HashKey.of(
+ super.hashKey(),
+ addBeanTypesYaml
+ );
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder ignoreInvocationExceptionsOnGetters() {
+ super.ignoreInvocationExceptionsOnGetters();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder ignoreInvocationExceptionsOnSetters() {
+ super.ignoreInvocationExceptionsOnSetters();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder ignoreRecursions() {
+ super.ignoreRecursions();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder ignoreRecursions(boolean value) {
+ super.ignoreRecursions(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder ignoreUnknownBeanProperties() {
+ super.ignoreUnknownBeanProperties();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder ignoreUnknownEnumValues() {
+ super.ignoreUnknownEnumValues();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder impl(Context value) {
+ super.impl(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder implClass(Class<?> interfaceClass, Class<?> implClass) {
+ super.implClass(interfaceClass, implClass);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder implClasses(Map<Class<?>,Class<?>> values) {
+ super.implClasses(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder initialDepth(int value) {
+ super.initialDepth(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder interfaceClass(Class<?> on, Class<?> value) {
+ super.interfaceClass(on, value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder interfaces(java.lang.Class<?>...value) {
+ super.interfaces(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder keepNullProperties() {
+ super.keepNullProperties();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder keepNullProperties(boolean value) {
+ super.keepNullProperties(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder listener(Class<? extends org.apache.juneau.serializer.SerializerListener> value) {
+ super.listener(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder locale(Locale value) {
+ super.locale(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder maxDepth(int value) {
+ super.maxDepth(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder maxIndent(int value) {
+ super.maxIndent(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder mediaType(MediaType value) {
+ super.mediaType(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder notBeanClasses(java.lang.Class<?>...values) {
+ super.notBeanClasses(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder notBeanPackages(String...values) {
+ super.notBeanPackages(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder produces(String value) {
+ super.produces(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder propertyNamer(Class<?> on, Class<? extends org.apache.juneau.PropertyNamer> value) {
+ super.propertyNamer(on, value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder propertyNamer(Class<? extends org.apache.juneau.PropertyNamer> value) {
+ super.propertyNamer(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder quoteChar(char value) {
+ super.quoteChar(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder quoteCharOverride(char value) {
+ super.quoteCharOverride(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder sortCollections() {
+ super.sortCollections();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder sortCollections(boolean value) {
+ super.sortCollections(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder sortMaps() {
+ super.sortMaps();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder sortMaps(boolean value) {
+ super.sortMaps(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder sortProperties() {
+ super.sortProperties();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder sortProperties(java.lang.Class<?>...on) {
+ super.sortProperties(on);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder sq() {
+ super.sq();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder stopClass(Class<?> on, Class<?> value) {
+ super.stopClass(on, value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder streamCharset(Charset value) {
+ super.streamCharset(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public <T,S> Builder swap(Class<T> normalClass, Class<S> swappedClass, ThrowingFunction<T,S> swapFunction) {
+ super.swap(normalClass, swappedClass, swapFunction);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public <T,S> Builder swap(Class<T> normalClass, Class<S> swappedClass, ThrowingFunction<T,S> swapFunction, ThrowingFunction<S,T> unswapFunction) {
+ super.swap(normalClass, swappedClass, swapFunction, unswapFunction);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder swaps(Class<?>...values) {
+ super.swaps(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder swaps(Object...values) {
+ super.swaps(values);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder timeZone(TimeZone value) {
+ super.timeZone(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder trimEmptyCollections() {
+ super.trimEmptyCollections();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder trimEmptyCollections(boolean value) {
+ super.trimEmptyCollections(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder trimEmptyMaps() {
+ super.trimEmptyMaps();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder trimEmptyMaps(boolean value) {
+ super.trimEmptyMaps(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder trimStrings() {
+ super.trimStrings();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder trimStrings(boolean value) {
+ super.trimStrings(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder type(Class<? extends org.apache.juneau.Context> value) {
+ super.type(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder typeName(Class<?> on, String value) {
+ super.typeName(on, value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder typePropertyName(Class<?> on, String value) {
+ super.typePropertyName(on, value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder typePropertyName(String value) {
+ super.typePropertyName(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder uriContext(UriContext value) {
+ super.uriContext(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder uriRelativity(UriRelativity value) {
+ super.uriRelativity(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder uriResolution(UriResolution value) {
+ super.uriResolution(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder useEnumNames() {
+ super.useEnumNames();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder useJavaBeanIntrospector() {
+ super.useJavaBeanIntrospector();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder useWhitespace() {
+ super.useWhitespace();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder useWhitespace(boolean value) {
+ super.useWhitespace(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder ws() {
+ super.ws();
+ return this;
+ }
+ }
+
+ /** Default serializer, with whitespace. */
+ public static class Readable extends YamlSerializer {
+
+ /**
+ * Constructor.
+ *
+ * @param builder The builder for this object.
+ */
+ public Readable(Builder builder) {
+ super(builder.useWhitespace());
+ }
+ }
+
+ /** Default serializer, all default settings. */
+ public static final YamlSerializer DEFAULT = new YamlSerializer(create());
+
+ /** Default serializer, with whitespace. */
+ public static final YamlSerializer DEFAULT_READABLE = new Readable(create());
+
+ /**
+ * Creates a new builder for this object.
+ *
+ * @return A new builder.
+ */
+ public static Builder create() {
+ return new Builder();
+ }
+
+ protected final boolean addBeanTypesYaml;
+
+ private final boolean addBeanTypes2;
+
+ /**
+ * Constructor.
+ *
+ * @param builder The builder for this object.
+ */
+ public YamlSerializer(Builder builder) {
+ super(builder);
+ addBeanTypesYaml = builder.addBeanTypesYaml;
+ addBeanTypes2 = addBeanTypesYaml || super.isAddBeanTypes();
+ }
+
+ @Override /* Overridden from Context */
+ public Builder copy() {
+ return new Builder(this);
+ }
+
+ @Override /* Overridden from Context */
+ public YamlSerializerSession.Builder createSession() {
+ return YamlSerializerSession.create(this);
+ }
+
+ @Override /* Overridden from Context */
+ public YamlSerializerSession getSession() { return createSession().build(); }
+
+ /**
+ * Add <js>"_type"</js> properties when needed.
+ *
+ * @see Builder#addBeanTypesYaml()
+ * @return
+ * <jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred
+ * through reflection.
+ */
+ @Override
+ protected final boolean isAddBeanTypes() { return addBeanTypes2; }
+
+ @Override /* Overridden from WriterSerializer */
+ protected FluentMap<String,Object> properties() {
+ return super.properties()
+ .a(PROP_addBeanTypesYaml, addBeanTypesYaml);
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializerSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializerSession.java
new file mode 100644
index 0000000000..bd9ea06ae7
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializerSession.java
@@ -0,0 +1,453 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.juneau.yaml;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.IoUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.nio.charset.*;
+import java.util.*;
+import java.util.function.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.lang.*;
+import org.apache.juneau.httppart.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.svl.*;
+
+/**
+ * Session object that lives for the duration of a single use of {@link YamlSerializer}.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='warn'>This class is not thread safe and is typically discarded after one use.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/YamlBasics">YAML Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+ "resource", // Resource management handled externally
+ "java:S110", // Inheritance depth acceptable for this class hierarchy
+ "java:S115" // Constants use UPPER_snakeCase naming convention
+})
+public class YamlSerializerSession extends WriterSerializerSession {
+
+ // Argument name constants for assertArgNotNull
+ private static final String ARG_ctx = "ctx";
+
+ /**
+ * Builder class.
+ */
+ public static class Builder extends WriterSerializerSession.Builder {
+
+ private YamlSerializer ctx;
+
+ /**
+ * Constructor
+ *
+ * @param ctx The context creating this session.
+ * <br>Cannot be <jk>null</jk>.
+ */
+ protected Builder(YamlSerializer ctx) {
+ super(assertArgNotNull(ARG_ctx, ctx));
+ this.ctx = ctx;
+ }
+
+ @Override /* Overridden from Builder */
+ public <T> Builder apply(Class<T> type, Consumer<T> apply) {
+ super.apply(type, apply);
+ return this;
+ }
+
+ @Override
+ public YamlSerializerSession build() {
+ return new YamlSerializerSession(this);
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder debug(Boolean value) {
+ super.debug(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder fileCharset(Charset value) {
+ super.fileCharset(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder javaMethod(Method value) {
+ super.javaMethod(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder locale(Locale value) {
+ super.locale(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder mediaType(MediaType value) {
+ super.mediaType(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder mediaTypeDefault(MediaType value) {
+ super.mediaTypeDefault(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder properties(Map<String,Object> value) {
+ super.properties(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder property(String key, Object value) {
+ super.property(key, value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder resolver(VarResolverSession value) {
+ super.resolver(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder schema(HttpPartSchema value) {
+ super.schema(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder schemaDefault(HttpPartSchema value) {
+ super.schemaDefault(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder streamCharset(Charset value) {
+ super.streamCharset(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder timeZone(TimeZone value) {
+ super.timeZone(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder timeZoneDefault(TimeZone value) {
+ super.timeZoneDefault(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder unmodifiable() {
+ super.unmodifiable();
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder uriContext(UriContext value) {
+ super.uriContext(value);
+ return this;
+ }
+
+ @Override /* Overridden from Builder */
+ public Builder useWhitespace(Boolean value) {
+ super.useWhitespace(value);
+ return this;
+ }
+ }
+
+ /**
+ * Creates a new builder for this object.
+ *
+ * @param ctx The context creating this session.
+ * <br>Cannot be <jk>null</jk>.
+ * @return A new builder.
+ */
+ public static Builder create(YamlSerializer ctx) {
+ return new Builder(assertArgNotNull(ARG_ctx, ctx));
+ }
+
+ private final YamlSerializer ctx;
+
+ /**
+ * Constructor.
+ *
+ * @param builder The builder for this object.
+ */
+ protected YamlSerializerSession(Builder builder) {
+ super(builder);
+ this.ctx = builder.ctx;
+ }
+
+ @Override /* Overridden from SerializerSession */
+ protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException {
+ serializeAnything(getYamlWriter(out), o, getExpectedRootType(o), "root", null, true);
+ }
+
+ /**
+ * Converts the specified output target object to a {@link YamlWriter}.
+ *
+ * @param out The output target object.
+ * @return The output target object wrapped in a {@link YamlWriter}.
+ */
+ protected final YamlWriter getYamlWriter(SerializerPipe out) {
+ var output = out.getRawOutput();
+ if (output instanceof YamlWriter output2)
+ return output2;
+ var w = new YamlWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), isTrimStrings(), getUriResolver());
+ out.setWriter(w);
+ return w;
+ }
+
+ /**
+ * Workhorse method.
+ * Determines the type of object, and then calls the appropriate type-specific serialization method.
+ *
+ * @param out The output writer.
+ * @param o The object to serialize.
+ * @param eType The expected type.
+ * @param attrName The attribute name.
+ * @param pMeta The bean property currently being parsed.
+ * @param asField If <jk>true</jk>, this value appears inline after a key or at root level.
+ * If <jk>false</jk>, this value needs its own indentation as a standalone block.
+ * @return The same writer passed in.
+ * @throws SerializeException General serialization error occurred.
+ */
+ @SuppressWarnings({
+ "rawtypes", // Raw types necessary for generic type handling
+ "java:S3776", // Cognitive complexity acceptable for this specific logic
+ })
+ protected YamlWriter serializeAnything(YamlWriter out, Object o, ClassMeta<?> eType, String attrName, BeanPropertyMeta pMeta, boolean asField) throws SerializeException {
+
+ if (o == null) {
+ out.append("null");
+ return out;
+ }
+
+ if (eType == null)
+ eType = object();
+
+ ClassMeta<?> aType;
+ ClassMeta<?> sType;
+
+ aType = push2(attrName, o, eType);
+ boolean isRecursion = aType == null;
+
+ if (aType == null) {
+ o = null;
+ aType = object();
+ }
+
+ if (isOptional(aType)) {
+ o = getOptionalValue(o);
+ eType = getOptionalType(eType);
+ aType = getClassMetaForObject(o, object());
+ }
+
+ sType = aType;
+ String typeName = getBeanTypeName(this, eType, aType, pMeta);
+
+ var swap = aType.getSwap(this);
+ if (nn(swap)) {
+ o = swap(swap, o);
+ sType = swap.getSwapClassMeta(this);
+
+ if (sType.isObject())
+ sType = getClassMetaForObject(o);
+ }
+
+ if (o == null || (sType.isChar() && ((Character)o).charValue() == 0)) {
+ out.append("null");
+ } else if (sType.isNumber() || sType.isBoolean()) {
+ out.append(o);
+ } else if (sType.isBean()) {
+ serializeBeanMap(out, toBeanMap(o), typeName);
+ } else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) {
+ out.uriValue(o);
+ } else if (sType.isMap()) {
+ if (o instanceof BeanMap o2)
+ serializeBeanMap(out, o2, typeName);
+ else
+ serializeMap(out, (Map)o, eType);
+ } else if (sType.isCollection()) {
+ serializeCollection(out, (Collection)o, eType, asField);
+ } else if (sType.isArray()) {
+ serializeCollection(out, toList(sType.inner(), o), eType, asField);
+ } else if (sType.isReader()) {
+ pipe((Reader)o, out, SerializerSession::handleThrown);
+ } else if (sType.isInputStream()) {
+ pipe((InputStream)o, out, SerializerSession::handleThrown);
+ } else {
+ out.textValue(toString(o));
+ }
+
+ if (! isRecursion)
+ pop();
+ return out;
+ }
+
+ /**
+ * Determines whether a ClassMeta represents a complex type that requires block-style YAML formatting.
+ */
+ private boolean isComplexValue(ClassMeta<?> cm) {
+ if (cm == null)
+ return false;
+ if (cm.isMapOrBean() || cm.isCollectionOrArrayOrOptional())
+ return true;
+ var swap = cm.getSwap(this);
+ if (swap != null) {
+ var swapType = swap.getSwapClassMeta(this);
+ return swapType.isMapOrBean() || swapType.isCollectionOrArrayOrOptional();
+ }
+ return false;
+ }
+
+ private SerializerWriter serializeBeanMap(YamlWriter out, BeanMap<?> m, String typeName) throws SerializeException {
+ int i = indent - 1;
+
+ var addAtt = Flag.create();
+ Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x);
+
+ if (nn(typeName)) {
+ BeanPropertyMeta pm = m.getMeta().getTypeProperty();
+ out.key(pm.getName()).textValue(typeName);
+ addAtt.set();
+ }
+
+ m.forEachValue(checkNull, (pMeta, key, value, thrown) -> {
+ var cMeta = pMeta.getClassMeta();
+ if (nn(thrown))
+ onBeanGetterException(pMeta, thrown);
+
+ if (canIgnoreValue(cMeta, key, value))
+ return;
+
+ addAtt.ifSet(() -> out.yamlIndent(i)).set();
+
+ ClassMeta<?> actualType = value == null ? cMeta : getClassMetaForObject(value, cMeta);
+ if (value != null && isComplexValue(actualType)) {
+ out.keyName(key).w(": ");
+ if (actualType.isCollectionOrArrayOrOptional()) {
+ serializeAnything(out, value, cMeta, key, pMeta, false);
+ } else {
+ out.yamlIndent(i + 1);
+ serializeAnything(out, value, cMeta, key, pMeta, true);
+ }
+ } else {
+ out.key(key);
+ serializeAnything(out, value, cMeta, key, pMeta, true);
+ }
+ });
+
+ if (addAtt.isUnset())
+ out.w("{}");
+
+ return out;
+ }
+
+ @SuppressWarnings({
+ "rawtypes", // Raw types necessary for generic collection/map serialization
+ "unchecked", // Type erasure requires unchecked casts in collection/map serialization
+ })
+ private SerializerWriter serializeMap(YamlWriter out, Map m, ClassMeta<?> type) throws SerializeException {
+ int i = indent - 1;
+
+ var keyType = type.getKeyType();
+ var valueType = type.getValueType();
+
+ if (m.isEmpty()) {
+ out.w("{}");
+ return out;
+ }
+
+ var addAtt = Flag.create();
+ forEachEntry(m, x -> {
+ addAtt.ifSet(() -> out.yamlIndent(i)).set();
+ Object value = x.getValue();
+ Object key = generalize(x.getKey(), keyType);
+
+ ClassMeta<?> actualType = value == null ? valueType : getClassMetaForObject(value, valueType);
+ if (value != null && isComplexValue(actualType)) {
+ out.keyName(toString(key)).w(": ");
+ if (actualType.isCollectionOrArrayOrOptional()) {
+ serializeAnything(out, value, valueType, (key == null ? null : toString(key)), null, false);
+ } else {
+ out.yamlIndent(i + 1);
+ serializeAnything(out, value, valueType, (key == null ? null : toString(key)), null, true);
+ }
+ } else {
+ out.key(toString(key));
+ serializeAnything(out, value, valueType, (key == null ? null : toString(key)), null, true);
+ }
+ });
+
+ return out;
+ }
+
+ @SuppressWarnings({
+ "rawtypes", // Raw types necessary for generic collection/map serialization
+ "unchecked", // Type erasure requires unchecked casts in collection/map serialization
+ })
+ private SerializerWriter serializeCollection(YamlWriter out, Collection c, ClassMeta<?> type, boolean asField) throws SerializeException {
+ int i = indent - 1;
+
+ if (c.isEmpty()) {
+ out.w("[]");
+ return out;
+ }
+
+ var elementType = type.getElementType();
+
+ var addEntry = Flag.create();
+ forEachEntry(c, x -> {
+ if (asField && addEntry.isUnset()) {
+ out.w("- ");
+ } else {
+ out.listEntry(i);
+ }
+ addEntry.set();
+ serializeAnything(out, x, elementType, "<iterator>", null, true);
+ });
+
+ return out;
+ }
+
+ /**
+ * Add <js>"_type"</js> properties when needed.
+ *
+ * @return
+ * <jk>true</jk> if <js>"_type"</js> properties will be added to beans if their type cannot be inferred
+ * through reflection.
+ */
+ @Override
+ protected final boolean isAddBeanTypes() { return ctx.isAddBeanTypes(); }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlWriter.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlWriter.java
new file mode 100644
index 0000000000..b2e94a11c7
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlWriter.java
@@ -0,0 +1,409 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.juneau.yaml;
+
+import static org.apache.juneau.commons.utils.StringUtils.*;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.lang.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * Specialized writer for serializing YAML.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>
+ * This class is not intended for external use.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/YamlBasics">YAML Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+ "resource" // Writer resource managed by calling code
+})
+public class YamlWriter extends SerializerWriter {
+
+ private static final AsciiSet specialChars = AsciiSet.of(":#{}[],&*!|>'\"%@`");
+
+ // @formatter:off
+ private static final Set<String> reservedWords = new HashSet<>(Arrays.asList(
+ "null", "Null", "NULL", "~",
+ "true", "True", "TRUE",
+ "false", "False", "FALSE",
+ "yes", "Yes", "YES",
+ "no", "No", "NO",
+ "on", "On", "ON",
+ "off", "Off", "OFF",
+ ".inf", ".Inf", ".INF",
+ "-.inf", "-.Inf", "-.INF",
+ ".nan", ".NaN", ".NAN"
+ ));
+ // @formatter:on
+
+ private static final AsciiSet escapedChars = AsciiSet.of("\\\"\n\t\r\0\u0007\b\f\u001b");
+
+ /**
+ * Constructor.
+ *
+ * @param out The writer being wrapped.
+ * @param useWhitespace If <jk>true</jk>, tabs and spaces will be used in output.
+ * @param maxIndent The maximum indentation level.
+ * @param trimStrings If <jk>true</jk>, strings will be trimmed before being serialized.
+ * @param uriResolver The URI resolver for resolving URIs to absolute or root-relative form.
+ */
+ protected YamlWriter(Writer out, boolean useWhitespace, int maxIndent, boolean trimStrings, UriResolver uriResolver) {
+ super(out, useWhitespace, maxIndent, trimStrings, '"', uriResolver);
+ }
+
+ /**
+ * Writes a YAML mapping key.
+ *
+ * <p>
+ * If the key needs quoting (contains special YAML characters, starts/ends with whitespace,
+ * is empty, or is a YAML reserved word), it will be double-quoted with proper escaping.
+ * A colon-space separator is always appended after the key.
+ *
+ * @param s The key name.
+ * @return This object.
+ */
+ public YamlWriter key(String s) {
+ keyName(s);
+ w(": ");
+ return this;
+ }
+
+ /**
+ * Writes just the key name portion (with quoting if necessary) but without any separator.
+ *
+ * @param s The key name.
+ * @return This object.
+ */
+ public YamlWriter keyName(String s) {
+ if (trimStrings)
+ s = trim(s);
+
+ if (s == null) {
+ w("null");
+ } else if (needsQuoting(s)) {
+ writeQuoted(s);
+ } else {
+ w(s);
+ }
+ return this;
+ }
+
+ /**
+ * Writes a YAML scalar text value.
+ *
+ * <p>
+ * If null, writes the literal <c>null</c>. If the string needs quoting (is empty,
+ * looks like a boolean/null/number, contains special characters, starts/ends with whitespace,
+ * or contains newlines), it will be double-quoted with proper YAML escaping.
+ *
+ * @param s The string value.
+ * @return This object.
+ */
+ public YamlWriter textValue(String s) {
+ if (trimStrings)
+ s = trim(s);
+
+ if (s == null) {
+ w("null");
+ } else if (needsQuoting(s)) {
+ writeQuoted(s);
+ } else {
+ w(s);
+ }
+ return this;
+ }
+
+ /**
+ * Appends a URI value to the output.
+ *
+ * <p>
+ * Resolves the URI and writes it as a double-quoted YAML string.
+ *
+ * @param uri The URI to append to the output.
+ * @return This object.
+ */
+ public YamlWriter uriValue(Object uri) {
+ writeQuoted(uriResolver.resolve(uri));
+ return this;
+ }
+
+ /**
+ * Writes YAML block indentation.
+ *
+ * <p>
+ * Writes a newline followed by {@code depth * 2} spaces of indentation.
+ *
+ * @param depth The indentation depth.
+ * @return This object.
+ */
+ public YamlWriter yamlIndent(int depth) {
+ w('\n');
+ for (var i = 0; i < depth * 2; i++)
+ w(' ');
+ return this;
+ }
+
+ /**
+ * Writes a YAML list entry prefix.
+ *
+ * <p>
+ * Writes a newline followed by {@code depth * 2} spaces and a dash-space (<js>"- "</js>) prefix.
+ *
+ * @param depth The indentation depth.
+ * @return This object.
+ */
+ public YamlWriter listEntry(int depth) {
+ w('\n');
+ for (var i = 0; i < depth * 2; i++)
+ w(' ');
+ w("- ");
+ return this;
+ }
+
+ /**
+ * Returns <jk>true</jk> if the specified string needs double-quoting in YAML.
+ *
+ * @param s The string to check.
+ * @return <jk>true</jk> if the string needs quoting.
+ */
+ @SuppressWarnings({
+ "java:S3776" // Cognitive complexity acceptable for YAML quoting checks
+ })
+ private boolean needsQuoting(String s) {
+ if (s.isEmpty())
+ return true;
+
+ if (Character.isWhitespace(s.charAt(0)) || Character.isWhitespace(s.charAt(s.length() - 1)))
+ return true;
+
+ if (reservedWords.contains(s))
+ return true;
+
+ if (looksLikeNumber(s))
+ return true;
+
+ for (var i = 0; i < s.length(); i++) {
+ var c = s.charAt(i);
+ if (c == '\n' || c == '\r')
+ return true;
+ if (specialChars.contains(c))
+ return true;
+ if (escapedChars.contains(c))
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns <jk>true</jk> if the string looks like a YAML number (integer, float, hex, or octal).
+ */
+ private static boolean looksLikeNumber(String s) {
+ if (s.isEmpty())
+ return false;
+ var c0 = s.charAt(0);
+ if (c0 == '+' || c0 == '-')
+ return s.length() > 1 && isDigitOrDot(s.charAt(1));
+ if (c0 == '.')
+ return s.length() > 1 && Character.isDigit(s.charAt(1));
+ if (c0 == '0' && s.length() > 1) {
+ var c1 = s.charAt(1);
+ if (c1 == 'x' || c1 == 'X' || c1 == 'o' || c1 == 'O')
+ return true;
+ }
+ return Character.isDigit(c0);
+ }
+
+ private static boolean isDigitOrDot(char c) {
+ return Character.isDigit(c) || c == '.';
+ }
+
+ /**
+ * Writes a double-quoted YAML string with proper escaping.
+ */
+ private void writeQuoted(String s) {
+ w('"');
+ if (s != null) {
+ for (var i = 0; i < s.length(); i++) {
+ var c = s.charAt(i);
+ if (escapedChars.contains(c)) {
+ switch (c) {
+ case '\\': w('\\').w('\\'); break;
+ case '"': w('\\').w('"'); break;
+ case '\n': w('\\').w('n'); break;
+ case '\t': w('\\').w('t'); break;
+ case '\r': w('\\').w('r'); break;
+ case '\0': w('\\').w('0'); break;
+ case '\u0007': w('\\').w('a'); break;
+ case '\b': w('\\').w('b'); break;
+ case '\f': w('\\').w('f'); break;
+ case '\u001b': w('\\').w('e'); break;
+ default: w(c);
+ }
+ } else {
+ w(c);
+ }
+ }
+ }
+ w('"');
+ }
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // Overridden methods - return type covariance for chaining
+ //-----------------------------------------------------------------------------------------------------------------
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter append(char c) {
+ super.append(c);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter append(char[] value) {
+ super.append(value);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter append(int indent, char c) {
+ super.append(indent, c);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter append(int indent, String text) {
+ super.append(indent, text);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter append(Object text) {
+ super.append(text);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter append(String text) {
+ super.append(text);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter appendIf(boolean b, char c) {
+ super.appendIf(b, c);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter appendIf(boolean b, String text) {
+ super.appendIf(b, text);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter appendln(int indent, String text) {
+ super.appendln(indent, text);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter appendln(String text) {
+ super.appendln(text);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter appendUri(Object value) {
+ super.appendUri(value);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter cr(int depth) {
+ super.cr(depth);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter cre(int depth) {
+ super.cre(depth);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter i(int indent) {
+ super.i(indent);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter ie(int indent) {
+ super.ie(indent);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter nl(int indent) {
+ super.nl(indent);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter nlIf(boolean flag, int indent) {
+ super.nlIf(flag, indent);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter q() {
+ super.q();
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter s() {
+ super.s();
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter sIf(boolean flag) {
+ super.sIf(flag);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter w(char value) {
+ super.w(value);
+ return this;
+ }
+
+ @Override /* Overridden from SerializerWriter */
+ public YamlWriter w(String value) {
+ super.w(value);
+ return this;
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/annotation/YamlConfig.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/annotation/YamlConfig.java
new file mode 100644
index 0000000000..8ec65cadbf
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/annotation/YamlConfig.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.juneau.yaml.annotation;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+
+import java.lang.annotation.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.yaml.*;
+
+/**
+ * Annotation for specifying config properties defined in {@link YamlSerializer} and {@link YamlParser}.
+ *
+ * <p>
+ * Used primarily for specifying bean configuration properties on REST classes and methods.
+ */
+@Target({ TYPE, METHOD })
+@Retention(RUNTIME)
+@Inherited
+@ContextApply({ YamlConfigAnnotation.SerializerApply.class, YamlConfigAnnotation.ParserApply.class })
+public @interface YamlConfig {
+
+ /**
+ * Add <js>"_type"</js> properties when needed.
+ *
+ * <p>
+ * If <js>"true"</js>, then <js>"_type"</js> properties will be added to beans if their type cannot be inferred
+ * through reflection.
+ *
+ * <p>
+ * When present, this value overrides the {@link org.apache.juneau.serializer.Serializer.Builder#addBeanTypes()} setting and is
+ * provided to customize the behavior of specific serializers in a {@link SerializerSet}.
+ *
+ * <ul class='values'>
+ * <li><js>"true"</js>
+ * <li><js>"false"</js> (default)
+ * </ul>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>
+ * Supports <a class="doclink" href="https://juneau.apache.org/docs/topics/DefaultVarResolver">VarResolver.DEFAULT</a> (e.g. <js>"$C{myConfigVar}"</js>).
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='jm'>{@link org.apache.juneau.yaml.YamlSerializer.Builder#addBeanTypesYaml()}
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String addBeanTypes() default "";
+
+ /**
+ * Optional rank for this config.
+ *
+ * <p>
+ * Can be used to override default ordering and application of config annotations.
+ *
+ * @return The annotation value.
+ */
+ int rank() default 0;
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/annotation/YamlConfigAnnotation.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/annotation/YamlConfigAnnotation.java
new file mode 100644
index 0000000000..f86ada422f
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/annotation/YamlConfigAnnotation.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.juneau.yaml.annotation;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.svl.*;
+import org.apache.juneau.yaml.*;
+
+/**
+ * Utility classes and methods for the {@link YamlConfig @YamlConfig} annotation.
+ */
+public class YamlConfigAnnotation {
+
+ private YamlConfigAnnotation() {}
+
+ /**
+ * Applies {@link YamlConfig} annotations to a {@link org.apache.juneau.yaml.YamlParser.Builder}.
+ */
+ public static class ParserApply extends AnnotationApplier<YamlConfig,YamlParser.Builder> {
+
+ /**
+ * Constructor.
+ *
+ * @param vr The resolver for resolving values in annotations.
+ */
+ public ParserApply(VarResolverSession vr) {
+ super(YamlConfig.class, YamlParser.Builder.class, vr);
+ }
+
+ @Override
+ public void apply(AnnotationInfo<YamlConfig> ai, YamlParser.Builder b) {
+ // No parser-specific properties yet.
+ }
+ }
+
+ /**
+ * Applies {@link YamlConfig} annotations to a {@link org.apache.juneau.yaml.YamlSerializer.Builder}.
+ */
+ public static class SerializerApply extends AnnotationApplier<YamlConfig,YamlSerializer.Builder> {
+
+ /**
+ * Constructor.
+ *
+ * @param vr The resolver for resolving values in annotations.
+ */
+ public SerializerApply(VarResolverSession vr) {
+ super(YamlConfig.class, YamlSerializer.Builder.class, vr);
+ }
+
+ @Override
+ public void apply(AnnotationInfo<YamlConfig> ai, YamlSerializer.Builder b) {
+ YamlConfig a = ai.inner();
+
+ bool(a.addBeanTypes()).ifPresent(b::addBeanTypesYaml);
+ }
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/annotation/package-info.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/annotation/package-info.java
new file mode 100644
index 0000000000..0565eae630
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/annotation/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+/**
+ * YAML Annotations
+ */
+package org.apache.juneau.yaml.annotation;
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/package-info.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/package-info.java
new file mode 100644
index 0000000000..4fc497904f
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+/**
+ * YAML Marshalling Support
+ */
+package org.apache.juneau.yaml;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicUniversalConfig.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicUniversalConfig.java
index b82b79ef2d..d0677ade9d 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicUniversalConfig.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicUniversalConfig.java
@@ -28,6 +28,7 @@ import org.apache.juneau.rest.annotation.*;
import org.apache.juneau.rest.servlet.*;
import org.apache.juneau.serializer.annotation.*;
import org.apache.juneau.soap.*;
+import org.apache.juneau.yaml.*;
import org.apache.juneau.uon.*;
import org.apache.juneau.urlencoding.*;
import org.apache.juneau.xml.*;
@@ -145,7 +146,8 @@ import org.apache.juneau.xml.*;
MsgPackSerializer.class,
SoapXmlSerializer.class,
PlainTextSerializer.class,
- CsvSerializer.class
+ CsvSerializer.class,
+ YamlSerializer.class
},
// Default parsers for all Java methods in the class.
@@ -159,7 +161,8 @@ import org.apache.juneau.xml.*;
OpenApiParser.class,
MsgPackParser.class,
PlainTextParser.class,
- CsvParser.class
+ CsvParser.class,
+ YamlParser.class
}
)
public interface BasicUniversalConfig extends DefaultConfig, DefaultHtmlConfig {}
\ No newline at end of file
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicYamlConfig.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicYamlConfig.java
new file mode 100644
index 0000000000..47bc301728
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/config/BasicYamlConfig.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.juneau.rest.config;
+
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.yaml.*;
+
+/**
+ * Basic configuration for a REST resource that supports YAML transport.
+ *
+ * <p>
+ * Default settings defined:
+ * </p>
+ * <ul class='spaced-list'>
+ * <li class='ja'>{@link Rest}:
+ * <ul>
+ * <li class='jma'>{@link Rest#serializers() serializers}:
+ * <ul class='javatree'>
+ * <li class='jc'>{@link YamlSerializer}
+ * </ul>
+ * </li>
+ * <li class='jma'>{@link Rest#parsers() parsers}:
+ * <ul class='javatree'>
+ * <li class='jc'>{@link YamlParser}
+ * </ul>
+ * </li>
+ * <li class='jma'>{@link Rest#defaultAccept() defaultAccept}: <js>"application/yaml"</js>
+ * </ul>
+ * </li>
+ * </ul>
+ *
+ * <p>
+ * This annotation can be applied to REST resource classes to define common YAML default configurations:
+ * </p>
+ * <p class='bjava'>
+ * <jc>// Used on a top-level resource.</jc>
+ * <ja>@Rest</ja>
+ * <jk>public class</jk> MyResource <jk>extends</jk> RestServlet <jk>implements</jk> BasicYamlConfig { ... }
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestAnnotatedClassBasics">@Rest-Annotated Class Basics</a>
+ * </ul>
+ */
+// @formatter:off
+@Rest(
+
+ // Default serializers for all Java methods in the class.
+ serializers={
+ YamlSerializer.class,
+ },
+
+ // Default parsers for all Java methods in the class.
+ parsers={
+ YamlParser.class,
+ },
+
+ defaultAccept="application/yaml"
+)
+public interface BasicYamlConfig extends DefaultConfig {}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java b/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
index f55010f7a9..385049bb9e 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTripTest_Base.java
@@ -440,4 +440,70 @@ public abstract class ComboRoundTripTest_Base extends TestBase {
public void f24_verifyMsgPackT(ComboRoundTrip_Tester<?> t) throws Exception {
t.testParseVerify("msgPackT");
}
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // YAML
+ //-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void g11_serializeYaml(ComboRoundTrip_Tester<?> t) throws Exception {
+ t.testSerialize("yaml");
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void g12_parseYaml(ComboRoundTrip_Tester<?> t) throws Exception {
+ t.testParse("yaml");
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void g13_verifyYaml(ComboRoundTrip_Tester<?> t) throws Exception {
+ t.testParseVerify("yaml");
+ }
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // YAML - 't' property
+ //-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void g21_serializeYamlT(ComboRoundTrip_Tester<?> t) throws Exception {
+ t.testSerialize("yamlT");
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void g22_parseYamlT(ComboRoundTrip_Tester<?> t) throws Exception {
+ t.testParse("yamlT");
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void g23_verifyYamlT(ComboRoundTrip_Tester<?> t) throws Exception {
+ t.testParseVerify("yamlT");
+ }
+
+ //-----------------------------------------------------------------------------------------------------------------
+ // YAML - Readable
+ //-----------------------------------------------------------------------------------------------------------------
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void g31_serializeYamlR(ComboRoundTrip_Tester<?> t) throws Exception {
+ t.testSerialize("yamlR");
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void g32_parseYamlR(ComboRoundTrip_Tester<?> t) throws Exception {
+ t.testParse("yamlR");
+ }
+
+ @ParameterizedTest
+ @MethodSource("testers")
+ public void g33_verifyYamlR(ComboRoundTrip_Tester<?> t) throws Exception {
+ t.testParseVerify("yamlR");
+ }
}
\ No newline at end of file
diff --git a/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java b/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
index 551b0fa926..b51ab17fba 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/ComboRoundTrip_Tester.java
@@ -34,6 +34,7 @@ import org.apache.juneau.serializer.*;
import org.apache.juneau.uon.*;
import org.apache.juneau.urlencoding.*;
import org.apache.juneau.xml.*;
+import org.apache.juneau.yaml.*;
/**
* Represents the input to a ComboTest.
@@ -115,6 +116,9 @@ public class ComboRoundTrip_Tester<T> {
public Builder<T> rdfXml(String value) { expected.put("rdfXml", value); return this; }
public Builder<T> rdfXmlT(String value) { expected.put("rdfXmlT", value); return this; }
public Builder<T> rdfXmlR(String value) { expected.put("rdfXmlR", value); return this; }
+ public Builder<T> yaml(String value) { expected.put("yaml", value); return this; }
+ public Builder<T> yamlT(String value) { expected.put("yamlT", value); return this; }
+ public Builder<T> yamlR(String value) { expected.put("yamlR", value); return this; }
public ComboRoundTrip_Tester<T> build() {
return new ComboRoundTrip_Tester<>(this);
@@ -160,6 +164,9 @@ public class ComboRoundTrip_Tester<T> {
serializers.put("urlEncR", create(b, UrlEncodingSerializer.DEFAULT_READABLE.copy().addBeanTypes().addRootType()));
serializers.put("msgPack", create(b, MsgPackSerializer.create().addBeanTypes().addRootType()));
serializers.put("msgPackT", create(b, MsgPackSerializer.create().typePropertyName("t").addBeanTypes().addRootType()));
+ serializers.put("yaml", create(b, YamlSerializer.DEFAULT.copy().addBeanTypes().addRootType()));
+ serializers.put("yamlT", create(b, YamlSerializer.create().typePropertyName("t").addBeanTypes().addRootType()));
+ serializers.put("yamlR", create(b, YamlSerializer.DEFAULT_READABLE.copy().addBeanTypes().addRootType()));
parsers.put("json", create(b, JsonParser.DEFAULT.copy()));
parsers.put("jsonT", create(b, JsonParser.create().typePropertyName("t")));
@@ -179,6 +186,9 @@ public class ComboRoundTrip_Tester<T> {
parsers.put("urlEncR", create(b, UrlEncodingParser.DEFAULT.copy()));
parsers.put("msgPack", create(b, MsgPackParser.DEFAULT.copy()));
parsers.put("msgPackT", create(b, MsgPackParser.create().typePropertyName("t")));
+ parsers.put("yaml", create(b, YamlParser.DEFAULT.copy()));
+ parsers.put("yamlT", create(b, YamlParser.create().typePropertyName("t")));
+ parsers.put("yamlR", create(b, YamlParser.DEFAULT.copy()));
}
private Serializer create(Builder<?> tb, Serializer.Builder sb) {
@@ -215,7 +225,7 @@ public class ComboRoundTrip_Tester<T> {
}
private boolean isSkipped(String testName, String expected) {
- return "SKIP".equals(expected) || skipTest.test(testName);
+ return expected == null || "SKIP".equals(expected) || skipTest.test(testName);
}
public void testSerialize(String testName) throws Exception {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
index b1e6fd7c82..45ed8cce98 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripDateTime_Test.java
@@ -30,6 +30,7 @@ import org.apache.juneau.msgpack.*;
import org.apache.juneau.uon.*;
import org.apache.juneau.urlencoding.*;
import org.apache.juneau.xml.*;
+import org.apache.juneau.yaml.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
@@ -109,6 +110,10 @@ class RoundTripDateTime_Test extends TestBase {
.serializer(JsonSchemaSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.returnOriginalObject()
.build(),
+ tester(17, "Yaml - default")
+ .serializer(YamlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+ .parser(YamlParser.create())
+ .build(),
};
static RoundTrip_Tester[] testers() {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
index 13a6b2b06e..47a7142214 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
@@ -23,6 +23,7 @@ import org.apache.juneau.msgpack.*;
import org.apache.juneau.uon.*;
import org.apache.juneau.urlencoding.*;
import org.apache.juneau.xml.*;
+import org.apache.juneau.yaml.*;
/**
* Tests designed to serialize and parse objects to make sure we end up
@@ -101,6 +102,10 @@ public abstract class RoundTripTest_Base extends TestBase {
.serializer(JsonSchemaSerializer.create().keepNullProperties().addBeanTypes().addRootType())
.returnOriginalObject()
.build(),
+ tester(17, "Yaml - default")
+ .serializer(YamlSerializer.create().keepNullProperties().addBeanTypes().addRootType())
+ .parser(YamlParser.create())
+ .build(),
};
static RoundTrip_Tester[] testers() {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/yaml/YamlMarshaller_Test.java b/juneau-utest/src/test/java/org/apache/juneau/yaml/YamlMarshaller_Test.java
new file mode 100644
index 0000000000..5e8c8652cc
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/yaml/YamlMarshaller_Test.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.juneau.yaml;
+
+import static org.junit.jupiter.api.Assertions.*;
+import java.util.*;
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.apache.juneau.marshaller.*;
+import org.junit.jupiter.api.*;
+
+class YamlMarshaller_Test extends TestBase {
+
+ @Test void a01_writeAndRead() throws Exception {
+ var m = new LinkedHashMap<String,Object>();
+ m.put("a", "1");
+ m.put("b", 2);
+
+ String yaml = Yaml.of(m);
+ assertNotNull(yaml);
+
+ JsonMap parsed = Yaml.to(yaml, JsonMap.class);
+ assertEquals("1", parsed.getString("a"));
+ assertEquals(2, parsed.getInt("b"));
+ }
+
+ @Test void a02_roundTripString() throws Exception {
+ String original = "hello world";
+ String yaml = Yaml.of(original);
+ String parsed = Yaml.to(yaml, String.class);
+ assertEquals(original, parsed);
+ }
+
+ @Test void a03_roundTripNumber() throws Exception {
+ int original = 42;
+ String yaml = Yaml.of(original);
+ int parsed = Yaml.to(yaml, int.class);
+ assertEquals(original, parsed);
+ }
+
+ @Test void a04_roundTripBoolean() throws Exception {
+ String yaml = Yaml.of(true);
+ boolean parsed = Yaml.to(yaml, boolean.class);
+ assertTrue(parsed);
+ }
+
+ @Test void a05_roundTripNull() throws Exception {
+ String yaml = Yaml.of(null);
+ assertEquals("null", yaml);
+ assertNull(Yaml.to(yaml, String.class));
+ }
+
+ @Test void a06_roundTripList() throws Exception {
+ var original = List.of("a", "b", "c");
+ String yaml = Yaml.of(original);
+ JsonList parsed = Yaml.to(yaml, JsonList.class);
+ assertEquals(3, parsed.size());
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/yaml/YamlParser_Test.java b/juneau-utest/src/test/java/org/apache/juneau/yaml/YamlParser_Test.java
new file mode 100644
index 0000000000..883c217abf
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/yaml/YamlParser_Test.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.juneau.yaml;
+
+import static org.junit.jupiter.api.Assertions.*;
+import java.util.*;
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.junit.jupiter.api.*;
+
+class YamlParser_Test extends TestBase {
+
+ @Test void a01_parseString() throws Exception {
+ var p = YamlParser.DEFAULT;
+ assertEquals("hello", p.parse("hello", String.class));
+ }
+
+ @Test void a02_parseQuotedString() throws Exception {
+ var p = YamlParser.DEFAULT;
+ assertEquals("hello world", p.parse("\"hello world\"", String.class));
+ assertEquals("hello world", p.parse("'hello world'", String.class));
+ }
+
+ @Test void a03_parseNumber() throws Exception {
+ var p = YamlParser.DEFAULT;
+ assertEquals(123, p.parse("123", int.class));
+ assertEquals(1.5, p.parse("1.5", double.class));
+ }
+
+ @Test void a04_parseBoolean() throws Exception {
+ var p = YamlParser.DEFAULT;
+ assertEquals(true, p.parse("true", boolean.class));
+ assertEquals(false, p.parse("false", boolean.class));
+ }
+
+ @Test void a05_parseNull() throws Exception {
+ var p = YamlParser.DEFAULT;
+ assertNull(p.parse("null", String.class));
+ }
+
+ @Test void a06_parseFlowMapping() throws Exception {
+ var p = YamlParser.DEFAULT;
+ JsonMap m = p.parse("{a: 1, b: 2}", JsonMap.class);
+ assertEquals("1", m.getString("a"));
+ assertEquals("2", m.getString("b"));
+ }
+
+ @Test void a07_parseFlowSequence() throws Exception {
+ var p = YamlParser.DEFAULT;
+ JsonList l = p.parse("[1, 2, 3]", JsonList.class);
+ assertEquals(3, l.size());
+ }
+
+ @Test void a08_parseBlockMapping() throws Exception {
+ var p = YamlParser.DEFAULT;
+ JsonMap m = p.parse("a: 1\nb: 2", JsonMap.class);
+ assertEquals("1", m.getString("a"));
+ assertEquals("2", m.getString("b"));
+ }
+
+ @Test void a09_parseBlockSequence() throws Exception {
+ var p = YamlParser.DEFAULT;
+ JsonList l = p.parse("- a\n- b\n- c", JsonList.class);
+ assertEquals(3, l.size());
+ assertEquals("a", l.getString(0));
+ }
+
+ @Test void a10_parseNestedBlockMapping() throws Exception {
+ var p = YamlParser.DEFAULT;
+ JsonMap m = p.parse("outer:\n inner: value", JsonMap.class);
+ assertNotNull(m.get("outer"));
+ }
+
+ @Test void a11_parseComment() throws Exception {
+ var p = YamlParser.DEFAULT;
+ assertEquals("hello", p.parse("hello # this is a comment", String.class));
+ }
+
+ @Test void a12_parseEmptyInput() throws Exception {
+ var p = YamlParser.DEFAULT;
+ assertNull(p.parse("", String.class));
+ }
+
+ @Test void a13_parseTildeNull() throws Exception {
+ var p = YamlParser.DEFAULT;
+ assertNull(p.parse("~", String.class));
+ }
+
+ public static class DurationBean {
+ public java.time.Duration negative;
+ public java.time.Duration fractional;
+ public java.time.Duration large;
+ }
+
+ @Test void a14_parseBeanWithDuration() throws Exception {
+ var s = YamlSerializer.create().keepNullProperties().addBeanTypes().addRootType().build();
+ var p = YamlParser.DEFAULT;
+
+ var x = new DurationBean();
+ x.negative = java.time.Duration.ofHours(-6);
+ x.fractional = java.time.Duration.ofSeconds(20, 345000000);
+ x.large = java.time.Duration.ofDays(365);
+
+ String yaml = s.serialize(x);
+ var x2 = p.parse(yaml, DurationBean.class);
+ assertEquals(x.negative, x2.negative);
+ assertEquals(x.fractional, x2.fractional);
+ assertEquals(x.large, x2.large);
+ }
+
+ public static class ArrayBean {
+ public boolean[][] paBoolean;
+ }
+
+ @Test void a15_parse2dArrayBean() throws Exception {
+ var s = YamlSerializer.create().keepNullProperties().addBeanTypes().addRootType().build();
+ var p = YamlParser.DEFAULT;
+
+ var x = new ArrayBean();
+ x.paBoolean = new boolean[][]{{true, false}, {true}};
+
+ String yaml = s.serialize(x);
+ System.out.println("2D Array YAML:\n[" + yaml + "]");
+
+ var x2 = p.parse(yaml, ArrayBean.class);
+ assertEquals(true, x2.paBoolean[0][0]);
+ assertEquals(false, x2.paBoolean[0][1]);
+ assertEquals(true, x2.paBoolean[1][0]);
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/yaml/Yaml_Test.java b/juneau-utest/src/test/java/org/apache/juneau/yaml/Yaml_Test.java
new file mode 100644
index 0000000000..b818d1f6c9
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/yaml/Yaml_Test.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.juneau.yaml;
+
+import static org.junit.jupiter.api.Assertions.*;
+import java.util.*;
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.junit.jupiter.api.*;
+
+@SuppressWarnings({"serial"})
+class Yaml_Test extends TestBase {
+
+ @Test void a01_basicString() throws Exception {
+ var s = YamlSerializer.DEFAULT;
+ assertEquals("hello", s.serialize("hello"));
+ }
+
+ @Test void a02_basicNumber() throws Exception {
+ var s = YamlSerializer.DEFAULT;
+ assertEquals("123", s.serialize(123));
+ }
+
+ @Test void a03_basicBoolean() throws Exception {
+ var s = YamlSerializer.DEFAULT;
+ assertEquals("true", s.serialize(true));
+ assertEquals("false", s.serialize(false));
+ }
+
+ @Test void a04_basicNull() throws Exception {
+ var s = YamlSerializer.DEFAULT;
+ assertEquals("null", s.serialize(null));
+ }
+
+ @Test void a05_basicMap() throws Exception {
+ var s = YamlSerializer.DEFAULT;
+ var m = new LinkedHashMap<String,Object>();
+ m.put("a", "1");
+ m.put("b", 2);
+ // Map should serialize as: "a: '1'\nb: 2" (strings that look like numbers are quoted)
+ String result = s.serialize(m);
+ assertTrue(result.contains("a:"), "Expected 'a:' in: " + result);
+ assertTrue(result.contains("b:"), "Expected 'b:' in: " + result);
+ }
+
+ @Test void a06_basicList() throws Exception {
+ var s = YamlSerializer.DEFAULT;
+ var l = List.of("a", "b", "c");
+ String result = s.serialize(l);
+ assertTrue(result.contains("- a"), "Expected '- a' in: " + result);
+ assertTrue(result.contains("- b"), "Expected '- b' in: " + result);
+ }
+
+ @Test void a07_nestedMap() throws Exception {
+ var s = YamlSerializer.DEFAULT;
+ var inner = new LinkedHashMap<String,Object>();
+ inner.put("x", "1");
+ var outer = new LinkedHashMap<String,Object>();
+ outer.put("inner", inner);
+ String result = s.serialize(outer);
+ assertTrue(result.contains("inner:"), "Expected 'inner:' in: " + result);
+ assertTrue(result.contains("x:"), "Expected 'x:' in: " + result);
+ }
+
+ @Test void a08_mapWithList() throws Exception {
+ var s = YamlSerializer.DEFAULT;
+ var m = new LinkedHashMap<String,Object>();
+ m.put("name", "John");
+ m.put("hobbies", List.of("reading", "coding"));
+ String result = s.serialize(m);
+ assertTrue(result.contains("name: John"), "Expected 'name: John' in: " + result);
+ assertTrue(result.contains("- reading"), "Expected '- reading' in: " + result);
+ }
+
+ @Test void a09_specialCharQuoting() throws Exception {
+ var s = YamlSerializer.DEFAULT;
+ var m = new LinkedHashMap<String,Object>();
+ m.put("key", "value: with colon");
+ String result = s.serialize(m);
+ assertTrue(result.contains("\"value: with colon\"") || result.contains("'value: with colon'"),
+ "Special chars should be quoted in: " + result);
+ }
+
+ @Test void a10_reservedWords() throws Exception {
+ var s = YamlSerializer.DEFAULT;
+ var m = new LinkedHashMap<String,Object>();
+ m.put("val", "true");
+ String result = s.serialize(m);
+ assertTrue(result.contains("\"true\""), "Reserved word 'true' should be quoted in: " + result);
+ }
+
+ @Test void a11_emptyMap() throws Exception {
+ var s = YamlSerializer.DEFAULT;
+ var m = new LinkedHashMap<String,Object>();
+ String result = s.serialize(m);
+ assertNotNull(result);
+ }
+
+ @Test void a12_emptyList() throws Exception {
+ var s = YamlSerializer.DEFAULT;
+ var l = List.of();
+ String result = s.serialize(l);
+ assertNotNull(result);
+ }
+}