ConfigUtils.java
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 }