X-Git-Url: https://code.grnet.gr/git/aquarium/blobdiff_plain/517f5b14c2ae1b393971cdf7e74fb54226ad6e17..f2e3cc2beadcdfa86fb63e6b0429d958ec2adb8f:/src/main/scala/gr/grnet/aquarium/store/mongodb/MongoDBStore.scala diff --git a/src/main/scala/gr/grnet/aquarium/store/mongodb/MongoDBStore.scala b/src/main/scala/gr/grnet/aquarium/store/mongodb/MongoDBStore.scala index 1204d97..1433e48 100644 --- a/src/main/scala/gr/grnet/aquarium/store/mongodb/MongoDBStore.scala +++ b/src/main/scala/gr/grnet/aquarium/store/mongodb/MongoDBStore.scala @@ -1,5 +1,5 @@ /* - * Copyright 2011 GRNET S.A. All rights reserved. + * Copyright 2011-2012 GRNET S.A. All rights reserved. * * Redistribution and use in source and binary forms, with or * without modification, are permitted provided that the following @@ -35,245 +35,432 @@ package gr.grnet.aquarium.store.mongodb -import gr.grnet.aquarium.util.Loggable -import com.ckkloverdos.maybe.{Failed, Just, Maybe} -import com.mongodb.util.JSON -import gr.grnet.aquarium.user.UserState -import gr.grnet.aquarium.util.displayableObjectInfo -import gr.grnet.aquarium.util.json.JsonSupport -import collection.mutable.{ListBuffer} -import gr.grnet.aquarium.store._ -import gr.grnet.aquarium.logic.events.{WalletEntry, UserEvent, ResourceEvent, AquariumEvent} -import gr.grnet.aquarium.logic.events.ResourceEvent.JsonNames -import java.util.Date +import collection.immutable import com.mongodb._ +import gr.grnet.aquarium.computation.BillingMonthInfo +import gr.grnet.aquarium.converter.StdConverters +import gr.grnet.aquarium.logic.accounting.dsl.Timeslot +import gr.grnet.aquarium.message.MessageConstants +import gr.grnet.aquarium.message.avro.gen.{UserAgreementHistoryMsg, UserStateMsg, IMEventMsg, ResourceEventMsg, PolicyMsg} +import gr.grnet.aquarium.message.avro.{MessageHelpers, MessageFactory, OrderingHelpers, AvroHelpers} +import gr.grnet.aquarium.store._ +import gr.grnet.aquarium.util._ +import gr.grnet.aquarium.util.Once +import gr.grnet.aquarium.util.json.JsonSupport +import gr.grnet.aquarium.{Aquarium, AquariumException} +import org.apache.avro.specific.SpecificRecord +import org.bson.types.ObjectId /** - * Mongodb implementation of the event _store (and soon the user _store). + * Mongodb implementation of the various aquarium stores. * * @author Christos KK Loverdos * @author Georgios Gousios + * @author Prodromos Gerakios */ class MongoDBStore( + val aquarium: Aquarium, val mongo: Mongo, val database: String, val username: String, val password: String) - extends ResourceEventStore with UserStore with WalletStore with Loggable { + extends ResourceEventStore + with UserStateStore + with IMEventStore + with PolicyStore + with Loggable { + + private[store] lazy val resourceEvents = getCollection(MongoDBStore.ResourceEventCollection) + private[store] lazy val userStates = getCollection(MongoDBStore.UserStateCollection) + private[store] lazy val imEvents = getCollection(MongoDBStore.IMEventCollection) + private[store] lazy val policies = getCollection(MongoDBStore.PolicyCollection) + + private[store] lazy val indicesMap = { + val resev= new BasicDBObjectBuilder(). + add(MongoDBStore.JsonNames.id,1). + add(MongoDBStore.JsonNames.userID,1). + add(MongoDBStore.JsonNames.occurredMillis,1). + add(MongoDBStore.JsonNames.receivedMillis,1).get + val imev = new BasicDBObjectBuilder(). + add(MongoDBStore.JsonNames.userID,1). + add(MongoDBStore.JsonNames.eventType,""). + add(MongoDBStore.JsonNames.occurredMillis,1).get + val policy = new BasicDBObjectBuilder(). + add("validFromMillis",1). + add("validToMillis",1).get + val user = new BasicDBObjectBuilder(). + add( "occurredMillis",1). + add("isFullBillingMonth",false). + add("billingYear",1). + add("billingMonth",1). + add("billingMonthDay",1).get + Map(MongoDBStore.ResourceEventCollection -> resev, + MongoDBStore.IMEventCollection-> imev, + MongoDBStore.PolicyCollection-> policy, + MongoDBStore.UserStateCollection-> user + ) + } + private[this] val once = new Once() - private[store] lazy val events: DBCollection = getCollection(MongoDBStore.EVENTS_COLLECTION) - private[store] lazy val users: DBCollection = getCollection(MongoDBStore.USERS_COLLECTION) - private[store] lazy val imevents: DBCollection = getCollection(MongoDBStore.IM_EVENTS_COLLECTION) - private[store] lazy val wallets: DBCollection = getCollection(MongoDBStore.IM_WALLETS) + private[this] def doAuthenticate(db: DB) { + if(!db.isAuthenticated && !db.authenticate(username, password.toCharArray)) { + throw new AquariumException("Could not authenticate user %s".format(username)) + } + } private[this] def getCollection(name: String): DBCollection = { val db = mongo.getDB(database) - if(!db.authenticate(username, password.toCharArray)) { - throw new StoreException("Could not authenticate user %s".format(username)) + doAuthenticate(db) + once.run { /* this code is thread-safe and will run exactly once*/ + indicesMap.foreach { case (collection,obj) => + mongo.getDB(database).getCollection(collection).createIndex(obj) + } } db.getCollection(name) } - /* TODO: Some of the following methods rely on JSON (de-)serialization). - * A method based on proper object serialization would be much faster. - */ - - private[this] def _deserializeEvent[A <: AquariumEvent](a: DBObject): A = { - //TODO: Distinguish events and deserialize appropriately - ResourceEvent.fromJson(JSON.serialize(a)).asInstanceOf[A] + //+ResourceEventStore + def pingResourceEventStore(): Unit = synchronized { + getCollection(MongoDBStore.ResourceEventCollection) + MongoDBStore.ping(mongo) } - private[this] def _deserializeUserState(dbObj: DBObject): UserState = { - val jsonString = JSON.serialize(dbObj) - UserState.fromJson(jsonString) - } + def insertResourceEvent(event: ResourceEventMsg) = { + val mongoID = new ObjectId() + event.setInStoreID(mongoID.toStringMongod) - private[this] def _makeDBObject(any: JsonSupport): DBObject = { - JSON.parse(any.toJson) match { - case dbObject: DBObject ⇒ - dbObject - case _ ⇒ - throw new StoreException("Could not transform %s -> %s".format(displayableObjectInfo(any), classOf[DBObject].getName)) - } + val dbObject = new BasicDBObjectBuilder(). + add(MongoDBStore.JsonNames._id, mongoID). + add(MongoDBStore.JsonNames.payload, AvroHelpers.bytesOfSpecificRecord(event)). + add(MongoDBStore.JsonNames.userID, event.getUserID). + add(MongoDBStore.JsonNames.occurredMillis, event.getOccurredMillis). + add(MongoDBStore.JsonNames.receivedMillis, event.getReceivedMillis). + get() + + MongoDBStore.insertDBObject(dbObject, resourceEvents) + event } - private[this] def _prepareFieldQuery(name: String, value: String): DBObject = { - val dbObj = new BasicDBObject(1) - dbObj.put(name, value) - dbObj + def findResourceEventByID(id: String): Option[ResourceEventMsg] = { + val dbObjectOpt = MongoDBStore.findOneByAttribute(resourceEvents, MongoDBStore.JsonNames.id, id) + for { + dbObject ← dbObjectOpt + payload = dbObject.get(MongoDBStore.JsonNames.payload) + msg = AvroHelpers.specificRecordOfBytes(payload.asInstanceOf[Array[Byte]], new ResourceEventMsg) + } yield msg } - private[this] def _insertObject(collection: DBCollection, obj: JsonSupport): DBObject = { - val dbObj = _makeDBObject(obj) - collection insert dbObj - dbObj + def countOutOfSyncResourceEventsForBillingPeriod(userID: String, startMillis: Long, stopMillis: Long): Long = { + val query = new BasicDBObjectBuilder(). + add(MongoDBStore.JsonNames.userID, userID). + // received within the period + add(MongoDBStore.JsonNames.receivedMillis, new BasicDBObject("$gte", startMillis)). + add(MongoDBStore.JsonNames.receivedMillis, new BasicDBObject("$lte", stopMillis)). + // occurred outside the period + add("$or", { + val dbList = new BasicDBList() + dbList.add(0, new BasicDBObject(MongoDBStore.JsonNames.occurredMillis, new BasicDBObject("$lt", startMillis))) + dbList.add(1, new BasicDBObject(MongoDBStore.JsonNames.occurredMillis, new BasicDBObject("$gt", stopMillis))) + dbList + }). + get() + + resourceEvents.count(query) } - private[this] def _checkWasInserted(collection: DBCollection, obj: JsonSupport, idName: String, id: String): String = { - val cursor = collection.find(_prepareFieldQuery(idName, id)) - if (!cursor.hasNext) { - val errMsg = "Failed to _store %s".format(displayableObjectInfo(obj)) - logger.error(errMsg) - throw new StoreException(errMsg) + def foreachResourceEventOccurredInPeriod( + userID: String, + startMillis: Long, + stopMillis: Long + )(f: ResourceEventMsg ⇒ Unit): Long = { + var _counter= 0L + val query = new BasicDBObjectBuilder(). + add(MongoDBStore.JsonNames.userID, userID). + add(MongoDBStore.JsonNames.occurredMillis, new BasicDBObject("$gte", startMillis)). + add(MongoDBStore.JsonNames.occurredMillis, new BasicDBObject("$lte", stopMillis)). + get() + + val sorter = new BasicDBObject(MongoDBStore.JsonNames.occurredMillis, 1) + val cursor = resourceEvents.find(query).sort(sorter) + + withCloseable(cursor) { cursor ⇒ + while(cursor.hasNext) { + val nextDBObject = cursor.next() + val payload = nextDBObject.get(MongoDBStore.JsonNames.payload).asInstanceOf[Array[Byte]] + val nextEvent = AvroHelpers.specificRecordOfBytes(payload, new ResourceEventMsg) + + f(nextEvent) + _counter += 1 + } } - val retval = cursor.next.get("_id").toString - cursor.close() - retval + _counter + } + //-ResourceEventStore + + //+ UserStateStore + def findUserStateByUserID(userID: String) = { + val dbObjectOpt = MongoDBStore.findOneByAttribute(userStates, MongoDBStore.JsonNames.userID, userID) + for { + dbObject <- dbObjectOpt + payload = dbObject.get(MongoDBStore.JsonNames.payload).asInstanceOf[Array[Byte]] + msg = AvroHelpers.specificRecordOfBytes(payload, new UserStateMsg) + } yield { + msg + } } - private[this] def _store[A <: AquariumEvent](entry: A, col: DBCollection) : Maybe[RecordID] = { - try { - // Store - val dbObj = _makeDBObject(entry) - col.insert(dbObj) + def findLatestUserStateForFullMonthBilling(userID: String, bmi: BillingMonthInfo) = { + val query = new BasicDBObjectBuilder(). + add(MongoDBStore.JsonNames.userID, userID). + add(MongoDBStore.JsonNames.isForFullMonth, true). + add(MongoDBStore.JsonNames.billingYear, bmi.year). + add(MongoDBStore.JsonNames.billingMonth, bmi.month). + get() - // Get back to retrieve unique id - val cursor = col.find(_prepareFieldQuery(JsonNames.id, entry.id)) + // Descending order, so that the latest comes first + val sorter = new BasicDBObject(MongoDBStore.JsonNames.occurredMillis, -1) - if (!cursor.hasNext) { - cursor.close() - logger.error("Failed to _store entry: %s".format(entry)) - return Failed(new StoreException("Failed to _store entry: %s".format(entry))) - } + val cursor = userStates.find(query).sort(sorter) - val retval = Just(RecordID(cursor.next.get(JsonNames._id).toString)) - cursor.close() - retval - } catch { - case m: MongoException => - logger.error("Unknown Mongo error: %s".format(m)); Failed(m) + withCloseable(cursor) { cursor ⇒ + MongoDBStore.findNextPayloadRecord(cursor, new UserStateMsg) } } - private[this] def _findById[A <: AquariumEvent](id: String, col: DBCollection) : Option[A] = { - val q = new BasicDBObject() - q.put(JsonNames.id, id) + def findLatestUserState(userID: String) = { + val query = new BasicDBObjectBuilder(). + add(MongoDBStore.JsonNames.userID, userID). + get() - val cur = col.find(q) + // Descending order, so that the latest comes first + val sorter = new BasicDBObject(MongoDBStore.JsonNames.occurredMillis, -1) - val retval = if (cur.hasNext) - Some(_deserializeEvent(cur.next)) - else - None - - cur.close() - retval - } - - private[this] def _query[A <: AquariumEvent](q: BasicDBObject, - col: DBCollection) - (sortWith: Option[(A, A) => Boolean]): List[A] = { - val cur = col.find(q) - if (!cur.hasNext) { - cur.close() - return List() - } + val cursor = userStates.find(query).sort(sorter) - val buff = new ListBuffer[A]() + withCloseable(cursor) { cursor ⇒ + MongoDBStore.findNextPayloadRecord(cursor, new UserStateMsg) + } + } - while(cur.hasNext) - buff += _deserializeEvent(cur.next) + /** + * Stores a user state. + */ + def insertUserState(event: UserStateMsg)= { + val mongoID = new ObjectId() + event.setInStoreID(mongoID.toStringMongod) + + val dbObject = new BasicDBObjectBuilder(). + add(MongoDBStore.JsonNames._id, mongoID). + add(MongoDBStore.JsonNames.payload, AvroHelpers.bytesOfSpecificRecord(event)). + add(MongoDBStore.JsonNames.userID, event.getUserID). + add(MongoDBStore.JsonNames.occurredMillis, event.getOccurredMillis). + add(MongoDBStore.JsonNames.isForFullMonth, event.getIsForFullMonth). + add(MongoDBStore.JsonNames.billingYear, event.getBillingYear). + add(MongoDBStore.JsonNames.billingMonth, event.getBillingMonth). + add(MongoDBStore.JsonNames.billingMonthDay, event.getBillingMonthDay). + get() + + MongoDBStore.insertDBObject(dbObject, userStates) + event + } + //- UserStateStore - cur.close() - - sortWith match { - case Some(sorter) => buff.toList.sortWith(sorter) - case None => buff.toList - } + //+IMEventStore + def pingIMEventStore(): Unit = { + getCollection(MongoDBStore.IMEventCollection) + MongoDBStore.ping(mongo) } - private[this] def _sortByTimestampAsc[A <: AquariumEvent](one: A, two: A): Boolean = { - if (one.occurredMillis > two.occurredMillis) false - else if (one.occurredMillis < two.occurredMillis) true - else true + def insertIMEvent(event: IMEventMsg) = { + val mongoID = new ObjectId() + event.setInStoreID(mongoID.toStringMongod) + + val dbObject = new BasicDBObjectBuilder(). + add(MongoDBStore.JsonNames._id, mongoID). + add(MongoDBStore.JsonNames.payload, AvroHelpers.bytesOfSpecificRecord(event)). + add(MongoDBStore.JsonNames.userID, event.getUserID). + add(MongoDBStore.JsonNames.eventType, event.getEventType().toLowerCase). + add(MongoDBStore.JsonNames.occurredMillis, event.getOccurredMillis). + add(MongoDBStore.JsonNames.receivedMillis, event.getReceivedMillis). + get() + + MongoDBStore.insertDBObject(dbObject, imEvents) + event } - private[this] def _sortByTimestampDesc[A <: AquariumEvent](one: A, two: A): Boolean = { - if (one.occurredMillis < two.occurredMillis) false - else if (one.occurredMillis > two.occurredMillis) true - else true + def findIMEventByID(id: String) = { + val dbObjectOpt = MongoDBStore.findOneByAttribute(imEvents, MongoDBStore.JsonNames.id, id) + for { + dbObject ← dbObjectOpt + payload = dbObject.get(MongoDBStore.JsonNames.payload).asInstanceOf[Array[Byte]] + msg = AvroHelpers.specificRecordOfBytes(payload, new IMEventMsg) + } yield { + msg + } } - //+ResourceEventStore - def storeResourceEvent(event: ResourceEvent): Maybe[RecordID] = _store(event, events) - def findResourceEventById(id: String): Option[ResourceEvent] = _findById(id, events) + /** + * Find the `CREATE` even for the given user. Note that there must be only one such event. + */ + def findCreateIMEventByUserID(userID: String) = { + val query = new BasicDBObjectBuilder(). + add(MongoDBStore.JsonNames.userID, userID). + add(MongoDBStore.JsonNames.eventType, MessageConstants.IMEventMsg.EventTypes.create).get() - def findResourceEventsByUserId(userId: String) - (sortWith: Option[(ResourceEvent, ResourceEvent) => Boolean]): List[ResourceEvent] = { - val q = new BasicDBObject() - q.put(JsonNames.userId, userId) + // Normally one such event is allowed ... + val cursor = imEvents.find(query).sort(new BasicDBObject(MongoDBStore.JsonNames.occurredMillis, 1)) - _query(q, events)(sortWith) + val dbObjectOpt = withCloseable(cursor) { cursor ⇒ + if(cursor.hasNext) { + Some(cursor.next()) + } else { + None + } + } + + for { + dbObject <- dbObjectOpt + payload = dbObject.get(MongoDBStore.JsonNames.payload).asInstanceOf[Array[Byte]] + msg = AvroHelpers.specificRecordOfBytes(payload, new IMEventMsg) + } yield { + msg + } } - def findResourceEventsByUserIdAfterTimestamp(userId: String, timestamp: Long): List[ResourceEvent] = { - val query = new BasicDBObject() - query.put(JsonNames.userId, userId) - query.put(JsonNames.timestamp, new BasicDBObject("$gte", timestamp)) - - val sort = new BasicDBObject(JsonNames.timestamp, 1) - - val cursor = events.find(query).sort(sort) - val buffer = new scala.collection.mutable.ListBuffer[ResourceEvent] - while(cursor.hasNext) { - buffer += _deserializeEvent(cursor.next()) + /** + * Scans events for the given user, sorted by `occurredMillis` in ascending order and runs them through + * the given function `f`. + * + * Any exception is propagated to the caller. The underlying DB resources are properly disposed in any case. + */ + def foreachIMEventInOccurrenceOrder(userID: String)(f: (IMEventMsg) ⇒ Boolean) = { + val query = new BasicDBObject(MongoDBStore.JsonNames.userID, userID) + val cursor = imEvents.find(query).sort(new BasicDBObject(MongoDBStore.JsonNames.occurredMillis, 1)) + + var _shouldContinue = true + withCloseable(cursor) { cursor ⇒ + while(_shouldContinue && cursor.hasNext) { + val dbObject = cursor.next() + val payload = dbObject.get(MongoDBStore.JsonNames.payload).asInstanceOf[Array[Byte]] + val msg = AvroHelpers.specificRecordOfBytes(payload, new IMEventMsg) + + _shouldContinue = f(msg) + } } - cursor.close() + _shouldContinue + } + //-IMEventStore + + //+PolicyStore + def foreachPolicy[U](f: PolicyMsg ⇒ U) { + val cursor = policies.find() + withCloseable(cursor) { cursor ⇒ + while(cursor.hasNext) { + val dbObject = cursor.next() + val payload = dbObject.get(MongoDBStore.JsonNames.payload).asInstanceOf[Array[Byte]] + val policy = AvroHelpers.specificRecordOfBytes(payload, new PolicyMsg) + f(policy) + } + } + } - buffer.toList + def insertPolicy(policy: PolicyMsg): PolicyMsg = { + val mongoID = new ObjectId() + policy.setInStoreID(mongoID.toStringMongod) + val dbObject = new BasicDBObjectBuilder(). + add(MongoDBStore.JsonNames._id, mongoID). + add(MongoDBStore.JsonNames.validFromMillis, policy.getValidFromMillis). + add(MongoDBStore.JsonNames.validToMillis, policy.getValidToMillis). + add(MongoDBStore.JsonNames.payload, AvroHelpers.bytesOfSpecificRecord(policy)). + get() + + MongoDBStore.insertDBObject(dbObject, policies) + policy } - //-ResourceEventStore - //+UserStore - def storeUserState(userState: UserState): Maybe[RecordID] = { - Maybe { - val dbObj = _insertObject(users, userState) - val id = _checkWasInserted(users, userState, JsonNames.userId, userState.userId) - RecordID(id) - } + def loadPolicyAt(atMillis: Long): Option[PolicyMsg] = { + // FIXME Inefficient + var _policies = immutable.TreeSet[PolicyMsg]()(OrderingHelpers.DefaultPolicyMsgOrdering) + foreachPolicy(_policies += _) + _policies.to(MessageFactory.newDummyPolicyMsgAt(atMillis)).lastOption + } + + def loadSortedPoliciesWithin(fromMillis: Long, toMillis: Long): immutable.SortedMap[Timeslot, PolicyMsg] = { + // FIXME Inefficient + var _policies = immutable.TreeSet[PolicyMsg]()(OrderingHelpers.DefaultPolicyMsgOrdering) + foreachPolicy(_policies += _) + + immutable.SortedMap(_policies. + from(MessageFactory.newDummyPolicyMsgAt(fromMillis)). + to(MessageFactory.newDummyPolicyMsgAt(toMillis)).toSeq. + map(p ⇒ (Timeslot(p.getValidFromMillis, p.getValidToMillis), p)): _* + ) } + //-PolicyStore +} + +object MongoDBStore { + final val JsonNames = gr.grnet.aquarium.util.json.JsonNames - def findUserStateByUserId(userId: String): Maybe[UserState] = { - Maybe { - val queryObj = _prepareFieldQuery(JsonNames.userId, userId) - val cursor = events.find(queryObj) + final val collections = List("resevents","userstates","imevents","policies") - if(!cursor.hasNext) { - cursor.close() - null + final val ResourceEventCollection = collections(0) + + final val UserStateCollection = collections(1) + + final val IMEventCollection = collections(2) + + final val PolicyCollection = collections(3) + + def firstResultIfExists[A](cursor: DBCursor, f: DBObject ⇒ A): Option[A] = { + withCloseable(cursor) { cursor ⇒ + if(cursor.hasNext) { + Some(f(cursor.next())) } else { - val userState = _deserializeUserState(cursor.next()) - cursor.close() - userState + None } } } - //-UserStore - //+WalletStore - def store(entry: WalletEntry): Maybe[RecordID] = _store(entry, wallets) - - def findEntryById(id: String): Option[WalletEntry] = _findById[WalletEntry](id, wallets) + def ping(mongo: Mongo): Unit = synchronized { + // This requires a network roundtrip + mongo.isLocked + } - def findAllUserEntries(userId: String) = findUserEntriesFromTo(userId, new Date(0), new Date(Int.MaxValue)) + def findOneByAttribute( + collection: DBCollection, + attributeName: String, + attributeValue: String, + sortByOpt: Option[DBObject] = None + ): Option[DBObject] = { + val query = new BasicDBObject(attributeName, attributeValue) + val cursor = sortByOpt match { + case None ⇒ collection find query + case Some(sortBy) ⇒ collection find query sort sortBy + } + withCloseable(cursor) { cursor ⇒ + if(cursor.hasNext) Some(cursor.next()) else None + } + } - def findUserEntriesFromTo(userId: String, from: Date, to: Date) : List[WalletEntry] = { - val q = new BasicDBObject() - q.put(JsonNames.timestamp, new BasicDBObject("$gt", from.getTime)) - q.put(JsonNames.timestamp, new BasicDBObject("$lt", to.getTime)) - q.put(JsonNames.userId, userId) + def insertDBObject(dbObj: DBObject, collection: DBCollection) { + collection.insert(dbObj, WriteConcern.JOURNAL_SAFE) + } - _query[WalletEntry](q, wallets)(Some(_sortByTimestampAsc)) + def findNextPayloadRecord[R <: SpecificRecord](cursor: DBCursor, fresh: R): Option[R] = { + for { + dbObject <- if(cursor.hasNext) Some(cursor.next()) else None + payload = dbObject.get(MongoDBStore.JsonNames.payload).asInstanceOf[Array[Byte]] + msg = AvroHelpers.specificRecordOfBytes(payload, fresh) + } yield { + msg + } } - //-WalletStore -} -object MongoDBStore { - def EVENTS_COLLECTION = "events" - def USERS_COLLECTION = "users" - def IM_EVENTS_COLLECTION = "imevents" - def IM_WALLETS = "wallets" + def jsonSupportToDBObject(jsonSupport: JsonSupport) = { + StdConverters.AllConverters.convertEx[DBObject](jsonSupport) + } }