diff --git a/api/src/com/todoroo/andlib/sql/Field.java b/api/src/com/todoroo/andlib/sql/Field.java index 809cce160..caa321a05 100644 --- a/api/src/com/todoroo/andlib/sql/Field.java +++ b/api/src/com/todoroo/andlib/sql/Field.java @@ -33,6 +33,8 @@ public class Field extends DBObject { */ @SuppressWarnings("nls") public Criterion eqCaseInsensitive(String value) { + if(value == null) + return UnaryCriterion.isNull(this); String escaped = value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_"); return UnaryCriterion.like(this, escaped, "\\"); } diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/TagViewActivity.java b/astrid/plugin-src/com/todoroo/astrid/actfm/TagViewActivity.java index 92b84d1f4..06067010c 100644 --- a/astrid/plugin-src/com/todoroo/astrid/actfm/TagViewActivity.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/TagViewActivity.java @@ -397,7 +397,7 @@ public class TagViewActivity extends TaskListActivity implements OnTabChangeList if(newTag) getIntent().putExtra(TOKEN_FILTER, Filter.emptyFilter(getString(R.string.tag_new_list))); - TodorooCursor cursor = tagDataService.query(Query.select(TagData.PROPERTIES).where(Criterion.or(TagData.NAME.eq(tag), + TodorooCursor cursor = tagDataService.query(Query.select(TagData.PROPERTIES).where(Criterion.or(TagData.NAME.eqCaseInsensitive(tag), Criterion.and(TagData.REMOTE_ID.gt(0), TagData.REMOTE_ID.eq(remoteId))))); try { tagData = new TagData(); @@ -637,11 +637,23 @@ public class TagViewActivity extends TaskListActivity implements OnTabChangeList String newName = tagName.getText().toString(); boolean nameChanged = !oldName.equals(newName); + TagService service = TagService.getInstance(); if (nameChanged) { - tagData.setValue(TagData.NAME, newName); - TagService.getInstance().rename(oldName, newName); - tagData.setFlag(TagData.FLAGS, TagData.FLAG_EMERGENT, false); - + if (oldName.equalsIgnoreCase(newName)) { // Change the capitalization of a list manually + tagData.setValue(TagData.NAME, newName); + service.renameCaseSensitive(oldName, newName); + tagData.setFlag(TagData.FLAGS, TagData.FLAG_EMERGENT, false); + } else { // Rename list--check for existing name + newName = service.getTagWithCase(newName); + tagName.setText(newName); + if (!newName.equals(oldName)) { + tagData.setValue(TagData.NAME, newName); + service.rename(oldName, newName); + tagData.setFlag(TagData.FLAGS, TagData.FLAG_EMERGENT, false); + } else { + nameChanged = false; + } + } } if(newName.length() > 0 && oldName.length() == 0) { diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmDataService.java b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmDataService.java index e3144bbe1..6fe8a738e 100644 --- a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmDataService.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmDataService.java @@ -174,7 +174,7 @@ public final class ActFmDataService { TodorooCursor cursor = tagDataService.query(Query.select(TagData.PROPERTIES).where( Criterion.or(TagData.REMOTE_ID.eq(tagObject.get("id")), Criterion.and(TagData.REMOTE_ID.eq(0), - TagData.NAME.eq(tagObject.getString("name")))))); + TagData.NAME.eqCaseInsensitive(tagObject.getString("name")))))); try { cursor.moveToNext(); TagData tagData = new TagData(); diff --git a/astrid/plugin-src/com/todoroo/astrid/tags/TagCaseMigrator.java b/astrid/plugin-src/com/todoroo/astrid/tags/TagCaseMigrator.java new file mode 100644 index 000000000..8bd029400 --- /dev/null +++ b/astrid/plugin-src/com/todoroo/astrid/tags/TagCaseMigrator.java @@ -0,0 +1,123 @@ +package com.todoroo.astrid.tags; + +import java.util.HashMap; + +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.Join; +import com.todoroo.andlib.sql.Query; +import com.todoroo.andlib.utility.Preferences; +import com.todoroo.astrid.data.Metadata; +import com.todoroo.astrid.data.TagData; +import com.todoroo.astrid.data.Task; +import com.todoroo.astrid.service.MetadataService; +import com.todoroo.astrid.service.TagDataService; +import com.todoroo.astrid.service.TaskService; + +public class TagCaseMigrator { + + @Autowired TaskService taskService; + @Autowired TagDataService tagDataService; + @Autowired MetadataService metadataService; + + private static final String PREF_CASE_MIGRATION_PERFORMED = "tag_case_migration"; //$NON-NLS-1$ + + public TagCaseMigrator() { + DependencyInjectionService.getInstance().inject(this); + } + + private final HashMap renameMap = new HashMap(); + private final HashMap nameToRemoteId = new HashMap(); + private final HashMap nameCountMap = new HashMap(); + + public void performTagCaseMigration() { + if (!Preferences.getBoolean(PREF_CASE_MIGRATION_PERFORMED, false)) { + TagService.Tag[] allTagData = TagService.getInstance().getGroupedTags(TagService.GROUPED_TAGS_BY_ALPHA, Criterion.all); + + for (int i = 0; i < allTagData.length - 1; i++) { + TagService.Tag first = allTagData[i]; + TagService.Tag second = allTagData[i+1]; + + if (first.tag.equalsIgnoreCase(second.tag)) { + markForRenaming(first.tag, first.remoteId); + markForRenaming(second.tag, second.remoteId); + } + } + + for (String key : renameMap.keySet()) { + TagService.getInstance().renameCaseSensitive(key, renameMap.get(key)); + updateTagData(key); + } + + Preferences.setBoolean(PREF_CASE_MIGRATION_PERFORMED, true); + } + } + + private String targetNameForTag(String tag) { + String targetName = tag.toLowerCase(); + targetName = targetName.substring(0, 1).toUpperCase() + targetName.substring(1); + return targetName; + } + + private void markForRenaming(String tag, long remoteId) { + if (renameMap.containsKey(tag)) return; + + String targetName = targetNameForTag(tag); + + int suffix = 1; + if (nameCountMap.containsKey(targetName)) { + suffix = nameCountMap.get(targetName); + } + + String newName = targetName + "_" + suffix; //$NON-NLS-1$ + nameCountMap.put(targetName, suffix + 1); + renameMap.put(tag, newName); + nameToRemoteId.put(tag, remoteId); + } + + private void updateTagData(String tag) { + long remoteId = nameToRemoteId.get(tag); + TodorooCursor tagData = tagDataService.query(Query.select(TagData.NAME, TagData.REMOTE_ID) + .where(Criterion.and( + TagData.NAME.eq(tag), TagData.REMOTE_ID.eq(remoteId)))); + try { + for (tagData.moveToFirst(); !tagData.isAfterLast(); tagData.moveToNext()) { + TagData curr = new TagData(tagData); + curr.setValue(TagData.NAME, renameMap.get(tag)); + tagDataService.save(curr); + } + } finally { + tagData.close(); + } + + addTasksToTargetTag(renameMap.get(tag), targetNameForTag(tag)); + } + + private void addTasksToTargetTag(String tag, String target) { + TodorooCursor tasks = taskService.query(Query.select(Task.ID).join(Join.inner(Metadata.TABLE, + Task.ID.eq(Metadata.TASK))).where(TagService.tagEq(tag, null))); + try { + for (tasks.moveToFirst(); !tasks.isAfterLast(); tasks.moveToNext()) { + Task curr = new Task(tasks); + TodorooCursor tagMetadata = metadataService.query(Query.select(TagService.TAG) + .where(Criterion.and(TagService.TAG.eq(target), Metadata.KEY.eq(TagService.KEY)))); + try { + if (tagMetadata.getCount() == 0) { + Metadata newTag = new Metadata(); + newTag.setValue(Metadata.KEY, TagService.KEY); + newTag.setValue(Metadata.TASK, curr.getId()); + newTag.setValue(TagService.TAG, target); + metadataService.save(newTag); + } // else already exists for some weird reason + } finally { + tagMetadata.close(); + } + } + } finally { + tasks.close(); + } + + } +} diff --git a/astrid/plugin-src/com/todoroo/astrid/tags/TagService.java b/astrid/plugin-src/com/todoroo/astrid/tags/TagService.java index 51741d3da..0671719ef 100644 --- a/astrid/plugin-src/com/todoroo/astrid/tags/TagService.java +++ b/astrid/plugin-src/com/todoroo/astrid/tags/TagService.java @@ -1,6 +1,7 @@ package com.todoroo.astrid.tags; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashSet; import com.todoroo.andlib.data.Property.CountProperty; @@ -116,16 +117,23 @@ public final class TagService { */ public QueryTemplate queryTemplate(Criterion criterion) { return new QueryTemplate().join(Join.inner(Metadata.TABLE, - Task.ID.eq(Metadata.TASK))).where(tagEq(tag, criterion)); + Task.ID.eq(Metadata.TASK))).where(tagEqIgnoreCase(tag, criterion)); } } - private static Criterion tagEq(String tag, Criterion additionalCriterion) { + public static Criterion tagEq(String tag, Criterion additionalCriterion) { return Criterion.and( MetadataCriteria.withKey(KEY), TAG.eq(tag), additionalCriterion); } + + public static Criterion tagEqIgnoreCase(String tag, Criterion additionalCriterion) { + return Criterion.and( + MetadataCriteria.withKey(KEY), TAG.eqCaseInsensitive(tag), + additionalCriterion); + } + public QueryTemplate untaggedTemplate() { return new QueryTemplate().where(Criterion.and( Criterion.not(Task.ID.in(Query.select(Metadata.TASK).from(Metadata.TABLE).where(MetadataCriteria.withKey(KEY)))), @@ -214,12 +222,17 @@ public final class TagService { public boolean synchronizeTags(long taskId, LinkedHashSet tags) { MetadataService service = PluginServices.getMetadataService(); + HashSet addedTags = new HashSet(); ArrayList metadata = new ArrayList(); for(String tag : tags) { + String tagWithCase = getTagWithCase(tag); // Find if any tag exists that matches with case ignore + if (addedTags.contains(tagWithCase)) // Prevent two identical tags from being added twice (e.g. don't add "Tag, tag" as "tag, tag") + continue; + addedTags.add(tagWithCase); Metadata item = new Metadata(); item.setValue(Metadata.KEY, KEY); - item.setValue(TAG, tag); - TagData tagData = tagDataService.getTag(tag, TagData.REMOTE_ID); + item.setValue(TAG, tagWithCase); + TagData tagData = tagDataService.getTag(tagWithCase, TagData.REMOTE_ID); if(tagData != null) item.setValue(REMOTE_ID, tagData.getValue(TagData.REMOTE_ID)); @@ -229,13 +242,53 @@ public final class TagService { return service.synchronizeMetadata(taskId, metadata, Metadata.KEY.eq(KEY)); } + /** + * If a tag already exists in the database that case insensitively matches the + * given tag, return that. Otherwise, return the argument + * @param tag + * @return + */ + public String getTagWithCase(String tag) { + MetadataService service = PluginServices.getMetadataService(); + String tagWithCase = tag; + TodorooCursor tagMetadata = service.query(Query.select(TAG).where(TagService.tagEqIgnoreCase(tag, Criterion.all)).limit(1)); + try { + if (tagMetadata.getCount() > 0) { + tagMetadata.moveToFirst(); + Metadata tagMatch = new Metadata(tagMetadata); + tagWithCase = tagMatch.getValue(TagService.TAG); + } else { + TodorooCursor tagData = tagDataService.query(Query.select(TagData.NAME).where(TagData.NAME.eqCaseInsensitive(tag))); + try { + if (tagData.getCount() > 0) { + tagData.moveToFirst(); + tagWithCase = new TagData(tagData).getValue(TagData.NAME); + } + } finally { + tagData.close(); + } + } + } finally { + tagMetadata.close(); + } + return tagWithCase; + } + public int delete(String tag) { invalidateTaskCache(tag); - return PluginServices.getMetadataService().deleteWhere(tagEq(tag, Criterion.all)); + return PluginServices.getMetadataService().deleteWhere(tagEqIgnoreCase(tag, Criterion.all)); } public int rename(String oldTag, String newTag) { - // First remove newTag from all tasks that have both oldTag and newTag. + return renameHelper(oldTag, newTag, false); + } + + public int renameCaseSensitive(String oldTag, String newTag) { // Need this for tag case migration process + return renameHelper(oldTag, newTag, true); + } + + private int renameHelper(String oldTag, String newTag, boolean caseSensitive) { + // First remove newTag from all tasks that have both oldTag and newTag. MetadataService metadataService = PluginServices.getMetadataService(); metadataService.deleteWhere( Criterion.and( @@ -245,11 +298,16 @@ public final class TagService { // Then rename all instances of oldTag to newTag. Metadata metadata = new Metadata(); metadata.setValue(TAG, newTag); - int ret = metadataService.update(tagEq(oldTag, Criterion.all), metadata); + int ret; + if (caseSensitive) + ret = metadataService.update(tagEq(oldTag, Criterion.all), metadata); + else + ret = metadataService.update(tagEqIgnoreCase(oldTag, Criterion.all), metadata); invalidateTaskCache(newTag); return ret; } + private Query rowsWithTag(String tag, Field... projections) { return Query.select(projections).from(Metadata.TABLE).where(Metadata.VALUE1.eq(tag)); } diff --git a/astrid/src/com/todoroo/astrid/service/TagDataService.java b/astrid/src/com/todoroo/astrid/service/TagDataService.java index 6aec55e98..527ad578c 100644 --- a/astrid/src/com/todoroo/astrid/service/TagDataService.java +++ b/astrid/src/com/todoroo/astrid/service/TagDataService.java @@ -80,7 +80,7 @@ public class TagDataService { * @return null if doesn't exist */ public TagData getTag(String name, Property... properties) { - TodorooCursor cursor = tagDataDao.query(Query.select(properties).where(TagData.NAME.eq(name))); + TodorooCursor cursor = tagDataDao.query(Query.select(properties).where(TagData.NAME.eqCaseInsensitive(name))); try { if(cursor.getCount() == 0) return null; diff --git a/astrid/src/com/todoroo/astrid/service/UpgradeService.java b/astrid/src/com/todoroo/astrid/service/UpgradeService.java index 7345fed8d..64c188496 100644 --- a/astrid/src/com/todoroo/astrid/service/UpgradeService.java +++ b/astrid/src/com/todoroo/astrid/service/UpgradeService.java @@ -30,6 +30,7 @@ import com.todoroo.astrid.data.Task; import com.todoroo.astrid.gtasks.GtasksPreferenceService; import com.todoroo.astrid.notes.NoteMetadata; import com.todoroo.astrid.producteev.sync.ProducteevDataService; +import com.todoroo.astrid.tags.TagCaseMigrator; import com.todoroo.astrid.utility.AstridPreferences; @@ -110,6 +111,9 @@ public final class UpgradeService { if(from < V3_1_0) new Astrid2To3UpgradeHelper().upgrade3To3_1(context, from); + if (from <= V3_8_3_1) + new TagCaseMigrator().performTagCaseMigration(); + } finally { DialogUtilities.dismissDialog((Activity)context, dialog); }