diff --git a/Rakefile b/Rakefile index c5b518a1..09b69a2e 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/java/src/json/ext/ByteListDirectOutputStream.java b/java/src/json/ext/ByteListDirectOutputStream.java new file mode 100644 index 00000000..178cf11c --- /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 6f6ab66c..0fedcabd 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 f76dcb38..15889969 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,11 +57,12 @@ 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) { Session session = new Session(context, config); Handler handler = getHandlerFor(context.runtime, object); + return handler.generateNew(session, object); } @@ -171,17 +181,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 +202,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 +215,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 +230,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 +281,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 +304,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 +313,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 +346,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 +363,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 +421,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 +461,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 +490,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/GeneratorState.java b/java/src/json/ext/GeneratorState.java index 1600b04a..28797f71 100644 --- a/java/src/json/ext/GeneratorState.java +++ b/java/src/json/ext/GeneratorState.java @@ -230,15 +230,21 @@ public IRubyObject initialize_copy(ThreadContext context, IRubyObject vOrig) { */ @JRubyMethod public IRubyObject generate(ThreadContext context, IRubyObject obj) { - RubyString result = Generator.generateJson(context, obj, this); + IRubyObject result = Generator.generateJson(context, obj, this); 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, diff --git a/java/src/json/ext/StringDecoder.java b/java/src/json/ext/StringDecoder.java index 76cf1837..f4877e93 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 290aa249..b1e7096e 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);