/**
* Yobi, Project Hosting SW
*
* Copyright 2012 NAVER Corp.
* http://yobi.io
*
* @Author Tae
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package models;
import models.enumeration.*;
import models.resource.Resource;
import models.resource.ResourceConvertible;
import models.support.*;
import org.apache.commons.lang3.time.DateUtils;
import play.data.format.*;
import play.data.validation.*;
import play.db.ebean.*;
import play.i18n.Messages;
import utils.JodaDateUtil;
import javax.persistence.*;
import java.text.*;
import java.util.*;
@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"project_id", "title"}))
public class Milestone extends Model implements ResourceConvertible {
private static final long serialVersionUID = 1L;
public static final Finder<Long, Milestone> find = new Finder<>(Long.class, Milestone.class);
public static final String DEFAULT_SORTER = "dueDate";
public static final Long NULL_MILESTONE_ID = -1l;
@Id
public Long id;
@Constraints.Required
public String title;
@Formats.DateTime(pattern = "yyyy-MM-dd")
public Date dueDate;
@Constraints.Required @Lob
public String contents;
@Constraints.Required
public State state;
@ManyToOne
public Project project;
@OneToMany(mappedBy = "milestone")
public Set<Issue> issues;
public void delete() {
// Set all issues' milestone to null.
// I don't know why Ebean does not do this by itself.
for(Issue issue : issues) {
issue.milestone = null;
issue.update();
}
super.delete();
}
public static void create(Milestone milestone) {
milestone.save();
}
public int getNumClosedIssues() {
return Issue.finder.where().eq("milestone", this).eq("state", State.CLOSED).findRowCount();
}
public int getNumOpenIssues() {
return Issue.finder.where().eq("milestone", this).eq("state", State.OPEN).findRowCount();
}
public List<Issue> sortedByNumberOfIssue(){
List <Issue>sortedIssues = new ArrayList<>(this.issues);
Collections.sort(sortedIssues, new Comparator<Issue>() {
@Override
public int compare(Issue a, Issue b) {
return b.getNumber().compareTo(a.getNumber());
}
});
return sortedIssues;
}
public List<Issue> sortedByNumberOfOpenIssue(){
List<Issue> openedIssues = new ArrayList<>();
for(Issue issue : sortedByNumberOfIssue()) {
if(issue.isOpen()) {
openedIssues.add(issue);
}
}
return openedIssues;
}
public List<Issue> sortedByNumberOfClosedIssue(){
List<Issue> closedIssues = new ArrayList<>();
for(Issue issue : sortedByNumberOfIssue()) {
if(issue.isClosed()) {
closedIssues.add(issue);
}
}
return closedIssues;
}
public int getNumTotalIssues() {
return issues.size();
}
public int getCompletionRate() {
return (int) (((double) getNumClosedIssues() / (double) getNumTotalIssues()) * 100);
}
public static Milestone findById(Long id) {
return find.byId(id);
}
public static List<Milestone> findByProjectId(Long projectId) {
return Milestone.findMilestones(projectId, State.ALL);
}
public static List<Milestone> findClosedMilestones(Long projectId) {
return Milestone.findMilestones(projectId, State.CLOSED);
}
public static List<Milestone> findOpenMilestones(Long projectId) {
return Milestone.findMilestones(projectId, State.OPEN);
}
/**
* convert mildestone due date string into yyyy-MM-dd format
*
* @return
*/
public String getDueDateString() {
if (dueDate == null) {
return null;
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(this.dueDate);
}
/**
* Sort milestone list by direction.
* If there is no direction, then sort with DEFAULT_SORTER.
*
* @param projectId
* @param state
* @return
*/
public static List<Milestone> findMilestones(Long projectId,
State state) {
return findMilestones(projectId, state, DEFAULT_SORTER, Direction.ASC);
}
/**
* find milestone with OrderParam (string for sorting)
*
* @param projectId
* @param state
* @param sort
* @param direction
* @return
*/
public static List<Milestone> findMilestones(Long projectId,
State state, String sort, final Direction direction) {
OrderParams orderParams = new OrderParams();
if(!"completionRate".equals(sort)) {
orderParams.add(sort, direction);
}
SearchParams searchParams = new SearchParams().add("project.id", projectId, Matching.EQUALS);
if(state != null && state != State.ALL) {
searchParams.add("state", state, Matching.EQUALS);
}
List<Milestone> milestones = FinderTemplate.findBy(orderParams, searchParams, find);
if("completionRate".equals(sort)) {
Collections.sort(milestones, new Comparator<Milestone>() {
@Override
public int compare(Milestone o1, Milestone o2) {
int o1CompletionRate = o1.getCompletionRate();
int o2CompletionRate = o2.getCompletionRate();
if(direction == Direction.ASC) {
return (o1CompletionRate < o2CompletionRate ? -1 : (o1CompletionRate == o2CompletionRate ? 0 : 1));
} else {
return (o1CompletionRate < o2CompletionRate ? 1 : (o1CompletionRate == o2CompletionRate ? 0 : -1));
}
}
});
}
return milestones;
}
public void updateWith(Milestone newMilestone) {
this.contents = newMilestone.contents;
this.title = newMilestone.title;
this.dueDate = newMilestone.dueDate;
this.state = newMilestone.state;
save();
}
/**
* get milestone list by hashmap.
* key: milestone id
* value: milestone title
*
* @return linkedHashMap
*/
public static Map<String, String> options(Long projectId) {
LinkedHashMap<String, String> options = new LinkedHashMap<>();
for (Milestone milestone : findMilestones(projectId, State.ALL, "title", Direction.ASC)) {
options.put(milestone.id.toString(), milestone.title);
}
return options;
}
public static boolean isUniqueProjectIdAndTitle(Long projectId, String title) {
int count = find.where().eq("project.id", projectId).eq("title", title).findRowCount();
return (count == 0);
}
public String until() {
if (dueDate == null) {
return null;
}
Date now = JodaDateUtil.now();
if (DateUtils.isSameDay(now, dueDate)) {
return Messages.get("common.time.today");
} else if (isOverDueDate()) {
return Messages.get("common.time.overday", JodaDateUtil.localDaysBetween(dueDate, now));
} else {
return Messages.get("common.time.leftday", JodaDateUtil.localDaysBetween(now, dueDate));
}
}
public Boolean isOverDueDate(){
return (JodaDateUtil.ago(dueDate).getMillis() > 0);
}
@Override
public Resource asResource() {
return new Resource() {
@Override
public String getId() {
return id.toString();
}
@Override
public Project getProject() {
return project;
}
@Override
public ResourceType getType() {
return ResourceType.MILESTONE;
}
};
}
public void open() {
this.state = State.OPEN;
update();
}
public void close() {
this.state = State.CLOSED;
update();
}
public boolean isNullMilestone() {
return this.id.equals(NULL_MILESTONE_ID);
}
public static int countOpened(Project project) {
return find.where()
.eq("project", project)
.eq("state", State.OPEN)
.findRowCount();
}
}