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

Java tutorial

Introduction

Here is the source code for com.ushahidi.swiftriver.core.api.service.RiverService.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.Calendar;
import java.util.Date;
import java.util.List;

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

import com.ushahidi.swiftriver.core.api.controller.RiversController;
import com.ushahidi.swiftriver.core.api.dao.AccountDao;
import com.ushahidi.swiftriver.core.api.dao.ChannelDao;
import com.ushahidi.swiftriver.core.api.dao.LinkDao;
import com.ushahidi.swiftriver.core.api.dao.PlaceDao;
import com.ushahidi.swiftriver.core.api.dao.RiverCollaboratorDao;
import com.ushahidi.swiftriver.core.api.dao.RiverDao;
import com.ushahidi.swiftriver.core.api.dao.RiverDropDao;
import com.ushahidi.swiftriver.core.api.dao.RiverDropFormDao;
import com.ushahidi.swiftriver.core.api.dao.RuleDao;
import com.ushahidi.swiftriver.core.api.dao.TagDao;
import com.ushahidi.swiftriver.core.api.dto.ChannelUpdateNotification;
import com.ushahidi.swiftriver.core.api.dto.CreateChannelDTO;
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.CreateRiverDTO;
import com.ushahidi.swiftriver.core.api.dto.CreateRuleDTO;
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.GetChannelDTO;
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.GetPlaceTrend;
import com.ushahidi.swiftriver.core.api.dto.GetRiverDTO;
import com.ushahidi.swiftriver.core.api.dto.GetRuleDTO;
import com.ushahidi.swiftriver.core.api.dto.GetTagTrend;
import com.ushahidi.swiftriver.core.api.dto.ModifyChannelDTO;
import com.ushahidi.swiftriver.core.api.dto.ModifyCollaboratorDTO;
import com.ushahidi.swiftriver.core.api.dto.ModifyFormValueDTO;
import com.ushahidi.swiftriver.core.api.dto.ModifyRiverDTO;
import com.ushahidi.swiftriver.core.api.dto.RuleUpdateNotification;
import com.ushahidi.swiftriver.core.api.exception.BadRequestException;
import com.ushahidi.swiftriver.core.api.exception.ErrorField;
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.api.filter.TrendFilter;
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.BucketDrop;
import com.ushahidi.swiftriver.core.model.Channel;
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.River;
import com.ushahidi.swiftriver.core.model.RiverCollaborator;
import com.ushahidi.swiftriver.core.model.RiverDrop;
import com.ushahidi.swiftriver.core.model.RiverDropComment;
import com.ushahidi.swiftriver.core.model.RiverDropForm;
import com.ushahidi.swiftriver.core.model.RiverTagTrend;
import com.ushahidi.swiftriver.core.model.Rule;
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.ErrorUtil;
import com.ushahidi.swiftriver.core.util.MD5Util;

@Service
@Transactional(readOnly = true)
public class RiverService {

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

    @Autowired
    private RiverDao riverDao;

    @Autowired
    private AccountDao accountDao;

    @Autowired
    private AccountService accountService;

    @Autowired
    private ChannelDao channelDao;

    @Autowired
    private RiverCollaboratorDao riverCollaboratorDao;

    @Autowired
    private Mapper mapper;

    @Autowired
    private RiverDropDao riverDropDao;

    @Autowired
    private RiverDropFormDao riverDropFormDao;

    @Autowired
    private RuleDao ruleDao;

    @Autowired
    private TagDao tagDao;

    @Autowired
    private LinkDao linkDao;

    @Autowired
    private PlaceDao placeDao;

    @Autowired
    private AmqpTemplate amqpTemplate;

    @Autowired
    private DropDocumentRepository repository;

    private int dropQuota;

    public void setRiverDao(RiverDao riverDao) {
        this.riverDao = riverDao;
    }

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

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

    public ChannelDao getChannelDao() {
        return channelDao;
    }

    public void setChannelDao(ChannelDao channelDao) {
        this.channelDao = channelDao;
    }

    public void setRiverCollaboratorDao(RiverCollaboratorDao riverCollaboratorDao) {
        this.riverCollaboratorDao = riverCollaboratorDao;
    }

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

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

    public void setRiverDropFormDao(RiverDropFormDao riverDropFormDao) {
        this.riverDropFormDao = riverDropFormDao;
    }

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

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

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

    public void setAmqpTemplate(AmqpTemplate amqpTemplate) {
        this.amqpTemplate = amqpTemplate;
    }

    public void setDropQuota(int dropQuota) {
        this.dropQuota = dropQuota;
    }

    /**
     * Creates a new River
     * 
     * @param riverTO
     * @return
     */
    @Transactional(readOnly = false)
    public GetRiverDTO createRiver(CreateRiverDTO riverTO, String authUser) {
        Account account = accountDao.findByUsernameOrEmail(authUser);

        if (!(account.getRiverQuotaRemaining() > 0))
            throw new ForbiddenException("River quota exceeded");

        if (riverDao.findByName(riverTO.getRiverName()) != null) {
            BadRequestException ex = new BadRequestException("Duplicate river name");
            List<ErrorField> errors = new ArrayList<ErrorField>();
            errors.add(new ErrorField("name", "duplicate"));
            ex.setErrors(errors);
            throw ex;
        }

        River river = mapper.map(riverTO, River.class);
        river.setAccount(account);
        river.setActive(Boolean.TRUE);
        river.setDropQuota(dropQuota);
        riverDao.create(river);

        accountDao.decreaseRiverQuota(account, 1);

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

        return mapper.map(river, GetRiverDTO.class);
    }

    /**
     * Modify an existing river.
     * 
     * @param riverId
     * @param modifyRiverTO
     * @param authUser
     * @return
     */
    @Transactional(readOnly = false)
    public GetRiverDTO modifyRiver(Long riverId, ModifyRiverDTO modifyRiverTO, String authUser) {
        River river = getRiver(riverId);
        Account account = accountDao.findByUsernameOrEmail(authUser);

        if (!isOwner(river, account))
            throw new ForbiddenException("Authenticated user does not own the river");

        if (modifyRiverTO.getRiverName() != null && !modifyRiverTO.getRiverName().equals(river.getRiverName())) {
            if (riverDao.findByName(modifyRiverTO.getRiverName()) != null) {
                BadRequestException ex = new BadRequestException("Duplicate river name");
                List<ErrorField> errors = new ArrayList<ErrorField>();
                errors.add(new ErrorField("name", "duplicate"));
                ex.setErrors(errors);
                throw ex;
            }
        }

        mapper.map(modifyRiverTO, river);
        riverDao.update(river);

        return mapper.map(river, GetRiverDTO.class);
    }

    /**
     * Get a RiverDTO for the River with the given id
     * 
     * @param id
     * @return
     * @throws NotFoundException
     */
    public GetRiverDTO getRiverById(Long id) throws NotFoundException {
        River river = getRiver(id);

        return mapper.map(river, GetRiverDTO.class);
    }

    /**
     * Add a channel to the given river.
     * 
     * @param riverId
     * @param createChannelTO
     * @return
     */
    public GetChannelDTO createChannel(Long riverId, CreateChannelDTO createChannelTO) {
        River river = getRiver(riverId);

        Channel channel = mapper.map(createChannelTO, Channel.class);
        channel.setRiver(river);
        channel.setActive(Boolean.TRUE);
        channelDao.create(channel);

        // Construct the routing key
        String routingKey = String.format("web.channel.%s.add", channel.getChannel().toLowerCase());

        ChannelUpdateNotification notification = new ChannelUpdateNotification();
        notification.setId(channel.getId());
        notification.setChannel(channel.getChannel());
        notification.setRiverId(riverId);
        notification.setParameters(channel.getParameters());
        amqpTemplate.convertAndSend(routingKey, notification);

        logger.debug("Sending {} message for new '{}' parameter", routingKey, channel.getParameters());

        return mapper.map(channel, GetChannelDTO.class);
    }

    /**
     * @param riverId
     * @param channelId
     */
    @Transactional(readOnly = false)
    public void deleteChannel(Long riverId, Long channelId, String authUser) {
        Channel channel = getRiverChannel(riverId, channelId, authUser);

        int channelDropCount = channel.getDropCount();
        River river = channel.getRiver();

        channelDao.delete(channel);

        // Update the river drop count
        logger.debug("Reducing the drop count of river {} by {} drops", riverId, channelDropCount);

        river.setDropCount(river.getDropCount() - channelDropCount);
        riverDao.update(river);

        // Construct the routing key
        String routingKey = String.format("web.channel.%s.delete", channel.getChannel());

        ChannelUpdateNotification notification = new ChannelUpdateNotification();
        notification.setId(channelId);
        notification.setChannel(channel.getChannel());
        notification.setRiverId(riverId);
        notification.setParameters(channel.getParameters());
        amqpTemplate.convertAndSend(routingKey, notification);

        logger.debug("Sending {} message for deleted '{}' parameter", routingKey, channel.getParameters());
    }

    @Transactional(readOnly = false)
    public GetChannelDTO modifyChannel(Long riverId, Long channelId, ModifyChannelDTO modifyChannelTO,
            String authUser) {
        Channel channel = getRiverChannel(riverId, channelId, authUser);

        // Get the channel before modification for a deletion notification
        ChannelUpdateNotification beforeNotification = new ChannelUpdateNotification();
        beforeNotification.setId(channelId);
        beforeNotification.setChannel(channel.getChannel());
        beforeNotification.setRiverId(riverId);
        beforeNotification.setParameters(channel.getParameters());

        mapper.map(modifyChannelTO, channel);
        channelDao.update(channel);

        // Get the channel after modification for an add notification
        ChannelUpdateNotification afterNotification = new ChannelUpdateNotification();
        afterNotification.setId(channelId);
        afterNotification.setChannel(channel.getChannel());
        afterNotification.setRiverId(riverId);
        afterNotification.setParameters(channel.getParameters());

        amqpTemplate.convertAndSend("web.channel." + beforeNotification.getChannel() + ".delete",
                beforeNotification);
        amqpTemplate.convertAndSend("web.channel." + afterNotification.getChannel() + ".add", afterNotification);

        return mapper.map(channel, GetChannelDTO.class);
    }

    /**
     * Gets and returns the {@link Channel} with the specified <code>channelId</code>
     * for the {@link River} with the specified <code>river</code>
     * 
     * @param riverId   the unique id of the river with the desired channel
     * @param channelId the unique id of the channel
     * @param authUser  the username of the authenticating user 
     * @return
     */
    public Channel getRiverChannel(Long riverId, long channelId, String authUser) {
        Channel channel = channelDao.findById(channelId);

        if (channel == null)
            throw new NotFoundException("The given channel was not found");

        River river = channel.getRiver();
        if (!river.getId().equals(riverId))
            throw new NotFoundException("The given river does not countain the given channel.");

        Account account = accountDao.findByUsernameOrEmail(authUser);

        if (!isOwner(river, account))
            throw new ForbiddenException("Logged in user does not own the river.");

        return channel;
    }

    /**
     * Returns the drops for the river with the ID specified in <code>id</code>
     * using the {@link DropFilter} specified in <code>dropFilter</code>
     * 
     * @param id the unique id of the river
     * @param dropFilter the filters to be used to fetch the drops
     * @param page the page number
     * @param dropCount the maximum no. of drops to return
     * @param username the login ID of the user accessing the river
     * @return
     * @throws NotFoundException
     */
    public List<GetDropDTO> getDrops(Long id, DropFilter dropFilter, int page, int dropCount, String username)
            throws NotFoundException {

        // Get the river
        River river = getRiver(id);

        // Get the querying account
        Account queryingAccount = accountDao.findByUsernameOrEmail(username);

        if (!hasAccess(river, queryingAccount))
            throw new ForbiddenException("Access denied");

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

        // Farm fulltext and geospatial search to Solr
        if (dropFilter.getKeywords() != null || dropFilter.getBoundingBox() != null) {

            PageRequest pageRequest = new PageRequest(page - 1, dropCount);
            List<DropDocument> dropDocuments = repository.findInRiver(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);
        }

        // Get the drops
        List<Drop> drops = riverDao.getDrops(id, dropFilter, page, dropCount, queryingAccount);

        if (drops == null) {
            throw new NotFoundException("No drops found");
        }

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

    /**
     * Deletes a river
     * 
     * @param id
     */
    @Transactional(readOnly = false)
    public boolean deleteRiver(Long id, String authUser) {
        River river = getRiver(id);

        Account account = accountDao.findByUsernameOrEmail(authUser);

        // Only the creator can delete the river
        if (!river.getAccount().equals(account)) {
            throw new ForbiddenException("Access denied");
        }

        // Delete the river
        riverDao.delete(river);

        // Update the river remaining quota
        accountDao.increaseRiverQuota(account, 1);

        return true;
    }

    /**
     * Get collaborators of the given river
     * 
     * @param riverId
     * @return
     * @throws NotFoundException
     */
    public List<GetCollaboratorDTO> getCollaborators(Long riverId) throws NotFoundException {
        River river = getRiver(riverId);

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

        for (RiverCollaborator collaborator : river.getCollaborators()) {
            collaborators.add(mapCollaboratorDTO(collaborator));
        }

        return collaborators;
    }

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

        // Check if the river exists
        River river = getRiver(riverId);

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

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

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

        RiverCollaborator collaborator = riverDao.addCollaborator(river, account,
                createCollaboratorTO.isReadOnly());

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

        return mapCollaboratorDTO(collaborator);
    }

    /**
     * Modifies a collaborator
     * 
     * @param riverId
     * @param accountId
     * @param modifyCollaboratorTO
     * @return
     */
    @Transactional(readOnly = false)
    public GetCollaboratorDTO modifyCollaborator(Long riverId, Long accountId,
            ModifyCollaboratorDTO modifyCollaboratorTO, String authUser) {

        River river = getRiver(riverId);

        Account authAccount = accountDao.findByUsernameOrEmail(authUser);

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

        RiverCollaborator collaborator = riverDao.findCollaborator(riverId, 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
        riverDao.updateCollaborator(collaborator);

        return mapCollaboratorDTO(collaborator);
    }

    /**
     * Removes a collaborator in <code>accountId</code> from the river specified
     * in <code>riverId</code>. <code>accountId</code> is the {@link Account} id
     * of the collaborator
     * 
     * @param riverId the unique id of the <code>River</code>
     * @param accountId the unique id of the collaborating <code>Account</code>
     * @param authUser the username of the authenticated user
     */
    @Transactional
    public void deleteCollaborator(Long riverId, Long accountId, String authUser) {

        River river = getRiver(riverId);

        Account authAccount = accountDao.findByUsernameOrEmail(authUser);

        RiverCollaborator collaborator = riverDao.findCollaborator(riverId, accountId);
        if (collaborator == null)
            throw new NotFoundException("Collaborator not found.");

        // Check if the collaborator's account is the same as
        // the authenticating account
        if (!collaborator.getAccount().equals(authAccount)) {
            if (!isOwner(river, authAccount))
                throw new ForbiddenException("Permission denied.");
        }

        riverCollaboratorDao.delete(collaborator);
    }

    private GetCollaboratorDTO mapCollaboratorDTO(RiverCollaborator collaborator) {
        GetCollaboratorDTO collaboratorDTO = mapper.map(collaborator.getAccount(), GetCollaboratorDTO.class);
        collaboratorDTO.setActive(collaborator.isActive());
        collaboratorDTO.setReadOnly(collaborator.isReadOnly());

        return collaboratorDTO;
    }

    /**
     * Adds a follower to the specified river
     * 
     * @param id
     * @param accountId
     * @return
     */
    @Transactional
    public void addFollower(Long id, Long accountId) {
        // Does the river exist?
        River river = getRiver(id);

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

        river.getFollowers().add(account);
        riverDao.update(river);

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

    /**
     * Gets and returns a list of {@link Account} entities that are following
     * the river identified by <code>id</code>. The entities are transformed to
     * DTO for purposes of consumption by {@link RiversController}.
     * 
     * <code>accountId</code> can be null. When specified, the method verifies
     * that the {@link Account} associated with it is following the river. If
     * following, the return list contains only a single {@link FollowerDTO}
     * object else, a {@link NotFoundException} is thrown
     * 
     * @param id
     * @param accountId
     * @return
     */
    @Transactional
    public List<FollowerDTO> getFollowers(Long id, Long accountId) {
        River river = getRiver(id);

        List<FollowerDTO> followerList = new ArrayList<FollowerDTO>();
        if (accountId != null) {
            Account account = accountDao.findById(accountId);
            if (account == null) {
                throw new NotFoundException(String.format("Account %d does not exist", accountId));
            }

            if (river.getFollowers().contains(account)) {
                followerList.add(mapFollowerDTO(account));
            } else {
                throw new NotFoundException(String.format("Account %d does not follow river %d", accountId, id));
            }
        } else {
            for (Account account : river.getFollowers()) {
                followerList.add(mapFollowerDTO(account));
            }
        }

        return followerList;
    }

    /**
     * Helper method for transforming an {@link Account} entity to a
     * {@link FollowerDTO} object
     * 
     * @param account
     * @return
     */
    private FollowerDTO mapFollowerDTO(Account account) {
        FollowerDTO accountDto = mapper.map(account, FollowerDTO.class);

        accountDto.setName(account.getOwner().getName());
        accountDto.setEmail(account.getOwner().getEmail());

        return accountDto;
    }

    /**
     * Deletes the follower whose {@link Account} id is <code>accountId</code>
     * from the river specified by <code>riverId</code>
     * 
     * @param riverId
     * @param accountId
     */
    @Transactional
    public void deleteFollower(Long riverId, Long accountId) {
        // Load the river and check if it exists
        River river = getRiver(riverId);

        // Load the account and check if it exists
        Account account = accountDao.findById(accountId);
        if (account == null) {
            throw new NotFoundException("Account not found");
        }

        river.getFollowers().remove(account);
        riverDao.update(river);
    }

    /**
     * Deletes the drop specified by <code>dropId</code> from the river in
     * <code>id</code>
     * 
     * @param id
     * @param dropId
     * @param authUser
     */
    @Transactional(readOnly = false)
    public void deleteDrop(Long id, Long dropId, String authUser) {
        River river = getRiver(id);
        if (!isOwner(river, authUser)) {
            throw new ForbiddenException("Permission denied");
        }

        RiverDrop riverDrop = getRiverDrop(id, dropId);

        // Update the river drop count
        river.setDropCount(river.getDropCount() - 1);
        riverDao.update(river);

        // Update the channel drop count
        Channel channel = riverDrop.getChannel();
        channel.setDropCount(channel.getDropCount() - 1);
        channelDao.update(channel);

        // Delete the river drop
        riverDropDao.delete(riverDrop);
    }

    public boolean isOwner(River river, String authUser) {
        Account account = accountDao.findByUsernameOrEmail(authUser);
        return isOwner(river, account);
    }

    public boolean isOwner(River river, Account account) {
        RiverCollaborator collaborator = riverDao.findCollaborator(river.getId(), account.getId());

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

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

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

    private River getRiver(Long id) {
        River river = riverDao.findById(id);
        if (river == null) {
            throw new NotFoundException(String.format("River with id %d not found", id));
        }

        return river;
    }

    /**
     * Adds a {@link Tag} to the {@link RiverDrop} with the specified
     * <code>dropId</code> The drop must be in the {@link River} whose ID is
     * specified in <code>id</code>
     * 
     * The created {@link Tag} entity is transformed to a DTO for purposes of
     * consumption by {@link RiversController}
     * 
     * @param id
     * @param dropId
     * @param createDTO
     * @param name
     * @return
     */
    @Transactional
    public GetTagDTO addDropTag(Long riverId, Long dropId, CreateTagDTO createDTO, String authUser) {

        River river = getRiver(riverId);

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

        // Get the bucket drop
        RiverDrop riverDrop = getRiverDrop(riverId, 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 (riverDropDao.findTag(riverDrop, tag) != null) {
                throw new BadRequestException(String.format("Tag %s of type %s has already been added to drop %d",
                        tag.getTag(), tag.getType(), dropId));
            }
        }

        riverDropDao.addTag(riverDrop, tag);
        return mapper.map(tag, GetTagDTO.class);
    }

    /**
     * Deletes the {@link Tag} with the id specified in <code>tagId</code> from
     * the {@link RiverDrop} specified in <code>dropId</code>
     * 
     * The request {@link BucketDrop} must be a member of the {@link River} with
     * the ID specified in <code>id</code> else a {@link NotFoundException} is
     * thrown
     * 
     * @param riverId
     * @param dropId
     * @param tagId
     * @param authUser
     */
    @Transactional
    public void deleteDropTag(Long riverId, Long dropId, Long tagId, String authUser) {
        River river = getRiver(riverId);
        if (!isOwner(river, authUser))
            throw new ForbiddenException("Permission denied");

        RiverDrop riverDrop = getRiverDrop(riverId, dropId);

        Tag tag = tagDao.findById(tagId);

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

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

    }

    /**
     * Adds a {@link Link} to the {@link RiverDrop} with the specified
     * <code>dropId</code> The drop must be in the {@link River} whose ID is
     * specified in <code>id</code>
     * 
     * The created {@link Link} entity is transformed to a DTO for purposes of
     * consumption by {@link RiversController}
     * 
     * @param id
     * @param dropId
     * @param createDTO
     * @param authUser
     * @return
     */
    @Transactional
    public GetLinkDTO addDropLink(Long riverId, Long dropId, CreateLinkDTO createDTO, String authUser) {
        River river = getRiver(riverId);

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

        RiverDrop riverDrop = getRiverDrop(riverId, 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 (riverDropDao.findLink(riverDrop, link) != null) {
                throw new BadRequestException(
                        String.format("%s has already been added to drop %d", link.getUrl(), dropId));
            }
        }

        riverDropDao.addLink(riverDrop, link);
        return mapper.map(link, GetLinkDTO.class);
    }

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

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

        RiverDrop riverDrop = getRiverDrop(riverId, dropId);
        Link link = linkDao.findById(linkId);

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

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

    /**
     * Adds a {@link Place} to the {@link RiverDrop} with the specified
     * <code>dropId</code> The drop must be in the {@link River} whose ID is
     * specified in <code>id</code>
     * 
     * The created {@link Place} entity is transformed to a DTO for purposes of
     * consumption by {@link RiversController}
     * 
     * @param riverId
     * @param dropId
     * @param createDTO
     * @param authUser
     * @return
     */
    @Transactional
    public GetPlaceDTO addDropPlace(Long riverId, Long dropId, CreatePlaceDTO createDTO, String authUser) {
        River river = getRiver(riverId);

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

        RiverDrop riverDrop = getRiverDrop(riverId, 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.setLatitude(createDTO.getLatitude());
            place.setLongitude(createDTO.getLongitude());

            placeDao.create(place);
        } else {
            if (riverDropDao.findPlace(riverDrop, 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()));
            }
        }

        riverDropDao.addPlace(riverDrop, place);
        return mapper.map(place, GetPlaceDTO.class);
    }

    /**
     * Deletes the {@link Link} with the id specified in <code>linkId</code>
     * from the {@link RiverDrop} specified in <code>dropId</code>
     * 
     * The request {@link RiverDrop} must be a member of the {@link Bucket} with
     * the ID specified in <code>id</code> else a {@link NotFoundException} is
     * thrown
     * 
     * @param riverId
     * @param dropId
     * @param placeId
     * @param authUser
     */
    @Transactional
    public void deleteDropPlace(Long riverId, Long dropId, Long placeId, String authUser) {
        River river = getRiver(riverId);

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

        RiverDrop riverDrop = getRiverDrop(riverId, dropId);
        Place place = placeDao.findById(placeId);

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

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

    }

    /**
     * Helper method to retrieve a {@link RiverDrop} record from the database
     * and verify that the retrieved entity belongs to the {@link River}
     * specified in <code>river</code>
     * 
     * @param riverId
     * @param dropId
     * @return
     */
    private RiverDrop getRiverDrop(Long riverId, Long dropId) {
        RiverDrop riverDrop = riverDao.findRiverDrop(riverId, dropId);

        if (riverDrop == null) {
            throw new NotFoundException(String.format("Drop %d does not exist in river %d", dropId, riverId));
        }

        return riverDrop;
    }

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

        for (River river : rivers) {
            if (isOwner(river, queryingAccount) || river.getRiverPublic()) {
                visible.add(river);
            }
        }

        return visible;
    }

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

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

        River river = getRiver(riverId);

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

        RiverDrop riverDrop = getRiverDrop(riverId, dropId);
        Account account = accountDao.findByUsernameOrEmail(authUser);
        RiverDropComment dropComment = riverDropDao.addComment(riverDrop, account, createDTO.getCommentText());

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

    /**
     * Get and return the list of {@link RiverDropComment} entities for the
     * {@link RiverDrop} with the ID specified in <code>dropId</code>
     * 
     * @param riverId
     * @param dropId
     * @param authUser
     * @return
     */
    public List<GetCommentDTO> getDropComments(Long riverId, Long dropId, String authUser) {
        River river = getRiver(riverId);

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

        RiverDrop riverDrop = getRiverDrop(riverId, dropId);
        List<GetCommentDTO> commentsList = new ArrayList<GetCommentDTO>();
        for (RiverDropComment dropComment : riverDrop.getComments()) {
            GetCommentDTO commentDTO = mapper.map(dropComment, GetCommentDTO.class);
            commentsList.add(commentDTO);
        }

        return commentsList;
    }

    /**
     * Deletes the {@link RiverDropComment} entity specified in
     * <code>commentId</code> from the {@link RiverDrop} entity specified in
     * <code>dropId</code>
     * 
     * @param riverId
     * @param dropId
     * @param commentId
     * @param authUser
     */
    public void deleteDropComment(Long riverId, Long dropId, Long commentId, String authUser) {
        River river = getRiver(riverId);

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

        getRiverDrop(riverId, dropId);

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

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

        River river = getRiver(riverId);

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

        RiverDrop drop = getRiverDrop(riverId, dropId);

        RiverDropForm dropForm = mapper.map(createDTO, RiverDropForm.class);
        dropForm.setDrop(drop);

        try {
            riverDropFormDao.create(dropForm);
        } catch (DataIntegrityViolationException e) {
            throw ErrorUtil.getBadRequestException("id", "duplicate");
        }

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

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

        River river = getRiver(riverId);

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

        RiverDropForm dropForm = riverDropDao.findForm(dropId, formId);

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

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

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

    /**
     * Remove custom fields from a drop
     * 
     * @param riverId
     * @param dropId
     * @param formId
     * @param name
     */
    @Transactional(readOnly = false)
    public void deleteDropForm(Long riverId, Long dropId, Long formId, String authUser) {
        River river = getRiver(riverId);

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

        RiverDropForm dropForm = riverDropDao.findForm(dropId, formId);

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

        riverDropFormDao.delete(dropForm);
    }

    public List<GetRuleDTO> getRules(Long riverId, String authUser) {
        River river = getRiver(riverId);
        if (!isOwner(river, authUser)) {
            throw new ForbiddenException("Permission denied");
        }

        List<GetRuleDTO> rulesDTOList = new ArrayList<GetRuleDTO>();
        for (Rule rule : river.getRules()) {
            rulesDTOList.add(mapper.map(rule, GetRuleDTO.class));
        }

        return rulesDTOList;
    }

    /**
     * Creates a new {@link Rule} for the {@link River} with the ID specified in
     * <code>riverId</code>. The created entity is transformed to DTO for purposes
     * of consumption by {@link RiversController}
     * 
     * @param riverId
     * @param createRuleDTO
     * @param authUser
     * @return
     */
    @Transactional(readOnly = false)
    public GetRuleDTO addRule(Long riverId, CreateRuleDTO createRuleDTO, String authUser) {
        River river = getRiver(riverId);
        if (!isOwner(river, authUser)) {
            throw new ForbiddenException("Permission denied");
        }

        Rule rule = mapper.map(createRuleDTO, Rule.class);
        rule.setRiver(river);
        rule.setDateAdded(new Date());

        ruleDao.create(rule);

        // Send add_rule message on the MQ
        RuleUpdateNotification notification = mapper.map(rule, RuleUpdateNotification.class);
        amqpTemplate.convertAndSend("web.river.rules.add", notification);

        return mapper.map(rule, GetRuleDTO.class);
    }

    /**
     * Modifies the {@link RiverRule} specified in <code>ruleId</code>. 
     * The {@link RiverRule} must belong to the {@link River} with the ID
     * specified in <code>riverId</code> else a {@link NotFoundException} will
     * be thrown.
     * 
     * @param riverId
     * @param ruleId
     * @param createRuleDTO
     * @param authUser
     * @return
     */
    @Transactional(readOnly = false)
    public GetRuleDTO modifyRule(Long riverId, Long ruleId, CreateRuleDTO createRuleDTO, String authUser) {
        River river = getRiver(riverId);
        if (!isOwner(river, authUser)) {
            throw new ForbiddenException("Permission denied");
        }

        Rule rule = ruleDao.findById(ruleId);
        if (rule == null || (rule != null && !rule.getRiver().equals(river))) {
            throw new NotFoundException(String.format("Rule %d not found", ruleId));
        }

        mapper.map(createRuleDTO, rule);
        ruleDao.update(rule);

        RuleUpdateNotification notification = mapper.map(rule, RuleUpdateNotification.class);

        amqpTemplate.convertAndSend("web.river.rules.update", notification);

        return mapper.map(rule, GetRuleDTO.class);
    }

    /**
     * Deletes the {@link RiverRule} with the ID specified in <code>ruleId</code>
     * The {@link RiverRule} must belong to the {@link River} with the ID specified
     * in <code>riverId</code> else a {@link NotFoundException} will be thrown
     * 
     * @param riverId
     * @param ruleId
     * @param authUser
     */
    @Transactional(readOnly = false)
    public void deleteRule(Long riverId, Long ruleId, String authUser) {
        River river = getRiver(riverId);
        if (!isOwner(river, authUser)) {
            throw new ForbiddenException("Permission denied");
        }

        Rule rule = ruleDao.findById(ruleId);
        if (rule == null || (rule != null && !rule.getRiver().equals(river))) {
            throw new NotFoundException(String.format("Rule %d not found", ruleId));
        }

        RuleUpdateNotification notification = mapper.map(rule, RuleUpdateNotification.class);

        ruleDao.delete(rule);

        amqpTemplate.convertAndSend("web.river.rules.delete", notification);
    }

    /**
     * Adds the {@link RiverDrop} with the ID specified in <code>dropId</code>
     * to the list of read drops for the river with the ID 
     * specified in <code>riverId</code>.
     * 
     * @param riverId
     * @param dropId
     * @param authUser
     */
    @Transactional(readOnly = false)
    public void markDropAsRead(Long riverId, Long dropId, String authUser) {
        River river = getRiver(riverId);
        Account account = accountDao.findByUsernameOrEmail(authUser);
        if (!river.getRiverPublic() && !this.isOwner(river, account)) {
            throw new ForbiddenException("Access denied");
        }

        RiverDrop riverDrop = getRiverDrop(riverId, dropId);
        // Only add drop to the list if it doesn't exist
        if (!riverDropDao.isRead(riverDrop, account)) {
            account.getReadRiverDrops().add(riverDrop);
            accountDao.update(account);
        }
    }

    /**
     * Returns all {@link River} entities that contain the <code>searchTerm</code>
     * in their <code>name</code> or <code>description</code> fields. Only entities
     * with  <code>isPublic = true</code> are returned
     * 
     * @param searchTerm
     * @param count
     * @param page
     * @return
     */
    public List<GetRiverDTO> findRivers(String searchTerm, int count, int page) {
        List<River> rivers = riverDao.findAll(searchTerm, count, page);

        List<GetRiverDTO> riverDTOs = new ArrayList<GetRiverDTO>();
        for (River river : rivers) {
            riverDTOs.add(mapper.map(river, GetRiverDTO.class));
        }

        return riverDTOs;
    }

    /**
     * Returns the list of trending tags for the river with the ID specified
     * in <code>riverId</code>
     * 
     * @param riverId
     * @param trendFilter
     * @param authUser
     * @return
     */
    public List<GetTagTrend> getTrendingTags(Long riverId, TrendFilter trendFilter, String authUser) {

        River river = getRiver(riverId);
        Account queryingAccount = accountDao.findByUsernameOrEmail(authUser);

        if (!hasAccess(river, queryingAccount)) {
            throw new ForbiddenException("Permission denied");
        }

        // If no dates specified, get data for the last 1 week
        if (trendFilter.getDateFrom() == null && trendFilter.getDateTo() == null) {
            trendFilter.setDateTo(new Date());

            Calendar now = Calendar.getInstance();
            now.add(Calendar.DATE, -7);

            trendFilter.setDateFrom(now.getTime());
        }

        List<RiverTagTrend> tagTrends = riverDao.getTrendingTags(riverId, trendFilter);
        if (tagTrends == null) {
            throw new NotFoundException(String.format("No trending tags found for river %d", riverId));
        }

        List<GetTagTrend> tagTrendDtos = new ArrayList<GetTagTrend>();
        for (RiverTagTrend trend : tagTrends) {
            GetTagTrend trendDto = mapper.map(trend, GetTagTrend.class);
            tagTrendDtos.add(trendDto);
        }

        return tagTrendDtos;
    }

    /**
     * Returns the list of trending places in the river with the ID specified
     * in <code>riverId</code>
     * 
     * @param riverId
     * @param trendFilter
     * @param authUser
     * @return
     */
    public List<GetPlaceTrend> getTredingPlaces(Long riverId, TrendFilter trendFilter, String authUser) {
        River river = getRiver(riverId);
        Account queryingAccount = accountDao.findByUsernameOrEmail(authUser);

        if (!hasAccess(river, queryingAccount)) {
            throw new ForbiddenException("Permission denied");
        }

        // If no dates specified, get data for the last 1 week
        if (trendFilter.getDateFrom() == null && trendFilter.getDateTo() == null) {
            trendFilter.setDateTo(new Date());

            Calendar now = Calendar.getInstance();
            now.add(Calendar.DATE, -7);

            trendFilter.setDateFrom(now.getTime());
        }

        List<RiverTagTrend> placeTrends = riverDao.getTrendingPlaces(riverId, trendFilter);
        if (placeTrends == null) {
            throw new NotFoundException(String.format("No trending places found for river %d", riverId));
        }

        List<GetPlaceTrend> placeTrendDtos = new ArrayList<GetPlaceTrend>();
        for (RiverTagTrend trend : placeTrends) {
            GetPlaceTrend trendDto = mapper.map(trend, GetPlaceTrend.class);
            placeTrendDtos.add(trendDto);
        }

        return placeTrendDtos;
    }

}