Merge pull request #1305 from merico-dev/jira-server-support

fix: handle status code 404
diff --git a/models/domainlayer/ticket/sprint.go b/models/domainlayer/ticket/sprint.go
index 444be31..020276e 100644
--- a/models/domainlayer/ticket/sprint.go
+++ b/models/domainlayer/ticket/sprint.go
@@ -21,6 +21,7 @@
 	StartedDate   *time.Time
 	EndedDate     *time.Time
 	CompletedDate *time.Time
+	OriginBoardID string
 }
 
 type SprintIssue struct {
diff --git a/plugins/core/apiclient.go b/plugins/core/apiclient.go
index ed544d9..4df9942 100644
--- a/plugins/core/apiclient.go
+++ b/plugins/core/apiclient.go
@@ -187,7 +187,7 @@
 		logger.Print(fmt.Sprintf("[api-client][retry %v] %v %v", retry, method, *uri))
 		res, err = apiClient.client.Do(req)
 		if err != nil {
-			logger.Error("[api-client] error:%v", err)
+			logger.Error("[api-client] error:", err)
 			if retry < apiClient.maxRetry-1 {
 				retry += 1
 				continue
diff --git a/plugins/jira/tasks/jira_api_client.go b/plugins/jira/tasks/jira_api_client.go
index c890a12..f59b47d 100644
--- a/plugins/jira/tasks/jira_api_client.go
+++ b/plugins/jira/tasks/jira_api_client.go
@@ -25,7 +25,7 @@
 		map[string]string{
 			"Authorization": fmt.Sprintf("Basic %v", auth),
 		},
-		10*time.Second,
+		20*time.Second,
 		3,
 		scheduler,
 	)
diff --git a/plugins/jira/tasks/jira_remotelink_collector.go b/plugins/jira/tasks/jira_remotelink_collector.go
index f641f1c..4bba7d3 100644
--- a/plugins/jira/tasks/jira_remotelink_collector.go
+++ b/plugins/jira/tasks/jira_remotelink_collector.go
@@ -16,7 +16,7 @@
 	"github.com/merico-dev/lake/plugins/jira/models"
 )
 
-var ErrNotFoundIssue = errors.New("not found the issue")
+var ErrNotFoundResource = errors.New("not found the resource")
 
 type JiraApiRemotelink struct {
 	Id           uint64
@@ -91,7 +91,7 @@
 		updated := jiraIssue.Updated
 		err = issueScheduler.Submit(func() error {
 			err = collector(source, jiraApiClient, issueId)
-			if err == ErrNotFoundIssue {
+			if err == ErrNotFoundResource {
 				return nil
 			}
 			if err != nil {
@@ -123,7 +123,7 @@
 		return err
 	}
 	if res.StatusCode == http.StatusNotFound {
-		return ErrNotFoundIssue
+		return ErrNotFoundResource
 	}
 	apiRemotelinks := &JiraApiRemotelinksResponse{}
 	err = core.UnmarshalResponse(res, apiRemotelinks)
diff --git a/plugins/jira/tasks/jira_server_api_client.go b/plugins/jira/tasks/jira_server_api_client.go
index 19507ce..9a3b621 100644
--- a/plugins/jira/tasks/jira_server_api_client.go
+++ b/plugins/jira/tasks/jira_server_api_client.go
@@ -78,7 +78,7 @@
 func (v8 *ServerVersion8) collectRemotelinksByIssueId(source *models.JiraSource, jiraApiClient *JiraApiClient, issueId uint64) error {
 	var transformer v8models.RemoteLink
 	err := v8.Get(fmt.Sprintf("api/2/issue/%d/remotelink", issueId), v8.newHandlerWithIssueId(source.ID, issueId, transformer))
-	if err != nil {
+	if err != nil && err != ErrNotFoundResource {
 		logger.Error("collect remotelink", err)
 	}
 	return nil
@@ -112,7 +112,7 @@
 	query.Set("jql", jql)
 	query.Set("expand", "changelog")
 	handler := func(resp *http.Response) (int, error) {
-		return 0, v8.issueHandle(ctx, source, resp)
+		return 0, v8.issueHandle(ctx, boardId, source, resp)
 	}
 	err := v8.FetchPages(fmt.Sprintf("agile/1.0/board/%d/issue", boardId), &query, handler)
 	if err != nil {
@@ -121,7 +121,7 @@
 	return nil
 }
 
-func (v8 *ServerVersion8) issueHandle(ctx context.Context, source *models.JiraSource, resp *http.Response) error {
+func (v8 *ServerVersion8) issueHandle(ctx context.Context, boardId uint64, source *models.JiraSource, resp *http.Response) error {
 	blob, err := ioutil.ReadAll(resp.Body)
 	if err != nil {
 		logger.Error("issueHandle read response body", err)
@@ -143,6 +143,7 @@
 		return nil
 	}
 	var jiraIssues []*models.JiraIssue
+	var boardIssues []*models.JiraBoardIssue
 	for _, apiIssue := range issues {
 		sprints, issue, needCollectWorklog, worklogs, changelogs, changelogItems := apiIssue.ExtractEntities(source.ID, source.StoryPointField)
 		for _, sprintId := range sprints {
@@ -160,6 +161,8 @@
 			}
 		}
 		jiraIssues = append(jiraIssues, issue)
+		boardIssue := &models.JiraBoardIssue{SourceId: source.ID, BoardId: boardId, IssueId: apiIssue.ID}
+		boardIssues = append(boardIssues, boardIssue)
 		if needCollectWorklog {
 			err = v8.collectWorklog(source.ID, issue.IssueId)
 		} else {
@@ -185,6 +188,11 @@
 		logger.Error("jira collect issues: save jiraIssues failed", err)
 		return err
 	}
+	err = v8.db.Clauses(clause.OnConflict{DoNothing: true}).CreateInBatches(boardIssues, BatchSize).Error
+	if err != nil {
+		logger.Error("jira collect issues: save board issues failed", err)
+		return err
+	}
 	return nil
 }
 
@@ -265,6 +273,9 @@
 
 func (v8 *ServerVersion8) newHandler(sourceId uint64, transformer v8models.Transformer) func(resp *http.Response) (int, error) {
 	return func(resp *http.Response) (int, error) {
+		if resp.StatusCode == http.StatusNotFound {
+			return 0, ErrNotFoundResource
+		}
 		blob, err := ioutil.ReadAll(resp.Body)
 		if err != nil {
 			logger.Error("handler factory read response body", err)
@@ -297,6 +308,9 @@
 
 func (v8 *ServerVersion8) newHandlerWithIssueId(sourceId, issueId uint64, transformer v8models.TransformerWithIssueId) func(resp *http.Response) (int, error) {
 	return func(resp *http.Response) (int, error) {
+		if resp.StatusCode == http.StatusNotFound {
+			return 0, ErrNotFoundResource
+		}
 		blob, err := ioutil.ReadAll(resp.Body)
 		if err != nil {
 			logger.Error("handler factory read response body", err)
diff --git a/plugins/jira/tasks/jira_sprint_converter.go b/plugins/jira/tasks/jira_sprint_converter.go
index 4290519..92c4ec1 100644
--- a/plugins/jira/tasks/jira_sprint_converter.go
+++ b/plugins/jira/tasks/jira_sprint_converter.go
@@ -27,6 +27,7 @@
 	domainBoardId := didgen.NewDomainIdGenerator(&jiraModels.JiraBoard{}).Generate(sourceId, boardId)
 	sprintIdGen := didgen.NewDomainIdGenerator(&jiraModels.JiraSprint{})
 	issueIdGen := didgen.NewDomainIdGenerator(&jiraModels.JiraIssue{})
+	boardIdGen := didgen.NewDomainIdGenerator(&jiraModels.JiraBoard{})
 	// iterate all rows
 	for cursor.Next() {
 		var jiraSprint jiraModels.JiraSprint
@@ -42,6 +43,7 @@
 			StartedDate:   jiraSprint.StartDate,
 			EndedDate:     jiraSprint.EndDate,
 			CompletedDate: jiraSprint.CompleteDate,
+			OriginBoardID: boardIdGen.Generate(sourceId, jiraSprint.OriginBoardID),
 		}
 		err = lakeModels.Db.Clauses(clause.OnConflict{UpdateAll: true}).Create(sprint).Error
 		if err != nil {
diff --git a/plugins/jira/tasks/v8models/issue.go b/plugins/jira/tasks/v8models/issue.go
index b3c471e..5621bbe 100644
--- a/plugins/jira/tasks/v8models/issue.go
+++ b/plugins/jira/tasks/v8models/issue.go
@@ -72,28 +72,14 @@
 			Name    string `json:"name"`
 			ID      uint64 `json:"id,string"`
 		} `json:"priority"`
-		Labels                        []interface{} `json:"labels"`
-		Timeestimate                  interface{}   `json:"timeestimate"`
-		Aggregatetimeoriginalestimate interface{}   `json:"aggregatetimeoriginalestimate"`
-		Versions                      []interface{} `json:"versions"`
-		Issuelinks                    []interface{} `json:"issuelinks"`
-		Assignee                      *struct {
-			Self         string `json:"self"`
-			Name         string `json:"name"`
-			Key          string `json:"key"`
-			EmailAddress string `json:"emailAddress"`
-			AvatarUrls   struct {
-				Four8X48  string `json:"48x48"`
-				Two4X24   string `json:"24x24"`
-				One6X16   string `json:"16x16"`
-				Three2X32 string `json:"32x32"`
-			} `json:"avatarUrls"`
-			DisplayName string `json:"displayName"`
-			Active      bool   `json:"active"`
-			TimeZone    string `json:"timeZone"`
-		} `json:"assignee"`
-		Updated core.Iso8601Time `json:"updated"`
-		Status  struct {
+		Labels                        []interface{}    `json:"labels"`
+		Timeestimate                  interface{}      `json:"timeestimate"`
+		Aggregatetimeoriginalestimate interface{}      `json:"aggregatetimeoriginalestimate"`
+		Versions                      []interface{}    `json:"versions"`
+		Issuelinks                    []interface{}    `json:"issuelinks"`
+		Assignee                      *User            `json:"assignee"`
+		Updated                       core.Iso8601Time `json:"updated"`
+		Status                        struct {
 			Self           string `json:"self"`
 			Description    string `json:"description"`
 			IconURL        string `json:"iconUrl"`
@@ -115,26 +101,12 @@
 			RemainingEstimateSeconds int64  `json:"remainingEstimateSeconds"`
 			TimeSpentSeconds         int    `json:"timeSpentSeconds"`
 		} `json:"timetracking"`
-		Archiveddate          interface{} `json:"archiveddate"`
-		Aggregatetimeestimate *int64      `json:"aggregatetimeestimate"`
-		Summary               string      `json:"summary"`
-		Creator               struct {
-			Self         string `json:"self"`
-			Name         string `json:"name"`
-			Key          string `json:"key"`
-			EmailAddress string `json:"emailAddress"`
-			AvatarUrls   struct {
-				Four8X48  string `json:"48x48"`
-				Two4X24   string `json:"24x24"`
-				One6X16   string `json:"16x16"`
-				Three2X32 string `json:"32x32"`
-			} `json:"avatarUrls"`
-			DisplayName string `json:"displayName"`
-			Active      bool   `json:"active"`
-			TimeZone    string `json:"timeZone"`
-		} `json:"creator"`
-		Subtasks []interface{} `json:"subtasks"`
-		Reporter struct {
+		Archiveddate          interface{}   `json:"archiveddate"`
+		Aggregatetimeestimate *int64        `json:"aggregatetimeestimate"`
+		Summary               string        `json:"summary"`
+		Creator               User          `json:"creator"`
+		Subtasks              []interface{} `json:"subtasks"`
+		Reporter              struct {
 			Self         string `json:"self"`
 			Name         string `json:"name"`
 			Key          string `json:"key"`
@@ -191,7 +163,7 @@
 		StatusName:         i.Fields.Status.Name,
 		StatusKey:          i.Fields.Status.StatusCategory.Key,
 		ResolutionDate:     i.Fields.Resolutiondate.ToNullableTime(),
-		CreatorAccountId:   i.Fields.Creator.EmailAddress,
+		CreatorAccountId:   i.Fields.Creator.getAccountId(),
 		CreatorDisplayName: i.Fields.Creator.DisplayName,
 		Created:            i.Fields.Created.ToTime(),
 		Updated:            i.Fields.Updated.ToTime(),
@@ -200,7 +172,7 @@
 		result.EpicKey = i.Fields.Epic.Key
 	}
 	if i.Fields.Assignee != nil {
-		result.AssigneeAccountId = i.Fields.Assignee.EmailAddress
+		result.AssigneeAccountId = i.Fields.Assignee.getAccountId()
 		result.AssigneeDisplayName = i.Fields.Assignee.DisplayName
 	}
 	if i.Fields.Priority != nil {
diff --git a/plugins/jira/tasks/v8models/remotelink.go b/plugins/jira/tasks/v8models/remotelink.go
index 8069794..7cab0cf 100644
--- a/plugins/jira/tasks/v8models/remotelink.go
+++ b/plugins/jira/tasks/v8models/remotelink.go
@@ -2,6 +2,7 @@
 
 import (
 	"encoding/json"
+	"gorm.io/datatypes"
 
 	"github.com/merico-dev/lake/plugins/jira/models"
 )
@@ -34,7 +35,7 @@
 	} `json:"object"`
 }
 
-func (r RemoteLink) toToolLayer(sourceId, issueId uint64) *models.JiraRemotelink {
+func (r RemoteLink) toToolLayer(sourceId, issueId uint64, raw json.RawMessage) *models.JiraRemotelink {
 	return &models.JiraRemotelink{
 		SourceId:     sourceId,
 		RemotelinkId: r.ID,
@@ -42,18 +43,24 @@
 		Self:         r.Self,
 		Title:        r.Object.Title,
 		Url:          r.Object.URL,
+		RawJson:      datatypes.JSON(raw),
 	}
 }
 
 func (RemoteLink) FromAPI(sourceId, issueId uint64, raw json.RawMessage) (interface{}, error) {
-	var vv []RemoteLink
-	err := json.Unmarshal(raw, &vv)
+	var msgs []json.RawMessage
+	err := json.Unmarshal(raw, &msgs)
 	if err != nil {
 		return nil, err
 	}
-	list := make([]*models.JiraRemotelink, len(vv))
-	for i, item := range vv {
-		list[i] = item.toToolLayer(sourceId, issueId)
+	var list []*models.JiraRemotelink
+	for _, msg := range msgs {
+		var remoteLink RemoteLink
+		err = json.Unmarshal(msg, &remoteLink)
+		if err != nil {
+			return nil, err
+		}
+		list = append(list, remoteLink.toToolLayer(sourceId, issueId, msg))
 	}
 	return list, nil
 }
diff --git a/plugins/jira/tasks/v8models/user.go b/plugins/jira/tasks/v8models/user.go
index d463f51..c5412bc 100644
--- a/plugins/jira/tasks/v8models/user.go
+++ b/plugins/jira/tasks/v8models/user.go
@@ -5,6 +5,7 @@
 	Key          string `json:"key"`
 	Name         string `json:"name"`
 	EmailAddress string `json:"emailAddress"`
+	AccountId    string `json:"accountId"`
 	AvatarUrls   struct {
 		Four8X48  string `json:"48x48"`
 		Two4X24   string `json:"24x24"`
@@ -17,3 +18,13 @@
 	TimeZone    string `json:"timeZone"`
 	Locale      string `json:"locale"`
 }
+
+func (u *User) getAccountId() string {
+	if u == nil {
+		return ""
+	}
+	if u.AccountId != "" {
+		return u.AccountId
+	}
+	return u.EmailAddress
+}