1 // This file is part of OpenTSDB.
2 // Copyright (C) 2010-2012  The OpenTSDB Authors.
3 //
4 // This program is free software: you can redistribute it and/or modify it
5 // under the terms of the GNU Lesser General Public License as published by
6 // the Free Software Foundation, either version 2.1 of the License, or (at your
7 // option) any later version.  This program is distributed in the hope that it
8 // will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
9 // of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser
10 // General Public License for more details.  You should have received a copy
11 // of the GNU Lesser General Public License along with this program.  If not,
12 // see <http://www.gnu.org/licenses/>.
13 package net.opentsdb.utils;
14 
15 import java.io.FileInputStream;
16 import java.io.FileNotFoundException;
17 import java.io.IOException;
18 import java.util.ArrayList;
19 import java.util.Enumeration;
20 import java.util.HashMap;
21 import java.util.Map;
22 import java.util.Properties;
23 
24 import org.slf4j.Logger;
25 import org.slf4j.LoggerFactory;
26 
27 import com.google.common.collect.ImmutableMap;
28 
29 /**
30  * OpenTSDB Configuration Class
31  *
32  * This handles all of the user configurable variables for a TSD. On
33  * initialization default values are configured for all variables. Then
34  * implementations should call the {@link #loadConfig()} methods to search for a
35  * default configuration or try to load one provided by the user.
36  *
37  * To add a configuration, simply set a default value in {@link #setDefaults()}.
38  * Wherever you need to access the config value, use the proper helper to fetch
39  * the value, accounting for exceptions that may be thrown if necessary.
40  *
41  * The get<type> number helpers will return NumberFormatExceptions if the
42  * requested property is null or unparseable. The {@link #getString(String)}
43  * helper will return a NullPointerException if the property isn't found.
44  * <p>
45  * Plugins can extend this class and copy the properties from the main
46  * TSDB.config instance. Plugins should never change the main TSD's config
47  * properties, rather a plugin should use the Config(final Config parent)
48  * constructor to get a copy of the parent's properties and then work with the
49  * values locally.
50  * @since 2.0
51  */
52 public class Config {
53   private static final Logger LOG = LoggerFactory.getLogger(Config.class);
54 
55   /** Flag to determine if we're running under Windows or not */
56   public static final boolean IS_WINDOWS =
57       System.getProperty("os.name", "").contains("Windows");
58 
59   // These are accessed often so need a set address for fast access (faster
60   // than accessing the map. Their value will be changed when the config is
61   // loaded
62   // NOTE: edit the setDefaults() method if you add a public field
63 
64   /** tsd.core.auto_create_metrics */
65   private boolean auto_metric = false;
66 
67   /** tsd.core.auto_create_tagk */
68   private boolean auto_tagk = true;
69 
70   /** tsd.core.auto_create_tagv */
71   private boolean auto_tagv = true;
72 
73   /** tsd.storage.enable_compaction */
74   private boolean enable_compactions = true;
75 
76   /** tsd.storage.enable_appends */
77   private boolean enable_appends = false;
78 
79   /** tsd.storage.repair_appends */
80   private boolean repair_appends = false;
81 
82   /** tsd.core.meta.enable_realtime_ts */
83   private boolean enable_realtime_ts = false;
84 
85   /** tsd.core.meta.enable_realtime_uid */
86   private boolean enable_realtime_uid = false;
87 
88   /** tsd.core.meta.enable_tsuid_incrementing */
89   private boolean enable_tsuid_incrementing = false;
90 
91   /** tsd.core.meta.enable_tsuid_tracking */
92   private boolean enable_tsuid_tracking = false;
93 
94   /** tsd.http.request.enable_chunked */
95   private boolean enable_chunked_requests = false;
96 
97   /** tsd.storage.fix_duplicates */
98   private boolean fix_duplicates = false;
99 
100   /** tsd.http.request.max_chunk */
101   private int max_chunked_requests = 4096;
102 
103   /** tsd.core.tree.enable_processing */
104   private boolean enable_tree_processing = false;
105 
106   /** tsd.storage.hbase.scanner.maxNumRows */
107   private int scanner_max_num_rows = 128;
108 
109   /**
110    * The list of properties configured to their defaults or modified by users
111    */
112   protected final HashMap<String, String> properties =
113     new HashMap<String, String>();
114 
115   /** Holds default values for the config */
116   protected static final HashMap<String, String> default_map =
117     new HashMap<String, String>();
118 
119   /** Tracks the location of the file that was actually loaded */
120   protected String config_location;
121 
122   /**
123    * Constructor that initializes default configuration values. May attempt to
124    * search for a config file if configured.
125    * @param auto_load_config When set to true, attempts to search for a config
126    *          file in the default locations
127    * @throws IOException Thrown if unable to read or parse one of the default
128    *           config files
129    */
Config(final boolean auto_load_config)130   public Config(final boolean auto_load_config) throws IOException {
131     if (auto_load_config) {
132       loadConfig();
133     }
134     setDefaults();
135   }
136 
137   /**
138    * Constructor that initializes default values and attempts to load the given
139    * properties file
140    * @param file Path to the file to load
141    * @throws IOException Thrown if unable to read or parse the file
142    */
Config(final String file)143   public Config(final String file) throws IOException {
144     loadConfig(file);
145     setDefaults();
146   }
147 
148   /**
149    * Constructor for plugins or overloaders who want a copy of the parent
150    * properties but without the ability to modify them
151    *
152    * This constructor will not re-read the file, but it will copy the location
153    * so if a child wants to reload the properties periodically, they may do so
154    * @param parent Parent configuration object to load from
155    */
Config(final Config parent)156   public Config(final Config parent) {
157     // copy so changes to the local props by the plugin don't affect the master
158     properties.putAll(parent.properties);
159     config_location = parent.config_location;
160     setDefaults();
161   }
162 
163   /** @return The file that generated this config. May be null */
configLocation()164   public String configLocation() {
165     return config_location;
166   }
167 
168   /** @return the auto_metric value */
auto_metric()169   public boolean auto_metric() {
170     return auto_metric;
171   }
172 
173   /** @return the auto_tagk value */
auto_tagk()174   public boolean auto_tagk() {
175     return auto_tagk;
176   }
177 
178   /** @return the auto_tagv value */
auto_tagv()179   public boolean auto_tagv() {
180     return auto_tagv;
181   }
182 
183   /** @param auto_metric whether or not to auto create metrics */
setAutoMetric(boolean auto_metric)184   public void setAutoMetric(boolean auto_metric) {
185     this.auto_metric = auto_metric;
186     properties.put("tsd.core.auto_create_metrics",
187         Boolean.toString(auto_metric));
188   }
189 
190   /** @return the enable_compaction value */
enable_compactions()191   public boolean enable_compactions() {
192     return enable_compactions;
193   }
194 
195   /** @return whether or not to write data in the append format */
enable_appends()196   public boolean enable_appends() {
197     return enable_appends;
198   }
199 
200   /** @return whether or not to re-write appends with duplicates or out of order
201    * data when queried. */
repair_appends()202   public boolean repair_appends() {
203     return repair_appends;
204   }
205 
206   /** @return whether or not to record new TSMeta objects in real time */
enable_realtime_ts()207   public boolean enable_realtime_ts() {
208     return enable_realtime_ts;
209   }
210 
211   /** @return whether or not record new UIDMeta objects in real time */
enable_realtime_uid()212   public boolean enable_realtime_uid() {
213     return enable_realtime_uid;
214   }
215 
216   /** @return whether or not to increment TSUID counters */
enable_tsuid_incrementing()217   public boolean enable_tsuid_incrementing() {
218     return enable_tsuid_incrementing;
219   }
220 
221   /** @return whether or not to record a 1 for every TSUID */
enable_tsuid_tracking()222   public boolean enable_tsuid_tracking() {
223     return enable_tsuid_tracking;
224   }
225 
226   /** @return maximum number of rows to be fetched per round trip while scanning HBase */
scanner_maxNumRows()227   public int scanner_maxNumRows() {
228     return scanner_max_num_rows;
229   }
230 
231   /** @return whether or not chunked requests are supported */
enable_chunked_requests()232   public boolean enable_chunked_requests() {
233     return enable_chunked_requests;
234   }
235 
236   /** @return max incoming chunk size in bytes */
max_chunked_requests()237   public int max_chunked_requests() {
238     return max_chunked_requests;
239   }
240 
241   /** @return true if duplicate values should be fixed */
fix_duplicates()242   public boolean fix_duplicates() {
243     return fix_duplicates;
244   }
245 
246   /** @param fix_duplicates true if duplicate values should be fixed */
setFixDuplicates(final boolean fix_duplicates)247   public void setFixDuplicates(final boolean fix_duplicates) {
248     this.fix_duplicates = fix_duplicates;
249   }
250 
251   /** @return whether or not to process new or updated TSMetas through trees */
enable_tree_processing()252   public boolean enable_tree_processing() {
253     return enable_tree_processing;
254   }
255 
256   /**
257    * Allows for modifying properties after creation or loading.
258    *
259    * WARNING: This should only be used on initialization and is meant for
260    * command line overrides. Also note that it will reset all static config
261    * variables when called.
262    *
263    * @param property The name of the property to override
264    * @param value The value to store
265    */
overrideConfig(final String property, final String value)266   public void overrideConfig(final String property, final String value) {
267     properties.put(property, value);
268     loadStaticVariables();
269   }
270 
271   /**
272    * Returns the given property as a String
273    * @param property The property to load
274    * @return The property value as a string
275    * @throws NullPointerException if the property did not exist
276    */
getString(final String property)277   public final String getString(final String property) {
278     return properties.get(property);
279   }
280 
281   /**
282    * Returns the given property as an integer
283    * @param property The property to load
284    * @return A parsed integer or an exception if the value could not be parsed
285    * @throws NumberFormatException if the property could not be parsed
286    * @throws NullPointerException if the property did not exist
287    */
getInt(final String property)288   public final int getInt(final String property) {
289     return Integer.parseInt(sanitize(properties.get(property)));
290   }
291 
292   /**
293    * Returns the given string trimed or null if is null
294    * @param string The string be trimmed of
295    * @return The string trimed or null
296   */
sanitize(final String string)297   private final String sanitize(final String string) {
298     if (string == null) {
299       return null;
300     }
301 
302     return string.trim();
303   }
304 
305   /**
306    * Returns the given property as a short
307    * @param property The property to load
308    * @return A parsed short or an exception if the value could not be parsed
309    * @throws NumberFormatException if the property could not be parsed
310    * @throws NullPointerException if the property did not exist
311    */
getShort(final String property)312   public final short getShort(final String property) {
313     return Short.parseShort(sanitize(properties.get(property)));
314   }
315 
316   /**
317    * Returns the given property as a long
318    * @param property The property to load
319    * @return A parsed long or an exception if the value could not be parsed
320    * @throws NumberFormatException if the property could not be parsed
321    * @throws NullPointerException if the property did not exist
322    */
getLong(final String property)323   public final long getLong(final String property) {
324     return Long.parseLong(sanitize(properties.get(property)));
325   }
326 
327   /**
328    * Returns the given property as a float
329    * @param property The property to load
330    * @return A parsed float or an exception if the value could not be parsed
331    * @throws NumberFormatException if the property could not be parsed
332    * @throws NullPointerException if the property did not exist
333    */
getFloat(final String property)334   public final float getFloat(final String property) {
335     return Float.parseFloat(sanitize(properties.get(property)));
336   }
337 
338   /**
339    * Returns the given property as a double
340    * @param property The property to load
341    * @return A parsed double or an exception if the value could not be parsed
342    * @throws NumberFormatException if the property could not be parsed
343    * @throws NullPointerException if the property did not exist
344    */
getDouble(final String property)345   public final double getDouble(final String property) {
346     return Double.parseDouble(sanitize(properties.get(property)));
347   }
348 
349   /**
350    * Returns the given property as a boolean
351    *
352    * Property values are case insensitive and the following values will result
353    * in a True return value: - 1 - True - Yes
354    *
355    * Any other values, including an empty string, will result in a False
356    *
357    * @param property The property to load
358    * @return A parsed boolean
359    * @throws NullPointerException if the property was not found
360    */
getBoolean(final String property)361   public final boolean getBoolean(final String property) {
362     final String val = properties.get(property).trim().toUpperCase();
363     if (val.equals("1"))
364       return true;
365     if (val.equals("TRUE"))
366       return true;
367     if (val.equals("YES"))
368       return true;
369     return false;
370   }
371 
372   /**
373    * Returns the directory name, making sure the end is an OS dependent slash
374    * @param property The property to load
375    * @return The property value with a forward or back slash appended or null
376    * if the property wasn't found or the directory was empty.
377    */
getDirectoryName(final String property)378   public final String getDirectoryName(final String property) {
379     String directory = properties.get(property);
380     if (directory == null || directory.isEmpty()){
381       return null;
382     }
383     if (IS_WINDOWS) {
384       // Windows swings both ways. If a forward slash was already used, we'll
385       // add one at the end if missing. Otherwise use the windows default of \
386       if (directory.charAt(directory.length() - 1) == '\\' ||
387           directory.charAt(directory.length() - 1) == '/') {
388         return directory;
389       }
390       if (directory.contains("/")) {
391         return directory + "/";
392       }
393       return directory + "\\";
394     }
395     if (directory.contains("\\")) {
396       throw new IllegalArgumentException(
397           "Unix path names cannot contain a back slash");
398     }
399 
400     if (directory == null || directory.isEmpty()){
401     	return null;
402     }
403 
404     if (directory.charAt(directory.length() - 1) == '/') {
405       return directory;
406     }
407     return directory + "/";
408   }
409 
410   /**
411    * Determines if the given propery is in the map
412    * @param property The property to search for
413    * @return True if the property exists and has a value, not an empty string
414    */
hasProperty(final String property)415   public final boolean hasProperty(final String property) {
416     final String val = properties.get(property);
417     if (val == null)
418       return false;
419     if (val.isEmpty())
420       return false;
421     return true;
422   }
423 
424   /**
425    * Returns a simple string with the configured properties for debugging
426    * @return A string with information about the config
427    */
dumpConfiguration()428   public final String dumpConfiguration() {
429     if (properties.isEmpty())
430       return "No configuration settings stored";
431 
432     StringBuilder response = new StringBuilder("TSD Configuration:\n");
433     response.append("File [" + config_location + "]\n");
434     int line = 0;
435     for (Map.Entry<String, String> entry : properties.entrySet()) {
436       if (line > 0) {
437         response.append("\n");
438       }
439       response.append("Key [" + entry.getKey() + "]  Value [");
440       if (entry.getKey().toUpperCase().contains("PASS")) {
441          response.append("********");
442       } else {
443         response.append(entry.getValue());
444       }
445       response.append("]");
446       line++;
447     }
448     return response.toString();
449   }
450 
451   /** @return An immutable copy of the configuration map */
getMap()452   public final Map<String, String> getMap() {
453     return ImmutableMap.copyOf(properties);
454   }
455 
456   /**
457    * set enable_compactions to true
458    */
enableCompactions()459   public final void enableCompactions() {
460     this.enable_compactions = true;
461   }
462 
463   /**
464    * set enable_compactions to false
465    */
disableCompactions()466   public final void disableCompactions() {
467     this.enable_compactions = false;
468   }
469 
470   /**
471    * Loads default entries that were not provided by a file or command line
472    *
473    * This should be called in the constructor
474    */
setDefaults()475   protected void setDefaults() {
476     // map.put("tsd.network.port", ""); // does not have a default, required
477     // map.put("tsd.http.cachedir", ""); // does not have a default, required
478     // map.put("tsd.http.staticroot", ""); // does not have a default, required
479     default_map.put("tsd.mode", "rw");
480     default_map.put("tsd.no_diediedie", "false");
481     default_map.put("tsd.network.bind", "0.0.0.0");
482     default_map.put("tsd.network.worker_threads", "");
483     default_map.put("tsd.network.async_io", "true");
484     default_map.put("tsd.network.tcp_no_delay", "true");
485     default_map.put("tsd.network.keep_alive", "true");
486     default_map.put("tsd.network.reuse_address", "true");
487     default_map.put("tsd.core.auto_create_metrics", "false");
488     default_map.put("tsd.core.auto_create_tagks", "true");
489     default_map.put("tsd.core.auto_create_tagvs", "true");
490     default_map.put("tsd.core.connections.limit", "0");
491     default_map.put("tsd.core.enable_api", "true");
492     default_map.put("tsd.core.enable_ui", "true");
493     default_map.put("tsd.core.meta.enable_realtime_ts", "false");
494     default_map.put("tsd.core.meta.enable_realtime_uid", "false");
495     default_map.put("tsd.core.meta.enable_tsuid_incrementing", "false");
496     default_map.put("tsd.core.meta.enable_tsuid_tracking", "false");
497     default_map.put("tsd.core.meta.cache.enable", "false");
498     default_map.put("tsd.core.plugin_path", "");
499     default_map.put("tsd.core.socket.timeout", "0");
500     default_map.put("tsd.core.tree.enable_processing", "false");
501     default_map.put("tsd.core.preload_uid_cache", "false");
502     default_map.put("tsd.core.preload_uid_cache.max_entries", "300000");
503     default_map.put("tsd.core.storage_exception_handler.enable", "false");
504     default_map.put("tsd.core.uid.random_metrics", "false");
505     default_map.put("tsd.query.filter.expansion_limit", "4096");
506     default_map.put("tsd.query.skip_unresolved_tagvs", "false");
507     default_map.put("tsd.query.allow_simultaneous_duplicates", "true");
508     default_map.put("tsd.query.enable_fuzzy_filter", "true");
509     default_map.put("tsd.rtpublisher.enable", "false");
510     default_map.put("tsd.rtpublisher.plugin", "");
511     default_map.put("tsd.search.enable", "false");
512     default_map.put("tsd.search.plugin", "");
513     default_map.put("tsd.stats.canonical", "false");
514     default_map.put("tsd.startup.enable", "false");
515     default_map.put("tsd.startup.plugin", "");
516     default_map.put("tsd.storage.hbase.scanner.maxNumRows", "128");
517     default_map.put("tsd.storage.fix_duplicates", "false");
518     default_map.put("tsd.storage.flush_interval", "1000");
519     default_map.put("tsd.storage.hbase.data_table", "tsdb");
520     default_map.put("tsd.storage.hbase.uid_table", "tsdb-uid");
521     default_map.put("tsd.storage.hbase.tree_table", "tsdb-tree");
522     default_map.put("tsd.storage.hbase.meta_table", "tsdb-meta");
523     default_map.put("tsd.storage.hbase.zk_quorum", "localhost");
524     default_map.put("tsd.storage.hbase.zk_basedir", "/hbase");
525     default_map.put("tsd.storage.hbase.prefetch_meta", "false");
526     default_map.put("tsd.storage.enable_appends", "false");
527     default_map.put("tsd.storage.repair_appends", "false");
528     default_map.put("tsd.storage.enable_compaction", "true");
529     default_map.put("tsd.storage.compaction.flush_interval", "10");
530     default_map.put("tsd.storage.compaction.min_flush_threshold", "100");
531     default_map.put("tsd.storage.compaction.max_concurrent_flushes", "10000");
532     default_map.put("tsd.storage.compaction.flush_speed", "2");
533     default_map.put("tsd.timeseriesfilter.enable", "false");
534     default_map.put("tsd.uidfilter.enable", "false");
535     default_map.put("tsd.core.stats_with_port", "false");
536     default_map.put("tsd.http.show_stack_trace", "true");
537     default_map.put("tsd.http.query.allow_delete", "false");
538     default_map.put("tsd.http.request.enable_chunked", "false");
539     default_map.put("tsd.http.request.max_chunk", "4096");
540     default_map.put("tsd.http.request.cors_domains", "");
541     default_map.put("tsd.http.request.cors_headers", "Authorization, "
542       + "Content-Type, Accept, Origin, User-Agent, DNT, Cache-Control, "
543       + "X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since");
544     default_map.put("tsd.query.timeout", "0");
545 
546     for (Map.Entry<String, String> entry : default_map.entrySet()) {
547       if (!properties.containsKey(entry.getKey()))
548         properties.put(entry.getKey(), entry.getValue());
549     }
550 
551     loadStaticVariables();
552   }
553 
554   /**
555    * Searches a list of locations for a valid opentsdb.conf file
556    *
557    * The config file must be a standard JAVA properties formatted file. If none
558    * of the locations have a config file, then the defaults or command line
559    * arguments will be used for the configuration
560    *
561    * Defaults for Linux based systems are: ./opentsdb.conf /etc/opentsdb.conf
562    * /etc/opentsdb/opentdsb.conf /opt/opentsdb/opentsdb.conf
563    *
564    * @throws IOException Thrown if there was an issue reading a file
565    */
loadConfig()566   protected void loadConfig() throws IOException {
567     if (config_location != null && !config_location.isEmpty()) {
568       loadConfig(config_location);
569       return;
570     }
571 
572     final ArrayList<String> file_locations = new ArrayList<String>();
573 
574     // search locally first
575     file_locations.add("opentsdb.conf");
576 
577     // add default locations based on OS
578     if (System.getProperty("os.name").toUpperCase().contains("WINDOWS")) {
579       file_locations.add("C:\\Program Files\\opentsdb\\opentsdb.conf");
580       file_locations.add("C:\\Program Files (x86)\\opentsdb\\opentsdb.conf");
581     } else {
582       file_locations.add("/etc/opentsdb.conf");
583       file_locations.add("/etc/opentsdb/opentsdb.conf");
584       file_locations.add("/usr/local/etc/opentsdb/opentsdb.conf");
585       file_locations.add("/opt/opentsdb/opentsdb.conf");
586     }
587 
588     for (String file : file_locations) {
589       try {
590         FileInputStream file_stream = new FileInputStream(file);
591         Properties props = new Properties();
592         props.load(file_stream);
593 
594         // load the hash map
595         loadHashMap(props);
596       } catch (Exception e) {
597         // don't do anything, the file may be missing and that's fine
598         LOG.debug("Unable to find or load " + file, e);
599         continue;
600       }
601 
602       // no exceptions thrown, so save the valid path and exit
603       LOG.info("Successfully loaded configuration file: " + file);
604       config_location = file;
605       return;
606     }
607 
608     LOG.info("No configuration found, will use defaults");
609   }
610 
611   /**
612    * Attempts to load the configuration from the given location
613    * @param file Path to the file to load
614    * @throws IOException Thrown if there was an issue reading the file
615    * @throws FileNotFoundException Thrown if the config file was not found
616    */
loadConfig(final String file)617   protected void loadConfig(final String file) throws FileNotFoundException,
618       IOException {
619     final FileInputStream file_stream = new FileInputStream(file);
620     try {
621       final Properties props = new Properties();
622       props.load(file_stream);
623 
624       // load the hash map
625       loadHashMap(props);
626 
627       // no exceptions thrown, so save the valid path and exit
628       LOG.info("Successfully loaded configuration file: " + file);
629       config_location = file;
630     } finally {
631       file_stream.close();
632     }
633   }
634 
635   /**
636    * Loads the static class variables for values that are called often. This
637    * should be called any time the configuration changes.
638    */
loadStaticVariables()639   protected void loadStaticVariables() {
640     auto_metric = this.getBoolean("tsd.core.auto_create_metrics");
641     auto_tagk = this.getBoolean("tsd.core.auto_create_tagks");
642     auto_tagv = this.getBoolean("tsd.core.auto_create_tagvs");
643     enable_compactions = this.getBoolean("tsd.storage.enable_compaction");
644     enable_appends = this.getBoolean("tsd.storage.enable_appends");
645     repair_appends = this.getBoolean("tsd.storage.repair_appends");
646     enable_chunked_requests = this.getBoolean("tsd.http.request.enable_chunked");
647     enable_realtime_ts = this.getBoolean("tsd.core.meta.enable_realtime_ts");
648     enable_realtime_uid = this.getBoolean("tsd.core.meta.enable_realtime_uid");
649     enable_tsuid_incrementing =
650       this.getBoolean("tsd.core.meta.enable_tsuid_incrementing");
651     enable_tsuid_tracking =
652       this.getBoolean("tsd.core.meta.enable_tsuid_tracking");
653     if (this.hasProperty("tsd.http.request.max_chunk")) {
654       max_chunked_requests = this.getInt("tsd.http.request.max_chunk");
655     }
656     enable_tree_processing = this.getBoolean("tsd.core.tree.enable_processing");
657     fix_duplicates = this.getBoolean("tsd.storage.fix_duplicates");
658     scanner_max_num_rows = this.getInt("tsd.storage.hbase.scanner.maxNumRows");
659   }
660 
661   /**
662    * Called from {@link #loadConfig} to copy the properties into the hash map
663    * Tsuna points out that the Properties class is much slower than a hash
664    * map so if we'll be looking up config values more than once, a hash map
665    * is the way to go
666    * @param props The loaded Properties object to copy
667    */
loadHashMap(final Properties props)668   private void loadHashMap(final Properties props) {
669     properties.clear();
670 
671     @SuppressWarnings("rawtypes")
672     Enumeration e = props.propertyNames();
673     while (e.hasMoreElements()) {
674       String key = (String) e.nextElement();
675       properties.put(key, props.getProperty(key));
676     }
677   }
678 }
679