/* * This code is copyrighted work by Daniel Luz . * * Distributed under the Ruby license: https://www.ruby-lang.org/en/about/license.txt */ package json.ext; import org.jruby.Ruby; import org.jruby.RubyArray; import org.jruby.RubyBasicObject; import org.jruby.RubyBignum; import org.jruby.RubyBoolean; import org.jruby.RubyFixnum; import org.jruby.RubyFloat; import org.jruby.RubyHash; 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 RubyString generateJson(ThreadContext context, T object, Handler handler, IRubyObject[] args) { Session session = new Session(context, args.length > 0 ? args[0] : null); return session.infect(handler.generateNew(session, object)); } /** * Encodes the given object as a JSON string, detecting the appropriate handler * for the given object. */ static RubyString generateJson(ThreadContext context, T object, IRubyObject[] args) { Handler handler = getHandlerFor(context.runtime, 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 RubyString generateJson(ThreadContext context, T object, GeneratorState config) { Session session = new Session(context, config); Handler handler = getHandlerFor(context.runtime, object); return handler.generateNew(session, object); } /** * 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 Handler getHandlerFor(Ruby runtime, T object) { switch (((RubyBasicObject) object).getNativeClassIndex()) { case NIL : return (Handler) NIL_HANDLER; case TRUE : return (Handler) TRUE_HANDLER; case FALSE : return (Handler) FALSE_HANDLER; case FLOAT : return (Handler) FLOAT_HANDLER; case FIXNUM : return (Handler) FIXNUM_HANDLER; case BIGNUM : return (Handler) BIGNUM_HANDLER; case STRING : if (((RubyBasicObject) object).getMetaClass() != runtime.getString()) break; return (Handler) STRING_HANDLER; case ARRAY : if (((RubyBasicObject) object).getMetaClass() != runtime.getArray()) break; return (Handler) ARRAY_HANDLER; case HASH : if (((RubyBasicObject) object).getMetaClass() != runtime.getHash()) break; return (Handler) HASH_HANDLER; } return GENERIC_HANDLER; } /* Generator context */ /** * A class that concentrates all the information that is shared by * generators working on a single session. * *

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. * *

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(), getState().escapeSlash()); } return stringEncoder; } public void infectBy(IRubyObject object) { if (object.isTaint()) tainted = true; if (object.isUntrusted()) untrusted = true; } public T infect(T object) { if (tainted) object.setTaint(true); if (untrusted) object.setUntrusted(true); return object; } } /* Handler base classes */ private static abstract class Handler { /** * 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) { return 4; } 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; } abstract void generate(Session session, T object, ByteList buffer); } /** * A handler that returns a fixed keyword regardless of the passed object. */ private static class KeywordHandler extends Handler { private final ByteList keyword; private KeywordHandler(String keyword) { this.keyword = new ByteList(ByteList.plain(keyword), false); } @Override int guessSize(Session session, T object) { return keyword.length(); } @Override RubyString generateNew(Session session, T object) { return RubyString.newStringShared(session.getRuntime(), keyword); } @Override void generate(Session session, T object, ByteList buffer) { buffer.append(keyword); } } /* Handlers */ static final Handler BIGNUM_HANDLER = new Handler() { @Override void generate(Session session, RubyBignum object, ByteList buffer) { // 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 FIXNUM_HANDLER = new Handler() { @Override void generate(Session session, RubyFixnum object, ByteList buffer) { buffer.append(object.to_s().getByteList()); } }; 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)) { 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 ARRAY_HANDLER = new Handler() { @Override int guessSize(Session session, RubyArray object) { GeneratorState state = session.getState(); int depth = state.getDepth(); 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) { ThreadContext context = session.getContext(); Ruby runtime = context.getRuntime(); GeneratorState state = session.getState(); int depth = state.increaseDepth(); ByteList indentUnit = state.getIndent(); byte[] shift = Utils.repeat(indentUnit, depth); 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 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.append((byte)']'); } }; static final Handler HASH_HANDLER = new Handler() { @Override int guessSize(Session session, RubyHash object) { GeneratorState state = session.getState(); int perItem = 12 // key, colon, comma + (state.getDepth() + 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) { ThreadContext context = session.getContext(); final Ruby runtime = context.getRuntime(); final GeneratorState state = session.getState(); final int depth = state.increaseDepth(); final ByteList objectNl = state.getObjectNl(); final byte[] indent = Utils.repeat(state.getIndent(), depth); final ByteList spaceBefore = state.getSpaceBefore(); final ByteList space = state.getSpace(); buffer.append((byte)'{'); buffer.append(objectNl); 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); } if (objectNl.length() != 0) buffer.append(indent); STRING_HANDLER.generate(session, key.asString(), buffer); session.infectBy(key); buffer.append(spaceBefore); buffer.append((byte)':'); buffer.append(space); Handler valueHandler = getHandlerFor(runtime, value); valueHandler.generate(session, value, buffer); session.infectBy(value); } }); state.decreaseDepth(); if (!firstPair[0] && objectNl.length() != 0) { buffer.append(objectNl); buffer.append(Utils.repeat(state.getIndent(), state.getDepth())); } buffer.append((byte)'}'); } }; static final Handler STRING_HANDLER = new Handler() { @Override int guessSize(Session session, RubyString object) { // 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) { RuntimeInfo info = session.getInfo(); RubyString src; if (object.encoding(session.getContext()) != info.utf8.get()) { src = (RubyString)object.encode(session.getContext(), info.utf8.get()); } else { src = object; } session.getStringEncoder().encode(src.getByteList(), buffer); } }; static final Handler TRUE_HANDLER = new KeywordHandler("true"); static final Handler FALSE_HANDLER = new KeywordHandler("false"); static final Handler NIL_HANDLER = new KeywordHandler("null"); /** * The default handler (Object#to_json): coerces the object * to string using #to_s, and serializes that string. */ static final Handler OBJECT_HANDLER = new Handler() { @Override RubyString generateNew(Session session, IRubyObject object) { RubyString str = object.asString(); return STRING_HANDLER.generateNew(session, str); } @Override void generate(Session session, IRubyObject object, ByteList buffer) { RubyString str = object.asString(); STRING_HANDLER.generate(session, str, buffer); } }; /** * A handler that simply calls #to_json(state) on the * given object. */ static final Handler GENERIC_HANDLER = new Handler() { @Override RubyString generateNew(Session session, IRubyObject object) { if (object.respondsTo("to_json")) { IRubyObject result = object.callMethod(session.getContext(), "to_json", new IRubyObject[] {session.getState()}); if (result instanceof RubyString) return (RubyString)result; throw session.getRuntime().newTypeError("to_json must return a String"); } else { return OBJECT_HANDLER.generateNew(session, object); } } @Override void generate(Session session, IRubyObject object, ByteList buffer) { RubyString result = generateNew(session, object); buffer.append(result.getByteList()); } }; }