diff --git a/ext/cbson/cbson.c b/ext/cbson/cbson.c index 5fe6dfe..e75a56e 100644 --- a/ext/cbson/cbson.c +++ b/ext/cbson/cbson.c @@ -86,6 +86,7 @@ static VALUE DBRef; static VALUE Code; static VALUE MinKey; static VALUE MaxKey; +static VALUE Timestamp; static VALUE Regexp; static VALUE OrderedHash; static VALUE InvalidKeyName; @@ -433,6 +434,17 @@ static int write_element(VALUE key, VALUE value, VALUE extra, int allow_id) { write_name_and_type(buffer, key, 0xff); break; } + if (strcmp(cls, "BSON::Timestamp") == 0) { + write_name_and_type(buffer, key, 0x11); + int seconds = FIX2INT( + rb_funcall(value, rb_intern("seconds"), 0)); + int increment = FIX2INT( + rb_funcall(value, rb_intern("increment"), 0)); + + SAFE_WRITE(buffer, (const char*)&increment, 4); + SAFE_WRITE(buffer, (const char*)&seconds, 4); + break; + } if (strcmp(cls, "DateTime") == 0 || strcmp(cls, "Date") == 0 || strcmp(cls, "ActiveSupport::TimeWithZone") == 0) { buffer_free(buffer); rb_raise(InvalidDocument, "%s is not currently supported; use a UTC Time instance instead.", cls); @@ -813,11 +825,13 @@ static VALUE get_value(const char* buffer, int* position, int type) { } case 17: { - int i; - int j; - memcpy(&i, buffer + *position, 4); - memcpy(&j, buffer + *position + 4, 4); - value = rb_ary_new3(2, LL2NUM(i), LL2NUM(j)); + int sec, inc; + VALUE argv[2]; + memcpy(&inc, buffer + *position, 4); + memcpy(&sec, buffer + *position + 4, 4); + argv[0] = INT2FIX(sec); + argv[1] = INT2FIX(inc); + value = rb_class_new_instance(2, argv, Timestamp); *position += 8; break; } @@ -940,6 +954,8 @@ void Init_cbson() { rb_require("bson/types/min_max_keys"); MinKey = rb_const_get(bson, rb_intern("MinKey")); MaxKey = rb_const_get(bson, rb_intern("MaxKey")); + rb_require("bson/types/timestamp"); + Timestamp = rb_const_get(bson, rb_intern("Timestamp")); Regexp = rb_const_get(rb_cObject, rb_intern("Regexp")); rb_require("bson/exceptions"); InvalidKeyName = rb_const_get(bson, rb_intern("InvalidKeyName")); diff --git a/ext/java/jar/jbson.jar b/ext/java/jar/jbson.jar index ffcc70f..dc5a7d8 100644 Binary files a/ext/java/jar/jbson.jar and b/ext/java/jar/jbson.jar differ diff --git a/ext/java/src/org/jbson/RubyBSONCallback.java b/ext/java/src/org/jbson/RubyBSONCallback.java index 03d2f2b..b20ec2c 100644 --- a/ext/java/src/org/jbson/RubyBSONCallback.java +++ b/ext/java/src/org/jbson/RubyBSONCallback.java @@ -32,6 +32,7 @@ public class RubyBSONCallback implements BSONCallback { private RubyModule _rbclsBinary; private RubyModule _rbclsMinKey; private RubyModule _rbclsMaxKey; + private RubyModule _rbclsTimestamp; private RubyModule _rbclsDBRef; private RubyModule _rbclsCode; private final LinkedList _stack = new LinkedList(); @@ -47,6 +48,7 @@ public class RubyBSONCallback implements BSONCallback { _rbclsCode = _lookupConstant( _runtime, "BSON::Code" ); _rbclsMinKey = _lookupConstant( _runtime, "BSON::MinKey" ); _rbclsMaxKey = _lookupConstant( _runtime, "BSON::MaxKey" ); + _rbclsTimestamp = _lookupConstant( _runtime, "BSON::Timestamp" ); _rbclsObjectId = _lookupConstant( _runtime, "BSON::ObjectId"); } @@ -250,17 +252,16 @@ public class RubyBSONCallback implements BSONCallback { _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; + args[0] = rtime; + args[1] = rinc; - RubyArray result = RubyArray.newArray( _runtime, args ); + Object result = JavaEmbedUtils.invokeMethod(_runtime, _rbclsTimestamp, "new", args, Object.class); - _put ( name , result ); + _put ( name , (RubyObject)result ); } public void gotObjectId( String name , ObjectId id ){ diff --git a/ext/java/src/org/jbson/RubyBSONEncoder.java b/ext/java/src/org/jbson/RubyBSONEncoder.java index 51263f2..5b63be9 100644 --- a/ext/java/src/org/jbson/RubyBSONEncoder.java +++ b/ext/java/src/org/jbson/RubyBSONEncoder.java @@ -384,6 +384,9 @@ public class RubyBSONEncoder extends BSONEncoder { else if ( klass.equals("BSON::MaxKey") ) { _put( MAXKEY, name ); } + else if ( klass.equals("BSON::Timestamp") ) { + putRubyTimestamp( name, (RubyObject)val ); + } else if ( klass.equals("BSON::DBRef") ) { RubyHash ref = (RubyHash)JavaEmbedUtils.invokeMethod(_runtime, val, "to_hash", new Object[] {}, Object.class); @@ -504,6 +507,18 @@ public class RubyBSONEncoder extends BSONEncoder { _buf.writeInt( ts.getTime() ); } + protected void putRubyTimestamp(String name, RubyObject ts ){ + _put( TIMESTAMP , name ); + + Number inc = (Number)JavaEmbedUtils.invokeMethod(_runtime, ts, + "increment", new Object[] {}, Object.class); + Number sec = (Number)JavaEmbedUtils.invokeMethod(_runtime, ts, + "seconds", new Object[] {}, Object.class); + + _buf.writeInt( (int)inc.longValue() ); + _buf.writeInt( (int)sec.longValue() ); + } + protected void putRubyCodeWScope( String name , RubyObject code ){ _put( CODE_W_SCOPE , name ); int temp = _buf.getPosition(); diff --git a/lib/bson.rb b/lib/bson.rb index 60504de..b0a5ba9 100644 --- a/lib/bson.rb +++ b/lib/bson.rb @@ -100,6 +100,7 @@ require 'bson/types/code' require 'bson/types/dbref' require 'bson/types/object_id' require 'bson/types/min_max_keys' +require 'bson/types/timestamp' require 'base64' require 'bson/ordered_hash' diff --git a/lib/bson/bson_ruby.rb b/lib/bson/bson_ruby.rb index 17d03a7..de3440f 100644 --- a/lib/bson/bson_ruby.rb +++ b/lib/bson/bson_ruby.rb @@ -192,6 +192,8 @@ module BSON serialize_max_key_element(@buf, k) when MINKEY serialize_min_key_element(@buf, k) + when TIMESTAMP + serialize_timestamp_element(@buf, k, v) else raise "unhandled type #{type}" end @@ -261,8 +263,7 @@ module BSON doc[key] = deserialize_code_w_scope_data(@buf) when TIMESTAMP key = deserialize_cstr(@buf) - doc[key] = [deserialize_number_int_data(@buf), - deserialize_number_int_data(@buf)] + doc[key] = deserialize_timestamp_data(@buf) when MAXKEY key = deserialize_cstr(@buf) doc[key] = MaxKey.new @@ -346,6 +347,12 @@ module BSON Regexp.new(str, opts) end + def deserialize_timestamp_data(buf) + increment = buf.get_int + seconds = buf.get_int + Timestamp.new(seconds, increment) + end + def encoded_str(str) if RUBY_VERSION >= '1.9' str.force_encoding("utf-8") @@ -510,6 +517,14 @@ module BSON self.class.serialize_key(buf, key) end + def serialize_timestamp_element(buf, key, val) + buf.put(TIMESTAMP) + self.class.serialize_key(buf, key) + + buf.put_int(val.increment) + buf.put_int(val.seconds) + end + def serialize_oid_element(buf, key, val) buf.put(OID) self.class.serialize_key(buf, key) @@ -598,6 +613,8 @@ module BSON MAXKEY when MinKey MINKEY + when Timestamp + TIMESTAMP when Numeric raise InvalidDocument, "Cannot serialize the Numeric type #{o.class} as BSON; only Fixum, Bignum, and Float are supported." when Date, DateTime diff --git a/lib/bson/types/timestamp.rb b/lib/bson/types/timestamp.rb new file mode 100644 index 0000000..0b32715 --- /dev/null +++ b/lib/bson/types/timestamp.rb @@ -0,0 +1,76 @@ +# encoding: UTF-8 + +# -- +# Copyright (C) 2008-2011 10gen Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ++ + +module BSON + + # A class for representing BSON Timestamps. The Timestamp type is used + # by MongoDB internally; thus, it should be used by application developers + # for diagnostic purposes only. + class Timestamp + include Enumerable + + attr_reader :seconds, :increment + + # Create a new BSON Timestamp. + # + # @param [Integer] seconds The number of seconds + # @param increment + def initialize(seconds, increment) + @seconds = seconds + @increment = increment + end + + def to_s + "seconds: #{seconds}, increment: #{increment}" + end + + def ==(other) + self.seconds == other.seconds && + self.increment == other.increment + end + + # This is for backward-compatibility. Timestamps in the Ruby + # driver used to deserialize as arrays. So we provide + # an equivalent interface. + # + # @deprecated + def [](index) + warn "Timestamps are no longer deserialized as arrays. If you're working " + + "with BSON Timestamp objects, see the BSON::Timestamp class. This usage will " + + "be deprecated in Ruby Driver v2.0." + if index == 0 + self.increment + elsif index == 1 + self.seconds + else + nil + end + end + + # This method exists only for backward-compatibility. + # + # @deprecated + def each + i = 0 + while i < 2 + yield self[i] + i += 1 + end + end + end +end diff --git a/test/bson/bson_test.rb b/test/bson/bson_test.rb index f178442..9e02696 100644 --- a/test/bson/bson_test.rb +++ b/test/bson/bson_test.rb @@ -410,15 +410,24 @@ class BSONTest < Test::Unit::TestCase if !(RUBY_PLATFORM =~ /java/) def test_timestamp val = {"test" => [4, 20]} - assert_equal val, @encoder.deserialize([0x13, 0x00, 0x00, 0x00, - 0x11, 0x74, 0x65, 0x73, - 0x74, 0x00, 0x04, 0x00, - 0x00, 0x00, 0x14, 0x00, - 0x00, 0x00, 0x00]) + result = @encoder.deserialize([0x13, 0x00, 0x00, 0x00, + 0x11, 0x74, 0x65, 0x73, + 0x74, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x14, 0x00, + 0x00, 0x00, 0x00]) + assert_equal 4, result["test"][0] + assert_equal 20, result["test"][1] end end + def test_timestamp_type + ts = Timestamp.new(5000, 100) + doc = {:ts => ts} + bson = @encoder.serialize(doc) + assert_equal ts, @encoder.deserialize(bson)["ts"] + end + def test_overflow doc = {"x" => 2**75} assert_raise RangeError do diff --git a/test/bson/timestamp_test.rb b/test/bson/timestamp_test.rb new file mode 100644 index 0000000..37cfec3 --- /dev/null +++ b/test/bson/timestamp_test.rb @@ -0,0 +1,24 @@ +require './test/test_helper' + +class TiumestampTest < Test::Unit::TestCase + include Mongo + + def test_timestamp_equality + t1 = Timestamp.new(5000, 200) + t2 = Timestamp.new(5000, 200) + assert_equal t2, t1 + end + + def test_implements_array_for_backward_compatibility + ts = Timestamp.new(5000, 200) + assert_equal 200, ts[0] + assert_equal 5000, ts[1] + + array = ts.map {|t| t } + assert_equal 2, array.length + + assert_equal 200, array[0] + assert_equal 5000, array[1] + end + +end