Permalink
Browse files

TimeZone support.

  • Loading branch information...
1 parent 4243ea4 commit 19719a0df799fcdef6df18cb826e9d520c6ef678 @analytically analytically committed Dec 5, 2012
View
59 salat-core/src/main/scala/com/novus/salat/JodaDateTimeZoneHelpers.scala
@@ -0,0 +1,59 @@
+package com.novus.salat
+
+import com.mongodb.casbah.commons.conversions
+import conversions.{ MongoConversionHelper, scala }
+
+import com.mongodb.casbah.commons.Imports._
+import com.mongodb.casbah.commons.Logging
+
+import org.bson.{ BSON, Transformer }
+
+import org.scala_tools.time.Imports._
+import scala.JodaDateTimeHelpers
+import org.joda.time.tz.CachedDateTimeZone
+
+object RegisterJodaTimeZoneConversionHelpers extends JodaDateTimeZoneHelpers {
+ def apply() = {
+ log.debug("Registering Joda Time Scala Conversions.")
+ super.register()
+ }
+}
+
+object DeregisterJodaTimeZoneConversionHelpers extends JodaDateTimeHelpers {
+ def apply() = {
+ log.debug("Unregistering Joda Time Scala Conversions.")
+ super.unregister()
+ }
+}
+
+trait JodaDateTimeZoneHelpers extends JodaDateTimeZoneSerializer
+
+trait JodaDateTimeZoneSerializer extends MongoConversionHelper {
+
+ private val encodeType = classOf[DateTimeZone]
+ private val encodeTypeCached = classOf[CachedDateTimeZone]
+ /** Encoding hook for MongoDB To be able to persist Joda DateTimeZone to MongoDB */
+ private val transformer = new Transformer {
+ log.trace("Encoding a Joda DateTimeZone.")
+
+ def transform(o: AnyRef): AnyRef = o match {
+ case tz: DateTimeZone => tz.getID
+ case _ => o
+ }
+ }
+
+ override def register() = {
+ log.debug("Hooking up Joda DateTimeZone serializer.")
+ /** Encoding hook for MongoDB To be able to persist Joda DateTimeZone to MongoDB */
+ BSON.addEncodingHook(encodeType, transformer)
+ BSON.addEncodingHook(encodeTypeCached, transformer)
+ super.register()
+ }
+
+ override def unregister() = {
+ log.debug("De-registering Joda DateTimeZone serializer.")
+ BSON.removeEncodingHooks(encodeType)
+ BSON.removeEncodingHooks(encodeTypeCached)
+ super.unregister()
+ }
+}
View
24 salat-core/src/main/scala/com/novus/salat/json/JSONConfig.scala
@@ -28,7 +28,7 @@ package com.novus.salat.json
import org.scala_tools.time.Imports._
import org.joda.time.DateTimeZone
import org.joda.time.format.{ DateTimeFormatter, ISODateTimeFormat }
-import java.util.Date
+import java.util.{ TimeZone, Date }
import org.json4s._
import org.json4s.native.JsonMethods._
import org.bson.types.{ BSONTimestamp, ObjectId }
@@ -38,6 +38,7 @@ object JSONConfig {
}
case class JSONConfig(dateStrategy: JSONDateStrategy = StringDateStrategy(),
+ timeZoneStrategy: JSONTimeZoneStrategy = StringTimeZoneStrategy(),
objectIdStrategy: JSONObjectIdStrategy = StrictJSONObjectIdStrategy,
bsonTimestampStrategy: JSONbsTimesampStrategy = StrictBSONTimestampStrategy,
outputNullValues: Boolean = false)
@@ -81,6 +82,16 @@ trait JSONDateStrategy {
def toDate(j: JValue) = toDateTime(j).toDate
}
+trait JSONTimeZoneStrategy {
+ def out(tz: DateTimeZone): JValue
+
+ def out(tz: TimeZone): JValue
+
+ def toDateTimeZone(j: JValue): DateTimeZone
+
+ def toTimeZone(j: JValue) = toDateTimeZone(j).toTimeZone
+}
+
case class StringDateStrategy(dateFormatter: DateTimeFormatter = JSONConfig.ISO8601) extends JSONDateStrategy {
def out(d: Date) = JString(dateFormatter.print(d.getTime))
@@ -92,6 +103,17 @@ case class StringDateStrategy(dateFormatter: DateTimeFormatter = JSONConfig.ISO8
}
}
+case class StringTimeZoneStrategy() extends JSONTimeZoneStrategy {
+ def out(tz: DateTimeZone) = JString(tz.getID)
+
+ def out(tz: TimeZone) = JString(tz.getID)
+
+ def toDateTimeZone(j: JValue) = j match {
+ case JString(s) => DateTimeZone.forID(s)
+ case x => sys.error("toDateTimeZone: unsupported input type class='%s', value='%s'".format(x.getClass.getName, x.values))
+ }
+}
+
case class TimestampDateStrategy(zone: DateTimeZone = DateTimeZone.UTC) extends JSONDateStrategy {
def out(d: Date) = JInt(d.getTime)
View
8 salat-core/src/main/scala/com/novus/salat/json/ToJValue.scala
@@ -28,14 +28,15 @@ package com.novus.salat.json
import org.json4s._
import org.json4s.native.JsonMethods._
import com.mongodb.casbah.Imports._
-import org.joda.time.DateTime
+import org.joda.time.{ DateTimeZone, DateTime }
import com.novus.salat.{ Field => SField, _ }
import com.novus.salat.util.Logging
import java.net.URL
import com.novus.salat.TypeFinder
import com.novus.salat.StringTypeHintStrategy
import scala.tools.scalap.scalax.rules.scalasig.TypeRefType
import org.bson.types.BSONTimestamp
+import org.joda.time.tz.CachedDateTimeZone
object MapToJSON extends Logging {
@@ -98,6 +99,8 @@ object ToJValue extends Logging {
case b: Boolean => JBool(b)
case d: java.util.Date => ctx.jsonConfig.dateStrategy.out(d)
case d: DateTime => ctx.jsonConfig.dateStrategy.out(d)
+ case tz: java.util.TimeZone => ctx.jsonConfig.timeZoneStrategy.out(tz)
+ case tz: DateTimeZone => ctx.jsonConfig.timeZoneStrategy.out(tz)
case o: ObjectId => ctx.jsonConfig.objectIdStrategy.out(o)
case u: java.net.URL => JString(u.toString) // might as well
case n if n == null && ctx.jsonConfig.outputNullValues => JNull
@@ -157,6 +160,7 @@ object FromJValue extends Logging {
}
case o: JObject if field.tf.isOid => deserialize(o, field.tf)
case v: JValue if field.tf.isDate || field.tf.isDateTime => deserialize(v, field.tf)
+ case tz: JValue if field.tf.isTimeZone || field.tf.isDateTimeZone => deserialize(tz, field.tf)
case o: JInt if field.tf.isDate || field.tf.isDateTime => deserialize(o, field.tf)
case o: JObject if field.tf.isBSONTimestamp => deserialize(o, field.tf)
case o: JObject => ctx.lookup(if (childType.isDefined) childType.get.symbol.path else field.typeRefType.symbol.path).fromJSON(o)
@@ -182,6 +186,8 @@ object FromJValue extends Logging {
val v = j match {
case d if tf.isDateTime => ctx.jsonConfig.dateStrategy.toDateTime(d)
case d if tf.isDate => ctx.jsonConfig.dateStrategy.toDate(d)
+ case tz if tf.isTimeZone => ctx.jsonConfig.timeZoneStrategy.toTimeZone(tz)
+ case tz if tf.isDateTimeZone => ctx.jsonConfig.timeZoneStrategy.toDateTimeZone(tz)
case oid if tf.isOid => ctx.jsonConfig.objectIdStrategy.in(oid)
case bt if tf.isBSONTimestamp => ctx.jsonConfig.bsonTimestampStrategy.in(bt)
case s: JString if tf.isChar => s.values.charAt(0)
View
6 salat-core/src/main/scala/com/novus/salat/transformers/Transformer.scala
@@ -62,6 +62,12 @@ object `package` {
case _ => false
}
+ def isJodaDateTimeZone(path: String) = path match {
+ case "org.joda.time.DateTimeZone" => true
+ case "org.scala_tools.time.TypeImports.DateTimeZone" => true
+ case _ => false
+ }
+
def isInt(path: String) = path match {
case "java.lang.Integer" => true
case "scala.Int" => true
View
27 salat-core/src/main/scala/com/novus/salat/transformers/inject/Injectors.scala
@@ -61,6 +61,9 @@ package object in extends Logging {
case TypeRefType(_, symbol, _) if isJodaDateTime(symbol.path) =>
new Transformer(symbol.path, t)(ctx) with OptionInjector with DateToJodaDateTime
+ case TypeRefType(_, symbol, _) if isJodaDateTimeZone(symbol.path) =>
+ new Transformer(symbol.path, t)(ctx) with OptionInjector with TimeZoneToJodaDateTimeZone
+
case t @ TypeRefType(prefix @ SingleType(_, esym), sym, _) if sym.path == "scala.Enumeration.Value" => {
new Transformer(prefix.symbol.path, t)(ctx) with OptionInjector with EnumInflater
}
@@ -108,6 +111,11 @@ package object in extends Logging {
val parentType = pt
}
+ case TypeRefType(_, symbol, _) if isJodaDateTimeZone(symbol.path) =>
+ new Transformer(symbol.path, t)(ctx) with TimeZoneToJodaDateTimeZone with TraversableInjector {
+ val parentType = pt
+ }
+
case t @ TypeRefType(prefix @ SingleType(_, esym), sym, _) if sym.path == "scala.Enumeration.Value" => {
new Transformer(prefix.symbol.path, t)(ctx) with EnumInflater with TraversableInjector {
val parentType = pt
@@ -168,6 +176,12 @@ package object in extends Logging {
val grater = ctx.lookup_?(symbol.path)
}
+ case TypeRefType(_, symbol, _) if isJodaDateTimeZone(symbol.path) =>
+ new Transformer(symbol.path, t)(ctx) with TimeZoneToJodaDateTimeZone with MapInjector {
+ val parentType = pt
+ val grater = ctx.lookup_?(symbol.path)
+ }
+
case t @ TypeRefType(prefix @ SingleType(_, esym), sym, _) if sym.path == "scala.Enumeration.Value" => {
new Transformer(prefix.symbol.path, t)(ctx) with EnumInflater with MapInjector {
val parentType = pt
@@ -208,6 +222,9 @@ package object in extends Logging {
case TypeRefType(_, symbol, _) if isJodaDateTime(symbol.path) =>
new Transformer(symbol.path, pt)(ctx) with DateToJodaDateTime
+ case TypeRefType(_, symbol, _) if isJodaDateTimeZone(symbol.path) =>
+ new Transformer(symbol.path, pt)(ctx) with TimeZoneToJodaDateTimeZone
+
case TypeRefType(_, symbol, _) if Types.isBitSet(symbol) =>
new Transformer(symbol.path, pt)(ctx) with BitSetInjector
@@ -282,6 +299,16 @@ package in {
}
}
+ trait TimeZoneToJodaDateTimeZone extends Transformer {
+ self: Transformer =>
+
+ override def transform(value: Any)(implicit ctx: Context): Any = value match {
+ case tz: String if tz != null => DateTimeZone.forID(tz)
+ case tz: java.util.TimeZone if tz != null => DateTimeZone.forID(tz.getID)
+ case tz: DateTimeZone => tz
+ }
+ }
+
trait BigIntInjector extends Transformer {
self: Transformer =>
View
77 salat-core/src/test/scala/com/novus/salat/test/DateTimeZoneSpec.scala
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2010 - 2012 Novus Partners, Inc. (http://www.novus.com)
+ *
+ * Module: salat-core
+ * Class: DateTimeSpec.scala
+ * Last modified: 2012-10-15 20:40:58 EDT
+ *
+ * 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.
+ *
+ * Project: http://github.com/novus/salat
+ * Wiki: http://github.com/novus/salat/wiki
+ * Mailing list: http://groups.google.com/group/scala-salat
+ * StackOverflow: http://stackoverflow.com/questions/tagged/salat
+ */
+package com.novus.salat.test
+
+import com.novus.salat._
+import com.novus.salat.test.global._
+import com.novus.salat.test.model._
+
+import com.mongodb.casbah.Imports._
+import com.mongodb.util.JSON.parse
+
+class DateTimeZoneSpec extends SalatSpec {
+ "A grater" should {
+ "support org.scala_tools.time.TypeImports.DateTimeZone" in {
+ import org.scala_tools.time.Imports._
+ val tz = DateTimeZone.forID("Europe/London")
+ val n = Prue(zone = tz)
+ val dbo: MongoDBObject = grater[Prue].asDBObject(n)
+ // log.info(MapPrettyPrinter(dbo))
+ dbo must havePair("_typeHint", "com.novus.salat.test.model.Prue")
+ dbo must havePair("brawl" -> true)
+ dbo must havePair("zone", tz)
+
+ val coll = MongoConnection()(SalatSpecDb)("scala_timezone_test_1")
+ val wr = coll.insert(dbo)
+ val n_* = grater[Prue].asObject(coll.findOne().get)
+ n_* must_== n
+ }
+
+ "support org.joda.time.DateTimeZone" in {
+ val tz = org.joda.time.DateTimeZone.forID("Europe/London")
+ val n = Prue(zone = tz)
+ val dbo: MongoDBObject = grater[Prue].asDBObject(n)
+ // log.info(MapPrettyPrinter(dbo))
+ dbo must havePair("_typeHint", "com.novus.salat.test.model.Prue")
+ dbo must havePair("brawl" -> true)
+ dbo must havePair("zone", tz)
+
+ val coll = MongoConnection()(SalatSpecDb)("scala_timezone_test_2")
+ val wr = coll.insert(dbo)
+ val n_* = grater[Prue].asObject(coll.findOne().get)
+ n_* must_== n
+ }
+
+ "support timezones parsed from JSON" in {
+ val n = Prue(zone = org.joda.time.DateTimeZone.forID("Europe/London"))
+ val json = grater[Prue].asDBObject(n).toString
+ // log.info(json)
+ val n_* = grater[Prue].asObject(parse(json).asInstanceOf[DBObject])
+ n_* must_== n
+ }
+
+ }
+
+}
View
4 salat-core/src/test/scala/com/novus/salat/test/GraterSpec.scala
@@ -44,9 +44,10 @@ class GraterSpec extends SalatSpec {
val f = true
val g = TestDate
val h = TestChar
+ val i = TestTimeZone
"case class <-> map" in {
- val aural = Aural(_id = _id, a = a, b = b, c = c, d = d, e = e, f = f, g = g, h = h)
+ val aural = Aural(_id = _id, a = a, b = b, c = c, d = d, e = e, f = f, g = g, h = h, i = i)
val map = grater[Aural].toMap(aural)
map must havePair(ctx.typeHintStrategy.typeHint, "com.novus.salat.test.model.Aural")
map must havePair("_id", _id)
@@ -58,6 +59,7 @@ class GraterSpec extends SalatSpec {
map must havePair("f", f)
map must havePair("g", g)
map must havePair("h", h)
+ map must havePair("i", i)
val aural_* = grater[Aural].fromMap(map)
aural_* must_== aural
}
View
4 salat-core/src/test/scala/com/novus/salat/test/SalatSpec.scala
@@ -27,7 +27,7 @@ package com.novus.salat.test
import com.mongodb.casbah.Imports._
import org.specs2.mutable._
import org.specs2.specification.{ Scope, Step }
-import com.novus.salat.{ BigDecimalStrategy, Context }
+import com.novus.salat.{ RegisterJodaTimeZoneConversionHelpers, BigDecimalStrategy, Context }
import com.mongodb.casbah.commons.test.CasbahMutableSpecification
trait SalatSpec extends CasbahMutableSpecification {
@@ -38,6 +38,8 @@ trait SalatSpec extends CasbahMutableSpecification {
com.mongodb.casbah.commons.conversions.scala.RegisterConversionHelpers()
com.mongodb.casbah.commons.conversions.scala.RegisterJodaTimeConversionHelpers()
+ RegisterJodaTimeZoneConversionHelpers()
+
} ^
super.is ^
Step {
View
32 salat-core/src/test/scala/com/novus/salat/test/json/TimeZoneStrategySpec.scala
@@ -0,0 +1,32 @@
+package com.novus.salat.test.json
+
+import org.specs2.mutable.Specification
+import com.novus.salat.util.Logging
+import org.joda.time.DateTimeZone
+import com.novus.salat.json.StringTimeZoneStrategy
+import org.json4s.JsonAST.{ JInt, JString }
+
+class TimeZoneStrategySpec extends Specification with Logging {
+ val z = DateTimeZone.forID("US/Eastern")
+
+ "JSON date strategy" should {
+ "string" in {
+ val s = StringTimeZoneStrategy()
+ val formatted = "America/New_York"
+ val j = JString(formatted)
+
+ "from DateTime to string" in {
+ s.out(z) must_== j
+ }
+ "from string to DateTime" in {
+ s.toDateTimeZone(j) must_== z
+ }
+ "throw an error when an unexpected date format is submitted" in {
+ s.toDateTimeZone(JString("abc")) must throwA[IllegalArgumentException]
+ }
+ "throw an error when an unexpected JSON field type is submitted" in {
+ s.toDateTimeZone(JInt(1)) must throwA[RuntimeException]
+ }
+ }
+ }
+}
View
2 salat-core/src/test/scala/com/novus/salat/test/model/TestModel.scala
@@ -169,6 +169,8 @@ abstract class UnannotatedAbstractMaud()
case class Neville(ennui: Boolean = true, asOf: DateTime = new DateTime)
+case class Prue(brawl: Boolean = true, zone: DateTimeZone = DateTimeZone.forID("Europe/London"))
+
case class Olive(awl: java.util.UUID)
case class Quentin(mire: Float)
View
5 salat-core/src/test/scala/com/novus/salat/test/model/TestModel2.scala
@@ -27,7 +27,7 @@ package com.novus.salat.test.model
import org.bson.types.ObjectId
import com.novus.salat.annotations._
-import org.joda.time.DateTime
+import org.joda.time.{ DateTimeZone, DateTime }
import org.joda.time.DateTimeConstants._
// a reboot of test model using the Nearly Anacrophonic Phonetic Alphabet
@@ -67,7 +67,8 @@ case class Aural(_id: ObjectId = new ObjectId,
e: BigInt,
f: Boolean,
g: DateTime,
- h: Char)
+ h: Char,
+ i: DateTimeZone)
trait Bdellatomy {
val a: String
View
3 salat-core/src/test/scala/com/novus/salat/test/model/useful.scala
@@ -25,7 +25,7 @@
package com.novus.salat.test.model
-import org.joda.time.DateTime
+import org.joda.time.{ DateTimeZone, DateTime }
import org.joda.time.DateTimeConstants._
object useful {
@@ -34,4 +34,5 @@ object useful {
val KaprekarsConstant = 6174
val TestDate = new DateTime(2011, DECEMBER, 28, 14, 37, 56, 8)
val TestChar = 'e'
+ val TestTimeZone = DateTimeZone.forID("Europe/London")
}
View
5 salat-util/src/main/scala/com/novus/salat/TypeMatchers.scala
@@ -29,6 +29,8 @@ import tools.scalap.scalax.rules.scalasig.{ TypeRefType, Type, Symbol }
protected[salat] object Types {
val Date = "java.util.Date"
val DateTime = Set("org.joda.time.DateTime", "org.scala_tools.time.TypeImports.DateTime")
+ val TimeZone = "java.util.TimeZone"
+ val DateTimeZone = Set("org.joda.time.DateTimeZone", "org.scala_tools.time.TypeImports.DateTimeZone")
val Oid = Set("org.bson.types.ObjectId", "com.mongodb.casbah.commons.TypeImports.ObjectId")
val BsonTimestamp = "org.bson.types.BSONTimestamp"
val SBigDecimal = classOf[SBigDecimal].getName
@@ -62,6 +64,9 @@ protected[salat] case class TypeFinder(t: TypeRefType) {
lazy val isDate = TypeMatchers.matches(t, Types.Date)
lazy val isDateTime = TypeMatchers.matches(t, Types.DateTime)
+ lazy val isTimeZone = TypeMatchers.matches(t, Types.TimeZone)
+ lazy val isDateTimeZone = TypeMatchers.matches(t, Types.DateTimeZone)
+
lazy val isChar = TypeMatchers.matches(t, classOf[Char].getName)
lazy val isFloat = TypeMatchers.matches(t, classOf[Float].getName)
lazy val isShort = TypeMatchers.matches(t, classOf[Short].getName)

0 comments on commit 19719a0

Please sign in to comment.