package com.fortituderuby.ext.fortitude; import java.io.IOException; import org.jruby.Ruby; import org.jruby.RubyBasicObject; import org.jruby.RubyBoolean; import org.jruby.RubyArray; import org.jruby.RubyClass; import org.jruby.RubyFixnum; import org.jruby.RubyHash; import org.jruby.RubyNil; import org.jruby.RubyString; import org.jruby.RubySymbol; import org.jruby.anno.JRubyClass; import org.jruby.anno.JRubyMethod; import org.jruby.exceptions.RaiseException; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.runtime.load.Library; public class FortitudeNativeLibrary implements Library { public static final int BUFFER_SIZE = 256; static Ruby runtime; public void load(Ruby theRuntime, boolean wrap) throws IOException { runtime = theRuntime; RubyClass stringClass = runtime.getClass("String"); stringClass.defineAnnotatedMethod(FortitudeStringExtensions.class, "fortitude_append_escaped_string"); RubyClass hashClass = runtime.getClass("Hash"); hashClass.defineAnnotatedMethod(FortitudeHashExtensions.class, "fortitude_append_as_attributes"); } public static class FortitudeStringExtensions { public static final int MAX_SUBSTITUTION_LENGTH = 6; public static final byte AMPERSAND_BYTE = (byte) '&'; public static final byte LESS_THAN_BYTE = (byte) '<'; public static final byte GREATER_THAN_BYTE = (byte) '>'; public static final byte SINGLE_QUOTE_BYTE = (byte) '\''; public static final byte DOUBLE_QUOTE_BYTE = (byte) '\"'; @JRubyMethod(name = "fortitude_append_escaped_string") public static IRubyObject fortitude_append_escaped_string(ThreadContext context, IRubyObject self, IRubyObject output) { if (! (output instanceof RubyString)) { RaiseException exception = runtime.newArgumentError("You can only append to a String (this is a native (Java) method)"); throw exception; } RubyString selfString = (RubyString) self; RubyString outputString = (RubyString) output; IRubyObject htmlSafe = selfString.getInstanceVariable("@html_safe"); if (htmlSafe != null && htmlSafe.isTrue()) { outputString.cat(selfString); } else { byte[] selfBytes = selfString.getBytes(); fortitude_escaped_strcpy(outputString, selfBytes); } return runtime.getNil(); } public static void fortitude_escaped_strcpy(RubyString output, byte[] source) { byte[] buffer = new byte[BUFFER_SIZE]; int bufferPos = 0; for (int i = 0; i < source.length; ++i) { if (bufferPos > (BUFFER_SIZE - MAX_SUBSTITUTION_LENGTH)) { output.cat(buffer, 0, bufferPos); bufferPos = 0; } byte sourceByte = source[i]; switch(sourceByte) { case AMPERSAND_BYTE: buffer[bufferPos++] = '&'; buffer[bufferPos++] = 'a'; buffer[bufferPos++] = 'm'; buffer[bufferPos++] = 'p'; buffer[bufferPos++] = ';'; break; case LESS_THAN_BYTE: buffer[bufferPos++] = '&'; buffer[bufferPos++] = 'l'; buffer[bufferPos++] = 't'; buffer[bufferPos++] = ';'; break; case GREATER_THAN_BYTE: buffer[bufferPos++] = '&'; buffer[bufferPos++] = 'g'; buffer[bufferPos++] = 't'; buffer[bufferPos++] = ';'; break; case SINGLE_QUOTE_BYTE: buffer[bufferPos++] = '&'; buffer[bufferPos++] = '#'; buffer[bufferPos++] = '3'; buffer[bufferPos++] = '9'; buffer[bufferPos++] = ';'; break; case DOUBLE_QUOTE_BYTE: buffer[bufferPos++] = '&'; buffer[bufferPos++] = 'q'; buffer[bufferPos++] = 'u'; buffer[bufferPos++] = 'o'; buffer[bufferPos++] = 't'; buffer[bufferPos++] = ';'; break; default: buffer[bufferPos++] = sourceByte; break; } } if (bufferPos > 0) { output.cat(buffer, 0, bufferPos); } } } public static class FortitudeHashExtensions { public static final byte SPACE = (byte) ' '; public static void fortitude_append_to(IRubyObject object, RubyString rbOutput) { if (object instanceof RubyString) { FortitudeStringExtensions.fortitude_escaped_strcpy(rbOutput, ((RubyString) object).getBytes()); } else if (object instanceof RubySymbol) { FortitudeStringExtensions.fortitude_escaped_strcpy(rbOutput, ((RubyString) ((RubySymbol) object).to_s()).getBytes()); } else if (object instanceof RubyArray) { RubyArray array = (RubyArray) object; for (int i = 0; i < array.getLength(); ++i) { IRubyObject element = (IRubyObject) array.entry(i); if (i > 0) { rbOutput.cat(SPACE); } fortitude_append_to(element, rbOutput); } } else if (object instanceof RubyNil) { // nothing here } else if (object instanceof RubyFixnum) { RubyString asString = ((RubyFixnum) object).to_s(); FortitudeStringExtensions.fortitude_escaped_strcpy(rbOutput, asString.getBytes()); } else { RubyString asString = (RubyString) ((RubyBasicObject) object).callMethod("to_s"); FortitudeStringExtensions.fortitude_escaped_strcpy(rbOutput, asString.getBytes()); } } public static class AppendKeyAndValueVisitor extends RubyHash.Visitor { public final ThreadContext threadContext; public final RubyString prefix; public final RubyString output; public final RubyBoolean allowsBareAttributes; public static final byte[] EQUALS_QUOTE = new byte[] { (byte) '=', (byte) '"' }; public AppendKeyAndValueVisitor(ThreadContext threadContext, RubyString prefix, RubyString output, RubyBoolean allowsBareAttributes) { this.threadContext = threadContext; this.prefix = prefix; this.output = output; this.allowsBareAttributes = allowsBareAttributes; } public void visit(IRubyObject key, IRubyObject value) { if (value instanceof RubyHash) { RubyString newPrefix; if (prefix != null) { newPrefix = (RubyString) prefix.dup(); fortitude_append_to(key, newPrefix); } else { newPrefix = RubyString.newEmptyString(runtime); fortitude_append_to(key, newPrefix); } newPrefix.cat('-'); fortitude_append_as_attributes(threadContext, value, output, newPrefix, this.allowsBareAttributes); } else if ((value instanceof RubyNil) || ((value instanceof RubyBoolean) && (! value.isTrue()))) { // nothing here } else { output.cat(' '); if (prefix != null) { output.cat(prefix); } else { // nothing here } fortitude_append_to(key, output); if ((value instanceof RubyBoolean) && (value.isTrue())) { if (this.allowsBareAttributes.isTrue()) { // ok, nothing here } else { output.cat(EQUALS_QUOTE); fortitude_append_to(key, output); output.cat('"'); } } else { output.cat(EQUALS_QUOTE); fortitude_append_to(value, output); output.cat('"'); } } } } @JRubyMethod(name = "fortitude_append_as_attributes") public static IRubyObject fortitude_append_as_attributes(ThreadContext context, IRubyObject self, IRubyObject output, IRubyObject prefix, IRubyObject allowsBareAttributes) { if (! (output instanceof RubyString)) { RaiseException exception = runtime.newArgumentError("You can only append to a String (this is a native (Java) method)"); throw exception; } if (prefix instanceof RubyNil) { prefix = null; } if (prefix != null && (! (prefix instanceof RubyString))) { RaiseException exception = runtime.newArgumentError("You can only use a prefix that is a String (this is a native (Java) method)"); throw exception; } AppendKeyAndValueVisitor visitor = new AppendKeyAndValueVisitor(context, (RubyString) prefix, (RubyString) output, (RubyBoolean) allowsBareAttributes); ((RubyHash) self).visitAll(visitor); return runtime.getNil(); } } }