Start the real deal
[aquarium] / src / main / scala / gr / grnet / aquarium / logic / events / ResourceEvent.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.logic.events
37
38 import net.liftweb.json.{JsonAST, Xml}
39 import gr.grnet.aquarium.util.json.JsonHelpers
40 import gr.grnet.aquarium.logic.accounting.dsl._
41 import com.ckkloverdos.maybe.{MaybeOption, Maybe}
42 import java.util.Date
43 import gr.grnet.aquarium.util.date.DateCalculator
44
45 /**
46  * Event sent to Aquarium by clients for resource accounting.
47  * 
48  * @author Christos KK Loverdos <loverdos@gmail.com>.
49  * @author Georgios Gousios <gousiosg@gmail.com>.
50  */
51 case class ResourceEvent(
52     override val id: String,           // The id at the client side (the sender) TODO: Rename to remoteId or something...
53     override val occurredMillis: Long, // When it occurred at client side (the sender)
54     override val receivedMillis: Long, // When it was received by Aquarium
55     userId: String,                    // The user for which this resource is relevant
56     clientId: String,                  // The unique client identifier (usually some hash)
57     resource: String,                  // String representation of the resource type (e.g. "bndup", "vmtime").
58     instanceId: String,                // String representation of the resource instance id
59     eventVersion: String,
60     value: Double,
61     details: ResourceEvent.Details)
62   extends AquariumEvent(id, occurredMillis, receivedMillis) {
63
64   def validate() : Boolean = {
65     !safeResource.isEmpty
66   }
67
68   def safeResource   = if(resource eq null)   "" else resource
69   def safeInstanceId = if(instanceId eq null) "" else instanceId
70
71   def hasResource   = !safeResource.isEmpty
72   def hasInstanceId = !safeInstanceId.isEmpty
73
74   def fullResourceInfo = (safeResource, safeInstanceId)
75
76   def isOccurredWithinMillis(fromMillis: Long, toMillis: Long): Boolean = {
77     require(fromMillis <= toMillis, "fromMillis <= toMillis")
78     fromMillis <= occurredMillis && occurredMillis <= toMillis
79   }
80
81   def isOccurredWithinDates(fromDate: Date, toDate: Date): Boolean = {
82     isOccurredWithinMillis(fromDate.getTime, toDate.getTime)
83   }
84
85   def isReceivedWithinMillis(fromMillis: Long, toMillis: Long): Boolean = {
86     require(fromMillis <= toMillis, "fromMillis <= toMillis")
87     fromMillis <= receivedMillis && receivedMillis <= toMillis
88   }
89   
90   def isReceivedWithinDates(fromDate: Date, toDate: Date): Boolean = {
91     isReceivedWithinMillis(fromDate.getTime, toDate.getTime)
92   }
93
94   def isReceivedWithinDateCalcs(fromDate: DateCalculator, toDate: DateCalculator): Boolean = {
95     isReceivedWithinMillis(fromDate.getMillis, toDate.getMillis)
96   }
97
98   def isOccurredOrReceivedWithinMillis(fromMillis: Long, toMillis: Long): Boolean = {
99     isOccurredWithinMillis(fromMillis, toMillis) ||
100     isReceivedWithinMillis(fromMillis, toMillis)
101   }
102
103   def isOccurredOrReceivedWithinDates(fromDate: Date, toDate: Date): Boolean = {
104     isOccurredWithinDates(fromDate, toDate) ||
105     isReceivedWithinDates(fromDate, toDate)
106   }
107   
108   def isOutOfSyncForBillingMonth(yearOfBillingMonth: Int, billingMonth: Int) = {
109     val billingStartDateCalc = new DateCalculator(yearOfBillingMonth, billingMonth)
110     val billingStartMillis = billingStartDateCalc.toMillis
111     val billingStopMillis  = billingStartDateCalc.goEndOfThisMonth.toMillis
112
113     isOutOfSyncForBillingPeriod(billingStartMillis, billingStopMillis)
114   }
115   
116   def isOutOfSyncForBillingPeriod(billingStartMillis: Long, billingStopMillis: Long): Boolean = {
117     isReceivedWithinMillis(billingStartMillis, billingStopMillis) &&
118     (occurredMillis < billingStartMillis || occurredMillis > billingStopMillis)
119   }
120
121   def toDebugString(resourcesMap: DSLResourcesMap, useOnlyInstanceId: Boolean): String = {
122     val instanceInfo = if(useOnlyInstanceId) instanceId else "%s::%s".format(resource, instanceId)
123     val bvalue = beautifyValue(resourcesMap)
124     val occurredFormatted = new DateCalculator(occurredMillis).toString
125     if(occurredMillis == receivedMillis) {
126       "EVENT(%s, [%s], %s, %s, %s, %s, %s)".format(
127         id,
128         occurredFormatted,
129         bvalue,
130         instanceInfo,
131         details,
132         userId,
133         clientId
134       )
135     } else {
136       "EVENT(%s, [%s], [%s], %s, %s, %s, %s, %s)".format(
137         id,
138         occurredFormatted,
139         new DateCalculator(receivedMillis),
140         bvalue,
141         instanceInfo,
142         details,
143         userId,
144         clientId
145       )
146     }
147   }
148
149   private[this]
150   def beatifyValue(resourceProvider: (String) ⇒ Option[DSLResource]): String = {
151     resourceProvider(this.resource) match {
152       case Some(DSLComplexResource(_, _, OnOffCostPolicy, _)) ⇒
153         OnOffPolicyResourceState(this.value).state.toUpperCase
154       case Some(rc @ DSLComplexResource(_, _, _, _)) ⇒
155         "%s [%s]".format(value, rc.unit)
156       case Some(DSLSimpleResource(_, _, OnOffCostPolicy)) ⇒
157         OnOffPolicyResourceState(this.value).state.toUpperCase
158       case Some(rc @ DSLSimpleResource(_, _, _)) ⇒
159         "%s [%s]".format(value, rc.unit)
160       case _ ⇒
161         value.toString
162     }
163   }
164
165   /**
166    * Returns a beautiful string representation of the value.
167    *
168    * @param policy The policy to be asked for resources.
169    * @return A beautiful string representation of the value.
170    */
171   def beautifyValue(policy: DSLPolicy): String = {
172     beatifyValue(policy.findResource)
173   }
174
175   /**
176    * Returns a beautiful string representation of the value.
177    *
178    * @param resourcesMap The resources map to be asked for resources.
179    * @return A beautiful string representation of the value.
180    */
181   def beautifyValue(resourcesMap: DSLResourcesMap): String = {
182     beatifyValue(resourcesMap.findResource)
183   }
184
185   /**
186    * Return `true` iff this is an event regarding a resource with an
187    * [[gr.grnet.aquarium.logic.accounting.dsl.OnOffCostPolicy]].
188    */
189   def isOnOffEvent(policy: DSLPolicy): Boolean = {
190     policy.findResource(this.resource).map(_.costPolicy) match {
191       case Some(OnOffCostPolicy) ⇒ true
192       case _ ⇒ false
193     }
194   }
195
196   /**
197    * Return `true` iff this is an event regarding a resource with an
198    * [[gr.grnet.aquarium.logic.accounting.dsl.OnOffCostPolicy]] and a
199    * `value` of `"on"`.
200    */
201   def isOnEvent(policy: DSLPolicy): Boolean = {
202     policy.findResource(this.resource) match {
203       case Some(DSLComplexResource(_, _, OnOffCostPolicy, _)) ⇒
204         OnOffPolicyResourceState(this.value).isOn
205       case Some(DSLSimpleResource(_, _, OnOffCostPolicy)) ⇒
206         OnOffPolicyResourceState(this.value).isOn
207       case _ ⇒
208         false
209     }
210   }
211
212   /**
213    * Return `true` iff this is an event regarding a resource with an
214    * [[gr.grnet.aquarium.logic.accounting.dsl.OnOffCostPolicy]] and a
215    * `value` of `"off"`.
216    */
217   def isOffEvent(policy: DSLPolicy): Boolean = {
218     policy.findResource(this.resource) match {
219       case Some(DSLComplexResource(_, _, OnOffCostPolicy, _)) ⇒
220         OnOffPolicyResourceState(this.value).isOff
221       case Some(DSLSimpleResource(_, _, OnOffCostPolicy)) ⇒
222         OnOffPolicyResourceState(this.value).isOff
223       case _ ⇒
224         false
225     }
226   }
227
228   def copyWithReceivedMillis(millis: Long) = copy(receivedMillis = millis)
229
230   /**
231    * Find the cost policy of the resource named in this resource event.
232    *
233    * We do not expect cost policies for resources to change, because they are supposed
234    * to be one of their constant characteristics. That is why do not issue a time-dependent
235    * query here for the event's current policy.
236    *
237    * Should the need arises to change the cost policy for a resource, this is a good enough
238    * reason to consider creating another type of resource.
239    */
240   def findCostPolicyM(defaultPolicy: DSLPolicy): Maybe[DSLCostPolicy] = {
241     defaultPolicy.findResource(this.safeResource).map(_.costPolicy): Maybe[DSLCostPolicy]
242   }
243
244   /**
245    * Find the cost policy of the resource named in this resource event.
246    *
247    * We do not expect cost policies for resources to change, because they are supposed
248    * to be one of their constant characteristics. That is why do not issue a time-dependent
249    * query here for the event's current policy.
250    *
251    * Should the need arises to change the cost policy for a resource, this is a good enough
252    * reason to consider creating another type of resource.
253    */
254   def findCostPolicy(resourcesMap: DSLResourcesMap): Option[DSLCostPolicy] = {
255     resourcesMap.findResource(this.safeResource).map(_.costPolicy)
256   }
257 }
258
259 object ResourceEvent {
260   type Details = Map[String, String]
261
262   type ResourceType = String
263   type ResourceIdType = String
264   type FullResourceType = (ResourceType, ResourceIdType)
265   
266   def fromJson(json: String): ResourceEvent = {
267     JsonHelpers.jsonToObject[ResourceEvent](json)
268   }
269
270   def fromJValue(jsonAST: JsonAST.JValue): ResourceEvent = {
271     JsonHelpers.jValueToObject[ResourceEvent](jsonAST)
272   }
273
274   def fromBytes(bytes: Array[Byte]): ResourceEvent = {
275     JsonHelpers.jsonBytesToObject[ResourceEvent](bytes)
276   }
277
278   def fromXml(xml: String): ResourceEvent = {
279     fromJValue(Xml.toJson(scala.xml.XML.loadString(xml)))
280   }
281
282   object JsonNames {
283     final val _id = "_id"
284     final val id = "id"
285     final val userId = "userId"
286     //final val timestamp = "timestamp" // TODO: deprecate in favor of "occurredMillis"
287     final val occurredMillis = "occurredMillis"
288     final val receivedMillis = "receivedMillis"
289     final val clientId = "clientId"
290     final val resource = "resource"
291     final val resourceId = "resourceId"
292     final val eventVersion = "eventVersion"
293     final val value = "value"
294     final val details = "details"
295
296     // ResourceType: VMTime
297     final val vmId = "vmId"
298     final val action = "action" // "on", "off"
299   }
300 }