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.toml;
018
019import static org.apache.juneau.commons.utils.StringUtils.*;
020
021import java.io.*;
022import java.time.*;
023import java.time.temporal.*;
024
025import org.apache.juneau.marshall.*;
026import org.apache.juneau.marshall.serializer.*;
027
028/**
029 * Specialized writer for serializing TOML v1.0.0.
030 *
031 * <p>
032 * Extends {@link SerializerWriter} with TOML-specific methods for tables, key-value pairs,
033 * and typed values.
034 *
035 * <h5 class='section'>Notes:</h5><ul>
036 *    <li class='note'>This class is not intended for external use.
037 * </ul>
038 *
039 * <h5 class='section'>See Also:</h5><ul>
040 *    <li class='link'><a href="https://toml.io/en/v1.0.0">TOML v1.0.0 Specification</a>
041 * </ul>
042 */
043@SuppressWarnings({
044   "resource" // Writer resource managed by calling code
045})
046public class TomlWriter extends SerializerWriter<TomlWriter> {
047
048   /**
049    * Constructor.
050    *
051    * @param out The writer being wrapped.
052    * @param useWhitespace If <jk>true</jk>, use whitespace for formatting.
053    * @param maxIndent The maximum indentation level.
054    * @param trimStrings If <jk>true</jk>, trim strings before serialization.
055    * @param uriResolver The URI resolver.
056    */
057   protected TomlWriter(Writer out, boolean useWhitespace, int maxIndent, boolean trimStrings, UriResolver uriResolver) {
058      super(out, useWhitespace, maxIndent, trimStrings, '"', uriResolver);
059   }
060
061   TomlWriter tableHeader(String path) {
062      w('[').w(path).w(']').w('\n');
063      return this;
064   }
065
066   TomlWriter arrayOfTablesHeader(String path) {
067      w("[[").w(path).w("]]").w('\n');
068      return this;
069   }
070
071   TomlWriter keyValue(String key, String value) {
072      writeKey(key);
073      w(" = ");
074      stringValue(value);
075      w('\n');
076      return this;
077   }
078
079   TomlWriter bareKey(String key) {
080      w(key);
081      return this;
082   }
083
084   TomlWriter quotedKey(String key) {
085      w('"').w(escapeBasicString(key)).w('"');
086      return this;
087   }
088
089   private void writeKey(String key) {
090      if (isBareKey(key))
091         bareKey(key);
092      else
093         quotedKey(key);
094   }
095
096   TomlWriter stringValue(String value) {
097      if (trimStrings)
098         value = trim(value);
099      w('"').w(escapeBasicString(value == null ? "" : value)).w('"');
100      return this;
101   }
102
103   TomlWriter literalString(String value) {
104      if (trimStrings)
105         value = trim(value);
106      w('\'').w(value == null ? "" : value.replace("'", "''")).w('\'');
107      return this;
108   }
109
110   TomlWriter multiLineString(String value) {
111      if (trimStrings)
112         value = trim(value);
113      w("\"\"\"").w('\n').w(value == null ? "" : value).w("\"\"\"");
114      return this;
115   }
116
117   TomlWriter integerValue(long value) {
118      w(Long.toString(value));
119      return this;
120   }
121
122   TomlWriter floatValue(double value) {
123      if (Double.isNaN(value))
124         w("nan");
125      else if (value == Double.POSITIVE_INFINITY)
126         w("inf");
127      else if (value == Double.NEGATIVE_INFINITY)
128         w("-inf");
129      else {
130         String s = Double.toString(value);
131         if (s.endsWith(".0"))
132            s = s.substring(0, s.length() - 2);
133         w(s);
134      }
135      return this;
136   }
137
138   TomlWriter booleanValue(boolean value) {
139      w(value ? "true" : "false");
140      return this;
141   }
142
143   TomlWriter dateTimeValue(Object value) {
144      if (value instanceof TemporalAccessor ta) {
145         if (ta instanceof Instant i)
146            w(OffsetDateTime.ofInstant(i, ZoneOffset.UTC).format(java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME));
147         else if (ta instanceof OffsetDateTime odt)
148            w(odt.format(java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME));
149         else if (ta instanceof ZonedDateTime zdt)
150            w(zdt.format(java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME));
151         else if (ta instanceof LocalDateTime ldt)
152            w(ldt.format(java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME));
153         else if (ta instanceof LocalDate ld)
154            w(ld.format(java.time.format.DateTimeFormatter.ISO_LOCAL_DATE));
155         else if (ta instanceof LocalTime lt)
156            w(lt.format(java.time.format.DateTimeFormatter.ISO_LOCAL_TIME));
157         else
158            w(value.toString());
159      } else {
160         w(value.toString());
161      }
162      return this;
163   }
164
165   TomlWriter localDateTimeValue(Object value) {
166      if (value instanceof LocalDateTime value2)
167         w(value2.format(java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME));
168      else
169         w(value.toString());
170      return this;
171   }
172
173   TomlWriter localDateValue(Object value) {
174      if (value instanceof LocalDate value2)
175         w(value2.format(java.time.format.DateTimeFormatter.ISO_LOCAL_DATE));
176      else
177         w(value.toString());
178      return this;
179   }
180
181   TomlWriter localTimeValue(Object value) {
182      if (value instanceof LocalTime value2)
183         w(value2.format(java.time.format.DateTimeFormatter.ISO_LOCAL_TIME));
184      else
185         w(value.toString());
186      return this;
187   }
188
189   TomlWriter arrayStart() {
190      w('[');
191      return this;
192   }
193
194   TomlWriter arrayEnd() {
195      w(']');
196      return this;
197   }
198
199   TomlWriter inlineTableStart() {
200      w('{');
201      return this;
202   }
203
204   TomlWriter inlineTableEnd() {
205      w('}');
206      return this;
207   }
208
209   TomlWriter comment(String text) {
210      w("# ").w(text).w('\n');
211      return this;
212   }
213
214   TomlWriter blankLine() {
215      w('\n');
216      return this;
217   }
218
219   String escapeBasicString(String text) {
220      if (text == null)
221         return "";
222      var sb = new StringBuilder();
223      for (int i = 0; i < text.length(); i++) {
224         char c = text.charAt(i);
225         switch (c) {
226            case '\\' -> sb.append("\\\\");
227            case '"' -> sb.append("\\\"");
228            case '\b' -> sb.append("\\b");
229            case '\t' -> sb.append("\\t");
230            case '\n' -> sb.append("\\n");
231            case '\f' -> sb.append("\\f");
232            case '\r' -> sb.append("\\r");
233            default -> sb.append(c);
234         }
235      }
236      return sb.toString();
237   }
238
239   boolean isBareKey(String key) {
240      if (isEmpty(key))
241         return false;
242      for (int i = 0; i < key.length(); i++) {
243         char c = key.charAt(i);
244         if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-'))
245            return false;
246      }
247      return true;
248   }
249}