Fix NPE with initial user state
[aquarium] / src / main / scala / gr / grnet / aquarium / computation / state / parts / IMStateSnapshot.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.computation
37 package state
38 package parts
39
40 import gr.grnet.aquarium.event.model.im.IMEventModel
41 import gr.grnet.aquarium.util.shortClassNameOf
42 import gr.grnet.aquarium.util.date.MutableDateCalc
43 import gr.grnet.aquarium.computation.parts.RoleHistory
44 import gr.grnet.aquarium.util.json.JsonSupport
45
46 /**
47  *
48  * @author Christos KK Loverdos <loverdos@gmail.com>
49  */
50
51 case class IMStateSnapshot(
52     /**
53      * This is the latest processed IMEvent
54      */
55     latestIMEvent: IMEventModel,
56
57     /**
58      * The earliest activation time, if it exists.
59      */
60     userEarliestActivationMillis: Option[Long],
61
62     /**
63      * The user creation time, if it exists
64      */
65     userCreationMillis: Option[Long],
66
67     /**
68      * This is the recorded role history
69      */
70     roleHistory: RoleHistory
71 ) extends JsonSupport {
72
73   /**
74    * True iff the user has ever been activated even once.
75    */
76   def hasBeenActivated: Boolean = {
77     userEarliestActivationMillis.isDefined
78   }
79
80   def hasBeenCreated: Boolean = {
81     userCreationMillis.isDefined
82   }
83
84   /**
85    * Given the newly arrived event, we compute the updated user earliest activation time, if any.
86    * We always update activation time if it is earlier than the currently known activation time.
87    */
88   private[this] def updatedEarliestActivationTime(imEvent: IMEventModel): Option[Long] = {
89     this.userEarliestActivationMillis match {
90       case Some(activationMillis) if imEvent.isStateActive && activationMillis < imEvent.occurredMillis ⇒
91         Some(imEvent.occurredMillis)
92
93       case None if imEvent.isStateActive ⇒
94         Some(imEvent.occurredMillis)
95
96       case other ⇒
97         other
98     }
99   }
100
101   /**
102    * Given the newly arrived event, we compute the updated user creation time, if any.
103    * Only the first `create` event triggers an actual update.
104    */
105   private[this] def updatedCreationTime(imEvent: IMEventModel): Option[Long] = {
106     // Allow only the first `create` event
107     if(this.userCreationMillis.isDefined) {
108       this.userCreationMillis
109     } else if(imEvent.isCreateUser) {
110       Some(imEvent.occurredMillis)
111     } else {
112       None
113     }
114   }
115
116   /**
117    * Given the newly arrived event, we compute the updated role history.
118    */
119   private[this] def updatedRoleHistory(imEvent: IMEventModel): RoleHistory = {
120     this.roleHistory.updatedWithRole(imEvent.role, imEvent.occurredMillis)
121   }
122
123   /**
124    * Computes an updated state and returns a tuple made of four elements:
125    * a) the updated state, b) a `Boolean` indicating whether the user creation
126    * time has changed, c) a `Boolean` indicating whether the user activation
127    * time has changed and d) a `Boolean` indicating whether the user
128    * role history has changed.
129    *
130    * The role history is updated only if the `roleCheck` is not `None` and
131    * the role it represents is different than the role of the `imEvent`.
132    * The motivation for `roleCheck` is to use this method in a loop (as in replaying
133    * events from the [[gr.grnet.aquarium.store.IMEventStore]]).
134    */
135   def updatedWithEvent(imEvent: IMEventModel,
136                        roleCheck: Option[String]): (IMStateSnapshot, Boolean, Boolean, Boolean) = {
137     // Things of interest that may change by the imEvent:
138     // - user creation time
139     // - user activation time
140     // - user role
141
142     val newCreationTime = updatedCreationTime(imEvent)
143     val creationTimeChanged = this.userCreationMillis != newCreationTime
144
145     val newActivationTime = updatedEarliestActivationTime(imEvent)
146     val activationTimeChanged = this.userEarliestActivationMillis != newActivationTime
147
148     val (roleChanged, newRoleHistory) = roleCheck match {
149       case Some(role) if role != imEvent.role ⇒
150         (true, updatedRoleHistory(imEvent))
151
152       case _ ⇒
153         (false, this.roleHistory)
154     }
155
156     val newState = this.copy(
157       latestIMEvent      = imEvent,
158       userCreationMillis = newCreationTime,
159       userEarliestActivationMillis = newActivationTime,
160       roleHistory = newRoleHistory
161     )
162
163     (newState, creationTimeChanged, activationTimeChanged, roleChanged)
164   }
165
166   override def toString = {
167     "%s(\n!! %s\n!! %s\n!! %s\n!! %s)".format(
168       shortClassNameOf(this),
169       latestIMEvent,
170       userCreationMillis.map(new MutableDateCalc(_)),
171       userEarliestActivationMillis.map(new MutableDateCalc(_)),
172       roleHistory
173     )
174   }
175 }
176
177 object IMStateSnapshot {
178   def initial(imEvent: IMEventModel): IMStateSnapshot = {
179     IMStateSnapshot(
180       imEvent,
181       if(imEvent.isStateActive) Some(imEvent.occurredMillis) else None,
182       if(imEvent.isCreateUser) Some(imEvent.occurredMillis) else None,
183       RoleHistory.initial(imEvent.role, imEvent.occurredMillis))
184   }
185 }