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 * <h5 class='section'>Notes:</h5>
025 * <ul class='spaced-list'>
026 *    <li>
027 *       This class is not intended for external use.
028 * </ul>
029 */
030public final class UonWriter extends SerializerWriter {
031
032   private final UonSerializerSession session;
033   private final boolean encodeChars, plainTextParams;
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 uriResolver The URI resolver for resolving URIs to absolute or root-relative form.
060    */
061   protected UonWriter(UonSerializerSession session, Writer out, boolean useWhitespace, int maxIndent,
062         boolean encodeChars, boolean trimStrings, boolean plainTextParams, UriResolver uriResolver) {
063      super(out, useWhitespace, maxIndent, trimStrings, '\'', uriResolver);
064      this.session = session;
065      this.encodeChars = encodeChars;
066      this.plainTextParams = plainTextParams;
067   }
068
069   /**
070    * Serializes the specified simple object as a UON string value.
071    *
072    * @param o The object being serialized.
073    * @param isTopAttrName If this is a top-level attribute name we're serializing.
074    * @return This object (for method chaining).
075    * @throws IOException Should never happen.
076    */
077   public final UonWriter appendObject(Object o, boolean isTopAttrName) throws IOException {
078
079      if (o instanceof Boolean)
080         return appendBoolean(o);
081      if (o instanceof Number)
082         return appendNumber(o);
083      if (o == null)
084         return append("null");
085
086      String s = session.toString(o);
087
088      boolean needsQuotes = (! plainTextParams) && UonUtils.needsQuotes(s);
089
090      AsciiSet unenc = (isTopAttrName ? unencodedCharsAttrName : unencodedChars);
091      AsciiSet esc = plainTextParams ? noChars : escapedChars;
092
093      if (needsQuotes)
094         append('\'');
095      for (int i = 0; i < s.length(); i++) {
096         char c = s.charAt(i);
097         if (esc.contains(c))
098            append('~');
099         if ((!encodeChars) || unenc.contains(c))
100            append(c);
101         else {
102            if (c == ' ')
103               append('+');
104            else {
105               int p = s.codePointAt(i);
106               if (p < 0x0080)
107                  appendHex(p);
108               else if (p < 0x0800) {
109                  int p1=p>>>6;
110                  appendHex(p1+192).appendHex((p&63)+128);
111               } else if (p < 0x10000) {
112                  int p1=p>>>6, p2=p1>>>6;
113                  appendHex(p2+224).appendHex((p1&63)+128).appendHex((p&63)+128);
114               } else {
115                  i++;  // Two-byte codepoint...skip past surrogate pair lower byte.
116                  int p1=p>>>6, p2=p1>>>6, p3=p2>>>6;
117                  appendHex(p3+240).appendHex((p2&63)+128).appendHex((p1&63)+128).appendHex((p&63)+128);
118               }
119            }
120         }
121      }
122      if (needsQuotes)
123         append('\'');
124
125      return this;
126   }
127
128   /**
129    * Appends a boolean value to the output.
130    *
131    * @param o The boolean value to append to the output.
132    * @return This object (for method chaining).
133    * @throws IOException
134    */
135   protected UonWriter appendBoolean(Object o) throws IOException {
136      append(o.toString());
137      return this;
138   }
139
140   /**
141    * Appends a numeric value to the output.
142    *
143    * @param o The numeric value to append to the output.
144    * @return This object (for method chaining).
145    * @throws IOException
146    */
147   protected UonWriter appendNumber(Object o) throws IOException {
148      append(o.toString());
149      return this;
150   }
151
152   /**
153    * Prints out a two-byte %xx sequence for the given byte value.
154    */
155   private UonWriter appendHex(int b) throws IOException {
156      if (b > 255)
157         throw new IOException("Invalid value passed to appendHex.  Must be in the range 0-255.  Value=" + b);
158      append('%').append(hexArray[b>>>4]).append(hexArray[b&0x0F]);
159      return this;
160   }
161
162   /**
163    * Appends a URI to the output.
164    *
165    * @param uri The URI to append to the output.
166    * @return This object (for method chaining).
167    * @throws IOException
168    */
169   @Override
170   public SerializerWriter appendUri(Object uri) throws IOException {
171      return appendObject(uriResolver.resolve(uri), false);
172   }
173
174
175   //-----------------------------------------------------------------------------------------------------------------
176   // Overridden methods
177   //-----------------------------------------------------------------------------------------------------------------
178
179   @Override /* SerializerWriter */
180   public UonWriter cr(int depth) throws IOException {
181      super.cr(depth);
182      return this;
183   }
184
185   @Override /* SerializerWriter */
186   public UonWriter cre(int depth) throws IOException {
187      super.cre(depth);
188      return this;
189   }
190
191   @Override /* SerializerWriter */
192   public UonWriter appendln(int indent, String text) throws IOException {
193      super.appendln(indent, text);
194      return this;
195   }
196
197   @Override /* SerializerWriter */
198   public UonWriter appendln(String text) throws IOException {
199      super.appendln(text);
200      return this;
201   }
202
203   @Override /* SerializerWriter */
204   public UonWriter append(int indent, String text) throws IOException {
205      super.append(indent, text);
206      return this;
207   }
208
209   @Override /* SerializerWriter */
210   public UonWriter append(int indent, char c) throws IOException {
211      super.append(indent, c);
212      return this;
213   }
214
215   @Override /* SerializerWriter */
216   public UonWriter q() throws IOException {
217      super.q();
218      return this;
219   }
220
221   @Override /* SerializerWriter */
222   public UonWriter i(int indent) throws IOException {
223      super.i(indent);
224      return this;
225   }
226
227   @Override /* SerializerWriter */
228   public UonWriter nl(int indent) throws IOException {
229      super.nl(indent);
230      return this;
231   }
232
233   @Override /* SerializerWriter */
234   public UonWriter append(Object text) throws IOException {
235      super.append(text);
236      return this;
237   }
238
239   @Override /* SerializerWriter */
240   public UonWriter append(String text) throws IOException {
241      super.append(text);
242      return this;
243   }
244
245   @Override /* SerializerWriter */
246   public UonWriter appendIf(boolean b, String text) throws IOException {
247      super.appendIf(b, text);
248      return this;
249   }
250
251   @Override /* SerializerWriter */
252   public UonWriter appendIf(boolean b, char c) throws IOException {
253      super.appendIf(b, c);
254      return this;
255   }
256
257   @Override /* SerializerWriter */
258   public UonWriter append(char c) throws IOException {
259      super.append(c);
260      return this;
261   }
262}