com.ushahidi.swiftriver.core.api.service.BucketService.java Source code

Java tutorial

Introduction

Here is the source code for com.ushahidi.swiftriver.core.api.service.BucketService.java

Source

/**
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/agpl.html>
 * 
 * Copyright (C) Ushahidi Inc. All Rights Reserved.
 */
package com.ushahidi.swiftriver.core.api.service;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.dozer.Mapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.ushahidi.swiftriver.core.api.controller.BucketsController;
import com.ushahidi.swiftriver.core.api.dao.AccountDao;
import com.ushahidi.swiftriver.core.api.dao.BucketCollaboratorDao;
import com.ushahidi.swiftriver.core.api.dao.BucketCommentDao;
import com.ushahidi.swiftriver.core.api.dao.BucketDao;
import com.ushahidi.swiftriver.core.api.dao.BucketDropDao;
import com.ushahidi.swiftriver.core.api.dao.BucketDropFormDao;
import com.ushahidi.swiftriver.core.api.dao.DropDao;
import com.ushahidi.swiftriver.core.api.dao.LinkDao;
import com.ushahidi.swiftriver.core.api.dao.PlaceDao;
import com.ushahidi.swiftriver.core.api.dao.RiverDropDao;
import com.ushahidi.swiftriver.core.api.dao.TagDao;
import com.ushahidi.swiftriver.core.api.dto.CreateBucketDTO;
import com.ushahidi.swiftriver.core.api.dto.CreateCollaboratorDTO;
import com.ushahidi.swiftriver.core.api.dto.CreateCommentDTO;
import com.ushahidi.swiftriver.core.api.dto.CreateLinkDTO;
import com.ushahidi.swiftriver.core.api.dto.CreatePlaceDTO;
import com.ushahidi.swiftriver.core.api.dto.CreateTagDTO;
import com.ushahidi.swiftriver.core.api.dto.FollowerDTO;
import com.ushahidi.swiftriver.core.api.dto.FormValueDTO;
import com.ushahidi.swiftriver.core.api.dto.GetBucketDTO;
import com.ushahidi.swiftriver.core.api.dto.GetCollaboratorDTO;
import com.ushahidi.swiftriver.core.api.dto.GetCommentDTO;
import com.ushahidi.swiftriver.core.api.dto.GetDropDTO;
import com.ushahidi.swiftriver.core.api.dto.GetDropDTO.GetLinkDTO;
import com.ushahidi.swiftriver.core.api.dto.GetDropDTO.GetPlaceDTO;
import com.ushahidi.swiftriver.core.api.dto.GetDropDTO.GetTagDTO;
import com.ushahidi.swiftriver.core.api.dto.ModifyCollaboratorDTO;
import com.ushahidi.swiftriver.core.api.dto.ModifyFormValueDTO;
import com.ushahidi.swiftriver.core.api.exception.BadRequestException;
import com.ushahidi.swiftriver.core.api.exception.ForbiddenException;
import com.ushahidi.swiftriver.core.api.exception.NotFoundException;
import com.ushahidi.swiftriver.core.api.filter.DropFilter;
import com.ushahidi.swiftriver.core.model.Account;
import com.ushahidi.swiftriver.core.model.ActivityType;
import com.ushahidi.swiftriver.core.model.Bucket;
import com.ushahidi.swiftriver.core.model.BucketCollaborator;
import com.ushahidi.swiftriver.core.model.BucketComment;
import com.ushahidi.swiftriver.core.model.BucketDrop;
import com.ushahidi.swiftriver.core.model.BucketDropComment;
import com.ushahidi.swiftriver.core.model.BucketDropForm;
import com.ushahidi.swiftriver.core.model.Drop;
import com.ushahidi.swiftriver.core.model.Link;
import com.ushahidi.swiftriver.core.model.Place;
import com.ushahidi.swiftriver.core.model.Tag;
import com.ushahidi.swiftriver.core.solr.DropDocument;
import com.ushahidi.swiftriver.core.solr.repository.DropDocumentRepository;
import com.ushahidi.swiftriver.core.util.MD5Util;

/**
 * Service class for buckets
 * 
 * @author ekala
 * 
 */
@Service
@Transactional(readOnly = true)
public class BucketService {

    @Autowired
    private BucketDao bucketDao;

    @Autowired
    private Mapper mapper;

    @Autowired
    private AccountDao accountDao;

    @Autowired
    private BucketDropDao bucketDropDao;

    @Autowired
    private TagDao tagDao;

    @Autowired
    private LinkDao linkDao;

    @Autowired
    private PlaceDao placeDao;

    @Autowired
    private BucketCollaboratorDao bucketCollaboratorDao;

    @Autowired
    private RiverDropDao riverDropDao;

    @Autowired
    private BucketDropFormDao bucketDropFormDao;

    @Autowired
    private BucketCommentDao bucketCommentDao;

    @Autowired
    private DropDocumentRepository repository;

    @Autowired
    private AccountService accountService;

    @Autowired
    private DropDao dropDao;

    /* Logger */
    final static Logger logger = LoggerFactory.getLogger(BucketService.class);

    public void setBucketDao(BucketDao bucketDao) {
        this.bucketDao = bucketDao;
    }

    public BucketDao getBucketDao() {
        return bucketDao;
    }

    public Mapper getMapper() {
        return mapper;
    }

    public void setMapper(Mapper mapper) {
        this.mapper = mapper;
    }

    public AccountDao getAccountDao() {
        return accountDao;
    }

    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public BucketCollaboratorDao getBucketCollaboratorDao() {
        return bucketCollaboratorDao;
    }

    public void setBucketCollaboratorDao(BucketCollaboratorDao bucketCollaboratorDao) {
        this.bucketCollaboratorDao = bucketCollaboratorDao;
    }

    public void setBucketDropDao(BucketDropDao bucketDropDao) {
        this.bucketDropDao = bucketDropDao;
    }

    public BucketDropFormDao getBucketDropFormDao() {
        return bucketDropFormDao;
    }

    public void setBucketDropFormDao(BucketDropFormDao bucketDropFormDao) {
        this.bucketDropFormDao = bucketDropFormDao;
    }

    public void setTagDao(TagDao tagDao) {
        this.tagDao = tagDao;
    }

    public void setPlaceDao(PlaceDao placeDao) {
        this.placeDao = placeDao;
    }

    public void setLinkDao(LinkDao linkDao) {
        this.linkDao = linkDao;
    }

    public void setRiverDropDao(RiverDropDao riverDropDao) {
        this.riverDropDao = riverDropDao;
    }

    public void setAccountService(AccountService accountService) {
        this.accountService = accountService;
    }

    public void setDropDao(DropDao dropDao) {
        this.dropDao = dropDao;
    }

    /**
     * Creates a new {@link Bucket} entity under the {@link Account} associated
     * with <code>username</code>.
     * 
     * The created entity is transformed to DTO `
     * 
     * @param createDTO
     * @param username
     * @return
     */
    @Transactional(readOnly = false)
    public GetBucketDTO createBucket(CreateBucketDTO createDTO, String username) {
        // Get the bucket name
        String bucketName = createDTO.getName();

        if (bucketName == null) {
            throw new BadRequestException("The bucket name must be specified");
        }

        Account account = accountDao.findByUsernameOrEmail(username);

        // Check if a bucket with the specified name already exists
        if (bucketDao.findBucketByName(account, bucketName) != null) {
            throw new BadRequestException(
                    String.format("User %s already has a bucket named %s", username, bucketName));
        }

        Bucket bucket = new Bucket();
        bucket.setName(bucketName);
        if (createDTO.isPublished() != null) {
            bucket.setPublished(createDTO.isPublished());
        }
        bucket.setAccount(account);
        bucket.setDateAdded(new Date());

        // Save bucket
        bucketDao.create(bucket);

        accountService.logActivity(account, ActivityType.CREATE, bucket);

        return mapper.map(bucket, GetBucketDTO.class);
    }

    /**
     * Gets and returns a {@link Bucket} entity. If the {@link Bucket} has the
     * <code>published</code> property set to true, the querying {@link Account}
     * associated with <code>username</code> must be an owner of the
     * {@link Bucket} otherwise {@link BucketsController} returns a 404 to the
     * client
     * 
     * The retrieved entity is transformed to DTO for purposes of consumption by
     * {@link BucketsController}
     * 
     * @param id
     * @param username
     *            TODO
     * @return
     */
    public GetBucketDTO getBucket(Long id, String username) {
        Bucket bucket = getBucket(id);

        if (bucket == null)
            throw new NotFoundException("Bucket not found");

        Account queryingAccount = accountDao.findByUsernameOrEmail(username);

        if (!hasAccess(bucket, queryingAccount))
            throw new ForbiddenException("Permission Denied");

        return mapper.map(bucket, GetBucketDTO.class);
    }

    /**
     * Modifies a bucket with the specified <code>id</code> using the
     * information provided in <code>modifiedBucket</code> If the
     * {@link Account} associated with <code>username</code> is not the creator
     * of the bucket or is not a collaborator with edit privileges, an
     * {@link UnauthorizedExpection} is throw.
     * 
     * The modified {@link Bucket} is transformed into a DTO for purposes of
     * consumption by {@link BucketsController}
     * 
     * @param id
     * @param modifiedBucket
     * @param username
     * @return
     */
    @Transactional(readOnly = false)
    public GetBucketDTO modifyBucket(Long id, CreateBucketDTO modifiedBucket, String username) {
        Bucket bucket = bucketDao.findById(id);
        if (bucket == null) {
            throw new NotFoundException();
        }

        Account account = accountDao.findByUsernameOrEmail(username);

        // Is the account the creator of the bucket or a collaborator with edit
        // privileges?
        if (!isOwner(bucket, account))
            throw new ForbiddenException("Permission denied.");

        // Get the submitted name
        String bucketName = modifiedBucket.getName();

        if (bucketName == null) {
            throw new BadRequestException("The bucket name must be specified");
        }

        // Check if the owner already has a bucket with the
        // specified name
        Bucket existingBucket = bucketDao.findBucketByName(bucket.getAccount(), bucketName);
        if (!bucket.getName().equals(bucketName) && existingBucket != null
                && existingBucket.getId() != bucket.getId()) {
            throw new BadRequestException(String.format("Another bucket named %s already exists", bucketName));
        }

        bucket.setName(bucketName);

        // Have the privacy settings changed?
        if (modifiedBucket.isPublished() != null) {
            bucket.setPublished(modifiedBucket.isPublished());
        }

        // Has the layout changed?
        if (modifiedBucket.getDefaultLayout() != null) {
            bucket.setDefaultLayout(modifiedBucket.getDefaultLayout());
        }

        // Update the bucket
        bucketDao.update(bucket);

        return mapper.map(bucket, GetBucketDTO.class);
    }

    /**
     * Deletes the {@link Bucket} with the specified <code>id</code>. The
     * {@link Account} associated with <code>username</code> must be the creator
     * of the {@link Bucket} else a {@link ForbiddenException} is thrown
     * 
     * @param id
     * @param username
     */
    @Transactional(readOnly = false)
    public void deleteBucket(Long id, String username) {
        Bucket bucket = getBucket(id);
        Account queryingAccount = accountDao.findByUsernameOrEmail(username);
        if (!bucket.getAccount().equals(queryingAccount)) {
            throw new ForbiddenException();
        }
        bucketDao.delete(bucket);
    }

    /**
     * Get collaborators of the given bucket
     * 
     * @param bucketId
     * @return
     * @throws NotFoundException
     */
    public List<GetCollaboratorDTO> getCollaborators(Long bucketId) throws NotFoundException {
        Bucket bucket = bucketDao.findById(bucketId);
        if (bucket == null) {
            throw new NotFoundException("Bucket not found");
        }

        List<GetCollaboratorDTO> collaborators = new ArrayList<GetCollaboratorDTO>();

        for (BucketCollaborator collaborator : bucket.getCollaborators()) {
            collaborators.add(mapCollaboratorDTO(collaborator));
        }

        return collaborators;
    }

    /**
     * Adds a collaborator to the specified bucket
     * 
     * @param bucketId
     * @param createCollaboratorTO
     * @throws NotFoundException
     *             ,BadRequestException
     */
    public GetCollaboratorDTO addCollaborator(Long bucketId, CreateCollaboratorDTO createCollaboratorTO,
            String authUser) throws NotFoundException, BadRequestException {

        // Check if the bucket exists
        Bucket bucket = getBucket(bucketId);

        // Check if the authenticating user has permission to add a collaborator
        Account authAccount = accountDao.findByUsernameOrEmail(authUser);
        if (!isOwner(bucket, authAccount))
            throw new ForbiddenException("Permission denied.");

        // Is the account already collaborating on the bucket
        if (bucketDao.findCollaborator(bucketId, createCollaboratorTO.getAccount().getId()) != null)
            throw new BadRequestException("The account is already collaborating on the bucket");

        Account account = accountDao.findById(createCollaboratorTO.getAccount().getId());
        if (account == null)
            throw new NotFoundException("Account not found");

        BucketCollaborator collaborator = bucketDao.addCollaborator(bucket, account,
                createCollaboratorTO.isReadOnly());

        accountService.logActivity(authAccount, ActivityType.INVITE, collaborator);

        return mapCollaboratorDTO(collaborator);
    }

    /**
     * Modifies a collaborator
     * 
     * @param bucketId the unique id of the bucket
     * @param accountId the unique id of the collaborating account
     * @param modifyCollaboratorTO
     * @return
     */
    @Transactional(readOnly = false)
    public GetCollaboratorDTO modifyCollaborator(Long bucketId, Long accountId,
            ModifyCollaboratorDTO modifyCollaboratorTO, String authUser) {

        Bucket bucket = bucketDao.findById(bucketId);

        if (bucket == null)
            throw new NotFoundException("Bucket not found.");

        Account authAccount = accountDao.findByUsernameOrEmail(authUser);

        if (!isOwner(bucket, authAccount))
            throw new ForbiddenException("Permission denied.");

        // Find the collaborator
        BucketCollaborator collaborator = bucketDao.findCollaborator(bucketId, accountId);

        // Collaborator exists?
        if (collaborator == null) {
            throw new NotFoundException("Collaborator not found");
        }

        if (modifyCollaboratorTO.getActive() != null) {
            collaborator.setActive(modifyCollaboratorTO.getActive());
        }

        if (modifyCollaboratorTO.getReadOnly() != null) {
            collaborator.setReadOnly(modifyCollaboratorTO.getReadOnly());
        }

        // Post changes to the DB
        bucketDao.updateCollaborator(collaborator);

        return mapCollaboratorDTO(collaborator);
    }

    /**
     * Removes a collaborator in <code>accountId</code> from the bucket
     * specified in <code>bucketId</code>. <code>accountId</code> is the
     * {@link Account} id of the collaborator
     * 
     * @param bucketId the unique id of the bucket in the database
     * @param accountId the unique id of the collaborating account
     */
    @Transactional
    public void deleteCollaborator(Long bucketId, Long accountId, String authUser) {

        Bucket bucket = bucketDao.findById(bucketId);

        if (bucket == null)
            throw new NotFoundException("Bucket not found.");

        Account authAccount = accountDao.findByUsernameOrEmail(authUser);

        // Find the collaborator entry associated with the collaborating
        // accountId
        BucketCollaborator collaborator = bucketDao.findCollaborator(bucketId, accountId);

        if (collaborator == null)
            throw new NotFoundException("Collaborator not found.");

        // If the authenticating user is not a collaborator,
        // check for permissions
        if (!collaborator.getAccount().equals(authAccount)) {
            if (!isOwner(bucket, authAccount))
                throw new ForbiddenException("Permission denied.");
        }

        bucketCollaboratorDao.delete(collaborator);
    }

    /**
     * Utility method for mapping a {@link BucketCollaborator} object
     * to its DTO - {@link GetCollaboratorDTO}
     *  
     * @param collaborator
     * @return
     */
    private GetCollaboratorDTO mapCollaboratorDTO(BucketCollaborator collaborator) {
        GetCollaboratorDTO collaboratorDTO = mapper.map(collaborator.getAccount(), GetCollaboratorDTO.class);

        collaboratorDTO.setActive(collaborator.isActive());
        collaboratorDTO.setReadOnly(collaborator.isReadOnly());
        return collaboratorDTO;
    }

    /**
     * Adds the {@link Account} specified in <code>accountId</code> to the list
     * of followers for the {@link Bucket} identified by <code>id</code>
     * 
     * @param id
     * @param accountId
     * @param username
     */
    @Transactional
    public void addFollower(long id, long accountId, String username) {
        Bucket bucket = getBucket(id);

        Account account = accountDao.findById(accountId);
        if (account == null) {
            throw new NotFoundException();
        }

        // Verify that the account following the bucket is tied to the
        // currently logged in user
        if (!account.getOwner().getUsername().equals(username))
            throw new ForbiddenException("Permission denied.");

        // Is the account already following the bucket
        if (bucket.getFollowers().contains(account)) {
            throw new BadRequestException(String.format("%s  is already following bucket %d", accountId, id));
        }

        bucket.getFollowers().add(account);
        bucketDao.update(bucket);

        accountService.logActivity(account, ActivityType.FOLLOW, bucket);
    }

    /**
     * Gets and returns the list of followers for the {@link Bucket} with the
     * specified <code>id</code>. If <code>accountId</code> is specified, checks
     * if the {@link Account} associated with that id is following the bucket
     * 
     * @param id
     * @param accountId
     * @return {@link List<AccountDTO>}
     */
    @Transactional
    public List<FollowerDTO> getFollowers(Long id, Long accountId) {
        Bucket bucket = getBucket(id);

        List<FollowerDTO> followers = new ArrayList<FollowerDTO>();

        // Has the accountId parameter been specified
        if (accountId != null) {
            Account follower = accountDao.findById(accountId);
            if (follower == null) {
                throw new NotFoundException(String.format("The account %d does not exist", accountId));
            }

            if (bucket.getFollowers().contains(follower)) {
                followers.add(mapFollowerDTO(follower));
            } else {
                throw new NotFoundException(String.format("Account %d does not follow bucket %d", accountId, id));
            }
        } else {

            for (Account account : bucket.getFollowers()) {
                followers.add(mapFollowerDTO(account));
            }
        }

        return followers;
    }

    private FollowerDTO mapFollowerDTO(Account account) {
        FollowerDTO dto = mapper.map(account, FollowerDTO.class);

        // Set the name and email address
        dto.setName(account.getOwner().getName());
        dto.setEmail(account.getOwner().getEmail());

        return dto;
    }

    /**
     * Removes the account with the specified <code>accountId</code> from the
     * list of {@link Account}s following the bucket identified by
     * <code>id</code>
     * 
     * @param id
     * @param accountId
     */
    @Transactional
    public void deleteFollower(Long id, Long accountId) {
        Bucket bucket = getBucket(id);

        Account account = accountDao.findById(accountId);
        if (account == null) {
            throw new NotFoundException(String.format("Account %d does not exist", accountId));
        }

        // Does the account exist as a follower?
        if (bucket.getFollowers().contains(account)) {
            bucket.getFollowers().remove(account);
            bucketDao.update(bucket);
        } else {
            throw new NotFoundException(String.format("Account %d does not follow bucket %d", accountId, id));
        }
    }

    /**
     * Returns the drops for the bucket with the ID specified in <code>id</code>
     * using the {@link DropFilter} specified in <code>dropFilter</code>
     * 
     * @param id
     * @param dropFilter
     * @param page
     * @param dropCount
     * @param authUser
     * @return
     */
    public List<GetDropDTO> getDrops(Long id, DropFilter dropFilter, int page, int dropCount, String authUser) {

        Bucket bucket = getBucket(id);
        Account queryingAccount = accountDao.findByUsernameOrEmail(authUser);

        if (!hasAccess(bucket, queryingAccount)) {
            throw new ForbiddenException("Access denied");
        }

        List<GetDropDTO> getDropDTOs = new ArrayList<GetDropDTO>();

        if (dropFilter.getKeywords() != null || dropFilter.getBoundingBox() != null) {

            PageRequest pageRequest = new PageRequest(page - 1, dropCount);
            List<DropDocument> dropDocuments = repository.findInBucket(id, dropFilter, pageRequest);

            if (dropDocuments.isEmpty()) {
                return getDropDTOs;
            }

            List<Long> dropIds = new ArrayList<Long>();
            for (DropDocument document : dropDocuments) {
                dropIds.add(Long.parseLong(document.getId()));
            }

            // Set page number to 1
            page = 1;
            dropFilter.setDropIds(dropIds);
        }

        List<Drop> drops = bucketDao.getDrops(id, dropFilter, page, dropCount, queryingAccount);

        for (Drop drop : drops) {
            GetDropDTO dropDto = mapper.map(drop, GetDropDTO.class);
            getDropDTOs.add(dropDto);
        }

        return getDropDTOs;
    }

    /**
     * Verifies whether the {@link Account} specified in 
     * <code>queryingAccount</code> has access to the bucket specified
     * in <code>bucket</code>
     * 
     * @param bucket
     * @param queryingAccount
     * @return
     */
    private boolean hasAccess(Bucket bucket, Account queryingAccount) {
        if (bucket.isPublished())
            return true;

        return bucket.getAccount().equals(queryingAccount)
                || (bucketDao.findCollaborator(bucket.getId(), queryingAccount.getId()) != null);
    }

    /**
     * Deletes the {@link BucketDrop} specified in <code>dropId</code> from the
     * {@link Bucket} specified in <code>bucketId</code>
     * 
     * If the drop does not exist in the specified bucket, a
     * {@link NotFoundException} exception is thrown
     * 
     * @param bucketId
     * @param dropId
     * @param authUser
     */
    @Transactional(readOnly = false)
    public void deleteDrop(Long bucketId, Long dropId, String authUser) {
        Bucket bucket = getBucket(bucketId);

        if (!isOwner(bucket, authUser))
            throw new ForbiddenException("Permission denied");

        BucketDrop bucketDrop = getBucketDrop(bucketId, dropId);

        // Delete the drop and decrease bucket drop count
        bucketDropDao.delete(bucketDrop);
        bucketDao.decreaseDropCount(bucket);
    }

    /**
     * Adds the {@link Drop} specified in <code>dropId</code> to the
     * {@link Bucket} in <code>bucketId</code>. The {@link Account} associated
     * with <code>username</code> is used to verify whether the user submitting
     * the request is authorized
     * 
     * @param bucketId
     * @param dropId
     * @param username
     */
    @Transactional
    public void addDrop(Long bucketId, Long dropId, String username) {
        Bucket bucket = getBucket(bucketId);
        Account account = accountDao.findByUsernameOrEmail(username);

        if (!isOwner(bucket, account))
            throw new ForbiddenException("Permission denied.");

        Drop drop = dropDao.findById(dropId);
        if (dropId == null) {
            throw new NotFoundException(String.format("Drop %d does not exist", dropId));
        }
        BucketDrop bucketDrop = bucketDao.findBucketDrop(bucketId, dropId);
        if (bucketDrop != null) {
            // Increase the veracity
            bucketDropDao.increaseVeracity(bucketDrop);
            return;
        }

        // Create the bucket drop
        bucketDrop = new BucketDrop();
        bucketDrop.setBucket(bucket);
        bucketDrop.setDrop(drop);

        bucketDropDao.create(bucketDrop);

        bucketDao.increaseDropCount(bucket);
    }

    /**
     * Internal helper method to retrieve a bucket using its <code>id</code> in
     * the database
     * 
     * @param id
     * @return
     */
    private Bucket getBucket(Long id) {
        Bucket bucket = bucketDao.findById(id);
        if (bucket == null) {
            throw new NotFoundException(String.format("Bucket %d does not exist", id));
        }

        return bucket;
    }

    /**
     * Helper method for verifying whether the user submitting the request has
     * authorization to add/remove metadata to/from the {@link Bucket} specified
     * in <code>bucketId</code>
     * 
     * @param bucket
     * @param authUser
     */
    public boolean isOwner(Bucket bucket, String authUser) {
        Account account = accountDao.findByUsernameOrEmail(authUser);
        return isOwner(bucket, account);
    }

    /**
     * Verifies whether the {@link Account} in <code>account</code> is an owner
     * of the {@link Bucket} specified in <code>bucket</code>
     * 
     * An owner is an {@link Account} that is a creator of the {@link Bucket} 
     * or a {@link BucketCollaborator} with edit privileges i.e. the
     * <code>readOnly</code> property is false
     * 
     * @param bucket the {@link Bucket} being accessed
     * @param account the {@link Account} accessing the <code>bucket</code>
     * @return
     */
    public boolean isOwner(Bucket bucket, Account account) {
        BucketCollaborator collaborator = bucketDao.findCollaborator(bucket.getId(), account.getId());

        return bucket.getAccount() == account || (collaborator != null && !collaborator.isReadOnly());
    }

    /**
     * Adds a {@link Tag} to the {@link BucketDrop} with the specified
     * <code>dropId</code> The drop must be in the {@link Bucket} whose ID is
     * specified in <code>bucketId</code>
     * 
     * The created {@link Tag} entity is transformed to a DTO for purposes of
     * consumption by {@link BucketsController}
     * 
     * @param bucketId
     * @param dropId
     * @param createDTO
     * @param authUser
     * @return
     */
    @Transactional
    public GetTagDTO addDropTag(Long bucketId, Long dropId, CreateTagDTO createDTO, String authUser) {
        Bucket bucket = getBucket(bucketId);

        if (!isOwner(bucket, authUser))
            throw new ForbiddenException("Permission denied");

        // Get the bucket drop
        BucketDrop bucketDrop = getBucketDrop(bucketId, dropId);

        String hash = MD5Util.md5Hex(createDTO.getTag() + createDTO.getTagType());
        Tag tag = tagDao.findByHash(hash);
        if (tag == null) {
            tag = new Tag();
            tag.setTag(createDTO.getTag());
            tag.setType(createDTO.getTagType());

            tagDao.create(tag);
        } else {
            // Check if the tag exists in the bucket drop
            if (bucketDropDao.findTag(bucketDrop, tag) != null) {
                throw new BadRequestException(String.format("Tag %s of type %s has already been added to drop %d",
                        tag.getTag(), tag.getType(), dropId));
            }
        }

        bucketDropDao.addTag(bucketDrop, tag);
        return mapper.map(tag, GetTagDTO.class);
    }

    /**
     * Deletes the {@link Tag} with the id specified in <code>tagId</code> from
     * the {@link BucketDrop} specified in <code>dropId</code>
     * 
     * The request {@link BucketDrop} must be a member of the {@link Bucket}
     * with the ID specified in <code>bucketId</code> else a
     * {@link NotFoundException} is thrown
     * 
     * @param bucketId
     * @param dropId
     * @param tagId
     * @param authUser
     *            TODO
     */
    @Transactional
    public void deleteDropTag(Long bucketId, Long dropId, Long tagId, String authUser) {
        Bucket bucket = getBucket(bucketId);

        if (!isOwner(bucket, authUser))
            throw new ForbiddenException("Permission denied");

        BucketDrop bucketDrop = getBucketDrop(bucketId, dropId);

        Tag tag = tagDao.findById(tagId);

        if (tag == null) {
            throw new NotFoundException(String.format("Tag %d does not exist", tagId));
        }

        if (!bucketDropDao.deleteTag(bucketDrop, tag)) {
            throw new NotFoundException(String.format("Drop %d does not have tag %d", dropId, tagId));
        }
    }

    /**
     * Adds a {@link Link} to the {@link BucketDrop} with the specified
     * <code>dropId</code> The drop must be in the {@link Bucket} whose ID is
     * specified in <code>bucketId</code>
     * 
     * The created {@link Link} entity is transformed to a DTO for purposes of
     * consumption by {@link BucketsController}
     * 
     * @param bucketId
     * @param dropId
     * @param createDTO
     * @param authUser
     * @return
     */
    @Transactional
    public GetLinkDTO addDropLink(Long bucketId, Long dropId, CreateLinkDTO createDTO, String authUser) {
        Bucket bucket = getBucket(bucketId);

        if (!isOwner(bucket, authUser))
            throw new ForbiddenException("Permission denied");

        BucketDrop bucketDrop = getBucketDrop(bucketId, dropId);

        String hash = MD5Util.md5Hex(createDTO.getUrl());
        Link link = linkDao.findByHash(hash);
        if (link == null) {
            link = new Link();
            link.setUrl(createDTO.getUrl());
            link.setHash(hash);

            linkDao.create(link);
        } else {
            // Has the link already been added ?
            if (bucketDropDao.findLink(bucketDrop, link) != null) {
                throw new BadRequestException(
                        String.format("%s has already been added to drop %d", link.getUrl(), dropId));
            }
        }

        bucketDropDao.addLink(bucketDrop, link);
        return mapper.map(link, GetLinkDTO.class);
    }

    /**
     * Deletes the {@link Link} with the id specified in <code>linkId</code>
     * from the {@link BucketDrop} specified in <code>dropId</code>
     * 
     * The request {@link BucketDrop} must be a member of the {@link Bucket}
     * with the ID specified in <code>bucketId</code> else a
     * {@link NotFoundException} is thrown
     * 
     * @param bucketId
     * @param dropId
     * @param linkId
     * @param authUser
     */
    @Transactional
    public void deleteDropLink(Long bucketId, Long dropId, Long linkId, String authUser) {
        Bucket bucket = getBucket(bucketId);

        if (!isOwner(bucket, authUser))
            throw new ForbiddenException("Permission denied");

        BucketDrop bucketDrop = getBucketDrop(bucketId, dropId);
        Link link = linkDao.findById(linkId);

        if (link == null) {
            throw new NotFoundException(String.format("Link %d does not exist", linkId));
        }

        if (!bucketDropDao.deleteLink(bucketDrop, link)) {
            throw new NotFoundException(String.format("Drop %d does not have link %d", dropId, linkId));
        }
    }

    /**
     * Adds a {@link Place} to the {@link BucketDrop} with the specified
     * <code>dropId</code> The drop must be in the {@link Bucket} whose ID is
     * specified in <code>bucketId</code>
     * 
     * The created {@link Place} entity is transformed to a DTO for purposes of
     * consumption by {@link BucketsController}
     * 
     * @param bucketId
     * @param dropId
     * @param createDTO
     * @param authUser
     * @return
     */
    @Transactional
    public GetPlaceDTO addDropPlace(Long bucketId, Long dropId, CreatePlaceDTO createDTO, String authUser) {
        Bucket bucket = getBucket(bucketId);

        if (!isOwner(bucket, authUser))
            throw new ForbiddenException("Permission denied");

        BucketDrop bucketDrop = getBucketDrop(bucketId, dropId);

        String hashInput = createDTO.getName();
        hashInput += Float.toString(createDTO.getLongitude());
        hashInput += Float.toString(createDTO.getLatitude());

        String hash = MD5Util.md5Hex(hashInput);

        // Generate a hash for the place name
        Place place = placeDao.findByHash(hash);
        if (place == null) {
            place = new Place();
            place.setPlaceName(createDTO.getName());
            place.setPlaceNameCanonical(createDTO.getName().toLowerCase());
            place.setHash(hash);
            place.setLatitude(createDTO.getLatitude());
            place.setLongitude(createDTO.getLongitude());

            placeDao.create(place);
        } else {
            if (bucketDropDao.findPlace(bucketDrop, place) != null) {
                throw new BadRequestException(
                        String.format("Drop %d already has the place %s with coordinates [%f, %f]", dropId,
                                place.getPlaceName(), place.getLatitude(), place.getLongitude()));
            }
        }

        bucketDropDao.addPlace(bucketDrop, place);
        return mapper.map(place, GetPlaceDTO.class);
    }

    /**
     * Deletes the {@link Place} with the id specified in <code>placeId</code>
     * from the {@link BucketDrop} specified in <code>dropId</code>
     * 
     * The request {@link BucketDrop} must be a member of the {@link Bucket}
     * with the ID specified in <code>bucketId</code> else a
     * {@link NotFoundException} is thrown
     * 
     * @param bucketId
     * @param dropId
     * @param placeId
     * @param authUser
     */
    @Transactional
    public void deleteDropPlace(Long bucketId, Long dropId, Long placeId, String authUser) {
        Bucket bucket = getBucket(bucketId);

        if (!isOwner(bucket, authUser))
            throw new ForbiddenException("Permission denied");

        BucketDrop bucketDrop = getBucketDrop(bucketId, dropId);
        Place place = placeDao.findById(placeId);

        if (place == null) {
            throw new NotFoundException(String.format("Place %d does not exist", placeId));
        }

        if (!bucketDropDao.deletePlace(bucketDrop, place)) {
            throw new NotFoundException(String.format("Drop %d does not have place %d", dropId, placeId));
        }
    }

    /**
     * Helper method to retrieve a {@link BucketDrop} record from the database
     * and verify that the retrieved entity belongs to the {@link Bucket}
     * specified in <code>bucket</code>
     * 
     * @param bucketId
     * @param dropId
     * @return
     */
    private BucketDrop getBucketDrop(Long bucketId, Long dropId) {
        BucketDrop bucketDrop = bucketDao.findBucketDrop(bucketId, dropId);

        if (bucketDrop == null) {
            throw new NotFoundException(String.format("Drop %d does is not in bucket %d", dropId, bucketId));
        }

        return bucketDrop;
    }

    /**
     * Filter the given list of buckets returning only those that are visible to
     * the given queryingAccount.
     * 
     * @param buckets
     * @param queryingAccount
     * @return
     */
    public List<Bucket> filterVisible(List<Bucket> buckets, Account queryingAccount) {
        List<Bucket> visible = new ArrayList<Bucket>();

        for (Bucket bucket : buckets) {
            if (isOwner(bucket, queryingAccount) || bucket.isPublished()) {
                visible.add(bucket);
            }
        }

        return visible;
    }

    /**
     * Adds a comment to the {@link BucketDrop} entity specified in
     * <code>dropId</code>. This entity must be associated with the
     * {@link Bucket} entity specified in <code>bucketId</code> otherwise a
     * {@link NotFoundException} will be thrown.
     * 
     * @param bucketId
     * @param dropId
     * @param createDTO
     * @param authUser
     * @return
     */
    @Transactional
    public GetCommentDTO addDropComment(Long bucketId, Long dropId, CreateCommentDTO createDTO, String authUser) {

        if (createDTO.getCommentText() == null || createDTO.getCommentText().trim().length() == 0) {
            throw new BadRequestException("The no comment text specified");
        }

        Bucket bucket = getBucket(bucketId);

        if (!bucket.isPublished() && !isOwner(bucket, authUser))
            throw new ForbiddenException("Permission Denied");

        BucketDrop bucketDrop = getBucketDrop(bucketId, dropId);
        Account account = accountDao.findByUsernameOrEmail(authUser);
        BucketDropComment dropComment = bucketDropDao.addComment(bucketDrop, account, createDTO.getCommentText());

        return mapper.map(dropComment, GetCommentDTO.class);
    }

    /**
     * Get and return the list of {@link BucketDropComment} entities for the
     * {@link BucketDrop} with the ID specified in <code>dropId</code>
     * 
     * @param bucketId
     * @param dropId
     * @param authUser
     * @return
     */
    @Transactional
    public List<GetCommentDTO> getDropComments(Long bucketId, Long dropId, String authUser) {
        Bucket bucket = getBucket(bucketId);

        if (!bucket.isPublished() && !isOwner(bucket, authUser))
            throw new ForbiddenException("Permission Denied");

        BucketDrop bucketDrop = getBucketDrop(bucketId, dropId);
        List<GetCommentDTO> commentsList = new ArrayList<GetCommentDTO>();
        for (BucketDropComment dropComment : bucketDrop.getComments()) {
            GetCommentDTO commentDTO = mapper.map(dropComment, GetCommentDTO.class);
            commentsList.add(commentDTO);
        }

        return commentsList;
    }

    /**
     * Deletes the {@link BucketDropComment} entity specified in
     * <code>commentId</code> from the {@link BucketDrop} entity specified in
     * <code>dropId</code>
     * 
     * @param bucketId
     * @param dropId
     * @param commentId
     * @param authUser
     */
    @Transactional
    public void deleteDropComment(Long bucketId, Long dropId, Long commentId, String authUser) {
        Bucket bucket = getBucket(bucketId);

        if (!isOwner(bucket, authUser))
            throw new ForbiddenException("Permission Denied");

        getBucketDrop(bucketId, dropId);

        if (!bucketDropDao.deleteComment(commentId)) {
            throw new NotFoundException(String.format("Comment %d does not exist", commentId));
        }
    }

    /**
     * Adds a comment to comment the {@link Bucket} with the ID specified in
     * <code>bucketId</code>. If the {@link Bucket} is private i.e.
     * <code>published</code> is <code>FALSE</code>, a permissions check is
     * performed.
     * 
     * The created {@link BucketComment} entity is transformed to DTO for
     * purposes of consumption by {@link BucketsController}
     * 
     * @param bucketId
     * @param createDTO
     * @param authUser
     * @return
     */
    @Transactional
    public GetCommentDTO addBucketComment(Long bucketId, CreateCommentDTO createDTO, String authUser) {
        if (createDTO.getCommentText() == null) {
            throw new BadRequestException("The comment text has not been specified");
        }
        Bucket bucket = getBucket(bucketId);

        if (!bucket.isPublished() && !isOwner(bucket, authUser))
            throw new ForbiddenException("Permission Denied");

        Account account = accountDao.findByUsernameOrEmail(authUser);
        BucketComment bucketComment = bucketDao.addComment(bucket, createDTO.getCommentText(), account);

        return mapper.map(bucketComment, GetCommentDTO.class);
    }

    /**
     * Gets and returns the list of {@link BucketComment} entities for the
     * {@link Bucket} with the ID specified in <code>bucketId</code>
     * 
     * @param bucketId
     * @param authUser
     * @return
     */
    @Transactional
    public List<GetCommentDTO> getBucketComments(Long bucketId, String authUser) {
        Bucket bucket = getBucket(bucketId);

        if (!bucket.isPublished() && !isOwner(bucket, authUser))
            throw new ForbiddenException("Permission Denied");

        List<GetCommentDTO> dtoComments = new ArrayList<GetCommentDTO>();
        for (BucketComment bucketComment : bucket.getComments()) {
            dtoComments.add(mapper.map(bucketComment, GetCommentDTO.class));
        }

        return dtoComments;
    }

    /**
     * Deletes the {@link BucketComment} entity with the ID specified in
     * <code>commentId</code> from the {@link Bucket} with the ID specified in
     * <code>bucketId</code>. If the {@link BucketComment} does not belong to
     * the specified bucket, a {@link NotFoundException} is thrown
     * 
     * @param bucketId
     * @param commentId
     * @param authUser
     */
    @Transactional
    public void deleteBucketComment(Long bucketId, Long commentId, String authUser) {
        Bucket bucket = getBucket(bucketId);

        if (!isOwner(bucket, authUser))
            throw new ForbiddenException("Permission Denied");

        BucketComment bucketComment = bucketCommentDao.findById(commentId);

        if (bucketComment == null || !bucketComment.getBucket().equals(bucket)) {
            throw new NotFoundException(String.format("Comment %d does not exist", commentId));
        }

        bucketCommentDao.delete(bucketComment);
    }

    /**
     * Add custom form fields to a drop
     * 
     * @param bucketId
     * @param dropId
     * @param createDTO
     * @param authUser
     * @return
     */
    @Transactional(readOnly = false)
    public FormValueDTO addDropForm(Long bucketId, Long dropId, FormValueDTO createDTO, String authUser) {

        Bucket bucket = getBucket(bucketId);

        if (!isOwner(bucket, authUser))
            throw new ForbiddenException("Permission denied");

        BucketDrop drop = getBucketDrop(bucketId, dropId);

        BucketDropForm dropForm = mapper.map(createDTO, BucketDropForm.class);
        dropForm.setDrop(drop);
        bucketDropFormDao.create(dropForm);

        return mapper.map(dropForm, FormValueDTO.class);
    }

    /**
     * Modify custom form fields in a drop.
     * 
     * @param bucketId
     * @param dropId
     * @param formId
     * @param modifyFormTo
     * @param name
     * @return
     */
    @Transactional(readOnly = false)
    public FormValueDTO modifyDropForm(Long bucketId, Long dropId, Long formId, ModifyFormValueDTO modifyFormTo,
            String authUser) {

        Bucket bucket = getBucket(bucketId);

        if (!isOwner(bucket, authUser))
            throw new ForbiddenException("Permission denied");

        BucketDropForm dropForm = bucketDropDao.findForm(dropId, formId);

        if (dropForm == null)
            throw new NotFoundException("The specified form was not found");

        mapper.map(modifyFormTo, dropForm);
        bucketDropFormDao.update(dropForm);

        return mapper.map(dropForm, FormValueDTO.class);
    }

    /**
     * Remove custom fields from a drop
     * 
     * @param bucketId
     * @param dropId
     * @param formId
     * @param name
     */
    @Transactional(readOnly = false)
    public void deleteDropForm(Long bucketId, Long dropId, Long formId, String authUser) {
        Bucket bucket = getBucket(bucketId);

        if (!isOwner(bucket, authUser))
            throw new ForbiddenException("Permission denied");

        BucketDropForm dropForm = bucketDropDao.findForm(dropId, formId);

        if (dropForm == null)
            throw new NotFoundException("The specified form was not found");

        bucketDropFormDao.delete(dropForm);
    }

    /**
     * Adds the {@link BucketDrop} with the ID specified in <code>dropId</code>
     * to the list of read drops for the {@link Account} associated with the
     * username specified in <code>authUser</code>
     * 
     * @param bucketId
     * @param dropId
     * @param authUser
     */
    @Transactional(readOnly = false)
    public void markDropAsRead(Long bucketId, Long dropId, String authUser) {
        Bucket bucket = getBucket(bucketId);
        Account account = accountDao.findByUsernameOrEmail(authUser);

        if (!bucket.isPublished() && !this.isOwner(bucket, account)) {
            throw new ForbiddenException("Access denied");
        }

        // Only add a drop to the list if it doesn't exist
        BucketDrop bucketDrop = getBucketDrop(bucketId, dropId);
        if (!bucketDropDao.isRead(bucketDrop, account)) {
            account.getReadBucketDrops().add(bucketDrop);
            accountDao.update(account);
        }

    }

    /**
     * Returns all {@link Bucket} entities that contain the <code>searchTerm</code>
     * in their <code>name</code> or <code>description</code> fields. Only entities
     * with  <code>published = true</code> are returned
     * 
     * @param searchTerm
     * @param count
     * @param page
     * @return
     */
    public List<GetBucketDTO> findBuckets(String searchTerm, int count, int page) {
        List<Bucket> buckets = bucketDao.findAll(searchTerm, count, page);

        List<GetBucketDTO> bucketDTOs = new ArrayList<GetBucketDTO>();
        for (Bucket bucket : buckets) {
            bucketDTOs.add(mapper.map(bucket, GetBucketDTO.class));
        }

        return bucketDTOs;
    }

}