diff --git a/Rakefile b/Rakefile index c5b518a1c..09b69a2e5 100644 --- a/Rakefile +++ b/Rakefile @@ -161,7 +161,7 @@ if defined?(RUBY_ENGINE) and RUBY_ENGINE == 'jruby' file JRUBY_PARSER_JAR => :compile do cd 'java/src' do parser_classes = FileList[ - "json/ext/ByteListTranscoder*.class", + "json/ext/ByteList*.class", "json/ext/OptionsReader*.class", "json/ext/Parser*.class", "json/ext/RuntimeInfo*.class", @@ -179,7 +179,7 @@ if defined?(RUBY_ENGINE) and RUBY_ENGINE == 'jruby' file JRUBY_GENERATOR_JAR => :compile do cd 'java/src' do generator_classes = FileList[ - "json/ext/ByteListTranscoder*.class", + "json/ext/ByteList*.class", "json/ext/OptionsReader*.class", "json/ext/Generator*.class", "json/ext/RuntimeInfo*.class", diff --git a/ext/json/ext/fbuffer/fbuffer.h b/ext/json/ext/fbuffer/fbuffer.h index 3e154a5fa..0774c7e46 100644 --- a/ext/json/ext/fbuffer/fbuffer.h +++ b/ext/json/ext/fbuffer/fbuffer.h @@ -46,9 +46,11 @@ typedef struct FBufferStruct { unsigned long len; unsigned long capa; char *ptr; + VALUE io; } FBuffer; #define FBUFFER_STACK_SIZE 512 +#define FBUFFER_IO_BUFFER_SIZE (16384 - 1) #define FBUFFER_INITIAL_LENGTH_DEFAULT 1024 #define FBUFFER_PTR(fb) ((fb)->ptr) @@ -66,7 +68,7 @@ static void fbuffer_append_long(FBuffer *fb, long number); #endif static inline void fbuffer_append_char(FBuffer *fb, char newchr); #ifdef JSON_GENERATOR -static VALUE fbuffer_to_s(FBuffer *fb); +static VALUE fbuffer_finalize(FBuffer *fb); #endif static void fbuffer_stack_init(FBuffer *fb, unsigned long initial_length, char *stack_buffer, long stack_buffer_size) @@ -86,24 +88,19 @@ static void fbuffer_free(FBuffer *fb) } } -#ifndef JSON_GENERATOR static void fbuffer_clear(FBuffer *fb) { fb->len = 0; } -#endif -static void fbuffer_do_inc_capa(FBuffer *fb, unsigned long requested) +static void fbuffer_flush(FBuffer *fb) { - unsigned long required; - - if (RB_UNLIKELY(!fb->ptr)) { - fb->ptr = ALLOC_N(char, fb->initial_length); - fb->capa = fb->initial_length; - } - - for (required = fb->capa; requested > required - fb->len; required <<= 1); + rb_io_write(fb->io, rb_utf8_str_new(fb->ptr, fb->len)); + fbuffer_clear(fb); +} +static void fbuffer_realloc(FBuffer *fb, unsigned long required) +{ if (required > fb->capa) { if (fb->type == FBUFFER_STACK_ALLOCATED) { const char *old_buffer = fb->ptr; @@ -117,6 +114,32 @@ static void fbuffer_do_inc_capa(FBuffer *fb, unsigned long requested) } } +static void fbuffer_do_inc_capa(FBuffer *fb, unsigned long requested) +{ + if (RB_UNLIKELY(fb->io)) { + if (fb->capa < FBUFFER_IO_BUFFER_SIZE) { + fbuffer_realloc(fb, FBUFFER_IO_BUFFER_SIZE); + } else { + fbuffer_flush(fb); + } + + if (RB_LIKELY(requested < fb->capa)) { + return; + } + } + + unsigned long required; + + if (RB_UNLIKELY(!fb->ptr)) { + fb->ptr = ALLOC_N(char, fb->initial_length); + fb->capa = fb->initial_length; + } + + for (required = fb->capa; requested > required - fb->len; required <<= 1); + + fbuffer_realloc(fb, required); +} + static inline void fbuffer_inc_capa(FBuffer *fb, unsigned long requested) { if (RB_UNLIKELY(requested > fb->capa - fb->len)) { @@ -174,11 +197,18 @@ static void fbuffer_append_long(FBuffer *fb, long number) fbuffer_append(fb, buffer_end - len, len); } -static VALUE fbuffer_to_s(FBuffer *fb) +static VALUE fbuffer_finalize(FBuffer *fb) { - VALUE result = rb_utf8_str_new(FBUFFER_PTR(fb), FBUFFER_LEN(fb)); - fbuffer_free(fb); - return result; + if (fb->io) { + fbuffer_flush(fb); + fbuffer_free(fb); + rb_io_flush(fb->io); + return fb->io; + } else { + VALUE result = rb_utf8_str_new(FBUFFER_PTR(fb), FBUFFER_LEN(fb)); + fbuffer_free(fb); + return result; + } } #endif #endif diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index c4f356ac6..503baca65 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -54,7 +54,7 @@ struct generate_json_data { }; static VALUE cState_from_state_s(VALUE self, VALUE opts); -static VALUE cState_partial_generate(VALUE self, VALUE obj, generator_func); +static VALUE cState_partial_generate(VALUE self, VALUE obj, generator_func, VALUE io); static void generate_json(FBuffer *buffer, struct generate_json_data *data, JSON_Generator_State *state, VALUE obj); static void generate_json_object(FBuffer *buffer, struct generate_json_data *data, JSON_Generator_State *state, VALUE obj); static void generate_json_array(FBuffer *buffer, struct generate_json_data *data, JSON_Generator_State *state, VALUE obj); @@ -453,7 +453,7 @@ static VALUE mHash_to_json(int argc, VALUE *argv, VALUE self) { rb_check_arity(argc, 0, 1); VALUE Vstate = cState_from_state_s(cState, argc == 1 ? argv[0] : Qnil); - return cState_partial_generate(Vstate, self, generate_json_object); + return cState_partial_generate(Vstate, self, generate_json_object, Qfalse); } /* @@ -467,7 +467,7 @@ static VALUE mHash_to_json(int argc, VALUE *argv, VALUE self) static VALUE mArray_to_json(int argc, VALUE *argv, VALUE self) { rb_check_arity(argc, 0, 1); VALUE Vstate = cState_from_state_s(cState, argc == 1 ? argv[0] : Qnil); - return cState_partial_generate(Vstate, self, generate_json_array); + return cState_partial_generate(Vstate, self, generate_json_array, Qfalse); } #ifdef RUBY_INTEGER_UNIFICATION @@ -480,7 +480,7 @@ static VALUE mInteger_to_json(int argc, VALUE *argv, VALUE self) { rb_check_arity(argc, 0, 1); VALUE Vstate = cState_from_state_s(cState, argc == 1 ? argv[0] : Qnil); - return cState_partial_generate(Vstate, self, generate_json_integer); + return cState_partial_generate(Vstate, self, generate_json_integer, Qfalse); } #else @@ -493,7 +493,7 @@ static VALUE mFixnum_to_json(int argc, VALUE *argv, VALUE self) { rb_check_arity(argc, 0, 1); VALUE Vstate = cState_from_state_s(cState, argc == 1 ? argv[0] : Qnil); - return cState_partial_generate(Vstate, self, generate_json_fixnum); + return cState_partial_generate(Vstate, self, generate_json_fixnum, Qfalse); } /* @@ -505,7 +505,7 @@ static VALUE mBignum_to_json(int argc, VALUE *argv, VALUE self) { rb_check_arity(argc, 0, 1); VALUE Vstate = cState_from_state_s(cState, argc == 1 ? argv[0] : Qnil); - return cState_partial_generate(Vstate, self, generate_json_bignum); + return cState_partial_generate(Vstate, self, generate_json_bignum, Qfalse); } #endif @@ -518,7 +518,7 @@ static VALUE mFloat_to_json(int argc, VALUE *argv, VALUE self) { rb_check_arity(argc, 0, 1); VALUE Vstate = cState_from_state_s(cState, argc == 1 ? argv[0] : Qnil); - return cState_partial_generate(Vstate, self, generate_json_float); + return cState_partial_generate(Vstate, self, generate_json_float, Qfalse); } /* @@ -543,7 +543,7 @@ static VALUE mString_to_json(int argc, VALUE *argv, VALUE self) { rb_check_arity(argc, 0, 1); VALUE Vstate = cState_from_state_s(cState, argc == 1 ? argv[0] : Qnil); - return cState_partial_generate(Vstate, self, generate_json_string); + return cState_partial_generate(Vstate, self, generate_json_string, Qfalse); } /* @@ -638,7 +638,7 @@ static VALUE mObject_to_json(int argc, VALUE *argv, VALUE self) rb_scan_args(argc, argv, "01", &state); Check_Type(string, T_STRING); state = cState_from_state_s(cState, state); - return cState_partial_generate(state, string, generate_json_string); + return cState_partial_generate(state, string, generate_json_string, Qfalse); } static void State_mark(void *ptr) @@ -1045,12 +1045,14 @@ static VALUE generate_json_rescue(VALUE d, VALUE exc) return Qundef; } -static VALUE cState_partial_generate(VALUE self, VALUE obj, generator_func func) +static VALUE cState_partial_generate(VALUE self, VALUE obj, generator_func func, VALUE io) { GET_STATE(self); char stack_buffer[FBUFFER_STACK_SIZE]; - FBuffer buffer = {0}; + FBuffer buffer = { + .io = RTEST(io) ? io : Qfalse, + }; fbuffer_stack_init(&buffer, state->buffer_initial_length, stack_buffer, FBUFFER_STACK_SIZE); struct generate_json_data data = { @@ -1062,19 +1064,12 @@ static VALUE cState_partial_generate(VALUE self, VALUE obj, generator_func func) }; rb_rescue(generate_json_try, (VALUE)&data, generate_json_rescue, (VALUE)&data); - return fbuffer_to_s(&buffer); + return fbuffer_finalize(&buffer); } -/* - * call-seq: generate(obj) - * - * Generates a valid JSON document from object +obj+ and returns the - * result. If no valid JSON document can be created this method raises a - * GeneratorError exception. - */ -static VALUE cState_generate(VALUE self, VALUE obj) +static VALUE cState_generate(VALUE self, VALUE obj, VALUE io) { - VALUE result = cState_partial_generate(self, obj, generate_json); + VALUE result = cState_partial_generate(self, obj, generate_json, io); GET_STATE(self); (void)state; return result; @@ -1502,14 +1497,16 @@ static VALUE cState_configure(VALUE self, VALUE opts) return self; } -static VALUE cState_m_generate(VALUE klass, VALUE obj, VALUE opts) +static VALUE cState_m_generate(VALUE klass, VALUE obj, VALUE opts, VALUE io) { JSON_Generator_State state = {0}; state_init(&state); configure_state(&state, opts); char stack_buffer[FBUFFER_STACK_SIZE]; - FBuffer buffer = {0}; + FBuffer buffer = { + .io = RTEST(io) ? io : Qfalse, + }; fbuffer_stack_init(&buffer, state.buffer_initial_length, stack_buffer, FBUFFER_STACK_SIZE); struct generate_json_data data = { @@ -1521,7 +1518,7 @@ static VALUE cState_m_generate(VALUE klass, VALUE obj, VALUE opts) }; rb_rescue(generate_json_try, (VALUE)&data, generate_json_rescue, (VALUE)&data); - return fbuffer_to_s(&buffer); + return fbuffer_finalize(&buffer); } /* @@ -1583,9 +1580,9 @@ void Init_generator(void) rb_define_method(cState, "depth=", cState_depth_set, 1); rb_define_method(cState, "buffer_initial_length", cState_buffer_initial_length, 0); rb_define_method(cState, "buffer_initial_length=", cState_buffer_initial_length_set, 1); - rb_define_method(cState, "generate", cState_generate, 1); + rb_define_private_method(cState, "_generate", cState_generate, 2); - rb_define_singleton_method(cState, "generate", cState_m_generate, 2); + rb_define_singleton_method(cState, "generate", cState_m_generate, 3); VALUE mGeneratorMethods = rb_define_module_under(mGenerator, "GeneratorMethods"); diff --git a/java/src/json/ext/ByteListDirectOutputStream.java b/java/src/json/ext/ByteListDirectOutputStream.java new file mode 100644 index 000000000..178cf11c2 --- /dev/null +++ b/java/src/json/ext/ByteListDirectOutputStream.java @@ -0,0 +1,16 @@ +package json.ext; + +import org.jcodings.Encoding; +import org.jruby.util.ByteList; + +import java.io.ByteArrayOutputStream; + +public class ByteListDirectOutputStream extends ByteArrayOutputStream { + ByteListDirectOutputStream(int size) { + super(size); + } + + public ByteList toByteListDirect(Encoding encoding) { + return new ByteList(buf, 0, count, encoding, false); + } +} diff --git a/java/src/json/ext/ByteListTranscoder.java b/java/src/json/ext/ByteListTranscoder.java index 6f6ab66c1..0fedcabdd 100644 --- a/java/src/json/ext/ByteListTranscoder.java +++ b/java/src/json/ext/ByteListTranscoder.java @@ -9,6 +9,9 @@ import org.jruby.runtime.ThreadContext; import org.jruby.util.ByteList; +import java.io.IOException; +import java.io.OutputStream; + /** * A class specialized in transcoding a certain String format into another, * using UTF-8 ByteLists as both input and output. @@ -23,7 +26,7 @@ abstract class ByteListTranscoder { /** Position of the next character to read */ protected int pos; - private ByteList out; + private OutputStream 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 @@ -37,11 +40,11 @@ protected ByteListTranscoder(ThreadContext context) { this.context = context; } - protected void init(ByteList src, ByteList out) { + protected void init(ByteList src, OutputStream out) { this.init(src, 0, src.length(), out); } - protected void init(ByteList src, int start, int end, ByteList out) { + protected void init(ByteList src, int start, int end, OutputStream out) { this.src = src; this.pos = start; this.charStart = start; @@ -142,19 +145,19 @@ protected void quoteStart() { * recently read character, or {@link #charStart} to quote * until the character before it. */ - protected void quoteStop(int endPos) { + protected void quoteStop(int endPos) throws IOException { if (quoteStart != -1) { - out.append(src, quoteStart, endPos - quoteStart); + out.write(src.bytes(), quoteStart, endPos - quoteStart); quoteStart = -1; } } - protected void append(int b) { - out.append(b); + protected void append(int b) throws IOException { + out.write(b); } - protected void append(byte[] origin, int start, int length) { - out.append(origin, start, length); + protected void append(byte[] origin, int start, int length) throws IOException { + out.write(origin, start, length); } diff --git a/java/src/json/ext/Generator.java b/java/src/json/ext/Generator.java index f76dcb383..65c30ffa7 100644 --- a/java/src/json/ext/Generator.java +++ b/java/src/json/ext/Generator.java @@ -5,6 +5,8 @@ */ package json.ext; +import org.jcodings.Encoding; +import org.jcodings.specific.UTF8Encoding; import org.jruby.Ruby; import org.jruby.RubyArray; import org.jruby.RubyBasicObject; @@ -13,11 +15,18 @@ import org.jruby.RubyFixnum; import org.jruby.RubyFloat; import org.jruby.RubyHash; +import org.jruby.RubyIO; import org.jruby.RubyString; +import org.jruby.runtime.Helpers; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.util.ByteList; import org.jruby.exceptions.RaiseException; +import org.jruby.util.IOOutputStream; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; public final class Generator { private Generator() { @@ -48,12 +57,18 @@ private Generator() { * Encodes the given object as a JSON string, using the appropriate * handler if one is found or calling #to_json if not. */ - public static RubyString + public static IRubyObject generateJson(ThreadContext context, T object, - GeneratorState config) { + GeneratorState config, IRubyObject io) { Session session = new Session(context, config); Handler handler = getHandlerFor(context.runtime, object); - return handler.generateNew(session, object); + + if (io.isNil()) { + return handler.generateNew(session, object); + } + + handler.generateToBuffer(session, object, new IOOutputStream(io)); + return io; } /** @@ -171,17 +186,20 @@ int guessSize(Session session, T object) { } RubyString generateNew(Session session, T object) { - RubyString result; - ByteList buffer = new ByteList(guessSize(session, object)); - generate(session, object, buffer); - result = RubyString.newString(session.getRuntime(), buffer); - ThreadContext context = session.getContext(); - RuntimeInfo info = session.getInfo(); - result.force_encoding(context, info.utf8.get()); - return result; + ByteListDirectOutputStream buffer = new ByteListDirectOutputStream(guessSize(session, object)); + generateToBuffer(session, object, buffer); + return RubyString.newString(session.getRuntime(), buffer.toByteListDirect(UTF8Encoding.INSTANCE)); } - abstract void generate(Session session, T object, ByteList buffer); + void generateToBuffer(Session session, T object, OutputStream buffer) { + try { + generate(session, object, buffer); + } catch (IOException ioe) { + throw session.getRuntime().newIOErrorFromException(ioe); + } + } + + abstract void generate(Session session, T object, OutputStream buffer) throws IOException; } /** @@ -189,10 +207,10 @@ RubyString generateNew(Session session, T object) { */ private static class KeywordHandler extends Handler { - private final ByteList keyword; + private String keyword; private KeywordHandler(String keyword) { - this.keyword = new ByteList(ByteList.plain(keyword), false); + this.keyword = keyword; } @Override @@ -202,12 +220,12 @@ int guessSize(Session session, T object) { @Override RubyString generateNew(Session session, T object) { - return RubyString.newStringShared(session.getRuntime(), keyword); + return RubyString.newString(session.getRuntime(), keyword); } @Override - void generate(Session session, T object, ByteList buffer) { - buffer.append(keyword); + void generate(Session session, T object, OutputStream buffer) throws IOException { + buffer.write(keyword.getBytes(StandardCharsets.UTF_8)); } } @@ -217,39 +235,43 @@ void generate(Session session, T object, ByteList buffer) { static final Handler BIGNUM_HANDLER = new Handler() { @Override - void generate(Session session, RubyBignum object, ByteList buffer) { + void generate(Session session, RubyBignum object, OutputStream buffer) throws IOException { // 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()); + ByteList bytes = ((RubyString) object.to_s(IRubyObject.NULL_ARRAY)).getByteList(); + buffer.write(bytes.unsafeBytes(), bytes.begin(), bytes.length()); } }; static final Handler FIXNUM_HANDLER = new Handler() { @Override - void generate(Session session, RubyFixnum object, ByteList buffer) { - buffer.append(object.to_s().getByteList()); + void generate(Session session, RubyFixnum object, OutputStream buffer) throws IOException { + ByteList bytes = object.to_s().getByteList(); + buffer.write(bytes.unsafeBytes(), bytes.begin(), bytes.length()); } }; static final Handler FLOAT_HANDLER = new Handler() { @Override - void generate(Session session, RubyFloat object, ByteList buffer) { - double value = RubyFloat.num2dbl(object); - - if (Double.isInfinite(value) || Double.isNaN(value)) { + void generate(Session session, RubyFloat object, OutputStream buffer) throws IOException { + if (object.isInfinite() || object.isNaN()) { 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()); + + double value = RubyFloat.num2dbl(object); + + buffer.write(Double.toString(value).getBytes(StandardCharsets.UTF_8)); } }; + private static final byte[] EMPTY_ARRAY_BYTES = "[]".getBytes(); static final Handler ARRAY_HANDLER = new Handler() { @Override @@ -264,14 +286,14 @@ int guessSize(Session session, RubyArray object) { } @Override - void generate(Session session, RubyArray object, ByteList buffer) { + void generate(Session session, RubyArray object, OutputStream buffer) throws IOException { ThreadContext context = session.getContext(); Ruby runtime = context.getRuntime(); GeneratorState state = session.getState(); int depth = state.increaseDepth(); if (object.isEmpty()) { - buffer.append("[]".getBytes()); + buffer.write(EMPTY_ARRAY_BYTES); state.decreaseDepth(); return; } @@ -287,8 +309,8 @@ void generate(Session session, RubyArray object, ByteList buffer) { session.infectBy(object); - buffer.append((byte)'['); - buffer.append(arrayNl); + buffer.write((byte)'['); + buffer.write(arrayNl.bytes()); boolean firstItem = true; for (int i = 0, t = object.getLength(); i < t; i++) { IRubyObject element = object.eltInternal(i); @@ -296,23 +318,24 @@ void generate(Session session, RubyArray object, ByteList buffer) { if (firstItem) { firstItem = false; } else { - buffer.append(delim); + buffer.write(delim); } - buffer.append(shift); + buffer.write(shift); Handler handler = (Handler) getHandlerFor(runtime, element); handler.generate(session, element, buffer); } state.decreaseDepth(); if (arrayNl.length() != 0) { - buffer.append(arrayNl); - buffer.append(shift, 0, state.getDepth() * indentUnit.length()); + buffer.write(arrayNl.bytes()); + buffer.write(shift, 0, state.getDepth() * indentUnit.length()); } - buffer.append((byte)']'); + buffer.write((byte)']'); } }; + private static final byte[] EMPTY_HASH_BYTES = "{}".getBytes(); static final Handler HASH_HANDLER = new Handler() { @Override @@ -328,14 +351,14 @@ int guessSize(Session session, RubyHash object) { @Override void generate(final Session session, RubyHash object, - final ByteList buffer) { + final OutputStream buffer) throws IOException { ThreadContext context = session.getContext(); final Ruby runtime = context.getRuntime(); final GeneratorState state = session.getState(); final int depth = state.increaseDepth(); if (object.isEmpty()) { - buffer.append("{}".getBytes()); + buffer.write(EMPTY_HASH_BYTES); state.decreaseDepth(); return; } @@ -345,46 +368,50 @@ void generate(final Session session, RubyHash object, final ByteList spaceBefore = state.getSpaceBefore(); final ByteList space = state.getSpace(); - buffer.append((byte)'{'); - buffer.append(objectNl); + buffer.write((byte)'{'); + buffer.write(objectNl.bytes()); final boolean[] firstPair = new boolean[]{true}; object.visitAll(new RubyHash.Visitor() { @Override public void visit(IRubyObject key, IRubyObject value) { - if (firstPair[0]) { - firstPair[0] = false; - } else { - buffer.append((byte)','); - buffer.append(objectNl); + try { + if (firstPair[0]) { + firstPair[0] = false; + } else { + buffer.write((byte) ','); + buffer.write(objectNl.bytes()); + } + if (objectNl.length() != 0) buffer.write(indent); + + IRubyObject keyStr = key.callMethod(context, "to_s"); + if (keyStr.getMetaClass() == runtime.getString()) { + STRING_HANDLER.generate(session, (RubyString) keyStr, buffer); + } else { + Utils.ensureString(keyStr); + Handler keyHandler = (Handler) getHandlerFor(runtime, keyStr); + keyHandler.generate(session, keyStr, buffer); + } + session.infectBy(key); + + buffer.write(spaceBefore.bytes()); + buffer.write((byte) ':'); + buffer.write(space.bytes()); + + Handler valueHandler = (Handler) getHandlerFor(runtime, value); + valueHandler.generate(session, value, buffer); + session.infectBy(value); + } catch (Throwable t) { + Helpers.throwException(t); } - if (objectNl.length() != 0) buffer.append(indent); - - IRubyObject keyStr = key.callMethod(context, "to_s"); - if (keyStr.getMetaClass() == runtime.getString()) { - STRING_HANDLER.generate(session, (RubyString)keyStr, buffer); - } else { - Utils.ensureString(keyStr); - Handler keyHandler = (Handler) getHandlerFor(runtime, keyStr); - keyHandler.generate(session, keyStr, buffer); - } - session.infectBy(key); - - buffer.append(spaceBefore); - buffer.append((byte)':'); - buffer.append(space); - - Handler valueHandler = (Handler) getHandlerFor(runtime, value); - valueHandler.generate(session, value, buffer); - session.infectBy(value); } }); state.decreaseDepth(); if (!firstPair[0] && objectNl.length() != 0) { - buffer.append(objectNl); + buffer.write(objectNl.bytes()); } - buffer.append(Utils.repeat(state.getIndent(), state.getDepth())); - buffer.append((byte)'}'); + buffer.write(Utils.repeat(state.getIndent(), state.getDepth())); + buffer.write((byte)'}'); } }; @@ -399,7 +426,7 @@ int guessSize(Session session, RubyString object) { } @Override - void generate(Session session, RubyString object, ByteList buffer) { + void generate(Session session, RubyString object, OutputStream buffer) throws IOException { RuntimeInfo info = session.getInfo(); RubyString src; @@ -439,7 +466,7 @@ RubyString generateNew(Session session, IRubyObject object) { } @Override - void generate(Session session, IRubyObject object, ByteList buffer) { + void generate(Session session, IRubyObject object, OutputStream buffer) throws IOException { RubyString str = object.asString(); STRING_HANDLER.generate(session, str, buffer); } @@ -468,9 +495,10 @@ RubyString generateNew(Session session, IRubyObject object) { } @Override - void generate(Session session, IRubyObject object, ByteList buffer) { + void generate(Session session, IRubyObject object, OutputStream buffer) throws IOException { RubyString result = generateNew(session, object); - buffer.append(result.getByteList()); + ByteList bytes = result.getByteList(); + buffer.write(bytes.unsafeBytes(), bytes.begin(), bytes.length()); } }; } diff --git a/java/src/json/ext/GeneratorService.java b/java/src/json/ext/GeneratorService.java index e665ad144..1500c4126 100644 --- a/java/src/json/ext/GeneratorService.java +++ b/java/src/json/ext/GeneratorService.java @@ -37,6 +37,8 @@ public boolean basicLoad(Ruby runtime) throws IOException { generatorModule.defineModuleUnder("GeneratorMethods"); GeneratorMethods.populate(info, generatorMethods); + runtime.getLoadService().require("json/ext/generator/state"); + return true; } } diff --git a/java/src/json/ext/GeneratorState.java b/java/src/json/ext/GeneratorState.java index 1600b04aa..0d8a3617d 100644 --- a/java/src/json/ext/GeneratorState.java +++ b/java/src/json/ext/GeneratorState.java @@ -139,8 +139,8 @@ public static IRubyObject from_state(ThreadContext context, } @JRubyMethod(meta=true) - public static IRubyObject generate(ThreadContext context, IRubyObject klass, IRubyObject obj, IRubyObject opts) { - return fromState(context, opts).generate(context, obj); + public static IRubyObject generate(ThreadContext context, IRubyObject klass, IRubyObject obj, IRubyObject opts, IRubyObject io) { + return fromState(context, opts)._generate(context, obj, io); } static GeneratorState fromState(ThreadContext context, IRubyObject opts) { @@ -196,7 +196,7 @@ static GeneratorState fromState(ThreadContext context, RuntimeInfo info, */ @JRubyMethod(optional=1, visibility=Visibility.PRIVATE) public IRubyObject initialize(ThreadContext context, IRubyObject[] args) { - configure(context, args.length > 0 ? args[0] : null); + _configure(context, args.length > 0 ? args[0] : null); return this; } @@ -228,17 +228,23 @@ public IRubyObject initialize_copy(ThreadContext context, IRubyObject vOrig) { * the result. If no valid JSON document can be created this method raises * a GeneratorError exception. */ - @JRubyMethod - public IRubyObject generate(ThreadContext context, IRubyObject obj) { - RubyString result = Generator.generateJson(context, obj, this); + @JRubyMethod(visibility = Visibility.PRIVATE) + public IRubyObject _generate(ThreadContext context, IRubyObject obj, IRubyObject io) { + IRubyObject result = Generator.generateJson(context, obj, this, io); RuntimeInfo info = RuntimeInfo.forRuntime(context.getRuntime()); - if (result.getEncoding() != UTF8Encoding.INSTANCE) { - if (result.isFrozen()) { - result = result.strDup(context.getRuntime()); + if (!(result instanceof RubyString)) { + return result; + } + + RubyString resultString = result.convertToString(); + if (resultString.getEncoding() != UTF8Encoding.INSTANCE) { + if (resultString.isFrozen()) { + resultString = resultString.strDup(context.getRuntime()); } - result.force_encoding(context, info.utf8.get()); + resultString.force_encoding(context, info.utf8.get()); } - return result; + + return resultString; } private static boolean matchClosingBrace(ByteList bl, int pos, int len, @@ -405,7 +411,7 @@ public boolean strict() { return strict; } - @JRubyMethod(name="strict") + @JRubyMethod(name={"strict","strict?"}) public RubyBoolean strict_get(ThreadContext context) { return context.getRuntime().newBoolean(strict); } @@ -478,8 +484,8 @@ private ByteList prepareByteList(ThreadContext context, IRubyObject value) { * @param vOpts The options hash * @return The receiver */ - @JRubyMethod(alias = "merge") - public IRubyObject configure(ThreadContext context, IRubyObject vOpts) { + @JRubyMethod(visibility=Visibility.PRIVATE) + public IRubyObject _configure(ThreadContext context, IRubyObject vOpts) { OptionsReader opts = new OptionsReader(context, vOpts); ByteList indent = opts.getString("indent"); diff --git a/java/src/json/ext/StringDecoder.java b/java/src/json/ext/StringDecoder.java index 76cf18375..f4877e935 100644 --- a/java/src/json/ext/StringDecoder.java +++ b/java/src/json/ext/StringDecoder.java @@ -9,6 +9,8 @@ import org.jruby.runtime.ThreadContext; import org.jruby.util.ByteList; +import java.io.IOException; + /** * A decoder that reads a JSON-encoded string from the given sources and * returns its decoded form on a new ByteList. Escaped Unicode characters @@ -29,17 +31,20 @@ final class StringDecoder extends ByteListTranscoder { } ByteList decode(ByteList src, int start, int end) { - ByteList out = new ByteList(end - start); - out.setEncoding(src.getEncoding()); - init(src, start, end, out); - while (hasNext()) { - handleChar(readUtf8Char()); + try { + ByteListDirectOutputStream out = new ByteListDirectOutputStream(end - start); + init(src, start, end, out); + while (hasNext()) { + handleChar(readUtf8Char()); + } + quoteStop(pos); + return out.toByteListDirect(src.getEncoding()); + } catch (IOException e) { + throw context.runtime.newIOErrorFromException(e); } - quoteStop(pos); - return out; } - private void handleChar(int c) { + private void handleChar(int c) throws IOException { if (c == '\\') { quoteStop(charStart); handleEscapeSequence(); @@ -48,7 +53,7 @@ private void handleChar(int c) { } } - private void handleEscapeSequence() { + private void handleEscapeSequence() throws IOException { ensureMin(1); switch (readUtf8Char()) { case 'b': @@ -83,7 +88,7 @@ private void handleEscapeSequence() { } } - private void handleLowSurrogate(char highSurrogate) { + private void handleLowSurrogate(char highSurrogate) throws IOException { surrogatePairStart = charStart; ensureMin(1); int lowSurrogate = readUtf8Char(); @@ -103,7 +108,7 @@ private void handleLowSurrogate(char highSurrogate) { } } - private void writeUtf8Char(int codePoint) { + private void writeUtf8Char(int codePoint) throws IOException { if (codePoint < 0x80) { append(codePoint); } else if (codePoint < 0x800) { diff --git a/java/src/json/ext/StringEncoder.java b/java/src/json/ext/StringEncoder.java index 290aa2495..b1e7096e7 100644 --- a/java/src/json/ext/StringEncoder.java +++ b/java/src/json/ext/StringEncoder.java @@ -9,6 +9,9 @@ import org.jruby.runtime.ThreadContext; import org.jruby.util.ByteList; +import java.io.IOException; +import java.io.OutputStream; + /** * 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, @@ -43,7 +46,7 @@ final class StringEncoder extends ByteListTranscoder { this.scriptSafe = scriptSafe; } - void encode(ByteList src, ByteList out) { + void encode(ByteList src, OutputStream out) throws IOException { init(src, out); append('"'); while (hasNext()) { @@ -53,7 +56,7 @@ void encode(ByteList src, ByteList out) { append('"'); } - private void handleChar(int c) { + private void handleChar(int c) throws IOException { switch (c) { case '"': case '\\': @@ -97,13 +100,13 @@ private void handleChar(int c) { } } - private void escapeChar(char c) { + private void escapeChar(char c) throws IOException { quoteStop(charStart); aux[ESCAPE_CHAR_OFFSET + 1] = (byte)c; append(aux, ESCAPE_CHAR_OFFSET, 2); } - private void escapeUtf8Char(int codePoint) { + private void escapeUtf8Char(int codePoint) throws IOException { int numChars = Character.toChars(codePoint, utf16, 0); escapeCodeUnit(utf16[0], ESCAPE_UNI1_OFFSET + 2); if (numChars > 1) escapeCodeUnit(utf16[1], ESCAPE_UNI2_OFFSET + 2); diff --git a/lib/json/common.rb b/lib/json/common.rb index 3b0620209..a88a3fffa 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -286,7 +286,7 @@ def generate(obj, opts = nil) if State === opts opts.generate(obj) else - State.generate(obj, opts) + State.generate(obj, opts, nil) end end @@ -801,18 +801,15 @@ def dump(obj, anIO = nil, limit = nil, kwargs = nil) opts = opts.merge(:max_nesting => limit) if limit opts = merge_dump_options(opts, **kwargs) if kwargs - result = begin - generate(obj, opts) + begin + if State === opts + opts.generate(obj, anIO) + else + State.generate(obj, opts, anIO) + end rescue JSON::NestingError raise ArgumentError, "exceed depth limit" end - - if anIO.nil? - result - else - anIO.write result - anIO - end end # Encodes string using String.encode. diff --git a/lib/json/ext/generator/state.rb b/lib/json/ext/generator/state.rb index 6cd9496e6..1e0d5245b 100644 --- a/lib/json/ext/generator/state.rb +++ b/lib/json/ext/generator/state.rb @@ -47,6 +47,17 @@ def configure(opts) alias_method :merge, :configure + # call-seq: + # generate(obj) -> String + # generate(obj, anIO) -> anIO + # + # Generates a valid JSON document from object +obj+ and returns the + # result. If no valid JSON document can be created this method raises a + # GeneratorError exception. + def generate(obj, io = nil) + _generate(obj, io) + end + # call-seq: to_h # # Returns the configuration instance variables as a hash, that can be diff --git a/lib/json/truffle_ruby/generator.rb b/lib/json/truffle_ruby/generator.rb index b0f3e4209..84cfd53dc 100644 --- a/lib/json/truffle_ruby/generator.rb +++ b/lib/json/truffle_ruby/generator.rb @@ -96,8 +96,14 @@ def valid_utf8?(string) # This class is used to create State instances, that are use to hold data # while generating a JSON text from a Ruby data structure. class State - def self.generate(obj, opts = nil) - new(opts).generate(obj) + def self.generate(obj, opts = nil, io = nil) + string = new(opts).generate(obj) + if io + io.write(string) + io + else + string + end end # Creates a State object from _opts_, which ought to be Hash to create diff --git a/test/json/json_common_interface_test.rb b/test/json/json_common_interface_test.rb index a5d62337e..1f157da02 100644 --- a/test/json/json_common_interface_test.rb +++ b/test/json/json_common_interface_test.rb @@ -162,6 +162,17 @@ def test_dump assert_equal too_deep, dump(obj, strict: false) end + def test_dump_in_io + io = StringIO.new + assert_same io, JSON.dump([1], io) + assert_equal "[1]", io.string + + big_object = ["a" * 10, "b" * 40, { foo: 1.23 }] * 5000 + io.rewind + assert_same io, JSON.dump(big_object, io) + assert_equal JSON.dump(big_object), io.string + end + def test_dump_should_modify_defaults max_nesting = JSON.dump_default_options[:max_nesting] dump([], StringIO.new, 10)