/*
* Copyright (c) 2015 Jonas Kalderstam.
*
* 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.nononsenseapps.notepad.data.local.orgmode;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import android.util.Pair;
import com.nononsenseapps.notepad.data.model.sql.RemoteTask;
import com.nononsenseapps.notepad.data.model.sql.RemoteTaskList;
import com.nononsenseapps.notepad.data.model.sql.Task;
import com.nononsenseapps.notepad.data.model.sql.TaskList;
import org.cowboyprogrammer.org.parser.OrgParser;
import org.cowboyprogrammer.org.parser.RegexParser;
import org.cowboyprogrammer.org.OrgFile;
import org.cowboyprogrammer.org.OrgNode;
import org.cowboyprogrammer.org.OrgTimestamp;
import java.io.BufferedReader;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
/**
* This class is suitable for synchronizers to inherit from. It contains the
* necessary logic to handle the database communication and conversions.
*/
public abstract class DBSyncBase implements SynchronizerInterface {
protected Context context;
private ContentResolver resolver;
public DBSyncBase(final Context context) {
this.context = context;
this.resolver = context.getContentResolver();
}
/**
* Reads the database and the OrgFile. Returns the matching Tasks and Nodes.
* <p/>
* TODO
* For gods' sake, test me!
*
* @param file The OrgFile containing all the tasks
* @param list The TaskList corresponding to the OrgFile.
* @return A list of all task-related objects necessary for synchronization.
*/
protected List<Pair<OrgNode, Pair<RemoteTask, Task>>> getNodesAndDBEntries(
OrgFile file, TaskList list) {
final List<Pair<OrgNode, Pair<RemoteTask, Task>>> result = new ArrayList<Pair<OrgNode, Pair<RemoteTask, Task>>>();
final HashMap<Long, Task> tasks = getTasks(list);
final HashMap<Long, RemoteTask> remotes = getValidRemoteTasks(list);
final List<RemoteTask> remotesDeleted = getInvalidRemoteTasks(list);
final HashMap<String, OrgNode> nodes = getNodes(file);
// Start with tasks
for (long dbid : tasks.keySet()) {
Task task = tasks.get(dbid);
RemoteTask remote = remotes.remove(dbid);
OrgNode node = null;
// Can be null
if (remote != null) {
node = nodes.remove(remote.remoteId.toUpperCase());
}
result.add(new Pair<OrgNode, Pair<RemoteTask, Task>>(node,
new Pair<RemoteTask, Task>(remote, task)));
}
// Follow with remaining remotes where task is null
for (RemoteTask remote : remotes.values()) {
Task task = null;
OrgNode node = nodes.remove(remote.remoteId.toUpperCase());
result.add(new Pair<OrgNode, Pair<RemoteTask, Task>>(node,
new Pair<RemoteTask, Task>(remote, task)));
}
for (RemoteTask remote : remotesDeleted) {
Task task = null;
OrgNode node = nodes.remove(remote.remoteId.toUpperCase());
result.add(new Pair<OrgNode, Pair<RemoteTask, Task>>(node,
new Pair<RemoteTask, Task>(remote, task)));
}
// Last, nodes with no database connections
for (OrgNode node : nodes.values()) {
Task task = null;
RemoteTask remote = null;
result.add(new Pair<OrgNode, Pair<RemoteTask, Task>>(node,
new Pair<RemoteTask, Task>(remote, task)));
}
return result;
}
private HashMap<String, OrgNode> getNodes(final OrgFile file) {
final HashMap<String, OrgNode> map = new HashMap<String, OrgNode>();
for (OrgNode node : file.getSubNodes()) {
addNodeToMap(node, map);
}
return map;
}
/**
* By convention, all generated ids are stored in uppercase.
*/
private void addNodeToMap(final OrgNode node,
final HashMap<String, OrgNode> map) {
String key = OrgConverter.getNodeId(node);
Log.d(Synchronizer.TAG, "Key: " + key + ", node: " + node.getComments());
if (key == null) {
// This key won't necessarily be used later.
key = OrgConverter.generateId();
}
map.put(key.toUpperCase(), node);
for (OrgNode subnode : node.getSubNodes()) {
addNodeToMap(subnode, map);
}
}
private HashMap<Long, RemoteTask> getValidRemoteTasks(final TaskList list) {
final HashMap<Long, RemoteTask> map = new HashMap<Long, RemoteTask>();
final Cursor c = resolver.query(
RemoteTask.URI,
RemoteTask.Columns.FIELDS,
RemoteTask.Columns.SERVICE + " IS ? AND "
+ RemoteTask.Columns.ACCOUNT + " IS ? AND "
+ RemoteTask.Columns.LISTDBID + " IS ? AND "
+ RemoteTask.Columns.DBID + " > 0",
new String[]{getServiceName(), getAccountName(),
Long.toString(list._id)}, null);
try {
while (c.moveToNext()) {
RemoteTask remote = new RemoteTask(c);
map.put(remote.dbid, remote);
}
} finally {
if (c != null)
c.close();
}
return map;
}
/**
* These remote tasks are no longer connected to a task. This typically happens when a task is
* deleted or moved to another list.
*
* @param list
* @return
*/
private List<RemoteTask> getInvalidRemoteTasks(final TaskList list) {
final ArrayList<RemoteTask> remoteList = new ArrayList<RemoteTask>();
final Cursor c = resolver.query(
RemoteTask.URI,
RemoteTask.Columns.FIELDS,
RemoteTask.Columns.SERVICE + " IS ? AND "
+ RemoteTask.Columns.ACCOUNT + " IS ? AND "
+ RemoteTask.Columns.LISTDBID + " IS ? AND "
+ RemoteTask.Columns.DBID + " < 1",
new String[]{getServiceName(), getAccountName(),
Long.toString(list._id)}, null);
try {
while (c.moveToNext()) {
RemoteTask remote = new RemoteTask(c);
remoteList.add(remote);
}
} finally {
if (c != null)
c.close();
}
return remoteList;
}
private HashMap<Long, Task> getTasks(final TaskList list) {
final HashMap<Long, Task> map = new HashMap<Long, Task>();
final Cursor c = resolver.query(Task.URI, Task.Columns.FIELDS,
Task.Columns.DBLIST + " IS ?",
new String[]{Long.toString(list._id)}, null);
try {
while (c.moveToNext()) {
Task task = new Task(c);
map.put(task._id, task);
}
} finally {
if (c != null)
c.close();
}
return map;
}
/**
* Reads the database and the remote source.
*
* @return The matching TaskList and OrgFiles.
* @throws ParseException
* @throws IOException
*/
protected List<Pair<OrgFile, Pair<RemoteTaskList, TaskList>>> getFilesAndDBEntries()
throws IOException, ParseException {
final List<Pair<OrgFile, Pair<RemoteTaskList, TaskList>>> result = new ArrayList<Pair<OrgFile, Pair<RemoteTaskList, TaskList>>>();
// get all lists
final HashMap<Long, TaskList> lists = getLists();
// get all db entries
final HashMap<Long, RemoteTaskList> remotes = getRemoteTaskLists();
// get all files
final HashSet<String> filenames = getRemoteFilenames();
for (String filename : filenames) {
Log.d(Synchronizer.TAG, "Get Filename: " + filename);
}
OrgParser parser = new RegexParser();
// Construct pairs from lists first. This removes entries as it goes.
for (Long dbid : lists.keySet()) {
TaskList list = lists.get(dbid);
RemoteTaskList remote = remotes.remove(dbid);
OrgFile file = null;
// Can be null
if (remote != null && filenames.remove(remote.remoteId)) {
final BufferedReader br = getRemoteFile(remote.remoteId);
if (br != null) {
file = OrgFile.createFromBufferedReader(parser, remote.remoteId, br);
}
}
String l = list.title;
String r = null;
if (remote != null)
r = remote.remoteId;
String f = null;
if (file != null)
f = file.getFilename();
Log.d(Synchronizer.TAG, "Pair:" + l + ", " + r + ", " + f);
result.add(new Pair<OrgFile, Pair<RemoteTaskList, TaskList>>(file,
new Pair<RemoteTaskList, TaskList>(remote, list)));
}
// Add remotes that no longer have a list
for (RemoteTaskList remote : remotes.values()) {
TaskList list = null;
OrgFile file = null;
// Can be null
if (remote != null && filenames.remove(remote.remoteId)) {
final BufferedReader br = getRemoteFile(remote.remoteId);
if (br != null) {
file = OrgFile.createFromBufferedReader(parser, remote.remoteId, br);
}
}
String l = null;
String r = null;
if (remote != null)
r = remote.remoteId;
String f = null;
if (file != null)
f = file.getFilename();
Log.d(Synchronizer.TAG, "Pair:" + l + ", " + r + ", " + f);
result.add(new Pair<OrgFile, Pair<RemoteTaskList, TaskList>>(file,
new Pair<RemoteTaskList, TaskList>(remote, list)));
}
// Add files that do not exist in database
for (String filename : filenames) {
TaskList list = null;
RemoteTaskList remote = null;
OrgFile file = null;
final BufferedReader br = getRemoteFile(filename);
if (br != null) {
file = OrgFile.createFromBufferedReader(parser, filename, br);
}
String l = null;
String r = null;
String f = null;
// An obvious precaution. If everything is null,
// there's nothing to add.
if (file != null) {
f = file.getFilename();
Log.d(Synchronizer.TAG, "Pair:" + l + ", " + r + ", " + f);
result.add(new Pair<OrgFile, Pair<RemoteTaskList, TaskList>>(file,
new Pair<RemoteTaskList, TaskList>(remote, list)));
}
}
return result;
}
/**
* @return a map from list-dbid to RemoteTaskList
*/
private HashMap<Long, RemoteTaskList> getRemoteTaskLists() {
final HashMap<Long, RemoteTaskList> map = new HashMap<Long, RemoteTaskList>();
final Cursor c = resolver.query(RemoteTaskList.URI,
RemoteTaskList.Columns.FIELDS, RemoteTaskList.Columns.SERVICE
+ " IS ? AND " + RemoteTask.Columns.ACCOUNT + " IS ?",
new String[]{getServiceName(), getAccountName()}, null);
try {
while (c.moveToNext()) {
RemoteTaskList remote = new RemoteTaskList(c);
Log.d(Synchronizer.TAG, "Get remote: " + remote.remoteId);
map.put(remote.dbid, remote);
}
} finally {
if (c != null)
c.close();
}
return map;
}
/**
* @return a map from list-dbid to TaskList
*/
private HashMap<Long, TaskList> getLists() {
final HashMap<Long, TaskList> map = new HashMap<Long, TaskList>();
final Cursor c = resolver.query(TaskList.URI, TaskList.Columns.FIELDS,
null, null, null);
try {
while (c.moveToNext()) {
TaskList list = new TaskList(c);
Log.d(Synchronizer.TAG, "Get list: " + list.title);
map.put(list._id, list);
}
} finally {
if (c != null)
c.close();
}
return map;
}
/**
* Make sure notifications are synchronized from node to database.
*/
protected void replaceNotifications(final Task task, final OrgNode node) {
// TODO Auto-generated method stub
// Remove existing notifications
// Add new notifications
for (OrgTimestamp ts : node.getTimestamps()) {
if (!ts.isInactive()) {
}
}
}
protected boolean wasRenamed(final TaskList list,
final RemoteTaskList dbEntry, final OrgFile file) {
return !(OrgConverter.getTitleAsFilename(list)).equals(file.getFilename
());
}
/**
* (re)Names a file to match the DB version's current name.
*
* @param list Current version in the database
* @param dbEntry Current remote version in the database which will also be
* renamed.
* @param file File to rename.
*/
protected void renameFile(final TaskList list,
final RemoteTaskList dbEntry, final OrgFile file) {
if (list.title != null && list.title.length() > 0) {
file.setFilename(OrgConverter.getTitleAsFilename(list));
}
dbEntry.remoteId = file.getFilename();
dbEntry.save(context);
}
/**
* Delete remote versions of tasks to current service.
*
* @param listdbid List they belong to.
* @return Number of deletions made.
*/
private int deleteRemoteTasksIn(final long listdbid) {
return context.getContentResolver().delete(
RemoteTask.URI,
RemoteTask.Columns.SERVICE + " IS ? AND " + RemoteTask.Columns
.ACCOUNT
+ " IS ? AND " + RemoteTask.Columns.LISTDBID + " IS ?",
new String[]{getServiceName(), getAccountName(),
Long.toString(listdbid)});
}
/**
* Deletes a list and all tasks and related entries (to current service).
* Call this when remote file has been deleted.
*
* @param list List to delete. Can be null.
* @param dbEntry RemoteEntry in DB to delete. Can be null.
*/
protected void deleteLocal(final TaskList list, final RemoteTaskList dbEntry) {
long listdbid = -1;
if (list != null) {
list.delete(context);
listdbid = list._id;
}
if (dbEntry != null) {
dbEntry.delete(context);
listdbid = dbEntry.dbid;
}
// Tasks are deleted automatically, but not the
// remote-versions
deleteRemoteTasksIn(listdbid);
}
/**
* Deletes a task and dbEntry from database.
*
* @param task Task to delete, can be null.
* @param dbEntry dbEntry to delete, can be null.
*/
protected void deleteLocal(final Task task, final RemoteTask dbEntry) {
if (task != null) {
task.delete(context);
}
if (dbEntry != null) {
dbEntry.delete(context);
}
}
}