com.hichinaschool.flashcards.libanki.Storage.java Source code

Java tutorial

Introduction

Here is the source code for com.hichinaschool.flashcards.libanki.Storage.java

Source

/***************************************************************************************
 * Copyright (c) 2011 Norbert Nagold <norbert.nagold@gmail.com>                         *
 *                                                                                      *
 * This program is free software; you can redistribute it and/or modify it under        *
 * the terms of the GNU General Public License as published by the Free Software        *
 * Foundation; either version 3 of the License, or (at your option) any later           *
 * version.                                                                             *
 *                                                                                      *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
 *                                                                                      *
 * You should have received a copy of the GNU General Public License along with         *
 * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
 ****************************************************************************************/

package com.hichinaschool.flashcards.libanki;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.content.ContentValues;
import android.database.SQLException;

import com.hichinaschool.flashcards.anki.AnkiDatabaseManager;
import com.hichinaschool.flashcards.anki.AnkiDb;
import com.hichinaschool.flashcards.anki.AnkiDroidApp;

public class Storage {
    String mPath;

    /* Open a new or existing collection. Path must be unicode */
    public static Collection Collection(String path) {
        return Collection(path, false);
    }

    public static Collection Collection(String path, boolean server) {
        assert path.endsWith(".anki2");

        File dbFile = new File(path);
        boolean create = !dbFile.exists();
        if (create) {
            AnkiDroidApp.createDirectoryIfMissing(dbFile.getParentFile());
        }
        // connect
        AnkiDb db = AnkiDatabaseManager.getDatabase(path);
        int ver;
        if (create) {
            ver = _createDB(db);
        } else {
            ver = _upgradeSchema(db);
        }
        db.execute("PRAGMA temp_store = memory");

        // LIBANKI: sync, journal_mode --> in AnkiDroid done in AnkiDb

        // add db to col and do any remaining upgrades
        Collection col = new Collection(db, path, server);
        if (ver < Collection.SCHEMA_VERSION) {
            _upgrade(col, ver);
        } else if (create) {
            // add in reverse order so basic is default
            Models.addClozeModel(col);
            Models.addForwardOptionalReverse(col);
            Models.addForwardReverse(col);
            Models.addBasicModel(col);
            col.save();
        }
        return col;

    }

    private static int _upgradeSchema(AnkiDb db) {
        int ver = db.queryScalar("SELECT ver FROM col");
        if (ver == Collection.SCHEMA_VERSION) {
            return ver;
        }
        // add odid to cards, edue->odue
        if (db.queryScalar("SELECT ver FROM col") == 1) {
            db.execute("ALTER TABLE cards RENAME TO cards2");
            _addSchema(db, false);
            db.execute(
                    "insert into cards select id, nid, did, ord, mod, usn, type, queue, due, ivl, factor, reps, lapses, left, edue, 0, flags, data from cards2");
            db.execute("DROP TABLE cards2");
            db.execute("UPDATE col SET ver = 2");
            _updateIndices(db);
        }
        // remove did from notes
        if (db.queryScalar("SELECT ver FROM col") == 2) {
            db.execute("ALTER TABLE notes RENAME TO notes2");
            _addSchema(db, false);
            db.execute(
                    "insert into notes select id, guid, mid, mod, usn, tags, flds, sfld, csum, flags, data from notes2");
            db.execute("DROP TABLE notes2");
            db.execute("UPDATE col SET ver = 3");
            _updateIndices(db);
        }
        return ver;
    }

    private static void _upgrade(Collection col, int ver) {
        try {
            if (ver < 3) {
                // new deck properties
                for (JSONObject d : col.getDecks().all()) {
                    d.put("dyn", 0);
                    d.put("collapsed", false);
                    col.getDecks().save(d);
                }
            }
            if (ver < 4) {
                col.modSchema();
                ArrayList<JSONObject> clozes = new ArrayList<JSONObject>();
                for (JSONObject m : col.getModels().all()) {
                    if (!m.getJSONArray("tmpls").getJSONObject(0).getString("qfmt").contains("{{cloze:")) {
                        m.put("type", Sched.MODEL_STD);
                    } else {
                        clozes.add(m);
                    }
                }
                for (JSONObject m : clozes) {
                    _upgradeClozeModel(col, m);
                }
                col.getDb().execute("UPDATE col SET ver = 4");
            }
            if (ver < 5) {
                col.getDb().execute("UPDATE cards SET odue = 0 WHERE queue = 2");
                col.getDb().execute("UPDATE col SET ver = 5");
            }
            if (ver < 6) {
                col.modSchema();
                for (JSONObject m : col.getModels().all()) {
                    m.put("css", new JSONObject(Models.defaultModel).getString("css"));
                    JSONArray ar = m.getJSONArray("tmpls");
                    for (int i = 0; i < ar.length(); i++) {
                        JSONObject t = ar.getJSONObject(i);
                        if (!t.has("css")) {
                            continue;
                        }
                        m.put("css", m.getString("css") + "\n"
                                + t.getString("css").replace(".card ", ".card" + t.getInt("ord") + 1));
                        t.remove("css");
                    }
                    col.getModels().save(m);
                }
                col.getDb().execute("UPDATE col SET ver = 6");
            }
            if (ver < 7) {
                col.modSchema();
                col.getDb().execute("UPDATE cards SET odue = 0 WHERE (type = 1 OR queue = 2) AND NOT odid");
                col.getDb().execute("UPDATE col SET ver = 7");
            }
            if (ver < 8) {
                col.modSchema();
                col.getDb().execute("UPDATE cards SET due = due / 1000 WHERE due > 4294967296");
                col.getDb().execute("UPDATE col SET ver = 8");
            }
            if (ver < 9) {
                col.getDb().execute("UPDATE col SET ver = 9");
            }
            if (ver < 10) {
                col.getDb().execute("UPDATE cards SET left = left + left * 1000 WHERE queue = 1");
                col.getDb().execute("UPDATE col SET ver = 10");
            }
            if (ver < 11) {
                col.modSchema();
                for (JSONObject d : col.getDecks().all()) {
                    if (d.getInt("dyn") != 0) {
                        int order = d.getInt("order");
                        // failed order was removed
                        if (order >= 5) {
                            order -= 1;
                        }
                        JSONArray ja = new JSONArray(
                                Arrays.asList(new Object[] { d.getString("search"), d.getInt("limit"), order }));
                        d.put("terms", new JSONArray());
                        d.getJSONArray("terms").put(0, ja);
                        d.remove("search");
                        d.remove("limit");
                        d.remove("order");
                        d.put("resched", true);
                        d.put("return", true);
                    } else {
                        if (!d.has("extendNew")) {
                            d.put("extendNew", 10);
                            d.put("extendRev", 50);
                        }
                    }
                    col.getDecks().save(d);
                }
                for (JSONObject c : col.getDecks().allConf()) {
                    JSONObject r = c.getJSONObject("rev");
                    r.put("ivlFct", r.optDouble("ivlFct", 1));
                    if (r.has("ivlfct")) {
                        r.remove("ivlfct");
                    }
                    r.put("maxIvl", 36500);
                    col.getDecks().save(c);
                }
                for (JSONObject m : col.getModels().all()) {
                    JSONArray tmpls = m.getJSONArray("tmpls");
                    for (int ti = 0; ti < tmpls.length(); ++ti) {
                        JSONObject t = tmpls.getJSONObject(ti);
                        t.put("bqfmt", "");
                        t.put("bafmt", "");
                    }
                    col.getModels().save(m);
                }
                col.getDb().execute("update col set ver = 11");
            }
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    private static void _upgradeClozeModel(Collection col, JSONObject m) {
        try {
            m.put("type", Sched.MODEL_CLOZE);
            // convert first template
            JSONObject t = m.getJSONArray("tmpls").getJSONObject(0);
            for (String type : new String[] { "qfmt", "afmt" }) {
                t.put(type, t.getString(type).replaceAll("\\{\\{cloze:1:(.+?)\\}\\}", "{{cloze:$1}}"));
            }
            t.put("name", "Cloze");
            // delete non-cloze cards for the model
            JSONArray ja = m.getJSONArray("tmpls");
            ArrayList<JSONObject> rem = new ArrayList<JSONObject>();
            for (int i = 1; i < ja.length(); i++) {
                JSONObject ta = ja.getJSONObject(i);
                if (!ta.getString("afmt").contains("{{cloze:")) {
                    rem.add(ta);
                }
            }
            for (JSONObject r : rem) {
                // TODO: write remtemplate
                col.getModels().remTemplate(m, r);
            }
            JSONArray newArray = new JSONArray();
            newArray.put(ja.get(0));
            m.put("tmpls", newArray);
            col.getModels()._updateTemplOrds(m);
            col.getModels().save(m);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }

    }

    private static int _createDB(AnkiDb db) {
        db.execute("PRAGMA page_size = 4096");
        db.execute("PRAGMA legacy_file_format = 0");
        db.execute("VACUUM");
        _addSchema(db);
        _updateIndices(db);
        db.execute("ANALYZE");
        return Collection.SCHEMA_VERSION;
    }

    private static void _addSchema(AnkiDb db) {
        _addSchema(db, true);
    }

    private static void _addSchema(AnkiDb db, boolean setColConf) {
        db.execute("create table if not exists col ( " + "id              integer primary key, "
                + "crt             integer not null," + "mod             integer not null,"
                + "scm             integer not null," + "ver             integer not null,"
                + "dty             integer not null," + "usn             integer not null,"
                + "ls              integer not null," + "conf            text not null,"
                + "models          text not null," + "decks           text not null,"
                + "dconf           text not null," + "tags            text not null" + ");");
        db.execute("create table if not exists notes (" + "   id              integer primary key,   /* 0 */"
                + "  guid            text not null,   /* 1 */" + " mid             integer not null,   /* 2 */"
                + " mod             integer not null,   /* 3 */" + " usn             integer not null,   /* 4 */"
                + " tags            text not null,   /* 5 */" + " flds            text not null,   /* 6 */"
                + " sfld            integer not null,   /* 7 */" + " csum            integer not null,   /* 8 */"
                + " flags           integer not null,   /* 9 */" + " data            text not null   /* 10 */"
                + ");");
        db.execute("create table if not exists cards (" + "   id              integer primary key,   /* 0 */"
                + "  nid             integer not null,   /* 1 */" + "  did             integer not null,   /* 2 */"
                + "  ord             integer not null,   /* 3 */" + "  mod             integer not null,   /* 4 */"
                + " usn             integer not null,   /* 5 */" + " type            integer not null,   /* 6 */"
                + " queue           integer not null,   /* 7 */" + "    due             integer not null,   /* 8 */"
                + "   ivl             integer not null,   /* 9 */"
                + "  factor          integer not null,   /* 10 */" + " reps            integer not null,   /* 11 */"
                + "   lapses          integer not null,   /* 12 */"
                + "   left            integer not null,   /* 13 */"
                + "   odue            integer not null,   /* 14 */"
                + "   odid            integer not null,   /* 15 */"
                + "   flags           integer not null,   /* 16 */" + "   data            text not null   /* 17 */"
                + ");");
        db.execute("create table if not exists revlog (" + "   id              integer primary key,"
                + "   cid             integer not null," + "   usn             integer not null,"
                + "   ease            integer not null," + "   ivl             integer not null,"
                + "   lastIvl         integer not null," + "   factor          integer not null,"
                + "   time            integer not null," + "   type            integer not null" + ");");
        db.execute("create table if not exists graves (" + "    usn             integer not null,"
                + "    oid             integer not null," + "    type            integer not null" + ")");
        db.execute("INSERT OR IGNORE INTO col VALUES(1,0,0," + Utils.intNow(1000) + "," + Collection.SCHEMA_VERSION
                + ",0,0,0,'','{}','','','{}')");
        if (setColConf) {
            _setColVars(db);
        }
    }

    private static void _setColVars(AnkiDb db) {
        try {
            JSONObject g = new JSONObject(Decks.defaultDeck);
            g.put("id", 1);
            g.put("name", "Default");
            g.put("conf", 1);
            g.put("mod", Utils.intNow());
            JSONObject gc = new JSONObject(Decks.defaultConf);
            gc.put("id", 1);
            JSONObject ag = new JSONObject();
            ag.put("1", g);
            JSONObject agc = new JSONObject();
            agc.put("1", gc);
            ContentValues values = new ContentValues();
            values.put("conf", Collection.defaultConf);
            values.put("decks", Utils.jsonToString(ag));
            values.put("dconf", Utils.jsonToString(agc));
            db.update("col", values);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    private static void _updateIndices(AnkiDb db) {
        db.execute("create index if not exists ix_notes_usn on notes (usn);");
        db.execute("create index if not exists ix_cards_usn on cards (usn);");
        db.execute("create index if not exists ix_revlog_usn on revlog (usn);");
        db.execute("create index if not exists ix_cards_nid on cards (nid);");
        db.execute("create index if not exists ix_cards_sched on cards (did, queue, due);");
        db.execute("create index if not exists ix_revlog_cid on revlog (cid);");
        db.execute("create index if not exists ix_notes_csum on notes (csum);)");
    }

    public static void addIndices(AnkiDb db) {
        _updateIndices(db);
    }

    /*
     * Upgrading ************************************************************
     */

    // public void upgrade(String path) {
    // // mPath = path;
    // // _openDB(path);
    // // upgradeSchema();
    // // _openCol();
    // // _upgradeRest();
    // // return mCol;
    // }

    /*
     * Integrity checking ************************************************************
     */

    public static boolean check(String path) {
        AnkiDb db = AnkiDatabaseManager.getDatabase(path);
        // corrupt?
        try {
            if (!db.queryString("PRAGMA integrity_check").equalsIgnoreCase("ok")) {
                return false;
            }
        } catch (SQLException _) {
            return false;
        }
        // old version?
        if (db.queryScalar("SELECT version FROM decks") < 65) {
            return false;
        }
        // ensure we have indices for checks below
        db.execute("create index if not exists ix_cards_factId on cards (factId)");
        db.execute("create index if not exists ix_fields_factId on fieldModels (factId)");
        db.execute("analyze");
        // fields missing a field model?
        if (db.queryColumn(Integer.class,
                "select id from fields where fieldModelId not in (" + "select distinct id from fieldModels)", 0)
                .size() > 0) {
            return false;
        }
        // facts missing a field?
        if (db.queryColumn(Integer.class,
                "select distinct facts.id from facts, fieldModels where "
                        + "facts.modelId = fieldModels.modelId and fieldModels.id not in "
                        + "(select fieldModelId from fields where factId = facts.id)",
                0).size() > 0) {
            return false;
        }
        // cards missing a fact?
        if (db.queryColumn(Integer.class, "select id from cards where factId not in (select id from facts)", 0)
                .size() > 0) {
            return false;
        }
        // cards missing a card model?
        if (db.queryColumn(Integer.class,
                "select id from cards where cardModelId not in (select id from cardModels)", 0).size() > 0) {
            return false;
        }
        // cards with a card model from the wrong model?
        if (db.queryColumn(Integer.class,
                "select id from cards where cardModelId not in (select cm.id from "
                        + "cardModels cm, facts f where cm.modelId = f.modelId and " + "f.id = cards.factId)",
                0).size() > 0) {
            return false;
        }
        // facts missing a card?
        if (db.queryColumn(Integer.class,
                "select facts.id from facts " + "where facts.id not in (select distinct factId from cards)", 0)
                .size() > 0) {
            return false;
        }
        // dangling fields?
        if (db.queryColumn(Integer.class, "select id from fields where factId not in (select id from facts)", 0)
                .size() > 0) {
            return false;
        }
        // fields without matching interval
        if (db.queryColumn(Integer.class, "select id from fields where ordinal != (select ordinal from fieldModels "
                + "where id = fieldModelId)", 0).size() > 0) {
            return false;
        }
        // incorrect types
        if (db.queryColumn(Integer.class, "select id from cards where relativeDelay != (case "
                + "when successive then 1 when reps then 0 else 2 end)", 0).size() > 0) {
            return false;
        }
        if (db.queryColumn(Integer.class, "select id from cards where type != (case "
                + "when type >= 0 then relativeDelay else relativeDelay - 3 end)", 0).size() > 0) {
            return false;
        }
        return true;
    }

}