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.StringUtils.*; 020 021import java.io.*; 022import java.util.*; 023 024import org.apache.juneau.commons.lang.*; 025import org.apache.juneau.marshall.*; 026import org.apache.juneau.marshall.serializer.*; 027 028/** 029 * Specialized writer for serializing YAML. 030 * 031 * <h5 class='section'>Notes:</h5><ul> 032 * <li class='note'> 033 * This class is not intended for external use. 034 * </ul> 035 * 036 * <h5 class='section'>See Also:</h5><ul> 037 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/YamlSupport">YAML</a> 038 * </ul> 039 */ 040@SuppressWarnings({ 041 "resource" // Writer resource managed by calling code 042}) 043public class YamlWriter extends SerializerWriter<YamlWriter> { 044 045 private static final AsciiSet specialChars = AsciiSet.of(":#{}[],&*!|>'\"%@`"); 046 047 // @formatter:off 048 private static final Set<String> reservedWords = new HashSet<>(Arrays.asList( 049 "null", "Null", "NULL", "~", 050 "true", "True", "TRUE", 051 "false", "False", "FALSE", 052 "yes", "Yes", "YES", 053 "no", "No", "NO", 054 "on", "On", "ON", 055 "off", "Off", "OFF", 056 ".inf", ".Inf", ".INF", 057 "-.inf", "-.Inf", "-.INF", 058 ".nan", ".NaN", ".NAN" 059 )); 060 // @formatter:on 061 062 private static final AsciiSet escapedChars = AsciiSet.of("\\\"\n\t\r\0\u0007\b\f\u001b"); 063 064 /** 065 * Constructor. 066 * 067 * @param out The writer being wrapped. 068 * @param useWhitespace If <jk>true</jk>, tabs and spaces will be used in output. 069 * @param maxIndent The maximum indentation level. 070 * @param trimStrings If <jk>true</jk>, strings will be trimmed before being serialized. 071 * @param uriResolver The URI resolver for resolving URIs to absolute or root-relative form. 072 */ 073 protected YamlWriter(Writer out, boolean useWhitespace, int maxIndent, boolean trimStrings, UriResolver uriResolver) { 074 super(out, useWhitespace, maxIndent, trimStrings, '"', uriResolver); 075 } 076 077 /** 078 * Writes a YAML mapping key. 079 * 080 * <p> 081 * If the key needs quoting (contains special YAML characters, starts/ends with whitespace, 082 * is empty, or is a YAML reserved word), it will be double-quoted with proper escaping. 083 * A colon-space separator is always appended after the key. 084 * 085 * @param s The key name. 086 * <br>Can be <jk>null</jk> (written as the literal <c>null</c>). 087 * @return This object. 088 */ 089 public YamlWriter key(String s) { 090 keyName(s); 091 w(": "); 092 return this; 093 } 094 095 /** 096 * Writes just the key name portion (with quoting if necessary) but without any separator. 097 * 098 * @param s The key name. 099 * <br>Can be <jk>null</jk> (written as the literal <c>null</c>). 100 * @return This object. 101 */ 102 public YamlWriter keyName(String s) { 103 if (trimStrings) 104 s = trim(s); 105 106 if (s == null) { 107 w("null"); 108 } else if (needsQuoting(s)) { 109 writeQuoted(s); 110 } else { 111 w(s); 112 } 113 return this; 114 } 115 116 /** 117 * Writes a YAML scalar text value. 118 * 119 * <p> 120 * If null, writes the literal <c>null</c>. If the string needs quoting (is empty, 121 * looks like a boolean/null/number, contains special characters, starts/ends with whitespace, 122 * or contains newlines), it will be double-quoted with proper YAML escaping. 123 * 124 * @param s The string value. 125 * <br>Can be <jk>null</jk> (written as the literal <c>null</c>). 126 * @return This object. 127 */ 128 @SuppressWarnings({ 129 "java:S4144" // Identical to keyName by design; distinct semantic role (value vs key quoting) 130 }) 131 public YamlWriter textValue(String s) { 132 if (trimStrings) 133 s = trim(s); 134 135 if (s == null) { 136 w("null"); 137 } else if (needsQuoting(s)) { 138 writeQuoted(s); 139 } else { 140 w(s); 141 } 142 return this; 143 } 144 145 /** 146 * Appends a URI value to the output. 147 * 148 * <p> 149 * Resolves the URI and writes it as a double-quoted YAML string. 150 * 151 * @param uri The URI to append to the output. 152 * <br>Can be <jk>null</jk>. The effect is implementation-dependent — see {@link UriResolver#resolve(Object)}. 153 * @return This object. 154 */ 155 public YamlWriter uriValue(Object uri) { 156 writeQuoted(uriResolver.resolve(uri)); 157 return this; 158 } 159 160 /** 161 * Writes YAML block indentation. 162 * 163 * <p> 164 * Writes a newline followed by {@code depth * 2} spaces of indentation. 165 * 166 * @param depth The indentation depth. 167 * @return This object. 168 */ 169 public YamlWriter yamlIndent(int depth) { 170 w('\n'); 171 for (var i = 0; i < depth * 2; i++) 172 w(' '); 173 return this; 174 } 175 176 /** 177 * Writes a YAML list entry prefix. 178 * 179 * <p> 180 * Writes a newline followed by {@code depth * 2} spaces and a dash-space (<js>"- "</js>) prefix. 181 * 182 * @param depth The indentation depth. 183 * @return This object. 184 */ 185 public YamlWriter listEntry(int depth) { 186 w('\n'); 187 for (var i = 0; i < depth * 2; i++) 188 w(' '); 189 w("- "); 190 return this; 191 } 192 193 /** 194 * Returns <jk>true</jk> if the specified string needs double-quoting in YAML. 195 * 196 * @param s The string to check. 197 * @return <jk>true</jk> if the string needs quoting. 198 */ 199 @SuppressWarnings({ 200 "java:S3776" // Cognitive complexity acceptable for YAML quoting checks 201 }) 202 private static boolean needsQuoting(String s) { 203 if (s.isEmpty()) 204 return true; 205 206 if (Character.isWhitespace(s.charAt(0)) || Character.isWhitespace(s.charAt(s.length() - 1))) 207 return true; 208 209 if (reservedWords.contains(s)) 210 return true; 211 212 if (looksLikeNumber(s)) 213 return true; 214 215 for (var i = 0; i < s.length(); i++) { 216 var c = s.charAt(i); 217 if (c == '\n' || c == '\r') 218 return true; 219 if (specialChars.contains(c)) 220 return true; 221 if (escapedChars.contains(c)) 222 return true; 223 } 224 225 return false; 226 } 227 228 /** 229 * Returns <jk>true</jk> if the string looks like a YAML number (integer, float, hex, or octal). 230 */ 231 private static boolean looksLikeNumber(String s) { 232 if (s.isEmpty()) 233 return false; 234 var c0 = s.charAt(0); 235 if (c0 == '+' || c0 == '-') 236 return s.length() > 1 && isDigitOrDot(s.charAt(1)); 237 if (c0 == '.') 238 return s.length() > 1 && Character.isDigit(s.charAt(1)); 239 if (c0 == '0' && s.length() > 1) { 240 var c1 = s.charAt(1); 241 if (c1 == 'x' || c1 == 'X' || c1 == 'o' || c1 == 'O') 242 return true; 243 } 244 return Character.isDigit(c0); 245 } 246 247 private static boolean isDigitOrDot(char c) { 248 return Character.isDigit(c) || c == '.'; 249 } 250 251 /** 252 * Writes a double-quoted YAML string with proper escaping. 253 */ 254 private void writeQuoted(String s) { 255 w('"'); 256 if (s != null) { 257 for (var i = 0; i < s.length(); i++) { 258 var c = s.charAt(i); 259 if (escapedChars.contains(c)) { 260 switch (c) { 261 case '\\': w('\\').w('\\'); break; 262 case '"': w('\\').w('"'); break; 263 case '\n': w('\\').w('n'); break; 264 case '\t': w('\\').w('t'); break; 265 case '\r': w('\\').w('r'); break; 266 case '\0': w('\\').w('0'); break; 267 case '\u0007': w('\\').w('a'); break; 268 case '\b': w('\\').w('b'); break; 269 case '\f': w('\\').w('f'); break; 270 case '\u001b': w('\\').w('e'); break; 271 default: w(c); 272 } 273 } else { 274 w(c); 275 } 276 } 277 } 278 w('"'); 279 } 280}