Minor fix in BillEntry
[aquarium] / src / main / scala / gr / grnet / aquarium / charging / bill / BillEntry.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.charging.bill
37
38 import com.ckkloverdos.props.Props
39 import com.ckkloverdos.resource.FileStreamResource
40 import gr.grnet.aquarium.converter.{CompactJsonTextFormat, StdConverters}
41 import gr.grnet.aquarium.logic.accounting.dsl.Timeslot
42 import gr.grnet.aquarium.message.avro.{AvroHelpers, MessageHelpers}
43 import gr.grnet.aquarium.message.avro.gen.{ChargeslotMsg, WalletEntryMsg, UserStateMsg}
44 import gr.grnet.aquarium.store.memory.MemStoreProvider
45 import gr.grnet.aquarium.util.json.JsonSupport
46 import gr.grnet.aquarium.{Aquarium, ResourceLocator, AquariumBuilder}
47 import java.io.File
48 import java.util.concurrent.atomic.AtomicLong
49 import scala.collection.immutable.TreeMap
50 import scala.collection.mutable.ListBuffer
51 import gr.grnet.aquarium.policy.ResourceType
52
53
54 /*
55 * @author Prodromos Gerakios <pgerakios@grnet.gr>
56 */
57
58 case class ChargeEntry(val id:String,
59                        val unitPrice:String,
60                        val startTime:String,
61                        val endTime:String,
62                        val elapsedTime:String,
63                        val units:String,
64                        val credits:String)
65   extends JsonSupport {}
66
67
68 class EventEntry(val eventType : String,
69                  val details   : List[ChargeEntry])
70  extends JsonSupport {}
71
72
73 case class ResourceEntry(val resourceName : String,
74                          //val resourceType : String,
75                          //val unitName : String,
76                          val totalCredits : String,
77                          val totalElapsedTime : String,
78                          val totalUnits : String,
79                          val details : List[EventEntry])
80 extends JsonSupport {
81    var unitName = "EMPTY_UNIT_NAME"
82    var resourceType = "EMPTY_RESOURCE_TYPE"
83 }
84
85 case class ServiceEntry(val serviceName: String,
86                         val totalCredits : String,
87                         val totalElapsedTime : String,
88                         val totalUnits:String,
89                         val unitName:String,
90                         val details: List[ResourceEntry]
91                        )
92 extends JsonSupport {}
93
94 abstract class AbstractBillEntry
95  extends JsonSupport {}
96
97 class BillEntry(val id:String,
98                 val userID : String,
99                 val status : String,
100                 val remainingCredits:String,
101                 val deductedCredits:String,
102                 val startTime:String,
103                 val endTime:String,
104                 val bill:List[ServiceEntry]
105               )
106  extends AbstractBillEntry {}
107
108
109 object AbstractBillEntry {
110
111   private[this] val counter = new AtomicLong(0L)
112   private[this] def nextUIDObject() = counter.getAndIncrement
113
114   /*private[this] def walletTimeslot(i:WalletEntry) : Timeslot = {
115     val cal = new GregorianCalendar
116     cal.set(i.billingYear,i.billingMonth,1)
117     val dstart = cal.getTime
118     val lastDate = cal.getActualMaximum(Calendar.DATE)
119     cal.set(Calendar.DATE, lastDate)
120     val dend = cal.getTime
121    Timeslot(dstart,dend)
122   } */
123
124   private[this] def toChargeEntry(c:ChargeslotMsg) : (ChargeEntry,Long,Double) = {
125     val unitPrice = c.getUnitPrice.toString
126     val startTime = c.getStartMillis.toString
127     val endTime   = c.getStopMillis.toString
128     val difTime   = (c.getStopMillis - c.getStartMillis).toLong
129     val unitsD     = (c.getCreditsToSubtract/c.getUnitPrice)
130     val credits   = c.getCreditsToSubtract.toString
131     (new ChargeEntry(counter.getAndIncrement.toString,unitPrice,
132                     startTime,endTime,difTime.toString,unitsD.toString,credits),difTime,unitsD)
133   }
134
135   private[this] def toEventEntry(eventType:String,c:ChargeslotMsg) : (EventEntry,Long,Double) = {
136     val (c1,l1,d1) = toChargeEntry(c)
137     (new EventEntry(eventType,List(c1)),l1,d1)
138   }
139
140
141   private[this] def toResourceEntry(w:WalletEntryMsg) : ResourceEntry = {
142     assert(w.getSumOfCreditsToSubtract==0.0 || MessageHelpers.chargeslotCountOf(w) > 0)
143     val rcType =  w.getResourceType.getName
144     val rcName = rcType match {
145             case "diskspace" =>
146               String.valueOf(MessageHelpers.currentResourceEventOf(w).getDetails.get("path").getAnyValue)
147             case _ =>
148               MessageHelpers.currentResourceEventOf(w).getInstanceID
149         }
150     val rcUnitName = w.getResourceType.getUnit
151     val eventEntry = new ListBuffer[EventEntry]
152     val credits = w.getSumOfCreditsToSubtract
153     val eventType = //TODO: This is hardcoded; find a better solution
154         rcType match {
155           case "diskspace" =>
156             val action = MessageHelpers.currentResourceEventOf(w).getDetails.get("action").getAnyValue
157             //val path = MessageHelpers.currentResourceEventOf(w).getDetails.get("path")
158             //"%s@%s".format(action,path)
159             action
160           case "vmtime" =>
161             MessageHelpers.currentResourceEventOf(w).getValue.toInt match {
162               case 0 => // OFF
163                   "offOn"
164               case 1 =>  // ON
165                  "onOff"
166               case 2 =>
167                  "destroy"
168               case _ =>
169                  "BUG"
170             }
171           case "addcredits" =>
172             "once"
173         }
174     //w.
175     import scala.collection.JavaConverters.asScalaBufferConverter
176     ///FIXME: val elapsedTime = w.getChargeslots.asScala.foldLeft()
177    //c.getStopMillis - c.getStartMillis
178     var totalElapsedTime = 0L
179     var totalUnits = 0.0D
180       for { c <- w.getChargeslots.asScala }{
181         if(c.getCreditsToSubtract != 0.0) {
182           //Console.err.println("c.creditsToSubtract : " + c.creditsToSubtract)
183           val (e,l,u) = toEventEntry(eventType.toString,c)
184           eventEntry +=  e
185           totalElapsedTime += l
186           totalUnits += u
187         }
188       }
189     //Console.err.println("TOTAL resource event credits: " + credits)
190     val re = new ResourceEntry(rcName,/*rcType,rcUnitName,*/credits.toString,totalElapsedTime.toString,
191                        totalUnits.toString,eventEntry.toList)
192     re.unitName = rcUnitName
193     re.resourceType = rcType
194     re
195   }
196
197   private[this] def resourceEntriesAt(t:Timeslot,w:UserStateMsg) : (List[ResourceEntry],Double) = {
198     val ret = new ListBuffer[ResourceEntry]
199     var sum = 0.0
200     //Console.err.println("Wallet entries: " + w.walletEntries)
201     import scala.collection.JavaConverters.asScalaBufferConverter
202     val walletEntries = w.getWalletEntries.asScala
203     /*Console.err.println("Wallet entries ")
204     for { i <- walletEntries }
205       Console.err.println("WALLET ENTRY\n%s\nEND WALLET ENTRY".format(i.toJsonString))
206     Console.err.println("End wallet entries")*/
207     for { i <- walletEntries} {
208       val referenceTimeslot = MessageHelpers.referenceTimeslotOf(i)
209       if(t.contains(referenceTimeslot) && i.getSumOfCreditsToSubtract.toDouble != 0.0){
210         /*Console.err.println("i.sumOfCreditsToSubtract : " + i.sumOfCreditsToSubtract)*/
211         if(i.getSumOfCreditsToSubtract.toDouble > 0.0D)
212           sum += i.getSumOfCreditsToSubtract.toDouble
213         ret += toResourceEntry(i)
214       } else {
215         val ijson = AvroHelpers.jsonStringOfSpecificRecord(i)
216         val itimeslot = MessageHelpers.referenceTimeslotOf(i)
217         Console.err.println("IGNORING WALLET ENTRY : " + ijson + "\n" +
218                      t + "  does not contain " +  itimeslot + "  !!!!")
219       }
220     }
221     (ret.toList,sum)
222   }
223
224   private[this] def aggregateResourceEntries(re:List[ResourceEntry]) : List[ServiceEntry] = {
225     def addResourceEntries(a:ResourceEntry,b:ResourceEntry) : ResourceEntry = {
226       assert(a.resourceName == b.resourceName)
227       val totalCredits = (a.totalCredits.toDouble+b.totalCredits.toDouble).toString
228       val totalElapsedTime =  (a.totalElapsedTime.toLong+b.totalElapsedTime.toLong).toString
229       val totalUnits =  (a.totalUnits.toDouble+b.totalUnits.toDouble).toString
230       val ab = a.copy(a.resourceName/*,a.resourceType,a.unitName*/,totalCredits,totalElapsedTime,totalUnits,
231              a.details ::: b.details)
232       ab.unitName = a.unitName
233       ab.resourceType = a.resourceType
234       ab
235     }
236     val map0 = re.foldLeft(TreeMap[String,ResourceEntry]()){ (map,r1) =>
237       map.get(r1.resourceName) match {
238         case None => map + ((r1.resourceName,r1))
239         case Some(r0) => (map - r0.resourceName) +
240                          ((r0.resourceName, addResourceEntries(r0,r1)))
241       }
242     }
243     val map1 = map0.foldLeft(TreeMap[String,List[ResourceEntry]]()){ case (map,(_,r1)) =>
244       map.get(r1.resourceType) match {
245         case None =>  map + ((r1.resourceType,List(r1)))
246         case Some(rl) => (map - r1.resourceType) +  ((r1.resourceType,r1::rl))
247       }
248     }
249     map1.foldLeft(List[ServiceEntry]()){ case (ret,(serviceName,resList)) =>
250       val (totalCredits,totalElapsedTime,totalUnits) =
251         resList.foldLeft((0.0D,0L,0.0D)){ case ((a,b,c),r) =>
252             (a+r.totalCredits.toDouble,
253              b+r.totalElapsedTime.toLong,
254              c+r.totalUnits.toDouble
255             )}
256       new ServiceEntry(serviceName,totalCredits.toString,
257                        totalElapsedTime.toString,totalUnits.toString,
258                        resList.head.unitName,resList) :: ret
259     }
260   }
261
262   def addMissingServices(se:List[ServiceEntry],re:Map[String,ResourceType]) : List[ServiceEntry]=
263     se:::(re -- se.map(_.serviceName).toSet).foldLeft(List[ServiceEntry]()) { case (ret,(name,typ:ResourceType)) =>
264       new ServiceEntry(name,"0.0","0","0.0",typ.unit,List[ResourceEntry]()) :: ret
265     }
266
267   def fromWorkingUserState(t0:Timeslot,userID:String,w:Option[UserStateMsg],
268                            resourceTypes:Map[String,ResourceType]) : AbstractBillEntry = {
269     val t = t0.roundMilliseconds /* we do not care about milliseconds */
270     //Console.err.println("Timeslot: " + t0)
271     //Console.err.println("After rounding timeslot: " + t)
272     val ret = w match {
273       case None =>
274           val allMissing = addMissingServices(Nil,resourceTypes)
275           new BillEntry(counter.getAndIncrement.toString,
276                         userID,"processing",
277                         "0.0",
278                         "0.0",
279                         t.from.getTime.toString,t.to.getTime.toString,
280                         allMissing)
281       case Some(w) =>
282         val wjson = AvroHelpers.jsonStringOfSpecificRecord(w)
283         Console.err.println("Working user state: %s".format(wjson))
284         val (rcEntries,rcEntriesCredits) = resourceEntriesAt(t,w)
285         val resList0 = aggregateResourceEntries(rcEntries)
286         val resList1 = addMissingServices(resList0,resourceTypes)
287         new BillEntry(counter.getAndIncrement.toString,
288                       userID,"ok",
289                       w.getTotalCredits.toString,
290                       rcEntriesCredits.toString,
291                       t.from.getTime.toString,
292                       t.to.getTime.toString,
293                       resList1)
294     }
295     //Console.err.println("JSON: " +  ret.toJsonString)
296     ret
297   }
298
299   //
300   def main(args: Array[String]) = {
301     //Console.err.println("JSON: " +  (new BillEntry).toJsonString)
302     val propsfile = new FileStreamResource(new File("aquarium.properties"))
303     var _props: Props = Props(propsfile)(StdConverters.AllConverters).getOr(Props()(StdConverters.AllConverters))
304     val aquarium = new AquariumBuilder(_props, ResourceLocator.DefaultPolicyMsg).
305       //update(Aquarium.EnvKeys.storeProvider, new MemStoreProvider).
306       update(Aquarium.EnvKeys.eventsStoreFolder,Some(new File(".."))).
307       build()
308     aquarium.start()
309     ()
310   }
311 }