package edu.mit.mobile.android.locast.data; /* * Copyright (C) 2010 MIT Mobile Experience Lab * * 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 2 * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; import com.stackoverflow.CollectionUtils; import com.stackoverflow.Predicate; import edu.mit.mobile.android.locast.accounts.Authenticator; /** * DB entry for an item that can be tagged. * * @author stevep * */ public abstract class TaggableItem extends JsonSyncableItem { @SuppressWarnings("unused") private static final String TAG = TaggableItem.class.getSimpleName(); public static final String _PRIVACY = "privacy", _AUTHOR = "author", _AUTHOR_URI = "author_uri", _DRAFT = "draft"; public static final String PRIVACY_PUBLIC = "public", PRIVACY_PROTECTED = "protected", PRIVACY_PRIVATE = "private"; // the ordering of this must match the arrays.xml public static final String[] PRIVACY_LIST = {PRIVACY_PUBLIC, PRIVACY_PRIVATE}; public static final TaggableItemSyncMap SYNC_MAP = new TaggableItemSyncMap(); /** * The name of the server query parameter to filter using tags. */ public static final String SERVER_QUERY_PARAMETER = "tags"; /** * An item that will sync "tags" and "system_tags" fields. * @author steve * */ public static class TaggableItemSyncMap extends JsonSyncableItem.ItemSyncMap { public TaggableItemSyncMap() { super(); put(Tag.PATH, new SyncMapJoiner( new TagSyncField("tags", SyncItem.SYNC_TO), new TagSyncField("system_tags", SYSTEM_PREFIX, SyncItem.SYNC_TO)) { @Override public ContentValues joinContentValues(ContentValues[] cv) { return null; } }); final SyncMap authorSync = new SyncMap(); authorSync.put(_AUTHOR, new SyncFieldMap("display_name", SyncFieldMap.STRING, SyncItem.FLAG_OPTIONAL)); authorSync.put(_AUTHOR_URI, new SyncFieldMap("uri", SyncFieldMap.STRING)); put("_author", new SyncMapChain("author", authorSync, SyncItem.SYNC_FROM)); put(_PRIVACY, new SyncFieldMap("privacy", SyncFieldMap.STRING)); } /** * */ private static final long serialVersionUID = 1L; }; @Override public SyncMap getSyncMap() { return SYNC_MAP; } public static class TagSyncField extends SyncCustom { final private String prefix; public TagSyncField(String remoteKey, int flags) { super(remoteKey, flags); prefix = null; } public TagSyncField(String remoteKey, String prefix, int flags) { super(remoteKey, flags); this.prefix = prefix; } @Override public JSONArray toJSON(Context context, Uri localItem, Cursor c, String lProp) throws JSONException { if (localItem == null || context.getContentResolver().getType(localItem).startsWith("vnd.android.cursor.dir")){ return null; } JSONArray jo = null; if (localItem != null){ jo = new JSONArray(getTags(context.getContentResolver(), localItem, prefix)); } return jo; } @Override public ContentValues fromJSON(Context context, Uri localItem, JSONObject item, String lProp) throws JSONException { return null; // this shouldn't be called. } @Override public void onPostSyncItem(Context context, Uri uri, JSONObject item, boolean updated) throws SyncException, IOException { super.onPostSyncItem(context, uri, item, updated); if (updated){ // tags need to be loaded here, as they need a valid localUri in order to save. final JSONArray ja = item.optJSONArray(remoteKey); final List<String> tags = new ArrayList<String>(ja.length()); for (int i = 0; i < ja.length(); i++){ tags.add(ja.optString(i)); } //Log.d(TAG, uri + " has the following "+remoteKey +": "+ tags); TaggableItem.putTags(context.getContentResolver(), uri, tags, prefix); } } } /** * @param c a cursor pointing at an item's row * @return true if the item is editable by the logged-in user. */ public static boolean canEdit(Context context, Cursor c){ final String privacy = c.getString(c.getColumnIndex(_PRIVACY)); final String useruri = Authenticator.getUserUri(context); return privacy == null || useruri == null || useruri.length() == 0 || useruri.equals(c.getString(c.getColumnIndex(_AUTHOR_URI))); } /** * @param c * @return true if the authenticated user can change the item's privacy level. */ public static boolean canChangePrivacyLevel(Context context, Cursor c){ final String useruri = Authenticator.getUserUri(context); return useruri == null || useruri.equals(c.getString(c.getColumnIndex(_AUTHOR_URI))); } /** * @param cr * @return a list of all the tags attached to a given item */ public static Set<String> getTags(ContentResolver cr, Uri item) { return getTags(cr, item, null); } /** * @param cr * @param item * @param prefix * @return a list of all the tags attached to a given item */ public static Set<String> getTags(ContentResolver cr, Uri item, String prefix) { final Cursor tags = cr.query(Uri.withAppendedPath(item, Tag.PATH), Tag.DEFAULT_PROJECTION, null, null, null); final Set<String> tagSet = new HashSet<String>(tags.getCount()); final int tagColumn = tags.getColumnIndex(Tag._NAME); final Predicate<String> predicate = getPrefixPredicate(prefix); for (tags.moveToFirst(); !tags.isAfterLast(); tags.moveToNext()){ final String tag = tags.getString(tagColumn); if (predicate.apply(tag)){ final int separatorIndex = tag.indexOf(PREFIX_SEPARATOR); if (separatorIndex == -1){ tagSet.add(tag); }else{ tagSet.add(tag.substring(separatorIndex + 1)); } } } tags.close(); return tagSet; } /** * Sets the tags of the given item. Any existing tags will be deleted. * @param cr * @param item * @param tags */ public static void putTags(ContentResolver cr, Uri item, Collection<String> tags) { putTags(cr, item, tags, null); } public static String CV_TAG_PREFIX = "prefix"; /** * Sets the tags of a given prefix for the given item. Any existing tags using the given prefix will be deleted. * @param cr * @param item * @param tags * @param prefix */ public static void putTags(ContentResolver cr, Uri item, Collection<String> tags, String prefix) { final ContentValues cv = new ContentValues(); cv.put(Tag.PATH, TaggableItem.toListString(addPrefixToTags(prefix, tags))); cv.put(CV_TAG_PREFIX, prefix); cr.update(Uri.withAppendedPath(item, Tag.PATH), cv, null, null); } public static int MAX_POPULAR_TAGS = 10; /** * TODO make this pick the set of tags for a set of content. * * @param cr a content resolver * @return the top MAX_POPULAR_TAGS most popular tags in the set, with the most popular first. */ public static List<String> getPopularTags(ContentResolver cr){ final Map<String, Integer> tagPop = new HashMap<String, Integer>(); final List<String> popTags; final Cursor c = cr.query(Tag.CONTENT_URI, Tag.DEFAULT_PROJECTION, null, null, null); final int tagColumn = c.getColumnIndex(Tag._NAME); for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()){ final String tag = c.getString(tagColumn); final Integer count = tagPop.get(tag); if (count == null){ tagPop.put(tag, 1); }else{ tagPop.put(tag, count + 1); } } c.close(); popTags = new ArrayList<String>(tagPop.keySet()); Collections.sort(popTags, new Comparator<String>() { public int compare(String object1, String object2) { return tagPop.get(object2).compareTo(tagPop.get(object1)); } }); int limit; if (popTags.size() < MAX_POPULAR_TAGS){ limit = popTags.size(); }else{ limit = MAX_POPULAR_TAGS; } return popTags.subList(0, limit); } /** * Given a base content URI of a taggable item and a list of tags, constructs a URI * representing all the items of the baseUri that match all the listed tags. * * @param baseUri a content URI of a TaggableItem * @param tags a collection of tags * @return a URI representing all the items that match all the given tags * @see #getTagUri(Uri, Collection) */ public static Uri getTagUri(Uri baseUri, String ... tags){ return getTagUri(baseUri, Arrays.asList(tags)); } /** * Given a base content URI of a taggable item and a list of tags, constructs a URI * representing all the items of the baseUri that match all the listed tags. * * @param baseUri a content URI of a TaggableItem * @param tags a collection of tags * @return a URI representing all the items that match all the given tags */ public static Uri getTagUri(Uri baseUri, Collection<String> tags){ if (tags.isEmpty()){ return baseUri; } return baseUri.buildUpon().appendQueryParameter(SERVER_QUERY_PARAMETER, Tag.toTagQuery(tags)) .build(); } private final static char PREFIX_SEPARATOR = ':'; public final static String SYSTEM_PREFIX = "system"; // cache predicates so objects don't get created each time a query is made. private static HashMap<String,HasPrefixPredicate> predicates = new HashMap<String, HasPrefixPredicate>(); private static HasPrefixPredicate getPrefixPredicate(String prefix){ if (predicates.containsKey(prefix)){ return predicates.get(prefix); }else{ final HasPrefixPredicate predicate = new HasPrefixPredicate(prefix); predicates.put(prefix, predicate); return predicate; } } public static Collection<String> filterTags(String prefix, Collection<String> tags){ final Predicate<String> predicate = getPrefixPredicate(prefix); return CollectionUtils.filter(tags, predicate); } public static void filterTagsInPlace(String prefix, Collection<String> tags){ final Predicate<String> predicate = getPrefixPredicate(prefix); CollectionUtils.filterInPlace(tags, predicate); } private static class HasPrefixPredicate implements Predicate<String> { private final String mPrefix; /** * @param prefix prefix string or null for un-prefixed tags. */ public HasPrefixPredicate(String prefix) { mPrefix = prefix; } public boolean apply(String in) { final int separatorIndex = in.indexOf(PREFIX_SEPARATOR); if (separatorIndex == -1){ // we asked for a prefix, but this contains none. if (mPrefix != null){ return false; // a null prefix was requested, so it's good that we have no separator. }else{ return true; } } return in.substring(0, separatorIndex).equals(mPrefix); } } public static String addPrefixToTag(String prefix, String tag){ return prefix + PREFIX_SEPARATOR + tag; } private static Collection<String> addPrefixToTags(String prefix, Collection<String> tags){ if (prefix == null){ return tags; } final ArrayList<String> prefixedTags = new ArrayList<String>(tags.size()); for (final String tag: tags){ prefixedTags.add(addPrefixToTag(prefix, tag)); } return prefixedTags; } /** * Strips prefixes from tags. * * @param tags * @return a list of the tags with any prefix removed. */ public static Set<String> removePrefixesFromTags(Collection<String> tags){ if (tags == null){ return null; } final Set<String> nonPrefixedTags = new HashSet<String>(tags.size()); for (final String tag: tags){ final int sepIndex = tag.indexOf(PREFIX_SEPARATOR); if (sepIndex != -1){ nonPrefixedTags.add(tag.substring(sepIndex+1)); }else{ nonPrefixedTags.add(tag); } } return nonPrefixedTags; } }