Hyeonjae Park 2018-11-27
issue: Feature move posting to issue
@8e32e173ecbf9d0a15cc4dcb0d98da5efbce9884
app/controllers/BoardApp.java
--- app/controllers/BoardApp.java
+++ app/controllers/BoardApp.java
@@ -386,7 +386,7 @@
     }
 
     /**
-     * @see controllers.AbstractPostingApp#saveComment(models.Comment, play.data.Form, play.mvc.Call, Runnable)
+     * @see controllers.AbstractPostingApp#saveComment(Comment comment, Runnable containerUpdater)
      */
     @Transactional
     @IsAllowed(value = Operation.READ, resourceType = ResourceType.BOARD_POST)
app/controllers/api/IssueApi.java
--- app/controllers/api/IssueApi.java
+++ app/controllers/api/IssueApi.java
@@ -7,20 +7,66 @@
 
 package controllers.api;
 
+import static controllers.UserApp.*;
+import static controllers.api.UserApi.*;
+import static play.libs.Json.*;
+
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.annotation.Nonnull;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang3.StringUtils;
+
 import com.avaje.ebean.ExpressionList;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
+
 import controllers.AbstractPostingApp;
 import controllers.UserApp;
 import controllers.annotation.AnonymousCheck;
 import controllers.annotation.IsAllowed;
 import controllers.annotation.IsCreatable;
 import controllers.routes;
-import models.*;
-import models.enumeration.*;
-import org.apache.commons.codec.digest.DigestUtils;
-import org.apache.commons.lang3.StringUtils;
+import models.Assignee;
+import models.Attachment;
+import models.Comment;
+import models.Issue;
+import models.IssueComment;
+import models.IssueEvent;
+import models.IssueLabel;
+import models.IssueSharer;
+import models.Milestone;
+import models.NotificationEvent;
+import models.Posting;
+import models.PostingComment;
+import models.Project;
+import models.ProjectUser;
+import models.User;
+import models.enumeration.EventType;
+import models.enumeration.Operation;
+import models.enumeration.ProjectScope;
+import models.enumeration.ResourceType;
+import models.enumeration.State;
+import models.enumeration.UserState;
 import play.api.mvc.Codec;
 import play.db.ebean.Transactional;
 import play.i18n.Messages;
@@ -29,19 +75,11 @@
 import play.libs.ws.WS;
 import play.mvc.Http;
 import play.mvc.Result;
-import utils.*;
-
-import javax.annotation.Nonnull;
-import java.io.IOException;
-import java.text.DateFormat;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.*;
-
-import static controllers.UserApp.MAX_FETCH_USERS;
-import static controllers.UserApp.currentUser;
-import static controllers.api.UserApi.*;
-import static play.libs.Json.toJson;
+import utils.AccessControl;
+import utils.ErrorViews;
+import utils.JodaDateUtil;
+import utils.Markdown;
+import utils.RouteUtil;
 
 public class IssueApi extends AbstractPostingApp {
     public static String TRANSLATION_API = play.Configuration.root().getString("application.extras.translation.api", "");
@@ -49,6 +87,111 @@
     public static String TRANSLATION_SVCID = play.Configuration.root().getString("application.extras.translation.svcid", "");
 
     @Transactional
+    public static Result imports(String owner, String projectName) {
+        Project project = Project.findByOwnerAndProjectName(owner, projectName);
+
+        try {
+            String postNumber = request().getQueryString("postNumber");
+            Long number = Optional.ofNullable(postNumber)
+                    .map(Long::parseLong)
+                    .orElseThrow(NumberFormatException::new);
+
+            Posting posting = Posting.findByNumber(project, number);
+
+
+            Issue issue = Issue.from(posting);
+            issue.save();
+
+            Map<String, String> postingCommentIdToIssueCommentIdMap = copyCommentsToIssue(posting.comments, issue);
+            copyAttachmentsToIssue(posting, issue);
+            copyAttachmentsToIssueComments(postingCommentIdToIssueCommentIdMap);
+
+            removePosting(posting);
+
+            ObjectNode json = Json.newObject();
+            json.put("number", issue.getNumber());
+
+            return ok(json);
+        } catch (NumberFormatException numberFormatException) {
+            String errorMessage = String.format("IssueApi.imports() error with NumberFormatException. owner: %s, projectName: %s - ", owner, projectName);
+            play.Logger.error(errorMessage, numberFormatException);
+        }
+
+        return badRequest();
+    }
+
+    private static void copyAttachmentsToIssue(Posting from, Issue to) {
+        List<Attachment> attachments = Attachment.findByContainer(ResourceType.BOARD_POST, String.valueOf(from.id));
+        attachments.forEach(attachment -> {
+            Attachment newAttachment = Attachment.copyAs(attachment);
+            newAttachment.containerId = String.valueOf(to.id);
+            newAttachment.containerType = ResourceType.ISSUE_POST;
+            newAttachment.save();
+            attachment.delete();
+        });
+    }
+
+    private static void copyAttachmentsToIssueComments(Map<String, String> postingCommentIdToIssueCommentIdMap) {
+        List<Attachment> attachments = postingCommentIdToIssueCommentIdMap.keySet().stream()
+                .flatMap(postingCommentId -> Attachment.findByContainer(ResourceType.NONISSUE_COMMENT, String.valueOf(postingCommentId)).stream())
+                .collect(Collectors.toList());
+
+        attachments.forEach(attachment -> {
+            String containerId = postingCommentIdToIssueCommentIdMap.get(attachment.containerId);
+
+            Attachment newAttachment = Attachment.copyAs(attachment);
+            newAttachment.containerId = containerId;
+            newAttachment.containerType = ResourceType.ISSUE_COMMENT;
+            newAttachment.save();
+            attachment.delete();
+        });
+    }
+
+    private static void removePosting(Posting posting) {
+        posting.deleteOnly();
+    }
+
+    private static Map<String, String> copyCommentsToIssue(Collection<PostingComment> postingComments, Issue issue) {
+        // 최상위 댓글
+        List<PostingComment> topLevelPostingComments = postingComments.stream()
+                .filter(postingComment -> Objects.isNull(postingComment.getParentComment()))
+                .collect(Collectors.toList());
+
+        // 대댓글
+        List<PostingComment> secondLevelPostingComments = postingComments.stream()
+                .filter(postingComment -> Objects.nonNull(postingComment.getParentComment()))
+                .collect(Collectors.toList());
+
+        // 최상위 댓글의 postingCommentId와 새로 생성될 issueCommentId의 mapping
+        // XXX: id는 Long 타입이지만, parentId는 String 타입이다.
+        Map<String, String> postingCommentIdToIssueCommentIdMap = new HashMap<>();
+
+        // 최상위 댓글을 issueComment에 생성하고, 이때 발급된 issueCommentId를 보관한다.
+        List<IssueComment> issueComments = new ArrayList<>();
+        topLevelPostingComments.forEach(topLevelPostingComment -> {
+            IssueComment issueComment = IssueComment.from(topLevelPostingComment, issue);
+            issueComment.save();
+            postingCommentIdToIssueCommentIdMap.put(String.valueOf(topLevelPostingComment.id), String.valueOf(issueComment.id));
+
+            issueComments.add(issueComment);
+        });
+
+        // 대댓글을 issueComment에 생성하고, 이때 새로 발급된 issueCommentId를 parentCommentId에 넣어준다.
+        secondLevelPostingComments.forEach(secondLevelPostingComment -> {
+            String parentCommentId = postingCommentIdToIssueCommentIdMap.get(String.valueOf(secondLevelPostingComment.getParentComment().id));
+
+            IssueComment issueComment = IssueComment.from(secondLevelPostingComment, issue);
+            issueComment.parentCommentId = parentCommentId;
+            issueComment.setParentComment(IssueComment.find.byId(Long.valueOf(parentCommentId)));
+            issueComment.save();
+
+            issueComments.add(issueComment);
+        });
+
+        return postingCommentIdToIssueCommentIdMap;
+    }
+
+    @Transactional
     public static Result updateIssueLabel(String owner, String projectName, Long number) {
         JsonNode json = request().body().asJson();
         if(json == null) {
app/models/AbstractPosting.java
--- app/models/AbstractPosting.java
+++ app/models/AbstractPosting.java
@@ -228,6 +228,10 @@
         super.delete();
     }
 
+    public void deleteOnly() {
+        super.delete();
+    }
+
     public void updateProperties() {
         // default implementation for convenience
     }
app/models/Attachment.java
--- app/models/Attachment.java
+++ app/models/Attachment.java
@@ -602,4 +602,17 @@
         }
         return uploads;
     }
+
+    public static Attachment copyAs(Attachment other) {
+        Attachment attachment = new Attachment();
+        attachment.name = other.name;
+        attachment.hash = other.hash;
+        attachment.containerType = other.containerType;
+        attachment.mimeType = other.mimeType;
+        attachment.size = other.size;
+        attachment.containerId = other.containerId;
+        attachment.createdDate = other.createdDate;
+        attachment.ownerLoginId = other.ownerLoginId;
+        return attachment;
+    }
 }
app/models/Issue.java
--- app/models/Issue.java
+++ app/models/Issue.java
@@ -6,40 +6,63 @@
  **/
 package models;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+import javax.persistence.CascadeType;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.JoinColumn;
+import javax.persistence.JoinTable;
+import javax.persistence.ManyToMany;
+import javax.persistence.ManyToOne;
+import javax.persistence.OneToMany;
+import javax.persistence.OneToOne;
+import javax.persistence.Table;
+import javax.persistence.Transient;
+import javax.persistence.UniqueConstraint;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.time.DateUtils;
+import org.apache.shiro.util.CollectionUtils;
+
 import com.avaje.ebean.Ebean;
 import com.avaje.ebean.ExpressionList;
 import com.avaje.ebean.Page;
 import com.avaje.ebean.annotation.Formula;
-import controllers.routes;
+
 import jxl.Workbook;
 import jxl.format.Alignment;
 import jxl.format.Border;
 import jxl.format.BorderLineStyle;
 import jxl.format.Colour;
-import jxl.format.*;
+import jxl.format.ScriptStyle;
+import jxl.format.UnderlineStyle;
 import jxl.format.VerticalAlignment;
-import jxl.write.*;
+import jxl.write.DateFormat;
+import jxl.write.WritableCellFormat;
+import jxl.write.WritableFont;
+import jxl.write.WritableSheet;
+import jxl.write.WritableWorkbook;
+import jxl.write.WriteException;
 import models.enumeration.ResourceType;
 import models.enumeration.State;
 import models.resource.Resource;
 import models.support.SearchCondition;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.time.DateUtils;
-import org.apache.shiro.util.CollectionUtils;
 import play.data.Form;
 import play.data.format.Formats;
 import play.db.ebean.Model.Finder;
 import play.i18n.Messages;
 import utils.JodaDateUtil;
-
-import javax.persistence.*;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.lang.Boolean;
-import java.text.SimpleDateFormat;
-import java.util.*;
-import java.util.regex.Pattern;
-import java.util.regex.Pattern;
 
 @Entity
 @Table(uniqueConstraints = @UniqueConstraint(columnNames = {"project_id", "number"}))
@@ -717,4 +740,20 @@
                 .eq("state", State.OPEN)
                 .findRowCount();
     }
+
+    public static Issue from(Posting posting) {
+        Issue issue = new Issue();
+
+        issue.title = posting.title;
+        issue.body = posting.body;
+        issue.history = posting.history;
+        issue.createdDate = posting.createdDate;
+        issue.updatedDate = posting.updatedDate;
+        issue.authorId = posting.authorId;
+        issue.authorLoginId = posting.authorLoginId;
+        issue.authorName = posting.authorName;
+        issue.project = posting.project;
+
+        return issue;
+    }
 }
app/models/IssueComment.java
--- app/models/IssueComment.java
+++ app/models/IssueComment.java
@@ -20,14 +20,21 @@
  */
 package models;
 
-import models.enumeration.ResourceType;
-import models.resource.Resource;
-
-import javax.persistence.*;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import javax.persistence.CascadeType;
+import javax.persistence.Entity;
+import javax.persistence.JoinColumn;
+import javax.persistence.JoinTable;
+import javax.persistence.ManyToMany;
+import javax.persistence.ManyToOne;
+import javax.persistence.OneToOne;
+
+import models.enumeration.ResourceType;
+import models.resource.Resource;
 
 @Entity
 public class IssueComment extends Comment {
@@ -135,4 +142,30 @@
             update();
         }
     }
+
+    public static IssueComment from(PostingComment postingComment, Issue issue) {
+        User user = new User();
+        user.id = postingComment.authorId;
+        user.loginId = postingComment.authorLoginId;
+        user.name = postingComment.authorName;
+
+        String contents = postingComment.contents;
+
+        IssueComment issueComment = new IssueComment(issue, user, contents);
+        issueComment.createdDate = postingComment.createdDate;
+        issueComment.authorId = postingComment.authorId;
+        issueComment.authorLoginId = postingComment.authorLoginId;
+        issueComment.authorName = postingComment.authorName;
+        issueComment.projectId = postingComment.projectId;
+        return issueComment;
+    }
+
+    public static List<IssueComment> from(Collection<PostingComment> postingComments, Issue issue) {
+        List<IssueComment> issueComments = new ArrayList<>();
+        for (PostingComment postingComment : postingComments) {
+            issueComments.add(IssueComment.from(postingComment, issue));
+        }
+
+        return issueComments;
+    }
 }
app/models/Posting.java
--- app/models/Posting.java
+++ app/models/Posting.java
@@ -4,17 +4,25 @@
 
 package models;
 
-import models.enumeration.ResourceType;
-import models.resource.Resource;
-import utils.JodaDateUtil;
+import static com.avaje.ebean.Expr.*;
 
-import javax.persistence.*;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import javax.persistence.CascadeType;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.ManyToMany;
+import javax.persistence.OneToMany;
+import javax.persistence.OneToOne;
+import javax.persistence.Table;
+import javax.persistence.Transient;
+import javax.persistence.UniqueConstraint;
 
-import static com.avaje.ebean.Expr.eq;
+import models.enumeration.ResourceType;
+import models.resource.Resource;
+import utils.JodaDateUtil;
 
 @Entity
 @Table(uniqueConstraints = @UniqueConstraint(columnNames = {"project_id", "number"}))
@@ -159,4 +167,20 @@
         }
         return null;
     }
+
+    public static Posting from(Issue issue) {
+        Posting posting = new Posting();
+
+        posting.title = issue.title;
+        posting.body = issue.body;
+        posting.history = issue.history;
+        posting.createdDate = issue.createdDate;
+        posting.updatedDate = issue.updatedDate;
+        posting.authorId = issue.authorId;
+        posting.authorLoginId = issue.authorLoginId;
+        posting.authorName = issue.authorName;
+        posting.project = issue.project;
+
+        return posting;
+    }
 }
app/models/PostingComment.java
--- app/models/PostingComment.java
+++ app/models/PostingComment.java
@@ -102,4 +102,10 @@
             }
         };
     }
+
+    public static List<PostingComment> findAllBy(Posting posting) {
+        return find.where()
+                .eq("id", posting.id)
+                .findList();
+    }
 }
conf/routes
--- conf/routes
+++ conf/routes
@@ -44,6 +44,9 @@
 POST           /-_-api/v1/owners/:owner/projects/:projectName/posts                    controllers.api.BoardApi.newPostings(owner:String, projectName:String)
 PATCH          /-_-api/v1/owners/:owner/projects/:projectName/posts/:number/content    controllers.api.BoardApi.updatePostingContent(owner: String, projectName: String, number: Long)
 POST           /-_-api/v1/owners/:owner/projects/:projectName/posts/:number/comments   controllers.api.BoardApi.newPostingComment(owner:String, projectName:String, number:Long)
+
+POST           /-_-api/v1/owners/:owner/projects/:projectName/issues/imports           controllers.api.IssueApi.imports(owner:String, projectName:String)
+
 POST           /-_-api/v1/owners/:owner/projects/:projectName/postlabel/:number        controllers.api.BoardApi.updatePostLabel(owner:String, projectName:String, number:Long)
 POST           /-_-api/v1/owners/:owner/projects/:projectName/issues                   controllers.api.IssueApi.newIssues(owner:String, projectName:String)
 POST           /-_-api/v1/owners/:owner/projects/:projectName/issues/:number/comments  controllers.api.IssueApi.newIssueComment(owner:String, projectName:String, number:Long)
Add a comment
List