001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.uon;
014
015import java.io.*;
016
017import org.apache.juneau.*;
018import org.apache.juneau.internal.*;
019import org.apache.juneau.serializer.*;
020
021/**
022 * Specialized writer for serializing UON-encoded text.
023 *
024 * <ul class='notes'>
025 *    <li>
026 *       This class is not intended for external use.
027 * </ul>
028 */
029public final class UonWriter extends SerializerWriter {
030
031   private final UonSerializerSession session;
032   private final boolean encodeChars, plainTextParams;
033   private final char quoteChar;
034
035   // Characters that do not need to be URL-encoded in strings.
036   private static final AsciiSet unencodedChars = AsciiSet.create().ranges("a-z","A-Z","0-9").chars(";/?:@-_.!*'$(),~=").build();
037
038   // Characters that do not need to be URL-encoded in attribute names.
039   // Identical to unencodedChars, but excludes '='.
040   private static final AsciiSet unencodedCharsAttrName = AsciiSet.create().ranges("a-z","A-Z","0-9").chars(";/?:@-_.!*'$(),~").build();
041
042   // Characters that need to be preceded with an escape character.
043   private static final AsciiSet escapedChars = AsciiSet.create("~'");
044
045   private static final AsciiSet noChars = AsciiSet.create("");
046
047   private static char[] hexArray = "0123456789ABCDEF".toCharArray();
048
049   /**
050    * Constructor.
051    *
052    * @param session The session that created this writer.
053    * @param out The writer being wrapped.
054    * @param useWhitespace If <jk>true</jk>, tabs will be used in output.
055    * @param maxIndent The maximum indentation level.
056    * @param encodeChars If <jk>true</jk>, special characters should be encoded.
057    * @param trimStrings If <jk>true</jk>, strings should be trimmed before they're serialized.
058    * @param plainTextParams If <jk>true</jk>, don't use UON notation for values.
059    * @param quoteChar The quote character to use.  If <c>0</c>, defaults to <js>'\''</js>.
060    * @param uriResolver The URI resolver for resolving URIs to absolute or root-relative form.
061    */
062   protected UonWriter(UonSerializerSession session, Writer out, boolean useWhitespace, int maxIndent,
063         boolean encodeChars, boolean trimStrings, boolean plainTextParams, char quoteChar, UriResolver uriResolver) {
064      super(out, useWhitespace, maxIndent, trimStrings, quoteChar, uriResolver);
065      this.session = session;
066      this.encodeChars = encodeChars;
067      this.plainTextParams = plainTextParams;
068      this.quoteChar = quoteChar;
069   }
070
071   /**
072    * Serializes the specified simple object as a UON string value.
073    *
074    * @param o The object being serialized.
075    * @param isTopAttrName If this is a top-level attribute name we're serializing.
076    * @return This object (for method chaining).
077    * @throws IOException Should never happen.
078    */
079   public final UonWriter appendObject(Object o, boolean isTopAttrName) throws IOException {
080
081      if (o instanceof Boolean)
082         return appendBoolean(o);
083      if (o instanceof Number)
084         return appendNumber(o);
085      if (o == null)
086         return append("null");
087
088      String s = session.toString(o);
089
090      boolean needsQuotes = (! plainTextParams) && UonUtils.needsQuotes(s);
091
092      AsciiSet unenc = (isTopAttrName ? unencodedCharsAttrName : unencodedChars);
093      AsciiSet esc = plainTextParams ? noChars : escapedChars;
094
095      if (needsQuotes)
096         append(quoteChar);
097      for (int i = 0; i < s.length(); i++) {
098         char c = s.charAt(i);
099         if (esc.contains(c))
100            append('~');
101         if ((!encodeChars) || unenc.contains(c))
102            append(c);
103         else {
104            if (c == ' ')
105               append('+');
106            else {
107               int p = s.codePointAt(i);
108               if (p < 0x0080)
109                  appendHex(p);
110               else if (p < 0x0800) {
111                  int p1=p>>>6;
112                  appendHex(p1+192).appendHex((p&63)+128);
113               } else if (p < 0x10000) {
114                  int p1=p>>>6, p2=p1>>>6;
115                  appendHex(p2+224).appendHex((p1&63)+128).appendHex((p&63)+128);
116               } else {
117                  i++;  // Two-byte codepoint...skip past surrogate pair lower byte.
118                  int p1=p>>>6, p2=p1>>>6, p3=p2>>>6;
119                  appendHex(p3+240).appendHex((p2&63)+128).appendHex((p1&63)+128).appendHex((p&63)+128);
120               }
121            }
122         }
123      }
124      if (needsQuotes)
125         append(quoteChar);
126
127      return this;
128   }
129
130   /**
131    * Appends a boolean value to the output.
132    *
133    * @param o The boolean value to append to the output.
134    * @return This object (for method chaining).
135    * @throws IOException Thrown by underlying stream.
136    */
137   protected UonWriter appendBoolean(Object o) throws IOException {
138      append(o.toString());
139      return this;
140   }
141
142   /**
143    * Appends a numeric value to the output.
144    *
145    * @param o The numeric value to append to the output.
146    * @return This object (for method chaining).
147    * @throws IOException Thrown by underlying stream.
148    */
149   protected UonWriter appendNumber(Object o) throws IOException {
150      append(o.toString());
151      return this;
152   }
153
154   /**
155    * Prints out a two-byte %xx sequence for the given byte value.
156    */
157   private UonWriter appendHex(int b) throws IOException {
158      if (b > 255)
159         throw new IOException("Invalid value passed to appendHex.  Must be in the range 0-255.  Value=" + b);
160      append('%').append(hexArray[b>>>4]).append(hexArray[b&0x0F]);
161      return this;
162   }
163
164   /**
165    * Appends a URI to the output.
166    *
167    * @param uri The URI to append to the output.
168    * @return This object (for method chaining).
169    * @throws IOException Thrown by underlying stream.
170    */
171   @Override
172   public SerializerWriter appendUri(Object uri) throws IOException {
173      return appendObject(uriResolver.resolve(uri), false);
174   }
175
176
177   //-----------------------------------------------------------------------------------------------------------------
178   // Overridden methods
179   //-----------------------------------------------------------------------------------------------------------------
180
181   @Override /* SerializerWriter */
182   public UonWriter cr(int depth) throws IOException {
183      super.cr(depth);
184      return this;
185   }
186
187   @Override /* SerializerWriter */
188   public UonWriter cre(int depth) throws IOException {
189      super.cre(depth);
190      return this;
191   }
192
193   @Override /* SerializerWriter */
194   public UonWriter appendln(int indent, String text) throws IOException {
195      super.appendln(indent, text);
196      return this;
197   }
198
199   @Override /* SerializerWriter */
200   public UonWriter appendln(String text) throws IOException {
201      super.appendln(text);
202      return this;
203   }
204
205   @Override /* SerializerWriter */
206   public UonWriter append(int indent, String text) throws IOException {
207      super.append(indent, text);
208      return this;
209   }
210
211   @Override /* SerializerWriter */
212   public UonWriter append(int indent, char c) throws IOException {
213      super.append(indent, c);
214      return this;
215   }
216
217   @Override /* SerializerWriter */
218   public UonWriter q() throws IOException {
219      super.q();
220      return this;
221   }
222
223   @Override /* SerializerWriter */
224   public UonWriter i(int indent) throws IOException {
225      super.i(indent);
226      return this;
227   }
228
229   @Override /* SerializerWriter */
230   public UonWriter nl(int indent) throws IOException {
231      super.nl(indent);
232      return this;
233   }
234
235   @Override /* SerializerWriter */
236   public UonWriter append(Object text) throws IOException {
237      super.append(text);
238      return this;
239   }
240
241   @Override /* SerializerWriter */
242   public UonWriter append(String text) throws IOException {
243      super.append(text);
244      return this;
245   }
246
247   @Override /* SerializerWriter */
248   public UonWriter appendIf(boolean b, String text) throws IOException {
249      super.appendIf(b, text);
250      return this;
251   }
252
253   @Override /* SerializerWriter */
254   public UonWriter appendIf(boolean b, char c) throws IOException {
255      super.appendIf(b, c);
256      return this;
257   }
258
259   @Override /* SerializerWriter */
260   public UonWriter append(char c) throws IOException {
261      super.append(c);
262      return this;
263   }
264}