Merge branch 'master' into 1852_billing_period_calc
[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 // TODO: Check if needed
58 case class PaymentOrdersSnapshot(data: List[AnyRef], snapshotTime: Long) extends UserDataSnapshot[List[AnyRef]]
59
60 // TODO: Check if needed
61 case class OwnedGroupsSnapshot(data: List[String], snapshotTime: Long) extends UserDataSnapshot[List[String]]
62
63 // TODO: Check if needed
64 case class GroupMembershipsSnapshot(data: List[String], snapshotTime: Long) extends UserDataSnapshot[List[String]]
65
66 /**
67  * Represents an agreement valid for a specific amount of time. By convention,
68  * if an agreement is currently valid, then the validTo field is equal to -1.
69  */
70 case class Agreement(agreement: String, validFrom: Long, validTo: Long) {
71   if (validTo != -1) assert(validTo > validFrom)
72   assert(!agreement.isEmpty)
73
74   Policy.policy(new Date(validFrom)) match {
75     case Just(x) => x.findAgreement(agreement) match {
76       case None => assert(false)
77       case _ =>
78     }
79     case _ => assert(false)
80   }
81 }
82
83 /**
84  * All user agreements. The provided list of agreements cannot have time gaps. This
85  * is checked at object creation type.
86  */
87 case class AgreementSnapshot(data: List[Agreement], snapshotTime: Long)
88   extends UserDataSnapshot[List[Agreement]] {
89
90   ensureNoGaps(data.sortWith((a,b) => if (b.validFrom > a.validFrom) true else false))
91
92   def ensureNoGaps(agreements: List[Agreement]): Unit = agreements match {
93     case h :: t => assert(h.validTo - t.head.validFrom == 1); ensureNoGaps(t)
94     case h :: Nil => assert(h.validTo == -1)
95   }
96
97   /**
98    * Get the user agreement at the specified timestamp
99    */
100   def getAgreement(at: Long): Maybe[DSLAgreement] =
101     data.find{ x => x.validFrom < at && x.validTo > at} match {
102       case Some(x) => Policy.policy(new Date(at)) match {
103         case Just(y) =>  y.findAgreement(x.agreement) match {
104           case Some(z) => Just(z)
105           case None => NoVal
106         }
107         case NoVal => NoVal
108         case failed @ Failed(x, y) => failed
109       }
110       case None => NoVal
111     }
112 }
113
114 /**
115  * Maintains the current state of a resource instance owned by the user.
116  * The encoding is as follows:
117  *
118  * name: DSLResource.name
119  * instanceId: instance-id (in the resource's descriminatorField)
120  * data: current-resource-value
121  * snapshotTime: last-update-timestamp
122  *
123  * In order to have a uniform representation of the resource state for all
124  * resource types (complex or simple) the following convention applies:
125  *
126  *  - If the resource is complex, the (name, instanceId) is (DSLResource.name, instance-id)
127  *  - If the resource is simple,  the (name, instanceId) is (DSLResource.name, "1")
128  *
129  */
130 case class ResourceInstanceSnapshot(
131     name: String,
132     instanceId: String,
133     data: Double,
134     snapshotTime: Long)
135   extends UserDataSnapshot[Double] {
136
137   /**
138    * This is the amount kept for the resource instance.
139    *
140    * The general rule is that an amount saved in a [[gr.grnet.aquarium.user.ResourceInstanceSnapshot]]  represents a
141    * total value, while a value appearing in a [[gr.grnet.aquarium.logic.events.ResourceEvent]] represents a
142    * difference. How these two values are combined to form the new amount is dictated by the underlying
143    * [[gr.grnet.aquarium.logic.accounting.dsl.DSLCostPolicy]]
144    */
145   def instanceAmount = data
146   
147   def isSameResource(name: String, instanceId: String) = {
148     this.name == name &&
149     this.instanceId == instanceId
150   }
151 }
152
153 /**
154  * A map from (resourceName, resourceInstanceId) to (value, snapshotTime).
155  * This representation is convenient for computations and updating, while the
156  * [[gr.grnet.aquarium.user.OwnedResourcesSnapshot]] representation is convenient for JSON serialization.
157  */
158 class OwnedResourcesMap(map: Map[(String, String), (Double, Long)]) {
159   def toResourcesSnapshot(snapshotTime: Long): OwnedResourcesSnapshot =
160     OwnedResourcesSnapshot(
161       map map {
162         case ((name, instanceId), (value, snapshotTime)) ⇒
163           ResourceInstanceSnapshot(name, instanceId, value, snapshotTime
164       )} toList,
165       snapshotTime
166     )
167 }
168
169 case class OwnedResourcesSnapshot(data: List[ResourceInstanceSnapshot], snapshotTime: Long)
170   extends UserDataSnapshot[List[ResourceInstanceSnapshot]] with JsonSupport {
171
172   def toResourcesMap: OwnedResourcesMap = {
173     val tuples = for(rc <- data) yield ((rc.name, rc.instanceId), (rc.instanceAmount, rc.snapshotTime))
174
175     new OwnedResourcesMap(Map(tuples.toSeq: _*))
176   }
177
178   def findResourceSnapshot(name: String, instanceId: String): Option[ResourceInstanceSnapshot] =
179     data.find { x => name == x.name && instanceId == x.instanceId }
180
181   def addOrUpdateResourceSnapshot(name: String,       // resource name
182                                   instanceId: String, // resource instance id
183                                   newRCInstanceAmount: Double,
184                                   snapshotTime: Long): (OwnedResourcesSnapshot, Option[ResourceInstanceSnapshot], ResourceInstanceSnapshot) = {
185
186     val newRCInstance = ResourceInstanceSnapshot(name, instanceId, newRCInstanceAmount, snapshotTime)
187     val oldRCInstanceOpt = this.findResourceSnapshot(name, instanceId)
188
189     val newData = oldRCInstanceOpt match {
190       case Some(oldRCInstance) ⇒
191         // Resource instance found, so delete the old one and add the new one
192         newRCInstance :: (data.filterNot(_.isSameResource(name, instanceId)))
193       case None ⇒
194         // Resource not found, so this is the first time and we just add the new snapshot
195         newRCInstance :: data
196     }
197
198     val newOwnedResources = this.copy(data = newData, snapshotTime = snapshotTime)
199
200     (newOwnedResources, oldRCInstanceOpt, newRCInstance)
201  }
202 }
203
204
205 /**
206  * A generic exception thrown when errors occur in dealing with user data snapshots
207  *
208  * @author Georgios Gousios <gousiosg@gmail.com>
209  */
210 class UserDataSnapshotException(msg: String) extends Exception(msg)
211
212 /**
213  * Holds the user active/suspended status.
214  *
215  * @author Christos KK Loverdos <loverdos@gmail.com>
216  */
217 case class ActiveSuspendedSnapshot(data: Boolean, snapshotTime: Long) extends UserDataSnapshot[Boolean]