blob: d19409022121ad35f102480c890fbf0043c63bf3 [file] [log] [blame]
// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
// * with the License. You may obtain a copy of the License at *
// * *
// * http://www.apache.org/licenses/LICENSE-2.0 *
// * *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
// * specific language governing permissions and limitations under the License. *
// ***************************************************************************************************************************
package org.apache.juneau.json;
import java.io.*;
import org.apache.juneau.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.serializer.*;
/**
* Specialized writer for serializing JSON.
*
* <ul class='notes'>
* <li>
* This class is not intended for external use.
* </ul>
*/
public final class JsonWriter extends SerializerWriter {
private final boolean simpleMode, escapeSolidus;
// Characters that trigger special handling of serializing attribute values.
private static final AsciiSet
encodedChars = AsciiSet.create("\n\t\b\f\r'\"\\"),
encodedChars2 = AsciiSet.create("\n\t\b\f\r'\"\\/");
private static final KeywordSet reservedWords = new KeywordSet(
"arguments","break","case","catch","class","const","continue","debugger","default","delete",
"do","else","enum","eval","export","extends","false","finally","for","function","if",
"implements","import","in","instanceof","interface","let","new","null","package",
"private","protected","public","return","static","super","switch","this","throw",
"true","try","typeof","var","void","while","with","undefined","yield"
);
// Characters that represent attribute name characters that don't trigger quoting.
// These are actually more strict than the actual Javascript specification, but
// can be narrowed in the future if necessary.
// For example, we quote attributes that start with $ even though we don't need to.
private static final AsciiSet validAttrChars = AsciiSet.create().ranges("a-z","A-Z","0-9").chars("_").build();
private static final AsciiSet validFirstAttrChars = AsciiSet.create().ranges("a-z","A-Z").chars("_").build();
private final AsciiSet ec;
/**
* Constructor.
*
* @param out The writer being wrapped.
* @param useWhitespace If <jk>true</jk>, tabs and spaces will be used in output.
* @param maxIndent The maximum indentation level.
* @param escapeSolidus If <jk>true</jk>, forward slashes should be escaped in the output.
* @param quoteChar The quote character to use (i.e. <js>'\''</js> or <js>'"'</js>)
* @param simpleMode If <jk>true</jk>, JSON attributes will only be quoted when necessary.
* @param trimStrings If <jk>true</jk>, strings will be trimmed before being serialized.
* @param uriResolver The URI resolver for resolving URIs to absolute or root-relative form.
*/
protected JsonWriter(Writer out, boolean useWhitespace, int maxIndent, boolean escapeSolidus, char quoteChar,
boolean simpleMode, boolean trimStrings, UriResolver uriResolver) {
super(out, useWhitespace, maxIndent, trimStrings, quoteChar, uriResolver);
this.simpleMode = simpleMode;
this.escapeSolidus = escapeSolidus;
this.ec = escapeSolidus ? encodedChars2 : encodedChars;
}
/**
* Serializes the specified object as a JSON string value.
*
* @param s The object being serialized.
* @return This object (for method chaining).
* @throws IOException Should never happen.
*/
public JsonWriter stringValue(String s) throws IOException {
if (s == null)
return this;
boolean doConvert = false;
for (int i = 0; i < s.length() && ! doConvert; i++) {
char c = s.charAt(i);
doConvert |= ec.contains(c);
}
q();
if (! doConvert) {
out.append(s);
} else {
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (ec.contains(c)) {
if (c == '\n')
out.append('\\').append('n');
else if (c == '\t')
out.append('\\').append('t');
else if (c == '\b')
out.append('\\').append('b');
else if (c == '\f')
out.append('\\').append('f');
else if (c == quoteChar)
out.append('\\').append(quoteChar);
else if (c == '\\')
out.append('\\').append('\\');
else if (c == '/' && escapeSolidus)
out.append('\\').append('/');
else if (c != '\r')
out.append(c);
} else {
out.append(c);
}
}
}
q();
return this;
}
/**
* Serializes the specified object as a JSON attribute name.
*
* @param s The object being serialized.
* @return This object (for method chaining).
* @throws IOException Should never happen.
*/
public JsonWriter attr(String s) throws IOException {
/*
* Converts a Java string to an acceptable JSON attribute name. If
* simpleMode is true, then quotes will only be used if the attribute
* name consists of only alphanumeric characters.
*/
boolean doConvert = trimStrings || ! simpleMode; // Always convert when not in lax mode.
// If the attribute is null, it must always be printed as null without quotes.
// Technically, this isn't part of the JSON spec, but it does allow for null key values.
if (s == null) {
s = "null";
doConvert = false;
} else {
// Look for characters that would require the attribute to be quoted.
// All possible numbers should be caught here.
if (! doConvert) {
for (int i = 0; i < s.length() && ! doConvert; i++) {
char c = s.charAt(i);
doConvert |= ! (i == 0 ? validFirstAttrChars.contains(c) : validAttrChars.contains(c));
}
}
// Reserved words and blanks must be quoted.
if (! doConvert) {
if (s.isEmpty() || reservedWords.contains(s))
doConvert = true;
}
}
// If no conversion necessary, just print the attribute as-is.
if (doConvert)
stringValue(s);
else
out.append(s);
return this;
}
/**
* Appends a URI to the output.
*
* @param uri The URI to append to the output.
* @return This object (for method chaining).
* @throws IOException Thrown by underlying stream.
*/
public SerializerWriter uriValue(Object uri) throws IOException {
return stringValue(uriResolver.resolve(uri));
}
//-----------------------------------------------------------------------------------------------------------------
// Overridden methods
//-----------------------------------------------------------------------------------------------------------------
@Override /* SerializerWriter */
public JsonWriter cr(int depth) throws IOException {
super.cr(depth);
return this;
}
@Override /* SerializerWriter */
public JsonWriter cre(int depth) throws IOException {
super.cre(depth);
return this;
}
/**
* Performs an indentation only if we're currently past max indentation.
*
* @param depth The current indentation depth.
* @return This object (for method chaining).
* @throws IOException Thrown by underlying stream.
*/
public JsonWriter smi(int depth) throws IOException {
if (depth > maxIndent)
super.s();
return this;
}
@Override /* SerializerWriter */
public JsonWriter appendln(int indent, String text) throws IOException {
super.appendln(indent, text);
return this;
}
@Override /* SerializerWriter */
public JsonWriter appendln(String text) throws IOException {
super.appendln(text);
return this;
}
@Override /* SerializerWriter */
public JsonWriter append(int indent, String text) throws IOException {
super.append(indent, text);
return this;
}
@Override /* SerializerWriter */
public JsonWriter append(int indent, char c) throws IOException {
super.append(indent, c);
return this;
}
@Override /* SerializerWriter */
public JsonWriter s() throws IOException {
super.s();
return this;
}
/**
* Adds a space only if the current indentation level is below maxIndent.
*
* @param indent The number of spaces to indent.
* @return This object (for method chaining).
* @throws IOException Thrown by underlying stream.
*/
public JsonWriter s(int indent) throws IOException {
if (indent <= maxIndent)
super.s();
return this;
}
@Override /* SerializerWriter */
public JsonWriter q() throws IOException {
super.q();
return this;
}
@Override /* SerializerWriter */
public JsonWriter i(int indent) throws IOException {
super.i(indent);
return this;
}
@Override /* SerializerWriter */
public JsonWriter nl(int indent) throws IOException {
super.nl(indent);
return this;
}
@Override /* SerializerWriter */
public JsonWriter append(Object text) throws IOException {
super.append(text);
return this;
}
@Override /* SerializerWriter */
public JsonWriter append(String text) throws IOException {
super.append(text);
return this;
}
@Override /* SerializerWriter */
public JsonWriter appendIf(boolean b, String text) throws IOException {
super.appendIf(b, text);
return this;
}
@Override /* SerializerWriter */
public JsonWriter appendIf(boolean b, char c) throws IOException {
super.appendIf(b, c);
return this;
}
@Override /* SerializerWriter */
public JsonWriter append(char c) throws IOException {
super.append(c);
return this;
}
}