package com.todoroo.astrid.actfm.sync.messages; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import org.apache.http.entity.mime.MultipartEntity; import org.apache.http.entity.mime.content.FileBody; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.text.TextUtils; import android.util.Log; import com.crittercism.app.Crittercism; import com.todoroo.andlib.data.Property; import com.todoroo.andlib.data.Property.PropertyVisitor; import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.sql.Order; import com.todoroo.andlib.sql.Query; import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.astrid.actfm.sync.ActFmPreferenceService; import com.todoroo.astrid.actfm.sync.ActFmSyncThread.ModelType; import com.todoroo.astrid.core.PluginServices; import com.todoroo.astrid.dao.DaoReflectionHelpers; import com.todoroo.astrid.dao.OutstandingEntryDao; import com.todoroo.astrid.dao.RemoteModelDao; import com.todoroo.astrid.data.OutstandingEntry; import com.todoroo.astrid.data.RemoteModel; import com.todoroo.astrid.data.TagData; import com.todoroo.astrid.data.TagOutstanding; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.TaskAttachment; import com.todoroo.astrid.data.TaskAttachmentOutstanding; import com.todoroo.astrid.data.TaskListMetadata; import com.todoroo.astrid.data.TaskOutstanding; import com.todoroo.astrid.data.UserActivity; import com.todoroo.astrid.data.UserActivityOutstanding; import com.todoroo.astrid.data.WaitingOnMe; import com.todoroo.astrid.data.WaitingOnMeOutstanding; @SuppressWarnings("nls") public class ChangesHappened<TYPE extends RemoteModel, OE extends OutstandingEntry<TYPE>> extends ClientToServerMessage<TYPE> { private static final String ERROR_TAG = "actfm-changes-happened"; protected final Class<OE> outstandingClass; protected final List<OE> changes; protected final OutstandingEntryDao<OE> outstandingDao; public static final String CHANGES_KEY = "changes"; public static ChangesHappened<?, ?> instantiateChangesHappened(long id, ModelType modelType) { switch(modelType) { case TYPE_TASK: return new ChangesHappened<Task, TaskOutstanding>(id, Task.class, PluginServices.getTaskDao(), PluginServices.getTaskOutstandingDao()); case TYPE_TAG: return new ChangesHappened<TagData, TagOutstanding>(id, TagData.class, PluginServices.getTagDataDao(), PluginServices.getTagOutstandingDao()); case TYPE_ACTIVITY: return new ChangesHappened<UserActivity, UserActivityOutstanding>(id, UserActivity.class, PluginServices.getUserActivityDao(), PluginServices.getUserActivityOutstandingDao()); case TYPE_ATTACHMENT: return new ChangesHappened<TaskAttachment, TaskAttachmentOutstanding>(id, TaskAttachment.class, PluginServices.getTaskAttachmentDao(), PluginServices.getTaskAttachmentOutstandingDao()); case TYPE_TASK_LIST_METADATA: return new TaskListMetadataChangesHappened(id, TaskListMetadata.class, PluginServices.getTaskListMetadataDao(), PluginServices.getTaskListMetadataOutstandingDao()); case TYPE_WAITING_ON_ME: return new ChangesHappened<WaitingOnMe, WaitingOnMeOutstanding>(id, WaitingOnMe.class, PluginServices.getWaitingOnMeDao(), PluginServices.getWaitingOnMeOutstandingDao()); default: return null; } } public ChangesHappened(long id, Class<TYPE> modelClass, RemoteModelDao<TYPE> modelDao, OutstandingEntryDao<OE> outstandingDao) { super(id, modelClass, modelDao); this.outstandingClass = DaoReflectionHelpers.getOutstandingClass(modelClass); this.outstandingDao = outstandingDao; this.changes = new ArrayList<OE>(); if (!foundEntity) // Stop sending changes for entities that don't exist anymore outstandingDao.deleteWhere(OutstandingEntry.ENTITY_ID_PROPERTY.eq(id)); } @Override protected boolean serializeExtrasToJSON(JSONObject serializeTo, MultipartEntity entity) throws JSONException { // Process changes list and serialize to JSON JSONArray changesJson = changesToJSON(entity); if (changesJson == null || changesJson.length() == 0) return false; serializeTo.put(CHANGES_KEY, changesJson); return true; } @Override protected String getTypeString() { return "ChangesHappened"; } public List<OE> getChanges() { return changes; } private JSONArray changesToJSON(MultipartEntity entity) { if (!RemoteModel.NO_UUID.equals(uuid)) populateChanges(); JSONArray array = new JSONArray(); AtomicInteger uploadCounter = new AtomicInteger(); PropertyToJSONVisitor visitor = new PropertyToJSONVisitor(); for (OE change : changes) { try { String localColumn = change.getValue(OutstandingEntry.COLUMN_STRING_PROPERTY); JSONObject changeJson = new JSONObject(); changeJson.put("id", change.getId()); String serverColumn; if (NameMaps.TAG_ADDED_COLUMN.equals(localColumn)) { serverColumn = NameMaps.TAG_ADDED_COLUMN; changeJson.put("value", change.getValue(OutstandingEntry.VALUE_STRING_PROPERTY)); } else if (NameMaps.TAG_REMOVED_COLUMN.equals(localColumn)) { serverColumn = NameMaps.TAG_REMOVED_COLUMN; changeJson.put("value", change.getValue(OutstandingEntry.VALUE_STRING_PROPERTY)); } else if (NameMaps.MEMBER_ADDED_COLUMN.equals(localColumn)) { serverColumn = NameMaps.MEMBER_ADDED_COLUMN; changeJson.put("value", change.getValue(OutstandingEntry.VALUE_STRING_PROPERTY)); } else if (NameMaps.MEMBER_REMOVED_COLUMN.equals(localColumn)) { serverColumn = NameMaps.MEMBER_REMOVED_COLUMN; changeJson.put("value", change.getValue(OutstandingEntry.VALUE_STRING_PROPERTY)); } else if (NameMaps.ATTACHMENT_ADDED_COLUMN.equals(localColumn)) { serverColumn = NameMaps.ATTACHMENT_ADDED_COLUMN; JSONObject fileJson = getFileJson(change.getValue(OutstandingEntry.VALUE_STRING_PROPERTY)); String name = fileJson == null ? null : addToEntityFromFileJson(entity, fileJson, uploadCounter); if (name == null) { PluginServices.getTaskAttachmentDao().delete(id); PluginServices.getTaskAttachmentOutstandingDao().deleteWhere(TaskAttachmentOutstanding.ENTITY_ID_PROPERTY.eq(id)); return null; } changeJson.put("value", name); } else { Property<?> localProperty = NameMaps.localColumnNameToProperty(table, localColumn); if (localProperty == null) throw new RuntimeException("No local property found for local column " + localColumn + " in table " + table); serverColumn = NameMaps.localColumnNameToServerColumnName(table, localColumn); if (serverColumn == null) throw new RuntimeException("No server column found for local column " + localColumn + " in table " + table); Object value = localProperty.accept(visitor, change); if (!validateValue(localProperty, value)) return null; if (value == null) changeJson.put("value", JSONObject.NULL); else { if (localProperty.checkFlag(Property.PROP_FLAG_PICTURE) && value instanceof JSONObject) { JSONObject json = (JSONObject) value; String name = addToEntityFromFileJson(entity, json, uploadCounter); if (name != null) changeJson.put("value", name); } else { changeJson.put("value", value); } } } changeJson.put("column", serverColumn); String createdAt = DateUtilities.timeToIso8601(change.getValue(OutstandingEntry.CREATED_AT_PROPERTY), true); changeJson.put("created_at", createdAt != null ? createdAt : 0); array.put(changeJson); } catch (JSONException e) { Log.e(ERROR_TAG, "Error writing change to JSON", e); Crittercism.logHandledException(e); } } return array; } private String addToEntityFromFileJson(MultipartEntity entity, JSONObject json, AtomicInteger uploadCounter) { if (json.has("path")) { String path = json.optString("path"); String name = String.format("upload-%s-%s-%d", table, uuid, uploadCounter.get()); String type = json.optString("type"); File f = new File(path); if (f.exists()) { json.remove("path"); entity.addPart(name, new FileBody(f, type)); return name; } } return null; } protected void populateChanges() { TodorooCursor<OE> cursor = outstandingDao.query(Query.select(DaoReflectionHelpers.getModelProperties(outstandingClass)) .where(OutstandingEntry.ENTITY_ID_PROPERTY.eq(id)).orderBy(Order.asc(OutstandingEntry.CREATED_AT_PROPERTY))); try { for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { try { OE instance = outstandingClass.newInstance(); instance.readPropertiesFromCursor(cursor); changes.add(instance); } catch (IllegalAccessException e) { Log.e("ChangesHappened", "Error instantiating outstanding model class", e); } catch (InstantiationException e2) { Log.e("ChangesHappened", "Error instantiating outstanding model class", e2); } } } finally { cursor.close(); } } // Return false if value is detected to be something that would definitely cause a server error // (e.g. empty task title, etc.) private boolean validateValue(Property<?> property, Object value) { if (Task.TITLE.equals(property)) { if (!(value instanceof String) || TextUtils.isEmpty((String) value)) return false; } return true; } private JSONObject getFileJson(String value) { try { JSONObject obj = new JSONObject(value); String path = obj.optString("path"); if (TextUtils.isEmpty(path)) return null; File f = new File(path); if (!f.exists()) return null; return obj; } catch (JSONException e) { return null; } } private class PropertyToJSONVisitor implements PropertyVisitor<Object, OE> { private String getAsString(OE data) { return data.getValue(OutstandingEntry.VALUE_STRING_PROPERTY); } @Override public Object visitInteger(Property<Integer> property, OE data) { Integer i = data.getMergedValues().getAsInteger(OutstandingEntry.VALUE_STRING_PROPERTY.name); if (i != null) { if (property.checkFlag(Property.PROP_FLAG_BOOLEAN)) return i > 0; return i; } else { return getAsString(data); } } @Override public Object visitLong(Property<Long> property, OE data) { Long l = data.getMergedValues().getAsLong(OutstandingEntry.VALUE_STRING_PROPERTY.name); if (l != null) { if (property.checkFlag(Property.PROP_FLAG_DATE)) { boolean includeTime = true; if (Task.DUE_DATE.equals(property) && !Task.hasDueTime(l)) includeTime = false; return DateUtilities.timeToIso8601(l, includeTime); } return l; } else { return getAsString(data); } } @Override public Object visitDouble(Property<Double> property, OE data) { Double d = data.getMergedValues().getAsDouble(OutstandingEntry.VALUE_STRING_PROPERTY.name); if (d != null) { return d; } else { return getAsString(data); } } @Override public Object visitString(Property<String> property, OE data) { String value = getAsString(data); if (RemoteModel.NO_UUID.equals(value) && property.checkFlag(Property.PROP_FLAG_USER_ID)) return ActFmPreferenceService.userId(); if (property.checkFlag(Property.PROP_FLAG_JSON)) { if (TextUtils.isEmpty(value)) return null; try { if (value != null && value.startsWith("[")) return new JSONArray(value); else return new JSONObject(value); } catch (JSONException e) { return null; } } return value; } } }