edu.mit.mobile.android.locast.data.TaggableItem.java Source code

Java tutorial

Introduction

Here is the source code for edu.mit.mobile.android.locast.data.TaggableItem.java

Source

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.accounts.Account;
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.content.ProviderUtils;
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";

    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;

    };

    public TaggableItem(Cursor c) {
        super(c);
    }

    @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, Account account, Uri uri, JSONObject item, boolean updated)
                throws SyncException, IOException {
            super.onPostSyncItem(context, account, 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;
        }

        final List<String> path = baseUri.getPathSegments();
        // AUTHORITY/casts/tags
        if (path.size() >= 2 && Tag.PATH.equals(path.get(path.size() - 1))) {
            baseUri = ProviderUtils.removeLastPathSegment(baseUri);
        }

        return baseUri.buildUpon().appendQueryParameter(SERVER_QUERY_PARAMETER, Tag.toTagQuery(tags)).build();
    }

    /**
     * @param baseUri
     * @return the URI of the list of all tags for the given item
     */
    public static Uri getTagListUri(Uri baseUri) {
        return Uri.withAppendedPath(baseUri, Tag.PATH);
    }

    /**
     * Given a URI that includes a set of tags, parses them out and returns them as a set.
     * 
     * @param contentUri
     * @return the set of tags encoded in the URI
     * @throws IllegalArgumentException
     *             if the URI doesn't contain a tag query string
     */
    public static Set<String> getTagsFromUri(Uri contentUri) throws IllegalArgumentException {
        final String query = contentUri.getQueryParameter(SERVER_QUERY_PARAMETER);
        if (query == null) {
            throw new IllegalArgumentException("uri doesn't contain any tags");
        }
        return Tag.toSet(query);
    }

    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;
    }
}