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 = (Mapconfig.get(keys[i]);
063             else {
064                 return false;
065             }
066         }
067         if (config == nullreturn 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 = (Mapnode;
098                 else {
099                     return defaultValue;
100                 }
101             else {
102                 return defaultValue;
103             }
104         }
105         if (config == nullreturn 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 configFileNamethrows 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(baseConfigFileNamenull;
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 != nullconfig.merge(configReader.parse(configClass));
414             }
415 
416             if (fileExtension == nullcontinue;
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 classNamethrows 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 != nullthrow 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 }