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.jcs; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.io.*; 022 023import org.apache.juneau.marshall.*; 024import org.apache.juneau.marshall.json.*; 025 026/** 027 * JSON writer for JCS (RFC 8785) canonical output. 028 * 029 * <p> 030 * Extends {@link JsonWriter} with ECMAScript-compatible number serialization and JCS string 031 * escaping (control characters, lone surrogate validation). 032 * 033 * <h5 class='section'>See Also:</h5><ul> 034 * <li class='link'><a class="doclink" href="https://www.rfc-editor.org/rfc/rfc8785">RFC 8785 — JSON Canonicalization Scheme</a> 035 * </ul> 036 */ 037public class JcsWriter extends JsonWriter<JcsWriter> { 038 039 /** 040 * Constructor. 041 * 042 * @param out The writer being wrapped. 043 * @param useWhitespace If <jk>true</jk>, tabs and spaces will be used in output. 044 * @param maxIndent The maximum indentation level. 045 * @param escapeSolidus If <jk>true</jk>, forward slashes should be escaped in the output. 046 * @param quoteChar The quote character to use (always <js>'"'</js> for JCS). 047 * @param simpleAttrs If <jk>true</jk>, JSON attributes will only be quoted when necessary. 048 * @param trimStrings If <jk>true</jk>, strings will be trimmed before being serialized. 049 * @param uriResolver The URI resolver for resolving URIs to absolute or root-relative form. 050 */ 051 @SuppressWarnings({ 052 "java:S107" // Constructor requires 8 parameters for JSON writer configuration 053 }) 054 protected JcsWriter(Writer out, boolean useWhitespace, int maxIndent, boolean escapeSolidus, char quoteChar, boolean simpleAttrs, boolean trimStrings, UriResolver uriResolver) { 055 super(out, useWhitespace, maxIndent, escapeSolidus, quoteChar, simpleAttrs, trimStrings, uriResolver); 056 } 057 058 @SuppressWarnings({ 059 "resource" // Fluent writer methods return this; the writer is closed by its owner 060 }) 061 @Override /* Overridden from SerializerWriter */ 062 public JcsWriter append(Object value) { 063 if (value == null) { 064 w("null"); 065 return this; 066 } 067 if (value instanceof Number value2) 068 w(JcsSerializerSession.toEcmaNumber(value2)); 069 else 070 w(value.toString()); 071 return this; 072 } 073 074 @SuppressWarnings({ 075 "resource" // Fluent writer methods return this; the writer is closed by its owner 076 }) 077 @Override /* Overridden from JsonWriter */ 078 public JcsWriter stringValue(String s) { 079 if (s == null) 080 return this; 081 validateNoLoneSurrogates(s); 082 q(); 083 for (var i = 0; i < s.length(); i++) { 084 var c = s.charAt(i); 085 if (c == '"') 086 w('\\').w('"'); 087 else if (c == '\\') 088 w('\\').w('\\'); 089 else if (c == '\b') 090 w('\\').w('b'); 091 else if (c == '\t') 092 w('\\').w('t'); 093 else if (c == '\n') 094 w('\\').w('n'); 095 else if (c == '\f') 096 w('\\').w('f'); 097 else if (c == '\r') 098 w('\\').w('r'); 099 else if (c <= 0x1F) 100 w(f("\\u%04x", (int) c)); 101 else 102 w(c); 103 } 104 q(); 105 return this; 106 } 107 108 @SuppressWarnings({ 109 "java:S127" // Loop counter i++ needed to skip low surrogate after high surrogate pair 110 }) 111 private static void validateNoLoneSurrogates(String s) { 112 for (var i = 0; i < s.length(); i++) { 113 var c = s.charAt(i); 114 if (Character.isHighSurrogate(c)) { 115 if (i + 1 >= s.length() || !Character.isLowSurrogate(s.charAt(i + 1))) 116 throw rex("Lone high surrogate at index %s in string", i); 117 i++; 118 } else if (Character.isLowSurrogate(c)) { 119 throw rex("Lone low surrogate at index %s in string", i); 120 } 121 } 122 } 123}