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