Pruning stuff from user state
[aquarium] / src / main / scala / gr / grnet / aquarium / user / UserDataSnapshot.scala
1 /*
2  * Copyright 2011 GRNET S.A. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or
5  * without modification, are permitted provided that the following
6  * conditions are met:
7  *
8  *   1. Redistributions of source code must retain the above
9  *      copyright notice, this list of conditions and the following
10  *      disclaimer.
11  *
12  *   2. Redistributions in binary form must reproduce the above
13  *      copyright notice, this list of conditions and the following
14  *      disclaimer in the documentation and/or other materials
15  *      provided with the distribution.
16  *
17  * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
18  * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
21  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28  * POSSIBILITY OF SUCH DAMAGE.
29  *
30  * The views and conclusions contained in the software and
31  * documentation are those of the authors and should not be
32  * interpreted as representing official policies, either expressed
33  * or implied, of GRNET S.A.
34  */
35
36 package gr.grnet.aquarium
37 package user
38
39 import gr.grnet.aquarium.util.json.JsonSupport
40 import gr.grnet.aquarium.logic.accounting.Policy
41 import java.util.Date
42 import logic.accounting.dsl.DSLAgreement
43 import com.ckkloverdos.maybe.{Failed, NoVal, Maybe, Just}
44
45 /**
46  * Snapshot of data that are user-related.
47  *
48  * @author Christos KK Loverdos <loverdos@gmail.com>
49  */
50
51 sealed trait UserDataSnapshot[T] extends DataSnapshot[T]
52
53 case class CreditSnapshot(data: Double, snapshotTime: Long) extends UserDataSnapshot[Double]
54
55 case class RolesSnapshot(data: List[String], snapshotTime: Long) extends UserDataSnapshot[List[String]]
56
57 /**
58  * Represents an agreement valid for a specific amount of time. By convention,
59  * if an agreement is currently valid, then the validTo field is equal to -1.
60  */
61 case class Agreement(agreement: String, validFrom: Long, validTo: Long) {
62   if (validTo != -1) assert(validTo > validFrom)
63   assert(!agreement.isEmpty)
64
65 //  Policy.policy(new Date(validFrom)) match {
66 //    case Just(x) => x.findAgreement(agreement) match {
67 //      case None => assert(false)
68 //      case _ =>
69 //    }
70 //    case _ => assert(false)
71 //  }
72 }
73
74 /**
75  * All user agreements. The provided list of agreements cannot have time gaps. This
76  * is checked at object creation type.
77  */
78 case class AgreementSnapshot(data: List[Agreement], snapshotTime: Long)
79   extends UserDataSnapshot[List[Agreement]] {
80
81   ensureNoGaps(data.sortWith((a,b) => if (b.validFrom > a.validFrom) true else false))
82
83   def ensureNoGaps(agreements: List[Agreement]): Unit = agreements match {
84     case ha :: (t @ (hb :: tail)) =>
85       assert(ha.validTo - hb.validFrom == 1);
86       ensureNoGaps(t)
87     case h :: Nil =>
88       assert(h.validTo == -1)
89     case Nil => ()
90   }
91
92   /**
93    * Get the user agreement at the specified timestamp
94    */
95   def getAgreement(at: Long): Maybe[DSLAgreement] =
96     data.find{ x => x.validFrom < at && x.validTo > at} match {
97       case Some(x) => Policy.policy(new Date(at)) match {
98         case Just(y) =>  y.findAgreement(x.agreement) match {
99           case Some(z) => Just(z)
100           case None => NoVal
101         }
102         case NoVal => NoVal
103         case failed @ Failed(x, y) => failed
104       }
105       case None => NoVal
106     }
107 }
108
109 /**
110  * Maintains the current state of a resource instance owned by the user.
111  * The encoding is as follows:
112  *
113  * name: DSLResource.name
114  * instanceId: instance-id (in the resource's descriminatorField)
115  * data: current-resource-value
116  * snapshotTime: last-update-timestamp
117  *
118  * In order to have a uniform representation of the resource state for all
119  * resource types (complex or simple) the following convention applies:
120  *
121  *  - If the resource is complex, the (name, instanceId) is (DSLResource.name, instance-id)
122  *  - If the resource is simple,  the (name, instanceId) is (DSLResource.name, "1")
123  *
124  */
125 case class ResourceInstanceSnapshot(/**
126                                      * Same as `resource` of [[gr.grnet.aquarium.logic.events.ResourceEvent]]
127                                      */
128                                     resource: String,
129
130                                     /**
131                                      * Same as `instanceId` of [[gr.grnet.aquarium.logic.events.ResourceEvent]]
132                                      */
133                                     instanceId: String,
134                                     data: Double, // FIXME: data is a missleading name here
135                                     snapshotTime: Long)
136   extends UserDataSnapshot[Double] {
137
138   /**
139    * This is the amount kept for the resource instance.
140    *
141    * The general rule is that an amount saved in a [[gr.grnet.aquarium.user.ResourceInstanceSnapshot]]  represents a
142    * total value, while a value appearing in a [[gr.grnet.aquarium.logic.events.ResourceEvent]] represents a
143    * difference. How these two values are combined to form the new amount is dictated by the underlying
144    * [[gr.grnet.aquarium.logic.accounting.dsl.DSLCostPolicy]]
145    */
146   def instanceAmount = data
147   
148   def isSameResourceInstance(resource: String, instanceId: String) = {
149     this.resource == resource &&
150     this.instanceId == instanceId
151   }
152 }
153
154 /**
155  * A map from (resourceName, resourceInstanceId) to (value, snapshotTime).
156  * This representation is convenient for computations and updating, while the
157  * [[gr.grnet.aquarium.user.OwnedResourcesSnapshot]] representation is convenient for JSON serialization.
158  */
159 class OwnedResourcesMap(resourcesMap: Map[(String, String), (Double, Long)]) {
160   def toResourcesSnapshot(snapshotTime: Long): OwnedResourcesSnapshot =
161     OwnedResourcesSnapshot(
162       resourcesMap map {
163         case ((name, instanceId), (value, snapshotTime)) ⇒
164           ResourceInstanceSnapshot(name, instanceId, value, snapshotTime
165       )} toList,
166       snapshotTime
167     )
168 }
169
170 case class OwnedResourcesSnapshot(data: List[ResourceInstanceSnapshot], snapshotTime: Long)
171   extends UserDataSnapshot[List[ResourceInstanceSnapshot]] with JsonSupport {
172
173   def toResourcesMap: OwnedResourcesMap = {
174     val tuples = for(rc <- data) yield ((rc.resource, rc.instanceId), (rc.instanceAmount, rc.snapshotTime))
175
176     new OwnedResourcesMap(Map(tuples.toSeq: _*))
177   }
178
179   def resourceInstanceSnapshots = data
180
181   def resourceInstanceSnapshotsExcept(resource: String, instanceId: String) = {
182     // Unfortunately, we have to use a List for data, since JSON serialization is not as flexible
183     // (at least out of the box). Thus, the update is O(L), where L is the length of the data List.
184     resourceInstanceSnapshots.filterNot(_.isSameResourceInstance(resource, instanceId))
185   }
186
187   def findResourceInstanceSnapshot(resource: String, instanceId: String): Option[ResourceInstanceSnapshot] = {
188     data.find(x => resource == x.resource && instanceId == x.instanceId)
189   }
190
191   def getResourceInstanceAmount(resource: String, instanceId: String, defaultValue: Double): Double = {
192     findResourceInstanceSnapshot(resource, instanceId).map(_.instanceAmount).getOrElse(defaultValue)
193   }
194
195   def computeResourcesSnapshotUpdate(resource: String,   // resource name
196                                      instanceId: String, // resource instance id
197                                      newAmount: Double,
198                                      snapshotTime: Long): (OwnedResourcesSnapshot,
199                                                           Option[ResourceInstanceSnapshot],
200                                                           ResourceInstanceSnapshot) = {
201
202     val newResourceInstance = ResourceInstanceSnapshot(resource, instanceId, newAmount, snapshotTime)
203     val oldResourceInstanceOpt = this.findResourceInstanceSnapshot(resource, instanceId)
204
205     val newResourceInstances = oldResourceInstanceOpt match {
206       case Some(oldResourceInstance) ⇒
207         // Resource instance found, so delete the old one and add the new one
208         newResourceInstance :: resourceInstanceSnapshotsExcept(resource, instanceId)
209       case None ⇒
210         // Resource not found, so this is the first time and we just add the new snapshot
211         newResourceInstance :: resourceInstanceSnapshots
212     }
213
214     val newOwnedResources = OwnedResourcesSnapshot(newResourceInstances, snapshotTime)
215
216     (newOwnedResources, oldResourceInstanceOpt, newResourceInstance)
217  }
218 }
219
220
221 /**
222  * A generic exception thrown when errors occur in dealing with user data snapshots
223  *
224  * @author Georgios Gousios <gousiosg@gmail.com>
225  */
226 class UserDataSnapshotException(msg: String) extends Exception(msg)
227
228 /**
229  * Holds the user active/suspended status.
230  *
231  * @author Christos KK Loverdos <loverdos@gmail.com>
232  */
233 case class ActiveSuspendedSnapshot(data: Boolean, snapshotTime: Long) extends UserDataSnapshot[Boolean]