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.hocon; 018 019import static org.apache.juneau.commons.utils.StringUtils.*; 020 021import java.io.*; 022 023import org.apache.juneau.commons.lang.*; 024import org.apache.juneau.marshall.*; 025import org.apache.juneau.marshall.serializer.*; 026 027/** 028 * Specialized writer for serializing HOCON format. 029 * 030 * <p> 031 * Extends {@link SerializerWriter} with HOCON-specific methods for unquoted strings, 032 * triple-quoted strings ("""), equals sign separator, and formatting options. 033 * 034 * <h5 class='section'>See Also:</h5><ul> 035 * <li class='link'><a class="doclink" href="https://github.com/lightbend/config/blob/main/HOCON.md">HOCON Specification</a> 036 * </ul> 037 */ 038@SuppressWarnings({ 039 "java:S135", // Multiple break/continue necessary for isNumber char validation loop 040 "resource" // Writer resource managed by calling code 041}) 042public class HoconWriter extends SerializerWriter<HoconWriter> { 043 044 private static final AsciiSet QUOTE_KEY_CHARS = AsciiSet.of(" \t\n\r{},:[]=\"'#/"); 045 // HOCON parser-meaningful chars that must be quoted in a value to avoid mis-tokenization: 046 // `=` — alternate key-value assignment operator (also BASE64 padding). 047 // `+` — first half of PLUS_EQUALS, and forbidden in unquoted strings (also Calendar 048 // BASIC_ISO_DATE timezone offset, e.g. `20240615+0900`). 049 // `$` — first half of `${...}` substitution, and forbidden in unquoted strings (also 050 // ClassFormat.BINARY_NAME inner-class delimiter, e.g. `java.util.Map$Entry`). 051 // `^ \ ? ! @ * &` — forbidden in unquoted strings by the HOCON tokenizer/spec, so values 052 // containing them must be quoted for serializer->parser round-trip fidelity. 053 private static final AsciiSet QUOTE_VALUE_CHARS = AsciiSet.of(" \t\n\r{}[],:=+$\"'#^\\?!@*&"); 054 private static final AsciiSet ENCODED_CHARS = AsciiSet.of("\n\t\b\f\r\"\\"); 055 056 /** Use newlines instead of commas between members. */ 057 protected final boolean useNewlineSeparators; 058 059 /** Use unquoted strings for simple values. */ 060 protected final boolean useUnquotedStrings; 061 062 /** Use unquoted keys when safe. */ 063 protected final boolean useUnquotedKeys; 064 065 /** Omit root braces. */ 066 protected final boolean omitRootBraces; 067 068 /** Use triple-quoted """ for strings with newlines. */ 069 protected final boolean useMultilineStrings; 070 071 /** Use = (not :) for key-value separator. */ 072 protected final boolean useEqualsSign; 073 074 /** 075 * Constructor. 076 * 077 * @param out The writer being wrapped. 078 * @param useWhitespace If <jk>true</jk>, use indentation and newlines. 079 * @param maxIndent The maximum indentation level. 080 * @param trimStrings If <jk>true</jk>, trim strings before serialization. 081 * @param useNewlineSeparators Use newlines instead of commas. 082 * @param useUnquotedStrings Use unquoted strings for simple values. 083 * @param useUnquotedKeys Use unquoted keys when safe. 084 * @param omitRootBraces Omit root object braces. 085 * @param useMultilineStrings Use """ for multiline strings. 086 * @param useEqualsSign Use = for key-value separator. 087 * @param uriResolver The URI resolver. 088 */ 089 @SuppressWarnings({ 090 "java:S107" // Constructor requires many parameters for HOCON writer configuration 091 }) 092 protected HoconWriter(Writer out, boolean useWhitespace, int maxIndent, boolean trimStrings, 093 boolean useNewlineSeparators, boolean useUnquotedStrings, boolean useUnquotedKeys, 094 boolean omitRootBraces, boolean useMultilineStrings, boolean useEqualsSign, 095 UriResolver uriResolver) { 096 super(out, useWhitespace, maxIndent, trimStrings, '"', uriResolver); 097 this.useNewlineSeparators = useNewlineSeparators; 098 this.useUnquotedStrings = useUnquotedStrings; 099 this.useUnquotedKeys = useUnquotedKeys; 100 this.omitRootBraces = omitRootBraces; 101 this.useMultilineStrings = useMultilineStrings; 102 this.useEqualsSign = useEqualsSign; 103 } 104 105 /** 106 * Writes the key (unquoted or quoted). 107 * 108 * @param key The key to write. 109 * <br>Can be <jk>null</jk> (treated as an empty string). 110 * @return This object. 111 */ 112 public HoconWriter key(String key) { 113 if (key == null) 114 key = ""; 115 if (needsQuoting(key)) 116 quotedString(key); 117 else 118 unquotedString(key); 119 return this; 120 } 121 122 /** 123 * Writes the key-value separator (= or :). 124 * 125 * @return This object. 126 */ 127 public HoconWriter equalsSign() { 128 if (useEqualsSign) 129 w(" = "); 130 else 131 w(": "); 132 return this; 133 } 134 135 /** 136 * Writes object start. 137 * 138 * @param depth The current indentation depth. 139 * @return This object. 140 */ 141 public HoconWriter objectStart(int depth) { 142 w('{'); 143 if (useWhitespace) 144 nl(depth + 1); 145 return this; 146 } 147 148 /** 149 * Writes object end. 150 * 151 * @param depth The current indentation depth. 152 * @return This object. 153 */ 154 public HoconWriter objectEnd(int depth) { 155 if (useWhitespace) 156 nl(depth); 157 w('}'); 158 return this; 159 } 160 161 /** 162 * Writes array start. 163 * 164 * @return This object. 165 */ 166 public HoconWriter arrayStart() { 167 w('['); 168 return this; 169 } 170 171 /** 172 * Writes array end. 173 * 174 * @return This object. 175 */ 176 public HoconWriter arrayEnd() { 177 w(']'); 178 return this; 179 } 180 181 /** 182 * Writes member separator (newline or comma). 183 * 184 * @param depth The current indentation depth. 185 * @return This object. 186 */ 187 public HoconWriter separator(int depth) { 188 if (useNewlineSeparators) 189 nl(depth); 190 else 191 w(','); 192 return this; 193 } 194 195 /** 196 * Returns whether the key needs quoting. 197 * 198 * @param key The key to check. 199 * <br>Can be <jk>null</jk> (returns <jk>true</jk>). 200 * @return <jk>true</jk> if the key must be quoted. 201 */ 202 public boolean needsQuoting(String key) { 203 if (isEmpty(key)) 204 return true; 205 if (!useUnquotedKeys) 206 return true; 207 for (var i = 0; i < key.length(); i++) { 208 if (QUOTE_KEY_CHARS.contains(key.charAt(i))) 209 return true; 210 } 211 return "true".equals(key) || "false".equals(key) || "null".equals(key) || isNumber(key); 212 } 213 214 /** 215 * Returns whether the value can be written as unquoted. 216 * 217 * @param value The value to check. 218 * <br>Can be <jk>null</jk> (returns <jk>false</jk>). 219 * @return <jk>true</jk> if the value can be unquoted. 220 */ 221 public boolean isSimpleValue(String value) { 222 if (isEmpty(value)) 223 return false; 224 if (!useUnquotedStrings) 225 return false; 226 if (value.startsWith("{") || value.startsWith("[") || value.startsWith("\"") || value.startsWith("'")) 227 return false; 228 if (value.startsWith("#") || value.startsWith("//") || value.startsWith("$")) 229 return false; 230 if (value.contains("\n") || value.contains("\r")) 231 return false; 232 for (var i = 0; i < value.length(); i++) { 233 if (QUOTE_VALUE_CHARS.contains(value.charAt(i))) 234 return false; 235 } 236 if ("true".equals(value) || "false".equals(value) || "null".equals(value)) 237 return false; 238 return !isNumber(value); 239 } 240 241 private static boolean isNumber(String s) { 242 if (isEmpty(s)) 243 return false; 244 var i = s.startsWith("-") ? 1 : 0; 245 if (i >= s.length()) 246 return false; 247 if (s.charAt(i) == '0' && s.length() > i + 1 && s.charAt(i + 1) != '.') 248 return false; 249 for (; i < s.length(); i++) { 250 var c = s.charAt(i); 251 if (c >= '0' && c <= '9') 252 continue; 253 if (c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-') 254 continue; 255 return false; 256 } 257 try { 258 Double.parseDouble(s); 259 return true; 260 } catch (@SuppressWarnings("unused") NumberFormatException e) { 261 return false; 262 } 263 } 264 265 /** 266 * Writes an unquoted string value. 267 * 268 * @param value The value to write. 269 * <br>Can be <jk>null</jk> (nothing is written). 270 * @return This object. 271 */ 272 public HoconWriter unquotedString(String value) { 273 if (value != null) 274 w(value); 275 return this; 276 } 277 278 /** 279 * Writes a quoted string with JSON escaping. 280 * 281 * @param value The value to write. 282 * <br>Can be <jk>null</jk> (emitted as the literal <js>"null"</js>). 283 * @return This object. 284 */ 285 public HoconWriter quotedString(String value) { 286 if (value == null) { 287 w("null"); 288 return this; 289 } 290 w('"'); 291 for (var i = 0; i < value.length(); i++) { 292 var c = value.charAt(i); 293 if (ENCODED_CHARS.contains(c)) { 294 w('\\'); 295 w(switch (c) { 296 case '\n' -> 'n'; 297 case '\t' -> 't'; 298 case '\b' -> 'b'; 299 case '\f' -> 'f'; 300 case '\r' -> 'r'; 301 case '"' -> '"'; 302 case '\\' -> '\\'; 303 default -> c; 304 }); 305 } else { 306 w(c); 307 } 308 } 309 w('"'); 310 return this; 311 } 312 313 /** 314 * Writes a triple-quoted string using """ syntax. 315 * 316 * @param value The value to write. 317 * <br>Can be <jk>null</jk> (emitted as the literal <js>"null"</js>). 318 * @return This object. 319 */ 320 public HoconWriter tripleQuotedString(String value) { 321 if (value == null) { 322 w("null"); 323 return this; 324 } 325 w("\"\"\""); 326 w(value.replace("\r\n", "\n").replace("\r", "\n")); 327 w("\"\"\""); 328 return this; 329 } 330}