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