From 2a7b089a9b63df1af90264ddc75044e95c641e86 Mon Sep 17 00:00:00 2001 From: Kyle Banker Date: Thu, 30 Sep 2010 09:43:17 -0400 Subject: [PATCH] BSON for JRuby --- Rakefile | 23 + bson.gemspec | 1 + ext/java/src/org/jbson/RubyBSONCallback.class | Bin 0 -> 10226 bytes ext/java/src/org/jbson/RubyBSONCallback.java | 382 +++++++++ ext/java/src/org/jbson/RubyBSONDecoder.class | Bin 0 -> 218 bytes ext/java/src/org/jbson/RubyBSONDecoder.java | 27 + ext/java/src/org/jbson/RubyBSONEncoder.class | Bin 0 -> 17234 bytes ext/java/src/org/jbson/RubyBSONEncoder.java | 764 ++++++++++++++++++ lib/bson.rb | 49 +- lib/bson/bson_java.rb | 30 + lib/bson/bson_ruby.rb | 13 +- lib/bson/types/code.rb | 19 +- lib/bson/types/dbref.rb | 4 + test/collection_test.rb | 2 +- test/db_api_test.rb | 3 + test/mongo_bson/bson_test.rb | 2 +- test/mongo_bson/jbson_test.rb | 521 ++++++++++++ 17 files changed, 1810 insertions(+), 30 deletions(-) create mode 100644 ext/java/src/org/jbson/RubyBSONCallback.class create mode 100644 ext/java/src/org/jbson/RubyBSONCallback.java create mode 100644 ext/java/src/org/jbson/RubyBSONDecoder.class create mode 100644 ext/java/src/org/jbson/RubyBSONDecoder.java create mode 100644 ext/java/src/org/jbson/RubyBSONEncoder.class create mode 100644 ext/java/src/org/jbson/RubyBSONEncoder.java create mode 100644 lib/bson/bson_java.rb create mode 100644 test/mongo_bson/jbson_test.rb 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 0000000000000000000000000000000000000000..bca6824080a0956832713e92548fa89f3f129408 GIT binary patch literal 10226 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 ETk`$J;Q#;t literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..90dc0fc4a2caef6ed83360b459b7fdc372699803 GIT binary patch literal 218 zcmX^0Z`VEs1_mPrUM>b^1}=66ZgvJ9Mg}&U%)HDJJ4Oa(4b3n{1{UZ1lvG9rexJ;| zRKL>Pq|~C2#H1Xc2v=}^X;E^jTPBDj5>%R0=@jhm=aQNXR9~c*l~|U@!@$D8%E%y@ zUzDz&l~kOcr;nxzs1GCumO|3Zpvb@kbP)(J0wK^4Ajt;g$%6R|46It)85lQ$rP+Zb R8&H^mfgeb60BI%$P5^PyEMWiu literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0b3d96d89207e5c2f5da1f31efde326ed7119a38 GIT binary patch literal 17234 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 s(nsdu{9Kh|;vvBSONObject. + * 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