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.uon; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.io.*; 022 023import org.apache.juneau.commons.lang.*; 024import org.apache.juneau.marshall.*; 025import org.apache.juneau.marshall.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/UonSupport">UON</a> 037 038 * </ul> 039 */ 040@SuppressWarnings({ 041 "java:S4144", // Identical methods intentional for different test scenarios 042 "resource" // Resource management handled externally 043}) 044public class UonWriter extends SerializerWriter<UonWriter> { 045 046 // Characters that do not need to be URL-encoded in strings. 047 private static final AsciiSet unencodedChars = AsciiSet.create().ranges("a-z", "A-Z", "0-9").chars(";/?:@-_.!*'$(),~=").build(); 048 // Characters that do not need to be URL-encoded in attribute names. 049 // Identical to unencodedChars, but excludes '='. 050 private static final AsciiSet unencodedCharsAttrName = AsciiSet.create().ranges("a-z", "A-Z", "0-9").chars(";/?:@-_.!*'$(),~").build(); 051 // Characters that need to be preceded with an escape character. 052 private static final AsciiSet escapedChars = AsciiSet.of("~'"); 053 054 private static final AsciiSet noChars = AsciiSet.of(""); 055 056 private static char[] hexArray = "0123456789ABCDEF".toCharArray(); 057 058 private final UonSerializerSession session; 059 060 private final boolean encodeChars; 061 private final boolean plainTextParams; 062 063 private final char quoteChar2; 064 065 /** 066 * Constructor. 067 * 068 * @param session The session that created this writer. 069 * @param out The writer being wrapped. 070 * @param useWhitespace If <jk>true</jk>, tabs will be used in output. 071 * @param maxIndent The maximum indentation level. 072 * @param encodeChars If <jk>true</jk>, special characters should be encoded. 073 * @param trimStrings If <jk>true</jk>, strings should be trimmed before they're serialized. 074 * @param plainTextParams If <jk>true</jk>, don't use UON notation for values. 075 * @param quoteChar The quote character to use. If <c>0</c>, defaults to <js>'\''</js>. 076 * @param uriResolver The URI resolver for resolving URIs to absolute or root-relative form. 077 */ 078 @SuppressWarnings({ 079 "java:S107" // Constructor requires 9 parameters for UON writer configuration 080 }) 081 protected UonWriter(UonSerializerSession session, Writer out, boolean useWhitespace, int maxIndent, boolean encodeChars, boolean trimStrings, boolean plainTextParams, char quoteChar, 082 UriResolver uriResolver) { 083 super(out, useWhitespace, maxIndent, trimStrings, quoteChar, uriResolver); 084 this.session = session; 085 this.encodeChars = encodeChars; 086 this.plainTextParams = plainTextParams; 087 this.quoteChar2 = quoteChar; 088 } 089 090 /** 091 * Serializes the specified simple object as a UON string value. 092 * 093 * @param o The object being serialized. 094 * <br>Can be <jk>null</jk> (emitted as the literal <js>"null"</js>). 095 * @param isTopAttrName If this is a top-level attribute name we're serializing. 096 * @return This object. 097 */ 098 @SuppressWarnings({ 099 "java:S127", // Loop counter advances for surrogate pairs 100 "java:S3776" // Cognitive complexity acceptable for this specific logic 101 }) 102 public UonWriter appendObject(Object o, boolean isTopAttrName) { 103 104 if (o instanceof Boolean) 105 return appendBoolean(o); 106 if (o instanceof Number) 107 return appendNumber(o); 108 if (o == null) 109 return append("null"); 110 111 var s = session.toString(o); 112 113 var needsQuotes = (! plainTextParams) && UonUtils.needsQuotes(s); 114 115 var unenc = (isTopAttrName ? unencodedCharsAttrName : unencodedChars); 116 var esc = plainTextParams ? noChars : escapedChars; 117 118 if (needsQuotes) 119 w(quoteChar2); 120 for (var i = 0; i < s.length(); i++) { 121 var c = s.charAt(i); 122 if (esc.contains(c)) 123 w('~'); 124 if ((! encodeChars) || unenc.contains(c)) 125 w(c); 126 else { 127 if (c == ' ') 128 w('+'); 129 else { 130 var p = s.codePointAt(i); 131 if (p < 0x0080) 132 appendHex(p); 133 else if (p < 0x0800) { 134 var p1 = p >>> 6; 135 appendHex(p1 + 192).appendHex((p & 63) + 128); 136 } else if (p < 0x10000) { 137 var p1 = p >>> 6; 138 var p2 = p1 >>> 6; 139 appendHex(p2 + 224).appendHex((p1 & 63) + 128).appendHex((p & 63) + 128); 140 } else { 141 i++; // Two-byte codepoint...skip past surrogate pair lower byte. 142 var p1 = p >>> 6; 143 var p2 = p1 >>> 6; 144 var p3 = p2 >>> 6; 145 appendHex(p3 + 240).appendHex((p2 & 63) + 128).appendHex((p1 & 63) + 128).appendHex((p & 63) + 128); 146 } 147 } 148 } 149 } 150 if (needsQuotes) 151 w(quoteChar2); 152 153 return this; 154 } 155 156 /** 157 * Appends a URI to the output. 158 * 159 * @param uri The URI to append to the output. 160 * @return This object. 161 */ 162 @Override /* Overridden from SerializerWriter */ 163 public UonWriter appendUri(Object uri) { 164 return appendObject(uriResolver.resolve(uri), false); 165 } 166 167 /** 168 * Prints out a two-byte %xx sequence for the given byte value. 169 */ 170 private UonWriter appendHex(int b) { 171 if (b > 255) 172 throw rex("Invalid value passed to appendHex. Must be in the range 0-255. Value=%s", b); 173 w('%').w(hexArray[b >>> 4]).w(hexArray[b & 0x0F]); 174 return this; 175 } 176 177 /** 178 * Appends a boolean value to the output. 179 * 180 * @param o The boolean value to append to the output. 181 * @return This object. 182 */ 183 protected UonWriter appendBoolean(Object o) { 184 append(o.toString()); 185 return this; 186 } 187 188 /** 189 * Appends a numeric value to the output. 190 * 191 * @param o The numeric value to append to the output. 192 * @return This object. 193 */ 194 protected UonWriter appendNumber(Object o) { 195 append(o.toString()); 196 return this; 197 } 198}