Make withConfigurator viewable to the world
[aquarium] / src / main / scala / gr / grnet / aquarium / logic / accounting / Policy.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.accounting
37
38 import dsl.{Timeslot, DSLPolicy, DSL}
39 import gr.grnet.aquarium.Configurator._
40 import java.io.{InputStream, FileInputStream, File}
41 import java.util.Date
42 import gr.grnet.aquarium.util.date.TimeHelpers
43 import gr.grnet.aquarium.util.Loggable
44 import java.util.concurrent.atomic.AtomicReference
45 import gr.grnet.aquarium.Configurator
46 import com.ckkloverdos.maybe.{Failed, NoVal, Maybe, Just}
47 import collection.immutable.{TreeMap, SortedMap}
48
49 /**
50  * Searches for and loads the applicable accounting policy
51  *
52  * @author Georgios Gousios <gousiosg@gmail.com>
53  */
54 object Policy extends DSL with Loggable {
55
56   /* Pointer to the latest policy */
57   private[logic] lazy val policies = {
58     new AtomicReference[SortedMap[Timeslot, DSLPolicy]](reloadPolicies)
59   }
60
61   /* Pointer to the latest policy */
62   private lazy val currentPolicy = {new AtomicReference[DSLPolicy](latestPolicy)}
63
64   /* Configurator to use for loading information about the policy store */
65   private var config: Configurator = _
66
67   /**
68    * Get the latest defined policy.
69    */
70   def policy = currentPolicy.get
71
72   /**
73    * Get the policy that is valid at the specified time instance.
74    */
75   def policy(at: Date): DSLPolicy = {
76     policies.get.find {
77       a => (a._1.from.before(at) && a._1.to.after(at)) ||
78            (a._1.from.before(at) && a._1.to == Long.MaxValue)
79     } match {
80       case Some(x) => x._2
81       case None =>
82         throw new AccountingException("No valid policy for date: %s".format(at))
83     }
84   }
85
86   /**
87    * Get the policies that are valid between the specified time instances,
88    * in a map whose keys are sorted by time.
89    */
90   def policies(from: Date, to: Date): SortedMap[Timeslot, DSLPolicy] = {
91     policies.get.filter {
92       a => a._1.from.before(from) &&
93            a._1.to.after(to)
94     }
95   }
96
97   /**
98    * Get the policies that are valid throughout the specified
99    * [[gr.grnet.aquarium.logic.accounting.dsl.Timeslot]]
100    */
101   def policies(t: Timeslot): SortedMap[Timeslot, DSLPolicy] = policies(t.from, t.to)
102
103   /**
104    * Load and parse a policy from file.
105    */
106   def loadPolicyFromFile(pol: File): DSLPolicy = {
107
108     val stream = pol.exists() match {
109       case true =>
110         logger.info("Using policy file %s".format(pol.getAbsolutePath))
111         new FileInputStream(pol)
112       case false =>
113         logger.warn(("Cannot find user configured policy file %s, " +
114           "looking for default policy").format(pol.getAbsolutePath))
115         getClass.getClassLoader.getResourceAsStream("policy.yaml") match {
116           case x: InputStream =>
117             logger.warn("Using default policy, this is problably not what you want")
118             x
119           case null =>
120             logger.error("No valid policy file found, Aquarium will fail")
121             null
122         }
123     }
124     parse(stream)
125   }
126
127   /**
128    * Trigger a policy update cycle.
129    */
130   def updatePolicies = synchronized {
131     //XXX: The following update should happen as one transaction
132     val tmpPol = reloadPolicies
133     currentPolicy.set(latestPolicy)
134     policies.set(tmpPol)
135   }
136
137   /**
138    * Find the latest policy in the list of policies.
139    */
140   private def latestPolicy =
141     policies.get.foldLeft((Timeslot(new Date(1), new Date(2)) -> DSLPolicy.emptyPolicy)) {
142       (acc, p) =>
143         if (acc._2 == DSLPolicy.emptyPolicy)
144           p
145         else if (p._1.after(acc._1))
146           p
147         else
148           acc
149     }._2
150
151   /**
152    * Set the configurator to use for loading policy stores. Should only
153    * used for unit testing.
154    */
155   /*private[logic] */def withConfigurator(config: Configurator): Unit =
156     this.config = config
157
158   /**
159    * Search default locations for a policy file and provide an
160    * arbitrary default if this cannot be found.
161    */
162    private[logic] def policyFile = {
163     BasicResourceContext.getResource(PolicyConfName) match {
164       case Just(policyResource) ⇒
165         val path = policyResource.url.getPath
166         new File(path)
167       case _ ⇒
168         new File("policy.yaml")
169     }
170   }
171
172   /**
173    * Check whether the policy definition file (in whichever path) is
174    * newer than the latest stored policy, reload and set it as current.
175    * This method has side-effects to this object's state.
176    */
177   private[logic] def reloadPolicies: SortedMap[Timeslot, DSLPolicy] =
178     if (config == null)
179       reloadPolicies(MasterConfigurator)
180     else
181       reloadPolicies(config)
182
183   private def reloadPolicies(config: Configurator):
184   SortedMap[Timeslot, DSLPolicy] = {
185     //1. Load policies from db
186     val pol = config.policyStore.loadPolicies(0)
187
188     //2. Check whether policy file has been updated
189     val latestPolicyChange = if (pol.isEmpty) 0 else pol.last.validFrom
190     val policyf = policyFile
191     var updated = false
192
193     if (policyf.exists) {
194       if (policyf.lastModified > latestPolicyChange) {
195         logger.info("Policy file updated since last check, reloading")
196         updated = true
197       } else {
198         logger.info("Policy file not changed since last check")
199       }
200     } else {
201       logger.warn("User specified policy file %s does not exist, " +
202         "using stored policy information".format(policyf.getAbsolutePath))
203     }
204
205     if (updated) {
206       val ts = TimeHelpers.nowMillis
207       val parsedNew = loadPolicyFromFile(policyf)
208       val newPolicy = parsedNew.toPolicyEntry.copy(occurredMillis = ts,
209         receivedMillis = ts, validFrom = ts)
210
211       config.policyStore.findPolicy(newPolicy.id) match {
212         case Just(x) =>
213           logger.warn("Policy file contents not modified")
214         case NoVal =>
215           if (!pol.isEmpty) {
216             val toUpdate = pol.last.copy(validTo = ts - 1)
217             config.policyStore.updatePolicy(toUpdate)
218             config.policyStore.storePolicy(newPolicy)
219           } else {
220             config.policyStore.storePolicy(newPolicy)
221           }
222         case Failed(e, expl) =>
223           throw e
224       }
225     }
226
227     config.policyStore.loadPolicies(0).foldLeft(new TreeMap[Timeslot, DSLPolicy]()){
228       (acc, p) =>
229         acc ++ Map(Timeslot(new Date(p.validFrom), new Date(p.validTo)) -> parse(p.policyYAML))
230     }
231   }
232 }