/* * This code is copyrighted work by Daniel Luz . * * Distributed under the Ruby license: https://www.ruby-lang.org/en/about/license.txt */ 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 JSON::Ext::Generator::State class. * *

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. * 0 means disabled. */ private int maxNesting = DEFAULT_MAX_NESTING; static final int DEFAULT_MAX_NESTING = 100; /** * Whether special float values (NaN, Infinity, * -Infinity) are accepted. * If set to false, an exception will be thrown upon * encountering one. */ private boolean allowNaN = DEFAULT_ALLOW_NAN; static final boolean DEFAULT_ALLOW_NAN = false; /** * If set to true 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 true 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); } /** * State.from_state(opts) * *

Creates a State object from opts, which ought to be * {@link RubyHash Hash} to create a new State instance * configured by opts, something else to create an * unconfigured instance. If opts is a State * object, it is just returned. * @param clazzParam The receiver of the method call * ({@link RubyClass} State) * @param opts The object to use as a base for the new State * @param block The block passed to the method * @return A GeneratorState 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(); } /** * State#initialize(opts = {}) * * Instantiates a new State object, configured by opts. * * opts can have the following keys: * *

*
:indent *
a {@link RubyString String} used to indent levels (default: "") *
:space *
a String that is put after a ':' or ',' * delimiter (default: "") *
:space_before *
a String that is put before a ":" pair delimiter * (default: "") *
:object_nl *
a String that is put at the end of a JSON object (default: "") *
:array_nl *
a String that is put at the end of a JSON array (default: "") *
:allow_nan *
true if NaN, Infinity, and * -Infinity should be generated, otherwise an exception is * thrown if these values are encountered. * This options defaults to false. */ @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 obj 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 { IRubyObject value = getInstanceVariables().getInstanceVariable("@" + name); return value == null ? context.nil : value; } } @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(); } /** * State#configure(opts) * *

Configures this State instance with the {@link RubyHash Hash} * opts, 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; } /** * State#to_h() * *

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"); } } }