Squash a few message initialization bugs
[aquarium] / src / main / scala / gr / grnet / aquarium / charging / ChargingBehaviorSkeleton.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
37
38 import gr.grnet.aquarium.{Aquarium, AquariumInternalError}
39 import gr.grnet.aquarium.computation.{TimeslotComputations, BillingMonthInfo}
40 import gr.grnet.aquarium.event.{CreditsModel, DetailsModel}
41 import gr.grnet.aquarium.logic.accounting.dsl.Timeslot
42 import gr.grnet.aquarium.message.avro.gen.{EffectivePriceTableMsg, FullPriceTableMsg, ResourceTypeMsg, WalletEntryMsg, ResourceInstanceChargingStateMsg, ResourcesChargingStateMsg, ResourceEventMsg}
43 import gr.grnet.aquarium.message.avro.{MessageHelpers, AvroHelpers, MessageFactory}
44 import gr.grnet.aquarium.policy.{PolicyModel, EffectivePriceTableModel, FullPriceTableModel, UserAgreementModel}
45 import gr.grnet.aquarium.store.PolicyStore
46 import gr.grnet.aquarium.util._
47 import gr.grnet.aquarium.util.date.TimeHelpers
48 import java.{util ⇒ ju}
49 import java.util.{List ⇒ JList, ArrayList ⇒ JArrayList}
50 import scala.collection.immutable
51 import scala.collection.mutable
52 import gr.grnet.aquarium.message.MessageConstants
53
54
55 /**
56  * A charging behavior indicates how charging for a resource will be done
57  * wrt the various states a resource can be.
58  *
59  * @author Christos KK Loverdos <loverdos@gmail.com>
60  */
61
62 abstract class ChargingBehaviorSkeleton(
63     final val selectorLabelsHierarchy: List[String]
64 ) extends ChargingBehavior with Loggable {
65
66   final val HourMillis = CreditsModel.from(1000L * 60 * 60)
67   final val HourMillisInverse = CreditsModel.inv(HourMillis)
68   final val MB = CreditsModel.from(1024L * 1024L)
69   final val MBInverse = CreditsModel.inv(MB)
70   final val GB = CreditsModel.from(1024L * 1024L * 1024L)
71   final val GBInverse = CreditsModel.inv(GB)
72
73   @inline final def HrsOfMillis(timeDeltaMillis: Long): CreditsModel.Type = {
74     CreditsModel.*(
75       HourMillisInverse,
76       CreditsModel.from(timeDeltaMillis)
77     )
78   }
79
80   @inline final def MBsOfBytes(bytes: Double): CreditsModel.Type = {
81     CreditsModel.*(
82       MBInverse,
83       CreditsModel.from(bytes)
84     )
85   }
86
87   @inline final protected def rcDebugInfo(rcEvent: ResourceEventMsg) = {
88     AvroHelpers.jsonStringOfSpecificRecord(rcEvent)
89   }
90
91   protected def newResourceInstanceChargingStateMsg(
92       clientID: String,
93       resource: String,
94       instanceID: String
95   ) = {
96
97     MessageFactory.newResourceInstanceChargingStateMsg(
98
99       DetailsModel.make,
100       new JArrayList[ResourceEventMsg](),
101       new JArrayList[ResourceEventMsg](),
102       0.0,
103       0.0,
104       0.0,
105       0.0,
106       clientID,
107       resource,
108       instanceID
109     )
110   }
111
112   final protected def ensureInitializedWorkingState(
113       resourcesChargingState: ResourcesChargingStateMsg,
114       resourceEvent: ResourceEventMsg
115   ) {
116     ensureInitializedResourcesChargingStateDetails(resourcesChargingState.getDetails)
117     ensureInitializedResourceInstanceChargingState(resourcesChargingState, resourceEvent)
118   }
119
120   protected def ensureInitializedResourcesChargingStateDetails(details: DetailsModel.Type) {}
121
122   protected def ensureInitializedResourceInstanceChargingState(
123       resourcesChargingState: ResourcesChargingStateMsg,
124       resourceEvent: ResourceEventMsg
125   ) {
126
127     val instanceID = resourceEvent.getInstanceID
128     val clientID = resourceEvent.getClientID
129     val resource = resourceEvent.getResource
130     val stateOfResourceInstance = resourcesChargingState.getStateOfResourceInstance
131
132     stateOfResourceInstance.get(instanceID) match {
133       case null ⇒
134         stateOfResourceInstance.put(
135           instanceID,
136           newResourceInstanceChargingStateMsg(clientID, resource, instanceID)
137         )
138
139       case _ ⇒
140     }
141   }
142
143   protected def fillWorkingResourceInstanceChargingStateFromEvent(
144       resourceInstanceChargingState: ResourceInstanceChargingStateMsg,
145       resourceEvent: ResourceEventMsg
146   ) {
147
148     resourceInstanceChargingState.setCurrentValue(resourceEvent.getValue.toString.toDouble)
149   }
150
151   protected def computeWalletEntriesForNewEvent(
152       resourceEvent: ResourceEventMsg,
153       resourceType: ResourceTypeMsg,
154       billingMonthInfo: BillingMonthInfo,
155       totalCredits: Double,
156       referenceStartMillis: Long,
157       referenceStopMillis: Long,
158       agreementByTimeslot: immutable.SortedMap[Timeslot, UserAgreementModel],
159       workingResourcesChargingStateDetails: DetailsModel.Type,
160       resourceInstanceChargingState: ResourceInstanceChargingStateMsg,
161       aquarium: Aquarium,
162       walletEntryRecorder: WalletEntryMsg ⇒ Unit
163   ): (Int, Double) = {
164
165     val userID = resourceEvent.getUserID
166     val resourceEventDetails = resourceEvent.getDetails
167
168     var _oldTotalCredits = totalCredits
169
170     var _newAccumulatingAmount = computeNewAccumulatingAmount(resourceInstanceChargingState, resourceEventDetails)
171     // It will also update the old one inside the data structure.
172     resourceInstanceChargingState.setOldAccumulatingAmount(resourceInstanceChargingState.getAccumulatingAmount)
173     resourceInstanceChargingState.setAccumulatingAmount(_newAccumulatingAmount)
174
175     val policyByTimeslot = aquarium.policyStore.loadSortedPolicyModelsWithin(
176       referenceStartMillis,
177       referenceStopMillis
178     )
179
180     val effectivePriceTableModelSelector: FullPriceTableModel ⇒ EffectivePriceTableModel = fullPriceTable ⇒ {
181       this.selectEffectivePriceTableModel(
182         fullPriceTable,
183         workingResourcesChargingStateDetails,
184         resourceInstanceChargingState,
185         resourceEvent,
186         referenceStartMillis,
187         referenceStopMillis,
188         totalCredits
189       )
190     }
191
192     val fullPriceTableModelGetter = aquarium.unsafeFullPriceTableModelForAgreement(_,_)
193
194     val initialChargeslots = TimeslotComputations.computeInitialChargeslots(
195       Timeslot(referenceStartMillis, referenceStopMillis),
196       policyByTimeslot,
197       agreementByTimeslot,
198       fullPriceTableModelGetter,
199       effectivePriceTableModelSelector
200     )
201
202     val fullChargeslots = initialChargeslots.map { cs ⇒
203       val timeDeltaMillis = cs.getStopMillis - cs.getStartMillis
204
205       val (creditsToSubtract, explanation) = this.computeCreditsToSubtract(
206         resourceInstanceChargingState,
207         _oldTotalCredits, // FIXME ??? Should recalculate ???
208         timeDeltaMillis,
209         cs.getUnitPrice
210       )
211
212       cs.setCreditsToSubtract(creditsToSubtract)
213       cs.setExplanation(explanation)
214
215       cs
216     }
217
218     if(fullChargeslots.length == 0) {
219       throw new AquariumInternalError("No chargeslots computed for resource event %s".format(resourceEvent.getOriginalID))
220     }
221
222     val sumOfCreditsToSubtract = fullChargeslots.map(_.getCreditsToSubtract.toDouble).sum
223     val newTotalCredits = _oldTotalCredits - sumOfCreditsToSubtract
224
225     val eventsForWallet = new ju.ArrayList[ResourceEventMsg](resourceInstanceChargingState.getPreviousEvents)
226     eventsForWallet.add(0, resourceEvent)
227     import scala.collection.JavaConverters.seqAsJavaListConverter
228     val newWalletEntry = MessageFactory.newWalletEntryMsg(
229       userID,
230       CreditsModel.from(sumOfCreditsToSubtract),
231       CreditsModel.from(_oldTotalCredits),
232       CreditsModel.from(newTotalCredits),
233       TimeHelpers.nowMillis(),
234       referenceStartMillis,
235       referenceStopMillis,
236       billingMonthInfo.year,
237       billingMonthInfo.month,
238       billingMonthInfo.day,
239       fullChargeslots.asJava,
240       eventsForWallet,
241       resourceType,
242       resourceEvent.getIsSynthetic
243     )
244
245     logger.debug("newWalletEntry = {}", AvroHelpers.jsonStringOfSpecificRecord(newWalletEntry))
246
247     walletEntryRecorder.apply(newWalletEntry)
248
249     (1, sumOfCreditsToSubtract)
250   }
251
252
253   def selectEffectivePriceTableModel(
254       fullPriceTable: FullPriceTableModel,
255       chargingBehaviorDetails: DetailsModel.Type,
256       resourceInstanceChargingState: ResourceInstanceChargingStateMsg,
257       currentResourceEvent: ResourceEventMsg,
258       referenceStartMillis: Long,
259       referenceStopMillis: Long,
260       totalCredits: Double
261   ): EffectivePriceTableModel = {
262
263     val selectorPath = computeSelectorPath(
264       chargingBehaviorDetails,
265       resourceInstanceChargingState,
266       currentResourceEvent,
267       referenceStartMillis,
268       referenceStopMillis,
269       totalCredits
270     )
271
272     fullPriceTable.effectivePriceTableOfSelectorForResource(
273       selectorPath,
274       currentResourceEvent.getResource,
275       logger
276     )
277   }
278
279   final protected def constructDummyFirstEventFor(
280       actualFirst: ResourceEventMsg,
281       newOccurredMillis: Long,
282       value: String
283   ): ResourceEventMsg = {
284
285     val dm = DetailsModel.make
286     DetailsModel.setBoolean(dm, MessageConstants.DetailsKeys.aquarium_is_synthetic)
287     DetailsModel.setBoolean(dm, MessageConstants.DetailsKeys.aquarium_is_dummy_first)
288     DetailsModel.setString(dm, MessageConstants.DetailsKeys.aquarium_reference_event_id, actualFirst.getOriginalID)
289     DetailsModel.setString(dm, MessageConstants.DetailsKeys.aquarium_reference_event_id_in_store, actualFirst.getInStoreID)
290
291     ResourceEventMsg.newBuilder(actualFirst).
292       setDetails(dm).
293       setValue(value).
294       setOccurredMillis(newOccurredMillis).
295       setReceivedMillis(newOccurredMillis).
296     build
297   }
298 }