/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.tags;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
import android.widget.Toast;
import com.timsu.astrid.R;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.Property.CountProperty;
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.Field;
import com.todoroo.andlib.sql.Functions;
import com.todoroo.andlib.sql.Join;
import com.todoroo.andlib.sql.Order;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.sql.QueryTemplate;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.actfm.TagViewFragment;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.dao.MetadataDao;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.TagDataDao;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.RemoteModel;
import com.todoroo.astrid.data.SyncFlags;
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;
/**
* Provides operations for working with tags
*
* @author Tim Su <tim@todoroo.com>
*
*/
@SuppressWarnings("nls")
public final class TagService {
public static final String TOKEN_TAG_SQL = "tagSql"; //$NON-NLS-1$
public static final String SHOW_ACTIVE_TASKS = "show_main_task_view"; //$NON-NLS-1$
// --- singleton
private static TagService instance = null;
private static int[] default_tag_images = new int[] {
R.drawable.default_list_0,
R.drawable.default_list_1,
R.drawable.default_list_2,
R.drawable.default_list_3
};
public static synchronized TagService getInstance() {
if(instance == null)
instance = new TagService();
return instance;
}
// --- implementation details
@Autowired MetadataDao metadataDao;
@Autowired TaskService taskService;
@Autowired TagDataService tagDataService;
@Autowired TagDataDao tagDataDao;
public TagService() {
DependencyInjectionService.getInstance().inject(this);
}
/**
* Property for retrieving count of aggregated rows
*/
private static final CountProperty COUNT = new CountProperty();
public static final Order GROUPED_TAGS_BY_ALPHA = Order.asc(Functions.upper(TaskToTagMetadata.TAG_NAME));
public static final Order GROUPED_TAGS_BY_SIZE = Order.desc(COUNT);
/**
* Helper class for returning a tag/task count pair
*
* @author Tim Su <tim@todoroo.com>
*
*/
public static final class Tag {
public String tag;
public int count;
public long id;
public String uuid;
public String image;
public String userId;
public long memberCount;
public static Tag tagFromUUID(String uuid) {
TodorooCursor<TagData> tagData = PluginServices.getTagDataService().query(Query.select(TagData.PROPERTIES).where(TagData.UUID.eq(uuid)));
try {
if (tagData.getCount() > 0) {
tagData.moveToFirst();
return new Tag(new TagData(tagData));
} else {
return null;
}
} finally {
tagData.close();
}
}
public Tag(TagData tagData) {
id = tagData.getId();
tag = tagData.getValue(TagData.NAME);
count = tagData.getValue(TagData.TASK_COUNT);
uuid = tagData.getValue(TagData.UUID);
image = tagData.getPictureUrl(TagData.PICTURE, RemoteModel.PICTURE_THUMB);
userId = tagData.getValue(TagData.USER_ID);
memberCount = tagData.getValue(TagData.MEMBER_COUNT);
}
@Override
public String toString() {
return tag;
}
/**
* Return SQL selector query for getting tasks with a given tagData
*
* @param tagData
* @return
*/
public QueryTemplate queryTemplate(Criterion criterion) {
Criterion fullCriterion = Criterion.and(
Field.field("mtags." + Metadata.KEY.name).eq(TaskToTagMetadata.KEY),
Field.field("mtags." + TaskToTagMetadata.TAG_UUID.name).eq(uuid),
Field.field("mtags." + Metadata.DELETION_DATE.name).eq(0),
criterion);
return new QueryTemplate().join(Join.inner(Metadata.TABLE.as("mtags"), Task.UUID.eq(Field.field("mtags." + TaskToTagMetadata.TASK_UUID.name))))
.where(fullCriterion);
}
}
@Deprecated
private static Criterion tagEq(String tag, Criterion additionalCriterion) {
return Criterion.and(
MetadataCriteria.withKey(TaskToTagMetadata.KEY), TaskToTagMetadata.TAG_NAME.eq(tag),
additionalCriterion);
}
public static Criterion tagEqIgnoreCase(String tag, Criterion additionalCriterion) {
return Criterion.and(
MetadataCriteria.withKey(TaskToTagMetadata.KEY), TaskToTagMetadata.TAG_NAME.eqCaseInsensitive(tag),
additionalCriterion);
}
public QueryTemplate untaggedTemplate() {
return new QueryTemplate().where(Criterion.and(
Criterion.not(Task.UUID.in(Query.select(TaskToTagMetadata.TASK_UUID).from(Metadata.TABLE)
.where(Criterion.and(MetadataCriteria.withKey(TaskToTagMetadata.KEY), Metadata.DELETION_DATE.eq(0))))),
TaskCriteria.isActive(),
TaskCriteria.ownedByMe(),
TaskCriteria.isVisible()));
}
/**
* Return all tags ordered by given clause
*
* @param order ordering
* @param activeStatus criterion for specifying completed or uncompleted
* @return empty array if no tags, otherwise array
*/
public Tag[] getGroupedTags(Order order, Criterion activeStatus) {
Criterion criterion = Criterion.and(activeStatus, MetadataCriteria.withKey(TaskToTagMetadata.KEY));
Query query = Query.select(TaskToTagMetadata.TAG_NAME, TaskToTagMetadata.TAG_UUID, COUNT).
join(Join.inner(Task.TABLE, Metadata.TASK.eq(Task.ID))).
where(criterion).
orderBy(order).groupBy(TaskToTagMetadata.TAG_NAME);
TodorooCursor<Metadata> cursor = metadataDao.query(query);
try {
ArrayList<Tag> array = new ArrayList<Tag>();
for (int i = 0; i < cursor.getCount(); i++) {
cursor.moveToNext();
Tag tag = Tag.tagFromUUID(cursor.get(TaskToTagMetadata.TAG_UUID));
if (tag != null)
array.add(tag);
}
return array.toArray(new Tag[array.size()]);
} finally {
cursor.close();
}
}
public void createLink(Task task, String tagName) {
TodorooCursor<TagData> existingTag = tagDataService.query(Query.select(TagData.NAME, TagData.UUID)
.where(TagData.NAME.eqCaseInsensitive(tagName)));
try {
TagData tagData;
if (existingTag.getCount() == 0) {
tagData = new TagData();
tagData.setValue(TagData.NAME, tagName);
tagDataService.save(tagData);
} else {
existingTag.moveToFirst();
tagData = new TagData(existingTag);
}
createLink(task, tagData.getValue(TagData.NAME), tagData.getValue(TagData.UUID));
} finally {
existingTag.close();
}
}
public void createLink(Task task, String tagName, String tagUuid) {
Metadata link = TaskToTagMetadata.newTagMetadata(task.getId(), task.getUuid(), tagName, tagUuid);
if (metadataDao.update(Criterion.and(MetadataCriteria.byTaskAndwithKey(task.getId(), TaskToTagMetadata.KEY),
TaskToTagMetadata.TASK_UUID.eq(task.getValue(Task.UUID)), TaskToTagMetadata.TAG_UUID.eq(tagUuid)), link) <= 0) {
metadataDao.createNew(link);
}
}
/**
* Creates a link for a nameless tag. We expect the server to fill in the tag name with a MakeChanges message later
* @param taskId
* @param taskUuid
* @param tagUuid
*/
public void createLink(long taskId, String taskUuid, String tagUuid, boolean suppressOutstanding) {
TodorooCursor<TagData> existingTag = tagDataService.query(Query.select(TagData.NAME, TagData.UUID).where(TagData.UUID.eq(tagUuid)));
try {
TagData tagData;
String name = "";
if (existingTag.getCount() > 0) {
existingTag.moveToFirst();
tagData = new TagData(existingTag);
name = tagData.getValue(TagData.NAME);
}
Metadata link = TaskToTagMetadata.newTagMetadata(taskId, taskUuid, name, tagUuid);
if (suppressOutstanding)
link.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
if (metadataDao.update(Criterion.and(MetadataCriteria.byTaskAndwithKey(taskId, TaskToTagMetadata.KEY),
TaskToTagMetadata.TASK_UUID.eq(taskUuid), TaskToTagMetadata.TAG_UUID.eq(tagUuid)), link) <= 0) {
if (suppressOutstanding)
link.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
metadataDao.createNew(link);
}
} finally {
existingTag.close();
}
}
/**
* Delete a single task to tag link
* @param taskUuid
* @param tagUuid
*/
public void deleteLink(long taskId, String taskUuid, String tagUuid, boolean suppressOutstanding) {
Metadata deleteTemplate = new Metadata();
if (suppressOutstanding)
deleteTemplate.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
deleteTemplate.setValue(Metadata.TASK, taskId); // Need this for recording changes in outstanding table
deleteTemplate.setValue(TaskToTagMetadata.TAG_UUID, tagUuid); // Need this for recording changes in outstanding table
deleteTemplate.setValue(Metadata.DELETION_DATE, DateUtilities.now());
metadataDao.update(Criterion.and(MetadataCriteria.withKey(TaskToTagMetadata.KEY), Metadata.DELETION_DATE.eq(0),
TaskToTagMetadata.TASK_UUID.eq(taskUuid), TaskToTagMetadata.TAG_UUID.eq(tagUuid)), deleteTemplate);
}
/**
* Delete all links between the specified task and the list of tags
* @param taskUuid
* @param tagUuids
*/
public void deleteLinks(long taskId, String taskUuid, String[] tagUuids, boolean suppressOutstanding) {
Metadata deleteTemplate = new Metadata();
deleteTemplate.setValue(Metadata.TASK, taskId); // Need this for recording changes in outstanding table
deleteTemplate.setValue(Metadata.DELETION_DATE, DateUtilities.now());
if (tagUuids != null) {
for (String uuid : tagUuids) {
// TODO: Right now this is in a loop because each deleteTemplate needs the individual tagUuid in order to record
// the outstanding entry correctly. If possible, this should be improved to a single query
deleteTemplate.setValue(TaskToTagMetadata.TAG_UUID, uuid); // Need this for recording changes in outstanding table
if (suppressOutstanding)
deleteTemplate.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
metadataDao.update(Criterion.and(MetadataCriteria.withKey(TaskToTagMetadata.KEY), Metadata.DELETION_DATE.eq(0),
TaskToTagMetadata.TASK_UUID.eq(taskUuid), TaskToTagMetadata.TAG_UUID.eq(uuid)), deleteTemplate);
}
}
}
/**
* Return tags on the given task
*
* @param taskId
* @return cursor. PLEASE CLOSE THE CURSOR!
*/
public TodorooCursor<Metadata> getTags(long taskId) {
Criterion criterion = Criterion.and(MetadataCriteria.withKey(TaskToTagMetadata.KEY),
Metadata.DELETION_DATE.eq(0),
MetadataCriteria.byTask(taskId));
Query query = Query.select(TaskToTagMetadata.TAG_NAME, TaskToTagMetadata.TAG_UUID).where(criterion).orderBy(Order.asc(Functions.upper(TaskToTagMetadata.TAG_NAME)));
return metadataDao.query(query);
}
public TodorooCursor<TagData> getTagDataForTask(long taskId, Property<?>... properties) {
Criterion criterion = TagData.UUID.in(Query.select(TaskToTagMetadata.TAG_UUID)
.from(Metadata.TABLE)
.where(Criterion.and(MetadataCriteria.withKey(TaskToTagMetadata.KEY),
Metadata.DELETION_DATE.eq(0),
Metadata.TASK.eq(taskId))));
return tagDataService.query(Query.select(properties).where(criterion));
}
public TodorooCursor<TagData> getTagDataForTask(long taskId, Criterion additionalCriterion, Property<?>... properties) {
Criterion criterion = TagData.UUID.in(Query.select(TaskToTagMetadata.TAG_UUID).from(Metadata.TABLE).where(
Criterion.and(Metadata.DELETION_DATE.eq(0),
MetadataCriteria.byTaskAndwithKey(taskId, TaskToTagMetadata.KEY))));
return tagDataService.query(Query.select(properties).where(Criterion.and(criterion, additionalCriterion)));
}
/**
* Return tags as a comma-separated list of strings
*
* @param taskId
* @return empty string if no tags, otherwise string
*/
public String getTagsAsString(long taskId) {
return getTagsAsString(taskId, ", ");
}
/**
* Return tags as a list of strings separated by given separator
*
* @param taskId
* @return empty string if no tags, otherwise string
*/
public String getTagsAsString(long taskId, String separator) {
StringBuilder tagBuilder = new StringBuilder();
TodorooCursor<Metadata> tags = getTags(taskId);
try {
int length = tags.getCount();
Metadata metadata = new Metadata();
for (int i = 0; i < length; i++) {
tags.moveToNext();
metadata.readFromCursor(tags);
tagBuilder.append(metadata.getValue(TaskToTagMetadata.TAG_NAME));
if (i < length - 1)
tagBuilder.append(separator);
}
} finally {
tags.close();
}
return tagBuilder.toString();
}
public Intent deleteOrLeaveTag(Context context, String tag, String uuid) {
int deleted = deleteTagMetadata(uuid);
TagData tagData = tagDataDao.fetch(uuid, TagData.ID, TagData.UUID, TagData.DELETION_DATE, TagData.MEMBER_COUNT, TagData.USER_ID);
boolean shared = false;
Intent tagDeleted = new Intent(AstridApiConstants.BROADCAST_EVENT_TAG_DELETED);
if(tagData != null) {
tagData.setValue(TagData.DELETION_DATE, DateUtilities.now());
PluginServices.getTagDataService().save(tagData);
tagDeleted.putExtra(TagViewFragment.EXTRA_TAG_UUID, tagData.getUuid());
shared = tagData.getValue(TagData.MEMBER_COUNT) > 0 && !Task.USER_ID_SELF.equals(tagData.getValue(TagData.USER_ID)); // Was I a list member and NOT owner?
}
Toast.makeText(context, context.getString(shared ? R.string.TEA_tags_left : R.string.TEA_tags_deleted, tag, deleted),
Toast.LENGTH_SHORT).show();
context.sendBroadcast(tagDeleted);
return tagDeleted;
}
/**
* Return all tags (including metadata tags and TagData tags) in an array list
* @return
*/
public ArrayList<Tag> getTagList() {
ArrayList<Tag> tagList = new ArrayList<Tag>();
TodorooCursor<TagData> cursor = tagDataService.query(Query.select(TagData.PROPERTIES).where(Criterion.and(TagData.DELETION_DATE.eq(0), Criterion.or(TagData.IS_FOLDER.isNull(),
TagData.IS_FOLDER.neq(1)), TagData.NAME.isNotNull())).orderBy(Order.asc(Functions.upper(TagData.NAME))));
try {
TagData tagData = new TagData();
for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
tagData.readFromCursor(cursor);
if(tagData.getFlag(TagData.FLAGS, TagData.FLAG_FEATURED)) {
continue;
}
Tag tag = new Tag(tagData);
if(TextUtils.isEmpty(tag.tag))
continue;
tagList.add(tag);
}
} finally {
cursor.close();
}
return tagList;
}
public ArrayList<Tag> getFeaturedLists() {
HashMap<String, Tag> tags = new HashMap<String, Tag>();
TodorooCursor<TagData> cursor = tagDataService.query(Query.select(TagData.PROPERTIES)
.where(Functions.bitwiseAnd(TagData.FLAGS, TagData.FLAG_FEATURED).gt(0)));
try {
TagData tagData = new TagData();
for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
tagData.readFromCursor(cursor);
if (tagData.getValue(TagData.DELETION_DATE) > 0)
continue;
String tagName = tagData.getValue(TagData.NAME).trim();
Tag tag = new Tag(tagData);
if(TextUtils.isEmpty(tag.tag))
continue;
tags.put(tagName, tag);
}
} finally {
cursor.close();
}
ArrayList<Tag> tagList = new ArrayList<Tag>(tags.values());
Collections.sort(tagList,
new Comparator<Tag>() {
@Override
public int compare(Tag object1, Tag object2) {
return object1.tag.compareToIgnoreCase(object2.tag);
}
});
return tagList;
}
/**
* Save the given array of tags into the database
* @param taskId
* @param tags
*/
public boolean synchronizeTags(long taskId, String taskUuid, Set<String> tags) {
HashSet<String> existingLinks = new HashSet<String>();
TodorooCursor<Metadata> links = metadataDao.query(Query.select(Metadata.PROPERTIES)
.where(Criterion.and(TaskToTagMetadata.TASK_UUID.eq(taskUuid), Metadata.DELETION_DATE.eq(0))));
try {
for (links.moveToFirst(); !links.isAfterLast(); links.moveToNext()) {
Metadata link = new Metadata(links);
existingLinks.add(link.getValue(TaskToTagMetadata.TAG_UUID));
}
} finally {
links.close();
}
for (String tag : tags) {
TagData tagData = getTagDataWithCase(tag, TagData.NAME, TagData.UUID);
if (tagData == null) {
tagData = new TagData();
tagData.setValue(TagData.NAME, tag);
tagDataService.save(tagData);
}
if (existingLinks.contains(tagData.getValue(TagData.UUID))) {
existingLinks.remove(tagData.getValue(TagData.UUID));
} else {
Metadata newLink = TaskToTagMetadata.newTagMetadata(taskId, taskUuid, tag, tagData.getValue(TagData.UUID));
metadataDao.createNew(newLink);
}
}
// Mark as deleted links that don't exist anymore
deleteLinks(taskId, taskUuid, existingLinks.toArray(new String[existingLinks.size()]), false);
return true;
}
/**
* 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<Metadata> tagMetadata = service.query(Query.select(TaskToTagMetadata.TAG_NAME).where(TagService.tagEqIgnoreCase(tag, Criterion.all)).limit(1));
try {
if (tagMetadata.getCount() > 0) {
tagMetadata.moveToFirst();
Metadata tagMatch = new Metadata(tagMetadata);
tagWithCase = tagMatch.getValue(TaskToTagMetadata.TAG_NAME);
} else {
TodorooCursor<TagData> 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 TagData getTagDataWithCase(String tag, Property<?>... properties) {
TodorooCursor<TagData> tagData = tagDataService.query(Query.select(properties).where(TagData.NAME.eqCaseInsensitive(tag)));
try {
if (tagData.getCount() > 0) {
tagData.moveToFirst();
return new TagData(tagData);
}
} finally {
tagData.close();
}
return null;
}
public int deleteTagMetadata(String uuid) {
Metadata deleted = new Metadata();
deleted.setValue(Metadata.DELETION_DATE, DateUtilities.now());
return metadataDao.update(Criterion.and(MetadataCriteria.withKey(TaskToTagMetadata.KEY), TaskToTagMetadata.TAG_UUID.eq(uuid)), deleted);
}
public int rename(String uuid, String newName) {
return rename(uuid, newName, false);
}
public int rename(String uuid, String newName, boolean suppressSync) {
TagData template = new TagData();
template.setValue(TagData.NAME, newName);
if (suppressSync)
template.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
int result = tagDataDao.update(TagData.UUID.eq(uuid), template);
boolean tagRenamed = result > 0;
Metadata metadataTemplate = new Metadata();
metadataTemplate.setValue(TaskToTagMetadata.TAG_NAME, newName);
result = metadataDao.update(Criterion.and(MetadataCriteria.withKey(TaskToTagMetadata.KEY), TaskToTagMetadata.TAG_UUID.eq(uuid)), metadataTemplate);
tagRenamed = tagRenamed || result > 0;
return result;
}
public static int getDefaultImageIDForTag(String nameOrUUID) {
if (RemoteModel.NO_UUID.equals(nameOrUUID)) {
int random = (int)(Math.random()*4);
return default_tag_images[random];
}
return default_tag_images[((int)Math.abs(nameOrUUID.hashCode()))%4];
}
}