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