Remodeling events
[aquarium] / src / main / scala / gr / grnet / aquarium / user / UserDataSnapshot.scala
1 /*
2  * Copyright 2011-2012 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.{findFromMapAsMaybe, findAndRemoveFromMap, shortClassNameOf}
40 import gr.grnet.aquarium.logic.accounting.Policy
41 import java.util.Date
42 import com.ckkloverdos.maybe.{NoVal, Maybe, Just}
43 import gr.grnet.aquarium.event.resource.ResourceEventModel.FullMutableResourceTypeMap
44 import logic.accounting.dsl.{Timeslot, DSLAgreement}
45 import collection.immutable.{TreeMap, SortedMap}
46 import util.date.MutableDateCalc
47 import event.resource.ResourceEventModel
48
49 /**
50  * Snapshot of data that are user-related.
51  *
52  * @author Christos KK Loverdos <loverdos@gmail.com>
53  */
54
55 case class CreditSnapshot(creditAmount: Double, snapshotTime: Long) extends DataSnapshot
56
57 case class RolesSnapshot(roles: List[String], snapshotTime: Long) extends DataSnapshot
58
59 /**
60  * Represents an agreement valid for a specific amount of time. By convention,
61  * if an agreement is currently valid, then the validTo field is equal to `Long.MaxValue`.
62  */
63 case class Agreement(name: String, validFrom: Long, validTo: Long = Long.MaxValue) {
64   assert(validTo > validFrom)
65   assert(!name.isEmpty)
66
67 //  Policy.policy(new Date(validFrom)) match {
68 //    case Just(x) => x.findAgreement(agreement) match {
69 //      case None => assert(false)
70 //      case _ =>
71 //    }
72 //    case _ => assert(false)
73 //  }
74   
75   def timeslot = Timeslot(new Date(validFrom), new Date(validTo))
76
77   override def toString =
78     "Agreement(%s, %s, %s)".
79       format(name, new MutableDateCalc(validFrom), new MutableDateCalc(validTo))
80 }
81
82 /**
83  * All user agreements. The provided list of agreements cannot have time gaps. This
84  * is checked at object creation type.
85  */
86 case class AgreementSnapshot(agreements: List[Agreement], snapshotTime: Long) extends DataSnapshot {
87
88   ensureNoGaps(agreements.sortWith((a,b) => if (b.validFrom > a.validFrom) true else false))
89
90   def agreementsByTimeslot: SortedMap[Timeslot, String] = {
91     TreeMap(agreements.map(ag => (ag.timeslot, ag.name)): _*)
92   }
93
94   def ensureNoGaps(agreements: List[Agreement]): Unit = agreements match {
95     case ha :: (t @ (hb :: tail)) =>
96       assert(ha.validTo - hb.validFrom == 1);
97       ensureNoGaps(t)
98     case h :: Nil =>
99       assert(h.validTo == Long.MaxValue)
100     case Nil => ()
101   }
102
103   /**
104    * Get the user agreement at the specified timestamp
105    */
106   def getAgreement(at: Long): Maybe[DSLAgreement] =
107     agreements.find{ x => x.validFrom < at && x.validTo > at} match {
108       case Some(x) => Policy.policy(new Date(at)).findAgreement(x.name) match {
109           case Some(z) => Just(z)
110           case None => NoVal
111         }
112       case None => NoVal
113     }
114
115   override def toString = {
116     "%s(%s, %s)".format(shortClassNameOf(this), agreements, new MutableDateCalc(snapshotTime).toString)
117   }
118 }
119
120 /**
121  * Maintains the current state of a resource instance owned by the user.
122  * The encoding is as follows:
123  *
124  * name: DSLResource.name
125  * instanceId: instance-id (in the resource's descriminatorField)
126  * data: current-resource-value
127  * snapshotTime: last-update-timestamp
128  *
129  * In order to have a uniform representation of the resource state for all
130  * resource types (complex or simple) the following convention applies:
131  *
132  *  - If the resource is complex, the (name, instanceId) is (DSLResource.name, instance-id)
133  *  - If the resource is simple,  the (name, instanceId) is (DSLResource.name, "1")
134  *
135  * @param resource        Same as `resource` of [[gr.grnet.aquarium.event.ResourceEvent]]
136  * @param instanceId      Same as `instanceId` of [[gr.grnet.aquarium.event.ResourceEvent]]
137  * @param instanceAmount  This is the amount kept for the resource instance.
138 *                         The general rule is that an amount saved in a [[gr.grnet.aquarium.user.ResourceInstanceSnapshot]]
139  *                        represents a total value, while a value appearing in a [[gr.grnet.aquarium.event.ResourceEvent]]
140  *                        represents a difference. How these two values are combined to form the new amount is dictated
141  *                        by the underlying [[gr.grnet.aquarium.logic.accounting.dsl.DSLCostPolicy]]
142  * @param snapshotTime
143  *
144  * @author Christos KK Loverdos <loverdos@gmail.com>
145  */
146 case class ResourceInstanceSnapshot(resource: String,
147                                     instanceId: String,
148                                     instanceAmount: Double,
149                                     snapshotTime: Long) extends DataSnapshot {
150
151   def isSameResourceInstance(resource: String, instanceId: String) = {
152     this.resource == resource &&
153     this.instanceId == instanceId
154   }
155 }
156
157 /**
158  * A map from (resourceName, resourceInstanceId) to (value, snapshotTime).
159  * This representation is convenient for computations and updating, while the
160  * [[gr.grnet.aquarium.user.OwnedResourcesSnapshot]] representation is convenient for JSON serialization.
161  *
162  * @author Christos KK Loverdos <loverdos@gmail.com>
163  */
164 class OwnedResourcesMap(resourcesMap: Map[(String, String), (Double, Long)]) {
165   def toResourcesSnapshot(snapshotTime: Long): OwnedResourcesSnapshot =
166     OwnedResourcesSnapshot(
167       resourcesMap map {
168         case ((name, instanceId), (value, snapshotTime)) ⇒
169           ResourceInstanceSnapshot(name, instanceId, value, snapshotTime
170       )} toList,
171       snapshotTime
172     )
173 }
174
175 /**
176  *
177  * @param resourceInstanceSnapshots
178  * @param snapshotTime
179  *
180  * @author Christos KK Loverdos <loverdos@gmail.com>
181  */
182 case class OwnedResourcesSnapshot(resourceInstanceSnapshots: List[ResourceInstanceSnapshot], snapshotTime: Long)
183   extends DataSnapshot {
184
185   def toResourcesMap: OwnedResourcesMap = {
186     val tuples = for(rc <- resourceInstanceSnapshots) yield ((rc.resource, rc.instanceId), (rc.instanceAmount, rc.snapshotTime))
187
188     new OwnedResourcesMap(Map(tuples.toSeq: _*))
189   }
190
191   def resourceInstanceSnapshotsExcept(resource: String, instanceId: String) = {
192     // Unfortunately, we have to use a List for data, since JSON serialization is not as flexible
193     // (at least out of the box). Thus, the update is O(L), where L is the length of the data List.
194     resourceInstanceSnapshots.filterNot(_.isSameResourceInstance(resource, instanceId))
195   }
196
197   def findResourceInstanceSnapshot(resource: String, instanceId: String): Option[ResourceInstanceSnapshot] = {
198     resourceInstanceSnapshots.find(x => resource == x.resource && instanceId == x.instanceId)
199   }
200
201   def getResourceInstanceAmount(resource: String, instanceId: String, defaultValue: Double): Double = {
202     findResourceInstanceSnapshot(resource, instanceId).map(_.instanceAmount).getOrElse(defaultValue)
203   }
204
205   def computeResourcesSnapshotUpdate(resource: String,   // resource name
206                                      instanceId: String, // resource instance id
207                                      newAmount: Double,
208                                      snapshotTime: Long): (OwnedResourcesSnapshot,
209                                                           Option[ResourceInstanceSnapshot],
210                                                           ResourceInstanceSnapshot) = {
211
212     val newResourceInstance = ResourceInstanceSnapshot(resource, instanceId, newAmount, snapshotTime)
213     val oldResourceInstanceOpt = this.findResourceInstanceSnapshot(resource, instanceId)
214
215     val newResourceInstances = oldResourceInstanceOpt match {
216       case Some(oldResourceInstance) ⇒
217         // Resource instance found, so delete the old one and add the new one
218         newResourceInstance :: resourceInstanceSnapshotsExcept(resource, instanceId)
219       case None ⇒
220         // Resource not found, so this is the first time and we just add the new snapshot
221         newResourceInstance :: resourceInstanceSnapshots
222     }
223
224     val newOwnedResources = OwnedResourcesSnapshot(newResourceInstances, snapshotTime)
225
226     (newOwnedResources, oldResourceInstanceOpt, newResourceInstance)
227  }
228 }
229
230
231 /**
232  * A generic exception thrown when errors occur in dealing with user data snapshots
233  *
234  * @author Georgios Gousios <gousiosg@gmail.com>
235  */
236 class DataSnapshotException(msg: String) extends Exception(msg)
237
238 /**
239  * Holds the user active/suspended status.
240  *
241  * @author Christos KK Loverdos <loverdos@gmail.com>
242  */
243 case class ActiveStateSnapshot(isActive: Boolean, snapshotTime: Long) extends DataSnapshot
244
245 /**
246  * Keeps the latest resource event per resource instance.
247  *
248  * @param resourceEvents
249  * @param snapshotTime
250  *
251  * @author Christos KK Loverdos <loverdos@gmail.com>
252  */
253 case class LatestResourceEventsSnapshot(resourceEvents: List[ResourceEventModel],
254                                         snapshotTime: Long) extends DataSnapshot {
255
256   /**
257    * The gateway to playing with mutable state.
258    *
259    * @return A fresh instance of [[gr.grnet.aquarium.user.LatestResourceEventsWorker]].
260    */
261   def toMutableWorker = {
262     val map = scala.collection.mutable.Map[ResourceEventModel.FullResourceType, ResourceEventModel]()
263     for(latestEvent <- resourceEvents) {
264       map(latestEvent.fullResourceInfo) = latestEvent
265     }
266     LatestResourceEventsWorker(map)
267   }
268 }
269
270 /**
271  * This is the mutable cousin of [[gr.grnet.aquarium.user.LatestResourceEventsSnapshot]].
272  *
273  * @param latestEventsMap
274  *
275  * @author Christos KK Loverdos <loverdos@gmail.com>
276  */
277 case class LatestResourceEventsWorker(latestEventsMap: FullMutableResourceTypeMap) {
278
279   /**
280    * The gateway to immutable state.
281    *
282    * @param snapshotTime The relevant snapshot time.
283    * @return A fresh instance of [[gr.grnet.aquarium.user.LatestResourceEventsSnapshot]].
284    */
285   def toImmutableSnapshot(snapshotTime: Long) =
286     LatestResourceEventsSnapshot(latestEventsMap.valuesIterator.toList, snapshotTime)
287
288   def updateResourceEvent(resourceEvent: ResourceEventModel): Unit = {
289     latestEventsMap((resourceEvent.resource, resourceEvent.instanceID)) = resourceEvent
290   }
291   
292   def findResourceEvent(resource: String, instanceId: String): Maybe[ResourceEventModel] = {
293     findFromMapAsMaybe(latestEventsMap, (resource, instanceId))
294   }
295
296   def findAndRemoveResourceEvent(resource: String, instanceId: String): Maybe[ResourceEventModel] = {
297     findAndRemoveFromMap(latestEventsMap, (resource, instanceId))
298   }
299
300   def size = latestEventsMap.size
301
302   def foreach[U](f: ResourceEventModel => U): Unit = {
303     latestEventsMap.valuesIterator.foreach(f)
304   }
305 }
306
307 object LatestResourceEventsWorker {
308   final val Empty = LatestResourceEventsWorker(scala.collection.mutable.Map())
309
310   /**
311    * Helper factory to construct a worker from a list of events.
312    */
313   def fromList(latestEventsList: List[ResourceEventModel]): LatestResourceEventsWorker = {
314     LatestResourceEventsSnapshot(latestEventsList, 0L).toMutableWorker
315   }
316 }
317
318 /**
319  * Keeps the implicit OFF events when a billing period ends.
320  * This is normally recorded in the [[gr.grnet.aquarium.user.UserState]].
321  *
322  * @param implicitlyIssuedEvents
323  * @param snapshotTime
324  *
325  * @author Christos KK Loverdos <loverdos@gmail.com>
326  */
327 case class ImplicitlyIssuedResourceEventsSnapshot(implicitlyIssuedEvents: List[ResourceEventModel],
328                                                   snapshotTime: Long) extends DataSnapshot {
329   /**
330    * The gateway to playing with mutable state.
331    *
332    * @return A fresh instance of [[gr.grnet.aquarium.user.ImplicitlyIssuedResourceEventsWorker]].
333    */
334   def toMutableWorker = {
335     val map = scala.collection.mutable.Map[ResourceEventModel.FullResourceType, ResourceEventModel]()
336     for(implicitEvent <- implicitlyIssuedEvents) {
337       map(implicitEvent.fullResourceInfo) = implicitEvent
338     }
339
340     ImplicitlyIssuedResourceEventsWorker(map)
341   }
342 }
343
344 /**
345  * This is the mutable cousin of [[gr.grnet.aquarium.user.ImplicitlyIssuedResourceEventsSnapshot]].
346  *
347  * @param implicitlyIssuedEventsMap
348  *
349  * @author Christos KK Loverdos <loverdos@gmail.com>
350  */
351 case class ImplicitlyIssuedResourceEventsWorker(implicitlyIssuedEventsMap: FullMutableResourceTypeMap) {
352
353   def toList: scala.List[ResourceEventModel] = {
354     implicitlyIssuedEventsMap.valuesIterator.toList
355   }
356
357   def toImmutableSnapshot(snapshotTime: Long) =
358     ImplicitlyIssuedResourceEventsSnapshot(toList, snapshotTime)
359
360   def findAndRemoveResourceEvent(resource: String, instanceId: String): Maybe[ResourceEventModel] = {
361     findAndRemoveFromMap(implicitlyIssuedEventsMap, (resource, instanceId))
362   }
363
364   def size = implicitlyIssuedEventsMap.size
365
366   def foreach[U](f: ResourceEventModel => U): Unit = {
367     implicitlyIssuedEventsMap.valuesIterator.foreach(f)
368   }
369 }
370
371 object ImplicitlyIssuedResourceEventsWorker {
372   final val Empty = ImplicitlyIssuedResourceEventsWorker(scala.collection.mutable.Map())
373 }
374
375 /**
376  *
377  * @author Christos KK Loverdos <loverdos@gmail.com>
378  *
379  * @param ignoredFirstEvents
380  * @param snapshotTime
381  */
382 case class IgnoredFirstResourceEventsSnapshot(ignoredFirstEvents: List[ResourceEventModel],
383                                               snapshotTime: Long) extends DataSnapshot {
384   def toMutableWorker = {
385     val map = scala.collection.mutable.Map[ResourceEventModel.FullResourceType, ResourceEventModel]()
386     for(ignoredFirstEvent <- ignoredFirstEvents) {
387       map(ignoredFirstEvent.fullResourceInfo) = ignoredFirstEvent
388     }
389
390     IgnoredFirstResourceEventsWorker(map)
391   }
392 }
393
394 /**
395  *
396  * @author Christos KK Loverdos <loverdos@gmail.com>
397  * @param ignoredFirstEventsMap
398  */
399 case class IgnoredFirstResourceEventsWorker(ignoredFirstEventsMap: FullMutableResourceTypeMap) {
400   def toImmutableSnapshot(snapshotTime: Long) =
401     IgnoredFirstResourceEventsSnapshot(ignoredFirstEventsMap.valuesIterator.toList, snapshotTime)
402
403   def findAndRemoveResourceEvent(resource: String, instanceId: String): Maybe[ResourceEventModel] = {
404     findAndRemoveFromMap(ignoredFirstEventsMap, (resource, instanceId))
405   }
406
407   def updateResourceEvent(resourceEvent: ResourceEventModel): Unit = {
408     ignoredFirstEventsMap((resourceEvent.resource, resourceEvent.instanceID)) = resourceEvent
409   }
410
411   def size = ignoredFirstEventsMap.size
412
413   def foreach[U](f: ResourceEventModel => U): Unit = {
414     ignoredFirstEventsMap.valuesIterator.foreach(f)
415   }
416 }
417
418 object IgnoredFirstResourceEventsWorker {
419   final val Empty = IgnoredFirstResourceEventsWorker(scala.collection.mutable.Map())
420 }