Pruning stuff from user state
[aquarium] / src / main / scala / gr / grnet / aquarium / user / UserDataSnapshot.scala
index 350e719..c9a7706 100644 (file)
@@ -38,6 +38,9 @@ package user
 
 import gr.grnet.aquarium.util.json.JsonSupport
 import gr.grnet.aquarium.logic.accounting.Policy
+import java.util.Date
+import logic.accounting.dsl.DSLAgreement
+import com.ckkloverdos.maybe.{Failed, NoVal, Maybe, Just}
 
 /**
  * Snapshot of data that are user-related.
@@ -49,18 +52,59 @@ sealed trait UserDataSnapshot[T] extends DataSnapshot[T]
 
 case class CreditSnapshot(data: Double, snapshotTime: Long) extends UserDataSnapshot[Double]
 
-case class AgreementSnapshot(data: String, snapshotTime: Long) extends UserDataSnapshot[String]
-
 case class RolesSnapshot(data: List[String], snapshotTime: Long) extends UserDataSnapshot[List[String]]
 
-// TODO: Check if needed
-case class PaymentOrdersSnapshot(data: List[AnyRef], snapshotTime: Long) extends UserDataSnapshot[List[AnyRef]]
+/**
+ * Represents an agreement valid for a specific amount of time. By convention,
+ * if an agreement is currently valid, then the validTo field is equal to -1.
+ */
+case class Agreement(agreement: String, validFrom: Long, validTo: Long) {
+  if (validTo != -1) assert(validTo > validFrom)
+  assert(!agreement.isEmpty)
+
+//  Policy.policy(new Date(validFrom)) match {
+//    case Just(x) => x.findAgreement(agreement) match {
+//      case None => assert(false)
+//      case _ =>
+//    }
+//    case _ => assert(false)
+//  }
+}
 
-// TODO: Check if needed
-case class OwnedGroupsSnapshot(data: List[String], snapshotTime: Long) extends UserDataSnapshot[List[String]]
+/**
+ * All user agreements. The provided list of agreements cannot have time gaps. This
+ * is checked at object creation type.
+ */
+case class AgreementSnapshot(data: List[Agreement], snapshotTime: Long)
+  extends UserDataSnapshot[List[Agreement]] {
+
+  ensureNoGaps(data.sortWith((a,b) => if (b.validFrom > a.validFrom) true else false))
+
+  def ensureNoGaps(agreements: List[Agreement]): Unit = agreements match {
+    case ha :: (t @ (hb :: tail)) =>
+      assert(ha.validTo - hb.validFrom == 1);
+      ensureNoGaps(t)
+    case h :: Nil =>
+      assert(h.validTo == -1)
+    case Nil => ()
+  }
 
-// TODO: Check if needed
-case class GroupMembershipsSnapshot(data: List[String], snapshotTime: Long) extends UserDataSnapshot[List[String]]
+  /**
+   * Get the user agreement at the specified timestamp
+   */
+  def getAgreement(at: Long): Maybe[DSLAgreement] =
+    data.find{ x => x.validFrom < at && x.validTo > at} match {
+      case Some(x) => Policy.policy(new Date(at)) match {
+        case Just(y) =>  y.findAgreement(x.agreement) match {
+          case Some(z) => Just(z)
+          case None => NoVal
+        }
+        case NoVal => NoVal
+        case failed @ Failed(x, y) => failed
+      }
+      case None => NoVal
+    }
+}
 
 /**
  * Maintains the current state of a resource instance owned by the user.
@@ -78,17 +122,31 @@ case class GroupMembershipsSnapshot(data: List[String], snapshotTime: Long) exte
  *  - If the resource is simple,  the (name, instanceId) is (DSLResource.name, "1")
  *
  */
-case class ResourceInstanceSnapshot(
-    name: String,
-    instanceId: String,
-    data: Double,
-    snapshotTime: Long)
+case class ResourceInstanceSnapshot(/**
+                                     * Same as `resource` of [[gr.grnet.aquarium.logic.events.ResourceEvent]]
+                                     */
+                                    resource: String,
+
+                                    /**
+                                     * Same as `instanceId` of [[gr.grnet.aquarium.logic.events.ResourceEvent]]
+                                     */
+                                    instanceId: String,
+                                    data: Double, // FIXME: data is a missleading name here
+                                    snapshotTime: Long)
   extends UserDataSnapshot[Double] {
 
-  def value = data
+  /**
+   * This is the amount kept for the resource instance.
+   *
+   * The general rule is that an amount saved in a [[gr.grnet.aquarium.user.ResourceInstanceSnapshot]]  represents a
+   * total value, while a value appearing in a [[gr.grnet.aquarium.logic.events.ResourceEvent]] represents a
+   * difference. How these two values are combined to form the new amount is dictated by the underlying
+   * [[gr.grnet.aquarium.logic.accounting.dsl.DSLCostPolicy]]
+   */
+  def instanceAmount = data
   
-  def isSameResource(name: String, instanceId: String) = {
-    this.name == name &&
+  def isSameResourceInstance(resource: String, instanceId: String) = {
+    this.resource == resource &&
     this.instanceId == instanceId
   }
 }
@@ -98,10 +156,10 @@ case class ResourceInstanceSnapshot(
  * This representation is convenient for computations and updating, while the
  * [[gr.grnet.aquarium.user.OwnedResourcesSnapshot]] representation is convenient for JSON serialization.
  */
-class OwnedResourcesMap(map: Map[(String, String), (Double, Long)]) {
+class OwnedResourcesMap(resourcesMap: Map[(String, String), (Double, Long)]) {
   def toResourcesSnapshot(snapshotTime: Long): OwnedResourcesSnapshot =
     OwnedResourcesSnapshot(
-      map map {
+      resourcesMap map {
         case ((name, instanceId), (value, snapshotTime)) ⇒
           ResourceInstanceSnapshot(name, instanceId, value, snapshotTime
       )} toList,
@@ -113,39 +171,50 @@ case class OwnedResourcesSnapshot(data: List[ResourceInstanceSnapshot], snapshot
   extends UserDataSnapshot[List[ResourceInstanceSnapshot]] with JsonSupport {
 
   def toResourcesMap: OwnedResourcesMap = {
-    val tuples = for(rc <- data) yield ((rc.name, rc.instanceId), (rc.value, rc.snapshotTime))
+    val tuples = for(rc <- data) yield ((rc.resource, rc.instanceId), (rc.instanceAmount, rc.snapshotTime))
 
     new OwnedResourcesMap(Map(tuples.toSeq: _*))
   }
 
-  def findResourceSnapshot(name: String, instanceId: String): Option[ResourceInstanceSnapshot] =
-    data.find { x => name == x.name && instanceId == x.instanceId }
+  def resourceInstanceSnapshots = data
 
-  
-  def addOrUpdateResourceSnapshot(name: String,
-                                  instanceId: String,
-                                  newEventValue: Double,
-                                  snapshotTime: Long): (OwnedResourcesSnapshot, Option[ResourceInstanceSnapshot], ResourceInstanceSnapshot) = {
-
-    val newRCInstance = ResourceInstanceSnapshot(name, instanceId, newEventValue, snapshotTime)
-    val oldRCInstanceOpt = this.findResourceSnapshot(name, instanceId)
-    val newData = oldRCInstanceOpt match {
-      case Some(oldRCInstance) ⇒
-        // Need to delete the old one and add the new one
-        // FIXME: Get rid of this Policy.policy
-        val costPolicy = Policy.policy.findResource(name).get.costPolicy
-        val newValue = costPolicy.computeNewResourceInstanceValue(oldRCInstance.value, newRCInstance.value/* =newEventValue */)
-
-        newRCInstance.copy(data = newValue) :: (data.filterNot(_.isSameResource(name, instanceId)))
+  def resourceInstanceSnapshotsExcept(resource: String, instanceId: String) = {
+    // Unfortunately, we have to use a List for data, since JSON serialization is not as flexible
+    // (at least out of the box). Thus, the update is O(L), where L is the length of the data List.
+    resourceInstanceSnapshots.filterNot(_.isSameResourceInstance(resource, instanceId))
+  }
+
+  def findResourceInstanceSnapshot(resource: String, instanceId: String): Option[ResourceInstanceSnapshot] = {
+    data.find(x => resource == x.resource && instanceId == x.instanceId)
+  }
+
+  def getResourceInstanceAmount(resource: String, instanceId: String, defaultValue: Double): Double = {
+    findResourceInstanceSnapshot(resource, instanceId).map(_.instanceAmount).getOrElse(defaultValue)
+  }
+
+  def computeResourcesSnapshotUpdate(resource: String,   // resource name
+                                     instanceId: String, // resource instance id
+                                     newAmount: Double,
+                                     snapshotTime: Long): (OwnedResourcesSnapshot,
+                                                          Option[ResourceInstanceSnapshot],
+                                                          ResourceInstanceSnapshot) = {
+
+    val newResourceInstance = ResourceInstanceSnapshot(resource, instanceId, newAmount, snapshotTime)
+    val oldResourceInstanceOpt = this.findResourceInstanceSnapshot(resource, instanceId)
+
+    val newResourceInstances = oldResourceInstanceOpt match {
+      case Some(oldResourceInstance) ⇒
+        // Resource instance found, so delete the old one and add the new one
+        newResourceInstance :: resourceInstanceSnapshotsExcept(resource, instanceId)
       case None ⇒
         // Resource not found, so this is the first time and we just add the new snapshot
-        newRCInstance :: data
+        newResourceInstance :: resourceInstanceSnapshots
     }
 
-    val newOwnedResources = this.copy(data = newData, snapshotTime = snapshotTime)
+    val newOwnedResources = OwnedResourcesSnapshot(newResourceInstances, snapshotTime)
 
-    (newOwnedResources, oldRCInstanceOpt, newRCInstance)
-  }
+    (newOwnedResources, oldResourceInstanceOpt, newResourceInstance)
+ }
 }