org.intermine.api.profile.TagManager.java Source code

Java tutorial

Introduction

Here is the source code for org.intermine.api.profile.TagManager.java

Source

package org.intermine.api.profile;

/*
 * Copyright (C) 2002-2013 FlyMine
 *
 * This code may be freely distributed and modified under the
 * terms of the GNU Lesser General Public Licence.  This should
 * be distributed with the code.  See the LICENSE file for more
 * information or http://www.gnu.org/copyleft/lesser.html.
 *
 */

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.collections.keyvalue.MultiKey;
import org.apache.log4j.Logger;
import org.intermine.api.search.TaggingEvent;
import org.intermine.api.search.TaggingEvent.TagChange;
import org.intermine.api.search.WebSearchable;
import org.intermine.api.tag.TagNames;
import org.intermine.api.tag.TagTypes;
import org.intermine.metadata.ClassDescriptor;
import org.intermine.metadata.CollectionDescriptor;
import org.intermine.metadata.ReferenceDescriptor;
import org.intermine.model.userprofile.Tag;
import org.intermine.model.userprofile.UserProfile;
import org.intermine.objectstore.ObjectStore;
import org.intermine.objectstore.ObjectStoreException;
import org.intermine.objectstore.ObjectStoreWriter;
import org.intermine.objectstore.query.ConstraintOp;
import org.intermine.objectstore.query.ConstraintSet;
import org.intermine.objectstore.query.ContainsConstraint;
import org.intermine.objectstore.query.Query;
import org.intermine.objectstore.query.QueryClass;
import org.intermine.objectstore.query.QueryField;
import org.intermine.objectstore.query.QueryObjectReference;
import org.intermine.objectstore.query.QueryValue;
import org.intermine.objectstore.query.SimpleConstraint;
import org.intermine.objectstore.query.SingletonResults;
import org.intermine.util.CacheMap;
import org.intermine.util.DynamicUtil;

/**
 * Manager class for tags. Implements retrieving, adding and deleting tags in user profile
 * database.
 * @author Jakub Kulaviak <jakub@flymine.org>
 * @author Alex Kalderimis
 * @author Daniela Butano
 */
public class TagManager {
    private static final Logger LOG = Logger.getLogger(TagManager.class);
    protected ObjectStoreWriter osWriter;
    private CacheMap<MultiKey, List<Tag>> tagCache = null;

    /** What we tell users when they give us an invalid tag name **/
    public static final String INVALID_NAME_MSG = "Invalid name. " + "Names may only contain letters, "
            + "numbers, spaces, full stops, hyphens and colons.";

    /**
     * Constructor. Use TagManagerFactory for creating tag manager.
     * @param profileOsWriter user profile object store
     */
    public TagManager(ObjectStoreWriter profileOsWriter) {
        this.osWriter = profileOsWriter;
    }

    /**
     * Delete a tag object from the database.
     * @param tag Tag object
     */
    public synchronized void deleteTag(Tag tag) {
        try {
            tagCache = null;
            osWriter.delete(tag);
        } catch (ObjectStoreException e) {
            LOG.error("delete tag failed" + e);
            throw new RuntimeException("Delete tag failed", e);
        }
    }

    private void checkUserExists(String userName) {
        UserProfile profile = getUserProfile(userName);
        if (profile == null) {
            throw new UserNotFoundException("User " + userName + " doesn't exist");
        }
    }

    /**
     * Test whether a particular taggable thing has been tagged with a particular tag.
     *
     * This method returns true if ANY user has tagged this object.
     *
     * @param tagName The tag name to check.
     * @param taggable The particular taggable thing.
     * @return True if the object is associated with the tag.
     */
    public boolean hasTag(String tagName, Taggable taggable) {
        return hasTag(tagName, taggable, null);
    }

    /**
     * Test whether a particular taggable thing has been tagged with a particular tag,
     * by a particular user.
     *
     * If the profile argument is null, then that argument is treated as a wild-card.
     *
     * @param tagName The tag name to check.
     * @param taggable The particular taggable thing.
     * @param profile The user who is meant to have tagged the item, or null for a wildcard.
     * @return True if the object is associated with the tag.
     */
    public boolean hasTag(String tagName, Taggable taggable, Profile profile) {
        String userName = null;
        if (profile != null) {
            if (!profile.isLoggedIn()) {
                return false;
            }
            userName = profile.getUsername();
        }
        List<Tag> found = getTags(tagName, taggable.getName(), taggable.getTagType(), userName);
        return found != null && !found.isEmpty();
    }

    /**
     * Delete a tag by name from a web-searchable object.
     *
     * This action fires a TagChange.REMOVED event.
     *
     * @param tagName The tag to remove.
     * @param ws The object to remove it from.
     * @param profile The user who is meant to own the tag.
     */
    public void deleteTag(String tagName, WebSearchable ws, Profile profile) {
        deleteTag(tagName, ws.getName(), ws.getTagType(), profile.getUsername());
        ws.fireEvent(new TaggingEvent(ws, tagName, TagChange.REMOVED));
    }

    /**
     * Delete a tag by name from a class-descriptor.
     *
     * @param tagName The tag to remove.
     * @param cd The class descriptor to remove it from.
     * @param profile The profile the tag should be removed from.
     */
    public void deleteTag(String tagName, ClassDescriptor cd, Profile profile) {
        deleteTag(tagName, cd.getName(), TagTypes.CLASS, profile.getUsername());
    }

    /**
     * Delete a tag by name from a reference-descriptor.
     *
     * @param tagName The tag to remove.
     * @param rd The reference descriptor to remove it from.
     * @param profile The profile the tag should be removed from.
     */
    public void deleteTag(String tagName, ReferenceDescriptor rd, Profile profile) {
        String objIdentifier = rd.getClassDescriptor().getSimpleName() + "." + rd.getName();
        if (rd instanceof CollectionDescriptor) {
            deleteTag(tagName, objIdentifier, TagTypes.COLLECTION, profile.getUsername());
        } else {
            deleteTag(tagName, objIdentifier, TagTypes.REFERENCE, profile.getUsername());
        }
    }

    /**
     * Deletes a tag object from the database.
     *
     * If there is no such tag, an Exception will be thrown to that effect.
     *
     * @param tagName the tag name
     * @param taggedObject the object identifier of the tagged object
     * @param type the tag type
     * @param userName the user name associated with the tag.
     */
    protected void deleteTag(String tagName, String taggedObject, String type, String userName) {
        List<Tag> tags = getTags(tagName, taggedObject, type, userName);
        if (tags.size() > 0 && tags.get(0) != null) {
            deleteTag(tags.get(0));
        } else {
            throw new RuntimeException("Attempt to delete non existing tag. tagName=" + tagName + ", taggedObject="
                    + taggedObject + ", type=" + type + ", userName=" + userName);
        }
    }

    /**
     * Deletes tags object from the database. Any null arguments will be treated as
     * wildcards.
     * @param tagName tag name
     * @param taggedObject object id of tagged object
     * @param type tag type
     * @param userName user name
     */
    public void deleteTags(String tagName, String taggedObject, String type, String userName) {
        List<Tag> tags = getTags(tagName, taggedObject, type, userName);
        for (Tag tag : tags) {
            deleteTag(tag);
        }
    }

    private static Set<String> tagsToTagNames(List<Tag> tags) {
        Set<String> ret = new TreeSet<String>();
        for (Tag tag : tags) {
            ret.add(tag.getTagName());
        }
        return ret;
    }

    /**
     * Returns names of tags of specified user and tag type. For anonymous user returns empty set.
     *
     * @param type tag type
     * @param user the user. MAY NOT BE NULL.
     * @return tag names
     */
    public Set<String> getUserTagNames(String type, Profile user) {
        if (user == null) {
            throw new IllegalArgumentException("user may not be null.");
        }
        if (user.isLoggedIn()) {
            return Collections.emptySet();
        }
        return getUserTagNames(type, user.getUsername());
    }

    /**
     * Returns names of tags of specified user and tag type. For anonymous user returns empty set.
     *
     * <p>
     * Use of this method is <strong>strongly discouraged</strong>. It is better to use the typed methods
     * instead. This method will be removed in the future.
     * </p>
     *
     * @param type tag type
     * @param userName user name
     * @return tag names
     * @throws UserNotFoundException if user doesn't exist
     */
    public Set<String> getUserTagNames(String type, String userName) {
        return tagsToTagNames(getTags(null, null, type, userName));
    }

    /**
     * Returns tags of specified user.
     *
     * <p>
     * Use of this method is <strong>strongly discouraged</strong>. Use typed methods instead.
     * </p>
     *
     * @param userName user name
     * @return tags
     */
    public List<Tag> getUserTags(String userName) {
        return getTags(null, null, null, userName);
    }

    /**
     * Returns tags of specified user.
     *
     * @param userName user name
     * @return tags
     */
    public List<Tag> getUserTags(Profile user) {
        if (user == null) {
            throw new IllegalArgumentException("'user' may not be null.");
        }
        if (!user.isLoggedIn()) {
            return Collections.emptyList();
        }
        return getTags(null, null, null, user.getUsername());
    }

    /**
     * Returns names of tagged tags for specified object.
     * @param taggedObject tagged object, eg. template name
     * @param type tag type, eg. template
     * @param userName user name
     * @return tag names
     */
    public Set<String> getObjectTagNames(String taggedObject, String type, String userName) {
        List<Tag> tags = getTags(null, taggedObject, type, userName);
        return tagsToTagNames(tags);
    }

    /**
     * Returns names of tagged tags for specified object. For anonymous user returns empty set.
     * @param taggable A taggable object.
     * @param profile The profile of the user with access to these tags.
     * @return tag names
     */
    public Set<String> getObjectTagNames(Taggable taggable, Profile profile) {
        if (profile.isLoggedIn()) {
            return getObjectTagNames(taggable.getName(), taggable.getTagType(), profile.getUsername());
        } else {
            return Collections.emptySet();
        }
    }

    /**
     * Get the tags for a specific object.
     * @param taggable The object with the tags.
     * @param profile The user these tags should belong to.
     * @return tags.
     */
    public List<Tag> getObjectTags(Taggable taggable, Profile profile) {
        if (profile.isLoggedIn()) {
            return getTags(null, taggable.getName(), taggable.getTagType(), profile.getUsername());
        } else {
            return Collections.emptyList();
        }
    }

    /**
     * Get Tag by object id.
     * @param id intermine object id
     * @return Tag
     */
    public synchronized Tag getTagById(int id) {
        try {
            return (Tag) osWriter.getObjectById(new Integer(id), Tag.class);
        } catch (ObjectStoreException e) {
            throw new RuntimeException("Getting tag from database failed", e);
        }
    }

    /**
     * Helper method for getTags.
     * @param cs The constraint set being built up.
     * @param qc The class the constrain on.
     * @param fieldName The field to constrain.
     * @param value The value to constrain to.
     */
    private static void constrain(ConstraintSet cs, QueryClass qc, String fieldName, String value) {
        if (value != null) {
            QueryValue qv = new QueryValue(value);
            QueryField qf = new QueryField(qc, fieldName);
            SimpleConstraint c = new SimpleConstraint(qf, ConstraintOp.EQUALS, qv);
            cs.addConstraint(c);
        }
    }

    /**
     * Return a List of Tags that match all the arguments.  Any null arguments will be treated as
     * wildcards.
     *
     * <p>
     * Use of this method is <strong>strongly discouraged</strong>. There are typed methods that are more
     * suitable. Use them instead.
     * </p>
     *
     * @param tagName the tag name - any String
     * @param taggedObjectId an object identifier that is appropriate for the given tag type
     * (eg. "Department.name" for the "collection" type)
     * @param type the tag type (eg. "collection", "reference", "attribute", "bag")
     * @param userName the use name this tag is associated with
     * @return the matching Tags
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public synchronized List<Tag> getTags(String tagName, String taggedObjectId, String type, String userName) {
        if (type != null) {
            checkTagType(type);
        }

        Map<MultiKey, List<Tag>> cache = getTagCache();
        MultiKey key = makeKey(tagName, taggedObjectId, type, userName);

        if (cache.containsKey(key)) {
            return cache.get(key);
        }

        // if there isn't a cache for user, than check if user exists
        // for performance reasons don't put this check at the method beginning
        if (userName != null) {
            checkUserExists(userName);
        }

        Query q = new Query();
        QueryClass qc = new QueryClass(Tag.class);

        q.addFrom(qc);
        q.addToSelect(qc);

        QueryField orderByField = new QueryField(qc, "tagName");

        q.addToOrderBy(orderByField);

        ConstraintSet cs = new ConstraintSet(ConstraintOp.AND);

        constrain(cs, qc, "tagName", tagName);
        constrain(cs, qc, "objectIdentifier", taggedObjectId);
        constrain(cs, qc, "type", type);

        if (userName != null) {
            QueryClass userProfileQC = new QueryClass(UserProfile.class);
            q.addFrom(userProfileQC);
            QueryValue qv = new QueryValue(userName);
            QueryField qf = new QueryField(userProfileQC, "username");
            SimpleConstraint c = new SimpleConstraint(qf, ConstraintOp.EQUALS, qv);
            cs.addConstraint(c);

            QueryObjectReference qr = new QueryObjectReference(qc, "userProfile");

            ContainsConstraint cc = new ContainsConstraint(qr, ConstraintOp.CONTAINS, userProfileQC);
            cs.addConstraint(cc);
        }
        q.setConstraint(cs);

        ObjectStore userprofileOS = osWriter.getObjectStore();
        SingletonResults results = userprofileOS.executeSingleton(q);
        addToCache(cache, key, ((List) results));
        return ((List) results);
    }

    private MultiKey makeKey(String tagName, String objectIdentifier, String type, String userName) {
        return new MultiKey(tagName, objectIdentifier, type, userName);
    }

    private void addToCache(Map<MultiKey, List<Tag>> cache, MultiKey key, List<Tag> results) {

        cache.put(key, new ArrayList<Tag>(results));

        Iterator<?> resIter = results.iterator();

        while (resIter.hasNext()) {
            Tag tag = (Tag) resIter.next();

            Object[] tagKeys = new Object[4];
            tagKeys[0] = tag.getTagName();
            tagKeys[1] = tag.getObjectIdentifier();
            tagKeys[2] = tag.getType();
            tagKeys[3] = tag.getUserProfile().getUsername();
        }

    }

    private Map<MultiKey, List<Tag>> getTagCache() {
        if (tagCache == null) {
            tagCache = new CacheMap<MultiKey, List<Tag>>();
        }
        return tagCache;
    }

    /**
     * Add a new tag.  The format of objectIdentifier depends on the tag type.
     * For types "attribute", "reference" and "collection" the objectIdentifier should have the form
     * "ClassName.fieldName".
     *
     * Don't use this method.... It makes kittens cry,
     *
     * @param tagName the tag name - any String
     * @param objectIdentifier an object identifier that is appropriate for the given tag type
     * (eg. "Department.name" for the "collection" type)
     * @param type the tag type (eg. "collection", "reference", "attribute", "bag")
     * @param profile The Profile of the user to associate this tag with.
     * @return the new Tag
     * @throws TagNameException If the tag name is invalid.
     * @throws TagNamePermissionException If the user does not have the required
     *         permissions to add this tag.
     */
    public synchronized Tag addTag(String tagName, String objectIdentifier, String type, Profile profile)
            throws TagNameException, TagNamePermissionException {

        if (profile == null) {
            throw new IllegalArgumentException("profile cannot be null");
        }
        if (!profile.isLoggedIn()) {
            throw new IllegalArgumentException("profile must exist in the user database");
        }
        if (tagName == null) {
            throw new IllegalArgumentException("tagName cannot be null");
        }
        if (tagNameNeedsPermission(tagName) && !profile.isSuperuser()) {
            throw new TagNamePermissionException();
        }

        //if (tagNameNeedsPermission(tagName) && profile.isSuperuser()
        //    && type.equals(TagTypes.BAG) && profile.getSavedBags().get(objectIdentifier) == null) {
        //    throw new TagNamePermissionException("You cannot add a tag starting with "
        //        + TagNames.IM_PREFIX + ", you are not the owner.");
        //}
        if (!isValidTagName(tagName)) {
            throw new TagNameException();
        }

        return addTag(tagName, objectIdentifier, type, profile.getUsername());
    }

    private static boolean tagNameNeedsPermission(String tagName) {
        return tagName.startsWith(TagNames.IM_PREFIX) && !TagNames.IM_FAVOURITE.equals(tagName)
                && !tagName.startsWith(TagNames.IM_WIDGET);
    }

    /**
     * Associate a websearchable obj with a certain tag.
     * @param tagName The tag we want to give this template.
     * @param ws the websearchable obj to tag.
     * @param profile The profile to associate this tag with.
     * @return A tag object.
     * @throws TagNameException If the name is invalid (contains illegal characters)
     * @throws TagNamePermissionException If this tag name is restricted.
     */
    public synchronized Tag addTag(String tagName, WebSearchable ws, Profile profile)
            throws TagNameException, TagNamePermissionException {
        Tag ret = addTag(tagName, ws.getName(), ws.getTagType(), profile);
        ws.fireEvent(new TaggingEvent(ws, tagName, TagChange.ADDED));
        return ret;
    }

    /**
     * Associate a class with a certain tag.
     * @param tagName The tag we want to give this class.
     * @param cld The descriptor for this class.
     * @param profile The profile to associate this tag with.
     * @return A tag object.
     * @throws TagNameException If the name is invalid (contains illegal characters)
     * @throws TagNamePermissionException If this tag name is restricted.
     */
    public synchronized Tag addTag(String tagName, ClassDescriptor cld, Profile profile)
            throws TagNameException, TagNamePermissionException {
        return addTag(tagName, cld.getName(), TagTypes.CLASS, profile);
    }

    /**
     * Associate a reference with a certain tag.
     * @param tagName The tag we want to give this reference.
     * @param ref the reference.
     * @param profile The profile to associate this tag with.
     * @return A tag object.
     * @throws TagNameException If the name is invalid (contains illegal characters)
     * @throws TagNamePermissionException If this tag name is restricted.
     */
    public synchronized Tag addTag(String tagName, ReferenceDescriptor ref, Profile profile)
            throws TagNameException, TagNamePermissionException {
        String objIdentifier = ref.getClassDescriptor().getSimpleName() + "." + ref.getName();
        if (ref instanceof CollectionDescriptor) {
            return addTag(tagName, objIdentifier, TagTypes.COLLECTION, profile);
        } else {
            return addTag(tagName, objIdentifier, TagTypes.REFERENCE, profile);
        }
    }

    /**
     * Add a new tag. This method is meant to only be used from
     * XML unmarshalling. Unlike the publicly visible addTag, it performs its operation,
     * even if the name is invalid.
     *
     *
     * @param tagName the tag name - any String
     * @param objectIdentifier an object identifier that is appropriate for the given tag type
     * (eg. "Department.name" for the "collection" type)
     * @param type the tag type (eg. "collection", "reference", "attribute", "bag")
     * @param username The username of the user to associate this tag with.
     * @return the new Tag
     */
    synchronized Tag addTag(String tagName, String objectIdentifier, String type, String username) {

        checkUserExists(username);
        checkTagType(type);
        tagCache = null;
        if (tagName == null) {
            throw new IllegalArgumentException("tagName cannot be null");
        }
        if (objectIdentifier == null) {
            throw new IllegalArgumentException("objectIdentifier cannot be null");
        }
        if (type == null) {
            throw new IllegalArgumentException("type cannot be null");
        }

        UserProfile userProfile = getUserProfile(username);
        Tag tag = (Tag) DynamicUtil.createObject(Collections.singleton(Tag.class));
        tag.setTagName(tagName);
        tag.setObjectIdentifier(objectIdentifier);
        tag.setType(type);
        tag.setUserProfile(userProfile);

        try {
            osWriter.store(tag);
            return tag;
        } catch (ObjectStoreException e) {
            throw new RuntimeException("cannot set tag", e);
        }
    }

    private void checkTagType(String type) {
        if (!isKnownTagType(type)) {
            throw new IllegalArgumentException("unknown tag type: '" + type + "'");
        }
    }

    private boolean isKnownTagType(String type) {
        return ("collection".equals(type) || "reference".equals(type) || "attribute".equals(type)
                || "bag".equals(type) || "template".equals(type) || "class".equals(type));
    }

    // duplication of ProfileManager method here, so this class is not dependent on ProfileManager
    private UserProfile getUserProfile(String userName) {
        UserProfile profile = new UserProfile();
        profile.setUsername(userName);
        Set<String> fieldNames = new HashSet<String>();
        fieldNames.add("username");
        try {
            profile = (UserProfile) osWriter.getObjectByExample(profile, fieldNames);
        } catch (ObjectStoreException e) {
            throw new RuntimeException("Unable to load user profile", e);
        }
        return profile;
    }

    /**
     * TODO this should use the same validation method the other classes use
     * Verifies that tag name can only contain A-Z, a-z, 0-9, '_', '-', ' ', ':', '.'
     * @param name tag name
     * @return true if the name is valid else false
     */
    public static boolean isValidTagName(String name) {
        if (name == null) {
            return false;
        }
        Pattern p = Pattern.compile("[^\\w\\s\\.\\-,:]");
        Matcher m = p.matcher(name);
        return !m.find();
    }

    /**
     * Deletes all tags assigned to a specified object.
     * @param taggedObject tagged object
     * @param type tag type
     * @param userName user name
     */
    public void deleteObjectTags(String taggedObject, String type, String userName) {
        List<Tag> tags = getTags(null, taggedObject, type, userName);
        for (Tag tag : tags) {
            deleteTag(tag);
        }
    }

    /**
     * Class for reporting exceptions from tag manipulation actions.
     * @author Alex Kalderimis
     *
     */
    public static class TagException extends Exception {
        private static final long serialVersionUID = 8400582347265920471L;

        /**
         * Constructor.
         * @param message The message to report to the user.
         */
        public TagException(String message) {
            super(message);
        }
    }

    /**
     * Class for representing errors due to the use of illegal tag names.
     * @author Alex Kalderimis
     *
     */
    public static class TagNameException extends TagException {

        private static final long serialVersionUID = 120037828345744006L;

        /**
         * Constructor.
         */
        public TagNameException() {
            super(INVALID_NAME_MSG);
        }
    }

    /**
     * Class for representing errors due to the restricted nature of some tags.
     * @author Alex Kalderimis.
     *
     */
    public static class TagNamePermissionException extends TagException {
        private static final long serialVersionUID = -7843016338063884403L;
        private static final String PERMISSION_MESSAGE = "You cannot add a tag starting with " + TagNames.IM_PREFIX
                + ", " + "that is a reserved word.";

        /**
         * Constructor.
         */
        public TagNamePermissionException() {
            super(PERMISSION_MESSAGE);
        }

        /**
         * Constructor.
         * @param message the message to display
         */
        public TagNamePermissionException(String message) {
            super(message);
        }
    }
}