1 #ifndef _OptionsDB_h_
2 #define _OptionsDB_h_
3 
4 #include "Export.h"
5 #include "Logger.h"
6 #include "OptionValidators.h"
7 
8 #include <boost/any.hpp>
9 #include <boost/filesystem/fstream.hpp>
10 #include <boost/signals2/signal.hpp>
11 
12 #include <functional>
13 #include <map>
14 #include <unordered_set>
15 
16 
17 class OptionsDB;
18 class XMLDoc;
19 class XMLElement;
20 
21 /////////////////////////////////////////////
22 // Free Functions
23 /////////////////////////////////////////////
24 
25 //! The function signature for functions that add Options to the OptionsDB (void (OptionsDB&))
26 typedef std::function<void (OptionsDB&)> OptionsDBFn;
27 
28 /** adds \a function to a vector of pointers to functions that add Options to
29   * the OptionsDB.  This function returns a boolean so that it can be used to
30   * declare a dummy static variable that causes \a function to be registered as
31   * a side effect (e.g. at file scope:
32   * "bool unused_bool = RegisterOption(&foo)"). */
33 FO_COMMON_API bool RegisterOptions(OptionsDBFn function);
34 
35 /** returns the single instance of the OptionsDB class */
36 FO_COMMON_API OptionsDB& GetOptionsDB();
37 
38 
39 /////////////////////////////////////////////
40 // OptionsDB
41 /////////////////////////////////////////////
42 /** a database of values of arbitrarily mixed types that can be initialized
43   * from an XML config file and/or the command line.  OptionsDB should be used
44   * for initializing global settings for an application that should be
45   * specified from the command line or from config files.  Such options might
46   * be the resolution to use when running the program, the color depth, number
47   * of players, etc.  The entire DB can be written out to config file, to later
48   * be reloaded.  This allows runtime settings to be preserved from one
49   * execution to the next, and still allows overrides of these settings from
50   * the command line.
51   * <br><br>OptionsDB must have its options and their types specified before
52   * any values are assigned to those options.  This is because setting an
53   * option in the DB requires the type of the option to be known in advance.
54   * To specify the options, you may either use static initialization time or
55   * normal runtime calls to Add() and AddFlag().  Note that the exact type of
56   * added item must be specified with Add*(), so that subsequent calls to Get()
57   * do not throw.  For instance, if you want to add an unsigned value
58   * accessible as "foo", you should call: \verbatim
59         Add("foo", "The number of foos.", 1u);\endverbatim
60   * Making the same call as above with "1" instead of "1u" will cause a later
61   * call to Get<unsigned int>("foo") to throw, since "foo" would be an int, not
62   * an unsigned int.  To guard against this, you may wish to call Add() with an
63   * explicit template parameterization, such as Add<unsigned int>(...).
64   * <br><br>Flag options are just boolean values that are false by default.
65   * Their values may be read and set normally, the same as any other boolean
66   * option.  For exapmple, reading a flag's value form the DB can be done using
67   * using: \verbatim
68         Get<bool>(flag_name);endverbatim
69   * <br><br>Adding options at static initialization time means that the options
70   * specified will be available before main() is called, and yet you do not
71   * have to fill main.cpp with all your option specifications.  Instead, you
72   * can put them in the files in which their options are used.
73   * <br><br>OptionsDB has an optional dotted notation for option names.  This
74   * is important in use with XML only.  When options are specified as e.g.
75   * "foo.bar1" and "foo.bar2", the resulting XML file will show them as:\verbatim
76         <foo>
77             <bar1>x</bar>
78             <bar2>y</bar>
79         </foo>\endverbatim
80   * This allows options to be grouped in ways that are sensible for the
81   * application.  This is only done as a convenience to the user.  It does not
82   * change the way the options are treated in any way.  Note that is is
83   * perfectly legal also to have an option "foo" containing a value "z" in the
84   * example above.
85   * <br><br>A few things should be said about the command-line version of
86   * options.  All flag command-line options (specified with AddFlag()) are
87   * assumed to have false as their default value.  This means that their mere
88   * presence on the command line means that they indicate a value of true;
89   * they need no argument.  For example, specifying "--version" on the command
90   * line sets the option "version" in the DB to true, and leaving it out sets the
91   * option to false.
92   * <br><br>Long-form names should be preceded with "--", and the
93   * single-character version should be preceded with "-".  An exception to this
94   * is that multiple single-character (boolean) options may be run together
95   * (e.g. "-cxvf").  Also, the last option in such a group may take an
96   * argument, which must immediately follow the group, separated by a space as
97   * (usual.
98   * <br><br>Finally, note that std::runtime_error exceptions will be thrown any
99   * time a problem occurs with an option (calling Get() for one that doesn't
100   * exist, Add()ing one twice, etc.), and boost::bad_any_cast exceptions will
101   * be thrown in situations in which an invalid type-conversion occurs,
102   * including string-to-type, type-to-string or type-to-type as in the case of
103   * Get() calls with the wrong tempate parameter.
104   * \see RegisterOptions (for static-time options specification) */
105 class FO_COMMON_API OptionsDB {
106 public:
107     /** \name Signal Types */ //@{
108     /** emitted when an option has changed */
109     typedef boost::signals2::signal<void ()>                   OptionChangedSignalType;
110     /** emitted when an option is added */
111     typedef boost::signals2::signal<void (const std::string&)> OptionAddedSignalType;
112     /** emitted when an option is removed */
113     typedef boost::signals2::signal<void (const std::string&)> OptionRemovedSignalType;
114     //@}
115 
116     /** indicates whether an option with name \a name has been added to this
117         OptionsDB. */
OptionExists(const std::string & name)118     bool OptionExists(const std::string& name) const
119     { return m_options.count(name) && m_options.at(name).recognized; }
120 
121     /** write the optionDB's non-default state to the XML config file. */
122     bool Commit(bool only_if_dirty = true, bool only_non_default = true);
123 
124     /** Write any options that are not at default value to persistent config, replacing any existing file
125      *
126      *  @returns bool If file was successfully written
127      */
128     bool CommitPersistent();
129 
130     /** validates a value for an option. throws std::runtime_error if no option
131       * \a name exists.  throws bad_lexical_cast if \a value cannot be
132       * converted to the type of the option \a name. */
133     void Validate(const std::string& name, const std::string& value) const;
134 
135     /** returns the value of option \a name. Note that the exact type of item
136       * stored in the option \a name must be known in advance.  This means that
137       * Get() must be called as Get<int>("foo"), etc. */
138     template <typename T>
Get(const std::string & name)139     T Get(const std::string& name) const
140     {
141         auto it = m_options.find(name);
142         if (!OptionExists(it))
143             throw std::runtime_error("OptionsDB::Get<>() : Attempted to get nonexistent option \"" + name + "\".");
144         try {
145             return boost::any_cast<T>(it->second.value);
146         } catch (const boost::bad_any_cast&) {
147             ErrorLogger() << "bad any cast converting value option named: " << name << ". Returning default value instead";
148             try {
149                 return boost::any_cast<T>(it->second.default_value);
150             } catch (const boost::bad_any_cast&) {
151                 ErrorLogger() << "bad any cast converting default value of option named: " << name << ". Returning data-type default value instead: " << T();
152                 return T();
153             }
154         }
155     }
156 
157     /** returns the default value of option \a name. Note that the exact type
158       * of item stored in the option \a name must be known in advance.  This
159       * means that GetDefault() must be called as Get<int>("foo"), etc. */
160     template <typename T>
GetDefault(const std::string & name)161     T GetDefault(const std::string& name) const
162     {
163         auto it = m_options.find(name);
164         if (!OptionExists(it))
165             throw std::runtime_error("OptionsDB::GetDefault<>() : Attempted to get nonexistent option \"" + name + "\".");
166         try {
167             return boost::any_cast<T>(it->second.default_value);
168         } catch (const boost::bad_any_cast&) {
169             ErrorLogger() << "bad any cast converting default value of option named: " << name << "  returning type default value instead";
170             return T();
171         }
172     }
173 
IsDefaultValue(const std::string & name)174     bool IsDefaultValue(const std::string& name) const {
175         auto it = m_options.find(name);
176         if (!OptionExists(it))
177             throw std::runtime_error("OptionsDB::IsDefaultValue<>() : Attempted to get nonexistent option \"" + name + "\".");
178         return IsDefaultValue(it);
179     }
180 
181     /** returns the string representation of the value of the option \a name.*/
182     std::string GetValueString(const std::string& option_name) const;
183 
184     /** returns the string representation of the default value of the
185       * option \a name.*/
186     std::string GetDefaultValueString(const std::string& option_name) const;
187 
188     /** returns the description string for option \a option_name, or throws
189       * std::runtime_error if no such Option exists. */
190     const std::string&  GetDescription(const std::string& option_name) const;
191 
192     /** returns the validator for option \a option_name, or throws
193       * std::runtime_error if no such Option exists. */
194     std::shared_ptr<const ValidatorBase> GetValidator(const std::string& option_name) const;
195 
196     /** writes a usage message to \a os */
197     void GetUsage(std::ostream& os, const std::string& command_line = "", bool allow_unrecognized = false) const;
198 
199     /** @brief  Saves the contents of the options DB to the @p doc XMLDoc.
200      *
201      * @param[in,out] doc  The document this OptionsDB should be written to.
202      *      This resets the given @p doc.
203      * @param[in] non_default_only Do not include options which are set to their
204      *      default value, is unrecognized, or is "version.string"
205      */
206     void GetXML(XMLDoc& doc, bool non_default_only = false, bool include_version = true) const;
207 
208     /** find all registered Options that begin with \a prefix and store them in
209       * \a ret. If \p allow_unrecognized then include unrecognized options. */
210     void FindOptions(std::set<std::string>& ret, const std::string& prefix, bool allow_unrecognized = false) const;
211 
212     /** the option changed signal object for the given option */
213     OptionChangedSignalType&        OptionChangedSignal(const std::string& option);
214 
215     mutable OptionAddedSignalType   OptionAddedSignal;   ///< the option added signal object for this DB
216     mutable OptionRemovedSignalType OptionRemovedSignal; ///< the change removed signal object for this DB
217 
218     /** adds an Option, optionally with a custom validator */
219     template <typename T>
220     void Add(const std::string& name, const std::string& description, T default_value,
221              const ValidatorBase& validator = Validator<T>(), bool storable = true,
222              const std::string& section = std::string())
223     {
224         auto it = m_options.find(name);
225         boost::any value = default_value;
226         // Check that this option hasn't already been registered and apply any value that was specified on the command line or from a config file.
227         if (it != m_options.end()) {
228             if (it->second.recognized)
229                 throw std::runtime_error("OptionsDB::Add<>() : Option " + name + " was registered twice.");
230             if (it->second.flag) { // SetFrom[...]() sets "flag" to true for unrecognised options if they look like flags (i.e. no parameter is found for the option)
231                 ErrorLogger() << "OptionsDB::Add<>() : Option " << name << " was specified on the command line or in a config file with no value, using default value.";
232             } else {
233                 try {
234                     // This option was previously specified externally but was not recognized at the time, attempt to parse the value found there
235                     value = validator.Validate(it->second.ValueToString());
catch(boost::bad_lexical_cast &)236                 } catch (boost::bad_lexical_cast&) {
237                     ErrorLogger() << "OptionsDB::Add<>() : Option " << name << " was given the value \"" << it->second.ValueToString() << "\" from the command line or a config file but that value couldn't be converted to the correct type, using default value instead.";
238                 }
239             }
240         }
241         m_options[name] = Option(static_cast<char>(0), name, value, default_value,
242                                  description, validator.Clone(), storable, false, true, section);
243         m_dirty = true;
244         OptionAddedSignal(name);
245     }
246 
247     /** adds an Option with an alternative one-character shortened name,
248       * optionally with a custom validator */
249     template <typename T>
250     void Add(char short_name, const std::string& name, const std::string& description, T default_value,
251              const ValidatorBase& validator = Validator<T>(), bool storable = true,
252              const std::string& section = std::string())
253     {
254         auto it = m_options.find(name);
255         boost::any value = default_value;
256         // Check that this option hasn't already been registered and apply any value that was specified on the command line or from a config file.
257         if (it != m_options.end()) {
258             if (it->second.recognized)
259                 throw std::runtime_error("OptionsDB::Add<>() : Option " + name + " was registered twice.");
260             if (it->second.flag) { // SetFrom[...]() sets "flag" to true for unrecognised options if they look like flags (i.e. no parameter is found for the option)
261                 ErrorLogger() << "OptionsDB::Add<>() : Option " << name << " was specified on the command line or in a config file with no value, using default value.";
262             } else {
263                 try {
264                     // This option was previously specified externally but was not recognized at the time, attempt to parse the value found there
265                     value = validator.Validate(it->second.ValueToString());
catch(boost::bad_lexical_cast &)266                 } catch (boost::bad_lexical_cast&) {
267                     ErrorLogger() << "OptionsDB::Add<>() : Option " << name << " was given the value from the command line or a config file that cannot be converted to the correct type. Using default value instead.";
268                 }
269             }
270         }
271         m_options[name] = Option(short_name, name, value, default_value, description,
272                                  validator.Clone(), storable, false, true, section);
273         m_dirty = true;
274         OptionAddedSignal(name);
275     }
276 
277     /** adds a flag Option, which is treated as a boolean value with a default
278       * of false.  Using the flag on the command line at all indicates that its
279       * value it set to true. */
280     void AddFlag(const std::string& name, const std::string& description,
281                  bool storable = true, const std::string& section = std::string())
282     {
283         auto it = m_options.find(name);
284         bool value = false;
285         // Check that this option hasn't already been registered and apply any value that was specified on the command line or from a config file.
286         if (it != m_options.end()) {
287             if (it->second.recognized)
288                 throw std::runtime_error("OptionsDB::AddFlag<>() : Option " + name + " was registered twice.");
289             if (!it->second.flag) // SetFrom[...]() sets "flag" to false on unrecognised options if they don't look like flags (flags have no parameter on the command line or have an empty tag in XML)
290                 ErrorLogger() << "OptionsDB::AddFlag<>() : Option " << name << " was specified with the value \"" << it->second.ValueToString() << "\", but flags should not have values assigned to them.";
291             value = true; // if the flag is present at all its value is true
292         }
293         m_options[name] = Option(static_cast<char>(0), name, value,
294                                  boost::lexical_cast<std::string>(false),
295                                  description, nullptr, storable, true, true, section);
296         m_dirty = true;
297         OptionAddedSignal(name);
298     }
299 
300     /** adds an Option with an alternative one-character shortened name, which
301       * is treated as a boolean value with a default of false.  Using the flag
302       * on the command line at all indicates that its value it set to true. */
303     void AddFlag(char short_name, const std::string& name,
304                  const std::string& description, bool storable = true,
305                  const std::string& section = std::string())
306     {
307         auto it = m_options.find(name);
308         bool value = false;
309         // Check that this option hasn't already been registered and apply any value that was specified on the command line or from a config file.
310         if (it != m_options.end()) {
311             if (it->second.recognized)
312                 throw std::runtime_error("OptionsDB::AddFlag<>() : Option " + name + " was registered twice.");
313             if (!it->second.flag) // SetFrom[...]() sets "flag" to false on unrecognised options if they don't look like flags (flags have no parameter on the command line or have an empty tag in XML)
314                 ErrorLogger() << "OptionsDB::AddFlag<>() : Option " << name << " was specified with the value \"" << it->second.ValueToString() << "\", but flags should not have values assigned to them.";
315             value = true; // if the flag is present at all its value is true
316         }
317         m_options[name] = Option(short_name, name, value,
318                                  boost::lexical_cast<std::string>(false),
319                                  description, nullptr, storable, true, true, section);
320         m_dirty = true;
321         OptionAddedSignal(name);
322     }
323 
324     /** removes an Option */
325     void Remove(const std::string& name);
326 
327     /** removes all unrecognized Options that begin with \a prefix.  A blank
328       * string will remove all unrecognized Options. */
329     void RemoveUnrecognized(const std::string& prefix = "");
330 
331     /** sets the value of option \a name to \a value */
332     template <typename T>
Set(const std::string & name,const T & value)333     void Set(const std::string& name, const T& value)
334     {
335         auto it = m_options.find(name);
336         if (!OptionExists(it))
337             throw std::runtime_error("OptionsDB::Set<>() : Attempted to set nonexistent option \"" + name + "\".");
338         m_dirty |= it->second.SetFromValue(value);
339     }
340 
341     /** Set the default value of option @p name to @p value */
342     template <typename T>
SetDefault(const std::string & name,const T & value)343     void SetDefault(const std::string& name, const T& value) {
344         std::map<std::string, Option>::iterator it = m_options.find(name);
345         if (!OptionExists(it))
346             throw std::runtime_error("Attempted to set default value of nonexistent option \"" + name + "\".");
347         if (it->second.default_value.type() != typeid(T))
348             throw boost::bad_any_cast();
349         it->second.default_value = value;
350     }
351 
352     /** if an xml file exists at \a file_path and has the same version tag as \a version, fill the
353       * DB options contained in that file (read the file using XMLDoc, then fill the DB using SetFromXML)
354       * if the \a version string is empty, bypass that check */
355     void SetFromFile(const boost::filesystem::path& file_path,
356                      const std::string& version = "");
357 
358     /** fills some or all of the options of the DB from values passed in from
359       * the command line */
360     void SetFromCommandLine(const std::vector<std::string>& args);
361 
362     /** fills some or all of the options of the DB from values stored in
363       * XMLDoc \a doc */
364     void SetFromXML(const XMLDoc& doc);
365 
366     struct FO_COMMON_API Option {
367         Option();
368         Option(char short_name_, const std::string& name_, const boost::any& value_,
369                const boost::any& default_value_, const std::string& description_,
370                const ValidatorBase *validator_, bool storable_, bool flag_, bool recognized_,
371                const std::string& section = std::string());
372 
373         // SetFromValue returns true if this->value is successfully changed
374         template <typename T>
375         bool            SetFromValue(const T& value_);
376         // SetFromString returns true if this->value is successfully changed
377         bool            SetFromString(const std::string& str);
378         // SetToDefault returns true if this->value is successfully changed
379         bool            SetToDefault();
380         std::string     ValueToString() const;
381         std::string     DefaultValueToString() const;
382         bool            ValueIsDefault() const;
383 
384         std::string     name;           ///< the name of the option
385         char            short_name{0};  ///< the one character abbreviation of the option
386         boost::any      value;          ///< the value of the option
387         boost::any      default_value;  ///< the default value of the option
388         std::string     description;    ///< a desription of the option
389         std::unordered_set<std::string> sections; ///< sections this option should display under
390 
391         /** A validator for the option.  Flags have no validators; lexical_cast
392             boolean conversions are done for them. */
393         std::shared_ptr<const ValidatorBase> validator;
394 
395         bool            storable = false;   ///< whether this option can be stored in an XML config file for use across multiple runs
396         bool            flag = false;
397         bool            recognized = false; ///< whether this option has been registered before being specified via an XML input, unrecognized options can't be parsed (don't know their type) but are stored in case they are later registered with Add()
398 
399         mutable std::shared_ptr<boost::signals2::signal<void ()>> option_changed_sig_ptr;
400 
401         static std::map<char, std::string> short_names;   ///< the master list of abbreviated option names, and their corresponding long-form names
402     };
403 
404     struct FO_COMMON_API OptionSection {
405         OptionSection();
406         OptionSection(const std::string& name_, const std::string& description_,
407                       std::function<bool (const std::string&)> option_predicate_);
408 
409         std::string name;
410         std::string description;
411         std::function<bool (const std::string&)> option_predicate = nullptr;
412     };
413 
414     /** Defines an option section with a description and optionally a option predicate.
415      *  @param name Name of section, typically in the form of a left side subset of an option name.
416      *  @param description Stringtable key used for local description
417      *  @param option_predicate Functor accepting a option name in the form of a std::string const ref and
418      *                          returning a bool. Options which return true are displayed in the section for @p name */
419     void AddSection(const std::string& name, const std::string& description,
420                     std::function<bool (const std::string&)> option_predicate = nullptr);
421 
422 private:
423     /** indicates whether the option referenced by \a it has been added to this
424         OptionsDB.  Overloaded for convenient use within other OptionsDB
425         functions */
OptionExists(std::map<std::string,Option>::const_iterator it)426     bool OptionExists(std::map<std::string, Option>::const_iterator it) const
427     { return it != m_options.end() && it->second.recognized; }
428 
429     /** indicates whether the current value of the option references by \a is
430         the default value for that option */
IsDefaultValue(std::map<std::string,Option>::const_iterator it)431     bool IsDefaultValue(std::map<std::string, Option>::const_iterator it) const
432     { return it != m_options.end() && it->second.ValueToString() == it->second.DefaultValueToString(); }
433 
434     OptionsDB();
435 
436     void SetFromXMLRecursive(const XMLElement& elem, const std::string& section_name);
437 
438     /** Determine known option sections and which options each contains
439      *  A special "root" section is added for determined top-level sections */
440     std::unordered_map<std::string, std::set<std::string>> OptionsBySection(bool allow_unrecognized = false) const;
441 
442     std::map<std::string, Option>   m_options;
443     std::unordered_map<std::string, OptionSection> m_sections;
444     static OptionsDB*               s_options_db;
445     bool                            m_dirty; //< has OptionsDB changed since last Commit()
446 
447     friend FO_COMMON_API OptionsDB& GetOptionsDB();
448 };
449 
450 template <typename T>
SetFromValue(const T & value_)451 bool OptionsDB::Option::SetFromValue(const T& value_) {
452     if (value.type() != typeid(T))
453         ErrorLogger() << "OptionsDB::Option::SetFromValue expected type " << value.type().name() << " but got value of type " << typeid(T).name();
454 
455     bool changed = false;
456 
457     try {
458         if (!flag) {
459             changed = validator->String(value) != validator->String(value_);
460         } else {
461             changed = (boost::lexical_cast<std::string>(boost::any_cast<bool>(value))
462                     != boost::lexical_cast<std::string>(boost::any_cast<bool>(value_)));
463         }
464     } catch (...) {
465         ErrorLogger() << "Exception thrown when setting option value, probably due to the previous value being invalid?";
466         changed = true;
467     }
468 
469     if (changed) {
470         value = value_;
471         (*option_changed_sig_ptr)();
472     }
473     return changed;
474 }
475 
476 // needed because std::vector<std::string> is not streamable
477 template <>
478 FO_COMMON_API std::vector<std::string> OptionsDB::Get<std::vector<std::string>>(const std::string& name) const;
479 
480 
481 #endif // _OptionsDB_h_
482