Initial commit
[fillmore] / src / marina / ProjectLoader.vala
1 /* Copyright 2009 Yorba Foundation
2  *
3  * This software is licensed under the GNU Lesser General Public License
4  * (version 2.1 or later).  See the COPYING file in this distribution. 
5  */
6
7 using Logging;
8
9 namespace Model {
10
11 public class LoaderHandler : Object {
12     public signal void load_error(string error_message);
13     public signal void complete();
14     
15     public LoaderHandler() {
16     }
17     
18     public virtual bool commit_library(string[] attr_names, string[] attr_values) {
19         return true;
20     }
21     
22     public virtual bool commit_marina(string[] attr_names, string[] attr_values) {
23         return true;
24     }
25     
26     public virtual bool commit_track(string[] attr_names, string[] attr_values) {
27         return true;
28     }
29     
30     public virtual bool commit_clip(string[] attr_names, string[] attr_values) {
31         return true;
32     }
33     
34     public virtual bool commit_clipfile(string[] attr_names, string[] attr_values) {
35         return true;
36     }
37
38     public virtual bool commit_time_signature_entry(string[] attr_names, string[] attr_values) {
39         return true;
40     }
41
42     public virtual bool commit_tempo_entry(string[] attr_names, string[] attr_values) {
43         return true;
44     }
45
46     public virtual bool commit_click(string[] attr_names, string[] attr_values) {
47         return true;
48     }
49     
50     public virtual bool commit_library_preference(string[] attr_names, string[] attr_values) {
51         return true;
52     }
53     
54     public virtual void leave_library() {
55     }
56
57     public virtual void leave_marina() {
58     }    
59
60     public virtual void leave_track() {
61     }
62     
63     public virtual void leave_clip() {
64     }
65     
66     public virtual void leave_clipfile() {
67         
68     }
69 }
70
71 // TODO: Move these classes into separate file
72 public class XmlTreeLoader {
73     XmlElement current_element = null;
74     public XmlElement root = null;
75
76     public XmlTreeLoader(string document) {
77         MarkupParser parser = { xml_start_element, xml_end_element, null, null };
78         MarkupParseContext context =
79             new MarkupParseContext(parser, (MarkupParseFlags) 0, this, null);
80         try {
81             context.parse(document, document.length);
82         } catch (MarkupError e) {
83         }
84     
85     }    
86     
87     void xml_start_element(GLib.MarkupParseContext c, string name, 
88                            string[] attr_names, string[] attr_values) {
89         Model.XmlElement new_element = new Model.XmlElement(name, attr_names, attr_values, current_element);
90         if (root == null) {
91             root = new_element;
92         } else {
93             assert(current_element != null);
94             current_element.add_child(new_element);
95         }
96         
97         current_element = new_element;
98     }
99
100     void xml_end_element(GLib.MarkupParseContext c, string name) {
101         assert(current_element != null);
102         current_element = current_element.parent;
103     }
104 }
105
106 // ProjectBuilder is responsible for verifying the structure of the XML document.
107 // Subclasses of this class will be responsible for checking the attributes of 
108 // any particular element.
109 class ProjectBuilder : Object {
110     LoaderHandler handler;
111
112     public signal void error_occurred(string error);
113     
114     public ProjectBuilder(LoaderHandler handler) {
115         this.handler = handler;
116     }
117
118     bool check_name(string expected_name, XmlElement node) {
119         if (node.name == expected_name) {
120             return true;
121         }
122         
123         error_occurred("expected %s, got %s".printf(expected_name, node.name));
124         return false;
125     }
126     
127     void handle_clip(XmlElement clip) {
128         if (check_name("clip", clip)) {
129             if (handler.commit_clip(clip.attribute_names, clip.attribute_values)) {
130                 if (clip.children.size != 0) {
131                     error_occurred("clip cannot have children");
132                 }
133                 handler.leave_clip();
134             }
135         }
136     }
137
138     void handle_track(XmlElement track) {
139         if (check_name("track", track)) {
140             emit(this, Facility.LOADING, Level.VERBOSE, "loading track");
141             if (handler.commit_track(track.attribute_names, track.attribute_values)) {
142                 foreach (XmlElement child in track.children) {
143                     handle_clip(child);
144                 }
145                 handler.leave_track();
146             }
147         }
148     }
149
150     void handle_preference(XmlElement preference) {
151         if ("click" == preference.name) {
152             handler.commit_click(preference.attribute_names, preference.attribute_values);
153         } else if ("library" == preference.name) {
154             handler.commit_library_preference(
155                 preference.attribute_names, preference.attribute_values);
156         } else {
157             error_occurred("Unknown preference: %s".printf(preference.name));
158         }
159     }
160     
161     void handle_time_signature(XmlElement time_signature) {
162         foreach (XmlElement child in time_signature.children) {
163             if (check_name("entry", child)) {
164                 if (!handler.commit_time_signature_entry(child.attribute_names, 
165                     child.attribute_values)) {
166                         error_occurred("Improper time signature node");
167                 }
168             }
169         }
170     }
171     
172     void handle_tempo(XmlElement tempo) {
173         foreach (XmlElement child in tempo.children) {
174             if (check_name("entry", child)) {
175                 if (!handler.commit_tempo_entry(child.attribute_names, child.attribute_values)) {
176                     error_occurred("Improper tempo node");
177                 }
178             }
179         }
180     }
181     
182     void handle_map(XmlElement map) {
183         switch (map.name) {
184             case "tempo":
185                 handle_tempo(map);
186                 break;
187             case "time_signature":
188                 handle_time_signature(map);
189                 break;
190             default:
191                 error_occurred("improper map node");
192                 break;
193         }
194     }
195     
196     void handle_library(XmlElement library) {
197         if (handler.commit_library(library.attribute_names, library.attribute_values)) {
198             foreach (XmlElement child in library.children) {
199                 if (!handler.commit_clipfile(child.attribute_names, child.attribute_values))
200                     error_occurred("Improper library node");
201             } 
202             handler.leave_library();
203         }
204     }
205     
206     void handle_tracks(XmlElement tracks) {
207         foreach (XmlElement child in tracks.children) {
208             handle_track(child);
209         }
210     }
211
212     void handle_preferences(XmlElement preferences) {
213         foreach (XmlElement child in preferences.children) {
214             handle_preference(child);
215         }
216     }
217     void handle_maps(XmlElement maps) {
218         foreach (XmlElement child in maps.children) {
219             handle_map(child);
220         }
221     }
222     public bool check_project(XmlElement? root) {
223         if (root == null) {
224             error_occurred("Invalid XML file!");
225             return false;
226         }
227         
228         if (check_name("marina", root) &&
229             handler.commit_marina(root.attribute_names, root.attribute_values)) {
230             if (root.children.size != 3 && root.children.size != 4) {
231                 error_occurred("Improper number of children!");
232                 return false;
233             }
234             
235             if (!check_name("library", root.children[0]) ||
236                 !check_name("tracks", root.children[1]) ||
237                 !check_name("preferences", root.children[2]))
238                 return false;
239                 
240             if (root.children.size == 4 && !check_name("maps", root.children[3])) {
241                 return false;
242             }
243         } else
244             return false;
245         return true;
246     }
247
248     public void build_project(XmlElement? root) {
249         handle_library(root.children[0]);
250         handle_tracks(root.children[1]);
251         handle_preferences(root.children[2]);
252         if (root.children.size == 4) {
253             handle_maps(root.children[3]);
254         }
255         
256         handler.leave_marina();
257     }
258 }
259
260 public class XmlElement {
261     public string name { get; private set; }
262     
263     public string[] attribute_names;
264     
265     public string[] attribute_values;
266     
267     public Gee.ArrayList<XmlElement> children { get { return _children; } }
268     
269     public weak XmlElement? parent { get; private set; }
270     
271     private Gee.ArrayList<XmlElement> _children;
272     public XmlElement(string name, string[] attribute_names, string[] attribute_values, 
273                         XmlElement? parent) {
274         this.name = name;
275
276         this.attribute_names = copy_array(attribute_names);
277         this.attribute_values = copy_array(attribute_values);
278         this.parent = parent;
279         this._children = new Gee.ArrayList<XmlElement>();
280     }
281     
282     public void add_child(XmlElement child_element) {
283         _children.add(child_element);
284     }
285 }
286
287 public class ProjectLoader : Object {
288     string? file_name;
289     LoaderHandler loader_handler;
290     string text;
291     size_t text_len;
292     bool project_load_completed = false;
293     bool load_completed_fired = false;
294     bool handler_completed = false;
295
296     public signal void load_started(string filename);
297     public signal void load_complete();
298     public signal void load_error(string error);
299     
300     public ProjectLoader(LoaderHandler loader_handler, string? file_name) {
301         this.file_name = file_name;
302         this.loader_handler = loader_handler;
303         loader_handler.load_error.connect(on_load_error);
304         loader_handler.complete.connect(on_handler_complete);
305     }
306     
307     void on_load_error(string error) {
308         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_load_error");
309         load_error(error);
310     }
311     
312     void on_handler_complete() {
313         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_handler_complete");
314         handler_completed = true;
315         if (project_load_completed && !load_completed_fired) {
316             load_completed_fired = true;
317             load_complete();
318         }
319     }
320     
321     public void load() {
322         try {
323             FileUtils.get_contents(file_name, out text, out text_len);
324         } catch (FileError e) {
325             emit(this, Facility.LOADING, Level.MEDIUM, 
326                 "error loading %s: %s".printf(file_name, e.message));
327             load_error(e.message);
328             load_complete();
329             return;
330         }
331         emit(this, Facility.LOADING, Level.VERBOSE, "Building tree for %s".printf(file_name));
332         XmlTreeLoader tree_loader = new XmlTreeLoader(text);
333         
334         ProjectBuilder builder = new ProjectBuilder(loader_handler);
335         builder.error_occurred.connect(on_load_error);
336         
337         if (builder.check_project(tree_loader.root)) {
338             emit(this, Facility.LOADING, Level.VERBOSE, "project checked out.  starting load");
339             load_started(file_name);
340             builder.build_project(tree_loader.root);
341             project_load_completed = true;
342             if (handler_completed && !load_completed_fired) {
343                 load_completed_fired = true;
344                 load_complete();
345             }
346         }
347         else {
348             emit(this, Facility.LOADING, Level.INFO, "project did not check out.  stopping.");
349             load_complete(); 
350         }
351     }
352 }
353 }