/*
* This code is copyrighted work by Daniel Luz <dev at mernen dot com>.
*
* Distributed under the Ruby and GPLv2 licenses; see COPYING and GPL files
* for details.
*/
package json.ext;
import org.jruby.Ruby;
import org.jruby.RubyBoolean;
import org.jruby.RubyClass;
import org.jruby.RubyHash;
import org.jruby.RubyInteger;
import org.jruby.RubyNumeric;
import org.jruby.RubyObject;
import org.jruby.RubyString;
import org.jruby.anno.JRubyMethod;
import org.jruby.runtime.Block;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ByteList;
/**
* The <code>JSON::Ext::Generator::State</code> class.
*
* <p>This class is used to create State instances, that are use to hold data
* while generating a JSON text from a a Ruby data structure.
*
* @author mernen
*/
public class GeneratorState extends RubyObject {
/**
* The indenting unit string. Will be repeated several times for larger
* indenting levels.
*/
private ByteList indent = ByteList.EMPTY_BYTELIST;
/**
* The spacing to be added after a semicolon on a JSON object.
* @see #spaceBefore
*/
private ByteList space = ByteList.EMPTY_BYTELIST;
/**
* The spacing to be added before a semicolon on a JSON object.
* @see #space
*/
private ByteList spaceBefore = ByteList.EMPTY_BYTELIST;
/**
* Any suffix to be added after the comma for each element on a JSON object.
* It is assumed to be a newline, if set.
*/
private ByteList objectNl = ByteList.EMPTY_BYTELIST;
/**
* Any suffix to be added after the comma for each element on a JSON Array.
* It is assumed to be a newline, if set.
*/
private ByteList arrayNl = ByteList.EMPTY_BYTELIST;
/**
* The maximum level of nesting of structures allowed.
* <code>0</code> means disabled.
*/
private int maxNesting = DEFAULT_MAX_NESTING;
static final int DEFAULT_MAX_NESTING = 100;
/**
* Whether special float values (<code>NaN</code>, <code>Infinity</code>,
* <code>-Infinity</code>) are accepted.
* If set to <code>false</code>, an exception will be thrown upon
* encountering one.
*/
private boolean allowNaN = DEFAULT_ALLOW_NAN;
static final boolean DEFAULT_ALLOW_NAN = false;
/**
* If set to <code>true</code> all JSON documents generated do not contain
* any other characters than ASCII characters.
*/
private boolean asciiOnly = DEFAULT_ASCII_ONLY;
static final boolean DEFAULT_ASCII_ONLY = false;
/**
* If set to <code>true</code> all JSON values generated might not be
* RFC-conform JSON documents.
*/
private boolean quirksMode = DEFAULT_QUIRKS_MODE;
static final boolean DEFAULT_QUIRKS_MODE = false;
/**
* The initial buffer length of this state. (This isn't really used on all
* non-C implementations.)
*/
private int bufferInitialLength = DEFAULT_BUFFER_INITIAL_LENGTH;
static final int DEFAULT_BUFFER_INITIAL_LENGTH = 1024;
/**
* The current depth (inside a #to_json call)
*/
private int depth = 0;
static final ObjectAllocator ALLOCATOR = new ObjectAllocator() {
public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
return new GeneratorState(runtime, klazz);
}
};
public GeneratorState(Ruby runtime, RubyClass metaClass) {
super(runtime, metaClass);
}
/**
* <code>State.from_state(opts)</code>
*
* <p>Creates a State object from <code>opts</code>, which ought to be
* {@link RubyHash Hash} to create a new <code>State</code> instance
* configured by <codes>opts</code>, something else to create an
* unconfigured instance. If <code>opts</code> is a <code>State</code>
* object, it is just returned.
* @param clazzParam The receiver of the method call
* ({@link RubyClass} <code>State</code>)
* @param opts The object to use as a base for the new <code>State</code>
* @param block The block passed to the method
* @return A <code>GeneratorState</code> as determined above
*/
@JRubyMethod(meta=true)
public static IRubyObject from_state(ThreadContext context,
IRubyObject klass, IRubyObject opts) {
return fromState(context, opts);
}
static GeneratorState fromState(ThreadContext context, IRubyObject opts) {
return fromState(context, RuntimeInfo.forRuntime(context.getRuntime()), opts);
}
static GeneratorState fromState(ThreadContext context, RuntimeInfo info,
IRubyObject opts) {
RubyClass klass = info.generatorStateClass.get();
if (opts != null) {
// if the given parameter is a Generator::State, return itself
if (klass.isInstance(opts)) return (GeneratorState)opts;
// if the given parameter is a Hash, pass it to the instantiator
if (context.getRuntime().getHash().isInstance(opts)) {
return (GeneratorState)klass.newInstance(context,
new IRubyObject[] {opts}, Block.NULL_BLOCK);
}
}
// for other values, return the safe prototype
return (GeneratorState)info.getSafeStatePrototype(context).dup();
}
/**
* <code>State#initialize(opts = {})</code>
*
* Instantiates a new <code>State</code> object, configured by <code>opts</code>.
*
* <code>opts</code> can have the following keys:
*
* <dl>
* <dt><code>:indent</code>
* <dd>a {@link RubyString String} used to indent levels (default: <code>""</code>)
* <dt><code>:space</code>
* <dd>a String that is put after a <code>':'</code> or <code>','</code>
* delimiter (default: <code>""</code>)
* <dt><code>:space_before</code>
* <dd>a String that is put before a <code>":"</code> pair delimiter
* (default: <code>""</code>)
* <dt><code>:object_nl</code>
* <dd>a String that is put at the end of a JSON object (default: <code>""</code>)
* <dt><code>:array_nl</code>
* <dd>a String that is put at the end of a JSON array (default: <code>""</code>)
* <dt><code>:allow_nan</code>
* <dd><code>true</code> if <code>NaN</code>, <code>Infinity</code>, and
* <code>-Infinity</code> should be generated, otherwise an exception is
* thrown if these values are encountered.
* This options defaults to <code>false</code>.
*/
@JRubyMethod(optional=1, visibility=Visibility.PRIVATE)
public IRubyObject initialize(ThreadContext context, IRubyObject[] args) {
configure(context, args.length > 0 ? args[0] : null);
return this;
}
@JRubyMethod
public IRubyObject initialize_copy(ThreadContext context, IRubyObject vOrig) {
Ruby runtime = context.getRuntime();
if (!(vOrig instanceof GeneratorState)) {
throw runtime.newTypeError(vOrig, getType());
}
GeneratorState orig = (GeneratorState)vOrig;
this.indent = orig.indent;
this.space = orig.space;
this.spaceBefore = orig.spaceBefore;
this.objectNl = orig.objectNl;
this.arrayNl = orig.arrayNl;
this.maxNesting = orig.maxNesting;
this.allowNaN = orig.allowNaN;
this.asciiOnly = orig.asciiOnly;
this.quirksMode = orig.quirksMode;
this.bufferInitialLength = orig.bufferInitialLength;
this.depth = orig.depth;
return this;
}
/**
* Generates a valid JSON document from object <code>obj</code> and returns
* the result. If no valid JSON document can be created this method raises
* a GeneratorError exception.
*/
@JRubyMethod
public IRubyObject generate(ThreadContext context, IRubyObject obj) {
RubyString result = Generator.generateJson(context, obj, this);
if (!quirksMode && !objectOrArrayLiteral(result)) {
throw Utils.newException(context, Utils.M_GENERATOR_ERROR,
"only generation of JSON objects or arrays allowed");
}
RuntimeInfo info = RuntimeInfo.forRuntime(context.getRuntime());
if (info.encodingsSupported()) {
result.force_encoding(context, info.utf8.get());
}
return result;
}
/**
* Ensures the given string is in the form "[...]" or "{...}", being
* possibly surrounded by white space.
* The string's encoding must be ASCII-compatible.
* @param value
* @return
*/
private static boolean objectOrArrayLiteral(RubyString value) {
ByteList bl = value.getByteList();
int len = bl.length();
for (int pos = 0; pos < len - 1; pos++) {
int b = bl.get(pos);
if (Character.isWhitespace(b)) continue;
// match the opening brace
switch (b) {
case '[':
return matchClosingBrace(bl, pos, len, ']');
case '{':
return matchClosingBrace(bl, pos, len, '}');
default:
return false;
}
}
return false;
}
private static boolean matchClosingBrace(ByteList bl, int pos, int len,
int brace) {
for (int endPos = len - 1; endPos > pos; endPos--) {
int b = bl.get(endPos);
if (Character.isWhitespace(b)) continue;
return b == brace;
}
return false;
}
@JRubyMethod(name="[]", required=1)
public IRubyObject op_aref(ThreadContext context, IRubyObject vName) {
String name = vName.asJavaString();
if (getMetaClass().isMethodBound(name, true)) {
return send(context, vName, Block.NULL_BLOCK);
} else {
return getInstanceVariables().getInstanceVariable("@" + name);
}
}
@JRubyMethod(name="[]=", required=2)
public IRubyObject op_aset(ThreadContext context, IRubyObject vName, IRubyObject value) {
String name = vName.asJavaString();
String nameWriter = name + "=";
if (getMetaClass().isMethodBound(nameWriter, true)) {
return send(context, context.getRuntime().newString(nameWriter), value, Block.NULL_BLOCK);
} else {
getInstanceVariables().setInstanceVariable("@" + name, value);
}
return context.getRuntime().getNil();
}
public ByteList getIndent() {
return indent;
}
@JRubyMethod(name="indent")
public RubyString indent_get(ThreadContext context) {
return context.getRuntime().newString(indent);
}
@JRubyMethod(name="indent=")
public IRubyObject indent_set(ThreadContext context, IRubyObject indent) {
this.indent = prepareByteList(context, indent);
return indent;
}
public ByteList getSpace() {
return space;
}
@JRubyMethod(name="space")
public RubyString space_get(ThreadContext context) {
return context.getRuntime().newString(space);
}
@JRubyMethod(name="space=")
public IRubyObject space_set(ThreadContext context, IRubyObject space) {
this.space = prepareByteList(context, space);
return space;
}
public ByteList getSpaceBefore() {
return spaceBefore;
}
@JRubyMethod(name="space_before")
public RubyString space_before_get(ThreadContext context) {
return context.getRuntime().newString(spaceBefore);
}
@JRubyMethod(name="space_before=")
public IRubyObject space_before_set(ThreadContext context,
IRubyObject spaceBefore) {
this.spaceBefore = prepareByteList(context, spaceBefore);
return spaceBefore;
}
public ByteList getObjectNl() {
return objectNl;
}
@JRubyMethod(name="object_nl")
public RubyString object_nl_get(ThreadContext context) {
return context.getRuntime().newString(objectNl);
}
@JRubyMethod(name="object_nl=")
public IRubyObject object_nl_set(ThreadContext context,
IRubyObject objectNl) {
this.objectNl = prepareByteList(context, objectNl);
return objectNl;
}
public ByteList getArrayNl() {
return arrayNl;
}
@JRubyMethod(name="array_nl")
public RubyString array_nl_get(ThreadContext context) {
return context.getRuntime().newString(arrayNl);
}
@JRubyMethod(name="array_nl=")
public IRubyObject array_nl_set(ThreadContext context,
IRubyObject arrayNl) {
this.arrayNl = prepareByteList(context, arrayNl);
return arrayNl;
}
@JRubyMethod(name="check_circular?")
public RubyBoolean check_circular_p(ThreadContext context) {
return context.getRuntime().newBoolean(maxNesting != 0);
}
/**
* Returns the maximum level of nesting configured for this state.
*/
public int getMaxNesting() {
return maxNesting;
}
@JRubyMethod(name="max_nesting")
public RubyInteger max_nesting_get(ThreadContext context) {
return context.getRuntime().newFixnum(maxNesting);
}
@JRubyMethod(name="max_nesting=")
public IRubyObject max_nesting_set(IRubyObject max_nesting) {
maxNesting = RubyNumeric.fix2int(max_nesting);
return max_nesting;
}
public boolean allowNaN() {
return allowNaN;
}
@JRubyMethod(name="allow_nan?")
public RubyBoolean allow_nan_p(ThreadContext context) {
return context.getRuntime().newBoolean(allowNaN);
}
public boolean asciiOnly() {
return asciiOnly;
}
@JRubyMethod(name="ascii_only?")
public RubyBoolean ascii_only_p(ThreadContext context) {
return context.getRuntime().newBoolean(asciiOnly);
}
@JRubyMethod(name="quirks_mode")
public RubyBoolean quirks_mode_get(ThreadContext context) {
return context.getRuntime().newBoolean(quirksMode);
}
@JRubyMethod(name="quirks_mode=")
public IRubyObject quirks_mode_set(IRubyObject quirks_mode) {
quirksMode = quirks_mode.isTrue();
return quirks_mode.getRuntime().newBoolean(quirksMode);
}
@JRubyMethod(name="buffer_initial_length")
public RubyInteger buffer_initial_length_get(ThreadContext context) {
return context.getRuntime().newFixnum(bufferInitialLength);
}
@JRubyMethod(name="buffer_initial_length=")
public IRubyObject buffer_initial_length_set(IRubyObject buffer_initial_length) {
int newLength = RubyNumeric.fix2int(buffer_initial_length);
if (newLength > 0) bufferInitialLength = newLength;
return buffer_initial_length;
}
@JRubyMethod(name="quirks_mode?")
public RubyBoolean quirks_mode_p(ThreadContext context) {
return context.getRuntime().newBoolean(quirksMode);
}
public int getDepth() {
return depth;
}
@JRubyMethod(name="depth")
public RubyInteger depth_get(ThreadContext context) {
return context.getRuntime().newFixnum(depth);
}
@JRubyMethod(name="depth=")
public IRubyObject depth_set(IRubyObject vDepth) {
depth = RubyNumeric.fix2int(vDepth);
return vDepth;
}
private ByteList prepareByteList(ThreadContext context, IRubyObject value) {
RubyString str = value.convertToString();
RuntimeInfo info = RuntimeInfo.forRuntime(context.getRuntime());
if (info.encodingsSupported() && str.encoding(context) != info.utf8.get()) {
str = (RubyString)str.encode(context, info.utf8.get());
}
return str.getByteList().dup();
}
/**
* <code>State#configure(opts)</code>
*
* <p>Configures this State instance with the {@link RubyHash Hash}
* <code>opts</code>, and returns itself.
* @param vOpts The options hash
* @return The receiver
*/
@JRubyMethod(alias = "merge")
public IRubyObject configure(ThreadContext context, IRubyObject vOpts) {
OptionsReader opts = new OptionsReader(context, vOpts);
ByteList indent = opts.getString("indent");
if (indent != null) this.indent = indent;
ByteList space = opts.getString("space");
if (space != null) this.space = space;
ByteList spaceBefore = opts.getString("space_before");
if (spaceBefore != null) this.spaceBefore = spaceBefore;
ByteList arrayNl = opts.getString("array_nl");
if (arrayNl != null) this.arrayNl = arrayNl;
ByteList objectNl = opts.getString("object_nl");
if (objectNl != null) this.objectNl = objectNl;
maxNesting = opts.getInt("max_nesting", DEFAULT_MAX_NESTING);
allowNaN = opts.getBool("allow_nan", DEFAULT_ALLOW_NAN);
asciiOnly = opts.getBool("ascii_only", DEFAULT_ASCII_ONLY);
quirksMode = opts.getBool("quirks_mode", DEFAULT_QUIRKS_MODE);
bufferInitialLength = opts.getInt("buffer_initial_length", DEFAULT_BUFFER_INITIAL_LENGTH);
depth = opts.getInt("depth", 0);
return this;
}
/**
* <code>State#to_h()</code>
*
* <p>Returns the configuration instance variables as a hash, that can be
* passed to the configure method.
* @return the hash
*/
@JRubyMethod(alias = "to_hash")
public RubyHash to_h(ThreadContext context) {
Ruby runtime = context.getRuntime();
RubyHash result = RubyHash.newHash(runtime);
result.op_aset(context, runtime.newSymbol("indent"), indent_get(context));
result.op_aset(context, runtime.newSymbol("space"), space_get(context));
result.op_aset(context, runtime.newSymbol("space_before"), space_before_get(context));
result.op_aset(context, runtime.newSymbol("object_nl"), object_nl_get(context));
result.op_aset(context, runtime.newSymbol("array_nl"), array_nl_get(context));
result.op_aset(context, runtime.newSymbol("allow_nan"), allow_nan_p(context));
result.op_aset(context, runtime.newSymbol("ascii_only"), ascii_only_p(context));
result.op_aset(context, runtime.newSymbol("quirks_mode"), quirks_mode_p(context));
result.op_aset(context, runtime.newSymbol("max_nesting"), max_nesting_get(context));
result.op_aset(context, runtime.newSymbol("depth"), depth_get(context));
result.op_aset(context, runtime.newSymbol("buffer_initial_length"), buffer_initial_length_get(context));
for (String name: getInstanceVariableNameList()) {
result.op_aset(context, runtime.newSymbol(name.substring(1)), getInstanceVariables().getInstanceVariable(name));
}
return result;
}
public int increaseDepth() {
depth++;
checkMaxNesting();
return depth;
}
public int decreaseDepth() {
return --depth;
}
/**
* Checks if the current depth is allowed as per this state's options.
* @param context
* @param depth The corrent depth
*/
private void checkMaxNesting() {
if (maxNesting != 0 && depth > maxNesting) {
depth--;
throw Utils.newException(getRuntime().getCurrentContext(),
Utils.M_NESTING_ERROR, "nesting of " + depth + " is too deep");
}
}
}