/**
* Yobi, Project Hosting SW
*
* Copyright 2012 NAVER Corp.
* http://yobi.io
*
* @Author yoon
*
* 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 com.avaje.ebean.Ebean;
import com.avaje.ebean.Page;
import com.avaje.ebean.annotation.Formula;
import jxl.Workbook;
import jxl.format.Alignment;
import jxl.format.Border;
import jxl.format.BorderLineStyle;
import jxl.format.Colour;
import jxl.format.*;
import jxl.write.*;
import models.enumeration.ResourceType;
import models.enumeration.State;
import models.resource.Resource;
import models.support.SearchCondition;
import org.apache.commons.lang3.time.DateUtils;
import play.data.format.Formats;
import play.i18n.Messages;
import utils.JodaDateUtil;
import javax.persistence.*;
import play.data.Form;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.Boolean;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Pattern;
@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"project_id", "number"}))
public class Issue extends AbstractPosting implements LabelOwner {
/**
* @author Yobi TEAM
*/
private static final long serialVersionUID = -2409072006294045262L;
public static final Finder<Long, Issue> finder = new Finder<>(Long.class, Issue.class);
public static final String DEFAULT_SORTER = "createdDate";
public static final String TO_BE_ASSIGNED = "TBA";
public static final Pattern ISSUE_PATTERN = Pattern.compile("#\\d+");
public State state;
@Formats.DateTime(pattern = "yyyy-MM-dd")
public Date dueDate;
public static List<State> availableStates = new ArrayList<>();
static {
availableStates.add(State.OPEN);
availableStates.add(State.CLOSED);
}
@ManyToOne
public Milestone milestone;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
public Set<IssueLabel> labels;
@ManyToOne
public Assignee assignee;
@OneToMany(cascade = CascadeType.ALL, mappedBy="issue")
public List<IssueComment> comments;
@OneToMany(cascade = CascadeType.ALL, mappedBy="issue")
public List<IssueEvent> events;
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(
name = "issue_voter",
joinColumns = @JoinColumn(name = "issue_id"),
inverseJoinColumns = @JoinColumn(name = "user_id")
)
public List<User> voters = new ArrayList<>();
@Transient
@Formula(select = "case when due_date is null then cast('0001-01-01 00:00:00' as timestamp) else due_date end")
public Date dueDateDesc;
@Transient
@Formula(select = "case when due_date is null then cast('9999-12-31 23:59:59' as timestamp) else due_date end")
public Date dueDateAsc;
/**
* @see models.AbstractPosting#computeNumOfComments()
*/
public int computeNumOfComments() {
return comments.size();
}
/**
* @see models.Project#increaseLastIssueNumber()
*/
@Override
protected Long increaseNumber() {
return Project.increaseLastIssueNumber(project.id);
}
protected void fixLastNumber() {
Project.fixLastIssueNumber(project.id);
}
public String assigneeName() {
return ((assignee != null && assignee.user != null) ? assignee.user.name : null);
}
/**
* @see Assignee#add(Long, Long)
*/
private void updateAssignee() {
if (assignee != null && assignee.id == null && assignee.user.id != null) {
assignee = Assignee.add(assignee.user.id, project.id);
}
}
/**
* @see #updateAssignee()
*/
@Transient
public void update() {
updateAssignee();
super.update();
}
public void checkLabels() throws IssueLabel.IssueLabelException {
Set<IssueLabelCategory> notAllowedCategories = new HashSet<>();
for (IssueLabel label : labels) {
if (notAllowedCategories.contains(label.category)) {
throw new IssueLabel.IssueLabelException("This category does " +
"not allow an issue to have two or more labels of " +
"the category");
}
if (label.category.isExclusive) {
notAllowedCategories.add(label.category);
}
}
}
@Override
public void updateProperties() {
HashSet<String> updateProps = new HashSet<>();
// update null milestone explicitly
if(this.milestone == null) {
updateProps.add("milestone");
}
// update null assignee explicitly
if(this.assignee == null) {
updateProps.add("assignee");
}
if(!updateProps.isEmpty()) {
Ebean.update(this, updateProps);
}
}
/**
* @see #updateAssignee()
*/
@Transient
public void save() {
updateAssignee();
super.save();
}
public static int countIssues(Long projectId, State state) {
if (state == State.ALL) {
return finder.where().eq("project.id", projectId).findRowCount();
} else {
return finder.where().eq("project.id", projectId).eq("state", state).findRowCount();
}
}
public static int countIssuesBy(Long projectId, SearchCondition cond) {
return cond.asExpressionList(Project.find.byId(projectId)).findRowCount();
}
public static int countIssuesBy(SearchCondition cond) {
return cond.asExpressionList().findRowCount();
}
public static int countIssuesBy(Long projectId, Map<String, String> paramMap) {
Form<SearchCondition> paramForm = new Form<>(SearchCondition.class);
SearchCondition cond = paramForm.bind(paramMap).get();
return Issue.countIssuesBy(projectId, cond);
}
/**
* Generate a Microsoft Excel file in byte array from the given issue list,
* using JXL.
*/
public static byte[] excelFrom(List<Issue> issueList) throws WriteException, IOException {
WritableWorkbook workbook;
WritableSheet sheet;
WritableFont wf1 = new WritableFont(WritableFont.TIMES, 13, WritableFont.BOLD, false,
UnderlineStyle.SINGLE, Colour.BLUE_GREY, ScriptStyle.NORMAL_SCRIPT);
WritableCellFormat cf1 = new WritableCellFormat(wf1);
cf1.setBorder(Border.ALL, BorderLineStyle.DOUBLE);
cf1.setAlignment(Alignment.CENTRE);
WritableFont wf2 = new WritableFont(WritableFont.TAHOMA, 11, WritableFont.NO_BOLD, false, UnderlineStyle.NO_UNDERLINE, Colour.BLACK, ScriptStyle.NORMAL_SCRIPT);
WritableCellFormat cf2 = new WritableCellFormat(wf2);
cf2.setShrinkToFit(true);
cf2.setBorder(Border.ALL, BorderLineStyle.THIN);
cf2.setAlignment(Alignment.CENTRE);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
workbook = Workbook.createWorkbook(bos);
sheet = workbook.createSheet(String.valueOf(JodaDateUtil.today().getTime()), 0);
String[] labalArr = {"ID", "STATE", "TITLE", "ASSIGNEE", "DATE"};
for (int i = 0; i < labalArr.length; i++) {
sheet.addCell(new jxl.write.Label(i, 0, labalArr[i], cf1));
sheet.setColumnView(i, 20);
}
for (int i = 1; i < issueList.size() + 1; i++) {
Issue issue = issueList.get(i - 1);
int colcnt = 0;
sheet.addCell(new jxl.write.Label(colcnt++, i, issue.id.toString(), cf2));
sheet.addCell(new jxl.write.Label(colcnt++, i, issue.state.toString(), cf2));
sheet.addCell(new jxl.write.Label(colcnt++, i, issue.title, cf2));
sheet.addCell(new jxl.write.Label(colcnt++, i, getAssigneeName(issue.assignee), cf2));
sheet.addCell(new jxl.write.Label(colcnt++, i, issue.createdDate.toString(), cf2));
}
workbook.write();
try {
workbook.close();
} catch (WriteException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return bos.toByteArray();
}
private static String getAssigneeName(Assignee assignee) {
return (assignee != null ? assignee.user.name : TO_BE_ASSIGNED);
}
public boolean isOpen() {
return this.state == State.OPEN;
}
public boolean isClosed() {
return this.state == State.CLOSED;
}
@Override
public Resource asResource() {
return asResource(ResourceType.ISSUE_POST);
}
public Resource fieldAsResource(final ResourceType resourceType) {
return new Resource() {
@Override
public String getId() {
return id.toString();
}
@Override
public Project getProject() {
return project;
}
@Override
public ResourceType getType() {
return resourceType;
}
@Override
public Resource getContainer() {
return Issue.this.asResource();
}
};
}
public Resource stateAsResource() {
return fieldAsResource(ResourceType.ISSUE_STATE);
}
public Resource milestoneAsResource() {
return fieldAsResource(ResourceType.ISSUE_MILESTONE);
}
public Resource assigneeAsResource() {
return fieldAsResource(ResourceType.ISSUE_ASSIGNEE);
}
public static List<Issue> findRecentlyCreated(Project project, int size) {
return finder.where().eq("project.id", project.id)
.order().desc("createdDate")
.findPagingList(size).getPage(0)
.getList();
}
/**
* @see models.AbstractPosting#getComments()
*/
@Transient
public List<? extends Comment> getComments() {
Collections.sort(comments, Comment.comparator());
return comments;
}
public static Issue findByNumber(Project project, Long number) {
return AbstractPosting.findByNumber(finder, project, number);
}
/**
* Returns all users watching or voting the issue.
*
* @return The set watching and voting the issue.
*/
@Transient
public Set<User> getWatchers() {
Set<User> baseWatchers = new HashSet<>();
if (assignee != null) {
baseWatchers.add(assignee.user);
}
baseWatchers.addAll(this.voters);
return super.getWatchers(baseWatchers);
}
public boolean assignedUserEquals(Assignee otherAssignee) {
if (assignee == null || assignee.user == null || assignee.user.isAnonymous()) {
return otherAssignee == null || otherAssignee.user == null || otherAssignee.user.isAnonymous();
}
if (otherAssignee == null || otherAssignee.user == null || otherAssignee.user.isAnonymous()) {
return assignee == null || assignee.user == null || assignee.user.isAnonymous();
}
return assignee.equals(otherAssignee) || assignee.user.equals(otherAssignee.user);
}
/**
* @param project
* @param days days ago
* @return
*/
public static List<Issue> findRecentlyOpendIssuesByDaysAgo(Project project, int days) {
return finder.where()
.eq("project.id", project.id)
.eq("state", State.OPEN)
.ge("createdDate", JodaDateUtil.before(days)).order().desc("createdDate").findList();
}
public static Page<Issue> findIssuesByState(int size, int pageNum, State state) {
return finder.where().eq("state", state)
.order().desc("createdDate")
.findPagingList(size).getPage(pageNum);
}
public State previousState() {
int currentState = Issue.availableStates.indexOf(this.state);
if(isLastState(currentState)) {
return Issue.availableStates.get(0);
} else {
return Issue.availableStates.get(currentState + 1);
}
}
private boolean isLastState(int currentState) {
return currentState + 1 == Issue.availableStates.size();
}
public State nextState() {
int currentState = Issue.availableStates.indexOf(this.state);
if(isFirstState(currentState)) {
return Issue.availableStates.get(Issue.availableStates.size()-1);
} else {
return Issue.availableStates.get(currentState - 1);
}
}
private boolean isFirstState(int currentState) {
return currentState == 0;
}
public State toNextState(){
this.state = nextState();
this.updatedDate = JodaDateUtil.now();
super.update();
return this.state;
}
@Override
public Set<IssueLabel> getLabels() {
return labels;
}
public Set<Long> getLabelIds() {
Set<Long> labelIds = new HashSet<>();
for(IssueLabel label : this.labels){
labelIds.add(label.id);
}
return labelIds;
}
public List<TimelineItem> getTimeline() {
List<TimelineItem> timelineItems = new ArrayList<>();
timelineItems.addAll(comments);
timelineItems.addAll(events);
Collections.sort(timelineItems, TimelineItem.ASC);
return timelineItems;
}
public boolean canBeDeleted() {
if(this.comments == null || this.comments.isEmpty()) {
return true;
}
for(IssueComment comment : comments) {
if(!comment.authorLoginId.equals(this.authorLoginId)) {
return false;
}
}
return true;
}
/**
* Adds {@code user} as a voter.
*
* @param user
*/
public void addVoter(User user) {
this.voters.add(user);
this.update();
}
/**
* Cancels the vote of {@code user}.
*
* @param user
*/
public void removeVoter(User user) {
this.voters.remove(user);
this.update();
}
/**
* Returns whether {@code user} has voted or not.
*
* @param user
* @return True if the user has voted, if not False
*/
public boolean isVotedBy(User user) {
return this.voters.contains(user);
}
public String getDueDateString() {
if (dueDate == null) {
return null;
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(this.dueDate);
}
public Boolean isOverDueDate(){
return (JodaDateUtil.ago(dueDate).getMillis() > 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.default.day", JodaDateUtil.localDaysBetween(dueDate, now));
} else {
return Messages.get("common.time.default.day", JodaDateUtil.localDaysBetween(now, dueDate));
}
}
}