com.bloatit.data.DaoFeature.java Source code

Java tutorial

Introduction

Here is the source code for com.bloatit.data.DaoFeature.java

Source

//
// Copyright (c) 2011 Linkeos.
//
// This file is part of Elveos.org.
// Elveos.org is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the
// Free Software Foundation, either version 3 of the License, or (at your
// option) any later version.
//
// Elveos.org is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
// You should have received a copy of the GNU General Public License along
// with Elveos.org. If not, see http://www.gnu.org/licenses/.
//
package com.bloatit.data;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.NoSuchElementException;

import javax.persistence.Basic;
import javax.persistence.Cacheable;
import javax.persistence.Entity;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;

import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.CascadeType;
import org.hibernate.annotations.NamedQueries;
import org.hibernate.annotations.NamedQuery;
import org.hibernate.annotations.OrderBy;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.FullTextFilterDef;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.IndexedEmbedded;
import org.hibernate.search.annotations.Store;

import com.bloatit.common.Log;
import com.bloatit.data.DaoEvent.EventType;
import com.bloatit.data.DaoTeamRight.UserTeamRight;
import com.bloatit.data.exceptions.NotEnoughMoneyException;
import com.bloatit.data.queries.EmptyPageIterable;
import com.bloatit.data.queries.QueryCollection;
import com.bloatit.data.search.DaoFeatureSearchFilterFactory;
import com.bloatit.framework.exceptions.highlevel.BadProgrammerException;
import com.bloatit.framework.exceptions.lowlevel.NonOptionalParameterException;
import com.bloatit.framework.utils.PageIterable;
import com.bloatit.framework.utils.i18n.Language;

/**
 * A DaoFeature is a kudosable content. It has a translatable description, and
 * can have a specification and some offers. The state of the feature is managed
 * by its super class DaoKudosable. On a feature we can add some comment and
 * some contributions.
 */
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@Indexed
@FullTextFilterDef(name = "searchFilter", impl = DaoFeatureSearchFilterFactory.class)
//@formatter:off
@NamedQueries(value = { @NamedQuery(name = "feature.getOffers.bySelected", query = "FROM DaoOffer "
        + "WHERE feature = :this " + "AND state <= :state " + // <= PENDING and VALIDATED.
        "AND popularity = (select max(popularity) from DaoOffer where feature = :this) " + //
        "AND popularity >= 0 " + "ORDER BY amount ASC, creationDate DESC"),

        @NamedQuery(name = "feature.getAmounts.min", query = "SELECT min(amount) " + "FROM DaoContribution "
                + "WHERE feature = :this " + "AND state != :state "),
        @NamedQuery(name = "feature.getAmounts.max", query = "SELECT max(amount) " + "FROM DaoContribution "
                + "WHERE feature = :this " + "AND state != :state "),

        @NamedQuery(name = "feature.getAmounts.avg", query = "SELECT avg(amount) " + "FROM DaoContribution "
                + "WHERE feature = :this " + "AND state != :state "),

        @NamedQuery(name = "feature.getBugs.byNonState", query = "SELECT bugs_ "
                + "FROM com.bloatit.data.DaoOffer offer_ " + "JOIN offer_.milestones as bs "
                + "JOIN bs.bugs as bugs_ " + "WHERE offer_ = :offer " + "AND bugs_.state != :state "),

        @NamedQuery(name = "feature.getBugs.byNonState.size", query = "SELECT count(bugs_) "
                + "FROM com.bloatit.data.DaoOffer offer_ " + "JOIN offer_.milestones as bs "
                + "JOIN bs.bugs as bugs_ " + "WHERE offer_ = :offer " + "AND bugs_.state != :state "),

        @NamedQuery(name = "feature.getBugs.byState", query = "SELECT bugs_ "
                + "FROM com.bloatit.data.DaoOffer offer_ " + "JOIN offer_.milestones as bs "
                + "JOIN bs.bugs as bugs_ " + "WHERE offer_ = :offer " + "AND bugs_.state = :state "),

        @NamedQuery(name = "feature.getBugs.byState.size", query = "SELECT count(bugs_) "
                + "FROM com.bloatit.data.DaoOffer offer_ " + "JOIN offer_.milestones as bs "
                + "JOIN bs.bugs as bugs_ " + "WHERE offer_ = :offer "
                + "AND bugs_.state = :state ", readOnly = true),
        @NamedQuery(name = "feature.getComments.size", query = "SELECT count(*) "
                + "FROM com.bloatit.data.DaoComment  " + "WHERE feature = :this " + "OR father.id in ( "
                + "FROM com.bloatit.data.DaoComment  " + "WHERE feature = :this )", readOnly = true),
        @NamedQuery(name = "feature.getContributionOf", query = "SELECT sum(amount) "
                + "FROM com.bloatit.data.DaoContribution as c " + "WHERE c.feature = :this "
                + "AND c.member = :member " + "AND c.asTeam = null "),
        @NamedQuery(name = "feature.getContribution.canceled", query = "FROM com.bloatit.data.DaoContribution as c "
                + "WHERE c.feature = :this " + "AND c.state = :state "),
        @NamedQuery(name = "feature.getContribution.notcanceled", query = "FROM com.bloatit.data.DaoContribution as c "
                + "WHERE c.feature = :this " + "AND c.state != :state " + "ORDER BY c.amount DESC "),
        @NamedQuery(name = "feature.getContribution.canceled.size", query = "SELECT COUNT(*) "
                + "FROM com.bloatit.data.DaoContribution as c " + "WHERE c.feature = :this "
                + "AND c.state = :state "),
        @NamedQuery(name = "feature.getContribution.notcanceled.size", query = "SELECT COUNT(*) "
                + "FROM com.bloatit.data.DaoContribution as c " + "WHERE c.feature = :this "
                + "AND c.state != :state "),

        @NamedQuery(name = "feature.getAll.orderByCreationDate", query = "FROM com.bloatit.data.DaoFeature "
                + "WHERE featureState != :featureState " + "ORDER BY creationDate DESC "),
        @NamedQuery(name = "feature.getAll.orderByCreationDate.size", query = "SELECT COUNT(*)"
                + "FROM com.bloatit.data.DaoFeature " + "WHERE featureState != :featureState "),

})
// @formatter:on
public class DaoFeature extends DaoKudosable implements DaoCommentable {

    /**
     * This is the state of the feature. It's used in the workflow modeling. The
     * order is important !
     */
    public enum FeatureState {

        /** No offers, waiting for money and offer. */
        PENDING,

        /** One or more offer, waiting for money. */
        PREPARING,

        /** Development in progress. */
        DEVELOPPING,

        /** Something went wrong, the feature is canceled. */
        DISCARDED,

        /** All is good, the developer is paid and the users are happy. */
        FINISHED
    }

    /**
     * This is a calculated value with the sum of the value of all
     * contributions.
     */
    @Basic(optional = false)
    @Field(store = Store.NO, index = Index.UN_TOKENIZED)
    private BigDecimal contribution;

    @Basic(optional = false)
    @Field
    @Enumerated
    private FeatureState featureState;

    /**
     * A description is a translatable text with an title.
     */
    @OneToOne(optional = false)
    @Cascade(value = { CascadeType.ALL })
    @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
    @IndexedEmbedded
    private DaoDescription description;

    @OneToMany(mappedBy = "feature")
    @Cascade(value = { CascadeType.ALL })
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    @IndexedEmbedded
    private final List<DaoOffer> offers = new ArrayList<DaoOffer>(0);

    @OneToMany
    @Cascade(value = { CascadeType.ALL })
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    private final List<DaoContribution> contributions = new ArrayList<DaoContribution>(0);

    @OneToMany(mappedBy = "feature")
    @OrderBy(clause = "id")
    @Cascade(value = { CascadeType.ALL })
    @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
    @IndexedEmbedded
    private final List<DaoComment> comments = new ArrayList<DaoComment>();

    /**
     * The selected offer is the offer that is most likely to be validated and
     * used. If an offer is selected and has enough money and has a elapse time
     * done then this offer go into dev.
     */
    @ManyToOne(optional = true)
    @Cascade(value = { CascadeType.ALL })
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    @IndexedEmbedded
    private DaoOffer selectedOffer;

    @ManyToOne(optional = true, fetch = FetchType.LAZY)
    @Cascade(value = { CascadeType.ALL })
    @Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
    @IndexedEmbedded
    private DaoSoftware software;

    @Basic(optional = true)
    private Date validationDate;

    @OneToMany(mappedBy = "followed")
    private final List<DaoFollowFeature> followers = new ArrayList<DaoFollowFeature>();
    @SuppressWarnings("unused")
    @OneToMany(mappedBy = "feature")
    private final List<DaoEvent> events = new ArrayList<DaoEvent>();

    // ======================================================================
    // Construct.
    // ======================================================================

    /**
     * Creates the and persist.
     * 
     * @param member the author of the feature
     * @param team the as team property. Can be null.
     * @param description the description of the feature
     * @param software the software on which this feature has to be added
     * @return the new feature
     * @see #DaoFeature(DaoMember, DaoTeam, DaoDescription, DaoSoftware)
     */
    public static DaoFeature createAndPersist(final DaoMember member, final DaoTeam team,
            final DaoDescription description, final DaoSoftware software) {
        final Session session = SessionManager.getSessionFactory().getCurrentSession();
        final DaoFeature feature = new DaoFeature(member, team, description, software);
        try {
            session.save(feature);
        } catch (final HibernateException e) {
            session.getTransaction().rollback();
            SessionManager.getSessionFactory().getCurrentSession().beginTransaction();
            throw e;
        }
        DaoEvent.createFeatureEvent(feature, EventType.CREATE_FEATURE);
        return feature;
    }

    /**
     * Create a DaoFeature and set its state to the state PENDING.
     * 
     * @param member is the author of the feature
     * @param description is the description ...
     * @throws NonOptionalParameterException if any of the parameter is null.
     */
    private DaoFeature(final DaoMember member, final DaoTeam team, final DaoDescription description,
            final DaoSoftware software) {
        super(member, team);
        if (description == null) {
            throw new NonOptionalParameterException();
        }
        if (software != null) {
            this.software = software;
            software.addFeature(this);
        }
        this.description = description;
        this.validationDate = null;
        setSelectedOffer(null);
        this.contribution = BigDecimal.ZERO;
        setFeatureState(FeatureState.PENDING);
    }

    public void setSoftware(final DaoSoftware soft) {
        if (software != null) {
            this.software.removeFeature(this);
        }
        this.software = soft;
        if (software != null) {
            software.addFeature(this);
        }
    }

    /*
     * (non-Javadoc)
     * @see
     * com.bloatit.data.DaoCommentable#addComment(com.bloatit.data.DaoComment)
     */
    @Override
    public void addComment(final DaoComment comment) {
        this.comments.add(comment);
    }

    /**
     * Add a contribution to a feature.
     * 
     * @param member the author of the contribution
     * @param team add this contribution in the name of team.
     * @param amount the > 0 amount of euros on this contribution
     * @param comment a <= 144 char comment on this contribution
     * @return the new {@link DaoContribution}
     * @throws NotEnoughMoneyException the not enough money exception
     */
    public DaoContribution addContribution(final DaoMember member, final DaoTeam team, final BigDecimal amount,
            final String comment) throws NotEnoughMoneyException {
        if (amount == null) {
            throw new NonOptionalParameterException();
        }
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            Log.data().fatal("Cannot create a contribution with this amount " + amount.toEngineeringString()
                    + " by member " + member.getId());
            throw new BadProgrammerException("The amount of a contribution cannot be <= 0.", null);
        }
        if (comment != null && comment.length() > DaoContribution.COMMENT_MAX_LENGTH) {
            Log.data().fatal("The comment of a contribution must be <= 140 chars long.");
            throw new BadProgrammerException("Comments length of Contribution must be < 140.", null);
        }
        if (team != null && !team.getUserTeamRight(member).contains(UserTeamRight.BANK)) {
            throw new BadProgrammerException("This member cannot contribute as a team.");
        }

        final DaoContribution newContribution = new DaoContribution(member, team, this, amount, comment);
        this.contributions.add(newContribution);
        this.contribution = this.contribution.add(amount);

        // DaoEvent.createContributionEvent(this, EventType.ADD_CONTRIBUTION,
        // newContribution);
        return newContribution;
    }

    /**
     * Add a new offer for this feature. If there is no selected offer, select
     * this one.
     * 
     * @param offer the offer to add
     */
    public void addOffer(final DaoOffer offer) {
        this.offers.add(offer);
        DaoEvent.createOfferEvent(this, EventType.ADD_OFFER, offer);
    }

    /**
     * delete offer from this feature AND FROM DB !.
     * 
     * @param offer the offer we want to delete.
     */
    public void removeOffer(final DaoOffer offer) {
        this.offers.remove(offer);
        if (offer.equals(this.selectedOffer)) {
            this.selectedOffer = null;
        }
        SessionManager.getSessionFactory().getCurrentSession().delete(offer);
    }

    /**
     * Compute the selected offer. WARNING this does not assign anything to the
     * selectedOffer property.
     * 
     * @return the selected offer
     */
    public DaoOffer computeSelectedOffer() {
        try {
            return (DaoOffer) SessionManager.getNamedQuery("feature.getOffers.bySelected").setEntity("this", this)
                    .setParameter("state", DaoKudosable.PopularityState.PENDING).iterate().next();
        } catch (final NoSuchElementException e) {
            return null;
        }

    }

    /**
     * Override the selected offer.
     * 
     * @param selectedOffer the offer to set selected
     */
    public void setSelectedOffer(final DaoOffer selectedOffer) {
        if (selectedOffer != null && selectedOffer.equals(getSelectedOffer())) {
            DaoEvent.createOfferEvent(this, EventType.CHANGE_SELECTED_OFFER, selectedOffer);
        }
        if (selectedOffer == null && this.selectedOffer != null) {
            DaoEvent.createOfferEvent(this, EventType.REMOVE_SELECTED_OFFER, selectedOffer);
        }
        this.selectedOffer = selectedOffer;
    }

    /**
     * Set the validation date.
     * 
     * @param validationDate the new validation date.
     */
    public void setValidationDate(final Date validationDate) {
        this.validationDate = validationDate;
    }

    /**
     * Called by contribution when canceled.
     * 
     * @param amount the amount
     */
    void cancelContribution(final BigDecimal amount) {
        this.contribution = this.contribution.subtract(amount);
    }

    /**
     * set the feature state.
     * 
     * @param featureState the new state
     */
    public void setFeatureState(final FeatureState featureState) {
        this.featureState = featureState;
        switch (featureState) {
        case DEVELOPPING:
            DaoEvent.createFeatureEvent(this, EventType.IN_DEVELOPING_STATE);
            break;
        case DISCARDED:
            DaoEvent.createFeatureEvent(this, EventType.DISCARDED);
            break;
        case FINISHED:
            DaoEvent.createFeatureEvent(this, EventType.FINICHED);
            break;
        case PENDING:
            // Do nothing
            break;
        case PREPARING:
            // Do nothing
            break;
        }
    }

    public void setDescription(final String newDescription, final Language language) {
        getDescription().getTranslation(language).setText(newDescription, getMember());
    }

    public void setTitle(final String title, final Language language) {
        getDescription().getTranslation(language).setTitle(title);
    }

    // ======================================================================
    // Getters.
    // ======================================================================

    /** The Constant PROGRESSION_PERCENT. */
    public static final int PROGRESSION_PERCENT = 100;

    @Field(store = Store.YES, index = Index.UN_TOKENIZED)
    public float getProgress() {
        final DaoOffer currentOffer = getSelectedOffer();
        if (currentOffer == null) {
            return 0.f;
        }
        if (currentOffer.getAmount().floatValue() != 0) {
            return (getContribution().floatValue() * PROGRESSION_PERCENT) / currentOffer.getAmount().floatValue();
        }
        return Float.POSITIVE_INFINITY;
    }

    public List<DaoFollowFeature> getFollowers() {
        return followers;
    }

    /**
     * Gets the a description is a translatable text with an title.
     * 
     * @return the a description is a translatable text with an title
     */
    public DaoDescription getDescription() {
        return this.description;
    }

    /**
     * Gets the offers.
     * 
     * @return the offers
     */
    public PageIterable<DaoOffer> getOffers() {
        return new MappedUserContentList<DaoOffer>(this.offers);
    }

    /**
     * Gets the feature state.
     * 
     * @return the feature state
     */
    public FeatureState getFeatureState() {
        return this.featureState;
    }

    /**
     * Gets the contributions.
     * 
     * @return the contributions
     */
    public PageIterable<DaoContribution> getContributions() {
        return new MappedUserContentList<DaoContribution>(this.contributions);
    }

    public PageIterable<DaoContribution> getContributions(final boolean isCanceled) {
        if (isCanceled) {
            return new QueryCollection<DaoContribution>("feature.getContribution.canceled")
                    .setParameter("this", this).setParameter("state", DaoContribution.ContributionState.CANCELED);
        }
        return new QueryCollection<DaoContribution>("feature.getContribution.notcanceled")
                .setParameter("this", this).setParameter("state", DaoContribution.ContributionState.CANCELED);

    }

    /**
     * Gets the comments count.
     * 
     * @return the comments count
     */
    public Long getCommentsCount() {
        return (Long) SessionManager.getNamedQuery("feature.getComments.size").setEntity("this", this)
                .uniqueResult();
    }

    /*
     * (non-Javadoc)
     * @see com.bloatit.data.DaoCommentable#getCommentsFromQuery()
     */
    @Override
    public PageIterable<DaoComment> getComments() {
        return new MappedUserContentList<DaoComment>(comments);
    }

    /*
     * (non-Javadoc)
     * @see com.bloatit.data.DaoCommentable#getLastComment()
     */
    @Override
    public DaoComment getLastComment() {
        return CommentManager.getLastComment(comments);
    }

    /**
     * The selected offer is the offer that is most likely to be validated and
     * used.
     * 
     * @return the selected offer is the offer that is most likely to be
     *         validated and used
     */
    public DaoOffer getSelectedOffer() {
        return this.selectedOffer;
    }

    /**
     * This is a calculated value with the sum of the value of all
     * contributions.
     * 
     * @return the this is a calculated value with the sum of the value of all
     *         contributions
     */
    public BigDecimal getContribution() {
        return this.contribution;
    }

    /**
     * Gets the contribution min.
     * 
     * @return the minimum value of the contributions on this feature.
     */
    public BigDecimal getContributionMin() {
        return (BigDecimal) SessionManager.getNamedQuery("feature.getAmounts.min").setEntity("this", this)
                .setParameter("state", DaoContribution.ContributionState.CANCELED).uniqueResult();
    }

    /**
     * Gets the contribution max.
     * 
     * @return the maximum value of the contributions on this feature.
     */
    public BigDecimal getContributionMax() {
        return (BigDecimal) SessionManager.getNamedQuery("feature.getAmounts.max").setEntity("this", this)
                .setParameter("state", DaoContribution.ContributionState.CANCELED).uniqueResult();
    }

    /**
     * Gets the contribution avg.
     * 
     * @return the average value of the contributions on this feature.
     */
    public BigDecimal getContributionAvg() {
        return (BigDecimal) SessionManager.getNamedQuery("feature.getAmounts.avg").setEntity("this", this)
                .setParameter("state", DaoContribution.ContributionState.CANCELED).uniqueResult();
    }

    public BigDecimal getContributionOf(final DaoMember member) {
        final BigDecimal contributions = (BigDecimal) SessionManager.getNamedQuery("feature.getContributionOf")
                .setEntity("this", this).setEntity("member", member).uniqueResult();
        return contributions != null ? contributions : BigDecimal.ZERO;
    }

    /**
     * Gets the validation date.
     * 
     * @return the validation date
     */
    public Date getValidationDate() {
        return this.validationDate;
    }

    /**
     * Gets the software.
     * 
     * @return the software
     */
    public DaoSoftware getSoftware() {
        return this.software;
    }

    /**
     * Count open bugs.
     * 
     * @return the int
     */
    public int countOpenBugs() {
        if (selectedOffer == null) {
            return 0;
        }
        final Query query = SessionManager.getNamedQuery("feature.getBugs.byNonState.size");
        query.setEntity("offer", this.selectedOffer);
        query.setParameter("state", DaoBug.BugState.RESOLVED);
        return ((Long) query.uniqueResult()).intValue();
    }

    /**
     * Gets the open bugs.
     * 
     * @return the open bugs
     */
    public PageIterable<DaoBug> getOpenBugs() {
        if (this.selectedOffer == null) {
            return new EmptyPageIterable<DaoBug>();
        }
        return new QueryCollection<DaoBug>("feature.getBugs.byNonState").setEntity("offer", this.selectedOffer)
                .setParameter("state", DaoBug.BugState.RESOLVED);
    }

    /**
     * Gets the closed bugs.
     * 
     * @return the closed bugs
     */
    public PageIterable<DaoBug> getClosedBugs() {
        if (this.selectedOffer == null) {
            return new EmptyPageIterable<DaoBug>();
        }
        return new QueryCollection<DaoBug>("feature.getBugs.byState").setEntity("offer", this.selectedOffer)
                .setParameter("state", DaoBug.BugState.RESOLVED);
    }

    // ======================================================================
    // Static accessors.
    // ======================================================================

    public static PageIterable<DaoFeature> getAllByCreationDate() {
        return new QueryCollection<DaoFeature>("feature.getAll.orderByCreationDate").setParameter("featureState",
                FeatureState.DISCARDED);
    }

    // ======================================================================
    // Visitor.
    // ======================================================================

    /*
     * (non-Javadoc)
     * @see
     * com.bloatit.data.DaoIdentifiable#accept(com.bloatit.data.DataClassVisitor
     * )
     */
    @Override
    public <ReturnType> ReturnType accept(final DataClassVisitor<ReturnType> visitor) {
        return visitor.visit(this);
    }

    // ======================================================================
    // For hibernate mapping
    // ======================================================================

    /**
     * Instantiates a new dao feature.
     */
    protected DaoFeature() {
        super();
    }

    // ======================================================================
    // equals hashcode.
    // ======================================================================

    /*
     * (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = super.hashCode();
        result = prime * result + ((this.description == null) ? 0 : this.description.hashCode());
        return result;
    }

    /*
     * (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if (!super.equals(obj)) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final DaoFeature other = (DaoFeature) obj;
        if (this.description == null) {
            if (other.description != null) {
                return false;
            }
        } else if (!this.description.equals(other.description)) {
            return false;
        }
        return true;
    }

}