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}