001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.marshall.yaml; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.io.*; 022 023import org.apache.juneau.commons.bean.*; 024import org.apache.juneau.commons.collections.*; 025import org.apache.juneau.marshall.*; 026import org.apache.juneau.marshall.json.*; 027import org.apache.juneau.marshall.parser.*; 028import org.apache.juneau.marshall.stream.*; 029 030/** 031 * Parses YAML text into a POJO model. 032 * 033 * <h5 class='topic'>Media types</h5> 034 * <p> 035 * Handles <c>Content-Type</c> types: <bc>application/yaml, text/yaml</bc> 036 * 037 * <h5 class='topic'>Description</h5> 038 * <p> 039 * This parser uses an indentation-aware state machine to parse YAML directly into POJOs without intermediate 040 * DOM objects. It supports block-style and flow-style mappings and sequences, plain and quoted scalars, 041 * comments, document markers, and standard YAML scalar types. 042 * 043 * <h5 class='section'>Limitations compared to JSON</h5> 044 * <p> 045 * The YAML parser has some limitations when compared to {@link JsonParser}: 046 * <ul class='spaced-list'> 047 * <li> 048 * Maps with non-String keys ({@link Boolean}, {@link java.util.Date}, {@link java.time.temporal.Temporal}, 049 * {@link Enum}) may not round-trip correctly when the target type is a generic {@link java.util.Map} with 050 * those key types. {@link java.util.LinkedHashMap LinkedHashMap} and {@link java.util.TreeMap TreeMap} 051 * with {@link String} keys work reliably. 052 * <li> 053 * No strict vs non-strict mode; unlike JSON, there is no equivalent to JSON's lax parsing of comments, 054 * unquoted attributes, or concatenated strings. 055 * <li> 056 * YAML's indentation-based structure requires consistent formatting; malformed indentation can cause 057 * parsing errors where equivalent JSON would parse successfully. 058 * </ul> 059 * 060 * <h5 class='figure'>Example input (Map of name/age):</h5> 061 * <p class='bcode'> 062 * name: Alice 063 * age: 30 064 * </p> 065 * 066 * <h5 class='figure'>Complex (nested object + array):</h5> 067 * <p class='bcode'> 068 * name: Alice 069 * age: 30 070 * address: 071 * street: 123 Main St 072 * city: Boston 073 * state: MA 074 * tags: 075 * - a 076 * - b 077 * - c 078 * </p> 079 * 080 * <h5 class='section'>Notes:</h5><ul> 081 * <li class='note'>This class is thread safe and reusable. 082 * </ul> 083 * 084 * <h5 class='section'>See Also:</h5><ul> 085 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/YamlSupport">YAML</a> 086 * </ul> 087 */ 088@SuppressWarnings({ 089 "java:S110", // Inheritance depth acceptable 090 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 091 "java:S9149", // Per-format static factories intentionally shadow the parent's. 092 "resource" // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design. 093}) 094public class YamlParser extends ReaderParser implements YamlMetaProvider, RecordReadable, ArrayRecordReadable { 095 096 private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>,YamlClassMeta> yamlClassMetas = new java.util.concurrent.ConcurrentHashMap<>(); 097 private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta,YamlBeanPropertyMeta> yamlBeanPropertyMetas = new java.util.concurrent.ConcurrentHashMap<>(); 098 099 /** 100 * Builder class. 101 */ 102 public static class Builder extends ReaderParser.Builder<Builder> { 103 104 private static final Cache<HashKey,YamlParser> CACHE = Cache.of(HashKey.class, YamlParser.class).build(); 105 106 /** 107 * Constructor, default settings. 108 */ 109 protected Builder() { 110 consumes("application/yaml,text/yaml"); 111 } 112 113 /** 114 * Copy constructor. 115 * 116 * @param copyFrom The builder to copy from. 117 * <br>Cannot be <jk>null</jk>. 118 */ 119 protected Builder(Builder copyFrom) { 120 super(reqnn("copyFrom", copyFrom)); 121 } 122 123 /** 124 * Copy constructor. 125 * 126 * @param copyFrom The bean to copy from. 127 * <br>Cannot be <jk>null</jk>. 128 */ 129 protected Builder(YamlParser copyFrom) { 130 super(reqnn("copyFrom", copyFrom)); 131 } 132 133 @Override /* Overridden from Context.Builder<?> */ 134 public YamlParser build() { 135 return cache(CACHE).build(YamlParser.class); 136 } 137 138 @Override /* Overridden from Context.Builder<?> */ 139 public Builder copy() { 140 return new Builder(this); 141 } 142 143 @Override /* Overridden from Context.Builder<?> */ 144 public HashKey hashKey() { 145 return HashKey.of(super.hashKey()); 146 } 147 148 149 } 150 151 /** Default parser, all default settings.*/ 152 public static final YamlParser DEFAULT = new YamlParser(create()); 153 154 /** 155 * Creates a new builder for this object. 156 * 157 * @return A new builder. 158 */ 159 public static Builder create() { 160 return new Builder(); 161 } 162 163 /** 164 * Constructor. 165 * 166 * @param builder The builder for this object. 167 */ 168 public YamlParser(Builder builder) { 169 super(builder); 170 } 171 172 @Override /* Overridden from YamlMetaProvider */ 173 public YamlBeanPropertyMeta getYamlBeanPropertyMeta(BeanPropertyMeta bpm) { 174 if (bpm == null) 175 return YamlBeanPropertyMeta.DEFAULT; 176 return yamlBeanPropertyMetas.computeIfAbsent(bpm, k -> new YamlBeanPropertyMeta(k, this)); 177 } 178 179 @Override /* Overridden from YamlMetaProvider */ 180 public YamlClassMeta getYamlClassMeta(ClassMeta<?> cm) { 181 return yamlClassMetas.computeIfAbsent(cm, k -> new YamlClassMeta(k, this)); 182 } 183 184 @Override /* Overridden from Context */ 185 public Builder copy() { 186 return new Builder(this); 187 } 188 189 @Override /* Overridden from Context */ 190 public YamlParserSession.Builder createSession() { 191 return YamlParserSession.create(this); 192 } 193 194 @Override /* Overridden from Context */ 195 public YamlParserSession getSession() { return createSession().build(); } 196 197 /** 198 * Convenience delegator for the whole-value {@link RecordReader} using <b>default session 199 * arguments</b>. The real implementation lives on {@link YamlParserSession#readRecords(Object)}. 200 * 201 * @param input The input. 202 * @return A new {@link RecordReader} cursor. 203 * @throws IOException If a problem occurred opening the underlying input. 204 */ 205 @Override /* RecordReadable */ 206 public RecordReader readRecords(Object input) throws IOException { 207 return getSession().readRecords(input); 208 } 209 210 /** 211 * Convenience delegator for the buffered array-element {@link RecordReader} using <b>default 212 * session arguments</b>. The real implementation lives on 213 * {@link YamlParserSession#readArrayRecords(Object)}. 214 * 215 * @param input The input. 216 * @return A buffered {@link RecordReader}. 217 * @throws IOException If a problem occurred reading the input. 218 */ 219 @Override /* ArrayRecordReadable */ 220 public RecordReader readArrayRecords(Object input) throws IOException { 221 return getSession().readArrayRecords(input); 222 } 223 224 /** 225 * The YAML record cursor is buffered/{@link RecordAdapter}-backed, not O(1) streaming. 226 * 227 * @return Always <jk>false</jk>. 228 */ 229 @Override /* RecordReadable */ 230 public boolean isRecordStreaming() { return false; } 231 232 /** 233 * The YAML array-record cursor is buffered/{@link RecordAdapter}-backed, not O(1) streaming. 234 * 235 * @return Always <jk>false</jk>. 236 */ 237 @Override /* ArrayRecordReadable */ 238 public boolean isArrayRecordStreaming() { return false; } 239}