ios version
[wl-mobile.git] / www / js / catalogue.js
diff --git a/www/js/catalogue.js b/www/js/catalogue.js
new file mode 100644 (file)
index 0000000..40cd0bd
--- /dev/null
@@ -0,0 +1,408 @@
+/*
+ * This file is part of WolneLektury-Mobile, licensed under GNU Affero GPLv3 or later.
+ * Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+ */
+
+var DB_VER = '0.9.17';
+
+var WL_INITIAL = WL + '/media/api/mobile/initial/initial.db';
+var WL_UPDATE = WL + '/api/changes/SINCE.json?book_fields=author,html,parent,parent_number,sort_key,title' +
+               '&tag_fields=books,category,name,sort_key' +
+               '&tag_categories=author,epoch,genre,kind';
+
+
+
+var categories = {'author': 'autor',
+              'epoch': 'epoka', 
+              'genre': 'gatunek', 
+              'kind': 'rodzaj', 
+              'theme': 'motyw'
+              }
+
+// FIXME: htmlescape strings!
+
+
+// for preparing sql statements
+// use like: 
+//   var s = new Sql("INSERT ... '{0}', '{1}' ...";
+//   s.prepare("abc", ...)
+var Sql = function(scheme) {
+       var self = this;
+       self.text = scheme;
+       
+       self.sql_escape = function(term) {
+               return term.toString().replace("'", "''");
+       };
+       
+       self.prepare = function() {
+               var args = arguments;
+               return self.text.replace(/{(\d+)}/g, function(match, number) {
+                       return self.sql_escape(args[parseInt(number)]);
+               });
+       }
+};
+
+
+var Catalogue = new function() {
+       /* API for database */
+
+       var self = this;
+       self.db = null;
+
+       this.init = function(success, error) {
+               debug('Catalogue.init');
+               
+               self.updateDB(function() {
+                       if (!self.db)
+                               self.db = window.openDatabase("wolnelektury", "1.0", "WL Catalogue", 1000000);
+                       if (self.db) {
+                               /*var regexp = {
+                                               onFunctionCall: function(val) {
+                                                       var re = new RegExp(val.getString(0));
+                                                               if (val.getString(1).match(re))
+                                                                       return 1;
+                                                               else
+                                                                       return 0;
+                                               }
+                                       };
+                               self.db.createFunction("REGEXP", 2, regexp);*/
+
+                               success && success();
+                       } else {
+                               error && error('Nie mogę otworzyć bazy danych: ' + err);
+                       }
+                       
+               }, function(err) {
+                       error && error('Błąd migracji: ' + err);
+               });
+       };
+
+       self.sqlSanitize = function(term) {
+               return term.toString().replace("'", "''");
+       };
+
+
+       /* check if DB needs updating and upload a fresh copy, if so */
+       this.updateDB = function(success, error) {
+               var has_ver = window.localStorage.getItem('db_ver');
+               if (has_ver == DB_VER) {
+                       debug('db ok, skipping')
+                       success && success();
+                       return;
+               }
+
+               var done = function() {
+                       FileRepo.clear();
+                       window.localStorage.setItem('db_ver', DB_VER);
+                       debug('db updated');
+                       success && success();
+               };
+
+               // db initialize
+               // this is Android-specific for now
+               self.createdb(done, error);
+       };
+
+
+       this.createdb = function(success, error) {
+               debug('create db');
+
+               var dbname = "wolnelektury";
+               var db = window.openDatabase(dbname, "1.0", "WL Catalogue", 1000000);
+               if (db) {
+                       debug('db created successfully');
+                       self.db = db;
+                       var sqls = [];
+                       sqls.push('CREATE TABLE IF NOT EXISTS book (\
+                                         id INTEGER PRIMARY KEY,\
+                                         title VARCHAR,\
+                                         html_file VARCHAR,\
+                                         html_file_size INTEGER,\
+                                         parent INTEGER,\
+                                         parent_number INTEGER,\
+                                         sort_key VARCHAR,\
+                                         pretty_size VARCHAR,\
+                                         authors VARCHAR,\
+                                         _local BOOLEAN\
+                                         );');
+                       sqls.push('CREATE INDEX IF NOT EXISTS book_title_index ON book (title);');
+                       sqls.push('CREATE INDEX IF NOT EXISTS book_sort_key_index ON book (sort_key);');
+                       sqls.push('CREATE INDEX IF NOT EXISTS book_parent_index ON book (parent);');
+                       sqls.push('CREATE TABLE IF NOT EXISTS tag (\
+                                         id INTEGER PRIMARY KEY,\
+                                         name VARCHAR,\
+                                         category VARCHAR,\
+                                         sort_key VARCHAR,\
+                                         books VARCHAR\
+                                         );');
+                       sqls.push('CREATE INDEX IF NOT EXISTS tag_name_index ON tag (name);');
+                       sqls.push('CREATE INDEX IF NOT EXISTS tag_category_index ON tag (category);');
+                       sqls.push('CREATE INDEX IF NOT EXISTS tag_sort_key_index ON tag (name);');
+                       sqls.push('CREATE TABLE IF NOT EXISTS state (last_checked INTEGER);');
+                       sqls.push('DELETE FROM state;');
+                       sqls.push('INSERT INTO state (last_checked) VALUES(0);');
+                       self.chainSqls(sqls, success, error);
+                       /*DBPut.fetch(WL_INITIAL, function(data) {
+                               debug('db fetch successful');
+                               success && success();
+                       }, function(data) {
+                               error && error('Błąd podczas pobierania bazy danych: ' + data);
+                       });*/
+               } else {
+                       error && error('Błąd podczas inicjowania bazy danych: ' + data);
+               }
+       };
+
+
+       this.withState = function(callback) {
+               self.db.transaction(function(tx) {
+                       tx.executeSql("SELECT * FROM state", [], 
+                               function(tx, results) {
+                                       if (results.rows.length) {
+                                               callback(results.rows.item(0));
+                                       }
+                                       else {
+                                               callback({last_checked: 0});
+                                       }
+                               });
+               });
+       };
+
+
+       this.withBook = function(id, callback, error) {
+               debug('withBook '+id)
+               self.db.transaction(function(tx) {
+                       tx.executeSql("SELECT * FROM book WHERE id="+id, [], 
+                               function(tx, results) {
+                                       if (results.rows.length) {
+                                               callback(results.rows.item(0));
+                                       }
+                                       else {
+                                               error && error();
+                                       }
+                               });
+               });
+       };
+
+       this.withBooks = function(ids, callback) {
+               debug('withBooks ' + ids)
+               self.db.transaction(function(tx) {
+                       tx.executeSql("SELECT * FROM book WHERE id IN ("+ids+") ORDER BY sort_key", [], 
+                               function(tx, results) {
+                                       var items = [];
+                                       var count = results.rows.length;
+                                       for (var i=0; i<count; ++i) {
+                                               items.push(results.rows.item(i));
+                                       }
+                                       callback(items);
+                               });
+               });
+       };
+
+
+       this.withChildren = function(id, callback) {
+               debug('withChildren ' + id)
+               self.db.transaction(function(tx) {
+                       tx.executeSql("SELECT * FROM book WHERE parent="+id+" ORDER BY parent_number, sort_key", [], 
+                               function(tx, results) {
+                                       var books = [];
+                                       var count = results.rows.length;
+                                       for (var i=0; i<count; ++i) {
+                                               books.push(results.rows.item(i));
+                                       }
+                                       callback(books);
+                       });
+               });
+       };
+
+       this.withTag = function(id, callback, error) {
+               debug('withTag '+id)
+               self.db.transaction(function(tx) {
+                       tx.executeSql("SELECT * FROM tag WHERE id="+id, [], 
+                               function(tx, results) {
+                                       if (results.rows.length) {
+                                               callback(results.rows.item(0));
+                                       }
+                                       else {
+                                               error && error();
+                                       }
+                               });
+               });
+       };
+
+       this.withCategory = function(category, callback) {
+               debug('withCategory ' + category)
+               self.db.transaction(function(tx) {
+                       tx.executeSql("SELECT * FROM tag WHERE category='"+category+"' ORDER BY sort_key", [], 
+                               function(tx, results) {
+                                       var items = [];
+                                       var count = results.rows.length;
+                                       for (var i=0; i<count; ++i)
+                                               items.push(results.rows.item(i));
+                                       callback(items);
+                               });
+               });
+       };
+
+
+       /* takes a query, returns a list of {view,id,label} objects to a callback */
+       this.withSearch = function(term, callback) {
+               debug('searching...');
+               term =  term.replace(/^\s+|\s+$/g, '') ;
+               var found = [];
+
+               function booksFound(tx, results) {
+                       var len = results.rows.length;
+                       debug('found books: ' + len);
+                       for (var i=0; i<len; i++) {
+                               var item = results.rows.item(i);
+                               found.push({
+                                       view: "Book",
+                                       item: item
+                               });
+                       }
+               };
+
+               function tagsFound(tx, results) {
+                       var len = results.rows.length;
+                       debug('found tags: ' + len);
+                       for (var i=0; i<len; i++) {
+                               var item = results.rows.item(i);
+                               found.push({
+                                       view: "Tag",
+                                       item: item
+                               });
+                       }
+                       // TODO error handling
+                       callback(found);
+               };
+
+
+               // FIXME escaping
+               // TODO pliterki, start of the word match
+               self.db.transaction(function(tx) {
+                       sql_term = self.sqlSanitize(term); // this is still insane, % and _
+                       tx.executeSql("SELECT * FROM book WHERE title LIKE '%"+sql_term+"%' ORDER BY sort_key LIMIT 10", [],
+                       //tx.executeSql("SELECT * FROM book WHERE title REGEXP '.*"+sql_term+".*' ORDER BY sort_key", [],
+                               function(tx, results) {
+                                       // save the books
+                                       booksFound(tx, results);
+                                       // and proceed to tags
+                                       tx.executeSql("SELECT * FROM tag WHERE name LIKE '%"+sql_term+"%' ORDER BY sort_key LIMIT 10",
+                                                       [], tagsFound);
+                               },
+                               function(err) {
+                                       debug('ERROR:search: '+err.code);
+                                       callback([]);
+                               });
+               });
+       };
+
+       self.chainSqls = function(sqls, success, error) {
+               self.db.transaction(function(tx) {
+                       var do_next = function() {
+                               if (sqls.length) {
+                                       var sql = sqls.shift();
+                                       debug(sql);
+                                       tx.executeSql(sql, [], do_next, error);
+                               }
+                               else {
+                                       success && success();
+                               }
+                       }
+                       do_next();
+               });
+       };
+
+
+       self.update = function(data, success, error) {
+               var addBookSql = new Sql("\
+                       INSERT OR REPLACE INTO book \
+                               (id, title, html_file,  html_file_size, parent, parent_number, sort_key, pretty_size, authors) \
+                       VALUES \
+                               ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}', '{8}')");
+               var addTagSql = new Sql("INSERT OR REPLACE INTO tag (id, category, name, sort_key, books) VALUES ('{0}', '{1}', '{2}', '{3}', '{4}')");
+
+               var sqls = [];
+
+               if (data.deleted) {
+                       for (i in data.deleted.books) {
+                               var book_id = data.deleted.books[i];
+                               sqls.push("DELETE FROM book WHERE id=" + book_id);
+                               FileRepo.deleteIfExists(book_id);
+                       }
+
+                       for (i in data.deleted.tags) {
+                               var tag_id = data.deleted.tags[i];
+                               sqls.push("DELETE FROM tag WHERE id=" + tag_id);
+                       }
+               }
+
+               if (data.updated) {
+                       for (i in data.updated.books) {
+                               var book = data.updated.books[i];
+                               if (!book.html) book.html = {};
+                               if (!book.html.url) book.html.url = '';
+                               if (!book.html.size) book.html.size = '';
+                               if (!book.parent) book.parent = '';
+                               if (!book.parent_number) book.parent_number = '';
+                               var pretty_size = prettySize(book.html.size);
+                               sqls.push(addBookSql.prepare(
+                                       book.id, book.title, book.html.url, book.html.size,
+                                       book.parent, book.parent_number, book.sort_key, pretty_size, book.author
+                               ));
+                               FileRepo.deleteIfExists(book.id);
+                       }
+
+                       for (i in data.updated.tags) {
+                               var tag = data.updated.tags[i];
+                               var category = categories[tag.category];
+                               var books = tag.books.join(',');
+                               sqls.push(addTagSql.prepare(tag.id, category, tag.name, tag.sort_key, books));
+                       }
+               }
+
+               sqls.push("UPDATE state SET last_checked=" + data.time_checked);
+
+               self.chainSqls(sqls, success, error);
+       };
+
+
+       this.sync = function(success, error) {
+               self.withState(function(state) {
+                       var url = WL_UPDATE.replace("SINCE", state.last_checked); 
+                       debug('sync: ' + url);
+                       var xhr = new XMLHttpRequest();
+                       xhr.open("GET", url);
+                       xhr.onload = function() {
+                               debug('sync: fetched by ajax: ' + url);                 
+                               self.update(JSON.parse(xhr.responseText), success, error);
+                       }
+                       xhr.onerror = function(e) {
+                               error && error("Błąd aktualizacji bazy danych." + e);
+                       }
+                       xhr.send();
+               });
+               success && success();
+       };
+
+       this.updateLocal = function() {
+               FileRepo.withLocal(function(local) {
+                       self.db.transaction(function(tx) {
+                               tx.executeSql("UPDATE book SET _local=0", [], function(tx, results) {
+                                       ll = local.length;
+                                       var ids = [];
+                                       for (var i = 0; i < ll; i ++) {
+                                               ids.push(local[i].name);
+                                       }
+                                       ids = ids.join(',');
+                                       tx.executeSql("UPDATE book SET _local=1 where id in ("+ids+")"); 
+                               });
+                       });
+               }, function() {
+                       self.db.transaction(function(tx) {
+                               tx.executeSql("UPDATE book SET _local=0");
+                       });
+               });
+       };
+}