package models import repositories.{DailyRepository} import models.exceptions.{ConflictException, NotFoundException} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.{Future, Await} import scala.concurrent.duration._ import org.bson.types.ObjectId import java.util.Date import java.time.Instant import java.text.SimpleDateFormat import play.api.libs.json.{Json, JsValue, JsString, JsObject, JsArray} import org.bson.{BsonWriter, BsonReader, BsonType} import org.bson.codecs.{Codec, EncoderContext, DecoderContext} import org.bson.conversions.Bson import org.mongodb.scala.model.Updates case class Daily( id: Option[ObjectId], userId: ObjectId, questionId: ObjectId, content: String, usersLiked: Seq[ObjectId], createdAt: Date, updatedAt: Date ) object Daily { val dailyRepo = new DailyRepository() def createDailyAsync( userId: ObjectId, questionId: ObjectId, content: String, timeout: Int = 4 ): Daily = { val now: Date = Date.from(Instant.now()) val daily: Daily = Daily(None, userId, questionId, content, Seq.empty[ObjectId], now, now) val future: Future[Daily] = dailyRepo.insertDaily(daily) Await.result[Daily](future, timeout.seconds) } def getAllDailiesAsync(timeout: Int = 4): Seq[Daily] = { val future: Future[Seq[Daily]] = dailyRepo.getAll() Await.result[Seq[Daily]](future, timeout.seconds) } def getUserDailiesAsync(userId: ObjectId, timeout: Int = 4): Seq[Daily] = { val future: Future[Seq[Daily]] = dailyRepo.getByValue("user_id", userId) Await.result[Seq[Daily]](future, timeout.seconds) } def getUserFeedAsync(userId: ObjectId, timeout: Int = 4): Seq[Daily] = { // Sequentially waits for Future objects to complete before calling next method val result: Future[Seq[Daily]] = for { friends: Seq[ObjectId] <- User.getUserFriends(userId) feed: Seq[Daily] <- dailyRepo.getByValues[ObjectId]("user_id", friends) } yield feed Await.result[Seq[Daily]](result, timeout.seconds) } def likeAsync(dailyId: ObjectId, likerId: ObjectId, timeout: Int = 4): Unit = { val result: Future[Unit] = for { // Fetch Daily from given ID daily: Daily <- { dailyRepo.getById(dailyId).map((oDaily: Option[Daily]) => { if (oDaily.isEmpty) throw new NotFoundException("No daily with given ID.") else oDaily.get }) } // Check user has not already liked the Daily _ = if (daily.usersLiked.contains(likerId)) throw new ConflictException("User has already liked this Daily.") // Check user with given ID exists _ <- User.userExists(likerId).map((exists: Boolean) => if (!exists) throw new NotFoundException("No user with given ID.")) like: Unit <- { val updatedUsersLiked: Seq[ObjectId] = daily.usersLiked :+ likerId val update: Bson = Updates.set("usersLiked", updatedUsersLiked) dailyRepo.updateOne(dailyId, Seq(update)) } } yield like Await.result[Unit](result, timeout.seconds) } // Convert from Daily object to JSON (serializing to JSON) def toJson(daily: Daily): JsValue = { val usersLikedAsJsStrings: Seq[JsString] = daily.usersLiked.map[JsString](id => JsString(id.toString())) val dateFormat: SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") val formattedCreatedAt: String = dateFormat.format(daily.createdAt) val formattedUpdatedAt: String = dateFormat.format(daily.updatedAt) val dailyJson = Seq( "id" -> JsString(daily.id.getOrElse("").toString()), "userId" -> JsString(daily.userId.toString()), "questionId" -> JsString(daily.questionId.toString()), "content" -> JsString(daily.content), "usersLiked" -> JsArray(usersLikedAsJsStrings), "createdAt" -> JsString(formattedCreatedAt), "updatedAt" -> JsString(formattedUpdatedAt) ) Json.toJson[JsObject](JsObject(dailyJson)) } // Convert from Daily set to JSON (serializing to JSON) def toJson(dailies: Seq[Daily]): JsValue = { val dailiesJson: Seq[JsValue] = dailies.map(daily => Daily.toJson(daily)) Json.toJson[JsArray](JsArray(dailiesJson)) } def toString(daily: Daily): String = return s"Daily(${daily.id.toString()}, ${daily.userId.toString()}, ${daily.questionId.toString()}, ${daily.content}, ${daily.usersLiked.toString()})" // Codec instance for serialising/deserialising type User to or from BSON. // Implicit keyword lets Scala compiler automatically insert this into the code where it's needed. implicit val codec: Codec[Daily] = new Codec[Daily] { override def encode(writer: BsonWriter, value: Daily, encoderContext: EncoderContext): Unit = { writer.writeStartDocument() writer.writeObjectId("user_id", value.userId) writer.writeObjectId("question_id", value.questionId) writer.writeString("content", value.content) writer.writeStartArray("usersLiked") value.usersLiked.foreach(writer.writeObjectId) writer.writeEndArray() writer.writeDateTime("createdAt", value.createdAt.getTime()) writer.writeDateTime("updatedAt", value.updatedAt.getTime()) writer.writeEndDocument() } override def decode(reader: BsonReader, decoderContext: DecoderContext): Daily = { reader.readStartDocument() println("hi") val id = reader.readObjectId("_id") println(id) val userId = reader.readObjectId("user_id") val questionId = reader.readObjectId("question_id") val content = reader.readString("content") val usersLiked = { reader.readName("usersLiked") reader.readStartArray() val buffer = collection.mutable.Buffer.empty[ObjectId] while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) { buffer += reader.readObjectId() } reader.readEndArray() buffer.toSeq } val createdAt = reader.readDateTime("createdAt") val updatedAt = reader.readDateTime("updatedAt") reader.readEndDocument() val createdAtDate: Date = Date.from(Instant.ofEpochMilli(createdAt)) val updatedAtDate: Date = Date.from(Instant.ofEpochMilli(updatedAt)) Daily(Some(id), userId, questionId, content, usersLiked, createdAtDate, updatedAtDate) } override def getEncoderClass: Class[Daily] = classOf[Daily] } }