summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCharles Oliver Nutter <headius@headius.com>2010-08-03 12:21:11 -0500
committerCharles Oliver Nutter <headius@headius.com>2010-08-03 12:21:11 -0500
commit2288d74466463d34a5054ef413e099c850cd832b (patch)
tree76db71c439f26b5c3b7e60fb15178a2bce92c2fb
parent6ebee56297dfd9c380f8f8fb6c0b8bb5254901b7 (diff)
downloadjson-2288d74466463d34a5054ef413e099c850cd832b.tar.gz
Merge JRuby-compatible extension into json proper.
-rw-r--r--COPYING-json-jruby57
-rw-r--r--README-json-jruby.markdown33
-rw-r--r--build.xml71
-rw-r--r--json-jruby.gemspec20
-rw-r--r--nbproject/build-impl.xml700
-rw-r--r--nbproject/genfiles.properties8
-rw-r--r--nbproject/project.properties67
-rw-r--r--nbproject/project.xml16
-rw-r--r--src/json/ext/ByteListTranscoder.java167
-rw-r--r--src/json/ext/Generator.java452
-rw-r--r--src/json/ext/GeneratorMethods.java231
-rw-r--r--src/json/ext/GeneratorService.java42
-rw-r--r--src/json/ext/GeneratorState.java443
-rw-r--r--src/json/ext/OptionsReader.java113
-rw-r--r--src/json/ext/Parser.java2269
-rw-r--r--src/json/ext/Parser.rl799
-rw-r--r--src/json/ext/ParserService.java34
-rw-r--r--src/json/ext/RuntimeInfo.java119
-rw-r--r--src/json/ext/StringDecoder.java166
-rw-r--r--src/json/ext/StringEncoder.java106
-rw-r--r--src/json/ext/Utils.java89
21 files changed, 6002 insertions, 0 deletions
diff --git a/COPYING-json-jruby b/COPYING-json-jruby
new file mode 100644
index 0000000..137a3da
--- /dev/null
+++ b/COPYING-json-jruby
@@ -0,0 +1,57 @@
+JSON-JRuby is copyrighted free software by Daniel Luz <mernen at gmail dot com>,
+and is a derivative work of Florian Frank's json library <flori at ping dot de>.
+You can redistribute it and/or modify it under either the terms of the GPL
+version 2 (see the file GPL), or the conditions below:
+
+ 1. You may make and give away verbatim copies of the source form of the
+ software without restriction, provided that you duplicate all of the
+ original copyright notices and associated disclaimers.
+
+ 2. You may modify your copy of the software in any way, provided that
+ you do at least ONE of the following:
+
+ a) place your modifications in the Public Domain or otherwise
+ make them Freely Available, such as by posting said
+ modifications to Usenet or an equivalent medium, or by allowing
+ the author to include your modifications in the software.
+
+ b) use the modified software only within your corporation or
+ organization.
+
+ c) give non-standard binaries non-standard names, with
+ instructions on where to get the original software distribution.
+
+ d) make other distribution arrangements with the author.
+
+ 3. You may distribute the software in object code or binary form,
+ provided that you do at least ONE of the following:
+
+ a) distribute the binaries and library files of the software,
+ together with instructions (in the manual page or equivalent)
+ on where to get the original distribution.
+
+ b) accompany the distribution with the machine-readable source of
+ the software.
+
+ c) give non-standard binaries non-standard names, with
+ instructions on where to get the original software distribution.
+
+ d) make other distribution arrangements with the author.
+
+ 4. You may modify and include the part of the software into any other
+ software (possibly commercial). But some files in the distribution
+ are not written by the author, so that they are not under these terms.
+
+ For the list of those files and their copying conditions, see the
+ file LEGAL.
+
+ 5. The scripts and library files supplied as input to or produced as
+ output from the software do not automatically fall under the
+ copyright of the software, but belong to whomever generated them,
+ and may be sold commercially, and may be aggregated with this
+ software.
+
+ 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
+ IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE.
diff --git a/README-json-jruby.markdown b/README-json-jruby.markdown
new file mode 100644
index 0000000..1336837
--- /dev/null
+++ b/README-json-jruby.markdown
@@ -0,0 +1,33 @@
+JSON-JRuby
+==========
+
+JSON-JRuby is a port of Florian Frank's native
+[`json` library](http://json.rubyforge.org/) to JRuby.
+It aims to be a perfect drop-in replacement for `json_pure`.
+
+
+Development version
+===================
+
+The latest version is available from the
+[Git repository](http://github.com/mernen/json-jruby/tree):
+
+ git clone git://github.com/mernen/json-jruby.git
+
+
+Compiling
+=========
+
+You'll need JRuby version 1.2 or greater to build JSON-JRuby.
+Its path must be set on the `jruby.dir` property of
+`nbproject/project.properties` (defaults to `../jruby`).
+
+Additionally, you'll need [Ant](http://ant.apache.org/), and
+[Ragel](http://www.cs.queensu.ca/~thurston/ragel/) 6.4 or greater.
+
+Then, from the folder where the sources are located, type:
+
+ ant clean jar
+
+to clean any leftovers from previous builds and generate the `.jar` files.
+To generate a RubyGem, specify the `gem` action rather than `jar`.
diff --git a/build.xml b/build.xml
new file mode 100644
index 0000000..e11e27f
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project name="JSON-JRuby" default="gem" basedir=".">
+ <description>Builds, tests, and runs the project JSON-JRuby.</description>
+ <import file="nbproject/build-impl.xml"/>
+
+ <target name="ragel" description="Generate parser with Ragel.">
+ <exec executable="ragel" failonerror="true">
+ <arg value="-J"/>
+ <arg value="${src.dir}/json/ext/Parser.rl"/>
+ </exec>
+ </target>
+
+ <target name="-pre-compile" depends="ragel">
+ </target>
+
+ <target name="clean-dist">
+ <delete>
+ <file file="${generator.jar}"/>
+ <file file="${parser.jar}"/>
+ <fileset dir="." includes="*.gem"/>
+ </delete>
+ </target>
+
+ <target name="-post-clean" depends="clean-dist,ragel"/>
+
+ <target name="-do-jar-without-manifest">
+ <mkdir dir="${lib.dir}/json/ext"/>
+ <delete>
+ <file file="${generator.jar}"/>
+ <file file="${parser.jar}"/>
+ </delete>
+ <jar destfile="${generator.jar}">
+ <fileset dir="${build.classes.dir}">
+ <include name="json/ext/ByteListTranscoder*.class"/>
+ <include name="json/ext/Generator*.class"/>
+ <include name="json/ext/OptionsReader*.class"/>
+ <include name="json/ext/RuntimeInfo*.class"/>
+ <include name="json/ext/StringEncoder*.class"/>
+ <include name="json/ext/Utils*.class"/>
+ </fileset>
+ </jar>
+ <jar destfile="${parser.jar}">
+ <fileset dir="${build.classes.dir}">
+ <include name="json/ext/ByteListTranscoder*.class"/>
+ <include name="json/ext/OptionsReader*.class"/>
+ <include name="json/ext/Parser*.class"/>
+ <include name="json/ext/RuntimeInfo*.class"/>
+ <include name="json/ext/StringDecoder*.class"/>
+ <include name="json/ext/Utils*.class"/>
+ </fileset>
+ </jar>
+ </target>
+
+ <target name="gem" depends="jar" description="Build a RubyGem.">
+ <exec executable="${jruby.dir}/bin/jruby">
+ <arg value="json-jruby.gemspec"/>
+ </exec>
+ </target>
+
+ <target name="-post-test" depends="ruby-tests" />
+
+ <target name="ruby-tests" depends="jar"
+ description="Perform the json ruby library tests.">
+ <exec executable="${jruby.dir}/bin/jruby" failonerror="true">
+ <arg value="-v"/>
+ <arg value="-I"/>
+ <arg value="${lib.dir}"/>
+ <arg value="tests/runner.rb"/>
+ </exec>
+ </target>
+</project>
diff --git a/json-jruby.gemspec b/json-jruby.gemspec
new file mode 100644
index 0000000..144f650
--- /dev/null
+++ b/json-jruby.gemspec
@@ -0,0 +1,20 @@
+#! /usr/bin/env jruby
+require "rubygems"
+
+spec = Gem::Specification.new do |s|
+ s.name = "json"
+ s.version = File.read("VERSION").chomp
+ s.summary = "JSON implementation for JRuby"
+ s.description = "A JSON implementation as a JRuby extension."
+ s.author = "Daniel Luz"
+ s.email = "dev+ruby@mernen.com"
+ s.homepage = "http://json-jruby.rubyforge.org/"
+ s.platform = 'java'
+ s.rubyforge_project = "json-jruby"
+
+ s.files = Dir["{docs,lib,tests}/**/*"]
+end
+
+if $0 == __FILE__
+ Gem::Builder.new(spec).build
+end
diff --git a/nbproject/build-impl.xml b/nbproject/build-impl.xml
new file mode 100644
index 0000000..103789a
--- /dev/null
+++ b/nbproject/build-impl.xml
@@ -0,0 +1,700 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+*** GENERATED FROM project.xml - DO NOT EDIT ***
+*** EDIT ../build.xml INSTEAD ***
+
+For the purpose of easier reading the script
+is divided into following sections:
+
+ - initialization
+ - compilation
+ - jar
+ - execution
+ - debugging
+ - javadoc
+ - junit compilation
+ - junit execution
+ - junit debugging
+ - applet
+ - cleanup
+
+ -->
+<project xmlns:j2seproject1="http://www.netbeans.org/ns/j2se-project/1" xmlns:j2seproject3="http://www.netbeans.org/ns/j2se-project/3" xmlns:jaxrpc="http://www.netbeans.org/ns/j2se-project/jax-rpc" basedir=".." default="default" name="JSON-JRuby-impl">
+ <fail message="Please build using Ant 1.7.1 or higher.">
+ <condition>
+ <not>
+ <antversion atleast="1.7.1"/>
+ </not>
+ </condition>
+ </fail>
+ <target depends="test,jar,javadoc" description="Build and test whole project." name="default"/>
+ <!--
+ ======================
+ INITIALIZATION SECTION
+ ======================
+ -->
+ <target name="-pre-init">
+ <!-- Empty placeholder for easier customization. -->
+ <!-- You can override this target in the ../build.xml file. -->
+ </target>
+ <target depends="-pre-init" name="-init-private">
+ <property file="nbproject/private/config.properties"/>
+ <property file="nbproject/private/configs/${config}.properties"/>
+ <property file="nbproject/private/private.properties"/>
+ </target>
+ <target depends="-pre-init,-init-private" name="-init-user">
+ <property file="${user.properties.file}"/>
+ <!-- The two properties below are usually overridden -->
+ <!-- by the active platform. Just a fallback. -->
+ <property name="default.javac.source" value="1.4"/>
+ <property name="default.javac.target" value="1.4"/>
+ </target>
+ <target depends="-pre-init,-init-private,-init-user" name="-init-project">
+ <property file="nbproject/configs/${config}.properties"/>
+ <property file="nbproject/project.properties"/>
+ </target>
+ <target depends="-pre-init,-init-private,-init-user,-init-project,-init-macrodef-property" name="-do-init">
+ <available file="${manifest.file}" property="manifest.available"/>
+ <condition property="manifest.available+main.class">
+ <and>
+ <isset property="manifest.available"/>
+ <isset property="main.class"/>
+ <not>
+ <equals arg1="${main.class}" arg2="" trim="true"/>
+ </not>
+ </and>
+ </condition>
+ <condition property="manifest.available+main.class+mkdist.available">
+ <and>
+ <istrue value="${manifest.available+main.class}"/>
+ <isset property="libs.CopyLibs.classpath"/>
+ </and>
+ </condition>
+ <condition property="have.tests">
+ <or>
+ <available file="${test.src.dir}"/>
+ </or>
+ </condition>
+ <condition property="have.sources">
+ <or>
+ <available file="${src.dir}"/>
+ </or>
+ </condition>
+ <condition property="netbeans.home+have.tests">
+ <and>
+ <isset property="netbeans.home"/>
+ <isset property="have.tests"/>
+ </and>
+ </condition>
+ <condition property="no.javadoc.preview">
+ <and>
+ <isset property="javadoc.preview"/>
+ <isfalse value="${javadoc.preview}"/>
+ </and>
+ </condition>
+ <property name="run.jvmargs" value=""/>
+ <property name="javac.compilerargs" value=""/>
+ <property name="work.dir" value="${basedir}"/>
+ <condition property="no.deps">
+ <and>
+ <istrue value="${no.dependencies}"/>
+ </and>
+ </condition>
+ <property name="javac.debug" value="true"/>
+ <property name="javadoc.preview" value="true"/>
+ <property name="application.args" value=""/>
+ <property name="source.encoding" value="${file.encoding}"/>
+ <condition property="javadoc.encoding.used" value="${javadoc.encoding}">
+ <and>
+ <isset property="javadoc.encoding"/>
+ <not>
+ <equals arg1="${javadoc.encoding}" arg2=""/>
+ </not>
+ </and>
+ </condition>
+ <property name="javadoc.encoding.used" value="${source.encoding}"/>
+ <property name="includes" value="**"/>
+ <property name="excludes" value=""/>
+ <property name="do.depend" value="false"/>
+ <condition property="do.depend.true">
+ <istrue value="${do.depend}"/>
+ </condition>
+ <condition else="" property="javac.compilerargs.jaxws" value="-Djava.endorsed.dirs='${jaxws.endorsed.dir}'">
+ <and>
+ <isset property="jaxws.endorsed.dir"/>
+ <available file="nbproject/jaxws-build.xml"/>
+ </and>
+ </condition>
+ </target>
+ <target name="-post-init">
+ <!-- Empty placeholder for easier customization. -->
+ <!-- You can override this target in the ../build.xml file. -->
+ </target>
+ <target depends="-pre-init,-init-private,-init-user,-init-project,-do-init" name="-init-check">
+ <fail unless="src.dir">Must set src.dir</fail>
+ <fail unless="test.src.dir">Must set test.src.dir</fail>
+ <fail unless="build.dir">Must set build.dir</fail>
+ <fail unless="dist.dir">Must set dist.dir</fail>
+ <fail unless="build.classes.dir">Must set build.classes.dir</fail>
+ <fail unless="dist.javadoc.dir">Must set dist.javadoc.dir</fail>
+ <fail unless="build.test.classes.dir">Must set build.test.classes.dir</fail>
+ <fail unless="build.test.results.dir">Must set build.test.results.dir</fail>
+ <fail unless="build.classes.excludes">Must set build.classes.excludes</fail>
+ <fail unless="dist.jar">Must set dist.jar</fail>
+ </target>
+ <target name="-init-macrodef-property">
+ <macrodef name="property" uri="http://www.netbeans.org/ns/j2se-project/1">
+ <attribute name="name"/>
+ <attribute name="value"/>
+ <sequential>
+ <property name="@{name}" value="${@{value}}"/>
+ </sequential>
+ </macrodef>
+ </target>
+ <target name="-init-macrodef-javac">
+ <macrodef name="javac" uri="http://www.netbeans.org/ns/j2se-project/3">
+ <attribute default="${src.dir}" name="srcdir"/>
+ <attribute default="${build.classes.dir}" name="destdir"/>
+ <attribute default="${javac.classpath}" name="classpath"/>
+ <attribute default="${includes}" name="includes"/>
+ <attribute default="${excludes}" name="excludes"/>
+ <attribute default="${javac.debug}" name="debug"/>
+ <attribute default="${empty.dir}" name="sourcepath"/>
+ <attribute default="${empty.dir}" name="gensrcdir"/>
+ <element name="customize" optional="true"/>
+ <sequential>
+ <property location="${build.dir}/empty" name="empty.dir"/>
+ <mkdir dir="${empty.dir}"/>
+ <javac debug="@{debug}" deprecation="${javac.deprecation}" destdir="@{destdir}" encoding="${source.encoding}" excludes="@{excludes}" includeantruntime="false" includes="@{includes}" source="${javac.source}" sourcepath="@{sourcepath}" srcdir="@{srcdir}" target="${javac.target}">
+ <src>
+ <dirset dir="@{gensrcdir}" erroronmissingdir="false">
+ <include name="*"/>
+ </dirset>
+ </src>
+ <classpath>
+ <path path="@{classpath}"/>
+ </classpath>
+ <compilerarg line="${javac.compilerargs} ${javac.compilerargs.jaxws}"/>
+ <customize/>
+ </javac>
+ </sequential>
+ </macrodef>
+ <macrodef name="depend" uri="http://www.netbeans.org/ns/j2se-project/3">
+ <attribute default="${src.dir}" name="srcdir"/>
+ <attribute default="${build.classes.dir}" name="destdir"/>
+ <attribute default="${javac.classpath}" name="classpath"/>
+ <sequential>
+ <depend cache="${build.dir}/depcache" destdir="@{destdir}" excludes="${excludes}" includes="${includes}" srcdir="@{srcdir}">
+ <classpath>
+ <path path="@{classpath}"/>
+ </classpath>
+ </depend>
+ </sequential>
+ </macrodef>
+ <macrodef name="force-recompile" uri="http://www.netbeans.org/ns/j2se-project/3">
+ <attribute default="${build.classes.dir}" name="destdir"/>
+ <sequential>
+ <fail unless="javac.includes">Must set javac.includes</fail>
+ <pathconvert pathsep="," property="javac.includes.binary">
+ <path>
+ <filelist dir="@{destdir}" files="${javac.includes}"/>
+ </path>
+ <globmapper from="*.java" to="*.class"/>
+ </pathconvert>
+ <delete>
+ <files includes="${javac.includes.binary}"/>
+ </delete>
+ </sequential>
+ </macrodef>
+ </target>
+ <target name="-init-macrodef-junit">
+ <macrodef name="junit" uri="http://www.netbeans.org/ns/j2se-project/3">
+ <attribute default="${includes}" name="includes"/>
+ <attribute default="${excludes}" name="excludes"/>
+ <attribute default="**" name="testincludes"/>
+ <sequential>
+ <junit dir="${work.dir}" errorproperty="tests.failed" failureproperty="tests.failed" fork="true" showoutput="true">
+ <batchtest todir="${build.test.results.dir}">
+ <fileset dir="${test.src.dir}" excludes="@{excludes},${excludes}" includes="@{includes}">
+ <filename name="@{testincludes}"/>
+ </fileset>
+ </batchtest>
+ <classpath>
+ <path path="${run.test.classpath}"/>
+ </classpath>
+ <syspropertyset>
+ <propertyref prefix="test-sys-prop."/>
+ <mapper from="test-sys-prop.*" to="*" type="glob"/>
+ </syspropertyset>
+ <formatter type="brief" usefile="false"/>
+ <formatter type="xml"/>
+ <jvmarg line="${run.jvmargs}"/>
+ </junit>
+ </sequential>
+ </macrodef>
+ </target>
+ <target depends="-init-debug-args" name="-init-macrodef-nbjpda">
+ <macrodef name="nbjpdastart" uri="http://www.netbeans.org/ns/j2se-project/1">
+ <attribute default="${main.class}" name="name"/>
+ <attribute default="${debug.classpath}" name="classpath"/>
+ <attribute default="" name="stopclassname"/>
+ <sequential>
+ <nbjpdastart addressproperty="jpda.address" name="@{name}" stopclassname="@{stopclassname}" transport="${debug-transport}">
+ <classpath>
+ <path path="@{classpath}"/>
+ </classpath>
+ </nbjpdastart>
+ </sequential>
+ </macrodef>
+ <macrodef name="nbjpdareload" uri="http://www.netbeans.org/ns/j2se-project/1">
+ <attribute default="${build.classes.dir}" name="dir"/>
+ <sequential>
+ <nbjpdareload>
+ <fileset dir="@{dir}" includes="${fix.classes}">
+ <include name="${fix.includes}*.class"/>
+ </fileset>
+ </nbjpdareload>
+ </sequential>
+ </macrodef>
+ </target>
+ <target name="-init-debug-args">
+ <property name="version-output" value="java version &quot;${ant.java.version}"/>
+ <condition property="have-jdk-older-than-1.4">
+ <or>
+ <contains string="${version-output}" substring="java version &quot;1.0"/>
+ <contains string="${version-output}" substring="java version &quot;1.1"/>
+ <contains string="${version-output}" substring="java version &quot;1.2"/>
+ <contains string="${version-output}" substring="java version &quot;1.3"/>
+ </or>
+ </condition>
+ <condition else="-Xdebug" property="debug-args-line" value="-Xdebug -Xnoagent -Djava.compiler=none">
+ <istrue value="${have-jdk-older-than-1.4}"/>
+ </condition>
+ <condition else="dt_socket" property="debug-transport-by-os" value="dt_shmem">
+ <os family="windows"/>
+ </condition>
+ <condition else="${debug-transport-by-os}" property="debug-transport" value="${debug.transport}">
+ <isset property="debug.transport"/>
+ </condition>
+ </target>
+ <target depends="-init-debug-args" name="-init-macrodef-debug">
+ <macrodef name="debug" uri="http://www.netbeans.org/ns/j2se-project/3">
+ <attribute default="${main.class}" name="classname"/>
+ <attribute default="${debug.classpath}" name="classpath"/>
+ <element name="customize" optional="true"/>
+ <sequential>
+ <java classname="@{classname}" dir="${work.dir}" fork="true">
+ <jvmarg line="${debug-args-line}"/>
+ <jvmarg value="-Xrunjdwp:transport=${debug-transport},address=${jpda.address}"/>
+ <jvmarg value="-Dfile.encoding=${source.encoding}"/>
+ <redirector errorencoding="${source.encoding}" inputencoding="${source.encoding}" outputencoding="${source.encoding}"/>
+ <jvmarg line="${run.jvmargs}"/>
+ <classpath>
+ <path path="@{classpath}"/>
+ </classpath>
+ <syspropertyset>
+ <propertyref prefix="run-sys-prop."/>
+ <mapper from="run-sys-prop.*" to="*" type="glob"/>
+ </syspropertyset>
+ <customize/>
+ </java>
+ </sequential>
+ </macrodef>
+ </target>
+ <target name="-init-macrodef-java">
+ <macrodef name="java" uri="http://www.netbeans.org/ns/j2se-project/1">
+ <attribute default="${main.class}" name="classname"/>
+ <attribute default="${run.classpath}" name="classpath"/>
+ <element name="customize" optional="true"/>
+ <sequential>
+ <java classname="@{classname}" dir="${work.dir}" fork="true">
+ <jvmarg value="-Dfile.encoding=${source.encoding}"/>
+ <redirector errorencoding="${source.encoding}" inputencoding="${source.encoding}" outputencoding="${source.encoding}"/>
+ <jvmarg line="${run.jvmargs}"/>
+ <classpath>
+ <path path="@{classpath}"/>
+ </classpath>
+ <syspropertyset>
+ <propertyref prefix="run-sys-prop."/>
+ <mapper from="run-sys-prop.*" to="*" type="glob"/>
+ </syspropertyset>
+ <customize/>
+ </java>
+ </sequential>
+ </macrodef>
+ </target>
+ <target name="-init-presetdef-jar">
+ <presetdef name="jar" uri="http://www.netbeans.org/ns/j2se-project/1">
+ <jar compress="${jar.compress}" jarfile="${dist.jar}">
+ <j2seproject1:fileset dir="${build.classes.dir}"/>
+ </jar>
+ </presetdef>
+ </target>
+ <target depends="-pre-init,-init-private,-init-user,-init-project,-do-init,-post-init,-init-check,-init-macrodef-property,-init-macrodef-javac,-init-macrodef-junit,-init-macrodef-nbjpda,-init-macrodef-debug,-init-macrodef-java,-init-presetdef-jar" name="init"/>
+ <!--
+ ===================
+ COMPILATION SECTION
+ ===================
+ -->
+ <target depends="init" name="deps-jar" unless="no.deps"/>
+ <target depends="init,-check-automatic-build,-clean-after-automatic-build" name="-verify-automatic-build"/>
+ <target depends="init" name="-check-automatic-build">
+ <available file="${build.classes.dir}/.netbeans_automatic_build" property="netbeans.automatic.build"/>
+ </target>
+ <target depends="init" if="netbeans.automatic.build" name="-clean-after-automatic-build">
+ <antcall target="clean"/>
+ </target>
+ <target depends="init,deps-jar" name="-pre-pre-compile">
+ <mkdir dir="${build.classes.dir}"/>
+ </target>
+ <target name="-pre-compile">
+ <!-- Empty placeholder for easier customization. -->
+ <!-- You can override this target in the ../build.xml file. -->
+ </target>
+ <target if="do.depend.true" name="-compile-depend">
+ <pathconvert property="build.generated.subdirs">
+ <dirset dir="${build.generated.sources.dir}" erroronmissingdir="false">
+ <include name="*"/>
+ </dirset>
+ </pathconvert>
+ <j2seproject3:depend srcdir="${src.dir}:${build.generated.subdirs}"/>
+ </target>
+ <target depends="init,deps-jar,-pre-pre-compile,-pre-compile,-compile-depend" if="have.sources" name="-do-compile">
+ <j2seproject3:javac gensrcdir="${build.generated.sources.dir}"/>
+ <copy todir="${build.classes.dir}">
+ <fileset dir="${src.dir}" excludes="${build.classes.excludes},${excludes}" includes="${includes}"/>
+ </copy>
+ </target>
+ <target name="-post-compile">
+ <!-- Empty placeholder for easier customization. -->
+ <!-- You can override this target in the ../build.xml file. -->
+ </target>
+ <target depends="init,deps-jar,-verify-automatic-build,-pre-pre-compile,-pre-compile,-do-compile,-post-compile" description="Compile project." name="compile"/>
+ <target name="-pre-compile-single">
+ <!-- Empty placeholder for easier customization. -->
+ <!-- You can override this target in the ../build.xml file. -->
+ </target>
+ <target depends="init,deps-jar,-pre-pre-compile" name="-do-compile-single">
+ <fail unless="javac.includes">Must select some files in the IDE or set javac.includes</fail>
+ <j2seproject3:force-recompile/>
+ <j2seproject3:javac excludes="" gensrcdir="${build.generated.sources.dir}" includes="${javac.includes}" sourcepath="${src.dir}"/>
+ </target>
+ <target name="-post-compile-single">
+ <!-- Empty placeholder for easier customization. -->
+ <!-- You can override this target in the ../build.xml file. -->
+ </target>
+ <target depends="init,deps-jar,-verify-automatic-build,-pre-pre-compile,-pre-compile-single,-do-compile-single,-post-compile-single" name="compile-single"/>
+ <!--
+ ====================
+ JAR BUILDING SECTION
+ ====================
+ -->
+ <target depends="init" name="-pre-pre-jar">
+ <dirname file="${dist.jar}" property="dist.jar.dir"/>
+ <mkdir dir="${dist.jar.dir}"/>
+ </target>
+ <target name="-pre-jar">
+ <!-- Empty placeholder for easier customization. -->
+ <!-- You can override this target in the ../build.xml file. -->
+ </target>
+ <target depends="init,compile,-pre-pre-jar,-pre-jar" name="-do-jar-without-manifest" unless="manifest.available">
+ <j2seproject1:jar/>
+ </target>
+ <target depends="init,compile,-pre-pre-jar,-pre-jar" if="manifest.available" name="-do-jar-with-manifest" unless="manifest.available+main.class">
+ <j2seproject1:jar manifest="${manifest.file}"/>
+ </target>
+ <target depends="init,compile,-pre-pre-jar,-pre-jar" if="manifest.available+main.class" name="-do-jar-with-mainclass" unless="manifest.available+main.class+mkdist.available">
+ <j2seproject1:jar manifest="${manifest.file}">
+ <j2seproject1:manifest>
+ <j2seproject1:attribute name="Main-Class" value="${main.class}"/>
+ </j2seproject1:manifest>
+ </j2seproject1:jar>
+ <echo>To run this application from the command line without Ant, try:</echo>
+ <property location="${build.classes.dir}" name="build.classes.dir.resolved"/>
+ <property location="${dist.jar}" name="dist.jar.resolved"/>
+ <pathconvert property="run.classpath.with.dist.jar">
+ <path path="${run.classpath}"/>
+ <map from="${build.classes.dir.resolved}" to="${dist.jar.resolved}"/>
+ </pathconvert>
+ <echo>java -cp "${run.classpath.with.dist.jar}" ${main.class}</echo>
+ </target>
+ <target depends="init,compile,-pre-pre-jar,-pre-jar" if="manifest.available+main.class+mkdist.available" name="-do-jar-with-libraries">
+ <property location="${build.classes.dir}" name="build.classes.dir.resolved"/>
+ <pathconvert property="run.classpath.without.build.classes.dir">
+ <path path="${run.classpath}"/>
+ <map from="${build.classes.dir.resolved}" to=""/>
+ </pathconvert>
+ <pathconvert pathsep=" " property="jar.classpath">
+ <path path="${run.classpath.without.build.classes.dir}"/>
+ <chainedmapper>
+ <flattenmapper/>
+ <globmapper from="*" to="lib/*"/>
+ </chainedmapper>
+ </pathconvert>
+ <taskdef classname="org.netbeans.modules.java.j2seproject.copylibstask.CopyLibs" classpath="${libs.CopyLibs.classpath}" name="copylibs"/>
+ <copylibs compress="${jar.compress}" jarfile="${dist.jar}" manifest="${manifest.file}" runtimeclasspath="${run.classpath.without.build.classes.dir}">
+ <fileset dir="${build.classes.dir}"/>
+ <manifest>
+ <attribute name="Main-Class" value="${main.class}"/>
+ <attribute name="Class-Path" value="${jar.classpath}"/>
+ </manifest>
+ </copylibs>
+ <echo>To run this application from the command line without Ant, try:</echo>
+ <property location="${dist.jar}" name="dist.jar.resolved"/>
+ <echo>java -jar "${dist.jar.resolved}"</echo>
+ </target>
+ <target depends="init,compile,-pre-pre-jar,-pre-jar" if="libs.CopyLibs.classpath" name="-do-jar-with-libraries-without-manifest" unless="manifest.available+main.class">
+ <property location="${build.classes.dir}" name="build.classes.dir.resolved"/>
+ <pathconvert property="run.classpath.without.build.classes.dir">
+ <path path="${run.classpath}"/>
+ <map from="${build.classes.dir.resolved}" to=""/>
+ </pathconvert>
+ <pathconvert pathsep=" " property="jar.classpath">
+ <path path="${run.classpath.without.build.classes.dir}"/>
+ <chainedmapper>
+ <flattenmapper/>
+ <globmapper from="*" to="lib/*"/>
+ </chainedmapper>
+ </pathconvert>
+ <taskdef classname="org.netbeans.modules.java.j2seproject.copylibstask.CopyLibs" classpath="${libs.CopyLibs.classpath}" name="copylibs"/>
+ <copylibs compress="${jar.compress}" jarfile="${dist.jar}" runtimeclasspath="${run.classpath.without.build.classes.dir}">
+ <fileset dir="${build.classes.dir}"/>
+ </copylibs>
+ </target>
+ <target name="-post-jar">
+ <!-- Empty placeholder for easier customization. -->
+ <!-- You can override this target in the ../build.xml file. -->
+ </target>
+ <target depends="init,compile,-pre-jar,-do-jar-with-manifest,-do-jar-without-manifest,-do-jar-with-mainclass,-do-jar-with-libraries,-do-jar-with-libraries-without-manifest,-post-jar" description="Build JAR." name="jar"/>
+ <!--
+ =================
+ EXECUTION SECTION
+ =================
+ -->
+ <target depends="init,compile" description="Run a main class." name="run">
+ <j2seproject1:java>
+ <customize>
+ <arg line="${application.args}"/>
+ </customize>
+ </j2seproject1:java>
+ </target>
+ <target name="-do-not-recompile">
+ <property name="javac.includes.binary" value=""/>
+ </target>
+ <target depends="init,-do-not-recompile,compile-single" name="run-single">
+ <fail unless="run.class">Must select one file in the IDE or set run.class</fail>
+ <j2seproject1:java classname="${run.class}"/>
+ </target>
+ <target depends="init,-do-not-recompile,compile-test-single" name="run-test-with-main">
+ <fail unless="run.class">Must select one file in the IDE or set run.class</fail>
+ <j2seproject1:java classname="${run.class}" classpath="${run.test.classpath}"/>
+ </target>
+ <!--
+ =================
+ DEBUGGING SECTION
+ =================
+ -->
+ <target depends="init" if="netbeans.home" name="-debug-start-debugger">
+ <j2seproject1:nbjpdastart name="${debug.class}"/>
+ </target>
+ <target depends="init" if="netbeans.home" name="-debug-start-debugger-main-test">
+ <j2seproject1:nbjpdastart classpath="${debug.test.classpath}" name="${debug.class}"/>
+ </target>
+ <target depends="init,compile" name="-debug-start-debuggee">
+ <j2seproject3:debug>
+ <customize>
+ <arg line="${application.args}"/>
+ </customize>
+ </j2seproject3:debug>
+ </target>
+ <target depends="init,compile,-debug-start-debugger,-debug-start-debuggee" description="Debug project in IDE." if="netbeans.home" name="debug"/>
+ <target depends="init" if="netbeans.home" name="-debug-start-debugger-stepinto">
+ <j2seproject1:nbjpdastart stopclassname="${main.class}"/>
+ </target>
+ <target depends="init,compile,-debug-start-debugger-stepinto,-debug-start-debuggee" if="netbeans.home" name="debug-stepinto"/>
+ <target depends="init,compile-single" if="netbeans.home" name="-debug-start-debuggee-single">
+ <fail unless="debug.class">Must select one file in the IDE or set debug.class</fail>
+ <j2seproject3:debug classname="${debug.class}"/>
+ </target>
+ <target depends="init,-do-not-recompile,compile-single,-debug-start-debugger,-debug-start-debuggee-single" if="netbeans.home" name="debug-single"/>
+ <target depends="init,compile-test-single" if="netbeans.home" name="-debug-start-debuggee-main-test">
+ <fail unless="debug.class">Must select one file in the IDE or set debug.class</fail>
+ <j2seproject3:debug classname="${debug.class}" classpath="${debug.test.classpath}"/>
+ </target>
+ <target depends="init,-do-not-recompile,compile-test-single,-debug-start-debugger-main-test,-debug-start-debuggee-main-test" if="netbeans.home" name="debug-test-with-main"/>
+ <target depends="init" name="-pre-debug-fix">
+ <fail unless="fix.includes">Must set fix.includes</fail>
+ <property name="javac.includes" value="${fix.includes}.java"/>
+ </target>
+ <target depends="init,-pre-debug-fix,compile-single" if="netbeans.home" name="-do-debug-fix">
+ <j2seproject1:nbjpdareload/>
+ </target>
+ <target depends="init,-pre-debug-fix,-do-debug-fix" if="netbeans.home" name="debug-fix"/>
+ <!--
+ ===============
+ JAVADOC SECTION
+ ===============
+ -->
+ <target depends="init" name="-javadoc-build">
+ <mkdir dir="${dist.javadoc.dir}"/>
+ <javadoc additionalparam="${javadoc.additionalparam}" author="${javadoc.author}" charset="UTF-8" destdir="${dist.javadoc.dir}" docencoding="UTF-8" encoding="${javadoc.encoding.used}" failonerror="true" noindex="${javadoc.noindex}" nonavbar="${javadoc.nonavbar}" notree="${javadoc.notree}" private="${javadoc.private}" source="${javac.source}" splitindex="${javadoc.splitindex}" use="${javadoc.use}" useexternalfile="true" version="${javadoc.version}" windowtitle="${javadoc.windowtitle}">
+ <classpath>
+ <path path="${javac.classpath}"/>
+ </classpath>
+ <fileset dir="${src.dir}" excludes="${excludes}" includes="${includes}">
+ <filename name="**/*.java"/>
+ </fileset>
+ <fileset dir="${build.generated.sources.dir}" erroronmissingdir="false">
+ <include name="**/*.java"/>
+ </fileset>
+ </javadoc>
+ </target>
+ <target depends="init,-javadoc-build" if="netbeans.home" name="-javadoc-browse" unless="no.javadoc.preview">
+ <nbbrowse file="${dist.javadoc.dir}/index.html"/>
+ </target>
+ <target depends="init,-javadoc-build,-javadoc-browse" description="Build Javadoc." name="javadoc"/>
+ <!--
+ =========================
+ JUNIT COMPILATION SECTION
+ =========================
+ -->
+ <target depends="init,compile" if="have.tests" name="-pre-pre-compile-test">
+ <mkdir dir="${build.test.classes.dir}"/>
+ </target>
+ <target name="-pre-compile-test">
+ <!-- Empty placeholder for easier customization. -->
+ <!-- You can override this target in the ../build.xml file. -->
+ </target>
+ <target if="do.depend.true" name="-compile-test-depend">
+ <j2seproject3:depend classpath="${javac.test.classpath}" destdir="${build.test.classes.dir}" srcdir="${test.src.dir}"/>
+ </target>
+ <target depends="init,compile,-pre-pre-compile-test,-pre-compile-test,-compile-test-depend" if="have.tests" name="-do-compile-test">
+ <j2seproject3:javac classpath="${javac.test.classpath}" debug="true" destdir="${build.test.classes.dir}" srcdir="${test.src.dir}"/>
+ <copy todir="${build.test.classes.dir}">
+ <fileset dir="${test.src.dir}" excludes="${build.classes.excludes},${excludes}" includes="${includes}"/>
+ </copy>
+ </target>
+ <target name="-post-compile-test">
+ <!-- Empty placeholder for easier customization. -->
+ <!-- You can override this target in the ../build.xml file. -->
+ </target>
+ <target depends="init,compile,-pre-pre-compile-test,-pre-compile-test,-do-compile-test,-post-compile-test" name="compile-test"/>
+ <target name="-pre-compile-test-single">
+ <!-- Empty placeholder for easier customization. -->
+ <!-- You can override this target in the ../build.xml file. -->
+ </target>
+ <target depends="init,compile,-pre-pre-compile-test,-pre-compile-test-single" if="have.tests" name="-do-compile-test-single">
+ <fail unless="javac.includes">Must select some files in the IDE or set javac.includes</fail>
+ <j2seproject3:force-recompile destdir="${build.test.classes.dir}"/>
+ <j2seproject3:javac classpath="${javac.test.classpath}" debug="true" destdir="${build.test.classes.dir}" excludes="" includes="${javac.includes}" sourcepath="${test.src.dir}" srcdir="${test.src.dir}"/>
+ <copy todir="${build.test.classes.dir}">
+ <fileset dir="${test.src.dir}" excludes="${build.classes.excludes},${excludes}" includes="${includes}"/>
+ </copy>
+ </target>
+ <target name="-post-compile-test-single">
+ <!-- Empty placeholder for easier customization. -->
+ <!-- You can override this target in the ../build.xml file. -->
+ </target>
+ <target depends="init,compile,-pre-pre-compile-test,-pre-compile-test-single,-do-compile-test-single,-post-compile-test-single" name="compile-test-single"/>
+ <!--
+ =======================
+ JUNIT EXECUTION SECTION
+ =======================
+ -->
+ <target depends="init" if="have.tests" name="-pre-test-run">
+ <mkdir dir="${build.test.results.dir}"/>
+ </target>
+ <target depends="init,compile-test,-pre-test-run" if="have.tests" name="-do-test-run">
+ <j2seproject3:junit testincludes="**/*Test.java"/>
+ </target>
+ <target depends="init,compile-test,-pre-test-run,-do-test-run" if="have.tests" name="-post-test-run">
+ <fail if="tests.failed" unless="ignore.failing.tests">Some tests failed; see details above.</fail>
+ </target>
+ <target depends="init" if="have.tests" name="test-report"/>
+ <target depends="init" if="netbeans.home+have.tests" name="-test-browse"/>
+ <target depends="init,compile-test,-pre-test-run,-do-test-run,test-report,-post-test-run,-test-browse" description="Run unit tests." name="test"/>
+ <target depends="init" if="have.tests" name="-pre-test-run-single">
+ <mkdir dir="${build.test.results.dir}"/>
+ </target>
+ <target depends="init,compile-test-single,-pre-test-run-single" if="have.tests" name="-do-test-run-single">
+ <fail unless="test.includes">Must select some files in the IDE or set test.includes</fail>
+ <j2seproject3:junit excludes="" includes="${test.includes}"/>
+ </target>
+ <target depends="init,compile-test-single,-pre-test-run-single,-do-test-run-single" if="have.tests" name="-post-test-run-single">
+ <fail if="tests.failed" unless="ignore.failing.tests">Some tests failed; see details above.</fail>
+ </target>
+ <target depends="init,-do-not-recompile,compile-test-single,-pre-test-run-single,-do-test-run-single,-post-test-run-single" description="Run single unit test." name="test-single"/>
+ <!--
+ =======================
+ JUNIT DEBUGGING SECTION
+ =======================
+ -->
+ <target depends="init,compile-test" if="have.tests" name="-debug-start-debuggee-test">
+ <fail unless="test.class">Must select one file in the IDE or set test.class</fail>
+ <property location="${build.test.results.dir}/TEST-${test.class}.xml" name="test.report.file"/>
+ <delete file="${test.report.file}"/>
+ <mkdir dir="${build.test.results.dir}"/>
+ <j2seproject3:debug classname="org.apache.tools.ant.taskdefs.optional.junit.JUnitTestRunner" classpath="${ant.home}/lib/ant.jar:${ant.home}/lib/ant-junit.jar:${debug.test.classpath}">
+ <customize>
+ <syspropertyset>
+ <propertyref prefix="test-sys-prop."/>
+ <mapper from="test-sys-prop.*" to="*" type="glob"/>
+ </syspropertyset>
+ <arg value="${test.class}"/>
+ <arg value="showoutput=true"/>
+ <arg value="formatter=org.apache.tools.ant.taskdefs.optional.junit.BriefJUnitResultFormatter"/>
+ <arg value="formatter=org.apache.tools.ant.taskdefs.optional.junit.XMLJUnitResultFormatter,${test.report.file}"/>
+ </customize>
+ </j2seproject3:debug>
+ </target>
+ <target depends="init,compile-test" if="netbeans.home+have.tests" name="-debug-start-debugger-test">
+ <j2seproject1:nbjpdastart classpath="${debug.test.classpath}" name="${test.class}"/>
+ </target>
+ <target depends="init,-do-not-recompile,compile-test-single,-debug-start-debugger-test,-debug-start-debuggee-test" name="debug-test"/>
+ <target depends="init,-pre-debug-fix,compile-test-single" if="netbeans.home" name="-do-debug-fix-test">
+ <j2seproject1:nbjpdareload dir="${build.test.classes.dir}"/>
+ </target>
+ <target depends="init,-pre-debug-fix,-do-debug-fix-test" if="netbeans.home" name="debug-fix-test"/>
+ <!--
+ =========================
+ APPLET EXECUTION SECTION
+ =========================
+ -->
+ <target depends="init,compile-single" name="run-applet">
+ <fail unless="applet.url">Must select one file in the IDE or set applet.url</fail>
+ <j2seproject1:java classname="sun.applet.AppletViewer">
+ <customize>
+ <arg value="${applet.url}"/>
+ </customize>
+ </j2seproject1:java>
+ </target>
+ <!--
+ =========================
+ APPLET DEBUGGING SECTION
+ =========================
+ -->
+ <target depends="init,compile-single" if="netbeans.home" name="-debug-start-debuggee-applet">
+ <fail unless="applet.url">Must select one file in the IDE or set applet.url</fail>
+ <j2seproject3:debug classname="sun.applet.AppletViewer">
+ <customize>
+ <arg value="${applet.url}"/>
+ </customize>
+ </j2seproject3:debug>
+ </target>
+ <target depends="init,compile-single,-debug-start-debugger,-debug-start-debuggee-applet" if="netbeans.home" name="debug-applet"/>
+ <!--
+ ===============
+ CLEANUP SECTION
+ ===============
+ -->
+ <target depends="init" name="deps-clean" unless="no.deps"/>
+ <target depends="init" name="-do-clean">
+ <delete dir="${build.dir}"/>
+ <delete dir="${dist.dir}"/>
+ </target>
+ <target name="-post-clean">
+ <!-- Empty placeholder for easier customization. -->
+ <!-- You can override this target in the ../build.xml file. -->
+ </target>
+ <target depends="init,deps-clean,-do-clean,-post-clean" description="Clean build products." name="clean"/>
+</project>
diff --git a/nbproject/genfiles.properties b/nbproject/genfiles.properties
new file mode 100644
index 0000000..0deef00
--- /dev/null
+++ b/nbproject/genfiles.properties
@@ -0,0 +1,8 @@
+build.xml.data.CRC32=82862954
+build.xml.script.CRC32=4ee68ef3
+build.xml.stylesheet.CRC32=be360661
+# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
+# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
+nbproject/build-impl.xml.data.CRC32=82862954
+nbproject/build-impl.xml.script.CRC32=303f4a0e
+nbproject/build-impl.xml.stylesheet.CRC32=5c621a33@1.26.2.45
diff --git a/nbproject/project.properties b/nbproject/project.properties
new file mode 100644
index 0000000..e0d5bbe
--- /dev/null
+++ b/nbproject/project.properties
@@ -0,0 +1,67 @@
+application.homepage=http://json-jruby.rubyforge.org/
+application.title=JSON-JRuby
+application.vendor=mernen
+build.classes.dir=${build.dir}/classes
+build.classes.excludes=**/*.java,**/*.form
+# This directory is removed when the project is cleaned:
+build.dir=build
+build.generated.sources.dir=${build.dir}/generated-sources
+lib.dir=lib
+jruby.dir=../jruby
+build.generated.dir=${build.dir}/generated
+generator.jar=${lib.dir}/json/ext/generator.jar
+parser.jar=${lib.dir}/json/ext/parser.jar
+# Only compile against the classpath explicitly listed here:
+build.sysclasspath=ignore
+build.test.classes.dir=${build.dir}/test/classes
+build.test.results.dir=${build.dir}/test/results
+debug.classpath=\
+ ${run.classpath}
+debug.test.classpath=\
+ ${run.test.classpath}
+# This directory is removed when the project is cleaned:
+dist.dir=dist
+dist.jar=${dist.dir}/ext.jar
+dist.javadoc.dir=${dist.dir}/javadoc
+excludes=
+file.reference.jruby.jar=${jruby.dir}/lib/jruby.jar
+file.reference.bytelist.jar=${jruby.dir}/build_lib/bytelist.jar
+includes=**
+jar.compress=false
+javac.classpath=\
+ ${file.reference.jruby.jar}:\
+ ${file.reference.bytelist.jar}
+# Space-separated list of extra javac options
+javac.compilerargs=-Xlint -Xlint:-serial
+javac.deprecation=true
+javac.source=1.5
+javac.target=1.5
+javac.test.classpath=\
+ ${javac.classpath}:\
+ ${build.classes.dir}
+javadoc.additionalparam=
+javadoc.author=false
+javadoc.encoding=${source.encoding}
+javadoc.noindex=false
+javadoc.nonavbar=false
+javadoc.notree=false
+javadoc.private=false
+javadoc.splitindex=true
+javadoc.use=true
+javadoc.version=false
+javadoc.windowtitle=
+meta.inf.dir=${src.dir}/META-INF
+platform.active=default_platform
+run.classpath=\
+ ${javac.classpath}:\
+ ${build.classes.dir}
+# Space-separated list of JVM arguments used when running the project
+# (you may also define separate properties like run-sys-prop.name=value instead of -Dname=value
+# or test-sys-prop.name=value to set system properties for unit tests):
+run.jvmargs=
+run.test.classpath=\
+ ${javac.test.classpath}:\
+ ${build.test.classes.dir}
+source.encoding=UTF-8
+src.dir=src
+test.src.dir=test
diff --git a/nbproject/project.xml b/nbproject/project.xml
new file mode 100644
index 0000000..7a9c731
--- /dev/null
+++ b/nbproject/project.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://www.netbeans.org/ns/project/1">
+ <type>org.netbeans.modules.java.j2seproject</type>
+ <configuration>
+ <data xmlns="http://www.netbeans.org/ns/j2se-project/3">
+ <name>JSON-JRuby</name>
+ <minimum-ant-version>1.6.5</minimum-ant-version>
+ <source-roots>
+ <root id="src.dir"/>
+ </source-roots>
+ <test-roots>
+ <root id="test.src.dir"/>
+ </test-roots>
+ </data>
+ </configuration>
+</project>
diff --git a/src/json/ext/ByteListTranscoder.java b/src/json/ext/ByteListTranscoder.java
new file mode 100644
index 0000000..ed9e54b
--- /dev/null
+++ b/src/json/ext/ByteListTranscoder.java
@@ -0,0 +1,167 @@
+/*
+ * 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.exceptions.RaiseException;
+import org.jruby.runtime.ThreadContext;
+import org.jruby.util.ByteList;
+
+/**
+ * A class specialized in transcoding a certain String format into another,
+ * using UTF-8 ByteLists as both input and output.
+ */
+abstract class ByteListTranscoder {
+ protected final ThreadContext context;
+
+ protected ByteList src;
+ protected int srcEnd;
+ /** Position where the last read character started */
+ protected int charStart;
+ /** Position of the next character to read */
+ protected int pos;
+
+ private ByteList out;
+ /**
+ * When a character that can be copied straight into the output is found,
+ * its index is stored on this variable, and copying is delayed until
+ * the sequence of characters that can be copied ends.
+ *
+ * <p>The variable stores -1 when not in a plain sequence.
+ */
+ private int quoteStart = -1;
+
+ protected ByteListTranscoder(ThreadContext context) {
+ this.context = context;
+ }
+
+ protected void init(ByteList src, ByteList out) {
+ this.init(src, 0, src.length(), out);
+ }
+
+ protected void init(ByteList src, int start, int end, ByteList out) {
+ this.src = src;
+ this.pos = start;
+ this.charStart = start;
+ this.srcEnd = end;
+ this.out = out;
+ }
+
+ /**
+ * Returns whether there are any characters left to be read.
+ */
+ protected boolean hasNext() {
+ return pos < srcEnd;
+ }
+
+ /**
+ * Returns the next character in the buffer.
+ */
+ private char next() {
+ return src.charAt(pos++);
+ }
+
+ /**
+ * Reads an UTF-8 character from the input and returns its code point,
+ * while advancing the input position.
+ *
+ * <p>Raises an {@link #invalidUtf8()} exception if an invalid byte
+ * is found.
+ */
+ protected int readUtf8Char() {
+ charStart = pos;
+ char head = next();
+ if (head <= 0x7f) { // 0b0xxxxxxx (ASCII)
+ return head;
+ }
+ if (head <= 0xbf) { // 0b10xxxxxx
+ throw invalidUtf8(); // tail byte with no head
+ }
+ if (head <= 0xdf) { // 0b110xxxxx
+ ensureMin(1);
+ int cp = ((head & 0x1f) << 6)
+ | nextPart();
+ if (cp < 0x0080) throw invalidUtf8();
+ return cp;
+ }
+ if (head <= 0xef) { // 0b1110xxxx
+ ensureMin(2);
+ int cp = ((head & 0x0f) << 12)
+ | (nextPart() << 6)
+ | nextPart();
+ if (cp < 0x0800) throw invalidUtf8();
+ return cp;
+ }
+ if (head <= 0xf7) { // 0b11110xxx
+ ensureMin(3);
+ int cp = ((head & 0x07) << 18)
+ | (nextPart() << 12)
+ | (nextPart() << 6)
+ | nextPart();
+ if (!Character.isValidCodePoint(cp)) throw invalidUtf8();
+ return cp;
+ }
+ // 0b11111xxx?
+ throw invalidUtf8();
+ }
+
+ /**
+ * Throws a GeneratorError if the input list doesn't have at least this
+ * many bytes left.
+ */
+ protected void ensureMin(int n) {
+ if (pos + n > srcEnd) throw incompleteUtf8();
+ }
+
+ /**
+ * Reads the next byte of a multi-byte UTF-8 character and returns its
+ * contents (lower 6 bits).
+ *
+ * <p>Throws a GeneratorError if the byte is not a valid tail.
+ */
+ private int nextPart() {
+ char c = next();
+ // tail bytes must be 0b10xxxxxx
+ if ((c & 0xc0) != 0x80) throw invalidUtf8();
+ return c & 0x3f;
+ }
+
+
+ protected void quoteStart() {
+ if (quoteStart == -1) quoteStart = charStart;
+ }
+
+ /**
+ * When in a sequence of characters that can be copied directly,
+ * interrupts the sequence and copies it to the output buffer.
+ *
+ * @param endPos The offset until which the direct character quoting should
+ * occur. You may pass {@link #pos} to quote until the most
+ * recently read character, or {@link #charStart} to quote
+ * until the character before it.
+ */
+ protected void quoteStop(int endPos) {
+ if (quoteStart != -1) {
+ out.append(src, quoteStart, endPos - quoteStart);
+ quoteStart = -1;
+ }
+ }
+
+ protected void append(int b) {
+ out.append(b);
+ }
+
+ protected void append(byte[] origin, int start, int length) {
+ out.append(origin, start, length);
+ }
+
+
+ protected abstract RaiseException invalidUtf8();
+
+ protected RaiseException incompleteUtf8() {
+ return invalidUtf8();
+ }
+}
diff --git a/src/json/ext/Generator.java b/src/json/ext/Generator.java
new file mode 100644
index 0000000..b2f852c
--- /dev/null
+++ b/src/json/ext/Generator.java
@@ -0,0 +1,452 @@
+/*
+ * 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.RubyArray;
+import org.jruby.RubyBignum;
+import org.jruby.RubyBoolean;
+import org.jruby.RubyClass;
+import org.jruby.RubyFixnum;
+import org.jruby.RubyFloat;
+import org.jruby.RubyHash;
+import org.jruby.RubyNumeric;
+import org.jruby.RubyString;
+import org.jruby.runtime.ThreadContext;
+import org.jruby.runtime.builtin.IRubyObject;
+import org.jruby.util.ByteList;
+
+public final class Generator {
+ private Generator() {
+ throw new RuntimeException();
+ }
+
+ /**
+ * Encodes the given object as a JSON string, using the given handler.
+ */
+ static <T extends IRubyObject> RubyString
+ generateJson(ThreadContext context, T object,
+ Handler<? super T> handler, IRubyObject[] args) {
+ Session session = new Session(context, args.length > 0 ? args[0]
+ : null);
+ int depth = args.length > 1 ? RubyNumeric.fix2int(args[1]) : 0;
+ return session.infect(handler.generateNew(session, object, depth));
+ }
+
+ /**
+ * Encodes the given object as a JSON string, detecting the appropriate handler
+ * for the given object.
+ */
+ static <T extends IRubyObject> RubyString
+ generateJson(ThreadContext context, T object, IRubyObject[] args) {
+ Handler<? super T> handler = getHandlerFor(context.getRuntime(), object);
+ return generateJson(context, object, handler, args);
+ }
+
+ /**
+ * Encodes the given object as a JSON string, using the appropriate
+ * handler if one is found or calling #to_json if not.
+ */
+ public static <T extends IRubyObject> RubyString
+ generateJson(ThreadContext context, T object,
+ GeneratorState config, int depth) {
+ Session session = new Session(context, config);
+ Handler<? super T> handler = getHandlerFor(context.getRuntime(), object);
+ return handler.generateNew(session, object, depth);
+ }
+
+ /**
+ * Returns the best serialization handler for the given object.
+ */
+ // Java's generics can't handle this satisfactorily, so I'll just leave
+ // the best I could get and ignore the warnings
+ @SuppressWarnings("unchecked")
+ private static <T extends IRubyObject>
+ Handler<? super T> getHandlerFor(Ruby runtime, T object) {
+ RubyClass metaClass = object.getMetaClass();
+ if (metaClass == runtime.getString()) return (Handler)STRING_HANDLER;
+ if (metaClass == runtime.getFixnum()) return (Handler)FIXNUM_HANDLER;
+ if (metaClass == runtime.getHash()) return (Handler)HASH_HANDLER;
+ if (metaClass == runtime.getArray()) return (Handler)ARRAY_HANDLER;
+ if (object.isNil()) return (Handler)NIL_HANDLER;
+ if (object == runtime.getTrue()) return (Handler)TRUE_HANDLER;
+ if (object == runtime.getFalse()) return (Handler)FALSE_HANDLER;
+ if (metaClass == runtime.getFloat()) return (Handler)FLOAT_HANDLER;
+ if (metaClass == runtime.getBignum()) return (Handler)BIGNUM_HANDLER;
+ return GENERIC_HANDLER;
+ }
+
+
+ /* Generator context */
+
+ /**
+ * A class that concentrates all the information that is shared by
+ * generators working on a single session.
+ *
+ * <p>A session is defined as the process of serializing a single root
+ * object; any handler directly called by container handlers (arrays and
+ * hashes/objects) shares this object with its caller.
+ *
+ * <p>Note that anything called indirectly (via {@link GENERIC_HANDLER})
+ * won't be part of the session.
+ */
+ static class Session {
+ private final ThreadContext context;
+ private GeneratorState state;
+ private IRubyObject possibleState;
+ private RuntimeInfo info;
+ private StringEncoder stringEncoder;
+
+ private boolean tainted = false;
+ private boolean untrusted = false;
+
+ Session(ThreadContext context, GeneratorState state) {
+ this.context = context;
+ this.state = state;
+ }
+
+ Session(ThreadContext context, IRubyObject possibleState) {
+ this.context = context;
+ this.possibleState = possibleState == null || possibleState.isNil()
+ ? null : possibleState;
+ }
+
+ public ThreadContext getContext() {
+ return context;
+ }
+
+ public Ruby getRuntime() {
+ return context.getRuntime();
+ }
+
+ public GeneratorState getState() {
+ if (state == null) {
+ state = GeneratorState.fromState(context, getInfo(), possibleState);
+ }
+ return state;
+ }
+
+ public RuntimeInfo getInfo() {
+ if (info == null) info = RuntimeInfo.forRuntime(getRuntime());
+ return info;
+ }
+
+ public StringEncoder getStringEncoder() {
+ if (stringEncoder == null) {
+ stringEncoder = new StringEncoder(context, getState().asciiOnly());
+ }
+ return stringEncoder;
+ }
+
+ public void infectBy(IRubyObject object) {
+ if (object.isTaint()) tainted = true;
+ if (object.isUntrusted()) untrusted = true;
+ }
+
+ public <T extends IRubyObject> T infect(T object) {
+ if (tainted) object.setTaint(true);
+ if (untrusted) object.setUntrusted(true);
+ return object;
+ }
+ }
+
+
+ /* Handler base classes */
+
+ private static abstract class Handler<T extends IRubyObject> {
+ /**
+ * Returns an estimative of how much space the serialization of the
+ * given object will take. Used for allocating enough buffer space
+ * before invoking other methods.
+ */
+ int guessSize(Session session, T object, int depth) {
+ return 4;
+ }
+
+ RubyString generateNew(Session session, T object, int depth) {
+ ByteList buffer = new ByteList(guessSize(session, object, depth));
+ generate(session, object, buffer, depth);
+ return RubyString.newString(session.getRuntime(), buffer);
+ }
+
+ abstract void generate(Session session, T object, ByteList buffer,
+ int depth);
+ }
+
+ /**
+ * A handler that returns a fixed keyword regardless of the passed object.
+ */
+ private static class KeywordHandler<T extends IRubyObject>
+ extends Handler<T> {
+ private final ByteList keyword;
+
+ private KeywordHandler(String keyword) {
+ this.keyword = new ByteList(ByteList.plain(keyword), false);
+ }
+
+ @Override
+ int guessSize(Session session, T object, int depth) {
+ return keyword.length();
+ }
+
+ @Override
+ RubyString generateNew(Session session, T object, int depth) {
+ return RubyString.newStringShared(session.getRuntime(), keyword);
+ }
+
+ @Override
+ void generate(Session session, T object, ByteList buffer, int depth) {
+ buffer.append(keyword);
+ }
+ }
+
+
+ /* Handlers */
+
+ static final Handler<RubyBignum> BIGNUM_HANDLER =
+ new Handler<RubyBignum>() {
+ @Override
+ void generate(Session session, RubyBignum object, ByteList buffer,
+ int depth) {
+ // JRUBY-4751: RubyBignum.to_s() returns generic object
+ // representation (fixed in 1.5, but we maintain backwards
+ // compatibility; call to_s(IRubyObject[]) then
+ buffer.append(((RubyString)object.to_s(IRubyObject.NULL_ARRAY)).getByteList());
+ }
+ };
+
+ static final Handler<RubyFixnum> FIXNUM_HANDLER =
+ new Handler<RubyFixnum>() {
+ @Override
+ void generate(Session session, RubyFixnum object, ByteList buffer,
+ int depth) {
+ buffer.append(object.to_s().getByteList());
+ }
+ };
+
+ static final Handler<RubyFloat> FLOAT_HANDLER =
+ new Handler<RubyFloat>() {
+ @Override
+ void generate(Session session, RubyFloat object, ByteList buffer,
+ int depth) {
+ double value = RubyFloat.num2dbl(object);
+
+ if (Double.isInfinite(value) || Double.isNaN(value)) {
+ if (!session.getState().allowNaN()) {
+ throw Utils.newException(session.getContext(),
+ Utils.M_GENERATOR_ERROR,
+ object + " not allowed in JSON");
+ }
+ }
+ buffer.append(((RubyString)object.to_s()).getByteList());
+ }
+ };
+
+ static final Handler<RubyArray> ARRAY_HANDLER =
+ new Handler<RubyArray>() {
+ @Override
+ int guessSize(Session session, RubyArray object, int depth) {
+ GeneratorState state = session.getState();
+ int perItem =
+ 4 // prealloc
+ + (depth + 1) * state.getIndent().length() // indent
+ + 1 + state.getArrayNl().length(); // ',' arrayNl
+ return 2 + object.size() * perItem;
+ }
+
+ @Override
+ void generate(Session session, RubyArray object, ByteList buffer,
+ int depth) {
+ ThreadContext context = session.getContext();
+ Ruby runtime = context.getRuntime();
+ GeneratorState state = session.getState();
+ state.checkMaxNesting(context, depth + 1);
+
+ ByteList indentUnit = state.getIndent();
+ byte[] shift = Utils.repeat(indentUnit, depth + 1);
+
+ ByteList arrayNl = state.getArrayNl();
+ byte[] delim = new byte[1 + arrayNl.length()];
+ delim[0] = ',';
+ System.arraycopy(arrayNl.unsafeBytes(), arrayNl.begin(), delim, 1,
+ arrayNl.length());
+
+ session.infectBy(object);
+
+ buffer.append((byte)'[');
+ buffer.append(arrayNl);
+ boolean firstItem = true;
+ for (int i = 0, t = object.getLength(); i < t; i++) {
+ IRubyObject element = object.eltInternal(i);
+ session.infectBy(element);
+ if (firstItem) {
+ firstItem = false;
+ } else {
+ buffer.append(delim);
+ }
+ buffer.append(shift);
+ Handler<IRubyObject> handler = getHandlerFor(runtime, element);
+ handler.generate(session, element, buffer, depth + 1);
+ }
+
+ if (arrayNl.length() != 0) {
+ buffer.append(arrayNl);
+ buffer.append(shift, 0, depth * indentUnit.length());
+ }
+
+ buffer.append((byte)']');
+ }
+ };
+
+ static final Handler<RubyHash> HASH_HANDLER =
+ new Handler<RubyHash>() {
+ @Override
+ int guessSize(Session session, RubyHash object, int depth) {
+ GeneratorState state = session.getState();
+ int perItem =
+ 12 // key, colon, comma
+ + (depth + 1) * state.getIndent().length()
+ + state.getSpaceBefore().length()
+ + state.getSpace().length();
+ return 2 + object.size() * perItem;
+ }
+
+ @Override
+ void generate(final Session session, RubyHash object,
+ final ByteList buffer, final int depth) {
+ ThreadContext context = session.getContext();
+ final Ruby runtime = context.getRuntime();
+ final GeneratorState state = session.getState();
+ state.checkMaxNesting(context, depth + 1);
+
+ final ByteList objectNl = state.getObjectNl();
+ final byte[] indent = Utils.repeat(state.getIndent(), depth + 1);
+ final ByteList spaceBefore = state.getSpaceBefore();
+ final ByteList space = state.getSpace();
+
+ buffer.append((byte)'{');
+ buffer.append(objectNl);
+ object.visitAll(new RubyHash.Visitor() {
+ private boolean firstPair = true;
+
+ @Override
+ public void visit(IRubyObject key, IRubyObject value) {
+ if (firstPair) {
+ firstPair = false;
+ } else {
+ buffer.append((byte)',');
+ buffer.append(objectNl);
+ }
+ if (objectNl.length() != 0) buffer.append(indent);
+
+ STRING_HANDLER.generate(session, key.asString(),
+ buffer, depth + 1);
+ session.infectBy(key);
+
+ buffer.append(spaceBefore);
+ buffer.append((byte)':');
+ buffer.append(space);
+
+ Handler<IRubyObject> valueHandler = getHandlerFor(runtime, value);
+ valueHandler.generate(session, value, buffer, depth + 1);
+ session.infectBy(value);
+ }
+ });
+ if (objectNl.length() != 0) {
+ buffer.append(objectNl);
+ if (indent.length != 0) {
+ for (int i = 0; i < depth; i++) {
+ buffer.append(indent);
+ }
+ }
+ }
+ buffer.append((byte)'}');
+ }
+ };
+
+ static final Handler<RubyString> STRING_HANDLER =
+ new Handler<RubyString>() {
+ @Override
+ int guessSize(Session session, RubyString object, int depth) {
+ // for most applications, most strings will be just a set of
+ // printable ASCII characters without any escaping, so let's
+ // just allocate enough space for that + the quotes
+ return 2 + object.getByteList().length();
+ }
+
+ @Override
+ void generate(Session session, RubyString object, ByteList buffer,
+ int depth) {
+ RuntimeInfo info = session.getInfo();
+ RubyString src;
+
+ if (info.encodingsSupported() &&
+ object.encoding(session.getContext()) != info.utf8) {
+ src = (RubyString)object.encode(session.getContext(),
+ info.utf8);
+ } else {
+ src = object;
+ }
+
+ session.getStringEncoder().encode(src.getByteList(), buffer);
+ }
+ };
+
+ static final Handler<RubyBoolean> TRUE_HANDLER =
+ new KeywordHandler<RubyBoolean>("true");
+ static final Handler<RubyBoolean> FALSE_HANDLER =
+ new KeywordHandler<RubyBoolean>("false");
+ static final Handler<IRubyObject> NIL_HANDLER =
+ new KeywordHandler<IRubyObject>("null");
+
+ /**
+ * The default handler (<code>Object#to_json</code>): coerces the object
+ * to string using <code>#to_s</code>, and serializes that string.
+ */
+ static final Handler<IRubyObject> OBJECT_HANDLER =
+ new Handler<IRubyObject>() {
+ @Override
+ RubyString generateNew(Session session, IRubyObject object,
+ int depth) {
+ RubyString str = object.asString();
+ return STRING_HANDLER.generateNew(session, str, depth);
+ }
+
+ @Override
+ void generate(Session session, IRubyObject object, ByteList buffer,
+ int depth) {
+ RubyString str = object.asString();
+ STRING_HANDLER.generate(session, str, buffer, depth);
+ }
+ };
+
+ /**
+ * A handler that simply calls <code>#to_json(state, depth)</code> on the
+ * given object.
+ */
+ static final Handler<IRubyObject> GENERIC_HANDLER =
+ new Handler<IRubyObject>() {
+ @Override
+ RubyString generateNew(Session session, IRubyObject object,
+ int depth) {
+ RubyNumeric vDepth =
+ RubyNumeric.int2fix(session.getRuntime(), depth);
+ IRubyObject result =
+ object.callMethod(session.getContext(), "to_json",
+ new IRubyObject[] {session.getState(), vDepth});
+ if (result instanceof RubyString) return (RubyString)result;
+ throw session.getRuntime().newTypeError("to_json must return a String");
+ }
+
+ @Override
+ void generate(Session session, IRubyObject object, ByteList buffer,
+ int depth) {
+ RubyString result = generateNew(session, object, depth);
+ buffer.append(result.getByteList());
+ }
+ };
+}
diff --git a/src/json/ext/GeneratorMethods.java b/src/json/ext/GeneratorMethods.java
new file mode 100644
index 0000000..ce0f42f
--- /dev/null
+++ b/src/json/ext/GeneratorMethods.java
@@ -0,0 +1,231 @@
+/*
+ * 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.RubyArray;
+import org.jruby.RubyBoolean;
+import org.jruby.RubyFixnum;
+import org.jruby.RubyFloat;
+import org.jruby.RubyHash;
+import org.jruby.RubyInteger;
+import org.jruby.RubyModule;
+import org.jruby.RubyNumeric;
+import org.jruby.RubyString;
+import org.jruby.anno.JRubyMethod;
+import org.jruby.runtime.ThreadContext;
+import org.jruby.runtime.builtin.IRubyObject;
+import org.jruby.util.ByteList;
+
+/**
+ * A class that populates the
+ * <code>Json::Ext::Generator::GeneratorMethods</code> module.
+ *
+ * @author mernen
+ */
+class GeneratorMethods {
+ /**
+ * Populates the given module with all modules and their methods
+ * @param info
+ * @param generatorMethodsModule The module to populate
+ * (normally <code>JSON::Generator::GeneratorMethods</code>)
+ */
+ static void populate(RuntimeInfo info, RubyModule module) {
+ defineMethods(module, "Array", RbArray.class);
+ defineMethods(module, "FalseClass", RbFalse.class);
+ defineMethods(module, "Float", RbFloat.class);
+ defineMethods(module, "Hash", RbHash.class);
+ defineMethods(module, "Integer", RbInteger.class);
+ defineMethods(module, "NilClass", RbNil.class);
+ defineMethods(module, "Object", RbObject.class);
+ defineMethods(module, "String", RbString.class);
+ defineMethods(module, "TrueClass", RbTrue.class);
+
+ info.stringExtendModule = module.defineModuleUnder("String")
+ .defineModuleUnder("Extend");
+ info.stringExtendModule.defineAnnotatedMethods(StringExtend.class);
+ }
+
+ /**
+ * Convenience method for defining methods on a submodule.
+ * @param parentModule
+ * @param submoduleName
+ * @param klass
+ */
+ private static void defineMethods(RubyModule parentModule,
+ String submoduleName, Class klass) {
+ RubyModule submodule = parentModule.defineModuleUnder(submoduleName);
+ submodule.defineAnnotatedMethods(klass);
+ }
+
+
+ public static class RbHash {
+ @JRubyMethod(rest=true)
+ public static IRubyObject to_json(ThreadContext context,
+ IRubyObject vSelf, IRubyObject[] args) {
+ return Generator.generateJson(context, (RubyHash)vSelf,
+ Generator.HASH_HANDLER, args);
+ }
+ };
+
+ public static class RbArray {
+ @JRubyMethod(rest=true)
+ public static IRubyObject to_json(ThreadContext context,
+ IRubyObject vSelf, IRubyObject[] args) {
+ return Generator.generateJson(context, (RubyArray)vSelf,
+ Generator.ARRAY_HANDLER, args);
+ }
+ };
+
+ public static class RbInteger {
+ @JRubyMethod(rest=true)
+ public static IRubyObject to_json(ThreadContext context,
+ IRubyObject vSelf, IRubyObject[] args) {
+ return Generator.generateJson(context, vSelf, args);
+ }
+ };
+
+ public static class RbFloat {
+ @JRubyMethod(rest=true)
+ public static IRubyObject to_json(ThreadContext context,
+ IRubyObject vSelf, IRubyObject[] args) {
+ return Generator.generateJson(context, (RubyFloat)vSelf,
+ Generator.FLOAT_HANDLER, args);
+ }
+ };
+
+ public static class RbString {
+ @JRubyMethod(rest=true)
+ public static IRubyObject to_json(ThreadContext context,
+ IRubyObject vSelf, IRubyObject[] args) {
+ return Generator.generateJson(context, (RubyString)vSelf,
+ Generator.STRING_HANDLER, args);
+ }
+
+ /**
+ * <code>{@link RubyString String}#to_json_raw(*)</code>
+ *
+ * <p>This method creates a JSON text from the result of a call to
+ * {@link #to_json_raw_object} of this String.
+ */
+ @JRubyMethod(rest=true)
+ public static IRubyObject to_json_raw(ThreadContext context,
+ IRubyObject vSelf, IRubyObject[] args) {
+ RubyHash obj = toJsonRawObject(context, Utils.ensureString(vSelf));
+ return Generator.generateJson(context, obj,
+ Generator.HASH_HANDLER, args);
+ }
+
+ /**
+ * <code>{@link RubyString String}#to_json_raw_object(*)</code>
+ *
+ * <p>This method creates a raw object Hash, that can be nested into
+ * other data structures and will be unparsed as a raw string. This
+ * method should be used if you want to convert raw strings to JSON
+ * instead of UTF-8 strings, e.g. binary data.
+ */
+ @JRubyMethod(rest=true)
+ public static IRubyObject to_json_raw_object(ThreadContext context,
+ IRubyObject vSelf, IRubyObject[] args) {
+ return toJsonRawObject(context, Utils.ensureString(vSelf));
+ }
+
+ private static RubyHash toJsonRawObject(ThreadContext context,
+ RubyString self) {
+ Ruby runtime = context.getRuntime();
+ RubyHash result = RubyHash.newHash(runtime);
+
+ IRubyObject createId = RuntimeInfo.forRuntime(runtime)
+ .jsonModule.callMethod(context, "create_id");
+ result.op_aset(context, createId, self.getMetaClass().to_s());
+
+ ByteList bl = self.getByteList();
+ byte[] uBytes = bl.unsafeBytes();
+ RubyArray array = runtime.newArray(bl.length());
+ for (int i = bl.begin(), t = bl.begin() + bl.length(); i < t; i++) {
+ array.store(i, runtime.newFixnum(uBytes[i] & 0xff));
+ }
+
+ result.op_aset(context, runtime.newString("raw"), array);
+ return result;
+ }
+
+ @JRubyMethod(required=1, module=true)
+ public static IRubyObject included(ThreadContext context,
+ IRubyObject vSelf, IRubyObject module) {
+ RuntimeInfo info = RuntimeInfo.forRuntime(context.getRuntime());
+ return module.callMethod(context, "extend", info.stringExtendModule);
+ }
+ };
+
+ public static class StringExtend {
+ /**
+ * <code>{@link RubyString String}#json_create(o)</code>
+ *
+ * <p>Raw Strings are JSON Objects (the raw bytes are stored in an
+ * array for the key "raw"). The Ruby String can be created by this
+ * module method.
+ */
+ @JRubyMethod(required=1)
+ public static IRubyObject json_create(ThreadContext context,
+ IRubyObject vSelf, IRubyObject vHash) {
+ Ruby runtime = context.getRuntime();
+ RubyHash o = vHash.convertToHash();
+ IRubyObject rawData = o.fastARef(runtime.newString("raw"));
+ if (rawData == null) {
+ throw runtime.newArgumentError("\"raw\" value not defined "
+ + "for encoded String");
+ }
+ RubyArray ary = Utils.ensureArray(rawData);
+ byte[] bytes = new byte[ary.getLength()];
+ for (int i = 0, t = ary.getLength(); i < t; i++) {
+ IRubyObject element = ary.eltInternal(i);
+ if (element instanceof RubyFixnum) {
+ bytes[i] = (byte)RubyNumeric.fix2long(element);
+ } else {
+ throw runtime.newTypeError(element, runtime.getFixnum());
+ }
+ }
+ return runtime.newString(new ByteList(bytes, false));
+ }
+ };
+
+ public static class RbTrue {
+ @JRubyMethod(rest=true)
+ public static IRubyObject to_json(ThreadContext context,
+ IRubyObject vSelf, IRubyObject[] args) {
+ return Generator.generateJson(context, (RubyBoolean)vSelf,
+ Generator.TRUE_HANDLER, args);
+ }
+ }
+
+ public static class RbFalse {
+ @JRubyMethod(rest=true)
+ public static IRubyObject to_json(ThreadContext context,
+ IRubyObject vSelf, IRubyObject[] args) {
+ return Generator.generateJson(context, (RubyBoolean)vSelf,
+ Generator.FALSE_HANDLER, args);
+ }
+ }
+
+ public static class RbNil {
+ @JRubyMethod(rest=true)
+ public static IRubyObject to_json(ThreadContext context,
+ IRubyObject vSelf, IRubyObject[] args) {
+ return Generator.generateJson(context, vSelf,
+ Generator.NIL_HANDLER, args);
+ }
+ }
+
+ public static class RbObject {
+ @JRubyMethod(rest=true)
+ public static IRubyObject to_json(ThreadContext context,
+ IRubyObject self, IRubyObject[] args) {
+ return RbString.to_json(context, self.asString(), args);
+ }
+ };
+}
diff --git a/src/json/ext/GeneratorService.java b/src/json/ext/GeneratorService.java
new file mode 100644
index 0000000..b8deb22
--- /dev/null
+++ b/src/json/ext/GeneratorService.java
@@ -0,0 +1,42 @@
+/*
+ * 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 java.io.IOException;
+
+import org.jruby.Ruby;
+import org.jruby.RubyClass;
+import org.jruby.RubyModule;
+import org.jruby.runtime.load.BasicLibraryService;
+
+/**
+ * The service invoked by JRuby's {@link org.jruby.runtime.load.LoadService LoadService}.
+ * Defines the <code>JSON::Ext::Generator</code> module.
+ * @author mernen
+ */
+public class GeneratorService implements BasicLibraryService {
+ public boolean basicLoad(Ruby runtime) throws IOException {
+ runtime.getLoadService().require("json/common");
+ RuntimeInfo info = RuntimeInfo.initRuntime(runtime);
+
+ info.jsonModule = runtime.defineModule("JSON");
+ RubyModule jsonExtModule = info.jsonModule.defineModuleUnder("Ext");
+ RubyModule generatorModule = jsonExtModule.defineModuleUnder("Generator");
+
+ RubyClass stateClass =
+ generatorModule.defineClassUnder("State", runtime.getObject(),
+ GeneratorState.ALLOCATOR);
+ stateClass.defineAnnotatedMethods(GeneratorState.class);
+ info.generatorStateClass = stateClass;
+
+ RubyModule generatorMethods =
+ generatorModule.defineModuleUnder("GeneratorMethods");
+ GeneratorMethods.populate(info, generatorMethods);
+
+ return true;
+ }
+}
diff --git a/src/json/ext/GeneratorState.java b/src/json/ext/GeneratorState.java
new file mode 100644
index 0000000..107a204
--- /dev/null
+++ b/src/json/ext/GeneratorState.java
@@ -0,0 +1,443 @@
+/*
+ * 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 = 19;
+ /**
+ * 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;
+ /**
+ * XXX
+ */
+ private boolean asciiOnly = DEFAULT_ASCII_ONLY;
+ static final boolean DEFAULT_ASCII_ONLY = false;
+
+ 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;
+ 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 info.getSafeStatePrototype(context);
+ }
+
+ /**
+ * <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;
+ return this;
+ }
+
+ /**
+ * XXX
+ */
+ @JRubyMethod
+ public IRubyObject generate(ThreadContext context, IRubyObject obj) {
+ RubyString result = Generator.generateJson(context, obj, this, 0);
+ if (!objectOrArrayLiteral(result)) {
+ throw Utils.newException(context, Utils.M_GENERATOR_ERROR,
+ "only generation of JSON objects or arrays allowed");
+ }
+ 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;
+ if (b == brace) return true;
+ return false;
+ }
+ 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);
+ }
+ 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);
+ }
+
+ @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);
+ }
+
+ private ByteList prepareByteList(ThreadContext context, IRubyObject value) {
+ RubyString str = value.convertToString();
+ IRubyObject encoding = str.encoding(context);
+ if (encoding != null) {
+ RuntimeInfo info = RuntimeInfo.forRuntime(context.getRuntime());
+ if (encoding != info.utf8) {
+ str = (RubyString)str.encode(context, info.utf8);
+ }
+ }
+ 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
+ public IRubyObject configure(ThreadContext context, IRubyObject vOpts) {
+ OptionsReader opts = OptionsReader.withStrings(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);
+
+ 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
+ */
+ @JRubyMethod
+ 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("max_nesting"), max_nesting_get(context));
+ return result;
+ }
+
+ /**
+ * Returns the maximum level of nesting configured for this state.
+ * @return
+ */
+ public int getMaxNesting() {
+ return maxNesting;
+ }
+
+ /**
+ * Checks if the current depth is allowed as per this state's options.
+ * @param context
+ * @param depth The corrent depth
+ */
+ void checkMaxNesting(ThreadContext context, int depth) {
+ if (maxNesting != 0 && depth > maxNesting) {
+ throw Utils.newException(context, Utils.M_NESTING_ERROR,
+ "nesting of " + depth + " is too deep");
+ }
+ }
+}
diff --git a/src/json/ext/OptionsReader.java b/src/json/ext/OptionsReader.java
new file mode 100644
index 0000000..29de188
--- /dev/null
+++ b/src/json/ext/OptionsReader.java
@@ -0,0 +1,113 @@
+/*
+ * 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.RubyClass;
+import org.jruby.RubyHash;
+import org.jruby.RubyNumeric;
+import org.jruby.RubyString;
+import org.jruby.runtime.ThreadContext;
+import org.jruby.runtime.builtin.IRubyObject;
+import org.jruby.util.ByteList;
+
+final class OptionsReader {
+ private final ThreadContext context;
+ private final Ruby runtime;
+ private final RuntimeInfo info;
+ private final RubyHash opts;
+
+ private OptionsReader(ThreadContext context, Ruby runtime,
+ RuntimeInfo info, IRubyObject vOpts) {
+ this.context = context;
+ this.runtime = runtime;
+ this.info = info;
+
+ if (vOpts == null || vOpts.isNil()) {
+ opts = null;
+ } else if (vOpts.respondsTo("to_hash")) {
+ opts = vOpts.convertToHash();
+ } else {
+ opts = vOpts.callMethod(context, "to_h").convertToHash();
+ }
+ }
+
+ OptionsReader(ThreadContext context, IRubyObject vOpts) {
+ this(context, context.getRuntime(), null, vOpts);
+ }
+
+ static OptionsReader withStrings(ThreadContext context, IRubyObject vOpts) {
+ Ruby runtime = context.getRuntime();
+ return new OptionsReader(context, runtime,
+ RuntimeInfo.forRuntime(runtime), vOpts);
+ }
+
+ /**
+ * Efficiently looks up items with a {@link RubySymbol Symbol} key
+ * @param key The Symbol name to look up for
+ * @return The item in the {@link RubyHash Hash}, or <code>null</code>
+ * if not found
+ */
+ IRubyObject get(String key) {
+ return opts == null ? null : opts.fastARef(runtime.newSymbol(key));
+ }
+
+ boolean getBool(String key, boolean defaultValue) {
+ IRubyObject value = get(key);
+ return value == null ? defaultValue : value.isTrue();
+ }
+
+ int getInt(String key, int defaultValue) {
+ IRubyObject value = get(key);
+ if (value == null) return defaultValue;
+ if (!value.isTrue()) return 0;
+ return RubyNumeric.fix2int(value);
+ }
+
+ /**
+ * Reads the setting from the options hash. If no entry is set for this
+ * key or if it evaluates to <code>false</code>, returns null; attempts to
+ * coerce the value to {@link RubyString String} otherwise.
+ * @param key The Symbol name to look up for
+ * @return <code>null</code> if the key is not in the Hash or if
+ * its value evaluates to <code>false</code>
+ * @throws RaiseException <code>TypeError</code> if the value does not
+ * evaluate to <code>false</code> and can't be
+ * converted to string
+ */
+ ByteList getString(String key) {
+ IRubyObject value = get(key);
+ if (value == null || !value.isTrue()) return null;
+
+ RubyString str = value.convertToString();
+ if (info.encodingsSupported() && str.encoding(context) != info.utf8) {
+ str = (RubyString)str.encode(context, info.utf8);
+ }
+ return str.getByteList().dup();
+ }
+
+ /**
+ * Reads the setting from the options hash. If it is <code>nil</code> or
+ * undefined, returns the default value given.
+ * If not, ensures it is a RubyClass instance and shares the same
+ * allocator as the default value (i.e. for the basic types which have
+ * their specific allocators, this ensures the passed value is
+ * a subclass of them).
+ */
+ RubyClass getClass(String key, RubyClass defaultValue) {
+ IRubyObject value = get(key);
+
+ if (value == null || value.isNil()) return defaultValue;
+
+ if (value instanceof RubyClass &&
+ ((RubyClass)value).getAllocator() == defaultValue.getAllocator()) {
+ return (RubyClass)value;
+ }
+ throw runtime.newTypeError(key + " option must be a subclass of "
+ + defaultValue);
+ }
+}
diff --git a/src/json/ext/Parser.java b/src/json/ext/Parser.java
new file mode 100644
index 0000000..e7c1b14
--- /dev/null
+++ b/src/json/ext/Parser.java
@@ -0,0 +1,2269 @@
+
+// line 1 "src/json/ext/Parser.rl"
+/*
+ * 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.RubyArray;
+import org.jruby.RubyClass;
+import org.jruby.RubyEncoding;
+import org.jruby.RubyFloat;
+import org.jruby.RubyHash;
+import org.jruby.RubyInteger;
+import org.jruby.RubyModule;
+import org.jruby.RubyNumeric;
+import org.jruby.RubyObject;
+import org.jruby.RubyString;
+import org.jruby.anno.JRubyMethod;
+import org.jruby.exceptions.RaiseException;
+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::Parser</code> class.
+ *
+ * <p>This is the JSON parser implemented as a Java class. To use it as the
+ * standard parser, set
+ * <pre>JSON.parser = JSON::Ext::Parser</pre>
+ * This is performed for you when you <code>include "json/ext"</code>.
+ *
+ * <p>This class does not perform the actual parsing, just acts as an interface
+ * to Ruby code. When the {@link #parse()} method is invoked, a
+ * Parser.ParserSession object is instantiated, which handles the process.
+ *
+ * @author mernen
+ */
+public class Parser extends RubyObject {
+ private final RuntimeInfo info;
+ private RubyString vSource;
+ private RubyString createId;
+ private int maxNesting;
+ private boolean allowNaN;
+ private boolean symbolizeNames;
+ private RubyClass objectClass;
+ private RubyClass arrayClass;
+
+ private static final int DEFAULT_MAX_NESTING = 19;
+
+ private static final String JSON_MINUS_INFINITY = "-Infinity";
+ // constant names in the JSON module containing those values
+ private static final String CONST_NAN = "NaN";
+ private static final String CONST_INFINITY = "Infinity";
+ private static final String CONST_MINUS_INFINITY = "MinusInfinity";
+
+ static final ObjectAllocator ALLOCATOR = new ObjectAllocator() {
+ public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
+ return new Parser(runtime, klazz);
+ }
+ };
+
+ /**
+ * Multiple-value return for internal parser methods.
+ *
+ * <p>All the <code>parse<var>Stuff</var></code> methods return instances of
+ * <code>ParserResult</code> when successful, or <code>null</code> when
+ * there's a problem with the input data.
+ */
+ static final class ParserResult {
+ /**
+ * The result of the successful parsing. Should never be
+ * <code>null</code>.
+ */
+ final IRubyObject result;
+ /**
+ * The point where the parser returned.
+ */
+ final int p;
+
+ ParserResult(IRubyObject result, int p) {
+ this.result = result;
+ this.p = p;
+ }
+ }
+
+ public Parser(Ruby runtime, RubyClass metaClass) {
+ super(runtime, metaClass);
+ info = RuntimeInfo.forRuntime(runtime);
+ }
+
+ /**
+ * <code>Parser.new(source, opts = {})</code>
+ *
+ * <p>Creates a new <code>JSON::Ext::Parser</code> instance for the string
+ * <code>source</code>.
+ * It will be configured by the <code>opts</code> Hash.
+ * <code>opts</code> can have the following keys:
+ *
+ * <dl>
+ * <dt><code>:max_nesting</code>
+ * <dd>The maximum depth of nesting allowed in the parsed data
+ * structures. Disable depth checking with <code>:max_nesting => false|nil|0</code>,
+ * it defaults to 19.
+ *
+ * <dt><code>:allow_nan</code>
+ * <dd>If set to <code>true</code>, allow <code>NaN</code>,
+ * <code>Infinity</code> and <code>-Infinity</code> in defiance of RFC 4627
+ * to be parsed by the Parser. This option defaults to <code>false</code>.
+ *
+ * <dt><code>:symbolize_names</code>
+ * <dd>If set to <code>true</code>, returns symbols for the names (keys) in
+ * a JSON object. Otherwise strings are returned, which is also the default.
+ *
+ * <dt><code>:create_additions</code>
+ * <dd>If set to <code>false</code>, the Parser doesn't create additions
+ * even if a matchin class and <code>create_id</code> was found. This option
+ * defaults to <code>true</code>.
+ *
+ * <dt><code>:object_class</code>
+ * <dd>Defaults to Hash.
+ *
+ * <dt><code>:array_class</code>
+ * <dd>Defaults to Array.
+ * </dl>
+ */
+ @JRubyMethod(name = "new", required = 1, optional = 1, meta = true)
+ public static IRubyObject newInstance(IRubyObject clazz, IRubyObject[] args, Block block) {
+ Parser parser = (Parser)((RubyClass)clazz).allocate();
+
+ parser.callInit(args, block);
+
+ return parser;
+ }
+
+ @JRubyMethod(required = 1, optional = 1, visibility = Visibility.PRIVATE)
+ public IRubyObject initialize(ThreadContext context, IRubyObject[] args) {
+ Ruby runtime = context.getRuntime();
+ RubyString source = convertEncoding(context, args[0].convertToString());
+
+ OptionsReader opts =
+ new OptionsReader(context, args.length > 1 ? args[1] : null);
+
+ this.maxNesting = opts.getInt("max_nesting", DEFAULT_MAX_NESTING);
+ this.allowNaN = opts.getBool("allow_nan", false);
+ this.symbolizeNames = opts.getBool("symbolize_names", false);
+ this.createId =
+ opts.getBool("create_additions", true) ? getCreateId(context)
+ : null;
+ this.objectClass = opts.getClass("object_class", runtime.getHash());
+ this.arrayClass = opts.getClass("array_class", runtime.getArray());
+
+ this.vSource = source;
+ return this;
+ }
+
+ /**
+ * Checks the given string's encoding. If a non-UTF-8 encoding is detected,
+ * a converted copy is returned.
+ * Returns the source string if no conversion is needed.
+ */
+ private RubyString convertEncoding(ThreadContext context, RubyString source) {
+ ByteList bl = source.getByteList();
+ int len = bl.length();
+ if (len < 2) {
+ throw Utils.newException(context, Utils.M_PARSER_ERROR,
+ "A JSON text must at least contain two octets!");
+ }
+
+ if (info.encodingsSupported()) {
+ RubyEncoding encoding = (RubyEncoding)source.encoding(context);
+ if (encoding != info.ascii8bit) {
+ return (RubyString)source.encode(context, info.utf8);
+ }
+
+ String sniffedEncoding = sniffByteList(bl);
+ if (sniffedEncoding == null) return source; // assume UTF-8
+ return reinterpretEncoding(context, source, sniffedEncoding);
+ }
+
+ String sniffedEncoding = sniffByteList(bl);
+ if (sniffedEncoding == null) return source; // assume UTF-8
+ Ruby runtime = context.getRuntime();
+ return (RubyString)info.jsonModule.
+ callMethod(context, "iconv",
+ new IRubyObject[] {
+ runtime.newString("utf-8"),
+ runtime.newString(sniffedEncoding),
+ source});
+ }
+
+ /**
+ * Checks the first four bytes of the given ByteList to infer its encoding,
+ * using the principle demonstrated on section 3 of RFC 4627 (JSON).
+ */
+ private static String sniffByteList(ByteList bl) {
+ if (bl.length() < 4) return null;
+ if (bl.get(0) == 0 && bl.get(2) == 0) {
+ return bl.get(1) == 0 ? "utf-32be" : "utf-16be";
+ }
+ if (bl.get(1) == 0 && bl.get(3) == 0) {
+ return bl.get(2) == 0 ? "utf-32le" : "utf-16le";
+ }
+ return null;
+ }
+
+ /**
+ * Assumes the given (binary) RubyString to be in the given encoding, then
+ * converts it to UTF-8.
+ */
+ private RubyString reinterpretEncoding(ThreadContext context,
+ RubyString str, String sniffedEncoding) {
+ RubyEncoding actualEncoding = info.getEncoding(context, sniffedEncoding);
+ RubyEncoding targetEncoding = info.utf8;
+ RubyString dup = (RubyString)str.dup();
+ dup.force_encoding(context, actualEncoding);
+ return (RubyString)dup.encode_bang(context, targetEncoding);
+ }
+
+ /**
+ * <code>Parser#parse()</code>
+ *
+ * <p>Parses the current JSON text <code>source</code> and returns the
+ * complete data structure as a result.
+ */
+ @JRubyMethod
+ public IRubyObject parse(ThreadContext context) {
+ return new ParserSession(this, context).parse();
+ }
+
+ /**
+ * <code>Parser#source()</code>
+ *
+ * <p>Returns a copy of the current <code>source</code> string, that was
+ * used to construct this Parser.
+ */
+ @JRubyMethod(name = "source")
+ public IRubyObject source_get() {
+ return vSource.dup();
+ }
+
+ /**
+ * Queries <code>JSON.create_id</code>. Returns <code>null</code> if it is
+ * set to <code>nil</code> or <code>false</code>, and a String if not.
+ */
+ private RubyString getCreateId(ThreadContext context) {
+ IRubyObject v = info.jsonModule.callMethod(context, "create_id");
+ return v.isTrue() ? v.convertToString() : null;
+ }
+
+ /**
+ * A string parsing session.
+ *
+ * <p>Once a ParserSession is instantiated, the source string should not
+ * change until the parsing is complete. The ParserSession object assumes
+ * the source {@link RubyString} is still associated to its original
+ * {@link ByteList}, which in turn must still be bound to the same
+ * <code>byte[]</code> value (and on the same offset).
+ */
+ // Ragel uses lots of fall-through
+ @SuppressWarnings("fallthrough")
+ private static class ParserSession {
+ private final Parser parser;
+ private final ThreadContext context;
+ private final ByteList byteList;
+ private final byte[] data;
+ private final StringDecoder decoder;
+ private int currentNesting = 0;
+
+ // initialization value for all state variables.
+ // no idea about the origins of this value, ask Flori ;)
+ private static final int EVIL = 0x666;
+
+ private ParserSession(Parser parser, ThreadContext context) {
+ this.parser = parser;
+ this.context = context;
+ this.byteList = parser.vSource.getByteList();
+ this.data = byteList.unsafeBytes();
+ this.decoder = new StringDecoder(context);
+ }
+
+ private RaiseException unexpectedToken(int absStart, int absEnd) {
+ RubyString msg = getRuntime().newString("unexpected token at '")
+ .cat(data, absStart, absEnd - absStart)
+ .cat((byte)'\'');
+ return newException(Utils.M_PARSER_ERROR, msg);
+ }
+
+ private Ruby getRuntime() {
+ return context.getRuntime();
+ }
+
+
+// line 323 "src/json/ext/Parser.rl"
+
+
+
+// line 305 "src/json/ext/Parser.java"
+private static byte[] init__JSON_value_actions_0()
+{
+ return new byte [] {
+ 0, 1, 0, 1, 1, 1, 2, 1, 3, 1, 4, 1,
+ 5, 1, 6, 1, 7, 1, 8, 1, 9
+ };
+}
+
+private static final byte _JSON_value_actions[] = init__JSON_value_actions_0();
+
+
+private static byte[] init__JSON_value_key_offsets_0()
+{
+ return new byte [] {
+ 0, 0, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+ 21, 22, 23, 24, 25, 26, 27, 28, 29, 30
+ };
+}
+
+private static final byte _JSON_value_key_offsets[] = init__JSON_value_key_offsets_0();
+
+
+private static char[] init__JSON_value_trans_keys_0()
+{
+ return new char [] {
+ 34, 45, 73, 78, 91, 102, 110, 116, 123, 48, 57, 110,
+ 102, 105, 110, 105, 116, 121, 97, 78, 97, 108, 115, 101,
+ 117, 108, 108, 114, 117, 101, 0
+ };
+}
+
+private static final char _JSON_value_trans_keys[] = init__JSON_value_trans_keys_0();
+
+
+private static byte[] init__JSON_value_single_lengths_0()
+{
+ return new byte [] {
+ 0, 9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 0
+ };
+}
+
+private static final byte _JSON_value_single_lengths[] = init__JSON_value_single_lengths_0();
+
+
+private static byte[] init__JSON_value_range_lengths_0()
+{
+ return new byte [] {
+ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+ };
+}
+
+private static final byte _JSON_value_range_lengths[] = init__JSON_value_range_lengths_0();
+
+
+private static byte[] init__JSON_value_index_offsets_0()
+{
+ return new byte [] {
+ 0, 0, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29,
+ 31, 33, 35, 37, 39, 41, 43, 45, 47, 49
+ };
+}
+
+private static final byte _JSON_value_index_offsets[] = init__JSON_value_index_offsets_0();
+
+
+private static byte[] init__JSON_value_trans_targs_0()
+{
+ return new byte [] {
+ 21, 21, 2, 9, 21, 11, 15, 18, 21, 21, 0, 3,
+ 0, 4, 0, 5, 0, 6, 0, 7, 0, 8, 0, 21,
+ 0, 10, 0, 21, 0, 12, 0, 13, 0, 14, 0, 21,
+ 0, 16, 0, 17, 0, 21, 0, 19, 0, 20, 0, 21,
+ 0, 0, 0
+ };
+}
+
+private static final byte _JSON_value_trans_targs[] = init__JSON_value_trans_targs_0();
+
+
+private static byte[] init__JSON_value_trans_actions_0()
+{
+ return new byte [] {
+ 13, 11, 0, 0, 15, 0, 0, 0, 17, 11, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9,
+ 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 3,
+ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 5,
+ 0, 0, 0
+ };
+}
+
+private static final byte _JSON_value_trans_actions[] = init__JSON_value_trans_actions_0();
+
+
+private static byte[] init__JSON_value_from_state_actions_0()
+{
+ return new byte [] {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 19
+ };
+}
+
+private static final byte _JSON_value_from_state_actions[] = init__JSON_value_from_state_actions_0();
+
+
+static final int JSON_value_start = 1;
+static final int JSON_value_first_final = 21;
+static final int JSON_value_error = 0;
+
+static final int JSON_value_en_main = 1;
+
+
+// line 429 "src/json/ext/Parser.rl"
+
+
+ ParserResult parseValue(int p, int pe) {
+ int cs = EVIL;
+ IRubyObject result = null;
+
+
+// line 427 "src/json/ext/Parser.java"
+ {
+ cs = JSON_value_start;
+ }
+
+// line 436 "src/json/ext/Parser.rl"
+
+// line 434 "src/json/ext/Parser.java"
+ {
+ int _klen;
+ int _trans = 0;
+ int _acts;
+ int _nacts;
+ int _keys;
+ int _goto_targ = 0;
+
+ _goto: while (true) {
+ switch ( _goto_targ ) {
+ case 0:
+ if ( p == pe ) {
+ _goto_targ = 4;
+ continue _goto;
+ }
+ if ( cs == 0 ) {
+ _goto_targ = 5;
+ continue _goto;
+ }
+case 1:
+ _acts = _JSON_value_from_state_actions[cs];
+ _nacts = (int) _JSON_value_actions[_acts++];
+ while ( _nacts-- > 0 ) {
+ switch ( _JSON_value_actions[_acts++] ) {
+ case 9:
+// line 414 "src/json/ext/Parser.rl"
+ {
+ p--;
+ { p += 1; _goto_targ = 5; if (true) continue _goto;}
+ }
+ break;
+// line 466 "src/json/ext/Parser.java"
+ }
+ }
+
+ _match: do {
+ _keys = _JSON_value_key_offsets[cs];
+ _trans = _JSON_value_index_offsets[cs];
+ _klen = _JSON_value_single_lengths[cs];
+ if ( _klen > 0 ) {
+ int _lower = _keys;
+ int _mid;
+ int _upper = _keys + _klen - 1;
+ while (true) {
+ if ( _upper < _lower )
+ break;
+
+ _mid = _lower + ((_upper-_lower) >> 1);
+ if ( data[p] < _JSON_value_trans_keys[_mid] )
+ _upper = _mid - 1;
+ else if ( data[p] > _JSON_value_trans_keys[_mid] )
+ _lower = _mid + 1;
+ else {
+ _trans += (_mid - _keys);
+ break _match;
+ }
+ }
+ _keys += _klen;
+ _trans += _klen;
+ }
+
+ _klen = _JSON_value_range_lengths[cs];
+ if ( _klen > 0 ) {
+ int _lower = _keys;
+ int _mid;
+ int _upper = _keys + (_klen<<1) - 2;
+ while (true) {
+ if ( _upper < _lower )
+ break;
+
+ _mid = _lower + (((_upper-_lower) >> 1) & ~1);
+ if ( data[p] < _JSON_value_trans_keys[_mid] )
+ _upper = _mid - 2;
+ else if ( data[p] > _JSON_value_trans_keys[_mid+1] )
+ _lower = _mid + 2;
+ else {
+ _trans += ((_mid - _keys)>>1);
+ break _match;
+ }
+ }
+ _trans += _klen;
+ }
+ } while (false);
+
+ cs = _JSON_value_trans_targs[_trans];
+
+ if ( _JSON_value_trans_actions[_trans] != 0 ) {
+ _acts = _JSON_value_trans_actions[_trans];
+ _nacts = (int) _JSON_value_actions[_acts++];
+ while ( _nacts-- > 0 )
+ {
+ switch ( _JSON_value_actions[_acts++] )
+ {
+ case 0:
+// line 331 "src/json/ext/Parser.rl"
+ {
+ result = getRuntime().getNil();
+ }
+ break;
+ case 1:
+// line 334 "src/json/ext/Parser.rl"
+ {
+ result = getRuntime().getFalse();
+ }
+ break;
+ case 2:
+// line 337 "src/json/ext/Parser.rl"
+ {
+ result = getRuntime().getTrue();
+ }
+ break;
+ case 3:
+// line 340 "src/json/ext/Parser.rl"
+ {
+ if (parser.allowNaN) {
+ result = getConstant(CONST_NAN);
+ } else {
+ throw unexpectedToken(p - 2, pe);
+ }
+ }
+ break;
+ case 4:
+// line 347 "src/json/ext/Parser.rl"
+ {
+ if (parser.allowNaN) {
+ result = getConstant(CONST_INFINITY);
+ } else {
+ throw unexpectedToken(p - 7, pe);
+ }
+ }
+ break;
+ case 5:
+// line 354 "src/json/ext/Parser.rl"
+ {
+ if (pe > p + 9 &&
+ absSubSequence(p, p + 9).toString().equals(JSON_MINUS_INFINITY)) {
+
+ if (parser.allowNaN) {
+ result = getConstant(CONST_MINUS_INFINITY);
+ {p = (( p + 10))-1;}
+ p--;
+ { p += 1; _goto_targ = 5; if (true) continue _goto;}
+ } else {
+ throw unexpectedToken(p, pe);
+ }
+ }
+ ParserResult res = parseFloat(p, pe);
+ if (res != null) {
+ result = res.result;
+ {p = (( res.p))-1;}
+ }
+ res = parseInteger(p, pe);
+ if (res != null) {
+ result = res.result;
+ {p = (( res.p))-1;}
+ }
+ p--;
+ { p += 1; _goto_targ = 5; if (true) continue _goto;}
+ }
+ break;
+ case 6:
+// line 380 "src/json/ext/Parser.rl"
+ {
+ ParserResult res = parseString(p, pe);
+ if (res == null) {
+ p--;
+ { p += 1; _goto_targ = 5; if (true) continue _goto;}
+ } else {
+ result = res.result;
+ {p = (( res.p))-1;}
+ }
+ }
+ break;
+ case 7:
+// line 390 "src/json/ext/Parser.rl"
+ {
+ currentNesting++;
+ ParserResult res = parseArray(p, pe);
+ currentNesting--;
+ if (res == null) {
+ p--;
+ { p += 1; _goto_targ = 5; if (true) continue _goto;}
+ } else {
+ result = res.result;
+ {p = (( res.p))-1;}
+ }
+ }
+ break;
+ case 8:
+// line 402 "src/json/ext/Parser.rl"
+ {
+ currentNesting++;
+ ParserResult res = parseObject(p, pe);
+ currentNesting--;
+ if (res == null) {
+ p--;
+ { p += 1; _goto_targ = 5; if (true) continue _goto;}
+ } else {
+ result = res.result;
+ {p = (( res.p))-1;}
+ }
+ }
+ break;
+// line 638 "src/json/ext/Parser.java"
+ }
+ }
+ }
+
+case 2:
+ if ( cs == 0 ) {
+ _goto_targ = 5;
+ continue _goto;
+ }
+ if ( ++p != pe ) {
+ _goto_targ = 1;
+ continue _goto;
+ }
+case 4:
+case 5:
+ }
+ break; }
+ }
+
+// line 437 "src/json/ext/Parser.rl"
+
+ if (cs >= JSON_value_first_final && result != null) {
+ return new ParserResult(result, p);
+ } else {
+ return null;
+ }
+ }
+
+
+// line 668 "src/json/ext/Parser.java"
+private static byte[] init__JSON_integer_actions_0()
+{
+ return new byte [] {
+ 0, 1, 0
+ };
+}
+
+private static final byte _JSON_integer_actions[] = init__JSON_integer_actions_0();
+
+
+private static byte[] init__JSON_integer_key_offsets_0()
+{
+ return new byte [] {
+ 0, 0, 4, 7, 9, 11
+ };
+}
+
+private static final byte _JSON_integer_key_offsets[] = init__JSON_integer_key_offsets_0();
+
+
+private static char[] init__JSON_integer_trans_keys_0()
+{
+ return new char [] {
+ 45, 48, 49, 57, 48, 49, 57, 48, 57, 48, 57, 0
+ };
+}
+
+private static final char _JSON_integer_trans_keys[] = init__JSON_integer_trans_keys_0();
+
+
+private static byte[] init__JSON_integer_single_lengths_0()
+{
+ return new byte [] {
+ 0, 2, 1, 0, 0, 0
+ };
+}
+
+private static final byte _JSON_integer_single_lengths[] = init__JSON_integer_single_lengths_0();
+
+
+private static byte[] init__JSON_integer_range_lengths_0()
+{
+ return new byte [] {
+ 0, 1, 1, 1, 1, 0
+ };
+}
+
+private static final byte _JSON_integer_range_lengths[] = init__JSON_integer_range_lengths_0();
+
+
+private static byte[] init__JSON_integer_index_offsets_0()
+{
+ return new byte [] {
+ 0, 0, 4, 7, 9, 11
+ };
+}
+
+private static final byte _JSON_integer_index_offsets[] = init__JSON_integer_index_offsets_0();
+
+
+private static byte[] init__JSON_integer_indicies_0()
+{
+ return new byte [] {
+ 0, 2, 3, 1, 2, 3, 1, 1, 4, 3, 4, 1,
+ 0
+ };
+}
+
+private static final byte _JSON_integer_indicies[] = init__JSON_integer_indicies_0();
+
+
+private static byte[] init__JSON_integer_trans_targs_0()
+{
+ return new byte [] {
+ 2, 0, 3, 4, 5
+ };
+}
+
+private static final byte _JSON_integer_trans_targs[] = init__JSON_integer_trans_targs_0();
+
+
+private static byte[] init__JSON_integer_trans_actions_0()
+{
+ return new byte [] {
+ 0, 0, 0, 0, 1
+ };
+}
+
+private static final byte _JSON_integer_trans_actions[] = init__JSON_integer_trans_actions_0();
+
+
+static final int JSON_integer_start = 1;
+static final int JSON_integer_first_final = 5;
+static final int JSON_integer_error = 0;
+
+static final int JSON_integer_en_main = 1;
+
+
+// line 456 "src/json/ext/Parser.rl"
+
+
+ ParserResult parseInteger(int p, int pe) {
+ int cs = EVIL;
+
+
+// line 774 "src/json/ext/Parser.java"
+ {
+ cs = JSON_integer_start;
+ }
+
+// line 462 "src/json/ext/Parser.rl"
+ int memo = p;
+
+// line 782 "src/json/ext/Parser.java"
+ {
+ int _klen;
+ int _trans = 0;
+ int _acts;
+ int _nacts;
+ int _keys;
+ int _goto_targ = 0;
+
+ _goto: while (true) {
+ switch ( _goto_targ ) {
+ case 0:
+ if ( p == pe ) {
+ _goto_targ = 4;
+ continue _goto;
+ }
+ if ( cs == 0 ) {
+ _goto_targ = 5;
+ continue _goto;
+ }
+case 1:
+ _match: do {
+ _keys = _JSON_integer_key_offsets[cs];
+ _trans = _JSON_integer_index_offsets[cs];
+ _klen = _JSON_integer_single_lengths[cs];
+ if ( _klen > 0 ) {
+ int _lower = _keys;
+ int _mid;
+ int _upper = _keys + _klen - 1;
+ while (true) {
+ if ( _upper < _lower )
+ break;
+
+ _mid = _lower + ((_upper-_lower) >> 1);
+ if ( data[p] < _JSON_integer_trans_keys[_mid] )
+ _upper = _mid - 1;
+ else if ( data[p] > _JSON_integer_trans_keys[_mid] )
+ _lower = _mid + 1;
+ else {
+ _trans += (_mid - _keys);
+ break _match;
+ }
+ }
+ _keys += _klen;
+ _trans += _klen;
+ }
+
+ _klen = _JSON_integer_range_lengths[cs];
+ if ( _klen > 0 ) {
+ int _lower = _keys;
+ int _mid;
+ int _upper = _keys + (_klen<<1) - 2;
+ while (true) {
+ if ( _upper < _lower )
+ break;
+
+ _mid = _lower + (((_upper-_lower) >> 1) & ~1);
+ if ( data[p] < _JSON_integer_trans_keys[_mid] )
+ _upper = _mid - 2;
+ else if ( data[p] > _JSON_integer_trans_keys[_mid+1] )
+ _lower = _mid + 2;
+ else {
+ _trans += ((_mid - _keys)>>1);
+ break _match;
+ }
+ }
+ _trans += _klen;
+ }
+ } while (false);
+
+ _trans = _JSON_integer_indicies[_trans];
+ cs = _JSON_integer_trans_targs[_trans];
+
+ if ( _JSON_integer_trans_actions[_trans] != 0 ) {
+ _acts = _JSON_integer_trans_actions[_trans];
+ _nacts = (int) _JSON_integer_actions[_acts++];
+ while ( _nacts-- > 0 )
+ {
+ switch ( _JSON_integer_actions[_acts++] )
+ {
+ case 0:
+// line 450 "src/json/ext/Parser.rl"
+ {
+ p--;
+ { p += 1; _goto_targ = 5; if (true) continue _goto;}
+ }
+ break;
+// line 869 "src/json/ext/Parser.java"
+ }
+ }
+ }
+
+case 2:
+ if ( cs == 0 ) {
+ _goto_targ = 5;
+ continue _goto;
+ }
+ if ( ++p != pe ) {
+ _goto_targ = 1;
+ continue _goto;
+ }
+case 4:
+case 5:
+ }
+ break; }
+ }
+
+// line 464 "src/json/ext/Parser.rl"
+
+ if (cs < JSON_integer_first_final) {
+ return null;
+ }
+
+ ByteList num = absSubSequence(memo, p);
+ // note: this is actually a shared string, but since it is temporary and
+ // read-only, it doesn't really matter
+ RubyString expr = RubyString.newStringLight(getRuntime(), num);
+ RubyInteger number = RubyNumeric.str2inum(getRuntime(), expr, 10, true);
+ return new ParserResult(number, p + 1);
+ }
+
+
+// line 904 "src/json/ext/Parser.java"
+private static byte[] init__JSON_float_actions_0()
+{
+ return new byte [] {
+ 0, 1, 0
+ };
+}
+
+private static final byte _JSON_float_actions[] = init__JSON_float_actions_0();
+
+
+private static byte[] init__JSON_float_key_offsets_0()
+{
+ return new byte [] {
+ 0, 0, 4, 7, 10, 12, 18, 22, 24, 30, 35
+ };
+}
+
+private static final byte _JSON_float_key_offsets[] = init__JSON_float_key_offsets_0();
+
+
+private static char[] init__JSON_float_trans_keys_0()
+{
+ return new char [] {
+ 45, 48, 49, 57, 48, 49, 57, 46, 69, 101, 48, 57,
+ 69, 101, 45, 46, 48, 57, 43, 45, 48, 57, 48, 57,
+ 69, 101, 45, 46, 48, 57, 46, 69, 101, 48, 57, 0
+ };
+}
+
+private static final char _JSON_float_trans_keys[] = init__JSON_float_trans_keys_0();
+
+
+private static byte[] init__JSON_float_single_lengths_0()
+{
+ return new byte [] {
+ 0, 2, 1, 3, 0, 2, 2, 0, 2, 3, 0
+ };
+}
+
+private static final byte _JSON_float_single_lengths[] = init__JSON_float_single_lengths_0();
+
+
+private static byte[] init__JSON_float_range_lengths_0()
+{
+ return new byte [] {
+ 0, 1, 1, 0, 1, 2, 1, 1, 2, 1, 0
+ };
+}
+
+private static final byte _JSON_float_range_lengths[] = init__JSON_float_range_lengths_0();
+
+
+private static byte[] init__JSON_float_index_offsets_0()
+{
+ return new byte [] {
+ 0, 0, 4, 7, 11, 13, 18, 22, 24, 29, 34
+ };
+}
+
+private static final byte _JSON_float_index_offsets[] = init__JSON_float_index_offsets_0();
+
+
+private static byte[] init__JSON_float_indicies_0()
+{
+ return new byte [] {
+ 0, 2, 3, 1, 2, 3, 1, 4, 5, 5, 1, 6,
+ 1, 5, 5, 1, 6, 7, 8, 8, 9, 1, 9, 1,
+ 1, 1, 1, 9, 7, 4, 5, 5, 3, 1, 1, 0
+ };
+}
+
+private static final byte _JSON_float_indicies[] = init__JSON_float_indicies_0();
+
+
+private static byte[] init__JSON_float_trans_targs_0()
+{
+ return new byte [] {
+ 2, 0, 3, 9, 4, 6, 5, 10, 7, 8
+ };
+}
+
+private static final byte _JSON_float_trans_targs[] = init__JSON_float_trans_targs_0();
+
+
+private static byte[] init__JSON_float_trans_actions_0()
+{
+ return new byte [] {
+ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0
+ };
+}
+
+private static final byte _JSON_float_trans_actions[] = init__JSON_float_trans_actions_0();
+
+
+static final int JSON_float_start = 1;
+static final int JSON_float_first_final = 10;
+static final int JSON_float_error = 0;
+
+static final int JSON_float_en_main = 1;
+
+
+// line 492 "src/json/ext/Parser.rl"
+
+
+ ParserResult parseFloat(int p, int pe) {
+ int cs = EVIL;
+
+
+// line 1013 "src/json/ext/Parser.java"
+ {
+ cs = JSON_float_start;
+ }
+
+// line 498 "src/json/ext/Parser.rl"
+ int memo = p;
+
+// line 1021 "src/json/ext/Parser.java"
+ {
+ int _klen;
+ int _trans = 0;
+ int _acts;
+ int _nacts;
+ int _keys;
+ int _goto_targ = 0;
+
+ _goto: while (true) {
+ switch ( _goto_targ ) {
+ case 0:
+ if ( p == pe ) {
+ _goto_targ = 4;
+ continue _goto;
+ }
+ if ( cs == 0 ) {
+ _goto_targ = 5;
+ continue _goto;
+ }
+case 1:
+ _match: do {
+ _keys = _JSON_float_key_offsets[cs];
+ _trans = _JSON_float_index_offsets[cs];
+ _klen = _JSON_float_single_lengths[cs];
+ if ( _klen > 0 ) {
+ int _lower = _keys;
+ int _mid;
+ int _upper = _keys + _klen - 1;
+ while (true) {
+ if ( _upper < _lower )
+ break;
+
+ _mid = _lower + ((_upper-_lower) >> 1);
+ if ( data[p] < _JSON_float_trans_keys[_mid] )
+ _upper = _mid - 1;
+ else if ( data[p] > _JSON_float_trans_keys[_mid] )
+ _lower = _mid + 1;
+ else {
+ _trans += (_mid - _keys);
+ break _match;
+ }
+ }
+ _keys += _klen;
+ _trans += _klen;
+ }
+
+ _klen = _JSON_float_range_lengths[cs];
+ if ( _klen > 0 ) {
+ int _lower = _keys;
+ int _mid;
+ int _upper = _keys + (_klen<<1) - 2;
+ while (true) {
+ if ( _upper < _lower )
+ break;
+
+ _mid = _lower + (((_upper-_lower) >> 1) & ~1);
+ if ( data[p] < _JSON_float_trans_keys[_mid] )
+ _upper = _mid - 2;
+ else if ( data[p] > _JSON_float_trans_keys[_mid+1] )
+ _lower = _mid + 2;
+ else {
+ _trans += ((_mid - _keys)>>1);
+ break _match;
+ }
+ }
+ _trans += _klen;
+ }
+ } while (false);
+
+ _trans = _JSON_float_indicies[_trans];
+ cs = _JSON_float_trans_targs[_trans];
+
+ if ( _JSON_float_trans_actions[_trans] != 0 ) {
+ _acts = _JSON_float_trans_actions[_trans];
+ _nacts = (int) _JSON_float_actions[_acts++];
+ while ( _nacts-- > 0 )
+ {
+ switch ( _JSON_float_actions[_acts++] )
+ {
+ case 0:
+// line 483 "src/json/ext/Parser.rl"
+ {
+ p--;
+ { p += 1; _goto_targ = 5; if (true) continue _goto;}
+ }
+ break;
+// line 1108 "src/json/ext/Parser.java"
+ }
+ }
+ }
+
+case 2:
+ if ( cs == 0 ) {
+ _goto_targ = 5;
+ continue _goto;
+ }
+ if ( ++p != pe ) {
+ _goto_targ = 1;
+ continue _goto;
+ }
+case 4:
+case 5:
+ }
+ break; }
+ }
+
+// line 500 "src/json/ext/Parser.rl"
+
+ if (cs < JSON_float_first_final) {
+ return null;
+ }
+
+ ByteList num = absSubSequence(memo, p);
+ // note: this is actually a shared string, but since it is temporary and
+ // read-only, it doesn't really matter
+ RubyString expr = RubyString.newStringLight(getRuntime(), num);
+ RubyFloat number = RubyNumeric.str2fnum(getRuntime(), expr, true);
+ return new ParserResult(number, p + 1);
+ }
+
+
+// line 1143 "src/json/ext/Parser.java"
+private static byte[] init__JSON_string_actions_0()
+{
+ return new byte [] {
+ 0, 2, 0, 1
+ };
+}
+
+private static final byte _JSON_string_actions[] = init__JSON_string_actions_0();
+
+
+private static byte[] init__JSON_string_key_offsets_0()
+{
+ return new byte [] {
+ 0, 0, 1, 5, 8, 14, 20, 26, 32
+ };
+}
+
+private static final byte _JSON_string_key_offsets[] = init__JSON_string_key_offsets_0();
+
+
+private static char[] init__JSON_string_trans_keys_0()
+{
+ return new char [] {
+ 34, 34, 92, 0, 31, 117, 0, 31, 48, 57, 65, 70,
+ 97, 102, 48, 57, 65, 70, 97, 102, 48, 57, 65, 70,
+ 97, 102, 48, 57, 65, 70, 97, 102, 0
+ };
+}
+
+private static final char _JSON_string_trans_keys[] = init__JSON_string_trans_keys_0();
+
+
+private static byte[] init__JSON_string_single_lengths_0()
+{
+ return new byte [] {
+ 0, 1, 2, 1, 0, 0, 0, 0, 0
+ };
+}
+
+private static final byte _JSON_string_single_lengths[] = init__JSON_string_single_lengths_0();
+
+
+private static byte[] init__JSON_string_range_lengths_0()
+{
+ return new byte [] {
+ 0, 0, 1, 1, 3, 3, 3, 3, 0
+ };
+}
+
+private static final byte _JSON_string_range_lengths[] = init__JSON_string_range_lengths_0();
+
+
+private static byte[] init__JSON_string_index_offsets_0()
+{
+ return new byte [] {
+ 0, 0, 2, 6, 9, 13, 17, 21, 25
+ };
+}
+
+private static final byte _JSON_string_index_offsets[] = init__JSON_string_index_offsets_0();
+
+
+private static byte[] init__JSON_string_indicies_0()
+{
+ return new byte [] {
+ 0, 1, 2, 3, 1, 0, 4, 1, 0, 5, 5, 5,
+ 1, 6, 6, 6, 1, 7, 7, 7, 1, 0, 0, 0,
+ 1, 1, 0
+ };
+}
+
+private static final byte _JSON_string_indicies[] = init__JSON_string_indicies_0();
+
+
+private static byte[] init__JSON_string_trans_targs_0()
+{
+ return new byte [] {
+ 2, 0, 8, 3, 4, 5, 6, 7
+ };
+}
+
+private static final byte _JSON_string_trans_targs[] = init__JSON_string_trans_targs_0();
+
+
+private static byte[] init__JSON_string_trans_actions_0()
+{
+ return new byte [] {
+ 0, 0, 1, 0, 0, 0, 0, 0
+ };
+}
+
+private static final byte _JSON_string_trans_actions[] = init__JSON_string_trans_actions_0();
+
+
+static final int JSON_string_start = 1;
+static final int JSON_string_first_final = 8;
+static final int JSON_string_error = 0;
+
+static final int JSON_string_en_main = 1;
+
+
+// line 544 "src/json/ext/Parser.rl"
+
+
+ ParserResult parseString(int p, int pe) {
+ int cs = EVIL;
+ RubyString result = null;
+
+
+// line 1253 "src/json/ext/Parser.java"
+ {
+ cs = JSON_string_start;
+ }
+
+// line 551 "src/json/ext/Parser.rl"
+ int memo = p;
+
+// line 1261 "src/json/ext/Parser.java"
+ {
+ int _klen;
+ int _trans = 0;
+ int _acts;
+ int _nacts;
+ int _keys;
+ int _goto_targ = 0;
+
+ _goto: while (true) {
+ switch ( _goto_targ ) {
+ case 0:
+ if ( p == pe ) {
+ _goto_targ = 4;
+ continue _goto;
+ }
+ if ( cs == 0 ) {
+ _goto_targ = 5;
+ continue _goto;
+ }
+case 1:
+ _match: do {
+ _keys = _JSON_string_key_offsets[cs];
+ _trans = _JSON_string_index_offsets[cs];
+ _klen = _JSON_string_single_lengths[cs];
+ if ( _klen > 0 ) {
+ int _lower = _keys;
+ int _mid;
+ int _upper = _keys + _klen - 1;
+ while (true) {
+ if ( _upper < _lower )
+ break;
+
+ _mid = _lower + ((_upper-_lower) >> 1);
+ if ( data[p] < _JSON_string_trans_keys[_mid] )
+ _upper = _mid - 1;
+ else if ( data[p] > _JSON_string_trans_keys[_mid] )
+ _lower = _mid + 1;
+ else {
+ _trans += (_mid - _keys);
+ break _match;
+ }
+ }
+ _keys += _klen;
+ _trans += _klen;
+ }
+
+ _klen = _JSON_string_range_lengths[cs];
+ if ( _klen > 0 ) {
+ int _lower = _keys;
+ int _mid;
+ int _upper = _keys + (_klen<<1) - 2;
+ while (true) {
+ if ( _upper < _lower )
+ break;
+
+ _mid = _lower + (((_upper-_lower) >> 1) & ~1);
+ if ( data[p] < _JSON_string_trans_keys[_mid] )
+ _upper = _mid - 2;
+ else if ( data[p] > _JSON_string_trans_keys[_mid+1] )
+ _lower = _mid + 2;
+ else {
+ _trans += ((_mid - _keys)>>1);
+ break _match;
+ }
+ }
+ _trans += _klen;
+ }
+ } while (false);
+
+ _trans = _JSON_string_indicies[_trans];
+ cs = _JSON_string_trans_targs[_trans];
+
+ if ( _JSON_string_trans_actions[_trans] != 0 ) {
+ _acts = _JSON_string_trans_actions[_trans];
+ _nacts = (int) _JSON_string_actions[_acts++];
+ while ( _nacts-- > 0 )
+ {
+ switch ( _JSON_string_actions[_acts++] )
+ {
+ case 0:
+// line 519 "src/json/ext/Parser.rl"
+ {
+ int offset = byteList.begin();
+ ByteList decoded = decoder.decode(byteList, memo + 1 - offset,
+ p - offset);
+ result = getRuntime().newString(decoded);
+ if (result == null) {
+ p--;
+ { p += 1; _goto_targ = 5; if (true) continue _goto;}
+ } else {
+ {p = (( p + 1))-1;}
+ }
+ }
+ break;
+ case 1:
+// line 532 "src/json/ext/Parser.rl"
+ {
+ p--;
+ { p += 1; _goto_targ = 5; if (true) continue _goto;}
+ }
+ break;
+// line 1363 "src/json/ext/Parser.java"
+ }
+ }
+ }
+
+case 2:
+ if ( cs == 0 ) {
+ _goto_targ = 5;
+ continue _goto;
+ }
+ if ( ++p != pe ) {
+ _goto_targ = 1;
+ continue _goto;
+ }
+case 4:
+case 5:
+ }
+ break; }
+ }
+
+// line 553 "src/json/ext/Parser.rl"
+
+ if (cs >= JSON_string_first_final && result != null) {
+ return new ParserResult(result, p + 1);
+ } else {
+ return null;
+ }
+ }
+
+
+// line 1393 "src/json/ext/Parser.java"
+private static byte[] init__JSON_array_actions_0()
+{
+ return new byte [] {
+ 0, 1, 0, 1, 1
+ };
+}
+
+private static final byte _JSON_array_actions[] = init__JSON_array_actions_0();
+
+
+private static byte[] init__JSON_array_key_offsets_0()
+{
+ return new byte [] {
+ 0, 0, 1, 18, 25, 41, 43, 44, 46, 47, 49, 50,
+ 52, 53, 55, 56, 58, 59
+ };
+}
+
+private static final byte _JSON_array_key_offsets[] = init__JSON_array_key_offsets_0();
+
+
+private static char[] init__JSON_array_trans_keys_0()
+{
+ return new char [] {
+ 91, 13, 32, 34, 45, 47, 73, 78, 91, 93, 102, 110,
+ 116, 123, 9, 10, 48, 57, 13, 32, 44, 47, 93, 9,
+ 10, 13, 32, 34, 45, 47, 73, 78, 91, 102, 110, 116,
+ 123, 9, 10, 48, 57, 42, 47, 42, 42, 47, 10, 42,
+ 47, 42, 42, 47, 10, 42, 47, 42, 42, 47, 10, 0
+ };
+}
+
+private static final char _JSON_array_trans_keys[] = init__JSON_array_trans_keys_0();
+
+
+private static byte[] init__JSON_array_single_lengths_0()
+{
+ return new byte [] {
+ 0, 1, 13, 5, 12, 2, 1, 2, 1, 2, 1, 2,
+ 1, 2, 1, 2, 1, 0
+ };
+}
+
+private static final byte _JSON_array_single_lengths[] = init__JSON_array_single_lengths_0();
+
+
+private static byte[] init__JSON_array_range_lengths_0()
+{
+ return new byte [] {
+ 0, 0, 2, 1, 2, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0
+ };
+}
+
+private static final byte _JSON_array_range_lengths[] = init__JSON_array_range_lengths_0();
+
+
+private static byte[] init__JSON_array_index_offsets_0()
+{
+ return new byte [] {
+ 0, 0, 2, 18, 25, 40, 43, 45, 48, 50, 53, 55,
+ 58, 60, 63, 65, 68, 70
+ };
+}
+
+private static final byte _JSON_array_index_offsets[] = init__JSON_array_index_offsets_0();
+
+
+private static byte[] init__JSON_array_indicies_0()
+{
+ return new byte [] {
+ 0, 1, 0, 0, 2, 2, 3, 2, 2, 2, 4, 2,
+ 2, 2, 2, 0, 2, 1, 5, 5, 6, 7, 4, 5,
+ 1, 6, 6, 2, 2, 8, 2, 2, 2, 2, 2, 2,
+ 2, 6, 2, 1, 9, 10, 1, 11, 9, 11, 6, 9,
+ 6, 10, 12, 13, 1, 14, 12, 14, 5, 12, 5, 13,
+ 15, 16, 1, 17, 15, 17, 0, 15, 0, 16, 1, 0
+ };
+}
+
+private static final byte _JSON_array_indicies[] = init__JSON_array_indicies_0();
+
+
+private static byte[] init__JSON_array_trans_targs_0()
+{
+ return new byte [] {
+ 2, 0, 3, 13, 17, 3, 4, 9, 5, 6, 8, 7,
+ 10, 12, 11, 14, 16, 15
+ };
+}
+
+private static final byte _JSON_array_trans_targs[] = init__JSON_array_trans_targs_0();
+
+
+private static byte[] init__JSON_array_trans_actions_0()
+{
+ return new byte [] {
+ 0, 0, 1, 0, 3, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0
+ };
+}
+
+private static final byte _JSON_array_trans_actions[] = init__JSON_array_trans_actions_0();
+
+
+static final int JSON_array_start = 1;
+static final int JSON_array_first_final = 17;
+static final int JSON_array_error = 0;
+
+static final int JSON_array_en_main = 1;
+
+
+// line 594 "src/json/ext/Parser.rl"
+
+
+ ParserResult parseArray(int p, int pe) {
+ int cs = EVIL;
+
+ if (parser.maxNesting > 0 && currentNesting > parser.maxNesting) {
+ throw newException(Utils.M_NESTING_ERROR,
+ "nesting of " + currentNesting + " is too deep");
+ }
+
+ // this is guaranteed to be a RubyArray due to the earlier
+ // allocator test at OptionsReader#getClass
+ RubyArray result =
+ (RubyArray)parser.arrayClass.newInstance(context,
+ IRubyObject.NULL_ARRAY, Block.NULL_BLOCK);
+
+
+// line 1524 "src/json/ext/Parser.java"
+ {
+ cs = JSON_array_start;
+ }
+
+// line 611 "src/json/ext/Parser.rl"
+
+// line 1531 "src/json/ext/Parser.java"
+ {
+ int _klen;
+ int _trans = 0;
+ int _acts;
+ int _nacts;
+ int _keys;
+ int _goto_targ = 0;
+
+ _goto: while (true) {
+ switch ( _goto_targ ) {
+ case 0:
+ if ( p == pe ) {
+ _goto_targ = 4;
+ continue _goto;
+ }
+ if ( cs == 0 ) {
+ _goto_targ = 5;
+ continue _goto;
+ }
+case 1:
+ _match: do {
+ _keys = _JSON_array_key_offsets[cs];
+ _trans = _JSON_array_index_offsets[cs];
+ _klen = _JSON_array_single_lengths[cs];
+ if ( _klen > 0 ) {
+ int _lower = _keys;
+ int _mid;
+ int _upper = _keys + _klen - 1;
+ while (true) {
+ if ( _upper < _lower )
+ break;
+
+ _mid = _lower + ((_upper-_lower) >> 1);
+ if ( data[p] < _JSON_array_trans_keys[_mid] )
+ _upper = _mid - 1;
+ else if ( data[p] > _JSON_array_trans_keys[_mid] )
+ _lower = _mid + 1;
+ else {
+ _trans += (_mid - _keys);
+ break _match;
+ }
+ }
+ _keys += _klen;
+ _trans += _klen;
+ }
+
+ _klen = _JSON_array_range_lengths[cs];
+ if ( _klen > 0 ) {
+ int _lower = _keys;
+ int _mid;
+ int _upper = _keys + (_klen<<1) - 2;
+ while (true) {
+ if ( _upper < _lower )
+ break;
+
+ _mid = _lower + (((_upper-_lower) >> 1) & ~1);
+ if ( data[p] < _JSON_array_trans_keys[_mid] )
+ _upper = _mid - 2;
+ else if ( data[p] > _JSON_array_trans_keys[_mid+1] )
+ _lower = _mid + 2;
+ else {
+ _trans += ((_mid - _keys)>>1);
+ break _match;
+ }
+ }
+ _trans += _klen;
+ }
+ } while (false);
+
+ _trans = _JSON_array_indicies[_trans];
+ cs = _JSON_array_trans_targs[_trans];
+
+ if ( _JSON_array_trans_actions[_trans] != 0 ) {
+ _acts = _JSON_array_trans_actions[_trans];
+ _nacts = (int) _JSON_array_actions[_acts++];
+ while ( _nacts-- > 0 )
+ {
+ switch ( _JSON_array_actions[_acts++] )
+ {
+ case 0:
+// line 567 "src/json/ext/Parser.rl"
+ {
+ ParserResult res = parseValue(p, pe);
+ if (res == null) {
+ p--;
+ { p += 1; _goto_targ = 5; if (true) continue _goto;}
+ } else {
+ result.append(res.result);
+ {p = (( res.p))-1;}
+ }
+ }
+ break;
+ case 1:
+// line 578 "src/json/ext/Parser.rl"
+ {
+ p--;
+ { p += 1; _goto_targ = 5; if (true) continue _goto;}
+ }
+ break;
+// line 1631 "src/json/ext/Parser.java"
+ }
+ }
+ }
+
+case 2:
+ if ( cs == 0 ) {
+ _goto_targ = 5;
+ continue _goto;
+ }
+ if ( ++p != pe ) {
+ _goto_targ = 1;
+ continue _goto;
+ }
+case 4:
+case 5:
+ }
+ break; }
+ }
+
+// line 612 "src/json/ext/Parser.rl"
+
+ if (cs >= JSON_array_first_final) {
+ return new ParserResult(result, p + 1);
+ } else {
+ throw unexpectedToken(p, pe);
+ }
+ }
+
+
+// line 1661 "src/json/ext/Parser.java"
+private static byte[] init__JSON_object_actions_0()
+{
+ return new byte [] {
+ 0, 1, 0, 1, 1, 1, 2
+ };
+}
+
+private static final byte _JSON_object_actions[] = init__JSON_object_actions_0();
+
+
+private static byte[] init__JSON_object_key_offsets_0()
+{
+ return new byte [] {
+ 0, 0, 1, 8, 14, 16, 17, 19, 20, 36, 43, 49,
+ 51, 52, 54, 55, 57, 58, 60, 61, 63, 64, 66, 67,
+ 69, 70, 72, 73
+ };
+}
+
+private static final byte _JSON_object_key_offsets[] = init__JSON_object_key_offsets_0();
+
+
+private static char[] init__JSON_object_trans_keys_0()
+{
+ return new char [] {
+ 123, 13, 32, 34, 47, 125, 9, 10, 13, 32, 47, 58,
+ 9, 10, 42, 47, 42, 42, 47, 10, 13, 32, 34, 45,
+ 47, 73, 78, 91, 102, 110, 116, 123, 9, 10, 48, 57,
+ 13, 32, 44, 47, 125, 9, 10, 13, 32, 34, 47, 9,
+ 10, 42, 47, 42, 42, 47, 10, 42, 47, 42, 42, 47,
+ 10, 42, 47, 42, 42, 47, 10, 42, 47, 42, 42, 47,
+ 10, 0
+ };
+}
+
+private static final char _JSON_object_trans_keys[] = init__JSON_object_trans_keys_0();
+
+
+private static byte[] init__JSON_object_single_lengths_0()
+{
+ return new byte [] {
+ 0, 1, 5, 4, 2, 1, 2, 1, 12, 5, 4, 2,
+ 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2,
+ 1, 2, 1, 0
+ };
+}
+
+private static final byte _JSON_object_single_lengths[] = init__JSON_object_single_lengths_0();
+
+
+private static byte[] init__JSON_object_range_lengths_0()
+{
+ return new byte [] {
+ 0, 0, 1, 1, 0, 0, 0, 0, 2, 1, 1, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0
+ };
+}
+
+private static final byte _JSON_object_range_lengths[] = init__JSON_object_range_lengths_0();
+
+
+private static byte[] init__JSON_object_index_offsets_0()
+{
+ return new byte [] {
+ 0, 0, 2, 9, 15, 18, 20, 23, 25, 40, 47, 53,
+ 56, 58, 61, 63, 66, 68, 71, 73, 76, 78, 81, 83,
+ 86, 88, 91, 93
+ };
+}
+
+private static final byte _JSON_object_index_offsets[] = init__JSON_object_index_offsets_0();
+
+
+private static byte[] init__JSON_object_indicies_0()
+{
+ return new byte [] {
+ 0, 1, 0, 0, 2, 3, 4, 0, 1, 5, 5, 6,
+ 7, 5, 1, 8, 9, 1, 10, 8, 10, 5, 8, 5,
+ 9, 7, 7, 11, 11, 12, 11, 11, 11, 11, 11, 11,
+ 11, 7, 11, 1, 13, 13, 14, 15, 4, 13, 1, 14,
+ 14, 2, 16, 14, 1, 17, 18, 1, 19, 17, 19, 14,
+ 17, 14, 18, 20, 21, 1, 22, 20, 22, 13, 20, 13,
+ 21, 23, 24, 1, 25, 23, 25, 7, 23, 7, 24, 26,
+ 27, 1, 28, 26, 28, 0, 26, 0, 27, 1, 0
+ };
+}
+
+private static final byte _JSON_object_indicies[] = init__JSON_object_indicies_0();
+
+
+private static byte[] init__JSON_object_trans_targs_0()
+{
+ return new byte [] {
+ 2, 0, 3, 23, 27, 3, 4, 8, 5, 7, 6, 9,
+ 19, 9, 10, 15, 11, 12, 14, 13, 16, 18, 17, 20,
+ 22, 21, 24, 26, 25
+ };
+}
+
+private static final byte _JSON_object_trans_targs[] = init__JSON_object_trans_targs_0();
+
+
+private static byte[] init__JSON_object_trans_actions_0()
+{
+ return new byte [] {
+ 0, 0, 3, 0, 5, 0, 0, 0, 0, 0, 0, 1,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0
+ };
+}
+
+private static final byte _JSON_object_trans_actions[] = init__JSON_object_trans_actions_0();
+
+
+static final int JSON_object_start = 1;
+static final int JSON_object_first_final = 27;
+static final int JSON_object_error = 0;
+
+static final int JSON_object_en_main = 1;
+
+
+// line 668 "src/json/ext/Parser.rl"
+
+
+ ParserResult parseObject(int p, int pe) {
+ int cs = EVIL;
+ IRubyObject lastName = null;
+
+ if (parser.maxNesting > 0 && currentNesting > parser.maxNesting) {
+ throw newException(Utils.M_NESTING_ERROR,
+ "nesting of " + currentNesting + " is too deep");
+ }
+
+ // this is guaranteed to be a RubyHash due to the earlier
+ // allocator test at OptionsReader#getClass
+ RubyHash result =
+ (RubyHash)parser.objectClass.newInstance(context,
+ IRubyObject.NULL_ARRAY, Block.NULL_BLOCK);
+
+
+// line 1803 "src/json/ext/Parser.java"
+ {
+ cs = JSON_object_start;
+ }
+
+// line 686 "src/json/ext/Parser.rl"
+
+// line 1810 "src/json/ext/Parser.java"
+ {
+ int _klen;
+ int _trans = 0;
+ int _acts;
+ int _nacts;
+ int _keys;
+ int _goto_targ = 0;
+
+ _goto: while (true) {
+ switch ( _goto_targ ) {
+ case 0:
+ if ( p == pe ) {
+ _goto_targ = 4;
+ continue _goto;
+ }
+ if ( cs == 0 ) {
+ _goto_targ = 5;
+ continue _goto;
+ }
+case 1:
+ _match: do {
+ _keys = _JSON_object_key_offsets[cs];
+ _trans = _JSON_object_index_offsets[cs];
+ _klen = _JSON_object_single_lengths[cs];
+ if ( _klen > 0 ) {
+ int _lower = _keys;
+ int _mid;
+ int _upper = _keys + _klen - 1;
+ while (true) {
+ if ( _upper < _lower )
+ break;
+
+ _mid = _lower + ((_upper-_lower) >> 1);
+ if ( data[p] < _JSON_object_trans_keys[_mid] )
+ _upper = _mid - 1;
+ else if ( data[p] > _JSON_object_trans_keys[_mid] )
+ _lower = _mid + 1;
+ else {
+ _trans += (_mid - _keys);
+ break _match;
+ }
+ }
+ _keys += _klen;
+ _trans += _klen;
+ }
+
+ _klen = _JSON_object_range_lengths[cs];
+ if ( _klen > 0 ) {
+ int _lower = _keys;
+ int _mid;
+ int _upper = _keys + (_klen<<1) - 2;
+ while (true) {
+ if ( _upper < _lower )
+ break;
+
+ _mid = _lower + (((_upper-_lower) >> 1) & ~1);
+ if ( data[p] < _JSON_object_trans_keys[_mid] )
+ _upper = _mid - 2;
+ else if ( data[p] > _JSON_object_trans_keys[_mid+1] )
+ _lower = _mid + 2;
+ else {
+ _trans += ((_mid - _keys)>>1);
+ break _match;
+ }
+ }
+ _trans += _klen;
+ }
+ } while (false);
+
+ _trans = _JSON_object_indicies[_trans];
+ cs = _JSON_object_trans_targs[_trans];
+
+ if ( _JSON_object_trans_actions[_trans] != 0 ) {
+ _acts = _JSON_object_trans_actions[_trans];
+ _nacts = (int) _JSON_object_actions[_acts++];
+ while ( _nacts-- > 0 )
+ {
+ switch ( _JSON_object_actions[_acts++] )
+ {
+ case 0:
+// line 626 "src/json/ext/Parser.rl"
+ {
+ ParserResult res = parseValue(p, pe);
+ if (res == null) {
+ p--;
+ { p += 1; _goto_targ = 5; if (true) continue _goto;}
+ } else {
+ result.op_aset(context, lastName, res.result);
+ {p = (( res.p))-1;}
+ }
+ }
+ break;
+ case 1:
+// line 637 "src/json/ext/Parser.rl"
+ {
+ ParserResult res = parseString(p, pe);
+ if (res == null) {
+ p--;
+ { p += 1; _goto_targ = 5; if (true) continue _goto;}
+ } else {
+ RubyString name = (RubyString)res.result;
+ if (parser.symbolizeNames) {
+ lastName = context.getRuntime().is1_9()
+ ? name.intern19()
+ : name.intern();
+ } else {
+ lastName = name;
+ }
+ {p = (( res.p))-1;}
+ }
+ }
+ break;
+ case 2:
+// line 655 "src/json/ext/Parser.rl"
+ {
+ p--;
+ { p += 1; _goto_targ = 5; if (true) continue _goto;}
+ }
+ break;
+// line 1930 "src/json/ext/Parser.java"
+ }
+ }
+ }
+
+case 2:
+ if ( cs == 0 ) {
+ _goto_targ = 5;
+ continue _goto;
+ }
+ if ( ++p != pe ) {
+ _goto_targ = 1;
+ continue _goto;
+ }
+case 4:
+case 5:
+ }
+ break; }
+ }
+
+// line 687 "src/json/ext/Parser.rl"
+
+ if (cs < JSON_object_first_final) {
+ return null;
+ }
+
+ IRubyObject returnedResult = result;
+
+ // attempt to de-serialize object
+ if (parser.createId != null) {
+ IRubyObject vKlassName = result.op_aref(context, parser.createId);
+ if (!vKlassName.isNil()) {
+ // might throw ArgumentError, we let it propagate
+ IRubyObject klass = parser.info.jsonModule.
+ callMethod(context, "deep_const_get", vKlassName);
+ if (klass.respondsTo("json_creatable?") &&
+ klass.callMethod(context, "json_creatable?").isTrue()) {
+
+ returnedResult = klass.callMethod(context, "json_create", result);
+ }
+ }
+ }
+ return new ParserResult(returnedResult, p + 1);
+ }
+
+
+// line 1976 "src/json/ext/Parser.java"
+private static byte[] init__JSON_actions_0()
+{
+ return new byte [] {
+ 0, 1, 0, 1, 1
+ };
+}
+
+private static final byte _JSON_actions[] = init__JSON_actions_0();
+
+
+private static byte[] init__JSON_key_offsets_0()
+{
+ return new byte [] {
+ 0, 0, 7, 9, 10, 12, 13, 15, 16, 18, 19
+ };
+}
+
+private static final byte _JSON_key_offsets[] = init__JSON_key_offsets_0();
+
+
+private static char[] init__JSON_trans_keys_0()
+{
+ return new char [] {
+ 13, 32, 47, 91, 123, 9, 10, 42, 47, 42, 42, 47,
+ 10, 42, 47, 42, 42, 47, 10, 13, 32, 47, 9, 10,
+ 0
+ };
+}
+
+private static final char _JSON_trans_keys[] = init__JSON_trans_keys_0();
+
+
+private static byte[] init__JSON_single_lengths_0()
+{
+ return new byte [] {
+ 0, 5, 2, 1, 2, 1, 2, 1, 2, 1, 3
+ };
+}
+
+private static final byte _JSON_single_lengths[] = init__JSON_single_lengths_0();
+
+
+private static byte[] init__JSON_range_lengths_0()
+{
+ return new byte [] {
+ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1
+ };
+}
+
+private static final byte _JSON_range_lengths[] = init__JSON_range_lengths_0();
+
+
+private static byte[] init__JSON_index_offsets_0()
+{
+ return new byte [] {
+ 0, 0, 7, 10, 12, 15, 17, 20, 22, 25, 27
+ };
+}
+
+private static final byte _JSON_index_offsets[] = init__JSON_index_offsets_0();
+
+
+private static byte[] init__JSON_indicies_0()
+{
+ return new byte [] {
+ 0, 0, 2, 3, 4, 0, 1, 5, 6, 1, 7, 5,
+ 7, 0, 5, 0, 6, 8, 9, 1, 10, 8, 10, 11,
+ 8, 11, 9, 11, 11, 12, 11, 1, 0
+ };
+}
+
+private static final byte _JSON_indicies[] = init__JSON_indicies_0();
+
+
+private static byte[] init__JSON_trans_targs_0()
+{
+ return new byte [] {
+ 1, 0, 2, 10, 10, 3, 5, 4, 7, 9, 8, 10,
+ 6
+ };
+}
+
+private static final byte _JSON_trans_targs[] = init__JSON_trans_targs_0();
+
+
+private static byte[] init__JSON_trans_actions_0()
+{
+ return new byte [] {
+ 0, 0, 0, 3, 1, 0, 0, 0, 0, 0, 0, 0,
+ 0
+ };
+}
+
+private static final byte _JSON_trans_actions[] = init__JSON_trans_actions_0();
+
+
+static final int JSON_start = 1;
+static final int JSON_first_final = 10;
+static final int JSON_error = 0;
+
+static final int JSON_en_main = 1;
+
+
+// line 745 "src/json/ext/Parser.rl"
+
+
+ public IRubyObject parse() {
+ int cs = EVIL;
+ int p, pe;
+ IRubyObject result = null;
+
+
+// line 2089 "src/json/ext/Parser.java"
+ {
+ cs = JSON_start;
+ }
+
+// line 753 "src/json/ext/Parser.rl"
+ p = byteList.begin();
+ pe = p + byteList.length();
+
+// line 2098 "src/json/ext/Parser.java"
+ {
+ int _klen;
+ int _trans = 0;
+ int _acts;
+ int _nacts;
+ int _keys;
+ int _goto_targ = 0;
+
+ _goto: while (true) {
+ switch ( _goto_targ ) {
+ case 0:
+ if ( p == pe ) {
+ _goto_targ = 4;
+ continue _goto;
+ }
+ if ( cs == 0 ) {
+ _goto_targ = 5;
+ continue _goto;
+ }
+case 1:
+ _match: do {
+ _keys = _JSON_key_offsets[cs];
+ _trans = _JSON_index_offsets[cs];
+ _klen = _JSON_single_lengths[cs];
+ if ( _klen > 0 ) {
+ int _lower = _keys;
+ int _mid;
+ int _upper = _keys + _klen - 1;
+ while (true) {
+ if ( _upper < _lower )
+ break;
+
+ _mid = _lower + ((_upper-_lower) >> 1);
+ if ( data[p] < _JSON_trans_keys[_mid] )
+ _upper = _mid - 1;
+ else if ( data[p] > _JSON_trans_keys[_mid] )
+ _lower = _mid + 1;
+ else {
+ _trans += (_mid - _keys);
+ break _match;
+ }
+ }
+ _keys += _klen;
+ _trans += _klen;
+ }
+
+ _klen = _JSON_range_lengths[cs];
+ if ( _klen > 0 ) {
+ int _lower = _keys;
+ int _mid;
+ int _upper = _keys + (_klen<<1) - 2;
+ while (true) {
+ if ( _upper < _lower )
+ break;
+
+ _mid = _lower + (((_upper-_lower) >> 1) & ~1);
+ if ( data[p] < _JSON_trans_keys[_mid] )
+ _upper = _mid - 2;
+ else if ( data[p] > _JSON_trans_keys[_mid+1] )
+ _lower = _mid + 2;
+ else {
+ _trans += ((_mid - _keys)>>1);
+ break _match;
+ }
+ }
+ _trans += _klen;
+ }
+ } while (false);
+
+ _trans = _JSON_indicies[_trans];
+ cs = _JSON_trans_targs[_trans];
+
+ if ( _JSON_trans_actions[_trans] != 0 ) {
+ _acts = _JSON_trans_actions[_trans];
+ _nacts = (int) _JSON_actions[_acts++];
+ while ( _nacts-- > 0 )
+ {
+ switch ( _JSON_actions[_acts++] )
+ {
+ case 0:
+// line 717 "src/json/ext/Parser.rl"
+ {
+ currentNesting = 1;
+ ParserResult res = parseObject(p, pe);
+ if (res == null) {
+ p--;
+ { p += 1; _goto_targ = 5; if (true) continue _goto;}
+ } else {
+ result = res.result;
+ {p = (( res.p))-1;}
+ }
+ }
+ break;
+ case 1:
+// line 729 "src/json/ext/Parser.rl"
+ {
+ currentNesting = 1;
+ ParserResult res = parseArray(p, pe);
+ if (res == null) {
+ p--;
+ { p += 1; _goto_targ = 5; if (true) continue _goto;}
+ } else {
+ result = res.result;
+ {p = (( res.p))-1;}
+ }
+ }
+ break;
+// line 2206 "src/json/ext/Parser.java"
+ }
+ }
+ }
+
+case 2:
+ if ( cs == 0 ) {
+ _goto_targ = 5;
+ continue _goto;
+ }
+ if ( ++p != pe ) {
+ _goto_targ = 1;
+ continue _goto;
+ }
+case 4:
+case 5:
+ }
+ break; }
+ }
+
+// line 756 "src/json/ext/Parser.rl"
+
+ if (cs >= JSON_first_final && p == pe) {
+ return result;
+ } else {
+ throw unexpectedToken(p, pe);
+ }
+ }
+
+ /**
+ * Returns a subsequence of the source ByteList, based on source
+ * array byte offsets (i.e., the ByteList's own begin offset is not
+ * automatically added).
+ * @param start
+ * @param end
+ */
+ private ByteList absSubSequence(int absStart, int absEnd) {
+ int offset = byteList.begin();
+ return (ByteList)byteList.subSequence(absStart - offset,
+ absEnd - offset);
+ }
+
+ /**
+ * Retrieves a constant directly descended from the <code>JSON</code> module.
+ * @param name The constant name
+ */
+ private IRubyObject getConstant(String name) {
+ return parser.info.jsonModule.getConstant(name);
+ }
+
+ private RaiseException newException(String className, String message) {
+ return Utils.newException(context, className, message);
+ }
+
+ private RaiseException newException(String className, RubyString message) {
+ return Utils.newException(context, className, message);
+ }
+
+ private RaiseException newException(String className,
+ String messageBegin, ByteList messageEnd) {
+ return newException(className,
+ getRuntime().newString(messageBegin).cat(messageEnd));
+ }
+ }
+}
diff --git a/src/json/ext/Parser.rl b/src/json/ext/Parser.rl
new file mode 100644
index 0000000..00badc8
--- /dev/null
+++ b/src/json/ext/Parser.rl
@@ -0,0 +1,799 @@
+/*
+ * 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.RubyArray;
+import org.jruby.RubyClass;
+import org.jruby.RubyEncoding;
+import org.jruby.RubyFloat;
+import org.jruby.RubyHash;
+import org.jruby.RubyInteger;
+import org.jruby.RubyModule;
+import org.jruby.RubyNumeric;
+import org.jruby.RubyObject;
+import org.jruby.RubyString;
+import org.jruby.anno.JRubyMethod;
+import org.jruby.exceptions.RaiseException;
+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::Parser</code> class.
+ *
+ * <p>This is the JSON parser implemented as a Java class. To use it as the
+ * standard parser, set
+ * <pre>JSON.parser = JSON::Ext::Parser</pre>
+ * This is performed for you when you <code>include "json/ext"</code>.
+ *
+ * <p>This class does not perform the actual parsing, just acts as an interface
+ * to Ruby code. When the {@link #parse()} method is invoked, a
+ * Parser.ParserSession object is instantiated, which handles the process.
+ *
+ * @author mernen
+ */
+public class Parser extends RubyObject {
+ private final RuntimeInfo info;
+ private RubyString vSource;
+ private RubyString createId;
+ private int maxNesting;
+ private boolean allowNaN;
+ private boolean symbolizeNames;
+ private RubyClass objectClass;
+ private RubyClass arrayClass;
+
+ private static final int DEFAULT_MAX_NESTING = 19;
+
+ private static final String JSON_MINUS_INFINITY = "-Infinity";
+ // constant names in the JSON module containing those values
+ private static final String CONST_NAN = "NaN";
+ private static final String CONST_INFINITY = "Infinity";
+ private static final String CONST_MINUS_INFINITY = "MinusInfinity";
+
+ static final ObjectAllocator ALLOCATOR = new ObjectAllocator() {
+ public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
+ return new Parser(runtime, klazz);
+ }
+ };
+
+ /**
+ * Multiple-value return for internal parser methods.
+ *
+ * <p>All the <code>parse<var>Stuff</var></code> methods return instances of
+ * <code>ParserResult</code> when successful, or <code>null</code> when
+ * there's a problem with the input data.
+ */
+ static final class ParserResult {
+ /**
+ * The result of the successful parsing. Should never be
+ * <code>null</code>.
+ */
+ final IRubyObject result;
+ /**
+ * The point where the parser returned.
+ */
+ final int p;
+
+ ParserResult(IRubyObject result, int p) {
+ this.result = result;
+ this.p = p;
+ }
+ }
+
+ public Parser(Ruby runtime, RubyClass metaClass) {
+ super(runtime, metaClass);
+ info = RuntimeInfo.forRuntime(runtime);
+ }
+
+ /**
+ * <code>Parser.new(source, opts = {})</code>
+ *
+ * <p>Creates a new <code>JSON::Ext::Parser</code> instance for the string
+ * <code>source</code>.
+ * It will be configured by the <code>opts</code> Hash.
+ * <code>opts</code> can have the following keys:
+ *
+ * <dl>
+ * <dt><code>:max_nesting</code>
+ * <dd>The maximum depth of nesting allowed in the parsed data
+ * structures. Disable depth checking with <code>:max_nesting => false|nil|0</code>,
+ * it defaults to 19.
+ *
+ * <dt><code>:allow_nan</code>
+ * <dd>If set to <code>true</code>, allow <code>NaN</code>,
+ * <code>Infinity</code> and <code>-Infinity</code> in defiance of RFC 4627
+ * to be parsed by the Parser. This option defaults to <code>false</code>.
+ *
+ * <dt><code>:symbolize_names</code>
+ * <dd>If set to <code>true</code>, returns symbols for the names (keys) in
+ * a JSON object. Otherwise strings are returned, which is also the default.
+ *
+ * <dt><code>:create_additions</code>
+ * <dd>If set to <code>false</code>, the Parser doesn't create additions
+ * even if a matchin class and <code>create_id</code> was found. This option
+ * defaults to <code>true</code>.
+ *
+ * <dt><code>:object_class</code>
+ * <dd>Defaults to Hash.
+ *
+ * <dt><code>:array_class</code>
+ * <dd>Defaults to Array.
+ * </dl>
+ */
+ @JRubyMethod(name = "new", required = 1, optional = 1, meta = true)
+ public static IRubyObject newInstance(IRubyObject clazz, IRubyObject[] args, Block block) {
+ Parser parser = (Parser)((RubyClass)clazz).allocate();
+
+ parser.callInit(args, block);
+
+ return parser;
+ }
+
+ @JRubyMethod(required = 1, optional = 1, visibility = Visibility.PRIVATE)
+ public IRubyObject initialize(ThreadContext context, IRubyObject[] args) {
+ Ruby runtime = context.getRuntime();
+ RubyString source = convertEncoding(context, args[0].convertToString());
+
+ OptionsReader opts =
+ new OptionsReader(context, args.length > 1 ? args[1] : null);
+
+ this.maxNesting = opts.getInt("max_nesting", DEFAULT_MAX_NESTING);
+ this.allowNaN = opts.getBool("allow_nan", false);
+ this.symbolizeNames = opts.getBool("symbolize_names", false);
+ this.createId =
+ opts.getBool("create_additions", true) ? getCreateId(context)
+ : null;
+ this.objectClass = opts.getClass("object_class", runtime.getHash());
+ this.arrayClass = opts.getClass("array_class", runtime.getArray());
+
+ this.vSource = source;
+ return this;
+ }
+
+ /**
+ * Checks the given string's encoding. If a non-UTF-8 encoding is detected,
+ * a converted copy is returned.
+ * Returns the source string if no conversion is needed.
+ */
+ private RubyString convertEncoding(ThreadContext context, RubyString source) {
+ ByteList bl = source.getByteList();
+ int len = bl.length();
+ if (len < 2) {
+ throw Utils.newException(context, Utils.M_PARSER_ERROR,
+ "A JSON text must at least contain two octets!");
+ }
+
+ if (info.encodingsSupported()) {
+ RubyEncoding encoding = (RubyEncoding)source.encoding(context);
+ if (encoding != info.ascii8bit) {
+ return (RubyString)source.encode(context, info.utf8);
+ }
+
+ String sniffedEncoding = sniffByteList(bl);
+ if (sniffedEncoding == null) return source; // assume UTF-8
+ return reinterpretEncoding(context, source, sniffedEncoding);
+ }
+
+ String sniffedEncoding = sniffByteList(bl);
+ if (sniffedEncoding == null) return source; // assume UTF-8
+ Ruby runtime = context.getRuntime();
+ return (RubyString)info.jsonModule.
+ callMethod(context, "iconv",
+ new IRubyObject[] {
+ runtime.newString("utf-8"),
+ runtime.newString(sniffedEncoding),
+ source});
+ }
+
+ /**
+ * Checks the first four bytes of the given ByteList to infer its encoding,
+ * using the principle demonstrated on section 3 of RFC 4627 (JSON).
+ */
+ private static String sniffByteList(ByteList bl) {
+ if (bl.length() < 4) return null;
+ if (bl.get(0) == 0 && bl.get(2) == 0) {
+ return bl.get(1) == 0 ? "utf-32be" : "utf-16be";
+ }
+ if (bl.get(1) == 0 && bl.get(3) == 0) {
+ return bl.get(2) == 0 ? "utf-32le" : "utf-16le";
+ }
+ return null;
+ }
+
+ /**
+ * Assumes the given (binary) RubyString to be in the given encoding, then
+ * converts it to UTF-8.
+ */
+ private RubyString reinterpretEncoding(ThreadContext context,
+ RubyString str, String sniffedEncoding) {
+ RubyEncoding actualEncoding = info.getEncoding(context, sniffedEncoding);
+ RubyEncoding targetEncoding = info.utf8;
+ RubyString dup = (RubyString)str.dup();
+ dup.force_encoding(context, actualEncoding);
+ return (RubyString)dup.encode_bang(context, targetEncoding);
+ }
+
+ /**
+ * <code>Parser#parse()</code>
+ *
+ * <p>Parses the current JSON text <code>source</code> and returns the
+ * complete data structure as a result.
+ */
+ @JRubyMethod
+ public IRubyObject parse(ThreadContext context) {
+ return new ParserSession(this, context).parse();
+ }
+
+ /**
+ * <code>Parser#source()</code>
+ *
+ * <p>Returns a copy of the current <code>source</code> string, that was
+ * used to construct this Parser.
+ */
+ @JRubyMethod(name = "source")
+ public IRubyObject source_get() {
+ return vSource.dup();
+ }
+
+ /**
+ * Queries <code>JSON.create_id</code>. Returns <code>null</code> if it is
+ * set to <code>nil</code> or <code>false</code>, and a String if not.
+ */
+ private RubyString getCreateId(ThreadContext context) {
+ IRubyObject v = info.jsonModule.callMethod(context, "create_id");
+ return v.isTrue() ? v.convertToString() : null;
+ }
+
+ /**
+ * A string parsing session.
+ *
+ * <p>Once a ParserSession is instantiated, the source string should not
+ * change until the parsing is complete. The ParserSession object assumes
+ * the source {@link RubyString} is still associated to its original
+ * {@link ByteList}, which in turn must still be bound to the same
+ * <code>byte[]</code> value (and on the same offset).
+ */
+ // Ragel uses lots of fall-through
+ @SuppressWarnings("fallthrough")
+ private static class ParserSession {
+ private final Parser parser;
+ private final ThreadContext context;
+ private final ByteList byteList;
+ private final byte[] data;
+ private final StringDecoder decoder;
+ private int currentNesting = 0;
+
+ // initialization value for all state variables.
+ // no idea about the origins of this value, ask Flori ;)
+ private static final int EVIL = 0x666;
+
+ private ParserSession(Parser parser, ThreadContext context) {
+ this.parser = parser;
+ this.context = context;
+ this.byteList = parser.vSource.getByteList();
+ this.data = byteList.unsafeBytes();
+ this.decoder = new StringDecoder(context);
+ }
+
+ private RaiseException unexpectedToken(int absStart, int absEnd) {
+ RubyString msg = getRuntime().newString("unexpected token at '")
+ .cat(data, absStart, absEnd - absStart)
+ .cat((byte)'\'');
+ return newException(Utils.M_PARSER_ERROR, msg);
+ }
+
+ private Ruby getRuntime() {
+ return context.getRuntime();
+ }
+
+ %%{
+ machine JSON_common;
+
+ cr = '\n';
+ cr_neg = [^\n];
+ ws = [ \t\r\n];
+ c_comment = '/*' ( any* - (any* '*/' any* ) ) '*/';
+ cpp_comment = '//' cr_neg* cr;
+ comment = c_comment | cpp_comment;
+ ignore = ws | comment;
+ name_separator = ':';
+ value_separator = ',';
+ Vnull = 'null';
+ Vfalse = 'false';
+ Vtrue = 'true';
+ VNaN = 'NaN';
+ VInfinity = 'Infinity';
+ VMinusInfinity = '-Infinity';
+ begin_value = [nft"\-[{NI] | digit;
+ begin_object = '{';
+ end_object = '}';
+ begin_array = '[';
+ end_array = ']';
+ begin_string = '"';
+ begin_name = begin_string;
+ begin_number = digit | '-';
+ }%%
+
+ %%{
+ machine JSON_value;
+ include JSON_common;
+
+ write data;
+
+ action parse_null {
+ result = getRuntime().getNil();
+ }
+ action parse_false {
+ result = getRuntime().getFalse();
+ }
+ action parse_true {
+ result = getRuntime().getTrue();
+ }
+ action parse_nan {
+ if (parser.allowNaN) {
+ result = getConstant(CONST_NAN);
+ } else {
+ throw unexpectedToken(p - 2, pe);
+ }
+ }
+ action parse_infinity {
+ if (parser.allowNaN) {
+ result = getConstant(CONST_INFINITY);
+ } else {
+ throw unexpectedToken(p - 7, pe);
+ }
+ }
+ action parse_number {
+ if (pe > fpc + 9 &&
+ absSubSequence(fpc, fpc + 9).toString().equals(JSON_MINUS_INFINITY)) {
+
+ if (parser.allowNaN) {
+ result = getConstant(CONST_MINUS_INFINITY);
+ fexec p + 10;
+ fhold;
+ fbreak;
+ } else {
+ throw unexpectedToken(p, pe);
+ }
+ }
+ ParserResult res = parseFloat(fpc, pe);
+ if (res != null) {
+ result = res.result;
+ fexec res.p;
+ }
+ res = parseInteger(fpc, pe);
+ if (res != null) {
+ result = res.result;
+ fexec res.p;
+ }
+ fhold;
+ fbreak;
+ }
+ action parse_string {
+ ParserResult res = parseString(fpc, pe);
+ if (res == null) {
+ fhold;
+ fbreak;
+ } else {
+ result = res.result;
+ fexec res.p;
+ }
+ }
+ action parse_array {
+ currentNesting++;
+ ParserResult res = parseArray(fpc, pe);
+ currentNesting--;
+ if (res == null) {
+ fhold;
+ fbreak;
+ } else {
+ result = res.result;
+ fexec res.p;
+ }
+ }
+ action parse_object {
+ currentNesting++;
+ ParserResult res = parseObject(fpc, pe);
+ currentNesting--;
+ if (res == null) {
+ fhold;
+ fbreak;
+ } else {
+ result = res.result;
+ fexec res.p;
+ }
+ }
+ action exit {
+ fhold;
+ fbreak;
+ }
+
+ main := ( Vnull @parse_null |
+ Vfalse @parse_false |
+ Vtrue @parse_true |
+ VNaN @parse_nan |
+ VInfinity @parse_infinity |
+ begin_number >parse_number |
+ begin_string >parse_string |
+ begin_array >parse_array |
+ begin_object >parse_object
+ ) %*exit;
+ }%%
+
+ ParserResult parseValue(int p, int pe) {
+ int cs = EVIL;
+ IRubyObject result = null;
+
+ %% write init;
+ %% write exec;
+
+ if (cs >= JSON_value_first_final && result != null) {
+ return new ParserResult(result, p);
+ } else {
+ return null;
+ }
+ }
+
+ %%{
+ machine JSON_integer;
+
+ write data;
+
+ action exit {
+ fhold;
+ fbreak;
+ }
+
+ main := '-'? ( '0' | [1-9][0-9]* ) ( ^[0-9] @exit );
+ }%%
+
+ ParserResult parseInteger(int p, int pe) {
+ int cs = EVIL;
+
+ %% write init;
+ int memo = p;
+ %% write exec;
+
+ if (cs < JSON_integer_first_final) {
+ return null;
+ }
+
+ ByteList num = absSubSequence(memo, p);
+ // note: this is actually a shared string, but since it is temporary and
+ // read-only, it doesn't really matter
+ RubyString expr = RubyString.newStringLight(getRuntime(), num);
+ RubyInteger number = RubyNumeric.str2inum(getRuntime(), expr, 10, true);
+ return new ParserResult(number, p + 1);
+ }
+
+ %%{
+ machine JSON_float;
+ include JSON_common;
+
+ write data;
+
+ action exit {
+ fhold;
+ fbreak;
+ }
+
+ main := '-'?
+ ( ( ( '0' | [1-9][0-9]* ) '.' [0-9]+ ( [Ee] [+\-]?[0-9]+ )? )
+ | ( ( '0' | [1-9][0-9]* ) ( [Ee] [+\-]? [0-9]+ ) ) )
+ ( ^[0-9Ee.\-] @exit );
+ }%%
+
+ ParserResult parseFloat(int p, int pe) {
+ int cs = EVIL;
+
+ %% write init;
+ int memo = p;
+ %% write exec;
+
+ if (cs < JSON_float_first_final) {
+ return null;
+ }
+
+ ByteList num = absSubSequence(memo, p);
+ // note: this is actually a shared string, but since it is temporary and
+ // read-only, it doesn't really matter
+ RubyString expr = RubyString.newStringLight(getRuntime(), num);
+ RubyFloat number = RubyNumeric.str2fnum(getRuntime(), expr, true);
+ return new ParserResult(number, p + 1);
+ }
+
+ %%{
+ machine JSON_string;
+ include JSON_common;
+
+ write data;
+
+ action parse_string {
+ int offset = byteList.begin();
+ ByteList decoded = decoder.decode(byteList, memo + 1 - offset,
+ p - offset);
+ result = getRuntime().newString(decoded);
+ if (result == null) {
+ fhold;
+ fbreak;
+ } else {
+ fexec p + 1;
+ }
+ }
+
+ action exit {
+ fhold;
+ fbreak;
+ }
+
+ main := '"'
+ ( ( ^(["\\]|0..0x1f)
+ | '\\'["\\/bfnrt]
+ | '\\u'[0-9a-fA-F]{4}
+ | '\\'^(["\\/bfnrtu]|0..0x1f)
+ )* %parse_string
+ ) '"' @exit;
+ }%%
+
+ ParserResult parseString(int p, int pe) {
+ int cs = EVIL;
+ RubyString result = null;
+
+ %% write init;
+ int memo = p;
+ %% write exec;
+
+ if (cs >= JSON_string_first_final && result != null) {
+ return new ParserResult(result, p + 1);
+ } else {
+ return null;
+ }
+ }
+
+ %%{
+ machine JSON_array;
+ include JSON_common;
+
+ write data;
+
+ action parse_value {
+ ParserResult res = parseValue(fpc, pe);
+ if (res == null) {
+ fhold;
+ fbreak;
+ } else {
+ result.append(res.result);
+ fexec res.p;
+ }
+ }
+
+ action exit {
+ fhold;
+ fbreak;
+ }
+
+ next_element = value_separator ignore* begin_value >parse_value;
+
+ main := begin_array
+ ignore*
+ ( ( begin_value >parse_value
+ ignore* )
+ ( ignore*
+ next_element
+ ignore* )* )?
+ ignore*
+ end_array @exit;
+ }%%
+
+ ParserResult parseArray(int p, int pe) {
+ int cs = EVIL;
+
+ if (parser.maxNesting > 0 && currentNesting > parser.maxNesting) {
+ throw newException(Utils.M_NESTING_ERROR,
+ "nesting of " + currentNesting + " is too deep");
+ }
+
+ // this is guaranteed to be a RubyArray due to the earlier
+ // allocator test at OptionsReader#getClass
+ RubyArray result =
+ (RubyArray)parser.arrayClass.newInstance(context,
+ IRubyObject.NULL_ARRAY, Block.NULL_BLOCK);
+
+ %% write init;
+ %% write exec;
+
+ if (cs >= JSON_array_first_final) {
+ return new ParserResult(result, p + 1);
+ } else {
+ throw unexpectedToken(p, pe);
+ }
+ }
+
+ %%{
+ machine JSON_object;
+ include JSON_common;
+
+ write data;
+
+ action parse_value {
+ ParserResult res = parseValue(fpc, pe);
+ if (res == null) {
+ fhold;
+ fbreak;
+ } else {
+ result.op_aset(context, lastName, res.result);
+ fexec res.p;
+ }
+ }
+
+ action parse_name {
+ ParserResult res = parseString(fpc, pe);
+ if (res == null) {
+ fhold;
+ fbreak;
+ } else {
+ RubyString name = (RubyString)res.result;
+ if (parser.symbolizeNames) {
+ lastName = context.getRuntime().is1_9()
+ ? name.intern19()
+ : name.intern();
+ } else {
+ lastName = name;
+ }
+ fexec res.p;
+ }
+ }
+
+ action exit {
+ fhold;
+ fbreak;
+ }
+
+ a_pair = ignore*
+ begin_name >parse_name
+ ignore* name_separator ignore*
+ begin_value >parse_value;
+
+ main := begin_object
+ (a_pair (ignore* value_separator a_pair)*)?
+ ignore* end_object @exit;
+ }%%
+
+ ParserResult parseObject(int p, int pe) {
+ int cs = EVIL;
+ IRubyObject lastName = null;
+
+ if (parser.maxNesting > 0 && currentNesting > parser.maxNesting) {
+ throw newException(Utils.M_NESTING_ERROR,
+ "nesting of " + currentNesting + " is too deep");
+ }
+
+ // this is guaranteed to be a RubyHash due to the earlier
+ // allocator test at OptionsReader#getClass
+ RubyHash result =
+ (RubyHash)parser.objectClass.newInstance(context,
+ IRubyObject.NULL_ARRAY, Block.NULL_BLOCK);
+
+ %% write init;
+ %% write exec;
+
+ if (cs < JSON_object_first_final) {
+ return null;
+ }
+
+ IRubyObject returnedResult = result;
+
+ // attempt to de-serialize object
+ if (parser.createId != null) {
+ IRubyObject vKlassName = result.op_aref(context, parser.createId);
+ if (!vKlassName.isNil()) {
+ // might throw ArgumentError, we let it propagate
+ IRubyObject klass = parser.info.jsonModule.
+ callMethod(context, "deep_const_get", vKlassName);
+ if (klass.respondsTo("json_creatable?") &&
+ klass.callMethod(context, "json_creatable?").isTrue()) {
+
+ returnedResult = klass.callMethod(context, "json_create", result);
+ }
+ }
+ }
+ return new ParserResult(returnedResult, p + 1);
+ }
+
+ %%{
+ machine JSON;
+ include JSON_common;
+
+ write data;
+
+ action parse_object {
+ currentNesting = 1;
+ ParserResult res = parseObject(fpc, pe);
+ if (res == null) {
+ fhold;
+ fbreak;
+ } else {
+ result = res.result;
+ fexec res.p;
+ }
+ }
+
+ action parse_array {
+ currentNesting = 1;
+ ParserResult res = parseArray(fpc, pe);
+ if (res == null) {
+ fhold;
+ fbreak;
+ } else {
+ result = res.result;
+ fexec res.p;
+ }
+ }
+
+ main := ignore*
+ ( begin_object >parse_object
+ | begin_array >parse_array )
+ ignore*;
+ }%%
+
+ public IRubyObject parse() {
+ int cs = EVIL;
+ int p, pe;
+ IRubyObject result = null;
+
+ %% write init;
+ p = byteList.begin();
+ pe = p + byteList.length();
+ %% write exec;
+
+ if (cs >= JSON_first_final && p == pe) {
+ return result;
+ } else {
+ throw unexpectedToken(p, pe);
+ }
+ }
+
+ /**
+ * Returns a subsequence of the source ByteList, based on source
+ * array byte offsets (i.e., the ByteList's own begin offset is not
+ * automatically added).
+ * @param start
+ * @param end
+ */
+ private ByteList absSubSequence(int absStart, int absEnd) {
+ int offset = byteList.begin();
+ return (ByteList)byteList.subSequence(absStart - offset,
+ absEnd - offset);
+ }
+
+ /**
+ * Retrieves a constant directly descended from the <code>JSON</code> module.
+ * @param name The constant name
+ */
+ private IRubyObject getConstant(String name) {
+ return parser.info.jsonModule.getConstant(name);
+ }
+
+ private RaiseException newException(String className, String message) {
+ return Utils.newException(context, className, message);
+ }
+
+ private RaiseException newException(String className, RubyString message) {
+ return Utils.newException(context, className, message);
+ }
+
+ private RaiseException newException(String className,
+ String messageBegin, ByteList messageEnd) {
+ return newException(className,
+ getRuntime().newString(messageBegin).cat(messageEnd));
+ }
+ }
+}
diff --git a/src/json/ext/ParserService.java b/src/json/ext/ParserService.java
new file mode 100644
index 0000000..e0805a7
--- /dev/null
+++ b/src/json/ext/ParserService.java
@@ -0,0 +1,34 @@
+/*
+ * 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 java.io.IOException;
+
+import org.jruby.Ruby;
+import org.jruby.RubyClass;
+import org.jruby.RubyModule;
+import org.jruby.runtime.load.BasicLibraryService;
+
+/**
+ * The service invoked by JRuby's {@link org.jruby.runtime.load.LoadService LoadService}.
+ * Defines the <code>JSON::Ext::Parser</code> class.
+ * @author mernen
+ */
+public class ParserService implements BasicLibraryService {
+ public boolean basicLoad(Ruby runtime) throws IOException {
+ runtime.getLoadService().require("json/common");
+ RuntimeInfo info = RuntimeInfo.initRuntime(runtime);
+
+ info.jsonModule = runtime.defineModule("JSON");
+ RubyModule jsonExtModule = info.jsonModule.defineModuleUnder("Ext");
+ RubyClass parserClass =
+ jsonExtModule.defineClassUnder("Parser", runtime.getObject(),
+ Parser.ALLOCATOR);
+ parserClass.defineAnnotatedMethods(Parser.class);
+ return true;
+ }
+}
diff --git a/src/json/ext/RuntimeInfo.java b/src/json/ext/RuntimeInfo.java
new file mode 100644
index 0000000..f446afe
--- /dev/null
+++ b/src/json/ext/RuntimeInfo.java
@@ -0,0 +1,119 @@
+/*
+ * 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 java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.WeakHashMap;
+import org.jruby.Ruby;
+import org.jruby.RubyClass;
+import org.jruby.RubyEncoding;
+import org.jruby.RubyModule;
+import org.jruby.runtime.ThreadContext;
+import org.jruby.runtime.builtin.IRubyObject;
+
+
+final class RuntimeInfo {
+ // since the vast majority of cases runs just one runtime,
+ // we optimize for that
+ private static WeakReference<Ruby> runtime1 = new WeakReference<Ruby>(null);
+ private static RuntimeInfo info1;
+ // store remaining runtimes here (does not include runtime1)
+ private static Map<Ruby, RuntimeInfo> runtimes;
+
+ // these fields are filled by the service loaders
+ /** JSON */
+ RubyModule jsonModule;
+ /** JSON::Ext::Generator::GeneratorMethods::String::Extend */
+ RubyModule stringExtendModule;
+ /** JSON::Ext::Generator::State */
+ RubyClass generatorStateClass;
+ /** JSON::SAFE_STATE_PROTOTYPE */
+ GeneratorState safeStatePrototype;
+
+ final RubyEncoding utf8;
+ final RubyEncoding ascii8bit;
+ // other encodings
+ private final Map<String, RubyEncoding> encodings;
+
+ private RuntimeInfo(Ruby runtime) {
+ RubyClass encodingClass = runtime.getEncoding();
+ if (encodingClass == null) { // 1.8 mode
+ utf8 = ascii8bit = null;
+ encodings = null;
+ } else {
+ ThreadContext context = runtime.getCurrentContext();
+
+ utf8 = (RubyEncoding)RubyEncoding.find(context,
+ encodingClass, runtime.newString("utf-8"));
+ ascii8bit = (RubyEncoding)RubyEncoding.find(context,
+ encodingClass, runtime.newString("ascii-8bit"));
+ encodings = new HashMap<String, RubyEncoding>();
+ }
+ }
+
+ static RuntimeInfo initRuntime(Ruby runtime) {
+ synchronized (RuntimeInfo.class) {
+ if (runtime1.get() == runtime) {
+ return info1;
+ } else if (runtime1.get() == null) {
+ runtime1 = new WeakReference<Ruby>(runtime);
+ info1 = new RuntimeInfo(runtime);
+ return info1;
+ } else {
+ if (runtimes == null) {
+ runtimes = new WeakHashMap<Ruby, RuntimeInfo>(1);
+ }
+ RuntimeInfo cache = runtimes.get(runtime);
+ if (cache == null) {
+ cache = new RuntimeInfo(runtime);
+ runtimes.put(runtime, cache);
+ }
+ return cache;
+ }
+ }
+ }
+
+ public static RuntimeInfo forRuntime(Ruby runtime) {
+ synchronized (RuntimeInfo.class) {
+ if (runtime1.get() == runtime) return info1;
+ RuntimeInfo cache = null;
+ if (runtimes != null) cache = runtimes.get(runtime);
+ assert cache != null : "Runtime given has not initialized JSON::Ext";
+ return cache;
+ }
+ }
+
+ public boolean encodingsSupported() {
+ return utf8 != null;
+ }
+
+ public RubyEncoding getEncoding(ThreadContext context, String name) {
+ synchronized (encodings) {
+ RubyEncoding encoding = encodings.get(name);
+ if (encoding == null) {
+ Ruby runtime = context.getRuntime();
+ encoding = (RubyEncoding)RubyEncoding.find(context,
+ runtime.getEncoding(), runtime.newString(name));
+ encodings.put(name, encoding);
+ }
+ return encoding;
+ }
+ }
+
+ public GeneratorState getSafeStatePrototype(ThreadContext context) {
+ if (safeStatePrototype == null) {
+ IRubyObject value = jsonModule.getConstant("SAFE_STATE_PROTOTYPE");
+ if (!(value instanceof GeneratorState)) {
+ throw context.getRuntime().newTypeError(value, generatorStateClass);
+ }
+ safeStatePrototype = (GeneratorState)value;
+ }
+ return safeStatePrototype;
+ }
+}
diff --git a/src/json/ext/StringDecoder.java b/src/json/ext/StringDecoder.java
new file mode 100644
index 0000000..a4ee975
--- /dev/null
+++ b/src/json/ext/StringDecoder.java
@@ -0,0 +1,166 @@
+/*
+ * 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.exceptions.RaiseException;
+import org.jruby.runtime.ThreadContext;
+import org.jruby.util.ByteList;
+
+/**
+ * A decoder that reads a JSON-encoded string from the given sources and
+ * returns its decoded form on a new ByteList. Escaped Unicode characters
+ * are encoded as UTF-8.
+ */
+final class StringDecoder extends ByteListTranscoder {
+ /**
+ * Stores the offset of the high surrogate when reading a surrogate pair,
+ * or -1 when not.
+ */
+ private int surrogatePairStart = -1;
+
+ // Array used for writing multi-byte characters into the buffer at once
+ private final byte[] aux = new byte[4];
+
+ StringDecoder(ThreadContext context) {
+ super(context);
+ }
+
+ ByteList decode(ByteList src, int start, int end) {
+ ByteList out = new ByteList(end - start);
+ init(src, start, end, out);
+ while (hasNext()) {
+ handleChar(readUtf8Char());
+ }
+ quoteStop(pos);
+ return out;
+ }
+
+ private void handleChar(int c) {
+ if (c == '\\') {
+ quoteStop(charStart);
+ handleEscapeSequence();
+ } else {
+ quoteStart();
+ }
+ }
+
+ private void handleEscapeSequence() {
+ ensureMin(1);
+ switch (readUtf8Char()) {
+ case 'b':
+ append('\b');
+ break;
+ case 'f':
+ append('\f');
+ break;
+ case 'n':
+ append('\n');
+ break;
+ case 'r':
+ append('\r');
+ break;
+ case 't':
+ append('\t');
+ break;
+ case 'u':
+ ensureMin(4);
+ int cp = readHex();
+ if (Character.isHighSurrogate((char)cp)) {
+ handleLowSurrogate((char)cp);
+ } else if (Character.isLowSurrogate((char)cp)) {
+ // low surrogate with no high surrogate
+ throw invalidUtf8();
+ } else {
+ writeUtf8Char(cp);
+ }
+ break;
+ default: // '\\', '"', '/'...
+ quoteStart();
+ }
+ }
+
+ private void handleLowSurrogate(char highSurrogate) {
+ surrogatePairStart = charStart;
+ ensureMin(1);
+ int lowSurrogate = readUtf8Char();
+
+ if (lowSurrogate == '\\') {
+ ensureMin(5);
+ if (readUtf8Char() != 'u') throw invalidUtf8();
+ lowSurrogate = readHex();
+ }
+
+ if (Character.isLowSurrogate((char)lowSurrogate)) {
+ writeUtf8Char(Character.toCodePoint(highSurrogate,
+ (char)lowSurrogate));
+ surrogatePairStart = -1;
+ } else {
+ throw invalidUtf8();
+ }
+ }
+
+ private void writeUtf8Char(int codePoint) {
+ if (codePoint < 0x80) {
+ append(codePoint);
+ } else if (codePoint < 0x800) {
+ aux[0] = (byte)(0xc0 | (codePoint >>> 6));
+ aux[1] = tailByte(codePoint & 0x3f);
+ append(aux, 0, 2);
+ } else if (codePoint < 0x10000) {
+ aux[0] = (byte)(0xe0 | (codePoint >>> 12));
+ aux[1] = tailByte(codePoint >>> 6);
+ aux[2] = tailByte(codePoint);
+ append(aux, 0, 3);
+ } else {
+ aux[0] = (byte)(0xf0 | codePoint >>> 18);
+ aux[1] = tailByte(codePoint >>> 12);
+ aux[2] = tailByte(codePoint >>> 6);
+ aux[3] = tailByte(codePoint);
+ append(aux, 0, 4);
+ }
+ }
+
+ private byte tailByte(int value) {
+ return (byte)(0x80 | (value & 0x3f));
+ }
+
+ /**
+ * Reads a 4-digit unsigned hexadecimal number from the source.
+ */
+ private int readHex() {
+ int numberStart = pos;
+ int result = 0;
+ int length = 4;
+ for (int i = 0; i < length; i++) {
+ int digit = readUtf8Char();
+ int digitValue;
+ if (digit >= '0' && digit <= '9') {
+ digitValue = digit - '0';
+ } else if (digit >= 'a' && digit <= 'f') {
+ digitValue = 10 + digit - 'a';
+ } else if (digit >= 'A' && digit <= 'F') {
+ digitValue = 10 + digit - 'A';
+ } else {
+ throw new NumberFormatException("Invalid base 16 number "
+ + src.subSequence(numberStart, numberStart + length));
+ }
+ result = result * 16 + digitValue;
+ }
+ return result;
+ }
+
+ @Override
+ protected RaiseException invalidUtf8() {
+ ByteList message = new ByteList(
+ ByteList.plain("partial character in source, " +
+ "but hit end near "));
+ int start = surrogatePairStart != -1 ? surrogatePairStart : charStart;
+ message.append(src, start, srcEnd - start);
+ return Utils.newException(context, Utils.M_PARSER_ERROR,
+ context.getRuntime().newString(message));
+ }
+}
diff --git a/src/json/ext/StringEncoder.java b/src/json/ext/StringEncoder.java
new file mode 100644
index 0000000..57bd19b
--- /dev/null
+++ b/src/json/ext/StringEncoder.java
@@ -0,0 +1,106 @@
+package json.ext;
+
+import org.jruby.exceptions.RaiseException;
+import org.jruby.runtime.ThreadContext;
+import org.jruby.util.ByteList;
+
+/**
+ * An encoder that reads from the given source and outputs its representation
+ * to another ByteList. The source string is fully checked for UTF-8 validity,
+ * and throws a GeneratorError if any problem is found.
+ */
+final class StringEncoder extends ByteListTranscoder {
+ private final boolean asciiOnly;
+
+ // Escaped characters will reuse this array, to avoid new allocations
+ // or appending them byte-by-byte
+ private final byte[] aux =
+ new byte[] {/* First unicode character */
+ '\\', 'u', 0, 0, 0, 0,
+ /* Second unicode character (for surrogate pairs) */
+ '\\', 'u', 0, 0, 0, 0,
+ /* "\X" characters */
+ '\\', 0};
+ // offsets on the array above
+ private static final int ESCAPE_UNI1_OFFSET = 0;
+ private static final int ESCAPE_UNI2_OFFSET = ESCAPE_UNI1_OFFSET + 6;
+ private static final int ESCAPE_CHAR_OFFSET = ESCAPE_UNI2_OFFSET + 6;
+ /** Array used for code point decomposition in surrogates */
+ private final char[] utf16 = new char[2];
+
+ private static final byte[] HEX =
+ new byte[] {'0', '1', '2', '3', '4', '5', '6', '7',
+ '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+
+ StringEncoder(ThreadContext context, boolean asciiOnly) {
+ super(context);
+ this.asciiOnly = asciiOnly;
+ }
+
+ void encode(ByteList src, ByteList out) {
+ init(src, out);
+ append('"');
+ while (hasNext()) {
+ handleChar(readUtf8Char());
+ }
+ quoteStop(pos);
+ append('"');
+ }
+
+ private void handleChar(int c) {
+ switch (c) {
+ case '"':
+ case '\\':
+ escapeChar((char)c);
+ break;
+ case '\n':
+ escapeChar('n');
+ break;
+ case '\r':
+ escapeChar('r');
+ break;
+ case '\t':
+ escapeChar('t');
+ break;
+ case '\f':
+ escapeChar('f');
+ break;
+ case '\b':
+ escapeChar('b');
+ break;
+ default:
+ if (c >= 0x20 && c <= 0x7f ||
+ (c >= 0x80 && !asciiOnly)) {
+ quoteStart();
+ } else {
+ quoteStop(charStart);
+ escapeUtf8Char(c);
+ }
+ }
+ }
+
+ private void escapeChar(char c) {
+ quoteStop(charStart);
+ aux[ESCAPE_CHAR_OFFSET + 1] = (byte)c;
+ append(aux, ESCAPE_CHAR_OFFSET, 2);
+ }
+
+ private void escapeUtf8Char(int codePoint) {
+ int numChars = Character.toChars(codePoint, utf16, 0);
+ escapeCodeUnit(utf16[0], ESCAPE_UNI1_OFFSET + 2);
+ if (numChars > 1) escapeCodeUnit(utf16[1], ESCAPE_UNI2_OFFSET + 2);
+ append(aux, ESCAPE_UNI1_OFFSET, 6 * numChars);
+ }
+
+ private void escapeCodeUnit(char c, int auxOffset) {
+ for (int i = 0; i < 4; i++) {
+ aux[auxOffset + i] = HEX[(c >>> (12 - 4 * i)) & 0xf];
+ }
+ }
+
+ @Override
+ protected RaiseException invalidUtf8() {
+ return Utils.newException(context, Utils.M_GENERATOR_ERROR,
+ "source sequence is illegal/malformed utf-8");
+ }
+}
diff --git a/src/json/ext/Utils.java b/src/json/ext/Utils.java
new file mode 100644
index 0000000..7a1dfee
--- /dev/null
+++ b/src/json/ext/Utils.java
@@ -0,0 +1,89 @@
+/*
+ * 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.RubyArray;
+import org.jruby.RubyClass;
+import org.jruby.RubyException;
+import org.jruby.RubyHash;
+import org.jruby.RubyString;
+import org.jruby.exceptions.RaiseException;
+import org.jruby.runtime.Block;
+import org.jruby.runtime.ThreadContext;
+import org.jruby.runtime.builtin.IRubyObject;
+import org.jruby.util.ByteList;
+
+/**
+ * Library of miscellaneous utility functions
+ */
+final class Utils {
+ public static final String M_GENERATOR_ERROR = "GeneratorError";
+ public static final String M_NESTING_ERROR = "NestingError";
+ public static final String M_PARSER_ERROR = "ParserError";
+
+ private Utils() {
+ throw new RuntimeException();
+ }
+
+ /**
+ * Safe {@link RubyArray} type-checking.
+ * Returns the given object if it is an <code>Array</code>,
+ * or throws an exception if not.
+ * @param object The object to test
+ * @return The given object if it is an <code>Array</code>
+ * @throws RaiseException <code>TypeError</code> if the object is not
+ * of the expected type
+ */
+ static RubyArray ensureArray(IRubyObject object) throws RaiseException {
+ if (object instanceof RubyArray) return (RubyArray)object;
+ Ruby runtime = object.getRuntime();
+ throw runtime.newTypeError(object, runtime.getArray());
+ }
+
+ static RubyHash ensureHash(IRubyObject object) throws RaiseException {
+ if (object instanceof RubyHash) return (RubyHash)object;
+ Ruby runtime = object.getRuntime();
+ throw runtime.newTypeError(object, runtime.getHash());
+ }
+
+ static RubyString ensureString(IRubyObject object) throws RaiseException {
+ if (object instanceof RubyString) return (RubyString)object;
+ Ruby runtime = object.getRuntime();
+ throw runtime.newTypeError(object, runtime.getString());
+ }
+
+ static RaiseException newException(ThreadContext context,
+ String className, String message) {
+ return newException(context, className,
+ context.getRuntime().newString(message));
+ }
+
+ static RaiseException newException(ThreadContext context,
+ String className, RubyString message) {
+ RuntimeInfo info = RuntimeInfo.forRuntime(context.getRuntime());
+ RubyClass klazz = info.jsonModule.getClass(className);
+ RubyException excptn =
+ (RubyException)klazz.newInstance(context,
+ new IRubyObject[] {message}, Block.NULL_BLOCK);
+ return new RaiseException(excptn);
+ }
+
+ static byte[] repeat(ByteList a, int n) {
+ return repeat(a.unsafeBytes(), a.begin(), a.length(), n);
+ }
+
+ static byte[] repeat(byte[] a, int begin, int length, int n) {
+ if (length == 0) return ByteList.NULL_ARRAY;
+ int resultLen = length * n;
+ byte[] result = new byte[resultLen];
+ for (int pos = 0; pos < resultLen; pos += length) {
+ System.arraycopy(a, begin, result, pos, length);
+ }
+ return result;
+ }
+}