1 module dprefhandler;
2 
3 import std.stdio;
4 import std.conv: to;
5 import std.process: environment;
6 import std.file: mkdirRecurse, exists;
7 import std.exception: basicExceptionCtors;
8 
9 /**
10 Collection of preferences, addressed by names, each containing actual value,
11 initial value and default value.
12 */
13 class DPrefHandler
14 {
15 private:
16     // TODO switch to dchar and dstring
17     static const string ROW_PREFIX = ":::::";
18     static const char EQSIGN = '=';
19     static const string CFG_FILE_NAME = "config.ini";
20     version(Windows)
21     {
22         static const string PATH_SEP = "\\";
23     }
24     version(Posix)
25     {
26         static const string PATH_SEP = "/";
27     }
28 
29     string _name, _configDirectoryPath;
30     DPref[string] prefs;
31 
32     /**
33     Fill the value of class variable _configDirectoryPath with directory
34     which stores the config file.
35     */
36     void generateConfigDirectoryPath()
37     {
38         string appDataDir = null;
39         version(Windows)
40         {
41             appDataDir = environment.get("APPDATA");
42         }
43         version(linux)
44         {
45             appDataDir = "~/.local/share";
46         }
47         version(OSX)
48         {
49             appDataDir = "~/Library/Application Support";
50         }
51         if (appDataDir !is null)
52         {
53             _configDirectoryPath = appDataDir ~ PATH_SEP ~ name;
54         }
55     }
56     
57 public:
58     /**
59     Constructs instance of DPrefHandler.
60     pName is later used as directory name in OS user preferences directory.
61     */
62     this(string pName)
63     {
64         name = pName;
65         generateConfigDirectoryPath;
66     }
67     
68     /**
69     Fill actual and initial values of all existing preferences, as well as
70     create new preferences with initial == existing == default values, from
71     config file, that is stored in OS user preferences directory.
72 
73     Loading of values from file does not happen in constructor!
74     First developer creates the instance, then adds prefs with default values,
75     only after that pref values can be populated from file by calling this method.
76     If file has a pref that is no defined by developer, new pref is created automatically.
77     */
78     string loadFromFile()
79     {
80         if (configDirectoryPath !is null)
81         {
82             mkdirRecurse(configDirectoryPath);
83             string fileFullPath = configDirectoryPath ~ PATH_SEP ~ CFG_FILE_NAME;
84             if (!exists(fileFullPath)) {
85                 return null;
86             }
87 
88             File file = File(fileFullPath, "r");
89 
90             char[] buf;
91             while (!file.eof)
92             {
93                 char[] line = buf;
94                 file.readln(line);
95                 if (line.length > ROW_PREFIX.length + 1)
96                 {
97                     if (line[0..ROW_PREFIX.length] == ROW_PREFIX)
98                     {
99                         // Start of the config line
100                         foreach (size_t i, char c; line[ROW_PREFIX.length..$])
101                         {
102                             if (c == EQSIGN)
103                             {
104                                 string key = line[ROW_PREFIX.length..ROW_PREFIX.length + i].idup;
105                                 string value = line[ROW_PREFIX.length + i + 1..$ - 1].idup;
106                                 try
107                                 {
108                                     // Key already exists
109                                     prefs[key].actualValue = value;
110                                     prefs[key].initialValue = value;
111                                 }
112                                 catch (core.exception.RangeError e)
113                                 {
114                                     // Key does not exist yet
115                                     prefs[key] = new DPref(key, value);
116                                 }
117                                 break;
118                             }
119                         }
120                     }
121                 }
122                 else
123                 {
124                     // TODO
125                 }
126             }
127             debug writeln(this);
128             return file.name;
129         }
130         return null;
131     }
132     
133     /**
134     Save actual values of all preferences to config file,
135     that is stored in OS user preferences directory.
136     */
137     string saveToFile()
138     {
139         if (configDirectoryPath !is null)
140         {
141             mkdirRecurse(configDirectoryPath);
142             File file = File(configDirectoryPath ~ PATH_SEP ~ CFG_FILE_NAME, "w+");
143             foreach (key; prefs.byKey)
144             {
145                 file.writeln(ROW_PREFIX ~ key ~ EQSIGN ~ prefs[key].actualValue);
146             }
147             return file.name;
148         }
149         return null;
150     }
151     
152     /**
153     Create new preference or overwrite an existing one.
154     */
155     DPrefHandler addPref(T)(string name, T defaultValue)
156     {
157         // TODO validate name, replace spaces with _, etc
158         prefs[name] = new DPref(name, to!string(defaultValue));
159         return this;
160     }
161     
162     /**
163     Get actual value of preference specified by provided name.
164     Throws DPrefException if preference with provided name does not exist.
165     */
166     T getActualValue(T)(string propertyName)
167     {
168         try
169         {
170             return to!T(prefs[propertyName].actualValue);
171         }
172         catch (core.exception.RangeError e)
173         {
174             throw new DPrefException(
175                 DPrefException.NO_PREF_FOUND ~ propertyName);
176         }
177     }
178 
179     /**
180     Get iniital value of preference specified by provided name.
181     Throws DPrefException if preference with provided name does not exist.
182     */
183     T getInitialValue(T)(string propertyName)
184     {
185         try
186         {
187             return to!T(prefs[propertyName].initialValue);
188         }
189         catch (core.exception.RangeError e)
190         {
191             throw new DPrefException(
192                 DPrefException.NO_PREF_FOUND ~ propertyName); 
193         }
194     }
195     
196     /**
197     Get default value of preference specified by provided name.
198     Throws DPrefException if preference with provided name does not exist.
199     */
200     T getDefaultValue(T)(string propertyName)
201     {
202         try
203         {
204             return to!T(prefs[propertyName].defaultValue);
205         }
206         catch (core.exception.RangeError e)
207         {
208             throw new DPrefException(
209                 DPrefException.NO_PREF_FOUND ~ propertyName); 
210         }
211     }
212     
213     /**
214     Set provided value as actual value of preference specified by provided name.
215     Throws DPrefException if preference with provided name does not exist.
216     */
217     void setActualValue(T)(string propertyName, T actualValue)
218     {
219         try
220         {
221             prefs[propertyName].actualValue = to!string(actualValue);
222         }
223         catch (core.exception.RangeError e)
224         {
225             throw new DPrefException(
226                 DPrefException.NO_PREF_FOUND ~ propertyName); 
227         }
228     }
229 
230     /**
231     Revert actual value to initial one for the preference specified by provided name.
232     Throws DPrefException if preference with provided name does not exist.
233     */
234     void revertActualToInitial(string propertyName)
235     {
236         try
237         {
238             prefs[propertyName].actualValue = prefs[propertyName].initialValue;
239         }
240         catch (core.exception.RangeError e)
241         {
242             throw new DPrefException(
243                 DPrefException.NO_PREF_FOUND ~ propertyName); 
244         }
245     }
246 
247     /**
248     Revert actual values of all preferences to initial values.
249     */
250     void revertAllActualToInitial()
251     {
252         foreach (key; prefs.byKey)
253         {
254             revertActualToInitial(key);
255         }
256     }
257 
258     /**
259     Revert actual value to default one for the preference specified by provided name.
260     Throws DPrefException if preference with provided name does not exist.
261     */
262     void revertActualToDefault(string propertyName)
263     {
264         try
265         {
266             prefs[propertyName].actualValue = prefs[propertyName].defaultValue;
267         }
268         catch (core.exception.RangeError e)
269         {
270             throw new DPrefException(
271                 DPrefException.NO_PREF_FOUND ~ propertyName); 
272         }
273     }
274 
275     /**
276     Revert actual values of all preferences to default values.
277     */
278     void revertAllActualToDefault()
279     {
280         foreach (key; prefs.byKey)
281         {
282             revertActualToDefault(key);
283         }
284     }
285     
286     /// Get name of the preference handler (used as name of the config directory)
287     string name() const @property
288     {
289         return _name;
290     }
291     
292     /// Set name of the preference handler (used as name of the config directory)
293     void name(string name) @property
294     {
295         // TODO generate default name if empty or null
296         _name = name;
297     }
298 
299     /// Get path of the config directory
300     string configDirectoryPath() const @property
301     {
302         return _configDirectoryPath;
303     }
304     
305     override string toString() const pure @safe
306     {
307         string result = _name ~ ":[";
308         foreach (key; prefs.byKey)
309         {
310             result ~= "{" ~ key ~ " : act(" ~ prefs[key].actualValue
311                 ~ "), ini(" ~ prefs[key].initialValue
312                 ~ "), def(" ~ prefs[key].defaultValue ~ ")}";
313         }
314         result ~= "]";
315         return result;
316     }
317 }
318 
319 /**
320 Single preference contains:
321 - name
322 - actual value
323 - initial value
324 - default value
325 */
326 private class DPref
327 {
328 private:
329     string name;
330     string defaultValue;
331     string initialValue;
332     string actualValue;
333     
334 public:
335     this(string name, string defaultValue)
336     {
337         this.name = name;
338         this.defaultValue = defaultValue;
339         this.initialValue = defaultValue;
340         this.actualValue = defaultValue;
341     }
342 }
343 
344 /**
345 Common Exception
346 */
347 class DPrefException : Exception
348 {
349     private static const string NO_PREF_FOUND = "No preference found by key: ";
350     /// Constructor for an extended exception
351     mixin basicExceptionCtors;
352 }