diff --git a/Rakefile b/Rakefile index 9d21c22..1b68779 100644 --- a/Rakefile +++ b/Rakefile @@ -13,6 +13,29 @@ require 'rbconfig' include Config ENV['TEST_MODE'] = 'TRUE' +task :java do + Rake::Task['build:java'].invoke + Rake::Task['test:ruby'].invoke +end + +namespace :build do + desc "Build the java extensions." + task :java do + puts "Building Java extensions..." + java_dir = File.join(File.dirname(__FILE__), 'ext', 'java') + jar_dir = File.join(java_dir, 'jar') + + jruby_jar = File.join(jar_dir, 'jruby.jar') + mongo_jar = File.join(jar_dir, 'mongo.jar') + bson_jar = File.join(jar_dir, 'bson.jar') + + src_base = File.join(java_dir, 'src') + + system("javac -Xlint:unchecked -classpath #{jruby_jar}:#{mongo_jar}:#{bson_jar} #{File.join(src_base, 'org', 'jbson', '*.java')}") + system("cd #{src_base} && jar cf #{File.join(jar_dir, 'jbson.jar')} #{File.join('.', 'org', 'jbson', '*.class')}") + end +end + desc "Test the MongoDB Ruby driver." task :test do puts "\nThis option has changed." diff --git a/bson.gemspec b/bson.gemspec index b6a5832..d1a2aa6 100644 --- a/bson.gemspec +++ b/bson.gemspec @@ -14,6 +14,7 @@ Gem::Specification.new do |s| s.files = ['Rakefile', 'bson.gemspec', 'LICENSE.txt'] s.files += ['lib/bson.rb'] + Dir['lib/bson/**/*.rb'] s.files += ['bin/b2json', 'bin/j2bson'] + s.files += Dir['ext/java/jar/**/*.jar'] s.test_files = Dir['test/mongo_bson/*.rb'] s.executables = ['b2json', 'j2bson'] diff --git a/ext/java/src/org/jbson/RubyBSONCallback.class b/ext/java/src/org/jbson/RubyBSONCallback.class new file mode 100644 index 0000000..bca6824 Binary files /dev/null and b/ext/java/src/org/jbson/RubyBSONCallback.class differ diff --git a/ext/java/src/org/jbson/RubyBSONCallback.java b/ext/java/src/org/jbson/RubyBSONCallback.java new file mode 100644 index 0000000..de91723 --- /dev/null +++ b/ext/java/src/org/jbson/RubyBSONCallback.java @@ -0,0 +1,382 @@ +// BSON Callback +// RubyBSONCallback.java +package org.jbson; + +import org.jruby.*; +import org.jruby.util.ByteList; +import org.jruby.RubyString; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.runtime.Block; +import org.jruby.runtime.CallType; +import org.jruby.runtime.callsite.CacheEntry; + +import org.jruby.javasupport.JavaEmbedUtils; +import org.jruby.javasupport.JavaUtil; + +import org.jruby.parser.ReOptions; + +import org.jruby.RubyArray; + +import java.io.*; +import java.util.*; +import java.util.regex.*; + +import org.bson.*; +import org.bson.types.*; + +public class RubyBSONCallback implements BSONCallback { + + private RubyHash _root; + private RubyModule _rbclsOrderedHash; + private RubyModule _rbclsObjectId; + private RubyModule _rbclsBinary; + private RubyModule _rbclsMinKey; + private RubyModule _rbclsMaxKey; + private RubyModule _rbclsDBRef; + private RubyModule _rbclsCode; + private final LinkedList _stack = new LinkedList(); + private final LinkedList _nameStack = new LinkedList(); + private Ruby _runtime; + static final HashMap _runtimeCache = new HashMap(); + + public RubyBSONCallback(Ruby runtime) { + _runtime = runtime; + _rbclsOrderedHash = _lookupConstant( _runtime, "BSON::OrderedHash" ); + _rbclsBinary = _lookupConstant( _runtime, "BSON::Binary" ); + _rbclsDBRef = _lookupConstant( _runtime, "BSON::DBRef" ); + _rbclsCode = _lookupConstant( _runtime, "BSON::Code" ); + _rbclsMinKey = _lookupConstant( _runtime, "BSON::MinKey" ); + _rbclsMaxKey = _lookupConstant( _runtime, "BSON::MaxKey" ); + _rbclsObjectId = _lookupConstant( _runtime, "BSON::ObjectId"); + } + + public BSONCallback createBSONCallback(){ + return new RubyBSONCallback(_runtime); + } + + public void reset(){ + _root = null; + _stack.clear(); + _nameStack.clear(); + } + + public RubyHash createHash() { + RubyHash h = (RubyHash)JavaEmbedUtils.invokeMethod(_runtime, _rbclsOrderedHash, "new", + new Object[] { }, Object.class); + + return h; + } + + public RubyArray createArray() { + return RubyArray.newArray(_runtime); + } + + public RubyObject create( boolean array , List path ){ + if ( array ) + return createArray(); + return createHash(); + } + + public void objectStart(){ + if ( _stack.size() > 0 ) { + throw new IllegalStateException( "something is wrong" ); + } + + _root = createHash(); + _stack.add(_root); + } + + public void objectStart(boolean f) { + objectStart(); + } + + public void objectStart(String key){ + RubyHash hash = createHash(); + + _nameStack.addLast( key ); + + RubyObject lastObject = _stack.getLast(); + + // Yes, this is a bit hacky. + if(lastObject instanceof RubyHash) { + writeRubyHash(key, (RubyHash)lastObject, (IRubyObject)hash); + } + else { + writeRubyArray(key, (RubyArray)lastObject, (IRubyObject)hash); + } + + _stack.addLast( (RubyObject)hash ); + } + + public void writeRubyHash(String key, RubyHash hash, IRubyObject obj) { + RubyString rkey = _runtime.newString(key); + JavaEmbedUtils.invokeMethod(_runtime, hash, "[]=", + new Object[] { (IRubyObject)rkey, obj }, Object.class); + } + + public void writeRubyArray(String key, RubyArray array, IRubyObject obj) { + Long rkey = Long.parseLong(key); + RubyFixnum index = new RubyFixnum(_runtime, rkey); + array.aset((IRubyObject)index, obj); + } + + public void arrayStart(String key){ + RubyArray array = createArray(); + + RubyObject lastObject = _stack.getLast(); + _nameStack.addLast( key ); + + if(lastObject instanceof RubyHash) { + writeRubyHash(key, (RubyHash)lastObject, (IRubyObject)array); + } + else { + writeRubyArray(key, (RubyArray)lastObject, (IRubyObject)array); + } + + _stack.addLast( (RubyObject)array ); + } + + public RubyObject objectDone(){ + RubyObject o =_stack.removeLast(); + if ( _nameStack.size() > 0 ) + _nameStack.removeLast(); + else if ( _stack.size() > 0 ) { + throw new IllegalStateException( "something is wrong" ); + } + return o; + } + + // Not used by Ruby decoder + public void arrayStart(){ + } + + public RubyObject arrayDone(){ + return objectDone(); + } + + public void gotNull( String name ){ + _put(name, (RubyObject)_runtime.getNil()); + } + + // Undefined should be represented as a lack of key / value. + public void gotUndefined( String name ){ + } + + // TODO: Handle this + public void gotUUID( String name , long part1, long part2) { + //_put( name , new UUID(part1, part2) ); + } + + public void gotCode( String name , String code ){ + RubyString code_string = _runtime.newString( code ); + Object rb_code_obj = JavaEmbedUtils.invokeMethod(_runtime, _rbclsCode, + "new", new Object[] { code_string }, Object.class); + _put( name , (RubyObject)rb_code_obj ); + } + + public void gotCodeWScope( String name , String code , Object scope ){ + RubyString code_string = _runtime.newString( code ); + + Object rb_code_obj = JavaEmbedUtils.invokeMethod(_runtime, _rbclsCode, + "new", new Object[] { code_string, (RubyHash)scope }, Object.class); + + _put( name , (RubyObject)rb_code_obj ); + } + + public void gotMinKey( String name ){ + Object minkey = JavaEmbedUtils.invokeMethod(_runtime, _rbclsMinKey, "new", new Object[] {}, Object.class); + + _put( name, (RubyObject)minkey); + } + + public void gotMaxKey( String name ){ + Object maxkey = JavaEmbedUtils.invokeMethod(_runtime, _rbclsMaxKey, "new", new Object[] {}, Object.class); + + _put( name, (RubyObject)maxkey); + } + + public void gotBoolean( String name , boolean v ){ + RubyBoolean b = RubyBoolean.newBoolean( _runtime, v ); + _put(name , b); + } + + public void gotDouble( String name , double v ){ + RubyFloat f = new RubyFloat( _runtime, v ); + _put(name , (RubyObject)f); + } + + public void gotInt( String name , int v ){ + RubyFixnum f = new RubyFixnum( _runtime, v ); + _put(name , (RubyObject)f); + } + + public void gotLong( String name , long v ){ + RubyFixnum f = new RubyFixnum( _runtime, v ); + _put(name , (RubyObject)f); + } + + public void gotDate( String name , long millis ){ + RubyTime time = RubyTime.newTime(_runtime, millis).gmtime(); + _put( name , time ); + } + + public void gotRegex( String name , String pattern , String flags ){ + int f = 0; + ByteList b = new ByteList(pattern.getBytes()); + + if(flags.contains("i")) { + f = f | ReOptions.RE_OPTION_IGNORECASE; + } + if(flags.contains("m")) { + f = f | ReOptions.RE_OPTION_MULTILINE; + } + if(flags.contains("x")) { + f = f | ReOptions.RE_OPTION_EXTENDED; + } + + _put( name , RubyRegexp.newRegexp(_runtime, b, f) ); + } + + public void gotString( String name , String v ){ + RubyString str = RubyString.newString(_runtime, v); + _put( name , str ); + } + + public void gotSymbol( String name , String v ){ + ByteList bytes = new ByteList(v.getBytes()); + RubySymbol symbol = _runtime.getSymbolTable().getSymbol(bytes); + _put( name , symbol ); + } + + // Timestamp is currently rendered in Ruby as a two-element array. + public void gotTimestamp( String name , int time , int inc ){ + RubyFixnum rtime = RubyFixnum.newFixnum( _runtime, time ); + RubyFixnum rinc = RubyFixnum.newFixnum( _runtime, inc ); + RubyObject[] args = new RubyObject[2]; + args[0] = rinc; + args[1] = rtime; + + RubyArray result = RubyArray.newArray( _runtime, args ); + + _put ( name , result ); + } + + public void gotObjectId( String name , ObjectId id ){ + IRubyObject arg = (IRubyObject)RubyString.newString(_runtime, id.toString()); + Object[] args = new Object[] { arg }; + + Object result = JavaEmbedUtils.invokeMethod(_runtime, _rbclsObjectId, "from_string", args, Object.class); + + _put( name, (RubyObject)result ); + } + + // TODO: Incredibly annoying to deserialize to a Ruby DBRef. Might just + // stop supporting this altogether in the driver. + public void gotDBRef( String name , String ns , ObjectId id ){ + // _put( name , new BasicBSONObject( "$ns" , ns ).append( "$id" , id ) ); + } + + // TODO: I know that this is horrible. To be optimized. + private RubyArray ja2ra( byte[] b ) { + RubyArray result = RubyArray.newArray( _runtime, b.length ); + + for ( int i=0; i cache = _runtimeCache.get( runtime ); + + if(cache == null) { + cache = new HashMap(); + _runtimeCache.put( runtime, cache ); + } + return cache; + } + + static final RubyModule _lookupConstant(Ruby runtime, String name) + { + HashMap cache = _getRuntimeCache( runtime ); + RubyModule module = (RubyModule) cache.get( name ); + + if(module == null && !cache.containsKey( name )) { + module = runtime.getClassFromPath( name ); + cache.put( (String)name, (Object)module ); + } + return module; + } +} diff --git a/ext/java/src/org/jbson/RubyBSONDecoder.class b/ext/java/src/org/jbson/RubyBSONDecoder.class new file mode 100644 index 0000000..90dc0fc Binary files /dev/null and b/ext/java/src/org/jbson/RubyBSONDecoder.class differ diff --git a/ext/java/src/org/jbson/RubyBSONDecoder.java b/ext/java/src/org/jbson/RubyBSONDecoder.java new file mode 100644 index 0000000..b9e4300 --- /dev/null +++ b/ext/java/src/org/jbson/RubyBSONDecoder.java @@ -0,0 +1,27 @@ +// RubyBSONDecoder.java + +package org.jbson; + +import static org.bson.BSON.*; + +import java.io.*; + +import org.jruby.*; + +import org.bson.*; +import org.bson.io.*; +import org.bson.types.*; + +public class RubyBSONDecoder extends BSONDecoder { + +// public int decode( RubyString s , BSONCallback callback ){ +// byte[] b = s.getBytes(); +// try { +// return decode( new Input( new ByteArrayInputStream(b) ) , callback ); +// } +// catch ( IOException ioe ){ +// throw new RuntimeException( "should be impossible" , ioe ); +// } +// } + +} diff --git a/ext/java/src/org/jbson/RubyBSONEncoder.class b/ext/java/src/org/jbson/RubyBSONEncoder.class new file mode 100644 index 0000000..0b3d96d Binary files /dev/null and b/ext/java/src/org/jbson/RubyBSONEncoder.class differ diff --git a/ext/java/src/org/jbson/RubyBSONEncoder.java b/ext/java/src/org/jbson/RubyBSONEncoder.java new file mode 100644 index 0000000..ef695f6 --- /dev/null +++ b/ext/java/src/org/jbson/RubyBSONEncoder.java @@ -0,0 +1,764 @@ +// BSONEncoder.java + +package org.jbson; + +import static org.bson.BSON.*; + +import java.nio.*; +import java.nio.charset.*; +import java.util.*; +import java.util.concurrent.atomic.*; +import java.util.regex.*; +import java.io.*; +import org.jruby.util.Pack; + + +import java.math.BigInteger; + +import org.bson.BSONEncoder; + +import org.jruby.javasupport.JavaEmbedUtils; +import org.jruby.javasupport.JavaUtil; + +import org.jruby.*; +import org.jruby.runtime.builtin.IRubyObject; + +import org.jruby.parser.ReOptions; + +import org.jcodings.Encoding; + +import org.bson.BSONObject; +import org.bson.io.*; +import org.bson.types.*; +import org.bson.BSON; + +import org.jruby.exceptions.RaiseException; + +import org.jruby.java.addons.ArrayJavaAddons; + +/** + * this is meant to be pooled or cached + * there is some per instance memory for string conversion, etc... + */ +@SuppressWarnings("unchecked") +public class RubyBSONEncoder extends BSONEncoder { + + static final boolean DEBUG = false; + static final Map _runtimeCache = new HashMap(); + + private Ruby _runtime; + + private RubyModule _rbclsByteBuffer; + private RubyModule _rbclsDBRef; + private RubyModule _rbclsInvalidDocument; + private RubyModule _rbclsInvalidKeyName; + private RubyModule _rbclsRangeError; + private RubySymbol _idAsSym; + private RubyString _idAsString; + private RubyString _tfAsString; + + private static final int BIT_SIZE = 64; + private static final long MAX = (1L << (BIT_SIZE - 1)) - 1; + private static final BigInteger LONG_MAX = BigInteger.valueOf(MAX); + private static final BigInteger LONG_MIN = BigInteger.valueOf(-MAX - 1); + + + public RubyBSONEncoder(Ruby runtime){ + _runtime = runtime; + _rbclsByteBuffer = _lookupConstant( _runtime, "BSON::ByteBuffer" ); + _rbclsDBRef = _lookupConstant( _runtime, "BSON::DBRef" ); + _rbclsInvalidDocument = _lookupConstant( _runtime, "BSON::InvalidDocument" ); + _rbclsInvalidKeyName = _lookupConstant( _runtime, "BSON::InvalidKeyName" ); + _rbclsRangeError = _lookupConstant( _runtime, "RangeError" ); + _idAsSym = _lookupSymbol( _runtime, "_id" ); + _tfAsString = _lookupString( _runtime, "_transientFields" ); + + if(_idAsString == null) { + _idAsString = _runtime.newString( "_id" ); + } + } + + public RubyString encode( Object arg ) { + RubyHash o = (RubyHash)arg; + BasicOutputBuffer buf = new BasicOutputBuffer(); + set( buf ); + putObject( o ); + done(); + + RubyString b = RubyString.newString(_runtime, buf.toByteArray()); + return b; + } + + public void set( OutputBuffer out ){ + if ( _buf != null ) { + done(); + throw new IllegalStateException( "in the middle of something" ); + } + + _buf = out; + } + + public void done(){ + _buf = null; + } + + /** + * @return true if object was handled + */ + protected boolean handleSpecialObjects( String name , RubyObject o ){ + return false; + } + + /** Encodes a BSONObject. + * This is for the higher level api calls + * @param o the object to encode + * @return the number of characters in the encoding + */ + public int putObject( RubyObject o ) { + return putObject( null, o ); + } + + /** + * this is really for embedded objects + */ + int putObject( String name , RubyObject o ){ + if ( o == null ) + throw new NullPointerException( "can't save a null object" ); + + final int start = _buf.getPosition(); + + byte myType = OBJECT; + if ( o instanceof RubyArray ) + myType = ARRAY; + + if ( handleSpecialObjects( name , o ) ) + return _buf.getPosition() - start; + + if ( name != null ){ + _put( myType , name ); + } + + final int sizePos = _buf.getPosition(); + _buf.writeInt( 0 ); // leaving space for sthis. set it at the end + + List transientFields = null; + boolean rewriteID = ( myType == OBJECT && name == null ); + + + if ( myType == OBJECT ) { + + if ( rewriteID ) { + + if ( _rbHashHasKey( (RubyHash)o, "_id" ) ) { + _putObjectField( "_id" , _rbHashGet( (RubyHash)o, _idAsString ) ); + } + else if ( ( _rbHashHasKey( (RubyHash)o, _idAsSym )) ) { + _putObjectField( "_id" , _rbHashGet( (RubyHash)o, _idAsSym ) ); + } + + RubyObject temp = (RubyObject)_rbHashGet( (RubyHash)o, _tfAsString ); + if ( temp instanceof RubyArray ) + transientFields = (RubyArray)temp; + } + + // Not sure we should invoke this way. Depends on if we can access the OrderedHash. + RubyArray keys = (RubyArray)JavaEmbedUtils.invokeMethod( _runtime, o , "keys" , new Object[] {} + , Object.class); + + for (Iterator i = keys.iterator(); i.hasNext(); ) { + + Object hashKey = i.next(); + + // Convert the key into a Java String + String str = ""; + if( hashKey instanceof String) { + str = hashKey.toString(); + } + + else if (hashKey instanceof RubyString) { + str = ((RubyString)hashKey).asJavaString(); + } + else if (hashKey instanceof RubySymbol) { + str = ((RubySymbol)hashKey).asJavaString(); + } + + testNull(str); + + // If we're rewriting the _id, we can move on. + if ( rewriteID && str.equals( "_id" ) ) + continue; + + RubyObject val = (RubyObject)_rbHashGet( (RubyHash)o, hashKey ); + _putObjectField( str , (Object)val ); + } + } + + // Make sure we're within the 4MB limit + if ( _buf.size() > 4 * 1024 * 1024 ) { + _rbRaise( (RubyClass)_rbclsInvalidDocument, + "Document is too large (" + _buf.size() + "). BSON documents are limited to 4MB (" + + 4 * 1024 * 1024 + ")."); + } + + _buf.write( EOO ); + + _buf.writeInt( sizePos , _buf.getPosition() - sizePos ); + return _buf.getPosition() - start; + } + + protected void _putObjectField( String name , Object val ){ + + if ( name.equals( "_transientFields" ) ) + return; + + if ( DEBUG ) { + System.out.println( "\t put thing : " + name ); + System.out.println( "\t class : " + val.getClass().getName() ); + } + + if ( name.equals( "$where") && val instanceof String ){ + _put( CODE , name ); + _putValueString( val.toString() ); + return; + } + + if ( val instanceof String ) + putString(name, val.toString() ); + + // TODO: Clean up + else if ( val instanceof Number ) { + if ( val instanceof Double ) { + putNumber(name, (Number)val); + } + else { + long jval = ((Number)val).longValue(); + if (jval > Integer.MIN_VALUE && jval < Integer.MAX_VALUE) { + putNumber(name, (int)jval ); + } + else + putNumber(name, (Number)jval ); + } + } + + else if ( val instanceof Boolean ) + putBoolean(name, (Boolean)val); + + else if ( val instanceof Map ) + putMap( name , (Map)val ); + + else if ( val instanceof Iterable) + putIterable( name , (Iterable)val ); + + else if ( val instanceof byte[] ) + putBinary( name , (byte[])val ); + + else if ( val.getClass().isArray() ) + putIterable( name , Arrays.asList( (Object[])val ) ); + + else if ( val instanceof RubyObject ) { + + if ( val instanceof RubyString ) { + putRubyString(name, ((RubyString)val).getUnicodeValue() ); + } + + else if (val instanceof RubySymbol) { + putSymbol(name, new Symbol(val.toString())); + } + + // TODO: Clean up + else if ( val instanceof RubyFixnum ) { + long jval = ((RubyFixnum)val).getLongValue(); + if (jval >= Integer.MIN_VALUE && jval <= Integer.MAX_VALUE) { + putNumber(name, (int)jval ); + } + else + putNumber(name, (Number)jval ); + } + + // TODO: Clean up + else if ( val instanceof RubyFloat ) { + double jval = ((RubyFloat)val).getValue(); + putNumber(name, (Number)jval ); + } + + else if ( val instanceof RubyNil ) + putNull(name); + + else if ( val instanceof RubyTime ) { + putDate( name , ((RubyTime)val).getDateTime().getMillis() ); + } + + else if ( val instanceof RubyBoolean ) + putBoolean(name, (Boolean)((RubyBoolean)val).toJava(Boolean.class)); + + else if ( val instanceof RubyRegexp ) + putRubyRegexp(name, (RubyRegexp)val ); + + else if (val instanceof RubyBignum) { + BigInteger big = ((RubyBignum)val).getValue(); + if( big.compareTo(LONG_MAX) > 0 || big.compareTo(LONG_MIN) < 0 ) { + _rbRaise( (RubyClass)_rbclsRangeError , "MongoDB can only handle 8-byte ints" ); + } + else { + long jval = big.longValue(); + putNumber(name, (Number)jval ); + } + } + + // This is where we handle special types defined in the Ruby BSON. + else { + String klass = JavaEmbedUtils.invokeMethod(_runtime, val, + "class", new Object[] {}, Object.class).toString(); + + if( klass.equals( "BSON::ObjectId" ) ) { + putRubyObjectId(name, (RubyObject)val ); + } + else if( klass.equals( "BSON::ObjectID" ) ) { + putRubyObjectId(name, (RubyObject)val ); + } + else if ( klass.equals( "BSON::Code" ) ) { + putRubyCodeWScope(name, (RubyObject)val ); + } + else if ( klass.equals( "BSON::Binary" ) ) { + putRubyBinary( name , (RubyObject)val ); + } + else if ( klass.equals("BSON::MinKey") ) { + _put( MINKEY, name ); + } + else if ( klass.equals("BSON::MaxKey") ) { + _put( MAXKEY, name ); + } + else if ( klass.equals("BSON::DBRef") ) { + RubyHash ref = (RubyHash)JavaEmbedUtils.invokeMethod(_runtime, val, + "to_hash", new Object[] {}, Object.class); + putMap( name , (Map)ref ); + } + else if ( klass.equals("Date") || klass.equals("DateTime") || + klass.equals("ActiveSupport::TimeWithZone") ) { + + _rbRaise( (RubyClass)_rbclsInvalidDocument, + klass + " is not currently supported; use a UTC Time instance instead."); + } + else { + _rbRaise( (RubyClass)_rbclsInvalidDocument, + "Cannot serialize " + klass + " as a BSON type; " + + "it either isn't supported or won't translate to BSON."); + + } + } + } + else { + String klass = JavaEmbedUtils.invokeMethod(_runtime, val, + "class", new Object[] {}, Object.class).toString(); + + _rbRaise( (RubyClass)_rbclsInvalidDocument, + "Cannot serialize " + klass + " as a BSON type; " + + "it either isn't supported or won't translate to BSON."); + } + } + + private void testNull(String str) { + byte[] bytes = str.getBytes(); + + for(int j = 0; j < bytes.length; j++ ) { + if(bytes[j] == '\u0000') { + _rbRaise( (RubyClass)_rbclsInvalidDocument, "Null not allowed"); + } + } + } + + private void putIterable( String name , Iterable l ){ + _put( ARRAY , name ); + final int sizePos = _buf.getPosition(); + _buf.writeInt( 0 ); + + int i=0; + for ( Object obj: l ) { + _putObjectField( String.valueOf( i ) , obj ); + i++; + } + + + _buf.write( EOO ); + _buf.writeInt( sizePos , _buf.getPosition() - sizePos ); + } + + private void putMap( String name , Map m ){ + _put( OBJECT , name ); + final int sizePos = _buf.getPosition(); + _buf.writeInt( 0 ); + + for ( Map.Entry entry : (Set)m.entrySet() ) + _putObjectField( entry.getKey().toString() , entry.getValue() ); + + _buf.write( EOO ); + _buf.writeInt( sizePos , _buf.getPosition() - sizePos ); + } + + + protected void putNull( String name ){ + _put( NULL , name ); + } + + protected void putUndefined(String name){ + _put(UNDEFINED, name); + } + + protected void putTimestamp(String name, BSONTimestamp ts ){ + _put( TIMESTAMP , name ); + _buf.writeInt( ts.getInc() ); + _buf.writeInt( ts.getTime() ); + } + + protected void putRubyCodeWScope( String name , RubyObject code ){ + _put( CODE_W_SCOPE , name ); + int temp = _buf.getPosition(); + _buf.writeInt( 0 ); + + String code_string = (String)JavaEmbedUtils.invokeMethod(_runtime, code, + "code", new Object[] {}, Object.class); + + _putValueString( code_string ); + putObject( (RubyObject)JavaEmbedUtils.invokeMethod(_runtime, code, "scope", new Object[] {}, Object.class) ); + _buf.writeInt( temp , _buf.getPosition() - temp ); + } + + protected void putCodeWScope( String name , CodeWScope code ){ + _put( CODE_W_SCOPE , name ); + int temp = _buf.getPosition(); + _buf.writeInt( 0 ); + _putValueString( code.getCode() ); + _buf.writeInt( temp , _buf.getPosition() - temp ); + } + + protected void putBoolean( String name , Boolean b ){ + _put( BOOLEAN , name ); + _buf.write( b ? (byte)0x1 : (byte)0x0 ); + } + + protected void putDate( String name , long millis ){ + _put( DATE , name ); + _buf.writeLong( millis ); + } + + protected void putNumber( String name , Number n ){ + if ( n instanceof Integer || + n instanceof Short || + n instanceof Byte || + n instanceof AtomicInteger ){ + _put( NUMBER_INT , name ); + _buf.writeInt( n.intValue() ); + } + else if ( n instanceof Long || + n instanceof AtomicLong ) { + _put( NUMBER_LONG , name ); + _buf.writeLong( n.longValue() ); + } + else { + _put( NUMBER , name ); + _buf.writeDouble( n.doubleValue() ); + } + } + + private void putRubyBinary( String name , RubyObject binary ) { + RubyArray rarray = (RubyArray)JavaEmbedUtils.invokeMethod(_runtime, + binary, "to_a", new Object[] {}, Object.class); + Long rbSubtype = (Long)JavaEmbedUtils.invokeMethod(_runtime, + binary, "subtype", new Object[] {}, Object.class); + long subtype = rbSubtype.longValue(); + byte[] data = ra2ba( rarray ); + if ( subtype == 2 ) { + putBinary( name, data ); + } + else { + _put( BINARY , name ); + _buf.writeInt( data.length ); + _buf.write( (byte)subtype ); + _buf.write( data ); + } + } + + protected void putBinary( String name , byte[] data ){ + _put( BINARY , name ); + _buf.writeInt( 4 + data.length ); + + _buf.write( B_BINARY ); + _buf.writeInt( data.length ); + int before = _buf.getPosition(); + _buf.write( data ); + int after = _buf.getPosition(); + + com.mongodb.util.MyAsserts.assertEquals( after - before , data.length ); + } + + protected void putBinary( String name , Binary val ){ + _put( BINARY , name ); + _buf.writeInt( val.length() ); + _buf.write( val.getType() ); + _buf.write( val.getData() ); + } + + protected void putUUID( String name , UUID val ){ + _put( BINARY , name ); + _buf.writeInt( 4 + 64*2); + _buf.write( 3 );// B_UUID ); + _buf.writeLong( val.getMostSignificantBits()); + _buf.writeLong( val.getLeastSignificantBits()); + } + + protected void putSymbol( String name , Symbol s ){ + _putString(name, s.getSymbol(), SYMBOL); + } + + protected void putRubyString( String name , String s ) { + _put( STRING , name ); + _putValueString( s ); + } + + protected void putString(String name, String s) { + _putString(name, s, STRING); + } + + private void _putString( String name , String s, byte type ){ + _put( type , name ); + _putValueString( s ); + } + + private void putRubyObjectId( String name, RubyObject oid ) { + _put( OID , name ); + + RubyArray roid = (RubyArray)JavaEmbedUtils.invokeMethod(_runtime, oid, + "to_a", new Object[] {}, Object.class); + byte[] joid = ra2ba( (RubyArray)roid ); + + _buf.writeInt( convertToInt(joid, 0) ); + _buf.writeInt( convertToInt(joid, 4) ); + _buf.writeInt( convertToInt(joid, 8) ); + } + + private void putRubyRegexp( String name, RubyRegexp r ) { + RubyString source = (RubyString)r.source(); + testNull(source.toString()); + + _put( REGEX , name ); + _put( (String)((RubyString)source).toJava(String.class) ); + + int regexOptions = (int)((RubyFixnum)r.options()).getLongValue(); + String options = ""; + + if( (regexOptions & ReOptions.RE_OPTION_IGNORECASE) != 0 ) + options = options.concat( "i" ); + + if( (regexOptions & ReOptions.RE_OPTION_MULTILINE) != 0 ) + options = options.concat( "m" ); + + if( (regexOptions & ReOptions.RE_OPTION_EXTENDED) != 0 ) + options = options.concat( "x" ); + + _put( options ); + } + + // --------------------------------------------- + // Ruby-based helper methods. + + // Converts four bytes from a byte array to an int + private int convertToInt(byte[] b, int offset) { + int intVal = ((b[offset + 3] & 0xff) << 24) | ((b[offset + 2] & 0xff) << 16) | ((b[offset + 1] & 0xff) << 8) | ((b[offset] & 0xff)); + + return intVal; + } + + // Ruby array to byte array + private byte[] ra2ba( RubyArray rArray ) { + int len = rArray.getLength(); + byte[] b = new byte[len]; + int n = 0; + + for ( Iterator i = rArray.iterator(); i.hasNext(); ) { + Object value = i.next(); + b[n] = (byte)((Long)value).intValue(); + n++; + } + + return b; + } + + // Helper method for getting a value from a Ruby hash. + private IRubyObject _rbHashGet(RubyHash hash, Object key) { + if (key instanceof String) { + return hash.op_aref( _runtime.getCurrentContext(), _runtime.newString((String)key) ); + } + else { + return hash.op_aref( _runtime.getCurrentContext(), (RubyObject)key ); + } + } + + // Helper method for checking whether a Ruby hash has a certain key. + private boolean _rbHashHasKey(RubyHash hash, String key) { + RubyBoolean b = hash.has_key_p( _runtime.newString( key ) ); + return b == _runtime.getTrue(); + } + + private boolean _rbHashHasKey(RubyHash hash, RubySymbol sym) { + RubyBoolean b = hash.has_key_p( sym ); + return b == _runtime.getTrue(); + } + + // Helper method for setting a value in a Ruby hash. + private IRubyObject _rbHashSet(RubyHash hash, String key, IRubyObject value) { + return hash.op_aset( _runtime.getCurrentContext(), _runtime.newString( key ), value ); + } + + + // Helper method for returning all keys from a Ruby hash. + private RubyArray _rbHashKeys(RubyHash hash) { + return hash.keys(); + } + + // Helper for raising a Ruby exception and aborting serialization. + private RaiseException _rbRaise( RubyClass exceptionClass , String message ) { + done(); + throw new RaiseException( _runtime, exceptionClass, message, true ); + } + + // ---------------------------------------------- + + /** + * Encodes the type and key. + * + */ + protected void _put( byte type , String name ){ + _buf.write( type ); + _put( name ); + } + + /** + * Encodes the type and key without checking the validity of the key. + * + */ + protected void _putWithoutCheck( byte type , String name ){ + _buf.write( type ); + _put( name ); + } + + protected void _putValueString( String s ){ + int lenPos = _buf.getPosition(); + _buf.writeInt( 0 ); // making space for size + int strLen = _put( s ); + _buf.writeInt( lenPos , strLen ); + } + + void _reset( Buffer b ){ + b.position(0); + b.limit( b.capacity() ); + } + + /** + * puts as utf-8 string + */ + protected int _put( String str ){ + int total = 0; + + final int len = str.length(); + int pos = 0; + while ( pos < len ){ + _reset( _stringC ); + _reset( _stringB ); + + int toEncode = Math.min( _stringC.capacity() - 1, len - pos ); + _stringC.put( str , pos , pos + toEncode ); + _stringC.flip(); + + CoderResult cr = _encoder.encode( _stringC , _stringB , false ); + + if ( cr.isMalformed() || cr.isUnmappable() ) + throw new IllegalArgumentException( "malforumed string" ); + + if ( cr.isOverflow() ) + throw new RuntimeException( "overflor should be impossible" ); + + if ( cr.isError() ) + throw new RuntimeException( "should never get here" ); + + if ( ! cr.isUnderflow() ) + throw new RuntimeException( "this should always be true" ); + + total += _stringB.position(); + _buf.write( _stringB.array() , 0 , _stringB.position() ); + + pos += toEncode; + } + + _buf.write( (byte)0 ); + total++; + + return total; + } + + public void writeInt( int x ){ + _buf.writeInt( x ); + } + + public void writeLong( long x ){ + _buf.writeLong( x ); + } + + public void writeCString( String s ){ + _put( s ); + } + + protected OutputBuffer _buf; + + private CharBuffer _stringC = CharBuffer.wrap( new char[256 + 1] ); + private ByteBuffer _stringB = ByteBuffer.wrap( new byte[1024 + 1] ); + private CharsetEncoder _encoder = Charset.forName( "UTF-8" ).newEncoder(); + + static final Map _getRuntimeCache(Ruby runtime) { + // each JRuby runtime may have different objects for these constants, + // so cache them separately for each runtime + Map cache = (Map) _runtimeCache.get( runtime ); + + if(cache == null) { + cache = new HashMap(); + _runtimeCache.put( runtime, cache ); + } + return cache; + } + + static final RubyModule _lookupConstant(Ruby runtime, String name) + { + Map cache = (Map) _getRuntimeCache( runtime ); + RubyModule module = (RubyModule) cache.get( name ); + + if(module == null && !cache.containsKey( name )) { + module = runtime.getClassFromPath( name ); + cache.put( name, module ); + } + return module; + } + + static final RubySymbol _lookupSymbol(Ruby runtime, String name) + { + Map cache = (Map) _getRuntimeCache( runtime ); + RubySymbol symbol = (RubySymbol) cache.get( name ); + + if(symbol == null && !cache.containsKey( name )) { + symbol = runtime.newSymbol( name ); + cache.put( name, symbol ); + } + return symbol; + } + + static final RubyString _lookupString(Ruby runtime, String name) + { + Map cache = (Map) _getRuntimeCache( runtime ); + RubyString string = (RubyString) cache.get( name ); + + if(string == null && !cache.containsKey( name )) { + string = runtime.newString( name ); + cache.put( name, string ); + } + return string; + } + +} diff --git a/lib/bson.rb b/lib/bson.rb index d5f9075..6375270 100644 --- a/lib/bson.rb +++ b/lib/bson.rb @@ -32,28 +32,39 @@ module BSON end end -begin - # Need this for running test with and without c ext in Ruby 1.9. - raise LoadError if ENV['TEST_MODE'] && !ENV['C_EXT'] - require 'bson_ext/cbson' - raise LoadError unless defined?(CBson::VERSION) - if CBson::VERSION < MINIMUM_BSON_EXT_VERSION - puts "Able to load bson_ext version #{CBson::VERSION}, but >= #{MINIMUM_BSON_EXT_VERSION} is required." - raise LoadError - end - require 'bson/bson_c' +if RUBY_PLATFORM =~ /java/ + jar_dir = File.join(File.dirname(__FILE__), '..', 'ext', 'java', 'jar') + require File.join(jar_dir, 'mongo.jar') + require File.join(jar_dir, 'bson.jar') + require File.join(jar_dir, 'jbson.jar') + require 'bson/bson_java' module BSON - BSON_CODER = BSON_C + BSON_CODER = BSON_JAVA end -rescue LoadError - require 'bson/bson_ruby' - module BSON - BSON_CODER = BSON_RUBY +else + begin + # Need this for running test with and without c ext in Ruby 1.9. + raise LoadError if ENV['TEST_MODE'] && !ENV['C_EXT'] + require 'bson_ext/cbson' + raise LoadError unless defined?(CBson::VERSION) + if CBson::VERSION < MINIMUM_BSON_EXT_VERSION + puts "Able to load bson_ext version #{CBson::VERSION}, but >= #{MINIMUM_BSON_EXT_VERSION} is required." + raise LoadError + end + require 'bson/bson_c' + module BSON + BSON_CODER = BSON_C + end + rescue LoadError + require 'bson/bson_ruby' + module BSON + BSON_CODER = BSON_RUBY + end + warn "\n**Notice: C extension not loaded. This is required for optimum MongoDB Ruby driver performance." + warn " You can install the extension as follows:\n gem install bson_ext\n" + warn " If you continue to receive this message after installing, make sure that the" + warn " bson_ext gem is in your load path and that the bson_ext and mongo gems are of the same version.\n" end - warn "\n**Notice: C extension not loaded. This is required for optimum MongoDB Ruby driver performance." - warn " You can install the extension as follows:\n gem install bson_ext\n" - warn " If you continue to receive this message after installing, make sure that the" - warn " bson_ext gem is in your load path and that the bson_ext and mongo gems are of the same version.\n" end require 'bson/types/binary' diff --git a/lib/bson/bson_java.rb b/lib/bson/bson_java.rb new file mode 100644 index 0000000..67ff8e7 --- /dev/null +++ b/lib/bson/bson_java.rb @@ -0,0 +1,30 @@ +include Java +module BSON + class BSON_JAVA + + def self.serialize(obj, check_keys=false, move_id=false) + raise InvalidDocument, "BSON_JAVA.serialize takes a Hash" unless obj.is_a?(Hash) + enc = get_encoder# Java::OrgJbson::RubyBSONEncoder.new(JRuby.runtime) + ByteBuffer.new(enc.encode(obj)) + end + + def self.get_encoder + @@enc ||= Java::OrgJbson::RubyBSONEncoder.new(JRuby.runtime) + end + + def self.get_decoder + @@dec ||= Java::OrgBson::BSONDecoder.new + end + + def self.deserialize(buf) + if buf.is_a? String + buf = ByteBuffer.new(buf) if buf + end + dec = get_decoder + callback = Java::OrgJbson::RubyBSONCallback.new(JRuby.runtime) + dec.decode(buf.to_s.to_java_bytes, callback) + callback.get + end + + end +end diff --git a/lib/bson/bson_ruby.rb b/lib/bson/bson_ruby.rb index a234d15..32d704e 100644 --- a/lib/bson/bson_ruby.rb +++ b/lib/bson/bson_ruby.rb @@ -44,6 +44,7 @@ module BSON def initialize @buf = ByteBuffer.new + @encoder = BSON_RUBY end if RUBY_VERSION >= '1.9' @@ -303,7 +304,7 @@ module BSON def deserialize_object_data(buf) size = buf.get_int buf.position -= 4 - object = BSON_CODER.new().deserialize(buf.get(size)) + object = @encoder.new().deserialize(buf.get(size)) if object.has_key? "$ref" DBRef.new(object["$ref"], object["$id"]) else @@ -358,7 +359,7 @@ module BSON scope_size = buf.get_int buf.position -= 4 - scope = BSON_CODER.new().deserialize(buf.get(scope_size)) + scope = @encoder.new().deserialize(buf.get(scope_size)) Code.new(encoded_str(code), scope) end @@ -452,7 +453,7 @@ module BSON def serialize_object_element(buf, key, val, check_keys, opcode=OBJECT) buf.put(opcode) self.class.serialize_key(buf, key) - buf.put_array(BSON_CODER.new.serialize(val, check_keys).to_a) + buf.put_array(@encoder.new.serialize(val, check_keys).to_a) end def serialize_array_element(buf, key, val, check_keys) @@ -527,9 +528,9 @@ module BSON len_pos = buf.position buf.put_int(0) - buf.put_int(val.length + 1) - self.class.serialize_cstr(buf, val) - buf.put_array(BSON_CODER.new.serialize(val.scope).to_a) + buf.put_int(val.code.length + 1) + self.class.serialize_cstr(buf, val.code) + buf.put_array(@encoder.new.serialize(val.scope).to_a) end_pos = buf.position buf.put_int(end_pos - len_pos, len_pos) diff --git a/lib/bson/types/code.rb b/lib/bson/types/code.rb index ca5a77a..60d6a99 100644 --- a/lib/bson/types/code.rb +++ b/lib/bson/types/code.rb @@ -19,10 +19,10 @@ module BSON # JavaScript code to be evaluated by MongoDB. - class Code < String + class Code # Hash mapping identifiers to their values - attr_accessor :scope + attr_accessor :scope, :code # Wrap code to be evaluated by MongoDB. # @@ -30,9 +30,22 @@ module BSON # @param [Hash] a document mapping identifiers to values, which # represent the scope in which the code is to be executed. def initialize(code, scope={}) - super(code) + @code = code @scope = scope end + def length + @code.length + end + + def ==(other) + self.class == other.class && + @code == other.code && @scope == other.scope + end + + def inspect + "" + end + end end diff --git a/lib/bson/types/dbref.rb b/lib/bson/types/dbref.rb index ca0924b..e417844 100644 --- a/lib/bson/types/dbref.rb +++ b/lib/bson/types/dbref.rb @@ -38,5 +38,9 @@ module BSON "ns: #{namespace}, id: #{object_id}" end + def to_hash + {"$ns" => @namespace, "$id" => @object_id } + end + end end diff --git a/test/collection_test.rb b/test/collection_test.rb index f4c235b..0b868e6 100644 --- a/test/collection_test.rb +++ b/test/collection_test.rb @@ -477,7 +477,7 @@ class TestCollection < Test::Unit::TestCase def test_saving_dates_pre_epoch begin @@test.save({'date' => Time.utc(1600)}) - assert_in_delta Time.utc(1600), @@test.find_one()["date"], 0.001 + assert_in_delta Time.utc(1600), @@test.find_one()["date"], 2 rescue ArgumentError # See note in test_date_before_epoch (BSONTest) end diff --git a/test/db_api_test.rb b/test/db_api_test.rb index 905b7b4..f5e3203 100644 --- a/test/db_api_test.rb +++ b/test/db_api_test.rb @@ -631,6 +631,7 @@ class DBAPITest < Test::Unit::TestCase assert_equal("mike", @@coll.find_one()["hello"]) end + if !RUBY_PLATFORM =~ /java/ def test_invalid_key_names @@coll.remove @@ -640,6 +641,7 @@ class DBAPITest < Test::Unit::TestCase assert_raise BSON::InvalidKeyName do @@coll.insert({"$hello" => "world"}) end + assert_raise BSON::InvalidKeyName do @@coll.insert({"hello" => {"$hello" => "world"}}) end @@ -666,6 +668,7 @@ class DBAPITest < Test::Unit::TestCase @@coll.insert({"hello" => {"hel.lo" => "world"}}) end end + end def test_collection_names assert_raise TypeError do diff --git a/test/mongo_bson/bson_test.rb b/test/mongo_bson/bson_test.rb index d0ce78e..fe6f514 100644 --- a/test/mongo_bson/bson_test.rb +++ b/test/mongo_bson/bson_test.rb @@ -322,7 +322,7 @@ class BSONTest < Test::Unit::TestCase assert_equal Binary::SUBTYPE_USER_DEFINED, bin2.subtype end - def test_binary_binary_subtype_0 + def test_binary_subtype_0 bin = Binary.new([1, 2, 3, 4, 5], Binary::SUBTYPE_SIMPLE) doc = {'bin' => bin} diff --git a/test/mongo_bson/jbson_test.rb b/test/mongo_bson/jbson_test.rb new file mode 100644 index 0000000..fdcf8b0 --- /dev/null +++ b/test/mongo_bson/jbson_test.rb @@ -0,0 +1,521 @@ +# encoding:utf-8 +require './test/test_helper' +require 'complex' +require 'bigdecimal' +require 'rational' + +begin + require 'active_support/core_ext' + Time.zone = "Pacific Time (US & Canada)" + Zone = Time.zone.now +rescue LoadError + warn 'Mocking time with zone' + module ActiveSupport + class TimeWithZone + end + end + Zone = ActiveSupport::TimeWithZone.new +end + +class BSONTest < Test::Unit::TestCase + + include BSON + + def setup + @encoder = BSON::BSON_JAVA + @decoder = BSON::BSON_RUBY#BSON::BSON_JAVA + end + + def assert_doc_pass(doc, options={}) + bson = @encoder.serialize(doc) + if options[:debug] + puts "DEBUGGIN DOC:" + p bson.to_a + puts "DESERIALIZES TO:" + p @decoder.deserialize(bson) + end + assert_equal @decoder.serialize(doc).to_a, bson.to_a + assert_equal doc, @decoder.deserialize(bson) + end + + def test_require_hash + assert_raise_error InvalidDocument, "takes a Hash" do + BSON.serialize('foo') + end + + assert_raise_error InvalidDocument, "takes a Hash" do + BSON.serialize(Object.new) + end + + assert_raise_error InvalidDocument, "takes a Hash" do + BSON.serialize(Set.new) + end + end + + def test_string + doc = {'doc' => 'hello, world'} + assert_doc_pass(doc) + end + + def test_valid_utf8_string + doc = {'doc' => 'aé'} + assert_doc_pass(doc) + end + + def test_valid_utf8_key + doc = {'aé' => 'hello'} + assert_doc_pass(doc) + end + + def test_document_length + doc = {'name' => 'a' * 5 * 1024 * 1024} + assert_raise InvalidDocument do + assert @encoder.serialize(doc) + end + end + + # In 1.8 we test that other string encodings raise an exception. + # In 1.9 we test that they get auto-converted. + if RUBY_VERSION < '1.9' + require 'iconv' + def test_invalid_string + string = Iconv.conv('iso-8859-1', 'utf-8', 'aé') + doc = {'doc' => string} + assert_raise InvalidStringEncoding do + @encoder.serialize(doc) + end + end + + def test_invalid_key + key = Iconv.conv('iso-8859-1', 'utf-8', 'aé') + doc = {key => 'hello'} + assert_raise InvalidStringEncoding do + @encoder.serialize(doc) + end + end + else + def test_non_utf8_string + bson = BSON::BSON_CODER.serialize({'str' => 'aé'.encode('iso-8859-1')}) + result = BSON::BSON_CODER.deserialize(bson)['str'] + assert_equal 'aé', result + assert_equal 'UTF-8', result.encoding.name + end + + def test_non_utf8_key + bson = BSON::BSON_CODER.serialize({'aé'.encode('iso-8859-1') => 'hello'}) + assert_equal 'hello', BSON::BSON_CODER.deserialize(bson)['aé'] + end + + # Based on a test from sqlite3-ruby + def test_default_internal_is_honored + before_enc = Encoding.default_internal + + str = "壁に耳あり、障子に目あり" + bson = BSON::BSON_CODER.serialize("x" => str) + + Encoding.default_internal = 'EUC-JP' + out = BSON::BSON_CODER.deserialize(bson)["x"] + + assert_equal Encoding.default_internal, out.encoding + assert_equal str.encode('EUC-JP'), out + assert_equal str, out.encode(str.encoding) + ensure + Encoding.default_internal = before_enc + end + end + + def test_code + doc = {'$where' => Code.new('this.a.b < this.b')} + assert_doc_pass(doc) + end + + def test_code_with_scope + doc = {'$where' => Code.new('this.a.b < this.b', {'foo' => 1})} + assert_doc_pass(doc) + end + + def test_double + doc = {'doc' => 41.25} + assert_doc_pass(doc) + end + + def test_int + doc = {'doc' => 42} + assert_doc_pass(doc) + + doc = {"doc" => -5600} + assert_doc_pass(doc) + + doc = {"doc" => 2147483647} + assert_doc_pass(doc) + + doc = {"doc" => -2147483648} + assert_doc_pass(doc) + end + + def test_ordered_hash + doc = BSON::OrderedHash.new + doc["b"] = 1 + doc["a"] = 2 + doc["c"] = 3 + doc["d"] = 4 + assert_doc_pass(doc) + end + + def test_object + doc = {'doc' => {'age' => 42, 'name' => 'Spongebob', 'shoe_size' => 9.5}} + assert_doc_pass(doc) + bson = BSON::BSON_CODER.serialize(doc) + end + + def test_oid + doc = {'doc' => ObjectId.new} + assert_doc_pass(doc) + end + + def test_array + doc = {'doc' => [1, 2, 'a', 'b']} + assert_doc_pass(doc) + end + + def test_regex + doc = {'doc' => /foobar/i} + assert_doc_pass(doc) + end + + def test_boolean + doc = {'doc' => true} + assert_doc_pass(doc) + end + + def test_date + doc = {'date' => Time.now} + bson = @encoder.serialize(doc) + doc2 = @decoder.deserialize(bson) + # Mongo only stores up to the millisecond + assert_in_delta doc['date'], doc2['date'], 0.001 + end + + def test_date_returns_as_utc + doc = {'date' => Time.now} + bson = @encoder.serialize(doc) + doc2 = @decoder.deserialize(bson) + assert doc2['date'].utc? + end + + def test_date_before_epoch + begin + doc = {'date' => Time.utc(1600)} + bson = @encoder.serialize(doc) + doc2 = @decoder.deserialize(bson) + # Mongo only stores up to the millisecond + assert_in_delta doc['date'], doc2['date'], 2 + rescue ArgumentError + # some versions of Ruby won't let you create pre-epoch Time instances + # + # TODO figure out how that will work if somebady has saved data + # w/ early dates already and is just querying for it. + end + end + + def test_exeption_on_using_unsupported_date_class + [DateTime.now, Date.today, Zone].each do |invalid_date| + doc = {:date => invalid_date} + begin + bson = BSON::BSON_CODER.serialize(doc) + rescue => e + ensure + if !invalid_date.is_a? Time + assert_equal InvalidDocument, e.class + assert_match /UTC Time/, e.message + end + end + end + end + + def test_dbref + oid = ObjectId.new + doc = {} + doc['dbref'] = DBRef.new('namespace', oid) + bson = @encoder.serialize(doc) + doc2 = @decoder.deserialize(bson) + if RUBY_PLATFORM =~ /java/ + assert_equal 'namespace', doc2['dbref']['$ns'] + assert_equal oid, doc2['dbref']['$id'] + else + assert_equal 'namespace', doc2['dbref'].namespace + assert_equal oid, doc2['dbref'].object_id + end + end + + def test_symbol + doc = {'sym' => :foo} + bson = @encoder.serialize(doc) + doc2 = @decoder.deserialize(bson) + assert_equal :foo, doc2['sym'] + end + + def test_binary + bin = Binary.new + 'binstring'.each_byte { |b| bin.put(b) } + + doc = {'bin' => bin} + bson = @encoder.serialize(doc) + doc2 = @decoder.deserialize(bson) + bin2 = doc2['bin'] + assert_kind_of Binary, bin2 + assert_equal 'binstring', bin2.to_s + assert_equal Binary::SUBTYPE_BYTES, bin2.subtype + end + + def test_binary_with_string + b = Binary.new('somebinarystring') + doc = {'bin' => b} + bson = @encoder.serialize(doc) + doc2 = @decoder.deserialize(bson) + bin2 = doc2['bin'] + assert_kind_of Binary, bin2 + assert_equal 'somebinarystring', bin2.to_s + assert_equal Binary::SUBTYPE_BYTES, bin2.subtype + end + + def test_binary_type + bin = Binary.new([1, 2, 3, 4, 5], Binary::SUBTYPE_USER_DEFINED) + + doc = {'bin' => bin} + bson = @encoder.serialize(doc) + doc2 = @decoder.deserialize(bson) + bin2 = doc2['bin'] + assert_kind_of Binary, bin2 + assert_equal [1, 2, 3, 4, 5], bin2.to_a + assert_equal Binary::SUBTYPE_USER_DEFINED, bin2.subtype + end + + # Java doesn't support this yet + if !(RUBY_PLATFORM =~ /java/) + def test_binary_subtype_0 + bin = Binary.new([1, 2, 3, 4, 5], Binary::SUBTYPE_SIMPLE) + + doc = {'bin' => bin} + bson = @encoder.serialize(doc) + doc2 = @decoder.deserialize(bson) + bin2 = doc2['bin'] + assert_kind_of Binary, bin2 + assert_equal [1, 2, 3, 4, 5], bin2.to_a + assert_equal Binary::SUBTYPE_SIMPLE, bin2.subtype + end + end + + def test_binary_byte_buffer + bb = Binary.new + 5.times { |i| bb.put(i + 1) } + + doc = {'bin' => bb} + bson = @encoder.serialize(doc) + doc2 = @decoder.deserialize(bson) + bin2 = doc2['bin'] + assert_kind_of Binary, bin2 + assert_equal [1, 2, 3, 4, 5], bin2.to_a + assert_equal Binary::SUBTYPE_BYTES, bin2.subtype + end + + def test_put_id_first + val = BSON::OrderedHash.new + val['not_id'] = 1 + val['_id'] = 2 + roundtrip = @decoder.deserialize(@encoder.serialize(val, false, true).to_s) + assert_kind_of BSON::OrderedHash, roundtrip + assert_equal '_id', roundtrip.keys.first + + val = {'a' => 'foo', 'b' => 'bar', :_id => 42, 'z' => 'hello'} + roundtrip = @decoder.deserialize(@encoder.serialize(val, false, true).to_s) + assert_kind_of BSON::OrderedHash, roundtrip + assert_equal '_id', roundtrip.keys.first + end + + def test_nil_id + doc = {"_id" => nil} + assert_doc_pass(doc) + end + + def test_timestamp + val = {"test" => [4, 20]} + assert_equal val, @decoder.deserialize([0x13, 0x00, 0x00, 0x00, + 0x11, 0x74, 0x65, 0x73, + 0x74, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x14, 0x00, + 0x00, 0x00, 0x00]) + end + + def test_overflow + doc = {"x" => 2**75} + assert_raise RangeError do + bson = @encoder.serialize(doc) + end + + doc = {"x" => 9223372036854775} + assert_doc_pass(doc) + + doc = {"x" => 9223372036854775807} + assert_doc_pass(doc) + + doc["x"] = doc["x"] + 1 + assert_raise RangeError do + bson = @encoder.serialize(doc) + end + + doc = {"x" => -9223372036854775} + assert_doc_pass(doc) + + doc = {"x" => -9223372036854775808} + assert_doc_pass(doc) + + doc["x"] = doc["x"] - 1 + assert_raise RangeError do + bson = BSON::BSON_CODER.serialize(doc) + end + end + + def test_invalid_numeric_types + [BigDecimal.new("1.0"), Complex(0, 1), Rational(2, 3)].each do |type| + doc = {"x" => type} + begin + @encoder.serialize(doc) + rescue => e + ensure + assert_equal InvalidDocument, e.class + assert_match /Cannot serialize/, e.message + end + end + end + + def test_do_not_change_original_object + val = BSON::OrderedHash.new + val['not_id'] = 1 + val['_id'] = 2 + assert val.keys.include?('_id') + @encoder.serialize(val) + assert val.keys.include?('_id') + + val = {'a' => 'foo', 'b' => 'bar', :_id => 42, 'z' => 'hello'} + assert val.keys.include?(:_id) + @encoder.serialize(val) + assert val.keys.include?(:_id) + end + + # note we only test for _id here because in the general case we will + # write duplicates for :key and "key". _id is a special case because + # we call has_key? to check for it's existence rather than just iterating + # over it like we do for the rest of the keys. thus, things like + # HashWithIndifferentAccess can cause problems for _id but not for other + # keys. rather than require rails to test with HWIA directly, we do this + # somewhat hacky test. + def test_no_duplicate_id + dup = {"_id" => "foo", :_id => "foo"} + one = {"_id" => "foo"} + + assert_equal @encoder.serialize(one).to_a, @encoder.serialize(dup).to_a + end + + def test_no_duplicate_id_when_moving_id + dup = {"_id" => "foo", :_id => "foo"} + one = {:_id => "foo"} + + assert_equal @encoder.serialize(one, false, true).to_s, @encoder.serialize(dup, false, true).to_s + end + + def test_null_character + doc = {"a" => "\x00"} + + assert_doc_pass(doc) + + assert_raise InvalidDocument do + @encoder.serialize({"\x00" => "a"}) + end + + assert_raise InvalidDocument do + @encoder.serialize({"a" => (Regexp.compile "ab\x00c")}) + end + end + + def test_max_key + doc = {"a" => MaxKey.new} + assert_doc_pass(doc) + end + + def test_min_key + doc = {"a" => MinKey.new} + assert_doc_pass(doc) + end + + def test_invalid_object + o = Object.new + assert_raise InvalidDocument do + @encoder.serialize({:foo => o}) + end + + assert_raise InvalidDocument do + @encoder.serialize({:foo => Date.today}) + end + end + + def test_move_id + a = BSON::OrderedHash.new + a['text'] = 'abc' + a['key'] = 'abc' + a['_id'] = 1 + + + assert_equal ")\000\000\000\020_id\000\001\000\000\000\002text" + + "\000\004\000\000\000abc\000\002key\000\004\000\000\000abc\000\000", + @encoder.serialize(a, false, true).to_s + + # Java doesn't support this. Isn't actually necessary. + if !(RUBY_PLATFORM =~ /java/) + assert_equal ")\000\000\000\002text\000\004\000\000\000abc\000\002key" + + "\000\004\000\000\000abc\000\020_id\000\001\000\000\000\000", + @encoder.serialize(a, false, false).to_s + end + end + + def test_move_id_with_nested_doc + b = BSON::OrderedHash.new + b['text'] = 'abc' + b['_id'] = 2 + c = BSON::OrderedHash.new + c['text'] = 'abc' + c['hash'] = b + c['_id'] = 3 + assert_equal ">\000\000\000\020_id\000\003\000\000\000\002text" + + "\000\004\000\000\000abc\000\003hash\000\034\000\000" + + "\000\002text\000\004\000\000\000abc\000\020_id\000\002\000\000\000\000\000", + @encoder.serialize(c, false, true).to_s + + # Java doesn't support this. Isn't actually necessary. + if !(RUBY_PLATFORM =~ /java/) + assert_equal ">\000\000\000\002text\000\004\000\000\000abc\000\003hash" + + "\000\034\000\000\000\002text\000\004\000\000\000abc\000\020_id" + + "\000\002\000\000\000\000\020_id\000\003\000\000\000\000", + @encoder.serialize(c, false, false).to_s + end + end + + # Mocking this class for testing + class ::HashWithIndifferentAccess < Hash; end + + def test_keep_id_with_hash_with_indifferent_access + doc = HashWithIndifferentAccess.new + embedded = HashWithIndifferentAccess.new + embedded['_id'] = ObjectId.new + doc['_id'] = ObjectId.new + doc['embedded'] = [embedded] + @encoder.serialize(doc, false, true).to_a + assert doc.has_key?("_id") + assert doc['embedded'][0].has_key?("_id") + + doc['_id'] = ObjectId.new + @encoder.serialize(doc, false, true).to_a + assert doc.has_key?("_id") + end + +end