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.uon;
018
019import static org.apache.juneau.commons.utils.ThrowableUtils.*;
020
021import java.io.*;
022
023import org.apache.juneau.*;
024import org.apache.juneau.commons.lang.*;
025import org.apache.juneau.serializer.*;
026
027/**
028 * Specialized writer for serializing UON-encoded text.
029 *
030 * <h5 class='section'>Notes:</h5><ul>
031 *    <li class='note'>
032 *       This class is not intended for external use.
033 * </ul>
034 *
035 * <h5 class='section'>See Also:</h5><ul>
036 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/UonBasics">UON Basics</a>
037
038 * </ul>
039 */
040@SuppressWarnings("resource")
041public class UonWriter extends SerializerWriter {
042
043   // Characters that do not need to be URL-encoded in strings.
044   private static final AsciiSet unencodedChars = AsciiSet.create().ranges("a-z", "A-Z", "0-9").chars(";/?:@-_.!*'$(),~=").build();
045   // Characters that do not need to be URL-encoded in attribute names.
046   // Identical to unencodedChars, but excludes '='.
047   private static final AsciiSet unencodedCharsAttrName = AsciiSet.create().ranges("a-z", "A-Z", "0-9").chars(";/?:@-_.!*'$(),~").build();
048   // Characters that need to be preceded with an escape character.
049   private static final AsciiSet escapedChars = AsciiSet.of("~'");
050
051   private static final AsciiSet noChars = AsciiSet.of("");
052
053   private static char[] hexArray = "0123456789ABCDEF".toCharArray();
054
055   private final UonSerializerSession session;
056
057   private final boolean encodeChars, plainTextParams;
058
059   private final char quoteChar;
060
061   /**
062    * Constructor.
063    *
064    * @param session The session that created this writer.
065    * @param out The writer being wrapped.
066    * @param useWhitespace If <jk>true</jk>, tabs will be used in output.
067    * @param maxIndent The maximum indentation level.
068    * @param encodeChars If <jk>true</jk>, special characters should be encoded.
069    * @param trimStrings If <jk>true</jk>, strings should be trimmed before they're serialized.
070    * @param plainTextParams If <jk>true</jk>, don't use UON notation for values.
071    * @param quoteChar The quote character to use.  If <c>0</c>, defaults to <js>'\''</js>.
072    * @param uriResolver The URI resolver for resolving URIs to absolute or root-relative form.
073    */
074   protected UonWriter(UonSerializerSession session, Writer out, boolean useWhitespace, int maxIndent, boolean encodeChars, boolean trimStrings, boolean plainTextParams, char quoteChar,
075      UriResolver uriResolver) {
076      super(out, useWhitespace, maxIndent, trimStrings, quoteChar, uriResolver);
077      this.session = session;
078      this.encodeChars = encodeChars;
079      this.plainTextParams = plainTextParams;
080      this.quoteChar = quoteChar;
081   }
082
083   @Override /* Overridden from SerializerWriter */
084   public UonWriter append(char c) {
085      super.append(c);
086      return this;
087   }
088
089   @Override /* Overridden from SerializerWriter */
090   public UonWriter append(char[] value) {
091      super.append(value);
092      return this;
093   }
094
095   @Override /* Overridden from SerializerWriter */
096   public UonWriter append(int indent, char c) {
097      super.append(indent, c);
098      return this;
099   }
100
101   @Override /* Overridden from SerializerWriter */
102   public UonWriter append(int indent, String text) {
103      super.append(indent, text);
104      return this;
105   }
106
107   @Override /* Overridden from SerializerWriter */
108   public UonWriter append(Object text) {
109      super.append(text);
110      return this;
111   }
112
113   @Override /* Overridden from SerializerWriter */
114   public UonWriter append(String text) {
115      super.append(text);
116      return this;
117   }
118
119   @Override /* Overridden from SerializerWriter */
120   public UonWriter appendIf(boolean b, char c) {
121      super.appendIf(b, c);
122      return this;
123   }
124
125   @Override /* Overridden from SerializerWriter */
126   public UonWriter appendIf(boolean b, String text) {
127      super.appendIf(b, text);
128      return this;
129   }
130
131   @Override /* Overridden from SerializerWriter */
132   public UonWriter appendln(int indent, String text) {
133      super.appendln(indent, text);
134      return this;
135   }
136
137   @Override /* Overridden from SerializerWriter */
138   public UonWriter appendln(String text) {
139      super.appendln(text);
140      return this;
141   }
142
143   /**
144    * Serializes the specified simple object as a UON string value.
145    *
146    * @param o The object being serialized.
147    * @param isTopAttrName If this is a top-level attribute name we're serializing.
148    * @return This object.
149    */
150   public UonWriter appendObject(Object o, boolean isTopAttrName) {
151
152      if (o instanceof Boolean)
153         return appendBoolean(o);
154      if (o instanceof Number)
155         return appendNumber(o);
156      if (o == null)
157         return append("null");
158
159      var s = session.toString(o);
160
161      var needsQuotes = (! plainTextParams) && UonUtils.needsQuotes(s);
162
163      var unenc = (isTopAttrName ? unencodedCharsAttrName : unencodedChars);
164      var esc = plainTextParams ? noChars : escapedChars;
165
166      if (needsQuotes)
167         w(quoteChar);
168      for (var i = 0; i < s.length(); i++) {
169         var c = s.charAt(i);
170         if (esc.contains(c))
171            w('~');
172         if ((! encodeChars) || unenc.contains(c))
173            w(c);
174         else {
175            if (c == ' ')
176               w('+');
177            else {
178               var p = s.codePointAt(i);
179               if (p < 0x0080)
180                  appendHex(p);
181               else if (p < 0x0800) {
182                  var p1 = p >>> 6;
183                  appendHex(p1 + 192).appendHex((p & 63) + 128);
184               } else if (p < 0x10000) {
185                  var p1 = p >>> 6;
186                  var p2 = p1 >>> 6;
187                  appendHex(p2 + 224).appendHex((p1 & 63) + 128).appendHex((p & 63) + 128);
188               } else {
189                  i++;  // Two-byte codepoint...skip past surrogate pair lower byte.
190                  var p1 = p >>> 6;
191                  var p2 = p1 >>> 6;
192                  var p3 = p2 >>> 6;
193                  appendHex(p3 + 240).appendHex((p2 & 63) + 128).appendHex((p1 & 63) + 128).appendHex((p & 63) + 128);
194               }
195            }
196         }
197      }
198      if (needsQuotes)
199         w(quoteChar);
200
201      return this;
202   }
203
204   /**
205    * Appends a URI to the output.
206    *
207    * @param uri The URI to append to the output.
208    * @return This object.
209    */
210   @Override /* Overridden from SerializerWriter */
211   public UonWriter appendUri(Object uri) {
212      return appendObject(uriResolver.resolve(uri), false);
213   }
214
215   @Override /* Overridden from SerializerWriter */
216   public UonWriter cr(int depth) {
217      super.cr(depth);
218      return this;
219   }
220
221   @Override /* Overridden from SerializerWriter */
222   public UonWriter cre(int depth) {
223      super.cre(depth);
224      return this;
225   }
226
227   @Override /* Overridden from SerializerWriter */
228   public UonWriter i(int indent) {
229      super.i(indent);
230      return this;
231   }
232
233   @Override /* Overridden from SerializerWriter */
234   public UonWriter ie(int indent) {
235      super.ie(indent);
236      return this;
237   }
238
239   @Override /* Overridden from SerializerWriter */
240   public UonWriter nl(int indent) {
241      super.nl(indent);
242      return this;
243   }
244
245   @Override /* Overridden from SerializerWriter */
246   public UonWriter nlIf(boolean flag, int indent) {
247      super.nlIf(flag, indent);
248      return this;
249   }
250
251   @Override /* Overridden from SerializerWriter */
252   public UonWriter q() {
253      super.q();
254      return this;
255   }
256
257   @Override /* Overridden from SerializerWriter */
258   public UonWriter s() {
259      super.s();
260      return this;
261   }
262
263   @Override /* Overridden from SerializerWriter */
264   public UonWriter sIf(boolean flag) {
265      super.sIf(flag);
266      return this;
267   }
268
269   @Override /* Overridden from SerializerWriter */
270   public UonWriter w(char value) {
271      super.w(value);
272      return this;
273   }
274
275   @Override /* Overridden from SerializerWriter */
276   public UonWriter w(String value) {
277      super.w(value);
278      return this;
279   }
280
281   /**
282    * Prints out a two-byte %xx sequence for the given byte value.
283    */
284   private UonWriter appendHex(int b) {
285      if (b > 255)
286         throw rex("Invalid value passed to appendHex.  Must be in the range 0-255.  Value={0}", b);
287      w('%').w(hexArray[b >>> 4]).w(hexArray[b & 0x0F]);
288      return this;
289   }
290
291   /**
292    * Appends a boolean value to the output.
293    *
294    * @param o The boolean value to append to the output.
295    * @return This object.
296    */
297   protected UonWriter appendBoolean(Object o) {
298      append(o.toString());
299      return this;
300   }
301
302   /**
303    * Appends a numeric value to the output.
304    *
305    * @param o The numeric value to append to the output.
306    * @return This object.
307    */
308   protected UonWriter appendNumber(Object o) {
309      append(o.toString());
310      return this;
311   }
312}