001 /*
002 * Copyright 2011-2013 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017 package griffon.util;
018
019 import groovy.util.ConfigObject;
020 import org.codehaus.groovy.runtime.DefaultGroovyMethods;
021 import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;
022 import org.slf4j.Logger;
023 import org.slf4j.LoggerFactory;
024
025 import java.io.FileInputStream;
026 import java.io.FileNotFoundException;
027 import java.io.IOException;
028 import java.io.InputStream;
029 import java.util.Locale;
030 import java.util.Map;
031 import java.util.Properties;
032
033 import static griffon.util.ApplicationHolder.getApplication;
034 import static griffon.util.GriffonExceptionHandler.sanitize;
035 import static griffon.util.GriffonNameUtils.isBlank;
036
037 /**
038 * Utility class for reading configuration properties.
039 *
040 * @author Andres Almiray
041 */
042 public final class ConfigUtils {
043 private static final Logger LOG = LoggerFactory.getLogger(ConfigUtils.class);
044 private static final String PROPERTIES_SUFFIX = "properties";
045 private static final String GROOVY_SUFFIX = "groovy";
046
047 private ConfigUtils() {
048 // prevent instantiation
049 }
050
051 /**
052 * Returns true if there's a on-null value for the specified key.
053 *
054 * @param config the configuration object to be searched upon
055 * @param key the key to be searched
056 * @return true if there's a value for the specified key, false otherwise
057 */
058 public static boolean isValueDefined(Map config, String key) {
059 String[] keys = key.split("\\.");
060 for (int i = 0; i < keys.length - 1; i++) {
061 if (config != null) {
062 config = (Map) config.get(keys[i]);
063 } else {
064 return false;
065 }
066 }
067 if (config == null) return false;
068 Object value = config.get(keys[keys.length - 1]);
069 return value != null;
070 }
071
072 /**
073 * Returns the value for the specified key.
074 *
075 * @param config the configuration object to be searched upon
076 * @param key the key to be searched
077 * @return the value of the key. May return null
078 */
079 public static Object getConfigValue(Map config, String key) {
080 return getConfigValue(config, key, null);
081 }
082
083 /**
084 * Returns the value for the specified key with an optional default value if no match is found.
085 *
086 * @param config the configuration object to be searched upon
087 * @param key the key to be searched
088 * @param defaultValue the value to send back if no match is found
089 * @return the value of the key or the default value if no match is found
090 */
091 public static Object getConfigValue(Map config, String key, Object defaultValue) {
092 String[] keys = key.split("\\.");
093 for (int i = 0; i < keys.length - 1; i++) {
094 if (config != null) {
095 Object node = config.get(keys[i]);
096 if (node instanceof Map) {
097 config = (Map) node;
098 } else {
099 return defaultValue;
100 }
101 } else {
102 return defaultValue;
103 }
104 }
105 if (config == null) return defaultValue;
106 Object value = config.get(keys[keys.length - 1]);
107 return value != null ? value : defaultValue;
108 }
109
110 /**
111 * Returns the value for the specified key coerced to a boolean.
112 *
113 * @param config the configuration object to be searched upon
114 * @param key the key to be searched
115 * @return the value of the key. Returns {@code false} if no match.
116 */
117 public static boolean getConfigValueAsBoolean(Map config, String key) {
118 return getConfigValueAsBoolean(config, key, false);
119 }
120
121 /**
122 * Returns the value for the specified key with an optional default value if no match is found.
123 *
124 * @param config the configuration object to be searched upon
125 * @param key the key to be searched
126 * @param defaultValue the value to send back if no match is found
127 * @return the value of the key or the default value if no match is found
128 */
129 public static boolean getConfigValueAsBoolean(Map config, String key, boolean defaultValue) {
130 Object value = getConfigValue(config, key, defaultValue);
131 return DefaultTypeTransformation.castToBoolean(value);
132 }
133
134 /**
135 * Returns the value for the specified key coerced to an int.
136 *
137 * @param config the configuration object to be searched upon
138 * @param key the key to be searched
139 * @return the value of the key. Returns {@code 0} if no match.
140 */
141 public static int getConfigValueAsInt(Map config, String key) {
142 return getConfigValueAsInt(config, key, 0);
143 }
144
145 /**
146 * Returns the value for the specified key with an optional default value if no match is found.
147 *
148 * @param config the configuration object to be searched upon
149 * @param key the key to be searched
150 * @param defaultValue the value to send back if no match is found
151 * @return the value of the key or the default value if no match is found
152 */
153 public static int getConfigValueAsInt(Map config, String key, int defaultValue) {
154 Object value = getConfigValue(config, key, defaultValue);
155 return DefaultTypeTransformation.castToNumber(value).intValue();
156 }
157
158 /**
159 * Returns the value for the specified key converted to a String.
160 *
161 * @param config the configuration object to be searched upon
162 * @param key the key to be searched
163 * @return the value of the key. Returns {@code ""} if no match.
164 */
165 public static String getConfigValueAsString(Map config, String key) {
166 return getConfigValueAsString(config, key, "");
167 }
168
169 /**
170 * Returns the value for the specified key with an optional default value if no match is found.
171 *
172 * @param config the configuration object to be searched upon
173 * @param key the key to be searched
174 * @param defaultValue the value to send back if no match is found
175 * @return the value of the key or the default value if no match is found
176 */
177 public static String getConfigValueAsString(Map config, String key, String defaultValue) {
178 Object value = getConfigValue(config, key, defaultValue);
179 return String.valueOf(value);
180 }
181
182 /**
183 * Merges two maps using <tt>ConfigObject.merge()</tt>.
184 *
185 * @param defaults configuration values available by default
186 * @param overrides configuration values that override defaults
187 * @return the result of merging both maps
188 */
189 public static Map merge(Map defaults, Map overrides) {
190 ConfigObject configDefaults = new ConfigObject();
191 ConfigObject configOverrides = new ConfigObject();
192 configDefaults.putAll(defaults);
193 configOverrides.putAll(overrides);
194 return configDefaults.merge(configOverrides);
195 }
196
197 /**
198 * Creates a new {@code ConfigReader} instance configured with default conditional blocks.<br/>
199 * The following list enumerates the conditional blocks that get registered automatically:
200 * <ul>
201 * <li><strong>environments</strong> = <tt>Environment.getCurrent().getName()</tt></strong></li>
202 * <li><strong>projects</strong> = <tt>Metadata.getCurrent().getApplicationName()</tt></strong></li>
203 * <li><strong>platforms</strong> = <tt>GriffonApplicationUtils.getFullPlatform()</tt></strong></li>
204 * </ul>
205 *
206 * @return a newly instantiated {@code ConfigReader}.
207 * @since 1.1.0
208 */
209 public static ConfigReader createConfigReader() {
210 ConfigReader configReader = new ConfigReader();
211 configReader.registerConditionalBlock("environments", Environment.getCurrent().getName());
212 configReader.registerConditionalBlock("projects", Metadata.getCurrent().getApplicationName());
213 configReader.registerConditionalBlock("platforms", GriffonApplicationUtils.getPlatform());
214 return configReader;
215 }
216
217 /**
218 * Loads configuration settings defined in a Groovy script and a properties file as fallback.<br/>
219 * The name of the script matches the name of the file.
220 *
221 * @param configFileName the configuration file
222 * @return a merged configuration between the script and the alternate file. The file has precedence over the script.
223 * @since 1.1.0
224 */
225 public static ConfigObject loadConfig(String configFileName) {
226 return loadConfig(createConfigReader(), safeLoadClass(configFileName), configFileName);
227 }
228
229 /**
230 * Loads configuration settings defined in a Groovy script and a properties file as fallback.<br/>
231 * The alternate properties file matches the simple name of the script.
232 *
233 * @param configClass the script's class, may be null
234 * @return a merged configuration between the script and the alternate file. The file has precedence over the script.
235 * @since 1.1.0
236 */
237 public static ConfigObject loadConfig(Class configClass) {
238 return loadConfig(createConfigReader(), configClass, configClass.getSimpleName());
239 }
240
241 /**
242 * Loads configuration settings defined in a Groovy script and a properties file as fallback.
243 *
244 * @param configClass the script's class, may be null
245 * @param configFileName the alternate configuration file
246 * @return a merged configuration between the script and the alternate file. The file has precedence over the script.
247 * @since 1.1.0
248 */
249 public static ConfigObject loadConfig(Class configClass, String configFileName) {
250 return loadConfig(createConfigReader(), configClass, configFileName);
251 }
252
253 /**
254 * Loads configuration settings defined in a Groovy script and a properties file as fallback.
255 *
256 * @param configReader a ConfigReader instance already configured
257 * @param configClass the script's class, may be null
258 * @param configFileName the alternate configuration file
259 * @return a merged configuration between the script and the alternate file. The file has precedence over the script.
260 * @since 1.1.0
261 */
262 public static ConfigObject loadConfig(ConfigReader configReader, Class configClass, String configFileName) {
263 ConfigObject config = new ConfigObject();
264 try {
265 if (configClass != null) {
266 config.merge(configReader.parse(configClass));
267 }
268 config.merge(loadConfigFile(configReader, configFileName));
269 } catch (FileNotFoundException fnfe) {
270 // ignore
271 } catch (Exception x) {
272 if (LOG.isWarnEnabled()) {
273 LOG.warn("Cannot read configuration [class: " + configClass + ", file: " + configFileName + "]", sanitize(x));
274 }
275 }
276 return config;
277 }
278
279 private static ConfigObject loadConfigFile(ConfigReader configReader, String configFileName) throws IOException {
280 ConfigObject config = new ConfigObject();
281
282 if (isBlank(configFileName)) return config;
283
284 String fileNameExtension = getFilenameExtension(configFileName);
285 if (isBlank(fileNameExtension)) {
286 configFileName += "." + PROPERTIES_SUFFIX;
287 fileNameExtension = PROPERTIES_SUFFIX;
288 }
289
290 if (PROPERTIES_SUFFIX.equals(fileNameExtension)) {
291 InputStream is = null;
292 if (configFileName.startsWith("/")) {
293 is = new FileInputStream(configFileName);
294 } else {
295 is = ApplicationClassLoader.get().getResourceAsStream(configFileName);
296 }
297 if (is != null) {
298 Properties p = new Properties();
299 p.load(is);
300 config = configReader.parse(p);
301 is.close();
302 }
303 } else if (GROOVY_SUFFIX.equals(fileNameExtension)) {
304 InputStream is = null;
305 if (configFileName.startsWith("/")) {
306 is = new FileInputStream(configFileName);
307 } else {
308 is = ApplicationClassLoader.get().getResourceAsStream(configFileName);
309 }
310 if (is != null) {
311 String scriptText = DefaultGroovyMethods.getText(is);
312 if (!isBlank(scriptText)) {
313 config = configReader.parse(scriptText);
314 }
315 }
316 } else {
317 if (LOG.isInfoEnabled()) {
318 LOG.info("Invalid configuration [file: " + configFileName + "]. Skipping");
319 }
320 }
321
322 return config;
323 }
324
325 /**
326 * Loads configuration settings defined in a Groovy script and a properties file. The script and file names
327 * are Locale aware.<p>
328 * The name of the script matches the name of the file.<br/>
329 * The following suffixes will be used besides the base names for script and file
330 * <ul>
331 * <li>locale.getLanguage()</li>
332 * <li>locale.getLanguage() + "_" + locale.getCountry()</li>
333 * <li>locale.getLanguage() + "_" + locale.getCountry() + "_" + locale.getVariant()</li>
334 * </ul>
335 *
336 * @param baseConfigFileName the configuration file
337 * @return a merged configuration between the script and the alternate file. The file has precedence over the script.
338 * @since 1.1.0
339 */
340 public static ConfigObject loadConfigWithI18n(String baseConfigFileName) {
341 return loadConfigWithI18n(getApplication().getLocale(), createConfigReader(), safeLoadClass(baseConfigFileName), baseConfigFileName);
342 }
343
344 /**
345 * Loads configuration settings defined in a Groovy script and a properties file. The script and file names
346 * are Locale aware.<p>
347 * The alternate properties file matches the simple name of the script.<br/>
348 * The following suffixes will be used besides the base names for script and file
349 * <ul>
350 * <li>locale.getLanguage()</li>
351 * <li>locale.getLanguage() + "_" + locale.getCountry()</li>
352 * <li>locale.getLanguage() + "_" + locale.getCountry() + "_" + locale.getVariant()</li>
353 * </ul>
354 *
355 * @param baseConfigClass the script's class
356 * @return a merged configuration between the script and the alternate file. The file has precedence over the script.
357 * @since 1.1.0
358 */
359 public static ConfigObject loadConfigWithI18n(Class baseConfigClass) {
360 return loadConfigWithI18n(getApplication().getLocale(), createConfigReader(), baseConfigClass, baseConfigClass.getSimpleName());
361 }
362
363 /**
364 * Loads configuration settings defined in a Groovy script and a properties file. The script and file names
365 * are Locale aware.<p>
366 * The following suffixes will be used besides the base names for script and file
367 * <ul>
368 * <li>locale.getLanguage()</li>
369 * <li>locale.getLanguage() + "_" + locale.getCountry()</li>
370 * <li>locale.getLanguage() + "_" + locale.getCountry() + "_" + locale.getVariant()</li>
371 * </ul>
372 *
373 * @param baseConfigClass the script's class, may be null
374 * @param baseConfigFileName the alternate configuration file
375 * @return a merged configuration between the script and the alternate file. The file has precedence over the script.
376 * @since 1.1.0
377 */
378 public static ConfigObject loadConfigWithI18n(Class baseConfigClass, String baseConfigFileName) {
379 return loadConfigWithI18n(getApplication().getLocale(), createConfigReader(), baseConfigClass, baseConfigFileName);
380 }
381
382 /**
383 * Loads configuration settings defined in a Groovy script and a properties file. The script and file names
384 * are Locale aware.<p>
385 * The following suffixes will be used besides the base names for script and file
386 * <ul>
387 * <li>locale.getLanguage()</li>
388 * <li>locale.getLanguage() + "_" + locale.getCountry()</li>
389 * <li>locale.getLanguage() + "_" + locale.getCountry() + "_" + locale.getVariant()</li>
390 * </ul>
391 *
392 * @param locale the locale to use
393 * @param configReader a ConfigReader instance already configured
394 * @param baseConfigClass the script's class, may be null
395 * @param baseConfigFileName the alternate configuration file
396 * @return a merged configuration between the script and the alternate file. The file has precedence over the script.
397 * @since 1.1.0
398 */
399 public static ConfigObject loadConfigWithI18n(Locale locale, ConfigReader configReader, Class baseConfigClass, String baseConfigFileName) {
400 ConfigObject config = loadConfig(configReader, baseConfigClass, baseConfigFileName);
401 String[] combinations = {
402 locale.getLanguage(),
403 locale.getLanguage() + "_" + locale.getCountry(),
404 locale.getLanguage() + "_" + locale.getCountry() + "_" + locale.getVariant()
405 };
406
407 String baseClassName = baseConfigClass != null ? baseConfigClass.getName() : null;
408 String fileExtension = !isBlank(baseConfigFileName) ? getFilenameExtension(baseConfigFileName) : null;
409 for (String suffix : combinations) {
410 if (isBlank(suffix) || suffix.endsWith("_")) continue;
411 if (baseClassName != null) {
412 Class configClass = safeLoadClass(baseClassName + "_" + suffix);
413 if (configClass != null) config.merge(configReader.parse(configClass));
414 }
415
416 if (fileExtension == null) continue;
417 String configFileName = stripFilenameExtension(baseConfigFileName) + "_" + suffix + "." + fileExtension;
418 try {
419 config.merge(loadConfigFile(configReader, configFileName));
420 } catch (FileNotFoundException fne) {
421 // ignore
422 } catch (IOException e) {
423 if (LOG.isWarnEnabled()) {
424 LOG.warn("Cannot read configuration [file: " + configFileName + "]", sanitize(e));
425 }
426 }
427 }
428
429 return config;
430 }
431
432 public static Class loadClass(String className) throws ClassNotFoundException {
433 ClassNotFoundException cnfe = null;
434
435 ClassLoader cl = ApplicationClassLoader.get();
436 try {
437 return cl.loadClass(className);
438 } catch (ClassNotFoundException e) {
439 cnfe = e;
440 }
441
442 cl = Thread.currentThread().getContextClassLoader();
443 try {
444 return cl.loadClass(className);
445 } catch (ClassNotFoundException e) {
446 cnfe = e;
447 }
448
449 if (cnfe != null) throw cnfe;
450 return null;
451 }
452
453 public static Class safeLoadClass(String className) {
454 try {
455 return loadClass(className);
456 } catch (ClassNotFoundException e) {
457 return null;
458 }
459 }
460
461 // the following taken from SpringFramework::org.springframework.util.StringUtils
462
463 /**
464 * Extract the filename extension from the given path,
465 * e.g. "mypath/myfile.txt" -> "txt".
466 *
467 * @param path the file path (may be <code>null</code>)
468 * @return the extracted filename extension, or <code>null</code> if none
469 */
470 public static String getFilenameExtension(String path) {
471 if (path == null) {
472 return null;
473 }
474 int extIndex = path.lastIndexOf(".");
475 if (extIndex == -1) {
476 return null;
477 }
478 int folderIndex = path.lastIndexOf("/");
479 if (folderIndex > extIndex) {
480 return null;
481 }
482 return path.substring(extIndex + 1);
483 }
484
485 /**
486 * Strip the filename extension from the given path,
487 * e.g. "mypath/myfile.txt" -> "mypath/myfile".
488 *
489 * @param path the file path (may be <code>null</code>)
490 * @return the path with stripped filename extension,
491 * or <code>null</code> if none
492 */
493 public static String stripFilenameExtension(String path) {
494 if (path == null) {
495 return null;
496 }
497 int extIndex = path.lastIndexOf(".");
498 if (extIndex == -1) {
499 return path;
500 }
501 int folderIndex = path.lastIndexOf("/");
502 if (folderIndex > extIndex) {
503 return path;
504 }
505 return path.substring(0, extIndex);
506 }
507 }
|