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(location) : new 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 == 1 && args[0] instanceof 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(name) instanceof 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 == 2 && args[1] instanceof 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 }
|