View Javadoc
1   package emissary.config;
2   
3   import jakarta.annotation.Nullable;
4   import org.apache.commons.lang3.ArrayUtils;
5   import org.apache.commons.lang3.StringUtils;
6   import org.apache.commons.lang3.Validate;
7   import org.slf4j.Logger;
8   import org.slf4j.LoggerFactory;
9   
10  import java.io.BufferedReader;
11  import java.io.File;
12  import java.io.FileWriter;
13  import java.io.IOException;
14  import java.io.InputStream;
15  import java.io.InputStreamReader;
16  import java.io.Reader;
17  import java.io.Serializable;
18  import java.io.StreamTokenizer;
19  import java.net.InetAddress;
20  import java.net.UnknownHostException;
21  import java.nio.file.Paths;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.Enumeration;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.Iterator;
29  import java.util.LinkedHashMap;
30  import java.util.LinkedHashSet;
31  import java.util.List;
32  import java.util.Locale;
33  import java.util.Map;
34  import java.util.Properties;
35  import java.util.Set;
36  
37  /**
38   * This class implements the Configurator interface for services within the Emissary framework.
39   */
40  
41  public class ServiceConfigGuide implements Configurator, Serializable {
42  
43      static final long serialVersionUID = 3906838615422657150L;
44      public static final char SLASH = '/';
45      public static final char COLON = ':';
46      public static final String DOUBLESLASH = "//";
47  
48      protected static final Logger logger = LoggerFactory.getLogger(ServiceConfigGuide.class);
49  
50      protected static final String DEFAULT_FILE_NAME = "default.cfg";
51      protected static final String POST_FILE_NAME = "post.cfg";
52  
53      // Used on the RHS to make a null assignment
54      // Obsolete, use @{NULL}
55      protected static final String NULL_VALUE = "<null>";
56  
57      // Hold all service specific parameters in a list
58      protected List<ConfigEntry> serviceParameters = new ArrayList<>();
59  
60      // Hold all remove config entries, operator of !=
61      protected List<ConfigEntry> removeParameters = new ArrayList<>();
62  
63      protected String operator;
64  
65      // Start and end to a dynamic substitution
66      protected static final String VSTART = "@{";
67      protected static final String VEND = "}";
68  
69      // Shared map of all environment properties
70      // Access them with @ENV{'os.name'} for example
71      // This is obsolete, all values from properties and
72      // environment are now in the main values map and available
73      // for immediate substitution
74      protected static final String ENVSTART = "@ENV{'";
75      protected static final String ENVSTOP = "'}";
76  
77      // Map of last values seen
78      protected Map<String, String> values = new HashMap<>();
79  
80      // Get this once per jvm
81      private static final String hostname;
82  
83      // Grab the hostname for @{HOST} replacement
84      static {
85          String tmpHostname;
86          try {
87              tmpHostname = InetAddress.getLocalHost().getCanonicalHostName();
88          } catch (UnknownHostException e) {
89              logger.error("Error getting host name", e);
90              tmpHostname = "localhost";
91          }
92          hostname = tmpHostname;
93      }
94  
95      /**
96       * Public default constructor
97       */
98      public ServiceConfigGuide() {
99          initializeValues();
100     }
101 
102     /**
103      * Public constructor with dir and filename
104      *
105      * @param path the directory where config files are
106      * @param file the name of te file in the directory
107      */
108     public ServiceConfigGuide(final String path, final String file) throws IOException {
109         this(path + File.separator + file);
110     }
111 
112     /**
113      * Public default constructor with file name
114      *
115      * @param filename the name of the disk file
116      */
117     public ServiceConfigGuide(final String filename) throws IOException {
118         this();
119         try {
120             readConfigData(filename);
121         } catch (ConfigSyntaxException ex) {
122             throw new IOException("Cannot parse configuration file " + ex.getMessage(), ex);
123         }
124     }
125 
126     /**
127      * Public default constructor with InputStream
128      *
129      * @param is the InputStream
130      */
131     public ServiceConfigGuide(final InputStream is) throws IOException {
132         this();
133         try {
134             readConfigData(is);
135         } catch (ConfigSyntaxException ex) {
136             throw new IOException("Cannot parse configuration file " + ex.getMessage(), ex);
137         }
138     }
139 
140     /**
141      * Public default constructor with InputStream and name
142      *
143      * @param is the InputStream
144      * @param name the name of the stream good for reporting errors
145      */
146     public ServiceConfigGuide(final InputStream is, final String name) throws IOException {
147         this();
148         try {
149             readConfigData(is, name);
150         } catch (ConfigSyntaxException ex) {
151             logger.error("Caught ConfigSytaxException {}", ex.getMessage());
152             throw new IOException("Cannot parse configuration file " + ex.getMessage(), ex);
153         }
154     }
155 
156     /**
157      * Initialize the values map, which is used to replace stuff in the configs
158      */
159     protected void initializeValues() {
160         this.values.clear();
161 
162         // TODO: see if we can stop adding all env variables and
163         // system properties to the replace values
164 
165         // Add all the environment variables
166         this.values.putAll(System.getenv());
167 
168         // Add all the system properties
169         final Properties props = System.getProperties();
170         for (Enumeration<?> e = props.propertyNames(); e.hasMoreElements();) {
171             final String key = (String) e.nextElement();
172             logger.trace("Adding {} to replaceable properties", key);
173             this.values.put(key, props.getProperty(key));
174         }
175 
176         // used for substitution when reading cfg files
177         this.values.put("CONFIG_DIR", StringUtils.join(ConfigUtil.getConfigDirs(), ","));
178         this.values.put("PRJ_BASE", ConfigUtil.getProjectBase());
179         this.values.put("PROJECT_BASE", ConfigUtil.getProjectBase());
180         this.values.put("OUTPUT_ROOT", ConfigUtil.getOutputRoot());
181         this.values.put("BIN_DIR", ConfigUtil.getBinDir());
182         this.values.put("HOST", hostname);
183         this.values.put("/", File.separator);
184         this.values.put("TMPDIR", System.getProperty("java.io.tmpdir"));
185         this.values.put("NULL", null);
186         this.values.put("OS.NAME", System.getProperty("os.name").replace(' ', '_'));
187         this.values.put("OS.VER", System.getProperty("os.version").replace(' ', '_'));
188         this.values.put("OS.ARCH", System.getProperty("os.arch").replace(' ', '_'));
189     }
190 
191     /**
192      * Reads the configuration file specified in the argument and sets the mandatory parameters.
193      */
194     protected void readConfigData(final String filename) throws IOException, ConfigSyntaxException {
195         readSingleConfigFile(filename);
196     }
197 
198     public void readConfigData(final InputStream is) throws IOException, ConfigSyntaxException {
199         readConfigData(is, "UNKNOWN");
200     }
201 
202 
203     protected void readConfigData(final InputStream is, final String filename) throws IOException, ConfigSyntaxException {
204         final Reader r = new BufferedReader(new InputStreamReader(is));
205         final StreamTokenizer in = new StreamTokenizer(r);
206         int nextToken = StreamTokenizer.TT_WORD;
207         String parmName;
208         String sval;
209 
210         in.commentChar('#');
211         in.wordChars(33, 33);
212         in.wordChars(36, 47);
213         in.wordChars(58, 64);
214         in.wordChars(91, 96);
215         in.wordChars(123, 65536);
216 
217         while (nextToken != StreamTokenizer.TT_EOF) {
218             // Read three tokens at a time (X = Y)
219             nextToken = in.nextToken();
220 
221             // Make sure the first token in the tuple is a word
222             if (nextToken == StreamTokenizer.TT_EOF) {
223                 break;
224             }
225             if (nextToken == StreamTokenizer.TT_NUMBER) {
226                 throw new ConfigSyntaxException("Illegal token " + in.sval + ", missing quote on line " + in.lineno() + "?");
227             }
228 
229             parmName = in.sval;
230 
231             nextToken = in.nextToken();
232             this.operator = in.sval;
233 
234             nextToken = in.nextToken();
235             if (nextToken == StreamTokenizer.TT_NUMBER) {
236                 sval = Long.toString((long) in.nval);
237             } else {
238                 sval = in.sval;
239             }
240 
241             if (sval == null) {
242                 // Problem is likely on previous line
243                 throw new ConfigSyntaxException("Illegal token " + parmName + ", missing space or value on line " + (in.lineno() - 1) + "?");
244             }
245 
246             handleNewEntry(parmName, sval, this.operator, filename, in.lineno() - 1, false);
247         }
248         r.close();
249         is.close();
250     }
251 
252     protected void readSingleConfigFile(final String filename) throws IOException, ConfigSyntaxException {
253         logger.debug("Reading config file {}", filename);
254         final InputStream is = ConfigUtil.getConfigData(filename);
255         readConfigData(is, filename);
256     }
257 
258     /**
259      * Handle a newly parsed or passed in entry. Substitutions are handled on both the LHS and RHS, then the values are
260      * stored as a ConfigEntry in our local list and map. Only the last value in the map is available for substitutions. LHS
261      * is analyzed before RHS and in L to R order.
262      *
263      * @param parmNameArg the LHS
264      * @param svalArg the raw RHS
265      * @param operatorArg the equation
266      * @param filename the filename we are parsing for error reporting
267      * @param lineno the line number we are currently reporting the error on
268      * @param merge true when adding in from a merge
269      * @return a new config entry with the expanded key and value
270      * @throws IOException when the key or value is malformed
271      */
272     protected ConfigEntry handleNewEntry(final String parmNameArg, final String svalArg, final String operatorArg, final String filename,
273             final int lineno, final boolean merge) throws IOException {
274         final String parmName = handleReplacements(parmNameArg, filename, lineno);
275         final String sval = handleReplacements(svalArg, filename, lineno);
276 
277         // Create a config entry from this
278         final ConfigEntry anEntry = new ConfigEntry(parmName, sval);
279 
280         if ("!=".equals(operatorArg)) {
281             if ("*".equals(sval)) {
282                 removeAllEntries(parmName);
283                 this.values.remove(parmName);
284             } else {
285                 removeEntry(anEntry);
286                 if (sval.equals(this.values.get(parmName))) {
287                     this.values.remove(parmName);
288                 }
289             }
290             this.removeParameters.add(anEntry);
291         } else {
292             // Save the entry in the list
293             if (merge) {
294                 this.serviceParameters.add(0, anEntry);
295             } else {
296                 this.serviceParameters.add(anEntry);
297             }
298 
299             // Save this pair in the map
300             this.values.put(parmName, sval);
301         }
302 
303         if ("IMPORT_FILE".equals(parmName) || "OPT_IMPORT_FILE".equals(parmName)) {
304             final List<String> fileFlavorList = new ArrayList<>();
305             // Add the base file and then add all the flavor versions
306             fileFlavorList.add(sval);
307             final String[] fileFlavors = ConfigUtil.addFlavors(sval);
308             if (ArrayUtils.isNotEmpty(fileFlavors)) {
309                 fileFlavorList.addAll(Arrays.asList(fileFlavors));
310             }
311             logger.debug("ServiceConfigGuide::handleNewEntry -- FileFlavorList = {}", fileFlavorList);
312 
313             // loop through the files and attempt to read/merger the configurations.
314             for (int i = 0; i < fileFlavorList.size(); i++) {
315                 final String fileFlavor = fileFlavorList.get(i);
316                 // recursion alert: This could lead to getFile being called
317                 try {
318                     readConfigData(ConfigUtil.getConfigStream(fileFlavor), fileFlavor);
319                 } catch (ConfigSyntaxException e) {
320                     // whether opt or not, syntax errors are a problem
321                     throw new IOException(parmName + " = " + sval + " from " + filename + " failed " + e.getMessage(), e);
322                 } catch (IOException e) {
323                     // Throw exception if it is an IMPORT_FILE and the base file is not found
324                     if ("IMPORT_FILE".equals(parmName) && i == 0) {
325                         String importFileName = Paths.get(svalArg).getFileName().toString();
326                         throw new IOException("In " + filename + ", cannot find IMPORT_FILE: " + sval
327                                 + " on the specified path. Make sure IMPORT_FILE (" + importFileName + ") exists, and the file path is correct.",
328                                 e);
329                     }
330                 }
331             }
332             return anEntry;
333         } else if ("CREATE_DIRECTORY".equals(parmName) && !createDirectory(sval)) {
334             logger.warn("{}: Cannot create directory {}", filename, sval);
335         } else if ("CREATE_FILE".equals(parmName) && !createFile(sval)) {
336             logger.warn("{}: Cannot create file {}", filename, sval);
337         }
338 
339         return anEntry;
340     }
341 
342     /**
343      * Handle all the possible replacements in a string value
344      *
345      * @param svalArg the raw value
346      * @param filename the filename we are parsing for error reporting
347      * @param lineno the line number we are currently reporting the error on
348      * @return the expanded value with all legal @{..} values replaced
349      * @throws IOException when the value is malformed
350      */
351     protected String handleReplacements(final String svalArg, final String filename, final int lineno) throws IOException {
352         String sval = svalArg;
353         int startpos = 0;
354         while (sval != null && sval.indexOf(VSTART, startpos) > -1) {
355             final int ndx = sval.indexOf(VSTART, startpos);
356             final int edx = sval.indexOf(VEND, ndx + VSTART.length());
357             if (ndx == -1 && ndx >= edx) {
358                 throw new IOException("Problem parsing line " + lineno + " " + sval);
359             }
360             final String tok = sval.substring(ndx + VSTART.length(), edx);
361             logger.debug("Replacement token is {}", tok);
362             final String mapval = this.values.get(tok);
363             if (mapval != null) {
364                 sval = sval.substring(0, ndx) + mapval + sval.substring(edx + VEND.length());
365             } else {
366                 logger.warn("Did not find replacement for '{}' in file {} at line {}", tok, filename, lineno);
367                 startpos = edx + VEND.length();
368             }
369         }
370 
371         // This is obsolete
372         if (sval != null && sval.contains(ENVSTART)) {
373             sval = substituteEnvProps(sval, filename, lineno);
374         }
375 
376         // Do unicode stuff
377         if (sval != null && (sval.contains("\\u") || sval.contains("\\U"))) {
378             sval = substituteUtfChars(sval, filename, lineno);
379         }
380 
381         // This is obsolete
382         if (sval != null && sval.equals(NULL_VALUE)) {
383             sval = null;
384             logger.debug("Using {} is deprecated, please just use {}NULL{}", NULL_VALUE, VSTART, VEND);
385         }
386         return sval;
387     }
388 
389     /**
390      * Substitute any java unicode character values: \\uxxxx
391      *
392      * @param s the string to process
393      * @param filename the name of the file we are in for error reporting
394      * @param lnum the current line number for error reporting
395      * @return string with character values replaced
396      */
397     protected String substituteUtfChars(final String s, final String filename, final int lnum) throws IOException {
398         final int slen = s.length();
399         final StringBuilder sb = new StringBuilder(slen);
400         for (int i = 0; i < slen; i++) {
401             if (s.charAt(i) != '\\') {
402                 sb.append(s.charAt(i));
403             } else if ((i + 4) < slen && (s.charAt(i + 1) == 'u' || s.charAt(i + 1) == 'U')) {
404                 int epos = i + 2;
405                 final int max = (s.charAt(epos) == '1' || s.charAt(epos) == '0') ? (i + 7) : (i + 6);
406                 while (epos < slen
407                         && epos < max
408                         && ((s.charAt(epos) >= '0' && s.charAt(epos) <= '9') || (s.charAt(epos) >= 'A' && s.charAt(epos) <= 'F')
409                                 || (s.charAt(epos) >= 'a' && s
410                                         .charAt(epos) <= 'f'))) {
411                     epos++;
412                 }
413                 if (epos <= slen) {
414                     try {
415                         final int digit = Integer.parseInt(s.substring(i + 2, epos), 16);
416                         sb.appendCodePoint(digit);
417                         i = epos - 1;
418                     } catch (RuntimeException ex) {
419                         throw new IOException("Unable to convert characters in " + s + ", from filename=" + filename + " line " + lnum, ex);
420                     }
421                 }
422             } else {
423                 sb.append(s.charAt(i));
424             }
425         }
426 
427         return sb.toString();
428     }
429 
430     /**
431      * Substitute any referenced env properties with their values. Look for @ENV{'foo'} and replace foo with
432      * System.getProperty("foo") or System.getenv("foo") in that order.
433      *
434      * @param str the string to process
435      * @param filename the name of the file we are in for error reporting
436      * @param lnum the current line number for error reporting
437      * @return string with env values replaced
438      */
439     protected String substituteEnvProps(final String str, final String filename, final int lnum) throws IOException {
440         int lastPos = -1;
441         int thisPos = 0;
442         int count = 0;
443 
444         logger.debug("{}{} style substitution is deprecated. Please just use {}yourvalue{}", ENVSTART, ENVSTOP, VSTART, VEND);
445 
446         String currentStr = str;
447         while ((thisPos = currentStr.indexOf(ENVSTART, thisPos)) > lastPos) {
448             final int start = thisPos + ENVSTART.length();
449             final int stop = currentStr.indexOf(ENVSTOP, thisPos);
450             count++;
451             // Pull out the env name they specified
452             if (stop > start) {
453                 final String envName = currentStr.substring(start, stop);
454                 String envVal = System.getProperty(envName);
455                 if (envVal == null) {
456                     envVal = System.getenv(envName);
457                 }
458                 // We got a replacement, do the subst
459                 if (envVal != null) {
460                     currentStr = currentStr.substring(0, thisPos) + // before
461                             envVal + // replacement value
462                             currentStr.substring(stop + ENVSTOP.length()); // tail
463                     logger.debug("Replaced {} with {} at {}: {}", envName, envVal, filename, lnum);
464                 } else {
465                     logger.debug("No env value for {} at {}: {}", envName, filename, lnum);
466                 }
467             } else {
468                 throw new IOException("Runaway string on line ->" + currentStr + "<- at " + filename + ": " + lnum);
469             }
470 
471             lastPos = thisPos;
472         }
473         logger.debug("Found {} env vars to subst --> {}", count, currentStr);
474         return currentStr;
475     }
476 
477     /**
478      * Create a directory as specified by the config driver
479      */
480     protected boolean createDirectory(final String sval) {
481         final String fixedSval = sval.replace('\\', '/');
482         logger.debug("Trying to create dir {}", fixedSval);
483         final File d = new File(fixedSval);
484         if (!d.exists() && !d.mkdirs()) {
485             logger.debug("Failed to create directory {}", fixedSval);
486             return false;
487         }
488         return true;
489     }
490 
491     /**
492      * Create a file as specified by the config driver
493      */
494     protected boolean createFile(final String sval) {
495 
496         final String fixedSval = sval.replace('\\', '/');
497         logger.debug("Trying to create file {}", fixedSval);
498         final File d = new File(fixedSval);
499         FileWriter newFile = null;
500         if (!d.exists()) {
501             try {
502                 // Ensure the directory exists to hold the file
503                 final File parent = new File(new File(d.getCanonicalPath()).getParent());
504                 if (!parent.exists() && !createDirectory(parent.toString())) {
505                     logger.debug("Failed to create parent directory for {}", fixedSval);
506                     return false;
507                 }
508                 // Create the file in the directory
509                 newFile = new FileWriter(d);
510             } catch (IOException e) {
511                 logger.debug("Failed to create file {}", fixedSval, e);
512                 return false;
513             } finally {
514                 if (newFile != null) {
515                     try {
516                         newFile.close();
517                     } catch (IOException ioe) {
518                         logger.debug("Error closing file", ioe);
519                     }
520                 }
521             }
522         }
523         return true;
524     }
525 
526     /**
527      * Get the names of all entries for this config This set is not backed by the configuration and any changes to it are
528      * not reflected in the configuration.
529      */
530     @Override
531     public Set<String> entryKeys() {
532         final Set<String> set = new HashSet<>();
533         for (final ConfigEntry curEntry : this.serviceParameters) {
534             set.add(curEntry.getKey());
535         }
536         return set;
537     }
538 
539     /**
540      * Get all the entries for this config This is a copy and changes to it are not reflected in the configuration
541      */
542     @Override
543     public List<ConfigEntry> getEntries() {
544         return new ArrayList<>(this.serviceParameters);
545     }
546 
547     /**
548      * Remove entries, those with operators of '!=' are stored and can be retrieved for replay during merge. This method is
549      * not part of the Configurator interface.
550      */
551     protected List<ConfigEntry> getRemoveEntries() {
552         return new ArrayList<>(this.removeParameters);
553     }
554 
555     /**
556      * Add an entry to this config
557      *
558      * @param key the name of the entry
559      * @param value the value
560      * @return the new entry or null if it fails
561      */
562     @Override
563     public ConfigEntry addEntry(final String key, final String value) {
564         ConfigEntry entry = null;
565         try {
566             entry = handleNewEntry(key, value, "=", "<user>", 1, false);
567         } catch (IOException ex) {
568             logger.error("Could not add entry for {}", key, ex);
569         }
570         return entry;
571     }
572 
573     /**
574      * Add a list of entries for the same key
575      *
576      * @param key the name of the entry
577      * @param values the values
578      * @return the new entries or null if it fails
579      */
580     @Override
581     public List<ConfigEntry> addEntries(final String key, final List<String> values) {
582         final List<ConfigEntry> list = new ArrayList<>();
583         try {
584             int i = 1;
585             for (final String value : values) {
586                 final ConfigEntry entry = handleNewEntry(key, value, "=", "<user>", i++, false);
587                 list.add(entry);
588             }
589         } catch (IOException ex) {
590             logger.error("Error adding entries for {}", key, ex);
591         }
592         return list;
593     }
594 
595     /**
596      * Remove all entries by the given name
597      *
598      * @param key the name of the entry or entries
599      * @param value the value
600      */
601     @Override
602     public void removeEntry(final String key, final String value) {
603         try {
604             handleNewEntry(key, value, "!=", "<user>", 1, false);
605         } catch (IOException ex) {
606             logger.warn("Cannot remove entry", ex);
607         }
608     }
609 
610     /**
611      * Remove an entry from the list of parameters matching the ConfigEntry argument passed in.
612      *
613      * @param anEntry the entry to remove
614      */
615     public void removeEntry(final ConfigEntry anEntry) {
616         // NB: enhanced for loop does not support remove
617         for (final Iterator<ConfigEntry> i = this.serviceParameters.iterator(); i.hasNext();) {
618             final ConfigEntry curEntry = i.next();
619             if (anEntry.getKey().equals(curEntry.getKey())
620                     && ((anEntry.getValue() == null && curEntry.getValue() == null) || (anEntry.getValue() != null && anEntry.getValue().equals(
621                             curEntry.getValue())))) {
622                 logger.debug("Removing {} = {}", curEntry.getKey(), curEntry.getValue());
623                 i.remove();
624             }
625         }
626     }
627 
628     /**
629      * Return a list containing all the parameter values matching the key argument passed in.
630      *
631      * @param theParameter the key to match
632      * @param defaultString value for list when no matches are found
633      * @return the list with all matching entries or the default value supplied
634      */
635     @Override
636     public List<String> findEntries(final String theParameter, final String defaultString) {
637         final List<String> result = findEntries(theParameter);
638         if (result.isEmpty()) {
639             result.add(defaultString);
640         }
641         return result;
642     }
643 
644     /**
645      * Return a list containing all the parameter values matching the key argument passed in
646      *
647      * @param theParameter the key to match
648      * @return list with all matching entries, or empty list if none
649      */
650     @Override
651     public List<String> findEntries(final String theParameter) {
652         final List<String> matchingEntries = new ArrayList<>();
653 
654         for (final ConfigEntry curEntry : this.serviceParameters) {
655             if (theParameter.equals(curEntry.getKey())) {
656                 matchingEntries.add(curEntry.getValue());
657             }
658         }
659         return matchingEntries;
660     }
661 
662     /**
663      * Remove all entries from the list of parameters matching the String argument passed in.
664      *
665      * @param theParameter key name to match, all matching will be removed
666      */
667     public void removeAllEntries(final String theParameter) {
668         // NB: enhanced for loop does not support remove
669         for (final Iterator<ConfigEntry> i = this.serviceParameters.iterator(); i.hasNext();) {
670             final ConfigEntry curEntry = i.next();
671             if (theParameter.equals(curEntry.getKey())) {
672                 logger.debug("Removing {} = {}", curEntry.getKey(), curEntry.getValue());
673                 i.remove();
674             }
675         }
676     }
677 
678     /**
679      * Return a set with all parameter values as members
680      *
681      * @param theParameter key value to match
682      * @return set of all entries found or empty set if none
683      */
684     @Override
685     public Set<String> findEntriesAsSet(final String theParameter) {
686 
687         final Set<String> matchingEntries = new HashSet<>();
688 
689         for (final ConfigEntry curEntry : this.serviceParameters) {
690             if (theParameter.equals(curEntry.getKey())) {
691                 matchingEntries.add(curEntry.getValue());
692             }
693         }
694         return matchingEntries;
695     }
696 
697     /**
698      * Find entries beginning with the specified string
699      *
700      * @param theParameter key to match with a startsWith
701      * @return list of entries matching specified value or empty list if none
702      */
703     @Override
704     public List<ConfigEntry> findStringMatchEntries(final String theParameter) {
705 
706         final List<ConfigEntry> matchingEntries = new ArrayList<>();
707 
708         for (final ConfigEntry curEntry : this.serviceParameters) {
709             if (curEntry.getKey().startsWith(theParameter)) {
710                 matchingEntries.add(curEntry);
711             }
712         }
713         return matchingEntries;
714     }
715 
716     /**
717      * Find entries beginning with the specified string and put them into a list with the specified part of the name
718      * stripped off like #findStringMatchMap
719      *
720      * @param theParameter key to match with a startsWith
721      * @return list of ConfigEntry
722      */
723     @Override
724     public List<ConfigEntry> findStringMatchList(final String theParameter) {
725         final List<ConfigEntry> list = findStringMatchEntries(theParameter);
726         for (final ConfigEntry entry : list) {
727             entry.setKey(entry.getKey().substring(theParameter.length()));
728         }
729         return list;
730     }
731 
732     /**
733      * Find entries beginning with the specified string and return a hash keyed on the remainder of the string with the
734      * value of the config line as the value of the hash
735      *
736      * @param theParameter the key to look for in the config file
737      */
738     @Override
739     public Map<String, String> findStringMatchMap(final String theParameter) {
740         return findStringMatchMap(theParameter, false);
741     }
742 
743     /**
744      * Find entries beginning with the specified string and return a hash keyed on the remainder of the string with the
745      * value of the config line as the value of the hash
746      *
747      * <pre>
748      * {@code
749      * Example config entries
750      *    FOO_ONE: AAA
751      *    FOO_TWO: BBB
752      * Calling findStringMatchMap("FOO_",true)
753      * will yield a map with
754      *     ONE -> AAA
755      *     TWO -> BBB
756      * }
757      * </pre>
758      *
759      * @param theParameter the key to look for in the config file
760      * @param preserveCase if false all keys will be upcased
761      * @return map where key is remainder after match and value is the config value, or an empty map if none found
762      */
763     @Override
764     public Map<String, String> findStringMatchMap(final String theParameter, final boolean preserveCase) {
765         return findStringMatchMap(theParameter, preserveCase, false);
766     }
767 
768     /**
769      * Find entries beginning with the specified string and return a hash keyed on the remainder of the string with the
770      * value of the config line as the value of the hash
771      *
772      * <pre>
773      * {@code
774      * Example config entries
775      *    FOO_ONE: AAA
776      *    FOO_TWO: BBB
777      * Calling findStringMatchMap("FOO_",true)
778      * will yield a map with
779      *     ONE -> AAA
780      *     TWO -> BBB
781      * }
782      * </pre>
783      *
784      * @param theParameter the key to look for in the config file
785      * @param preserveCase if false all keys will be upcased
786      * @param preserveOrder if true key ordering is preserved
787      * @return map where key is remainder after match and value is the config value, or an empty map if none found
788      */
789     @Override
790     public Map<String, String> findStringMatchMap(@Nullable final String theParameter, final boolean preserveCase, final boolean preserveOrder) {
791         if (theParameter == null) {
792             return Collections.emptyMap();
793         }
794 
795         final Map<String, String> theHash = preserveOrder ? new LinkedHashMap<>() : new HashMap<>();
796         final List<ConfigEntry> parameters = this.findStringMatchEntries(theParameter);
797 
798         for (final ConfigEntry el : parameters) {
799             String key = el.getKey();
800             key = key.substring(theParameter.length());
801             if (!preserveCase) {
802                 key = key.toUpperCase(Locale.getDefault());
803             }
804             theHash.put(key, el.getValue());
805         }
806         return theHash;
807     }
808 
809     /**
810      * Find entries beginning with the specified string and return a hash keyed on the remainder of the string with the
811      * value of the config line as the value of the hash Multiple values for the same hash are allowed and returned as a
812      * Set.
813      *
814      * <pre>
815      * {@code
816      * Example config entries
817      *    FOO_ONE: AAA
818      *    FOO_TWO: BBB
819      *    FOO_TWO: CCC
820      * Calling findStringMatchMap("FOO_",true)
821      * will yield a map with Sets
822      *     ONE -> {AAA}
823      *     TWO -> {BBB,CCC}
824      * }
825      * </pre>
826      *
827      * @param param the key to look for in the config file
828      * @return map where key is remainder after match and value is a Set of all found config values, or an empty map if none
829      *         found
830      */
831     @Override
832     public Map<String, Set<String>> findStringMatchMultiMap(@Nullable final String param) {
833         if (param == null) {
834             return Map.of();
835         }
836 
837         return findStringMatchMultiMap(param, false);
838     }
839 
840     /**
841      * Find entries beginning with the specified string and return a hash keyed on the remainder of the string with the
842      * value of the config line as the value of the hash Multiple values for the same hash are allowed and returned as a
843      * Set.
844      *
845      * <pre>
846      * {@code
847      * Example config entries
848      *    FOO_ONE: AAA
849      *    FOO_TWO: BBB
850      *    FOO_TWO: CCC
851      * Calling findStringMatchMap("FOO_",true)
852      * will yield a map with Sets
853      *     ONE -> {AAA}
854      *     TWO -> {BBB,CCC}
855      * }
856      * </pre>
857      *
858      * @param param the key to look for in the config file
859      * @param preserveOrder ordering of keys is preserved
860      * @return map where key is remainder after match and value is a Set of all found config values, or an empty map if none
861      *         found
862      */
863     @Override
864     public Map<String, Set<String>> findStringMatchMultiMap(@Nullable final String param, final boolean preserveOrder) {
865 
866         if (param == null) {
867             return Map.of();
868         }
869 
870         final Map<String, Set<String>> theHash = preserveOrder ? new LinkedHashMap<>() : new HashMap<>();
871         final List<ConfigEntry> parameters = this.findStringMatchEntries(param);
872 
873         for (final ConfigEntry el : parameters) {
874             final String key = el.getKey().substring(param.length()).toUpperCase(Locale.getDefault());
875 
876             if (theHash.containsKey(key)) {
877                 theHash.get(key).add(el.getValue());
878             } else {
879                 final Set<String> values = preserveOrder ? new LinkedHashSet<>() : new HashSet<>();
880                 values.add(el.getValue());
881                 theHash.put(key, values);
882             }
883         }
884         return theHash;
885 
886     }
887 
888     /**
889      * Return the first string entry matching the key parameter
890      *
891      * @param theParameter key to match
892      * @return the first matching value
893      * @throws IllegalArgumentException if no non-blank value is found
894      */
895     @Override
896     public String findRequiredStringEntry(final String theParameter) {
897         String value = findStringEntry(theParameter, null);
898         Validate.notBlank(value, "Missing required parameter [%s]", theParameter);
899         return value;
900     }
901 
902     /**
903      * Return the first string entry matching the key parameter or the default if no match is found
904      *
905      * @param theParameter the key to match
906      * @param dflt string to use when no matches are found
907      * @return the first matching entry of the default if none found
908      */
909     @Override
910     public String findStringEntry(final String theParameter, @Nullable final String dflt) {
911         final List<String> matchingEntries = findEntries(theParameter);
912         for (final String entry : matchingEntries) {
913             if (entry != null) {
914                 return entry;
915             }
916         }
917         return dflt;
918     }
919 
920     /**
921      * Return the first string entry matching the key parameter or null if no match is found
922      *
923      * @param theParameter key to match
924      * @return the first matching value or null if none
925      */
926     @Override
927     public String findStringEntry(final String theParameter) {
928         return findStringEntry(theParameter, null);
929     }
930 
931     /**
932      * Return the last (newest) string entry matching the key parameter or an empty string if no match is found
933      *
934      * @param theParameter the key to match
935      * @return the last matching value or empty string if none found
936      */
937     @Override
938     public String findLastStringEntry(final String theParameter) {
939         String result = "";
940         for (final ConfigEntry curEntry : this.serviceParameters) {
941             if (theParameter.equals(curEntry.getKey())) {
942                 result = curEntry.getValue();
943             }
944         }
945         return result;
946     }
947 
948     /**
949      * Return a long from a string entry representing either an int, a long, a double, with or without a final letter
950      * designation such as "m" or "M" for megabytes, "g" or "G" for gigabytes, etc. Legal designations are bBkKmMgGTt or
951      * just a number.
952      *
953      * @param theParameter the config entry name
954      * @param dflt the default value when nothing found in config
955      * @return the long value of the size parameter
956      */
957     @Override
958     public long findSizeEntry(final String theParameter, final long dflt) {
959         final List<String> matchingEntries = findEntries(theParameter);
960         if (!matchingEntries.isEmpty()) {
961             long val = dflt;
962             final String s = matchingEntries.get(0);
963             final char c = Character.toUpperCase(s.charAt(s.length() - 1));
964             final String ss = s.substring(0, s.length() - 1);
965             boolean broken = false;
966             switch (c) {
967                 case 'T':
968                     val = Long.parseLong(ss) * 1024 * 1024 * 1024 * 1024;
969                     break;
970                 case 'G':
971                     val = Long.parseLong(ss) * 1024 * 1024 * 1024;
972                     break;
973                 case 'M':
974                     val = Long.parseLong(ss) * 1024 * 1024;
975                     break;
976                 case 'K':
977                     val = Long.parseLong(ss) * 1024;
978                     break;
979                 case 'B':
980                     val = Long.parseLong(ss);
981                     break;
982                 case '0':
983                 case '1':
984                 case '2':
985                 case '3':
986                 case '4':
987                 case '5':
988                 case '6':
989                 case '7':
990                 case '8':
991                 case '9':
992                     val = Long.parseLong(s);
993                     break;
994                 default:
995                     broken = true;
996             }
997 
998             if (!broken) {
999                 return val;
1000             }
1001             return dflt;
1002         }
1003         return dflt;
1004     }
1005 
1006     /**
1007      * Return the string canonical file name of a matching entry
1008      *
1009      * @param theParameter the key to match
1010      * @param dflt the string to use as a default when no matches are found
1011      * @return the first matching value run through File.getCanonicalPath
1012      */
1013     @Override
1014     public String findCanonicalFileNameEntry(final String theParameter, final String dflt) {
1015         final String fn = findStringEntry(theParameter, dflt);
1016         if (fn != null && fn.length() > 0) {
1017             try {
1018                 return new File(fn).getCanonicalPath();
1019             } catch (IOException ex) {
1020                 logger.error("Cannot compute canonical path on {}", fn, ex);
1021             }
1022         }
1023         return fn;
1024     }
1025 
1026     /**
1027      * Return an int of the first entry matching the key parameter or the default if no match is found
1028      *
1029      * @param theParameter the key to match
1030      * @param dflt the int to use when no matches are found
1031      * @return the first matching value or the default when none found
1032      */
1033     @Override
1034     public int findIntEntry(final String theParameter, final int dflt) {
1035         final List<String> matchingEntries = findEntries(theParameter);
1036 
1037         if (!matchingEntries.isEmpty()) {
1038             try {
1039                 return Integer.parseInt(matchingEntries.get(0));
1040             } catch (NumberFormatException e) {
1041                 logger.warn("{} is non-numeric returning default value: {}", theParameter, dflt);
1042             }
1043         }
1044         return dflt;
1045     }
1046 
1047     /**
1048      * Return a long of the first entry matching the key parameter or the default if no match is found
1049      *
1050      * @param theParameter the key to match
1051      * @param dflt the value to use when no matches are found
1052      * @return the first matching value or the default when none found
1053      */
1054     @Override
1055     public long findLongEntry(final String theParameter, final long dflt) {
1056         final List<String> matchingEntries = findEntries(theParameter);
1057 
1058         if (!matchingEntries.isEmpty()) {
1059             try {
1060                 return Long.parseLong(matchingEntries.get(0));
1061             } catch (NumberFormatException e) {
1062                 logger.warn("{} is non-numeric returning default value: {}", theParameter, dflt);
1063             }
1064         }
1065         return dflt;
1066     }
1067 
1068     /**
1069      * Return a double of the first entry matching the key parameter or the default if no match is found
1070      *
1071      * @param theParameter the key to match
1072      * @param dflt the value to use when no matches are found
1073      * @return the first matching value or the default when none found
1074      */
1075     @Override
1076     public double findDoubleEntry(final String theParameter, final double dflt) {
1077         final List<String> matchingEntries = findEntries(theParameter);
1078 
1079         if (!matchingEntries.isEmpty()) {
1080             try {
1081                 return Double.parseDouble(matchingEntries.get(0));
1082             } catch (NumberFormatException e) {
1083                 logger.warn("{} is non-numeric returning default value: {}", theParameter, dflt);
1084             }
1085         }
1086         return dflt;
1087     }
1088 
1089     /**
1090      * Return boolean of the first entry matching the key parameter or the default if no match is found
1091      *
1092      * @param theParameter the key to match
1093      * @param dflt the value to use when no matches are found
1094      * @return the first matching value or the default when none found
1095      */
1096     @Override
1097     public boolean findBooleanEntry(final String theParameter, final boolean dflt) {
1098         final List<String> matchingEntries = findEntries(theParameter);
1099 
1100         if (!matchingEntries.isEmpty()) {
1101             String el = matchingEntries.get(0);
1102             el = el.toUpperCase(Locale.getDefault());
1103             if (el.startsWith("F")) {
1104                 return false;
1105             } else if (el.startsWith("T")) {
1106                 return true;
1107             }
1108         }
1109         return dflt;
1110     }
1111 
1112     /**
1113      * Return boolean of the first entry matching the key parameter or the default if no match is found
1114      *
1115      * @param theParameter the key to match
1116      * @param dflt the value to use when no matches are found
1117      * @return the first matching value or the default when none found
1118      */
1119     @Override
1120     public boolean findBooleanEntry(final String theParameter, final String dflt) {
1121         return findBooleanEntry(theParameter, Boolean.parseBoolean(dflt));
1122     }
1123 
1124     /**
1125      * Get the value of a parameter that is purported to be numeric
1126      *
1127      * @param name the name of the parameter
1128      */
1129     protected int getNumericParameter(final String name) {
1130         final String val = this.values.get(name);
1131         int i = -1;
1132         if (val != null) {
1133             try {
1134                 i = Integer.parseInt(val);
1135             } catch (NumberFormatException ex) {
1136                 logger.warn("{} is non-numeric: {}", name, val);
1137             }
1138         }
1139         return i;
1140     }
1141 
1142     public boolean debug() {
1143         return "TRUE".equalsIgnoreCase(this.values.get("DEBUG"));
1144     }
1145 
1146     /**
1147      * Merge in a new configuration set with this one. New things are supposed to override older things in the sense of
1148      * findStringEntry which only picks the top of the list, the new things should get added to the top. If the merged in
1149      * Configurator contains remove entries (operator of '!=') then it only applies to entries in this instance, not in
1150      * "other". This is slightly different than when a config is read in directly, but without that there would be no way to
1151      * remove entries from a super-config and continue to supply entries here and still be able to use the wildcard remove
1152      * (value of '*') The order in the merged config file is important. Any '!= "*"' operations must precede the new value
1153      * being supplied since normal remove operations take place in each config before the merge.
1154      *
1155      * @param other the new entries to merge in
1156      */
1157     @Override
1158     public void merge(final Configurator other) throws IOException {
1159         int i = 1;
1160 
1161         // First handle the remove entries from "other"
1162         if (other instanceof ServiceConfigGuide) {
1163             for (final ConfigEntry entry : ((ServiceConfigGuide) other).getRemoveEntries()) {
1164                 handleNewEntry(entry.getKey(), entry.getValue(), "!=", "<merge>", i++, true);
1165             }
1166         }
1167 
1168         // Add in new entries from "other" at the top of the list
1169         for (final ConfigEntry entry : other.getEntries()) {
1170             handleNewEntry(entry.getKey(), entry.getValue(), "=", "<merge>", i++, true);
1171         }
1172     }
1173 
1174     /**
1175      * Public main used to verify config file construction off-line
1176      */
1177     public static void main(final String[] args) {
1178         if (args.length < 1) {
1179             logger.error("usage: java ServiceConfigGuide configfile");
1180             return;
1181         }
1182 
1183         for (String arg : args) {
1184             try {
1185                 final ServiceConfigGuide sc = new ServiceConfigGuide(arg);
1186                 logger.info("Config File:{} ", arg);
1187                 for (int i = 0; i < sc.serviceParameters.size(); i++) {
1188                     final ConfigEntry c = sc.serviceParameters.get(i);
1189                     logger.info("{}: {}", c.getKey(), c.getValue());
1190                 }
1191                 logger.info("---");
1192             } catch (IOException e) {
1193                 logger.info("Cannot process {}:{}", arg, e.getLocalizedMessage());
1194             }
1195         }
1196     }
1197 }