ConfigReader.groovy
001 /*
002  * SPDX-License-Identifier: Apache-2.0
003  *
004  * Copyright 2008-2017 the original author or authors.
005  *
006  * Licensed under the Apache License, Version 2.0 (the "License");
007  * you may not use this file except in compliance with the License.
008  * You may obtain a copy of the License at
009  *
010  *     http://www.apache.org/licenses/LICENSE-2.0
011  *
012  * Unless required by applicable law or agreed to in writing, software
013  * distributed under the License is distributed on an "AS IS" BASIS,
014  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015  * See the License for the specific language governing permissions and
016  * limitations under the License.
017  */
018 package griffon.util
019 
020 import griffon.core.ApplicationClassLoader
021 import griffon.core.env.Environment
022 import griffon.core.env.GriffonEnvironment
023 import griffon.core.env.Metadata
024 import org.codehaus.groovy.runtime.InvokerHelper
025 
026 import javax.annotation.Nonnull
027 import javax.inject.Inject
028 
029 import static griffon.util.GriffonNameUtils.isBlank
030 import static griffon.util.GriffonNameUtils.isNotBlank
031 
032 /**
033  * Updated version of {@code groovy.util.ConfigSlurper}.<br/>
034  * New features include:
035  <ul>
036  *     <li>Ability to specify multiple conditional blocks, not just "environments".</li>
037  </ul>
038  *
039  @author Graeme Rocher (Groovy 1.5)
040  @author Andres Almiray
041  @since 2.0.0
042  */
043 @SuppressWarnings("rawtypes")
044 class ConfigReader {
045     private static final ENVIRONMENTS_METHOD = 'environments'
046     GroovyClassLoader classLoader
047     private Map bindingVars = [:]
048 
049     private Stack<String> currentConditionalBlock = new Stack<String>()
050     private final Map<String, String> conditionValues = [:]
051     private final Stack<Map<String, ConfigObject>> conditionalBlocks = new Stack<Map<String, ConfigObject>>()
052 
053     static class Provider implements javax.inject.Provider<ConfigReader> {
054         @Inject private ApplicationClassLoader applicationClassLoader
055         @Inject private Metadata metadata
056         @Inject private Environment environment
057 
058         @Override
059         ConfigReader get() {
060             ConfigReader configReader = new ConfigReader(applicationClassLoader)
061             configReader.setBinding(CollectionUtils.map()
062                 .e("userHome", System.getProperty("user.home"))
063                 .e("appName", metadata.getApplicationName())
064                 .e("appVersion", metadata.getApplicationVersion())
065                 .e("griffonVersion", GriffonEnvironment.getGriffonVersion()));
066             configReader.registerConditionalBlock("environments", environment.getName())
067             configReader.registerConditionalBlock("projects", metadata.getApplicationName())
068             configReader.registerConditionalBlock("platforms", GriffonApplicationUtils.getPlatform())
069             configReader
070         }
071     }
072 
073     @Inject
074     ConfigReader(@Nonnull ApplicationClassLoader applicationClassLoader) {
075         classLoader = new GroovyClassLoader(applicationClassLoader.get())
076     }
077 
078     void registerConditionalBlock(String blockName, String blockValue) {
079         if (isNotBlank(blockName)) {
080             if (isBlank(blockValue)) {
081                 conditionValues.remove(blockName)
082             else {
083                 conditionValues[blockName= blockValue
084             }
085         }
086     }
087 
088     Map<String, String> getConditionalBlockValues() {
089         Collections.unmodifiableMap(conditionValues)
090     }
091 
092     String getEnvironment() {
093         return conditionValues[ENVIRONMENTS_METHOD]
094     }
095 
096     void setEnvironment(String environment) {
097         conditionValues[ENVIRONMENTS_METHOD= environment
098     }
099 
100     void setBinding(Map vars) {
101         this.bindingVars = vars
102     }
103 
104     /**
105      * Parses a ConfigObject instances from an instance of java.util.Properties
106      @param The java.util.Properties instance
107      */
108     ConfigObject parse(Properties properties) {
109         ConfigObject config = new ConfigObject()
110         for (key in properties.keySet()) {
111             def tokens = key.split(/\./)
112 
113             def current = config
114             def last
115             def lastToken
116             def foundBase = false
117             for (token in tokens) {
118                 if (foundBase) {
119                     // handle not properly nested tokens by ignoring
120                     // hierarchy below this point
121                     lastToken += "." + token
122                     current = last
123                 else {
124                     last = current
125                     lastToken = token
126                     current = current."${token}"
127                     if (!(current instanceof ConfigObject)) foundBase = true
128                 }
129             }
130 
131             if (current instanceof ConfigObject) {
132                 if (last[lastToken]) {
133                     def flattened = last.flatten()
134                     last.clear()
135                     flattened.each k2, v2 -> last[k2= v2 }
136                     last[lastToken= properties.get(key)
137                 else {
138                     last[lastToken= properties.get(key)
139                 }
140             }
141             current = config
142         }
143         return config
144     }
145     /**
146      * Parse the given script as a string and return the configuration object
147      *
148      @see ConfigReader#parse(groovy.lang.Script)
149      */
150     ConfigObject parse(String script) {
151         return parse(classLoader.parseClass(script))
152     }
153 
154     /**
155      * Create a new instance of the given script class and parse a configuration object from it
156      *
157      @see ConfigReader#parse(groovy.lang.Script)
158      */
159     ConfigObject parse(Class<? extends Script> scriptClass) {
160         return parse(scriptClass.newInstance())
161     }
162 
163     /**
164      * Parse the given script into a configuration object (a Map)
165      @param script The script to parse
166      @return A Map of maps that can be navigating with dot de-referencing syntax to obtain configuration entries
167      */
168     ConfigObject parse(Script script) {
169         return parse(script, null)
170     }
171 
172     /**
173      * Parses a Script represented by the given URL into a ConfigObject
174      *
175      @param scriptLocation The location of the script to parse
176      @return The ConfigObject instance
177      */
178     ConfigObject parse(URL scriptLocation) {
179         return parse(classLoader.parseClass(scriptLocation.text).newInstance(), scriptLocation)
180     }
181 
182     /**
183      * Parses the passed groovy.lang.Script instance using the second argument to allow the ConfigObject
184      * to retain an reference to the original location other Groovy script
185      *
186      @param script The groovy.lang.Script instance
187      @param location The original location of the Script as a URL
188      @return The ConfigObject instance
189      */
190     ConfigObject parse(Script script, URL location) {
191         def config = location ? new ConfigObject(locationnew ConfigObject()
192         GroovySystem.metaClassRegistry.removeMetaClass(script.class)
193         def mc = script.class.metaClass
194         def prefix = ""
195         LinkedList stack = new LinkedList()
196         stack << [config: config, scope: [:]]
197         def pushStack = co ->
198             stack << [config: co, scope: stack.last.scope.clone()]
199         }
200         def assignName = name, co ->
201             def current = stack.last
202             /*
203             def cfg = current.config
204             if (cfg instanceof ConfigObject) {
205                 String[] keys = name.split(/\./)
206                 for (int i = 0; i < keys.length - 1; i++) {
207                     String key = keys[i]
208                     if (!cfg.containsKey(key)) {
209                         cfg[key] = new ConfigObject()
210                     }
211                     cfg = cfg.get(key)
212                 }
213                 name = keys[keys.length - 1]
214             }
215             cfg[name] = co
216             */
217             current.config[name= co
218             current.scope[name= co
219         }
220         mc.getProperty = String name ->
221             def current = stack.last
222             def result
223             if (current.config.get(name)) {
224                 result = current.config.get(name)
225             else if (current.scope[name]) {
226                 result = current.scope[name]
227             else {
228                 try {
229                     result = InvokerHelper.getProperty(this, name)
230                 catch (GroovyRuntimeException e) {
231                     result = new ConfigObject()
232                     assignName.call(name, result)
233                 }
234             }
235             result
236         }
237 
238         ConfigObject overrides = new ConfigObject()
239         mc.invokeMethod = String name, args ->
240             def result
241             if (args.length == && args[0instanceof Closure) {
242                 if (name in conditionValues.keySet()) {
243                     try {
244                         currentConditionalBlock.push(name)
245                         conditionalBlocks.push([:])
246                         args[0].call()
247                     finally {
248                         currentConditionalBlock.pop()
249                         for (entry in conditionalBlocks.pop().entrySet()) {
250                             def c = stack.last.config
251                             (c != config ? c : overrides).merge(entry.value)
252                         }
253                     }
254                 else if (currentConditionalBlock.size() 0) {
255                     String conditionalBlockKey = currentConditionalBlock.peek()
256                     if (name == conditionValues[conditionalBlockKey]) {
257                         def co = new ConfigObject()
258                         conditionalBlocks.peek()[conditionalBlockKey= co
259 
260                         pushStack.call(co)
261                         try {
262                             currentConditionalBlock.pop()
263                             args[0].call()
264                         finally {
265                             currentConditionalBlock.push(conditionalBlockKey)
266                         }
267                         stack.pop()
268                     }
269                 else {
270                     def co
271                     if (stack.last.config.get(nameinstanceof ConfigObject) {
272                         co = stack.last.config.get(name)
273                     else {
274                         co = new ConfigObject()
275                     }
276 
277                     assignName.call(name, co)
278                     pushStack.call(co)
279                     args[0].call()
280                     stack.pop()
281                 }
282             else if (args.length == && args[1instanceof Closure) {
283                 try {
284                     prefix = name + '.'
285                     assignName.call(name, args[0])
286                     args[1].call()
287                 finally prefix = "" }
288             else {
289                 MetaMethod mm = mc.getMetaMethod(name, args)
290                 if (mm) {
291                     result = mm.invoke(delegate, args)
292                 else {
293                     throw new MissingMethodException(name, getClass(), args)
294                 }
295             }
296             result
297         }
298         script.metaClass = mc
299 
300         def setProperty = String name, value ->
301             assignName.call(prefix + name, value)
302         }
303         def binding = new ConfigBinding(setProperty)
304         if (this.bindingVars) {
305             binding.getVariables().putAll(this.bindingVars)
306         }
307         script.binding = binding
308 
309         script.run()
310 
311         config.merge(overrides)
312 
313         return config
314     }
315 }