Add simple android client.
[odfkit:webodf.git] / android / assets / lib / odf / OdfContainer.js
1 /*global runtime odf core*/
2 runtime.loadClass("core.Base64");
3 runtime.loadClass("core.Zip");
4 /**
5  * This is a pure javascript implementation of the first simple OdfKit api.
6  **/
7 odf.OdfContainer = (function () {
8     var officens = "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
9         nodeorder = ['meta', 'settings', 'scripts', 'font-face-decls', 'styles',
10             'automatic-styles', 'master-styles', 'body'],
11         base64 = new core.Base64();
12     /**
13      * @param {?Node} node
14      * @param {!string} ns
15      * @param {!string} name
16      * @return {?Node}
17      */
18     function getDirectChild(node, ns, name) {
19         node = (node) ? node.firstChild : null;
20         while (node) {
21             if (node.localName === name && node.namespaceURI === ns) {
22                 return node;
23             }
24             node = node.nextSibling;
25         }
26     }
27     /**
28      * @param {!Node} child
29      * @return {!number}
30      */
31     function getNodePosition(child) {
32         var childpos = 0, i, l = nodeorder.length;
33         for (i = 0; i < l; i += 1) {
34             if (child.namespaceURI === officens &&
35                     child.localName === nodeorder[i]) {
36                 return i;
37             }
38         }
39         return -1;
40     }
41     /**
42      * @param {!Node} node
43      * @param {?Node} child
44      * @return {undefined}
45      */
46     function setChild(node, child) {
47         if (!child) {
48             return;
49         }
50         var childpos = getNodePosition(child),
51             pos,
52             c = node.firstChild;
53         if (childpos === -1) {
54             return;
55         }
56         while (c) {
57             pos = getNodePosition(c);
58             if (pos !== -1 && pos > childpos) {
59                 break;
60             }
61             c = c.nextSibling;
62         }
63         node.insertBefore(child, c);
64     }
65     /**
66      * A DOM element that is part of and ODF part of a DOM.
67      * @constructor
68      * @extends {Element}
69      */
70     function ODFElement() {
71     }
72     /**
73      * The root element of an ODF document.
74      * @constructor
75      * @extends {ODFElement}
76      */
77     function ODFDocumentElement(odfcontainer) {
78         this.OdfContainer = odfcontainer;
79     }
80     ODFDocumentElement.prototype = new ODFElement();
81     ODFDocumentElement.prototype.constructor = ODFDocumentElement;
82     ODFDocumentElement.namespaceURI = officens;
83     ODFDocumentElement.localName = 'document';
84     // private constructor
85     /**
86      * @constructor
87      * @param {!string} name
88      * @param {!OdfContainer} container
89      * @param {!core.Zip} zip
90      */
91     function OdfPart(name, container, zip) {
92         var self = this,
93             privatedata;
94
95         // declare public variables
96         this.size = 0;
97         this.type = null;
98         this.name = name;
99         this.container = container;
100         this.url = null;
101         this.document = null;
102         this.onreadystatechange = null;
103         this.onchange = null;
104         this.EMPTY = 0;
105         this.LOADING = 1;
106         this.DONE = 2;
107         this.state = this.EMPTY;
108
109         // private functions
110         function createUrl() {
111             self.url = null;
112             if (!privatedata) {
113                 return;
114             }
115             self.url = 'data:;base64,';
116             // to avoid exceptions, base64 encoding is done in chunks
117             var chunksize = 90000, // must be multiple of 3 and less than 100000
118                 i = 0;
119             while (i < privatedata.length) {
120                 self.url += base64.toBase64(privatedata.substr(i, chunksize));
121                 i += chunksize;
122             }
123         }
124         // public functions
125         this.load = function () {
126             zip.load(name, function (err, data) {
127                 privatedata = data;
128                 createUrl();
129                 if (self.onchange) {
130                     self.onchange(self);
131                 }
132                 if (self.onstatereadychange) {
133                     self.onstatereadychange(self);
134                 }
135             });
136         };
137         this.abort = function () {
138             // TODO
139         };
140     }
141     OdfPart.prototype.load = function () {
142     };
143     OdfPart.prototype.getUrl = function () {
144         if (this.data) {
145             return 'data:;base64,' + base64.toBase64(this.data);
146         }
147         return null;
148     };
149     /**
150      * @constructor
151      * @param {!OdfContainer} odfcontainer
152      */
153     function OdfPartList(odfcontainer) {
154         var self = this;
155         // declare public variables
156         this.length = 0;
157         this.item = function (index) {
158         };
159     }
160     /**
161      * @constructor
162      * @param {!string} url
163      */
164     function OdfContainer(url) {
165         var self = this,
166             zip = null;
167
168         // NOTE each instance of OdfContainer has a copy of the private functions
169         // it would be better to have a class OdfContainerPrivate where the
170         // private functions can be defined via OdfContainerPrivate.prototype
171         // without exposing them
172
173         // declare public variables
174         this.onstatereadychange = null;
175         this.onchange = null;
176         this.state = null;
177         this.rootElement = null;
178         this.parts = null;
179
180         // private functions
181         /**
182          * @param {!Document} xmldoc
183          * @return {!Node}
184          */
185         function importRootNode(xmldoc) {
186             var doc = self.rootElement.ownerDocument;
187             return doc.importNode(xmldoc.documentElement, true);
188         }
189         /**
190          * @param {!Document} xmldoc
191          * @return {undefined}
192          */
193         function handleStylesXml(xmldoc) {
194             var node = importRootNode(xmldoc),
195                 root = self.rootElement;
196             if (!node || node.localName !== 'document-styles' ||
197                     node.namespaceURI !== officens) {
198                 self.state = OdfContainer.INVALID;
199                 return;
200             }
201             root.styles = getDirectChild(node, officens, 'styles');
202             setChild(root, root.styles);
203             root.automaticStyles = getDirectChild(node, officens, 'automatic-styles');
204             setChild(root, root.automaticStyles);
205             root.masterStyles = getDirectChild(node, officens, 'master-styles');
206             setChild(root, root.masterStyles);
207         }
208         /**
209          * @param {!Document} xmldoc
210          * @return {undefined}
211          */
212         function handleContentXml(xmldoc) {
213             var node = importRootNode(xmldoc),
214                 root,
215                 automaticStyles,
216                 c;
217             if (!node || node.localName !== 'document-content' ||
218                     node.namespaceURI !== officens) {
219                 self.state = OdfContainer.INVALID;
220                 return;
221             }
222             root = self.rootElement;
223             root.body = getDirectChild(node, officens, 'body');
224             setChild(root, root.body);
225             automaticStyles = getDirectChild(node, officens, 'automatic-styles');
226             if (root.automaticStyles && automaticStyles) {
227                 c = automaticStyles.firstChild;
228                 while (c) {
229                     root.automaticStyles.appendChild(c);
230                     c = automaticStyles.firstChild; // works because node c moved
231                 }
232             } else if (automaticStyles) {
233                 root.automaticStyles = automaticStyles;
234                 setChild(root.automaticStyles, automaticStyles);
235             }
236         }
237         /**
238          * @param {!Document} xmldoc
239          * @return {undefined}
240          */
241         function handleMetaXml(xmldoc) {
242             var node = importRootNode(xmldoc),
243                 root;
244             if (!node || node.localName !== 'document-meta' ||
245                     node.namespaceURI !== officens) {
246                 return;
247             }
248             root = self.rootElement;
249             root.meta = getDirectChild(node, officens, 'meta');
250             setChild(root, root.meta);
251         }
252         /**
253          * @param {!Document} xmldoc
254          * @return {undefined}
255          */
256         function handleSettingsXml(xmldoc) {
257             var node = importRootNode(xmldoc),
258                 root;
259             if (!node || node.localName !== 'document-settings' ||
260                     node.namespaceURI !== officens) {
261                 return;
262             }
263             root = self.rootElement;
264             root.settings = getDirectChild(node, officens, 'settings');
265             setChild(root, root.settings);
266         }
267         /**
268          * @param {!string} filepath
269          * @param {!function(?string,?Document)} callback
270          * @return {undefined}
271          */
272         function getXmlNode(filepath, callback) {
273             zip.load(filepath, function (err, xmldata) {
274                 if (err) {
275                     callback(err, null);
276                     return;
277                 }
278                 // assume the xml input data is utf8
279                 // this can be done better
280                 base64.convertUTF8StringToUTF16String(xmldata,
281                         function (str, done) {
282                     if (done) {
283                         var parser = new DOMParser();
284                         str = parser.parseFromString(str, "text/xml");
285                         callback(null, str);
286                     }
287                     return true;
288                 });
289             });
290         }
291         function setState(state) {
292             self.state = state;
293             if (self.onchange) {
294                 self.onchange(self);
295             }
296             if (self.onstatereadychange) {
297                 self.onstatereadychange(self);
298             }
299         }
300         /**
301          * @return {undefined}
302          */
303         function loadComponents() {
304             // always load content.xml, meta.xml, styles.xml and settings.xml
305             getXmlNode('styles.xml', function (err, xmldoc) {
306                 handleStylesXml(xmldoc);
307                 if (self.state === OdfContainer.INVALID) {
308                     return;
309                 }
310                 getXmlNode('content.xml', function (err, xmldoc) {
311                     handleContentXml(xmldoc);
312                     if (self.state === OdfContainer.INVALID) {
313                         return;
314                     }
315                     getXmlNode('meta.xml', function (err, xmldoc) {
316                         handleMetaXml(xmldoc);
317                         if (self.state === OdfContainer.INVALID) {
318                             return;
319                         }
320                         getXmlNode('settings.xml', function (err, xmldoc) {
321                             handleSettingsXml(xmldoc);
322                             if (self.state !== OdfContainer.INVALID) {
323                                 setState(OdfContainer.DONE);
324                             }
325                         });                        
326                     });                    
327                 });
328             });
329         }
330         function createElement(Type) {
331             var original = document.createElementNS(
332                     Type.namespaceURI, Type.localName),
333                 method,
334                 iface = new Type();
335             for (method in iface) {
336                 if (iface.hasOwnProperty(method)) {
337                     original[method] = iface[method];
338                 }
339             }
340             return original;
341         }
342         // TODO: support single xml file serialization and different ODF
343         // versions
344         /**
345          * @param {!string} filepath
346          * @param {function(?string, ?string):undefined} callback
347          * @return {undefined}
348          */
349         function load(filepath, callback) {
350             zip.load(filepath, function (err, data) {
351                 if (self.onchange) {
352                     self.onchange(self);
353                 }
354                 if (self.onstatereadychange) {
355                     self.onstatereadychange(self);
356                 }
357             });
358         }
359         // public functions
360         /**
361          * Open file and parse it. Return the Xml Node. Return the root node of
362          * the file or null if this is not possible.
363          * For 'content.xml', 'styles.xml', 'meta.xml', and 'settings.xml', the
364          * elements 'document-content', 'document-styles', 'document-meta', or
365          * 'document-settings' will be returned respectively.
366          * @param {!string} partname
367          * @return {!OdfPart}
368          **/
369         this.getPart = function (partname) {
370             return new OdfPart(partname, self, zip);
371         };
372
373         // initialize public variables
374         this.state = OdfContainer.LOADING;
375         this.rootElement = createElement(ODFDocumentElement);
376         this.parts = new OdfPartList(this);
377
378         // initialize private variables
379         zip = new core.Zip(url, function (err, zipobject) {
380             zip = zipobject;
381             if (err) {
382                 zip.error = err;
383             } else {
384                 loadComponents();
385             }
386         });
387     }
388     OdfContainer.EMPTY = 0;
389     OdfContainer.LOADING = 1;
390     OdfContainer.DONE = 2;
391     OdfContainer.INVALID = 3;
392     OdfContainer.SAVING = 4;
393     OdfContainer.MODIFIED = 5;
394     /**
395      * @param {!string} url
396      * @return {!OdfContainer}
397      */
398     OdfContainer.getContainer = function (url) {
399         return new OdfContainer(url);
400     };
401     return OdfContainer;
402 }());