/** * Copyright (c) 2012 Todoroo Inc * * See the file "LICENSE" for the full license governing this code. */ package com.todoroo.astrid.utility; import java.util.ArrayList; import android.content.Context; import com.todoroo.andlib.data.Property; import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.sql.Criterion; import com.todoroo.andlib.sql.Order; import com.todoroo.andlib.sql.Query; import com.todoroo.astrid.dao.MetadataDao; import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria; import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.dao.TaskDao.TaskCriteria; import com.todoroo.astrid.data.Metadata; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.sync.SyncContainer; import com.todoroo.astrid.sync.SyncProviderUtilities; abstract public class SyncMetadataService<TYPE extends SyncContainer> { /** metadata key of tag add-on */ public static final String TAG_KEY = "tags-tag"; //$NON-NLS-1$ // --- instance variables @Autowired protected TaskDao taskDao; @Autowired protected MetadataDao metadataDao; // --- abstract methods /** @return metadata key identifying this sync provider's metadata */ abstract public String getMetadataKey(); /** @return sync provider utilities */ abstract public SyncProviderUtilities getUtilities(); /** create a task container based on the given data */ abstract public TYPE createContainerFromLocalTask(Task task, ArrayList<Metadata> metadata); /** @return criterion for matching all metadata keys that your provider synchronizes */ abstract public Criterion getMetadataCriteria(); /** @return criterion for finding local matches of sync container in task database */ abstract public Criterion getLocalMatchCriteria(TYPE remoteTask); /** @return criterion for matching metadata that indicate remote task exists */ abstract public Criterion getMetadataWithRemoteId(); // --- implementation public SyncMetadataService(Context context) { DependencyInjectionService.getInstance().inject(this); } /** * Clears metadata information. Used when user logs out of sync provider */ public void clearMetadata() { metadataDao.deleteWhere(Metadata.KEY.eq(getMetadataKey())); } /** * Gets cursor across all task metadata for joining * * @return cursor */ private TodorooCursor<Metadata> getRemoteTaskMetadata() { return metadataDao.query(Query.select(Metadata.TASK).where( Criterion.and(MetadataCriteria.withKey(getMetadataKey()), getMetadataWithRemoteId())).orderBy(Order.asc(Metadata.TASK))); } /** * Gets tasks that were created since last sync * @param properties * @return */ public TodorooCursor<Task> getLocallyCreated(Property<?>... properties) { TodorooCursor<Task> tasks = taskDao.query(Query.select(Task.ID).where( Criterion.and(TaskCriteria.isActive(), TaskCriteria.ownedByMe())).orderBy(Order.asc(Task.ID))); return joinWithMetadata(tasks, false, properties); } /** * Gets tasks that were modified since last sync * @param properties * @return null if never sync'd */ public TodorooCursor<Task> getLocallyUpdated(Property<?>... properties) { TodorooCursor<Task> tasks; long lastSyncDate = getUtilities().getLastSyncDate(); if(lastSyncDate == 0) tasks = taskDao.query(Query.select(Task.ID).where(Criterion.none)); else tasks = taskDao.query(Query.select(Task.ID).where(Criterion.and(TaskCriteria.ownedByMe(), Task.MODIFICATION_DATE.gt(lastSyncDate))) .orderBy(Order.asc(Task.ID))); tasks = filterLocallyUpdated(tasks, lastSyncDate); return joinWithMetadata(tasks, true, properties); } /** * @param tasks * @param lastSyncDate */ protected TodorooCursor<Task> filterLocallyUpdated(TodorooCursor<Task> tasks, long lastSyncDate) { // override hook return tasks; } private TodorooCursor<Task> joinWithMetadata(TodorooCursor<Task> tasks, boolean both, Property<?>... properties) { try { TodorooCursor<Metadata> metadata = getRemoteTaskMetadata(); try { ArrayList<Long> matchingRows = new ArrayList<Long>(); joinRows(tasks, metadata, matchingRows, both); return taskDao.query(Query.select(properties).where(Task.ID.in( matchingRows.toArray(new Long[matchingRows.size()])))); } finally { metadata.close(); } } finally { tasks.close(); } } /** * Join rows from two cursors on the first column, assuming its an id column * @param left * @param right * @param matchingRows * @param both - if false, returns rows no right row exists, if true, * returns rows where both exist */ private static void joinRows(TodorooCursor<?> left, TodorooCursor<?> right, ArrayList<Long> matchingRows, boolean both) { left.moveToPosition(-1); right.moveToFirst(); while(true) { left.moveToNext(); if(left.isAfterLast()) break; long leftValue = left.getLong(0); // advance right until it is equal or bigger while(!right.isAfterLast() && right.getLong(0) < leftValue) { right.moveToNext(); } if(right.isAfterLast()) { if(!both) matchingRows.add(leftValue); continue; } if((right.getLong(0) == leftValue) == both) matchingRows.add(leftValue); } } /** * Searches for a local task with same remote id, updates this task's id * @param remoteTask */ public void findLocalMatch(TYPE remoteTask) { if(remoteTask.task.getId() != Task.NO_ID) return; TodorooCursor<Metadata> cursor = metadataDao.query(Query.select(Metadata.TASK). where(Criterion.and(MetadataCriteria.withKey(getMetadataKey()), getLocalMatchCriteria(remoteTask)))); try { if(cursor.getCount() == 0) return; cursor.moveToFirst(); remoteTask.task.setId(cursor.get(Metadata.TASK)); } finally { cursor.close(); } } /** * Saves a task and its metadata * @param task */ public void saveTaskAndMetadata(TYPE task) { task.prepareForSaving(); taskDao.save(task.task); metadataDao.synchronizeMetadata(task.task.getId(), task.metadata, getMetadataCriteria()); } /** * Reads a task and its metadata * @param task * @return */ public TYPE readTaskAndMetadata(TodorooCursor<Task> taskCursor) { Task task = new Task(taskCursor); ArrayList<Metadata> metadata = new ArrayList<Metadata>(); TodorooCursor<Metadata> metadataCursor = metadataDao.query(Query.select(Metadata.PROPERTIES). where(Criterion.and(MetadataCriteria.byTask(task.getId()), getMetadataCriteria()))); try { for(metadataCursor.moveToFirst(); !metadataCursor.isAfterLast(); metadataCursor.moveToNext()) { metadata.add(new Metadata(metadataCursor)); } } finally { metadataCursor.close(); } return createContainerFromLocalTask(task, metadata); } /** * Reads metadata out of a task * @return null if no metadata found */ public Metadata getTaskMetadata(long taskId) { TodorooCursor<Metadata> cursor = metadataDao.query(Query.select(Metadata.PROPERTIES).where( MetadataCriteria.byTaskAndwithKey(taskId, getMetadataKey()))); try { if(cursor.getCount() == 0) return null; cursor.moveToFirst(); return new Metadata(cursor); } finally { cursor.close(); } } }