Merge branch 'master' of https://code.grnet.gr/git/aquarium
[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
52
53 /*
54 * @author Prodromos Gerakios <pgerakios@grnet.gr>
55 */
56
57 case class ChargeEntry(val id:String,
58                        val unitPrice:String,
59                        val startTime:String,
60                        val endTime:String,
61                        val ellapsedTime:String,
62                        val credits:String)
63   extends JsonSupport {}
64
65
66 class EventEntry(val eventType : String,
67                  val details   : List[ChargeEntry])
68  extends JsonSupport {}
69
70
71 case class ResourceEntry(val resourceName : String,
72                          val resourceType : String,
73                          val unitName : String,
74                          val totalCredits : String,
75                          val details : List[EventEntry])
76 extends JsonSupport {}
77
78
79 abstract class AbstractBillEntry
80  extends JsonSupport {}
81
82 class BillEntry(val id:String,
83                 val userID : String,
84                 val status : String,
85                 val remainingCredits:String,
86                 val deductedCredits:String,
87                 val startTime:String,
88                 val endTime:String,
89                 val bill:List[ResourceEntry]
90               )
91  extends AbstractBillEntry {}
92
93
94 object AbstractBillEntry {
95
96   private[this] val counter = new AtomicLong(0L)
97   private[this] def nextUIDObject() = counter.getAndIncrement
98
99   /*private[this] def walletTimeslot(i:WalletEntry) : Timeslot = {
100     val cal = new GregorianCalendar
101     cal.set(i.billingYear,i.billingMonth,1)
102     val dstart = cal.getTime
103     val lastDate = cal.getActualMaximum(Calendar.DATE)
104     cal.set(Calendar.DATE, lastDate)
105     val dend = cal.getTime
106    Timeslot(dstart,dend)
107   } */
108
109   private[this] def toChargeEntry(c:ChargeslotMsg) : ChargeEntry = {
110     val unitPrice = c.getUnitPrice.toString
111     val startTime = c.getStartMillis.toString
112     val endTime   = c.getStopMillis.toString
113     val difTime   = (c.getStopMillis - c.getStartMillis).toString
114     val credits   = c.getCreditsToSubtract.toString
115     new ChargeEntry(counter.getAndIncrement.toString,unitPrice,
116                     startTime,endTime,difTime,credits)
117   }
118
119   private[this] def toEventEntry(eventType:String,c:ChargeslotMsg) : EventEntry =
120     new EventEntry(eventType,List(toChargeEntry(c)))
121
122
123   private[this] def toResourceEntry(w:WalletEntryMsg) : ResourceEntry = {
124     assert(w.getSumOfCreditsToSubtract==0.0 || MessageHelpers.chargeslotCountOf(w) > 0)
125     val rcType =  w.getResourceType.getName
126     val rcName = rcType match {
127             case "diskspace" =>
128               String.valueOf(MessageHelpers.currentResourceEventOf(w).getDetails.get("path"))
129             case _ =>
130               MessageHelpers.currentResourceEventOf(w).getInstanceID
131         }
132     val rcUnitName = w.getResourceType.getUnit
133     val eventEntry = new ListBuffer[EventEntry]
134     val credits = w.getSumOfCreditsToSubtract
135     val eventType = //TODO: This is hardcoded; find a better solution
136         rcType match {
137           case "diskspace" =>
138             val action = MessageHelpers.currentResourceEventOf(w).getDetails.get("action")
139             val path = MessageHelpers.currentResourceEventOf(w).getDetails.get("path")
140             //"%s@%s".format(action,path)
141             action
142           case "vmtime" =>
143             MessageHelpers.currentResourceEventOf(w).getValue.toInt match {
144               case 0 => // OFF
145                   "offOn"
146               case 1 =>  // ON
147                  "onOff"
148               case 2 =>
149                  "destroy"
150               case _ =>
151                  "BUG"
152             }
153           case "addcredits" =>
154             "once"
155         }
156
157     import scala.collection.JavaConverters.asScalaBufferConverter
158     for { c <- w.getChargeslots.asScala }{
159       if(c.getCreditsToSubtract != 0.0) {
160         //Console.err.println("c.creditsToSubtract : " + c.creditsToSubtract)
161         eventEntry += toEventEntry(eventType.toString,c)
162         //credits += c.creditsToSubtract
163       }
164     }
165     //Console.err.println("TOTAL resource event credits: " + credits)
166     new ResourceEntry(rcName,rcType,rcUnitName,credits.toString,eventEntry.toList)
167   }
168
169   private[this] def resourceEntriesAt(t:Timeslot,w:UserStateMsg) : (List[ResourceEntry],Double) = {
170     val ret = new ListBuffer[ResourceEntry]
171     var sum = 0.0
172     //Console.err.println("Wallet entries: " + w.walletEntries)
173     import scala.collection.JavaConverters.asScalaBufferConverter
174     val walletEntries = w.getWalletEntries.asScala
175     /*Console.err.println("Wallet entries ")
176     for { i <- walletEntries }
177       Console.err.println("WALLET ENTRY\n%s\nEND WALLET ENTRY".format(i.toJsonString))
178     Console.err.println("End wallet entries")*/
179     for { i <- walletEntries} {
180       val referenceTimeslot = MessageHelpers.referenceTimeslotOf(i)
181       if(t.contains(referenceTimeslot) && i.getSumOfCreditsToSubtract.toDouble != 0.0){
182         /*Console.err.println("i.sumOfCreditsToSubtract : " + i.sumOfCreditsToSubtract)*/
183         if(i.getSumOfCreditsToSubtract.toDouble > 0.0D) sum += i.getSumOfCreditsToSubtract.toDouble
184         ret += toResourceEntry(i)
185       } else {
186         val ijson = AvroHelpers.jsonStringOfSpecificRecord(i)
187         val itimeslot = MessageHelpers.referenceTimeslotOf(i)
188         Console.err.println("IGNORING WALLET ENTRY : " + ijson + "\n" +
189                      t + "  does not contain " +  itimeslot + "  !!!!")
190       }
191     }
192     (ret.toList,sum)
193   }
194
195   private[this] def aggregateResourceEntries(re:List[ResourceEntry]) : List[ResourceEntry] = {
196     def addResourceEntries(a:ResourceEntry,b:ResourceEntry) : ResourceEntry = {
197       assert(a.resourceName == b.resourceName)
198       val totalCredits = (a.totalCredits.toDouble+b.totalCredits.toDouble).toString
199       a.copy(a.resourceName,a.resourceType,a.unitName,totalCredits,a.details ::: b.details)
200     }
201     re.foldLeft(TreeMap[String,ResourceEntry]()){ (map,r1) =>
202       map.get(r1.resourceName) match {
203         case None => map + ((r1.resourceName,r1))
204         case Some(r0) => (map - r0.resourceName) +
205                          ((r0.resourceName, addResourceEntries(r0,r1)))
206       }
207     }.values.toList
208   }
209
210   def fromWorkingUserState(t0:Timeslot,userID:String,w:Option[UserStateMsg]) : AbstractBillEntry = {
211     val t = t0.roundMilliseconds /* we do not care about milliseconds */
212     //Console.err.println("Timeslot: " + t0)
213     //Console.err.println("After rounding timeslot: " + t)
214     val ret = w match {
215       case None =>
216           new BillEntry(counter.getAndIncrement.toString,
217                         userID,"processing",
218                         "0.0",
219                         "0.0",
220                         t.from.getTime.toString,t.to.getTime.toString,
221                         Nil)
222       case Some(w) =>
223         val wjson = AvroHelpers.jsonStringOfSpecificRecord(w)
224         Console.err.println("Working user state: %s".format(wjson))
225         val (rcEntries,rcEntriesCredits) = resourceEntriesAt(t,w)
226         val resMap = aggregateResourceEntries(rcEntries)
227         new BillEntry(counter.getAndIncrement.toString,
228                       userID,"ok",
229                       w.getTotalCredits.toString,
230                       rcEntriesCredits.toString,
231                       t.from.getTime.toString,t.to.getTime.toString,
232                       resMap)
233     }
234     //Console.err.println("JSON: " +  ret.toJsonString)
235     ret
236   }
237
238   val jsonSample = "{\n  \"id\":\"2\",\n  \"userID\":\"loverdos@grnet.gr\",\n  \"status\":\"ok\",\n  \"remainingCredits\":\"3130.0000027777783\",\n  \"deductedCredits\":\"5739.9999944444435\",\n  \"startTime\":\"1341090000000\",\n  \"endTime\":\"1343768399999\",\n  \"bill\":[{\n    \"resourceName\":\"diskspace\",\n    \"resourceType\":\"diskspace\",\n    \"unitName\":\"MB/Hr\",\n    \"totalCredits\":\"2869.9999972222217\",\n    \"eventType\":\"object update@/Papers/GOTO_HARMFUL.PDF\",\n\t    \"details\":[\n\t     {\"totalCredits\":\"2869.9999972222217\",\n\t      \"details\":[{\n\t      \"id\":\"0\",\n\t      \"unitPrice\":\"0.01\",\n\t      \"startTime\":\"1342735200000\",\n\t      \"endTime\":\"1343768399999\",\n\t      \"ellapsedTime\":\"1033199999\",\n\t      \"credits\":\"2869.9999972222217\"\n\t    \t}]\n\t    }\n\t  ]\n  },{\n    \"resourceName\":\"diskspace\",\n    \"resourceType\":\"diskspace\",\n    \"unitName\":\"MB/Hr\",\n    \"totalCredits\":\"2869.9999972222217\",\n    \"eventType\":\"object update@/Papers/GOTO_HARMFUL.PDF\",\n    \"details\":[\t     {\"totalCredits\":\"2869.9999972222217\",\n\t      \"details\":[{\n\t      \"id\":\"0\",\n\t      \"unitPrice\":\"0.01\",\n\t      \"startTime\":\"1342735200000\",\n\t      \"endTime\":\"1343768399999\",\n\t      \"ellapsedTime\":\"1033199999\",\n\t      \"credits\":\"2869.9999972222217\"\n\t    \t}]\n\t    }\n\t]\n  }]\n}"
239
240   def main0(args: Array[String]) = {
241      val b : BillEntry = StdConverters.AllConverters.convertEx[BillEntry](CompactJsonTextFormat(jsonSample))
242      val l0 = b.bill
243      val l1 = aggregateResourceEntries(l0)
244
245      Console.err.println("Initial resources: ")
246      for{ i <- l0 } Console.err.println("RESOURCE: " + i.toJsonString)
247     Console.err.println("Aggregate resources: ")
248     for{ a <- l1 } {
249       Console.err.println("RESOURCE:  %s\n  %s\nEND RESOURCE".format(a.resourceName,a.toJsonString))
250     }
251
252     val aggr = new BillEntry(b.id,b.userID,b.status,b.remainingCredits,b.deductedCredits,b.startTime,b.endTime,l1)
253     Console.err.println("Aggregate:\n" + aggr.toJsonString)
254   }
255
256   //
257   def main(args: Array[String]) = {
258     //Console.err.println("JSON: " +  (new BillEntry).toJsonString)
259     val propsfile = new FileStreamResource(new File("aquarium.properties"))
260     var _props: Props = Props(propsfile)(StdConverters.AllConverters).getOr(Props()(StdConverters.AllConverters))
261     val aquarium = new AquariumBuilder(_props, ResourceLocator.DefaultPolicyMsg).
262       update(Aquarium.EnvKeys.storeProvider, new MemStoreProvider).
263       update(Aquarium.EnvKeys.eventsStoreFolder,Some(new File(".."))).
264       build()
265     aquarium.start()
266     ()
267   }
268 }