2 * This file is part of WolneLektury-Mobile, licensed under GNU Affero GPLv3 or later.
3 * Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
8 var WL_INITIAL = WL + '/media/api/mobile/initial/initial.db';
9 var WL_UPDATE = WL + '/api/changes/SINCE.json?book_fields=author,html,parent,parent_number,sort_key,title' +
10 '&tag_fields=books,category,name,sort_key' +
11 '&tag_categories=author,epoch,genre,kind';
15 var categories = {'author': 'autor',
22 // FIXME: htmlescape strings!
25 // for preparing sql statements
27 // var s = new Sql("INSERT ... '{0}', '{1}' ...";
28 // s.prepare("abc", ...)
29 var Sql = function(scheme) {
33 self.sql_escape = function(term) {
34 return term.toString().replace("'", "''");
37 self.prepare = function() {
39 return self.text.replace(/{(\d+)}/g, function(match, number) {
40 return self.sql_escape(args[parseInt(number)]);
46 var Catalogue = new function() {
47 /* API for database */
52 this.init = function(success, error) {
53 debug('Catalogue.init');
55 self.updateDB(function() {
57 self.db = window.openDatabase("wolnelektury", "1.0", "WL Catalogue", 1000000);
60 onFunctionCall: function(val) {
61 var re = new RegExp(val.getString(0));
62 if (val.getString(1).match(re))
68 self.db.createFunction("REGEXP", 2, regexp);*/
72 error && error('Nie mogę otworzyć bazy danych: ' + err);
76 error && error('Błąd migracji: ' + err);
80 self.sqlSanitize = function(term) {
81 return term.toString().replace("'", "''");
85 /* check if DB needs updating and upload a fresh copy, if so */
86 this.updateDB = function(success, error) {
87 var has_ver = window.localStorage.getItem('db_ver');
88 if (has_ver == DB_VER) {
89 debug('db ok, skipping')
94 var done = function() {
96 window.localStorage.setItem('db_ver', DB_VER);
102 // this is Android-specific for now
103 self.createdb(done, error);
107 this.createdb = function(success, error) {
110 var dbname = "wolnelektury";
111 var db = window.openDatabase(dbname, "1.0", "WL Catalogue", 1000000);
113 debug('db created successfully');
116 sqls.push('CREATE TABLE IF NOT EXISTS book (\
117 id INTEGER PRIMARY KEY,\
120 html_file_size INTEGER,\
122 parent_number INTEGER,\
124 pretty_size VARCHAR,\
128 sqls.push('CREATE INDEX IF NOT EXISTS book_title_index ON book (title);');
129 sqls.push('CREATE INDEX IF NOT EXISTS book_sort_key_index ON book (sort_key);');
130 sqls.push('CREATE INDEX IF NOT EXISTS book_parent_index ON book (parent);');
131 sqls.push('CREATE TABLE IF NOT EXISTS tag (\
132 id INTEGER PRIMARY KEY,\
138 sqls.push('CREATE INDEX IF NOT EXISTS tag_name_index ON tag (name);');
139 sqls.push('CREATE INDEX IF NOT EXISTS tag_category_index ON tag (category);');
140 sqls.push('CREATE INDEX IF NOT EXISTS tag_sort_key_index ON tag (name);');
141 sqls.push('CREATE TABLE IF NOT EXISTS state (last_checked INTEGER);');
142 sqls.push('DELETE FROM state;');
143 sqls.push('INSERT INTO state (last_checked) VALUES(0);');
144 self.chainSqls(sqls, success, error);
145 /*DBPut.fetch(WL_INITIAL, function(data) {
146 debug('db fetch successful');
147 success && success();
149 error && error('Błąd podczas pobierania bazy danych: ' + data);
152 error && error('Błąd podczas inicjowania bazy danych: ' + data);
157 this.withState = function(callback) {
158 self.db.transaction(function(tx) {
159 tx.executeSql("SELECT * FROM state", [],
160 function(tx, results) {
161 if (results.rows.length) {
162 callback(results.rows.item(0));
165 callback({last_checked: 0});
172 this.withBook = function(id, callback, error) {
173 debug('withBook '+id)
174 self.db.transaction(function(tx) {
175 tx.executeSql("SELECT * FROM book WHERE id="+id, [],
176 function(tx, results) {
177 if (results.rows.length) {
178 callback(results.rows.item(0));
187 this.withBooks = function(ids, callback) {
188 debug('withBooks ' + ids)
189 self.db.transaction(function(tx) {
190 tx.executeSql("SELECT * FROM book WHERE id IN ("+ids+") ORDER BY sort_key", [],
191 function(tx, results) {
193 var count = results.rows.length;
194 for (var i=0; i<count; ++i) {
195 items.push(results.rows.item(i));
203 this.withChildren = function(id, callback) {
204 debug('withChildren ' + id)
205 self.db.transaction(function(tx) {
206 tx.executeSql("SELECT * FROM book WHERE parent="+id+" ORDER BY parent_number, sort_key", [],
207 function(tx, results) {
209 var count = results.rows.length;
210 for (var i=0; i<count; ++i) {
211 books.push(results.rows.item(i));
218 this.withTag = function(id, callback, error) {
220 self.db.transaction(function(tx) {
221 tx.executeSql("SELECT * FROM tag WHERE id="+id, [],
222 function(tx, results) {
223 if (results.rows.length) {
224 callback(results.rows.item(0));
233 this.withCategory = function(category, callback) {
234 debug('withCategory ' + category)
235 self.db.transaction(function(tx) {
236 tx.executeSql("SELECT * FROM tag WHERE category='"+category+"' ORDER BY sort_key", [],
237 function(tx, results) {
239 var count = results.rows.length;
240 for (var i=0; i<count; ++i)
241 items.push(results.rows.item(i));
248 /* takes a query, returns a list of {view,id,label} objects to a callback */
249 this.withSearch = function(term, callback) {
250 debug('searching...');
251 term = term.replace(/^\s+|\s+$/g, '') ;
254 function booksFound(tx, results) {
255 var len = results.rows.length;
256 debug('found books: ' + len);
257 for (var i=0; i<len; i++) {
258 var item = results.rows.item(i);
266 function tagsFound(tx, results) {
267 var len = results.rows.length;
268 debug('found tags: ' + len);
269 for (var i=0; i<len; i++) {
270 var item = results.rows.item(i);
276 // TODO error handling
282 // TODO pliterki, start of the word match
283 self.db.transaction(function(tx) {
284 sql_term = self.sqlSanitize(term); // this is still insane, % and _
285 tx.executeSql("SELECT * FROM book WHERE title LIKE '%"+sql_term+"%' ORDER BY sort_key LIMIT 10", [],
286 //tx.executeSql("SELECT * FROM book WHERE title REGEXP '.*"+sql_term+".*' ORDER BY sort_key", [],
287 function(tx, results) {
289 booksFound(tx, results);
290 // and proceed to tags
291 tx.executeSql("SELECT * FROM tag WHERE name LIKE '%"+sql_term+"%' ORDER BY sort_key LIMIT 10",
295 debug('ERROR:search: '+err.code);
301 self.chainSqls = function(sqls, success, error) {
302 self.db.transaction(function(tx) {
303 var do_next = function() {
305 var sql = sqls.shift();
307 tx.executeSql(sql, [], do_next, error);
310 success && success();
318 self.update = function(data, success, error) {
319 var addBookSql = new Sql("\
320 INSERT OR REPLACE INTO book \
321 (id, title, html_file, html_file_size, parent, parent_number, sort_key, pretty_size, authors) \
323 ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}', '{8}')");
324 var addTagSql = new Sql("INSERT OR REPLACE INTO tag (id, category, name, sort_key, books) VALUES ('{0}', '{1}', '{2}', '{3}', '{4}')");
329 for (i in data.deleted.books) {
330 var book_id = data.deleted.books[i];
331 sqls.push("DELETE FROM book WHERE id=" + book_id);
332 FileRepo.deleteIfExists(book_id);
335 for (i in data.deleted.tags) {
336 var tag_id = data.deleted.tags[i];
337 sqls.push("DELETE FROM tag WHERE id=" + tag_id);
342 for (i in data.updated.books) {
343 var book = data.updated.books[i];
344 if (!book.html) book.html = {};
345 if (!book.html.url) book.html.url = '';
346 if (!book.html.size) book.html.size = '';
347 if (!book.parent) book.parent = '';
348 if (!book.parent_number) book.parent_number = '';
349 var pretty_size = prettySize(book.html.size);
350 sqls.push(addBookSql.prepare(
351 book.id, book.title, book.html.url, book.html.size,
352 book.parent, book.parent_number, book.sort_key, pretty_size, book.author
354 FileRepo.deleteIfExists(book.id);
357 for (i in data.updated.tags) {
358 var tag = data.updated.tags[i];
359 var category = categories[tag.category];
360 var books = tag.books.join(',');
361 sqls.push(addTagSql.prepare(tag.id, category, tag.name, tag.sort_key, books));
365 sqls.push("UPDATE state SET last_checked=" + data.time_checked);
367 self.chainSqls(sqls, success, error);
371 this.sync = function(success, error) {
372 self.withState(function(state) {
373 var url = WL_UPDATE.replace("SINCE", state.last_checked);
374 debug('sync: ' + url);
375 var xhr = new XMLHttpRequest();
376 xhr.open("GET", url);
377 xhr.onload = function() {
378 debug('sync: fetched by ajax: ' + url);
379 self.update(JSON.parse(xhr.responseText), success, error);
381 xhr.onerror = function(e) {
382 error && error("Błąd aktualizacji bazy danych." + e);
386 success && success();
389 this.updateLocal = function() {
390 FileRepo.withLocal(function(local) {
391 self.db.transaction(function(tx) {
392 tx.executeSql("UPDATE book SET _local=0", [], function(tx, results) {
395 for (var i = 0; i < ll; i ++) {
396 ids.push(local[i].name);
399 tx.executeSql("UPDATE book SET _local=1 where id in ("+ids+")");
403 self.db.transaction(function(tx) {
404 tx.executeSql("UPDATE book SET _local=0");