All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.couchbase.lite.DatabaseUpgrade Maven / Gradle / Ivy

//
//  DatabaseUpgrade.java
//
//  Created by Hideki Itakura on 7/21/15.
//  Copyright (c) 2015 Couchbase, Inc All rights reserved.
//
package com.couchbase.lite;

import com.couchbase.lite.internal.AttachmentInternal;
import com.couchbase.lite.internal.RevisionInternal;
import com.couchbase.lite.storage.Cursor;
import com.couchbase.lite.storage.SQLiteStorageEngine;
import com.couchbase.lite.storage.SQLiteStorageEngineFactory;
import com.couchbase.lite.support.FileDirUtils;
import com.couchbase.lite.util.Log;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Imports from the v1.0 SQLite database format into a CBLDatabase.
 * This class is optional: the source file does not need to be built into the app or the
 * Couchbase Lite library. If it's not present, Couchbase Lite will ignore old v1.0 databases
 * instead of importing them.
 */
final public class DatabaseUpgrade {
    private static String TAG = Log.TAG_DATABASE;

    private Manager manager;
    private Database db;
    private String path;
    private int numDocs = 0;
    private int numRevs = 0;
    private boolean canRemoveOldAttachmentsDir = true;
    private SQLiteStorageEngine storageEngine = null;

    protected DatabaseUpgrade(Manager manager, Database db, String sqliteFile) {
        this.manager = manager;
        this.db = db;
        this.path = sqliteFile;
        this.numDocs = 0;
        this.numRevs = 0;
        this.canRemoveOldAttachmentsDir = true;
        this.storageEngine = null;
    }

    protected int getNumDocs() {
        return numDocs;
    }

    protected int getNumRevs() {
        return numRevs;
    }

    protected boolean canRemoveOldAttachmentsDir() {
        return canRemoveOldAttachmentsDir;
    }

    protected boolean importData() {
        try {
            // Create the storage engine for source database
            SQLiteStorageEngineFactory factory = manager.getContext().getSQLiteStorageEngineFactory();
            try {
                storageEngine = factory.createStorageEngine();
            } catch (CouchbaseLiteException e) {
                Log.e(TAG, "Upgrade failed: Unable to create a storage engine", e);
                return false;
            }

            if (storageEngine == null) {
                Log.e(TAG, "Upgrade failed: Unable to create a storage engine");
                return false;
            }

            // TODO: We also need encryption key for database upgrade.
            if (!storageEngine.open(path, null)) {
                Log.e(TAG, "Upgrade failed: Couldn't open new db: %s", path);
                return false;
            }

            // Open destination database:
            try {
                db.open();
            } catch (CouchbaseLiteException e) {
                Log.e(TAG, "Upgrade failed:  Couldn't open new db: %s", e, db.toString());
                return false;
            }

            // Move attachment storage directory:
            if (!moveAttachmentsDir())
                return false;

            // Upgrade documents:
            // CREATE TABLE docs (doc_id INTEGER PRIMARY KEY, docid TEXT UNIQUE NOT NULL);
            String sql = "SELECT doc_id, docid FROM docs";
            String[] args = {};
            final Cursor cursor = storageEngine.rawQuery(sql, args);
            try {
                boolean ret = db.runInTransaction(new TransactionalTask() {
                    @Override
                    public boolean run() {
                        cursor.moveToNext();
                        while (!cursor.isAfterLast()) {
                            long docNumericID = cursor.getLong(0);
                            String docID = cursor.getString(1);
                            Status status = importDoc(docID, docNumericID);
                            if (status.isError())
                                return false;
                            cursor.moveToNext();
                        }
                        return true;
                    }
                });
                if (!ret)
                    return false;
            } finally {
                if (cursor != null)
                    cursor.close();
            }

            // Upgrade local docs:
            importLocalDocs();

            // Upgrade info (public/private UUIDs):
            importInfo();

            return true;
        } finally {
            if (storageEngine != null && storageEngine.isOpen())
                storageEngine.close();
        }
    }

    protected void backOut() {
        // Move attachments dir back to old path:
        File newAttachmentsPath = new File(db.getAttachmentStorePath());
        if (newAttachmentsPath.canRead()) {
            File oldAttachmentsPath = new File(FileDirUtils.getPathWithoutExt(path) + " attachments");
            if (canRemoveOldAttachmentsDir)
                newAttachmentsPath.renameTo(oldAttachmentsPath);
        }
        try {
            db.delete();
        } catch (CouchbaseLiteException e) {
            Log.w(TAG, "Failed to delete Database: %s: %s", db, e);
        }
    }

    protected void deleteSQLiteFiles() {
        for (String suffix : Arrays.asList("", "-wal", "-shm", "-journal")) {
            File file = new File(path + suffix);
            if (file.exists())
                file.delete();
        }
    }

    private boolean moveAttachmentsDir() {
        File oldAttachmentsPath = new File(FileDirUtils.getPathWithoutExt(path) + " attachments");
        if (!oldAttachmentsPath.canRead())
            return true;
        File newAttachmentsPath = new File(db.getAttachmentStorePath());
        Log.v(TAG, "Upgrade: Moving '%s' to '%s'",
                oldAttachmentsPath, newAttachmentsPath);
        FileDirUtils.deleteRecursive(newAttachmentsPath);

        try {
            if (canRemoveOldAttachmentsDir) {
                FileDirUtils.copyFolder(oldAttachmentsPath, newAttachmentsPath);
                FileDirUtils.deleteRecursive(oldAttachmentsPath);
            } else {
                FileDirUtils.copyFolder(oldAttachmentsPath, newAttachmentsPath);
            }
        } catch (IOException e) {
            Log.w(TAG, "Upgrade failed: Couldn't move attachments: %s", e);
            return false;
        }

        return true;
    }

    private Status importDoc(String docID, long docNumericID) {
        // Check if the attachments table exists or not:

        boolean attachmentsTableExists = this.attachmentsTableExists();

        // CREATE TABLE revs (
        //  sequence INTEGER PRIMARY KEY AUTOINCREMENT,
        //  doc_id INTEGER NOT NULL REFERENCES docs(doc_id) ON DELETE CASCADE,
        //  revid TEXT NOT NULL COLLATE REVID,
        //  parent INTEGER REFERENCES revs(sequence) ON DELETE SET NULL,
        //  current BOOLEAN,
        //  deleted BOOLEAN DEFAULT 0,
        //  json BLOB,
        //  no_attachments BOOLEAN,
        //  UNIQUE (doc_id, revid) );
        class Pair {
            Pair(String revID, Long parentSeq) {
                this.revID = revID;
                this.parentSeq = parentSeq;
            }

            String revID;
            Long parentSeq;
        }
        Map tree = new HashMap();
        String sql = "SELECT sequence, revid, parent, current, deleted, json, no_attachments" +
                " FROM revs WHERE doc_id=? ORDER BY sequence";
        String[] args = {String.valueOf(docNumericID)};
        Cursor cursor = storageEngine.rawQuery(sql, args);
        try {
            cursor.moveToNext();
            while (!cursor.isAfterLast()) {
                long sequence = cursor.getLong(0);
                String revID = cursor.getString(1);
                long parentSeq = cursor.getLong(2);
                boolean current = cursor.getInt(3) != 0;
                boolean noAtts = cursor.getInt(6) != 0;
                if (current) {
                    // Add a leaf revision:
                    boolean deleted = cursor.getInt(4) != 0;
                    byte[] json = cursor.getBlob(5);
                    if (json == null)
                        json = "{}".getBytes();
                    if (attachmentsTableExists) {
                        json = this.addAttachmentsToSequence(sequence, json);
                        if (json == null)
                            return new Status(Status.BAD_JSON);
                    } else {//.NET v1.1 database already has attachments bundled in the JSON data:
                        if (!noAtts) {
                            json = DatabaseUpgrade.updateAttachmentFollowsInJson(json);
                            if (json == null)
                                return new Status(Status.BAD_JSON);
                        }
                    }

                    RevisionInternal rev = new RevisionInternal(docID, revID, deleted);
                    rev.setJSON(json);

                    List history = new ArrayList();
                    history.add(revID);
                    while (parentSeq > 0) {
                        Pair ancestor = tree.get(parentSeq);
                        history.add(ancestor.revID);
                        parentSeq = ancestor.parentSeq;
                    }
                    try {
                        db.forceInsert(rev, history, null);
                    } catch (CouchbaseLiteException ex) {
                        return ex.getCBLStatus();
                    }
                    ++numRevs;
                } else {
                    tree.put(sequence, new Pair(revID, parentSeq));
                }
                cursor.moveToNext();
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }
        ++numDocs;
        return new Status(Status.OK);
    }

    private static byte[] updateAttachmentFollowsInJson(byte[] json) {
        Map object = null;
        try {
            object = Manager.getObjectMapper().readValue(json, Map.class);
        } catch (IOException e) {
            return null;
        }
        Map attachments = (Map) object.get("_attachments");
        for (String key : attachments.keySet()) {
            Map attachment = (Map) attachments.get(key);
            attachment.put("follows", true);
            attachment.remove("stub");
        }
        try {
            return Manager.getObjectMapper().writeValueAsBytes(object);
        } catch (IOException e) {
            return null;
        }
    }

    private byte[] addAttachmentsToSequence(long sequence, byte[] json) {
        // CREATE TABLE attachments (
        //  sequence INTEGER NOT NULL REFERENCES revs(sequence) ON DELETE CASCADE,
        //  filename TEXT NOT NULL,
        //  key BLOB NOT NULL,
        //  type TEXT,
        //  length INTEGER NOT NULL,
        //  revpos INTEGER DEFAULT 0,
        //  encoding INTEGER DEFAULT 0,
        //  encoded_length INTEGER );

        Map attachments = new HashMap();

        String sql = "SELECT filename, key, type, length," +
                " revpos, encoding, encoded_length FROM attachments WHERE sequence=?";
        String[] args = {String.valueOf(sequence)};
        Cursor cursor = storageEngine.rawQuery(sql, args);
        try {
            cursor.moveToNext();
            while (!cursor.isAfterLast()) {
                String name = cursor.getString(0);
                byte[] key = cursor.getBlob(1);
                String mimeType = cursor.getString(2);
                long length = cursor.getLong(3);
                int revpos = cursor.getInt(4);
                int encoding = cursor.getInt(5);
                long encodingLength = cursor.getLong(6);

                BlobKey blobKey = new BlobKey(key);
                String digest = blobKey.base64Digest();

                Map att = new HashMap();
                att.put("type", mimeType);
                att.put("digest", digest);
                att.put("length", length);
                att.put("revpos", revpos);
                att.put("follows", true);
                att.put("encoding", AttachmentInternal.AttachmentEncoding.values()[encoding] ==
                        AttachmentInternal.AttachmentEncoding.AttachmentEncodingGZIP ?
                        "gzip" : null);
                att.put("encoded_length", AttachmentInternal.AttachmentEncoding.values()[encoding] ==
                        AttachmentInternal.AttachmentEncoding.AttachmentEncodingGZIP ?
                        encodingLength : null);

                attachments.put(name, att);
                cursor.moveToNext();
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }


        if (attachments.size() > 0) {
            // Splice attachment JSON into the document JSON:
            Map attachmentsWrapper = new HashMap();
            attachmentsWrapper.put("_attachments", attachments);
            byte[] extraJSON;
            try {
                extraJSON = Manager.getObjectMapper().writeValueAsBytes(attachmentsWrapper);
            } catch (IOException e) {
                return null;
            }

            int jsonLength = json.length;
            int extraLength = extraJSON.length;
            if (jsonLength == 2) { // Original JSON was empty
                return extraJSON;
            }
            byte[] newJson = new byte[jsonLength + extraLength - 1];
            System.arraycopy(json, 0, newJson, 0, jsonLength - 1);  // Copy json w/o trailing '}'
            newJson[jsonLength - 1] = ',';  // Add a ','
            System.arraycopy(extraJSON, 1, newJson, jsonLength, extraLength - 1);
            return newJson;
        } else {
            return json;
        }
    }

    private boolean attachmentsTableExists() {
        String sql = "SELECT 1 FROM sqlite_master WHERE type='table' AND name='attachments'";
        Cursor cursor = storageEngine.rawQuery(sql, null);
        try {
            if (cursor.moveToNext())
                return true;
            else
                return false;
        } finally {
            if (cursor != null)
                cursor.close();
        }
    }

    private void importLocalDocs() {
        // CREATE TABLE localdocs (
        //  docid TEXT UNIQUE NOT NULL,
        //  revid TEXT NOT NULL COLLATE REVID,
        //  json BLOB );

        String sql = "SELECT docid, json FROM localdocs";
        Cursor cursor = storageEngine.rawQuery(sql, null);
        try {
            cursor.moveToNext();
            while (!cursor.isAfterLast()) {
                String docID = cursor.getString(0);
                // Remove "_local/" prefix:
                if(docID.startsWith("_local/"))
                    docID = docID.substring(7);
                byte[] json = cursor.getBlob(1);
                Log.v(TAG, "Upgrading local doc '%s'", docID);
                try {
                    Map props = Manager.getObjectMapper().readValue(json, Map.class);
                    if (!db.putLocalDocument(docID, props)) {
                        Log.w(TAG, "Couldn't import local doc '%s'", docID);
                    }
                } catch (Exception e) {
                    Log.w(TAG, "Couldn't import local doc '%s': '%s'", docID, e);
                }
                cursor.moveToNext();
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }
    }

    private void importInfo() {
        // CREATE TABLE info (key TEXT PRIMARY KEY, value TEXT);
        String sql = "SELECT key, value FROM info";
        Cursor cursor = storageEngine.rawQuery(sql, null);
        try {
            cursor.moveToNext();
            while (!cursor.isAfterLast()) {
                db.getStore().setInfo(cursor.getString(0), cursor.getString(1));
                cursor.moveToNext();
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy