diff --git a/.gitignore b/.gitignore index 6fe9f205..76f33fbc 100644 --- a/.gitignore +++ b/.gitignore @@ -21,5 +21,5 @@ backlog/project.json backlog/project mapping/*.json mapping2/*.json -backlog-migration-jira-*\.jar log/ +test.properties diff --git a/README.md b/README.md index e69de29b..ff9acf1f 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,415 @@ +# Backlog Migration for JIRA + +Migrate your projects from JIRA to [Backlog]. +(英語の下に日本文が記載されています) + +**Backlog Migration for JIRA is in beta. To avoid problems, create a new project and import data before importing data to the existing project in Backlog.** + +* Backlog + * [https://backlog.com](https://backlog.com/) + +## Requirements +* **Java 8** +* The Backlog Space's **administrator** roles. + +https://github.com/nulab/BacklogMigration-Jira/releases + +Download +------------ + +Please download the jar file from this link, and run from the command line as follows. + +https://github.com/nulab/BacklogMigration-Jira/releases/download/0.1.0b1/backlog-migration-jira-0.1.0b1.jar + + java -jar backlog-migration-jira-[latest version].jar + +To use via proxy server, run from the command line as follows. + + java -Dhttp.proxyHost=[proxy host name] -Dhttp.proxyPort=[proxy port] -jar backlog-migration-jira-[latest version].jar + +## How to use +### Preparation + +Create a directory. + + $ mkdir work + $ cd work + +### Export command + +Run the [**export**] command to export issues and mapping files. +(The mapping file is used to link data between Backlog and JIRA.) + + java -jar backlog-migration-jira-[latest version].jar \ + export \ + --jira.username [JIRA user name] \ + --jira.password [JIRA user password] \ + --jira.url [URL of JIRA] \ + --backlog.key [Backlog of API key] \ + --backlog.url [URL of Backlog] \ + --projectKey [JIRA project identifier]:[Backlog project key] +     +Sample commands: + + java -jar backlog-migration-jira-[latest version].jar \ + export \ + --jira.username XXXXXXXXXXXXX \ + --jira.password XXXXXXXXXXXXX \ + --jira.url https://nulab.atlassian.net \ + --backlog.key XXXXXXXXXXXXX \ + --backlog.url https://nulab.backlog.jp \ + --projectKey jira_project:BACKLOG_PROJECT +         +Issues and the mapping files are created as follows. + + . + ├── backlog + │   ├── project + │ │ └── ... + │ └── project.json + ├── log + │   ├── backlog-migration-jira-warn.log + │   └── backlog-migration-jira.log + └── mapping + ├── priorities.json + ├── statuses.json + └── users.json + +- 1.mapping / users.json (users) +- 2.mapping / priorities.json (priority) +- 3.mapping / statuses.json (state) + +#### About mapping projects + +Specify the destination project for **--projectKey** option by colon (:). i.e. [**--projectKey jira_project:BACKLOG_PROJECT**] migrates **jira_project** jira project in **BACKLOG_PROJECT** backlog project. + + --projectKey [JIRA project identifier]:[Backlog project key] + +### Fix the mapping file + +A file in json format will be automatically created. +The items that could not be automatically associated with Backlog will be blank. +The blanks need to be filled using the items in the description. + + { + "Description": "The values accepted for User in Backlog are "admin,tanaka". " + "Mappings": [{ + "jira": "admin", + "backlog": "admin" + }, { + "jira": "satou", + "backlog": "" + }] + } + +### Import command + +Run the [**import**] command to import data. + + java -jar backlog-migration-jira-[latest version].jar \ + import \ + --jira.username [JIRA user name] \ + --jira.password [JIRA user password] \ + --jira.url [URL of JIRA] \ + --backlog.key [Backlog of API key] \ + --backlog.url [URL of Backlog] \ + --projectKey [JIRA project identifier]:[Backlog project key] +     +Sample commands: + + java -jar backlog-migration-jira-[latest version].jar \ + import \ + --jira.username XXXXXXXXXXXXX \ + --jira.password XXXXXXXXXXXXX \ + --jira.url https://nulab.atlassian.net \ + --backlog.key XXXXXXXXXXXXX \ + --backlog.url https://nulab.backlog.jp \ + --projectKey jira_project:BACKLOG_PROJECT + +## Limitation + +### Supported JIRA version +JIRA **SaaS and JIRA REST API v2** is supported. + +### Backlog's user roles +This program is for the users with the Space's **administrator** roles. + +### Migration project with custom fields +Only applied to **max** or **platina** plan. + +### About Project +* Text formatting rules: **markdown** +* Some changes will be applied to the JIRA's project identifier to meet the project key format in Backlog. + +**Hyphen** → **underscore** + +Single-byte **lowercase** character → Single-byte **uppercase** character + +### About custom fields +* Versions and users will be registered as lists and will be the fixed values. +* User will not be converted. +* Boolean values will be registered in radio button format of "Yes" or "No". +* The date and time are converted to dates and registered. + +### About change logs +* Worklog is not supported. Scheduled for next release. + +### About limitations in Backlog +* Importing users will be terminated if the number of users will exceed the limit in Backlog. + +## Re-importing + +When the project key in Backlog and JIRA matches, they will be considered as the same project and data will be imported as follows. + +**If the person migrating data is not in the project.** + +The project will not be imported and the following message will be shown. Join the project to migrate data. +Importing to this project failed. You are not a member of this project. Join the project to add issues. + + +| Item | Specifications | +|:-----------|------------| +| Project | The project will not be added when there is a project with same project key. The issues and wikis will be added to the existing project. | +| Issues | Issues with matching subject, creator, creation date are not registered. | +| Custom fields | The custom field will not be added when there is a custom field with same name. | + +## Third party tracking system + +In this application, we collect information such as source URL, destination URL, migration source project key, migration destination project key, by third party service (Mixpanel) in order to grasp usage situation. +Please refer to Mixpanel's privacy policy for data to be tracked. Also, if you do not want your data to be used in Mixpanel, you can suspend (opt out) by the following methods. + +If you want to opt out, please use the optOut option. + + java -jar backlog-migration-jira-[latest version].jar \ + import \ + --jira.username XXXXXXXXXXXXX \ + --jira.password XXXXXXXXXXXXX \ + --jira.url https://nulab.atlassian.net \ + --backlog.key XXXXXXXXXXXXX \ + --backlog.url https://nulab.backlog.jp \ + --projectKey jira_project:BACKLOG_PROJECT + --optOut + +### Mixpanel + +[Mixpanel's Privacy Policy](https://mixpanel.com/privacy/ "Mixpanel's Privacy Policy") + +## License + +MIT License + +* http://www.opensource.org/licenses/mit-license.php + +## Inquiry + +Please contact us if you encounter any problems during the JIRA to Backlog migration. + +https://backlog.com/contact/ + + + +# Backlog Migration for JIRA +JIRAのプロジェクトを[Backlog]に移行するためのツールです。 + +**Backlog Migration for JIRAはベータバージョンです。Backlog上の既存プロジェクトにインポートする場合は、先に新しく別プロジェクトを作成し、こちらにインポートし内容を確認後、正式なプロジェクトにインポートしてください** + +* Backlog + * [https://www.backlog.jp](https://www.backlog.jp/) + +## 必須要件 +* **Java 8** +* Backlogの **管理者権限** + +https://github.com/nulab/BacklogMigration-Jira/releases + +ダウンロード +------------ + +こちらのリンクからjarファイルをダウンロードし、以下のようにコマンドラインから実行します。 + +https://github.com/nulab/BacklogMigration-Jira/releases/download/0.1.0b1/backlog-migration-jira-0.1.0b1.jar + + java -jar backlog-migration-jira-[最新バージョン].jar + +プロキシ経由で使用する場合は、以下のように実行します。 + + java -Dhttp.proxyHost=[プロキシサーバのホスト名] -Dhttp.proxyPort=[プロキシサーバのポート番号] -jar backlog-migration-jira-[最新バージョン].jar + +## 使い方 +### 前準備 + +作業用のディレクトリを作成します。 + + $ mkdir work + $ cd work + +### エクスポートコマンド + +[**export**]コマンドを実行し、課題等のエクスポートとマッピングファイルを準備する必要があります。 +(マッピングファイルはJIRAとBacklogのデータを対応付けるために使用します。) + + java -jar backlog-migration-jira-[最新バージョン].jar \ + export \ + --jira.username [JIRAのユーザー名] \ + --jira.password [JIRAのパスワード] \ + --jira.url [JIRAのURL] \ + --backlog.key [BacklogのAPIキー] \ + --backlog.url [BacklogのURL] \ + --projectKey [JIRAプロジェクト識別子]:[Backlogプロジェクトキー] + +サンプルコマンド: + + java -jar backlog-migration-jira-[最新バージョン].jar \ + export \ + --jira.username XXXXXXXXXXXXX \ + --jira.password XXXXXXXXXXXXX \ + --jira.url https://nulab.atlassian.net \ + --backlog.key XXXXXXXXXXXXX \ + --backlog.url https://nulab.backlog.jp \ + --projectKey jira_project:BACKLOG_PROJECT + +以下のように課題等とマッピングファイルが作成されます。 + + . + ├── backlog + │   ├── project + │ │ └── ... + │ └── project.json + ├── log + │   ├── backlog-migration-jira-warn.log + │   └── backlog-migration-jira.log + └── mapping + ├── priorities.json + ├── statuses.json + └── users.json + +- 1.mapping/users.json(ユーザー) +- 2.mapping/priorities.json(優先度) +- 3.mapping/statuses.json(状態) + +#### プロジェクトのマッピングについて + +**--projectKey** オプションに以下のようにコロン **[:]** 区切りでプロジェクトを指定することで、Backlog側の移行先のプロジェクトを指定することができます。 + + --projectKey [JIRAのプロジェクト識別子]:[Backlogのプロジェクトキー] + +### マッピングファイルを修正 + +自動作成されるファイルは以下のようにjson形式で出力されます。 +Backlog側の空白の項目は自動設定できなかった項目になります。 +descriptionにある項目を使い、空白を埋める必要が有ります。 + + { + "description": "Backlogに設定可能なユーザーは[admin,tanaka]です。", + "mappings": [{ + "jira": "admin", + "backlog": "admin" + }, { + "jira": "satou", + "backlog": "" + }] + } + +### インポートコマンド + +[**import**]コマンドを実行することでインポートを実行します。 + + java -jar backlog-migration-jira-[最新バージョン].jar \ + import \ + --jira.username [JIRAのユーザー名] \ + --jira.password [JIRAのパスワード] \ + --jira.url [JIRAのURL] \ + --backlog.key [BacklogのAPIキー] \ + --backlog.url [BacklogのURL] \ + --projectKey [JIRAプロジェクト識別子]:[Backlogプロジェクトキー] + +サンプルコマンド: + + java -jar backlog-migration-jira-[最新バージョン].jar \ + import \ + --jira.username XXXXXXXXXXXXX \ + --jira.password XXXXXXXXXXXXX \ + --jira.url https://nulab.atlassian.net \ + --backlog.key XXXXXXXXXXXXX \ + --backlog.url https://nulab.backlog.jp \ + --projectKey jira_project:BACKLOG_PROJECT + +## 制限事項 + +### JIRAの対応バージョン +JIRAの対応バージョンは **SaaS版かつJIRA REST API v2** になります。 + +### 実行できるユーザー +Backlogの **管理者権限** が必要になります。 + +### カスタムフィールドを使用しているプロジェクトの移行 +Backlogで **マックスプラン以上** のプランを契約している必要があります。 + +### プロジェクトについて +* テキスト整形のルール: **markdown** +* JIRAのプロジェクト識別子は以下のように変換されBacklogのプロジェクトキーとして登録されます。 + +**ハイフン** → **アンダースコア** + +**半角英子文字** → **半角英大文字** + +### カスタムフィールドについて +* バージョンとユーザーはリストとして登録され固定値になります。 +* ユーザーは変換されません。 +* 真偽値は[はい]、[いいえ]のラジオボタン形式で登録されます。 +* 日時は日付に変換され登録されます。 + +### Change logについて +* Worklogには対応していません。(次回リリースで対応予定) + +### Backlog側の制限について +* Backlogで登録可能なユーザー数を超えた場合、インポートは中断されます。 + +## 再インポートの仕様 + +Backlog側にJIRAに対応するプロジェクトキーがある場合同一プロジェクトとみなし、以下の仕様でインポートされます。 + +※ 対象のプロジェクトに 参加していない場合 + +対象プロジェクトはインポートされず以下のメッセージが表示されます。対象プロジェクトをインポートする場合は、対象プロジェクトに参加してください。[⭕️⭕️を移行しようとしましたが⭕️⭕️に参加していません。移行したい場合は⭕️⭕️に参加してください。] + +|項目|仕様| +|:-----------|------------| +|プロジェクト|同じプロジェクトキーのプロジェクトがある場合、プロジェクトを作成せず対象のプロジェクトに課題やWikiを登録します。| +|課題|件名、作成者、作成日が一致する課題は登録されません。| +|カスタム属性|同じ名前のカスタム属性がある場合登録しません。| + +## 第三者のトラッキングシステム + +当アプリケーションでは、利用状況把握のために、サードパーティのサービス(Mixpanel)によって、移行元のURL、移行先のURL、移行元のプロジェクトキー、移行先のプロジェクトキーなどの情報を収集します。 +トラッキングするデータについてはMixpanelのプライバシーポリシーを参照してください。また、お客様のデータがMixpanelで使用されることを望まない場合は、以下に掲げる方法で使用停止(オプトアウト)することができます。 + +次のようにoptOutオプションを使用することで使用停止(オプトアウト)することができます。 + + java -jar backlog-migration-jira-[最新バージョン].jar \ + import \ + --jira.username XXXXXXXXXXXXX \ + --jira.password XXXXXXXXXXXXX \ + --jira.url https://nulab.atlassian.net \ + --backlog.key XXXXXXXXXXXXX \ + --backlog.url https://nulab.backlog.jp \ + --projectKey jira_project:BACKLOG_PROJECT + --optOut + +### Mixpanel + +[Mixpanelのプライバシーポリシー](https://mixpanel.com/privacy/ "Mixpanelのプライバシーポリシー") + +## License + +MIT License + +* http://www.opensource.org/licenses/mit-license.php + +## お問い合わせ + +お問い合わせは下記サイトからご連絡ください。 + +https://www.backlog.jp/contact/ + +[Backlog]: https://www.backlog.jp/ diff --git a/build.sbt b/build.sbt index eda3ecf3..eec44634 100644 --- a/build.sbt +++ b/build.sbt @@ -2,8 +2,6 @@ import sbt.Keys._ lazy val projectVersion = "0.1.0b1" -assemblyOutputPath in assembly := file(s"./backlog-migration-jira-$projectVersion.jar") - lazy val commonSettings = Seq( organization := "com.nulabinc", version := projectVersion, @@ -115,18 +113,18 @@ lazy val mappingConverter = (project in file("mapping-converter")) ) .dependsOn(mappingBase, mappingFile) -//lazy val mappingCollector = (project in file("mapping-collector")) -// .settings(commonSettings: _*) -// .settings( -// name := "backlog-jira-mapping-collector", -// scapegoatVersion := "1.1.0", -// scapegoatDisabledInspections := Seq( -// "NullParameter", -// "CatchThrowable", -// "NoOpOverride" -// ) -// ) -// .dependsOn(mappingBase) +lazy val mappingCollector = (project in file("mapping-collector")) + .settings(commonSettings: _*) + .settings( + name := "backlog-jira-mapping-collector", + scapegoatVersion := "1.1.0", + scapegoatDisabledInspections := Seq( + "NullParameter", + "CatchThrowable", + "NoOpOverride" + ) + ) + .dependsOn(jira, mappingBase) lazy val mappingFile = (project in file("mapping-file")) .settings(commonSettings: _*) @@ -170,6 +168,7 @@ lazy val client = (project in file("jira-client")) ) lazy val root = (project in file(".")) + .enablePlugins(BuildInfoPlugin) .settings(commonSettings: _*) .settings( name := "backlog-migration-jira", @@ -186,7 +185,9 @@ lazy val root = (project in file(".")) ), test in assembly := {}, scapegoatVersion := "1.1.0", - scapegoatDisabledInspections := Seq("NullParameter", "CatchThrowable", "NoOpOverride") + scapegoatDisabledInspections := Seq("NullParameter", "CatchThrowable", "NoOpOverride"), + buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion), + buildInfoPackage := "com.nulabinc.backlog.j2b.buildinfo" ) - .dependsOn(common % "test->test;compile->compile", importer, exporter, writer, client, jira, mappingFile, mappingConverter) + .dependsOn(common % "test->test;compile->compile", importer, exporter, writer, client, jira, mappingFile, mappingConverter, mappingCollector) .aggregate(common, importer, exporter, writer, client, jira, mappingFile, mappingConverter) \ No newline at end of file diff --git a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/AttachmentFilter.scala b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/AttachmentFilter.scala index 0951a6b9..22161a6b 100644 --- a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/AttachmentFilter.scala +++ b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/AttachmentFilter.scala @@ -3,9 +3,11 @@ package com.nulabinc.backlog.j2b.exporter import com.nulabinc.jira.client.domain._ import com.nulabinc.jira.client.domain.issue.Issue +import scala.util.matching.Regex + object AttachmentFilter { - val fileNamePattern = """\[\^(.+?)\]""".r + val fileNamePattern: Regex = """\[\^(.+?)\]""".r def filteredIssue(issue: Issue, comments: Seq[Comment]): Issue = { val issueAttachments = issue.attachments diff --git a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogFilter.scala b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogFilter.scala new file mode 100644 index 00000000..81a79b1a --- /dev/null +++ b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogFilter.scala @@ -0,0 +1,52 @@ +package com.nulabinc.backlog.j2b.exporter + +import com.nulabinc.jira.client.domain.{Component, Version} +import com.nulabinc.jira.client.domain.changeLog._ +import com.nulabinc.jira.client.domain.field.{CustomLabel, Field} + +object ChangeLogFilter { + + def filter(definitions: Seq[Field], components: Seq[Component], versions: Seq[Version], changeLogs: Seq[ChangeLog]): Seq[ChangeLog] = { + changeLogs.map { changeLog => + val items = changeLog.items.map { item => + item.field match { + case ComponentChangeLogItemField => + List( + item.from, + item.to + ).flatten.flatMap { value => + components.find(_.id == value.toLong) + }.length match { + case n if n > 0 => item + case _ => item.copy(field = DefaultField("deleted_category"), fieldId = None) + } + case FixVersion => + List( + item.from, + item.to + ).flatten.flatMap { value => + versions.find(_.id == value.toLong) + }.length match { + case n if n > 0 => item + case _ => item.copy(field = DefaultField("deleted_version"), fieldId = None) + } + case LinkChangeLogItemField => item.copy(field = DefaultField("link_issue"), fieldId = None) + case _ => item.field match { + case DefaultField(fieldId) => definitions.find(_.name == fieldId) match { + case Some(definition) if definition.schema.isDefined => + if (definition.schema.get.customType.contains(CustomLabel)) + item.copy( + fromDisplayString = item.fromDisplayString.map(_.replace(" ", ",")), + toDisplayString = item.toDisplayString.map(_.replace(" ", ",")) + ) + else item + case _ => item + } + case _ => item + } + } + } + changeLog.copy(items = items.filterNot(_.field == WorkIdChangeLogItemField)) + } + } +} diff --git a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogReducer.scala b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogReducer.scala index 40d66089..e15ef333 100644 --- a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogReducer.scala +++ b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogReducer.scala @@ -6,6 +6,7 @@ import com.nulabinc.backlog.migration.common.domain._ import com.nulabinc.backlog.migration.common.utils.{IOUtil, Logging} import com.nulabinc.jira.client._ import com.nulabinc.jira.client.domain.Attachment +import com.nulabinc.jira.client.domain.changeLog._ import com.osinka.i18n.Messages import scalax.file.Path @@ -41,13 +42,24 @@ private [exporter] class ChangeLogReducer(issueDirPath: Path, case "timeestimate" => val message = Messages("common.change_comment", Messages("common.timeestimate"), getValue(changeLog.optOriginalValue), getValue(changeLog.optNewValue)) (None, s"${message}\n") - // TODO: Check project -// case "project_id" => -// val message = Messages("common.change_comment", -// Messages("common.project"), -// getProjectName(changeLog.optOriginalValue), -// getProjectName(changeLog.optNewValue)) -// (None, s"${message}\n") + case ParentChangeLogItemField.value => + val message = Messages("common.change_comment", Messages("common.parent_issue"), getValue(changeLog.optOriginalValue), getValue(changeLog.optNewValue)) + (None, s"${message}\n") + case "deleted_category" => + val message = Messages("common.change_comment", Messages("common.category"), getValue(changeLog.optOriginalValue), getValue(changeLog.optNewValue)) + (None, s"${message}\n") + case "deleted_version" => + val message = Messages("common.change_comment", Messages("common.version"), getValue(changeLog.optOriginalValue), getValue(changeLog.optNewValue)) + (None, s"${message}\n") + case "link_issue" => + val message = Messages("common.change_comment", Messages("common.link"), getValue(changeLog.optOriginalValue), getValue(changeLog.optNewValue)) + (None, s"${message}\n") + case LabelsChangeLogItemField.value => + val message = Messages("common.change_comment", Messages("common.labels"), getValue(changeLog.optOriginalValue), getValue(changeLog.optNewValue)) + (None, s"${message}\n") + case SprintChangeLogItemField.value => + val message = Messages("common.change_comment", Messages("common.sprint"), getValue(changeLog.optOriginalValue), getValue(changeLog.optNewValue)) + (None, s"${message}\n") case _ => (Some(changeLog.copy(optNewValue = ValueReducer.reduce(targetComment, changeLog))), "") } @@ -86,9 +98,9 @@ private [exporter] class ChangeLogReducer(issueDirPath: Path, val path = backlogPaths.issueAttachmentPath(dir, attachmentInfo.name) IOUtil.createDirectory(dir) issueService.downloadAttachments(attachmentInfoId.toLong, path, attachmentInfo.name) match { - case Success => + case DownloadSuccess => (Some(changeLog), "") - case Failure => + case DownloadFailure => val emptyMessage = Messages( "export.attachment.empty", changeLog.optOriginalValue.getOrElse(Messages("common.empty")), diff --git a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogsPlayer.scala b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogsPlayer.scala index e718d232..e4a46062 100644 --- a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogsPlayer.scala +++ b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogsPlayer.scala @@ -1,6 +1,6 @@ package com.nulabinc.backlog.j2b.exporter -import com.nulabinc.jira.client.domain.changeLog.{ChangeLog, ChangeLogItemField} +import com.nulabinc.jira.client.domain.changeLog.{ChangeLog, ChangeLogItemField, ParentChangeLogItemField} object Calc { @@ -26,12 +26,12 @@ object Calc { case class History(id: Long, from: Option[String], to: Option[String]) { def fromToSeq(): Seq[String] = from match { - case Some(f) => f.split(",").toSeq + case Some(f) => f.split(",").map(_.trim).toSeq case _ => Seq.empty[String] } def toToSeq(): Seq[String] = to match { - case Some(t) => t.split(",").toSeq + case Some(t) => t.split(",").map(_.trim).toSeq case _ => Seq.empty[String] } @@ -63,7 +63,7 @@ object ChangeLogsPlayer { val r = result.find(_.id == changeLog.id) if (changeLogItem.field == targetField) { - changeLogItem.copy(fromDisplayString = r.map(_.from.mkString(", ")), toDisplayString = r.map(_.to.mkString(", "))) + changeLogItem.copy(fromDisplayString = r.map(_.from.mkString(",")), toDisplayString = r.map(_.to.mkString(","))) } else changeLogItem }.distinct @@ -85,12 +85,18 @@ object ChangeLogsPlayer { private def concat(targetField: ChangeLogItemField, changeLog: ChangeLog): ChangeLog = { def makeStrings(array: Seq[String]) = { - if (array.nonEmpty) Some(array.mkString(", ")) + if (array.nonEmpty) Some(array.mkString(",")) else None } - val fromNames = changeLog.items.filter(_.field == targetField).flatten(_.fromDisplayString) - val toNames = changeLog.items.filter(_.field == targetField).flatten(_.toDisplayString) + val fromNames = targetField match { + case ParentChangeLogItemField => changeLog.items.filter(_.field == targetField).flatten(_.from) + case _ => changeLog.items.filter(_.field == targetField).flatten(_.fromDisplayString) + } + val toNames = targetField match { + case ParentChangeLogItemField => changeLog.items.filter(_.field == targetField).flatten(_.to) + case _ => changeLog.items.filter(_.field == targetField).flatten(_.toDisplayString) + } val fromStrings = makeStrings(fromNames) val toStrings = makeStrings(toNames) diff --git a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/CommentFileWriter.scala b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/CommentFileWriter.scala index b2864640..5d7e95bd 100644 --- a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/CommentFileWriter.scala +++ b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/CommentFileWriter.scala @@ -1,5 +1,6 @@ package com.nulabinc.backlog.j2b.exporter +import java.util.Date import javax.inject.Inject import com.nulabinc.backlog.j2b.issue.writer.convert._ @@ -8,7 +9,7 @@ import com.nulabinc.backlog.j2b.jira.writer.CommentWriter import com.nulabinc.backlog.migration.common.conf.BacklogPaths import com.nulabinc.backlog.migration.common.convert.Convert import com.nulabinc.backlog.migration.common.domain._ -import com.nulabinc.backlog.migration.common.utils.{DateUtil, IOUtil} +import com.nulabinc.backlog.migration.common.utils.IOUtil import com.nulabinc.jira.client.domain._ import com.nulabinc.jira.client.domain.changeLog.ChangeLog import spray.json._ @@ -20,12 +21,12 @@ class CommentFileWriter @Inject()(implicit val commentWrites: CommentWrites, issueService: IssueService) extends CommentWriter { override def write(backlogIssue: BacklogIssue, comments: Seq[Comment], changeLogs: Seq[ChangeLog], attachments: Seq[Attachment]) = { - val backlogChangeLogsAsComment = changeLogs.map(Convert.toBacklog(_)) - val backlogCommentsAsComment = comments.map(Convert.toBacklog(_)) + val backlogChangeLogsAsComment = changeLogs.map(c => (Convert.toBacklog(c), c.createdAt.toDate)) + val backlogCommentsAsComment = comments.map(c => (Convert.toBacklog(c), c.createdAt.toDate)) val backlogComments = backlogChangeLogsAsComment ++ backlogCommentsAsComment // TODO: sort? val reducedComments = backlogComments.zipWithIndex.map { case (comment, index) => - exportComment(comment, backlogIssue, backlogComments, attachments, index) + exportComment(comment._1, backlogIssue, backlogComments.map(_._1), attachments, comment._2, index) } Right(reducedComments) } @@ -34,12 +35,12 @@ class CommentFileWriter @Inject()(implicit val commentWrites: CommentWrites, issue: BacklogIssue, comments: Seq[BacklogComment], attachments: Seq[Attachment], + createdAt: Date, index: Int) = { import com.nulabinc.backlog.migration.common.domain.BacklogJsonProtocol._ - val commentCreated = DateUtil.tryIsoParse(comment.optCreated) - val issueDirPath = backlogPaths.issueDirectoryPath("comment", issue.id, commentCreated, index) + val issueDirPath = backlogPaths.issueDirectoryPath("comment", issue.id, createdAt, index) val changeLogReducer = new ChangeLogReducer(issueDirPath, backlogPaths, issue, comments, attachments, issueService) val commentReducer = new CommentReducer(issue.id, changeLogReducer) val reduced = commentReducer.reduce(comment) diff --git a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/Exporter.scala b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/Exporter.scala index 99fd7fbb..79e0ba59 100644 --- a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/Exporter.scala +++ b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/Exporter.scala @@ -2,12 +2,17 @@ package com.nulabinc.backlog.j2b.exporter import javax.inject.Inject +import com.nulabinc.backlog.j2b.jira.conf.JiraBacklogPaths +import com.nulabinc.backlog.j2b.jira.domain.mapping.MappingCollectDatabase import com.nulabinc.backlog.j2b.jira.domain.{CollectData, JiraProjectKey} import com.nulabinc.backlog.j2b.jira.service._ +import com.nulabinc.backlog.j2b.jira.utils.DateChangeLogConverter import com.nulabinc.backlog.j2b.jira.writer._ import com.nulabinc.backlog.migration.common.utils.{ConsoleOut, Logging, ProgressBar} import com.nulabinc.jira.client.domain._ -import com.nulabinc.jira.client.domain.changeLog.{AssigneeFieldId, ComponentChangeLogItemField, FixVersion} +import com.nulabinc.jira.client.domain.changeLog.{AssigneeFieldId, ComponentChangeLogItemField, CustomFieldFieldId, FixVersion} +import com.nulabinc.jira.client.domain.field.Field +import com.nulabinc.jira.client.domain.issue._ import com.osinka.i18n.Messages class Exporter @Inject()(projectKey: JiraProjectKey, @@ -28,104 +33,168 @@ class Exporter @Inject()(projectKey: JiraProjectKey, commentService: CommentService, commentWriter: CommentWriter, initializer: IssueInitializer, - userService: UserService) - extends Logging { + userService: UserService, + mappingCollectDatabase: MappingCollectDatabase) + extends Logging + with DateChangeLogConverter { private val console = (ProgressBar.progress _)(Messages("common.issues"), Messages("message.exporting"), Messages("message.exported")) // private val issuesInfoProgress = (ProgressBar.progress _)(Messages("common.issues_info"), Messages("message.collecting"), Messages("message.collected")) - def export(): CollectData = { - - val project = projectService.getProjectByKey(projectKey) - val categories = categoryService.all() - val versions = versionService.all() - val issueTypes = issueTypeService.all() - val fields = fieldService.all() - val priorities = priorityService.allPriorities() - val statuses = statusService.all() + def export(backlogPaths: JiraBacklogPaths): CollectData = { // project + val project = projectService.getProjectByKey(projectKey) projectWriter.write(project) ConsoleOut.boldln(Messages("message.executed", Messages("common.project"), Messages("message.exported")), 1) // category + val categories = categoryService.all() categoryWriter.write(categories) ConsoleOut.boldln(Messages("message.executed", Messages("common.category"), Messages("message.exported")), 1) // version - versionsWriter.write(versions) - ConsoleOut.boldln(Messages("message.executed", Messages("common.version"), Messages("message.exported")), 1) + val versions = versionService.all() // issue type + val issueTypes = issueTypeService.all() issueTypesWriter.write(issueTypes) ConsoleOut.boldln(Messages("message.executed", Messages("common.issue_type"), Messages("message.exported")), 1) + // issue + val statuses = statusService.all() + val total = issueService.count() + val fields = fieldService.all() + fetchIssue(statuses, categories, versions, fields, 1, total, 0, 100) + + // version & milestone + versionsWriter.write(versions, mappingCollectDatabase.milestones) + ConsoleOut.boldln(Messages("message.executed", Messages("common.version"), Messages("message.exported")), 1) + // custom field - fieldWriter.write(fields) + fieldWriter.write(mappingCollectDatabase, fields) ConsoleOut.boldln(Messages("message.executed", Messages("common.custom_field"), Messages("message.exported")), 1) - // issue - val total = issueService.count - val users = fetchIssue(Set.empty[User], statuses, 1, total, 0, 100) + // Output Jira data + val priorities = priorityService.allPriorities() + val collectedData = CollectData(mappingCollectDatabase.existUsers, statuses, priorities) + + collectedData.outputJiraUsersToFile(backlogPaths.jiraUsersJson) + collectedData.outputJiraPrioritiesToFile(backlogPaths.jiraPrioritiesJson) + collectedData.outputJiraStatusesToFile(backlogPaths.jiraStatusesJson) - CollectData(users, statuses, priorities) + collectedData } - private def fetchIssue(users: Set[User], statuses: Seq[Status], index: Long, total: Long, startAt: Long, maxResults: Long): Set[User] = { + private def fetchIssue(statuses: Seq[Status], + components: Seq[Component], + versions: Seq[Version], + fields: Seq[Field], + index: Long, total: Long, startAt: Long, maxResults: Long): Unit = { val issues = issueService.issues(startAt, maxResults) - - if (issues.isEmpty) users - else { - val collected = issues.zipWithIndex.map { + + if (issues.nonEmpty) { + issues.zipWithIndex.foreach { case (issue, i) => { // Change logs - val issueWithChangeLogs = issueService.injectChangeLogsToIssue(issue) // API Call + val issueChangeLogs = issueService.changeLogs(issue) // API Call // comments - val comments = commentService.issueComments(issueWithChangeLogs) + val comments = commentService.issueComments(issue) + + // milestone + val milestones = MilestoneExtractor.extract(fields, issue.issueFields) + milestones.foreach(m => mappingCollectDatabase.addMilestone(m)) + + // filter change logs and custom fields + val issueWithFilteredChangeLogs: Issue = issue.copy( + changeLogs = { + val filtered = ChangeLogFilter.filter(fields, components, versions, issueChangeLogs) + convertDateChangeLogs(filtered, fields) + }, + issueFields = IssueFieldFilter.filterMilestone(fields, issue.issueFields) + ) + + def saveIssueFieldValue(id: String, fieldValue: FieldValue): Unit = fieldValue match { + case StringFieldValue(value) => mappingCollectDatabase.addCustomField(id, Some(value)) + case NumberFieldValue(value) => mappingCollectDatabase.addCustomField(id, Some(value.toString)) + case ArrayFieldValue(values) => values.map(v => mappingCollectDatabase.addCustomField(id, Some(v.value))) + case OptionFieldValue(value) => saveIssueFieldValue(id, value.value) + case AnyFieldValue(value) => mappingCollectDatabase.addCustomField(id, Some(value)) + case UserFieldValue(user) => mappingCollectDatabase.addCustomField(id, Some(user.key)) + } + + issueWithFilteredChangeLogs.issueFields.foreach(v => saveIssueFieldValue(v.id, v.value)) + + // collect custom fields + val sprintDefinition = fields.find(_.name == "Sprint").get + issueWithFilteredChangeLogs.changeLogs.foreach { changeLog => + changeLog.items.foreach { changeLogItem => + changeLogItem.fieldId match { + case Some(CustomFieldFieldId(id)) if sprintDefinition.id == id => () + case Some(CustomFieldFieldId(id)) => + mappingCollectDatabase.addCustomField(id, changeLogItem.fromDisplayString) + mappingCollectDatabase.addCustomField(id, changeLogItem.toDisplayString) + case _ => () + } + } + } // export issue (values are initialized) - val initializedBacklogIssue = initializer.initialize(issueWithChangeLogs, comments) - issueWriter.write(initializedBacklogIssue, issueWithChangeLogs.createdAt.toDate) + val initializedBacklogIssue = initializer.initialize( + mappingCollectDatabase = mappingCollectDatabase, + fields = fields, + milestones = milestones, + issue = issueWithFilteredChangeLogs, + comments = comments + ) + issueWriter.write(initializedBacklogIssue, issue.createdAt.toDate) // export issue comments - val changeLogs1 = ChangeLogsPlayer.play(ComponentChangeLogItemField, initializedBacklogIssue.categoryNames, issueWithChangeLogs.changeLogs) - val changeLogs2 = ChangeLogsPlayer.play(FixVersion, initializedBacklogIssue.versionNames, changeLogs1) - val changeLogs = ChangeLogStatusConverter.convert(changeLogs2, statuses) - commentWriter.write(initializedBacklogIssue, comments, changeLogs, issueWithChangeLogs.attachments) + val categoryPlayedChangeLogs = ChangeLogsPlayer.play(ComponentChangeLogItemField, initializedBacklogIssue.categoryNames, issueWithFilteredChangeLogs.changeLogs) + val versionPlayedChangeLogs = ChangeLogsPlayer.play(FixVersion, initializedBacklogIssue.versionNames, categoryPlayedChangeLogs) + val changeLogs = ChangeLogStatusConverter.convert(versionPlayedChangeLogs, statuses) + commentWriter.write(initializedBacklogIssue, comments, changeLogs, issue.attachments) console(i + index.toInt, total.toInt) - val changeLogUsers = issueWithChangeLogs.changeLogs.map(_.author) - - val collectedUsers = Seq( - Some(issueWithChangeLogs.creator), - issueWithChangeLogs.assignee - ).filter(_.nonEmpty).flatten ++ changeLogUsers ++ users - - val changeLogItemUsers = changeLogs - .flatMap { changeLog => - changeLog.items - .filter(_.fieldId.contains(AssigneeFieldId)) // ChangeLogItem.FieldId is AssigneeFieldId - }.flatMap { changeLogItem => - Seq( - collectedUsers.find( u => changeLogItem.from.contains(u.name)) match { - case Some(user) => Some(user) - case None => userService.optUserOfKey(changeLogItem.from) - }, - collectedUsers.find( u => changeLogItem.to.contains(u.name)) match { - case Some(user) => Some(user) - case None => userService.optUserOfKey(changeLogItem.to) - } - ).flatten + val changeLogUsers = changeLogs.map(u => Some(u.author.name)) + val changeLogItemUsers = changeLogs.flatMap { changeLog => + changeLog.items.flatMap { changeLogItem => + changeLogItem.fieldId match { + case Some(AssigneeFieldId) => Set(changeLogItem.from, changeLogItem.to) + case _ => Set.empty[Option[String]] + } } - - collectedUsers ++ changeLogItemUsers + } + + Set( + Some(issue.creator.key), + issue.assignee.map(_.key) + ).foreach { maybeKey => + if (!mappingCollectDatabase.userExistsFromAllUsers(maybeKey)) { + userService.optUserOfKey(maybeKey) match { + case Some(u) if maybeKey.contains(u.name) => mappingCollectDatabase.add(u) + case Some(_) => mappingCollectDatabase.add(maybeKey) + case None => mappingCollectDatabase.addIgnoreUser(maybeKey) + } + } + } + + (changeLogUsers ++ changeLogItemUsers).foreach { maybeUserName => + if (!mappingCollectDatabase.userExistsFromAllUsers(maybeUserName)) { + userService.optUserOfName(maybeUserName) match { + case Some(u) if maybeUserName.contains(u.name) => mappingCollectDatabase.add(u) + case Some(_) => mappingCollectDatabase.add(maybeUserName) + case None => mappingCollectDatabase.add(maybeUserName) + } + } + } } } - fetchIssue(collected.flatten.toSet, statuses, index + collected.length , total, startAt + maxResults, maxResults) + fetchIssue(statuses, components, versions, fields, index + issues.length , total, startAt + maxResults, maxResults) } } diff --git a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/IssueFieldFilter.scala b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/IssueFieldFilter.scala new file mode 100644 index 00000000..93eba2b1 --- /dev/null +++ b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/IssueFieldFilter.scala @@ -0,0 +1,13 @@ +package com.nulabinc.backlog.j2b.exporter + +import com.nulabinc.jira.client.domain.field.Field +import com.nulabinc.jira.client.domain.issue.IssueField + +object IssueFieldFilter { + + def filterMilestone(definitions: Seq[Field], issueFields: Seq[IssueField]): Seq[IssueField] = + definitions.find(_.name == "Sprint") match { + case Some(sprintDefinition) => issueFields.filterNot(_.id == sprintDefinition.id) + case None => issueFields + } +} diff --git a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/IssueInitializer.scala b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/IssueInitializer.scala index 7cc3b815..6808a24f 100644 --- a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/IssueInitializer.scala +++ b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/IssueInitializer.scala @@ -3,12 +3,16 @@ package com.nulabinc.backlog.j2b.exporter import javax.inject.Inject import com.nulabinc.backlog.j2b.issue.writer.convert._ +import com.nulabinc.backlog.j2b.jira.domain.export.Milestone +import com.nulabinc.backlog.j2b.jira.domain.mapping.MappingCollectDatabase import com.nulabinc.backlog.j2b.jira.service.{IssueService, UserService} +import com.nulabinc.backlog.j2b.jira.utils._ import com.nulabinc.backlog.migration.common.convert.Convert import com.nulabinc.backlog.migration.common.domain._ import com.nulabinc.backlog.migration.common.utils._ -import com.nulabinc.jira.client.domain.Comment +import com.nulabinc.jira.client.domain.{Comment, User} import com.nulabinc.jira.client.domain.changeLog._ +import com.nulabinc.jira.client.domain.field.{DatetimeSchema, Field} import com.nulabinc.jira.client.domain.issue._ class IssueInitializer @Inject()(implicit val issueWrites: IssueWrites, @@ -18,9 +22,15 @@ class IssueInitializer @Inject()(implicit val issueWrites: IssueWrites, implicit val customFieldValueWrites: IssueFieldWrites, userService: UserService, issueService: IssueService) - extends Logging { - - def initialize(issue: Issue, comments: Seq[Comment]): BacklogIssue = { + extends Logging + with SecondToHourFormatter + with DatetimeToDateFormatter { + + def initialize(mappingCollectDatabase: MappingCollectDatabase, + fields: Seq[Field], + milestones: Seq[Milestone], + issue: Issue, + comments: Seq[Comment]): BacklogIssue = { //attachments // val attachmentFilter = new AttachmentFilter(issue.changeLogs) // val filteredAttachments = attachmentFilter.filter(issue.attachments) @@ -31,18 +41,20 @@ class IssueInitializer @Inject()(implicit val issueWrites: IssueWrites, backlogIssue.copy( summary = summary(filteredIssue), -// optParentIssueId = parentIssueId(issue), + optParentIssueId = parentIssueId(issue), description = description(filteredIssue), optDueDate = dueDate(filteredIssue), optEstimatedHours = estimatedHours(filteredIssue), optIssueTypeName = issueTypeName(filteredIssue), categoryNames = categoryNames(filteredIssue), -// milestoneNames = milestoneNames(issue), - versionNames = milestoneNames(filteredIssue), +// milestoneNames = milestoneNames(filteredIssue, milestones), + milestoneNames = milestones.map(_.name), + versionNames = versionNames(filteredIssue), priorityName = priorityName(filteredIssue), - optAssignee = assignee(filteredIssue), -// customFields = issue.issueFields.flatMap(customField), + optAssignee = assignee(mappingCollectDatabase, filteredIssue), + customFields = filteredIssue.issueFields.flatMap(f => customField(fields, f, filteredIssue.changeLogs)), attachments = attachmentNames(filteredIssue), + optActualHours = actualHours(filteredIssue), notifiedUsers = Seq.empty[BacklogUser] ) } @@ -55,6 +67,14 @@ class IssueInitializer @Inject()(implicit val issueWrites: IssueWrites, } } + private def parentIssueId(issue: Issue): Option[Long] = { + val currentValues = issue.parent match { + case Some(parentIssue) => Seq(parentIssue.id.toString) + case _ => Seq.empty[String] + } + ChangeLogsPlayer.reversePlay(ParentChangeLogItemField, currentValues, issue.changeLogs).headOption.map(_.toLong) + } + private def description(issue: Issue): String = { val issueInitialValue = new IssueInitialValue(ChangeLogItem.FieldType.JIRA, DescriptionFieldId) issueInitialValue.findChangeLogItem(issue.changeLogs) match { @@ -66,33 +86,32 @@ class IssueInitializer @Inject()(implicit val issueWrites: IssueWrites, private def dueDate(issue: Issue): Option[String] = { val issueInitialValue = new IssueInitialValue(ChangeLogItem.FieldType.JIRA, DueDateFieldId) issueInitialValue.findChangeLogItem(issue.changeLogs) match { - case Some(detail) => detail.fromDisplayString + case Some(detail) => detail.from case None => issue.dueDate.map(DateUtil.dateFormat) } } private def estimatedHours(issue: Issue): Option[Float] = { - val issueInitialValue = new IssueInitialValue(ChangeLogItem.FieldType.JIRA, TimeEstimateFieldId) - issueInitialValue.findChangeLogItem(issue.changeLogs) match { - case Some(detail) => detail.fromDisplayString.filter(_.nonEmpty).map(_.toFloat / 3600) - case None => issue.timeTrack.flatMap(_.originalEstimateSeconds.map(_.toFloat / 3600)) + val initialValues = issue.timeTrack.flatMap(t => t.originalEstimateSeconds) match { + case Some(second) => Seq(second.toString) + case _ => Seq.empty[String] } + val initializedEstimatedSeconds = ChangeLogsPlayer.reversePlay(TimeOriginalEstimateChangeLogItemField, initialValues, issue.changeLogs).headOption + initializedEstimatedSeconds.map(sec => secondsToHours(sec.toInt)) } - private def issueTypeName(issue: Issue): Option[String] = { - val issueInitialValue = new IssueInitialValue(ChangeLogItem.FieldType.JIRA, IssueTypeFieldId) - issueInitialValue.findChangeLogItem(issue.changeLogs) match { - case Some(detail) => detail.fromDisplayString - case None => Option(issue.issueType.name) - } - } + private def issueTypeName(issue: Issue): Option[String] = + ChangeLogsPlayer.reversePlay(IssueTypeChangeLogItemField, Seq(issue.issueType.name), issue.changeLogs).headOption private def categoryNames(issue: Issue): Seq[String] = ChangeLogsPlayer.reversePlay(ComponentChangeLogItemField, issue.components.map(_.name), issue.changeLogs) - private def milestoneNames(issue: Issue): Seq[String] = + private def versionNames(issue: Issue): Seq[String] = ChangeLogsPlayer.reversePlay(FixVersion, issue.fixVersions.map(_.name), issue.changeLogs) + private def milestoneNames(issue: Issue, milestones: Seq[Milestone]): Seq[String] = + ChangeLogsPlayer.reversePlay(SprintChangeLogItemField, milestones.map(_.name), issue.changeLogs) + private def attachmentNames(issue: Issue): Seq[BacklogAttachment] = { val histories = ChangeLogsPlayer.reversePlay(AttachmentChangeLogItemField, issue.attachments.map(_.fileName), issue.changeLogs) histories.map { h => @@ -108,49 +127,52 @@ class IssueInitializer @Inject()(implicit val issueWrites: IssueWrites, } } - private def assignee(issue: Issue): Option[BacklogUser] = { + private def assignee(mappingCollectDatabase: MappingCollectDatabase, issue: Issue): Option[BacklogUser] = { val issueInitialValue = new IssueInitialValue(ChangeLogItem.FieldType.JIRA, AssigneeFieldId) issueInitialValue.findChangeLogItem(issue.changeLogs) match { - case Some(detail) => userService.optUserOfKey(detail.from).map(Convert.toBacklog(_)) - case None => issue.assignee.map(Convert.toBacklog(_)) + case Some(detail) => + if (mappingCollectDatabase.userExistsFromAllUsers(detail.from)) { + mappingCollectDatabase.findByName(detail.from).map(Convert.toBacklog(_)) + } else { + val optUser = userService.optUserOfKey(detail.from) match { + case Some(u) => Some(mappingCollectDatabase.add(u)) + case None => mappingCollectDatabase.add(detail.from); None + } + optUser.map(Convert.toBacklog(_)) + } + case None => issue.assignee.map(Convert.toBacklog(_)) } } -// private def customField(customField: IssueField): Option[BacklogCustomField] = { -// val optCustomFieldDefinition = exportContext.propertyValue.customFieldDefinitionOfName(customField.getName) -// optCustomFieldDefinition match { -// case Some(customFieldDefinition) => -// if (customFieldDefinition.isMultiple) multipleCustomField(customField, customFieldDefinition) -// else singleCustomField(customField, customFieldDefinition) -// case _ => None -// } -// } -// -// private def multipleCustomField(issueField: IssueField, field: Field, changeLogs: Seq[ChangeLog]): Option[BacklogCustomField] = { -// val issueInitialValue = new IssueInitialValue(ChangeLogItem.FieldType.CUSTOM, field.id) -// val optDetails = issueInitialValue.findJournalDetails(changeLogs) -// val initialValues = optDetails match { -// case Some(details) => details.flatMap(detail => Convert.toBacklog((issueField.id, detail.from))) -// case _ => issueField.value.asInstanceOf[ArrayFieldValue].values -// } -// Convert.toBacklog(issueField) match { -// case Some(backlogCustomField) => Some(backlogCustomField.copy(values = initialValues)) -// case _ => None -// } -// } -// -// private def singleCustomField(issueField: IssueField, field: Field, changeLogs: Seq[ChangeLog]): Option[BacklogCustomField] = { -// val issueInitialValue = new IssueInitialValue(ChangeLogItem.FieldType.CUSTOM, field.id) -// val initialValue: Option[String] = -// issueInitialValue.findJournalDetail(changeLogs) match { -// case Some(detail) => Convert.toBacklog((issueField.id, detail.from)) -// case _ => Convert.toBacklog((issueField.id, Option(issueField.value.value))) -// } -// Convert.toBacklog(issueField) match { -// case Some(backlogCustomField) => Some(backlogCustomField.copy(optValue = initialValue)) -// case _ => None -// } -// } + private def actualHours(issue: Issue): Option[Float] = { + val initialValues = issue.timeTrack.flatMap(t => t.timeSpentSeconds) match { + case Some(second) => Seq(second.toString) + case _ => Seq.empty[String] + } + val initializedTimeSpentSeconds = ChangeLogsPlayer.reversePlay(TimeSpentChangeLogItemField, initialValues, issue.changeLogs).headOption + initializedTimeSpentSeconds.map(sec => secondsToHours(sec.toInt)) + } + private def customField(fields: Seq[Field], issueField: IssueField, changeLogs: Seq[ChangeLog]): Option[BacklogCustomField] = { + val fieldDefinition = fields.find(_.id == issueField.id).get // TODO Fix get + val currentValues = issueField.value match { + case ArrayFieldValue(values) => values.map(_.value) + case value => fieldDefinition.schema match { + case Some(v) => v.schemaType match { + case DatetimeSchema => Seq(dateTimeStringToDateString(value.value)) + case _ => Seq(value.value) + } + case None => Seq(value.value) + } + } + + val initialValues = ChangeLogsPlayer.reversePlay(DefaultField(fieldDefinition.name), currentValues, changeLogs) + Convert.toBacklog(issueField).map { converted => + issueField.value match { + case ArrayFieldValue(_) => converted.copy(values = initialValues) + case _ => converted.copy(optValue = initialValues.headOption) + } + } + } } diff --git a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/MilestoneExtractor.scala b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/MilestoneExtractor.scala new file mode 100644 index 00000000..2bac1968 --- /dev/null +++ b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/MilestoneExtractor.scala @@ -0,0 +1,20 @@ +package com.nulabinc.backlog.j2b.exporter + +import com.nulabinc.backlog.j2b.jira.domain.export.Milestone +import com.nulabinc.jira.client.domain.field.Field +import com.nulabinc.jira.client.domain.issue.{ArrayFieldValue, IssueField} + +object MilestoneExtractor { + + def extract(fieldDefinitions: Seq[Field], issueFields: Seq[IssueField]): Seq[Milestone] = { + fieldDefinitions.find(_.name == "Sprint") match { + case Some(sprintDefinition) => issueFields.find(_.id == sprintDefinition.id) match { + case Some(IssueField(_, ArrayFieldValue(values))) => values.map(v => Milestone(v.value)) + case _ => Seq.empty[Milestone] + } + case None => Seq.empty[Milestone] + } + + + } +} diff --git a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/service/JiraClientCommentService.scala b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/service/JiraClientCommentService.scala index 1f2bc471..22307214 100644 --- a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/service/JiraClientCommentService.scala +++ b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/service/JiraClientCommentService.scala @@ -4,11 +4,21 @@ import javax.inject.Inject import com.nulabinc.backlog.j2b.jira.service.CommentService import com.nulabinc.jira.client.JiraRestClient +import com.nulabinc.jira.client.domain.Comment import com.nulabinc.jira.client.domain.issue.Issue class JiraClientCommentService @Inject()(jira: JiraRestClient) extends CommentService { - override def issueComments(issue: Issue) = - jira.commentAPI.issueComments(issue.id).right.get + override def issueComments(issue: Issue): Seq[Comment] = fetch(issue, 0, 100, Seq.empty[Comment]) + + private def fetch(issue: Issue, startAt: Long, maxResults: Long, comments: Seq[Comment]): Seq[Comment] = + jira.commentAPI.issueComments(issue.id, startAt, maxResults) match { + case Right(result) => + val appendedComments = comments ++ result.comments + if (result.hasPage) fetch(issue, startAt + maxResults, maxResults, appendedComments) + else appendedComments + case Left(error) => + throw new RuntimeException(s"Cannot get issue comments: ${error.message}") + } } diff --git a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/service/JiraClientIssueService.scala b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/service/JiraClientIssueService.scala index 29112f4b..98135719 100644 --- a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/service/JiraClientIssueService.scala +++ b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/service/JiraClientIssueService.scala @@ -1,6 +1,5 @@ package com.nulabinc.backlog.j2b.exporter.service -import java.util.Date import javax.inject.Inject import com.nulabinc.backlog.j2b.jira.conf.JiraApiConfiguration @@ -8,10 +7,10 @@ import com.nulabinc.backlog.j2b.jira.domain.JiraProjectKey import com.nulabinc.backlog.j2b.jira.service.IssueService import com.nulabinc.backlog.migration.common.conf.BacklogPaths import com.nulabinc.backlog.migration.common.utils.Logging +import com.nulabinc.jira.client.domain.changeLog.ChangeLog import com.nulabinc.jira.client.{DownloadResult, JiraRestClient} import com.nulabinc.jira.client.domain.issue.Issue -import scala.util.{Failure, Success, Try} import scalax.file.Path class JiraClientIssueService @Inject()(apiConfig: JiraApiConfiguration, @@ -20,7 +19,7 @@ class JiraClientIssueService @Inject()(apiConfig: JiraApiConfiguration, backlogPaths: BacklogPaths) extends IssueService with Logging { - override def count() = { + override def count(): Long = { jira.searchAPI.searchJql(s"project=${projectKey.value}", 0, 0) match { case Right(result) => result.total case Left(error) => { @@ -30,7 +29,7 @@ class JiraClientIssueService @Inject()(apiConfig: JiraApiConfiguration, } } - override def issues(startAt: Long, maxResults: Long) = + override def issues(startAt: Long, maxResults: Long): Seq[Issue] = jira.issueAPI.projectIssues(projectKey.value, startAt, maxResults) match { case Right(result) => result case Left(error) => { @@ -39,10 +38,19 @@ class JiraClientIssueService @Inject()(apiConfig: JiraApiConfiguration, } } - override def injectChangeLogsToIssue(issue: Issue) = { - val changeLogs = jira.issueAPI.changeLogs(issue.id.toString, 0, 100) + override def changeLogs(issue: Issue): Seq[ChangeLog] = { - issue.copy(changeLogs = changeLogs.right.get.values) + def fetch(issue: Issue, startAt: Long, maxResults: Long, changeLogs: Seq[ChangeLog]): Seq[ChangeLog] = + jira.issueAPI.changeLogs(issue.id.toString, startAt, maxResults) match { + case Right(result) => + val appendedChangeLogs = changeLogs ++ result.values + if (result.hasPage) fetch(issue, startAt + maxResults, maxResults, appendedChangeLogs) + else appendedChangeLogs + case Left(error) => + throw new RuntimeException(s"Cannot get issue change logs: ${error.message}") + } + + fetch(issue, 0, 100, Seq.empty[ChangeLog]) } override def downloadAttachments(attachmentId: Long, saveDirectory: Path, fileName: String): DownloadResult = { @@ -50,6 +58,4 @@ class JiraClientIssueService @Inject()(apiConfig: JiraApiConfiguration, jira.httpClient.download(jira.url + s"/secure/attachment/$attachmentId/$fileName", saveDirectory.path) } - override def injectAttachmentsToIssue(issue: Issue) = ??? - } diff --git a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/service/JiraClientUserService.scala b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/service/JiraClientUserService.scala index 759600b9..a2a7195e 100644 --- a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/service/JiraClientUserService.scala +++ b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/service/JiraClientUserService.scala @@ -9,6 +9,7 @@ import com.nulabinc.jira.client.domain.User class JiraClientUserService @Inject()(jira: JiraRestClient) extends UserService with Logging { + override def allUsers() = jira.userAPI.users match { case Right(users) => users @@ -20,7 +21,7 @@ class JiraClientUserService @Inject()(jira: JiraRestClient) extends UserService override def optUserOfKey(key: Option[String]) = key.flatMap { k => - jira.userAPI.user(k) match { + jira.userAPI.findByKey(k) match { case Right(user) => Some(user) case Left(error) => { logger.error(error.message) @@ -29,4 +30,14 @@ class JiraClientUserService @Inject()(jira: JiraRestClient) extends UserService } } + override def optUserOfName(name: Option[String]) = + name.flatMap { k => + jira.userAPI.findByUsername(k) match { + case Right(user) => Some(user) + case Left(error) => { + logger.error(error.message) + None + } + } + } } diff --git a/exporter/src/test/scala/com/nulabinc/backlog/j2b/exporter/AttachmentFilterSpec.scala b/exporter/src/test/scala/com/nulabinc/backlog/j2b/exporter/AttachmentFilterSpec.scala index 70f884fe..fa5f89c6 100644 --- a/exporter/src/test/scala/com/nulabinc/backlog/j2b/exporter/AttachmentFilterSpec.scala +++ b/exporter/src/test/scala/com/nulabinc/backlog/j2b/exporter/AttachmentFilterSpec.scala @@ -30,7 +30,7 @@ class AttachmentFilterSpec extends Specification { ), status = Status("1", "status"), priority = Priority("priority"), - creator = User("name", "display"), + creator = User("key", "name", "display", "mail"), createdAt = DateTime.now, updatedAt = DateTime.now, changeLogs = Seq.empty[ChangeLog], @@ -38,7 +38,7 @@ class AttachmentFilterSpec extends Specification { Attachment( id = 1, fileName = "file1.txt", - author = User("user1", "user1"), + author = User("key1", "user1", "user1", "mail1"), createdAt = DateTime.now, size = 100, mimeType = "mine", @@ -47,7 +47,7 @@ class AttachmentFilterSpec extends Specification { Attachment( id = 2, fileName = "file2.txt", - author = User("user2", "user2"), + author = User("key2", "user2", "user2", "mail2"), createdAt = DateTime.now, size = 200, mimeType = "mine", @@ -56,7 +56,7 @@ class AttachmentFilterSpec extends Specification { Attachment( id = 3, fileName = "file3.txt", - author = User("user3", "user3"), + author = User("key3" ,"user3", "user3", "mail3"), createdAt = DateTime.now, size = 300, mimeType = "mine", @@ -69,7 +69,7 @@ class AttachmentFilterSpec extends Specification { Comment( id = 1, body = "test1 body [^file2.txt] ", - author = User("aaa", "aaa"), + author = User("aaa", "aaa", "aaa", "mmm"), createdAt = DateTime.now ) ) diff --git a/exporter/src/test/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogsPlayerSpec.scala b/exporter/src/test/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogsPlayerSpec.scala index 0f6c95ad..5bac3c7d 100644 --- a/exporter/src/test/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogsPlayerSpec.scala +++ b/exporter/src/test/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogsPlayerSpec.scala @@ -68,4 +68,22 @@ class ChangeLogsPlayerSpec extends Specification { replay(0).to must equalTo(Seq("テスト", "確認")) } + + "Calc.run4" >> { + val init = Seq("AAA") + val histories = Seq( + History( + id = 1, + from = None, + to = Some("CCC") + ), + History( + id = 2, + from = Some("BBB, CCC"), + to = None + ) + ) + val actual = Calc.run(init, histories) + actual.last must equalTo(Result(2, Seq("AAA", "CCC"), Seq("AAA"))) + } } diff --git a/icon.png b/icon.png new file mode 100644 index 00000000..c2d76ebc Binary files /dev/null and b/icon.png differ diff --git a/jira-client/src/main/scala/com/nulabinc/jira/client/HttpClient.scala b/jira-client/src/main/scala/com/nulabinc/jira/client/HttpClient.scala index f2025754..60b2c6cf 100644 --- a/jira-client/src/main/scala/com/nulabinc/jira/client/HttpClient.scala +++ b/jira-client/src/main/scala/com/nulabinc/jira/client/HttpClient.scala @@ -1,15 +1,16 @@ package com.nulabinc.jira.client import java.io.{File, FileOutputStream} -import java.nio.channels.Channels +import java.nio.channels.{Channels, ReadableByteChannel} import java.nio.charset.Charset import org.apache.commons.codec.binary.Base64 -import org.apache.http.{HttpHeaders, HttpStatus} -import org.apache.http.client.methods.HttpGet -import org.apache.http.impl.client.HttpClientBuilder +import org.apache.http._ +import org.apache.http.client.methods.{CloseableHttpResponse, HttpGet} +import org.apache.http.impl.client.{CloseableHttpClient, HttpClientBuilder} import spray.json.{JsArray, JsonParser} +import scala.io.Source sealed abstract class HttpClientError(val message: String) { override def toString: String = message @@ -17,71 +18,114 @@ sealed abstract class HttpClientError(val message: String) { case object AuthenticateFailedError extends HttpClientError("Bad credential") case class ApiNotFoundError(url: String) extends HttpClientError(url) case class BadRequestError(error: String) extends HttpClientError(error) +case class GetContentError(throwable: Throwable) extends HttpClientError(throwable.getMessage) +case class ThrowableError(throwable: Throwable) extends HttpClientError(throwable.getMessage) case class UndefinedError(statusCode: Int) extends HttpClientError(s"Unknown status code: $statusCode") sealed trait DownloadResult -case object Success extends DownloadResult -case object Failure extends DownloadResult +case object DownloadSuccess extends DownloadResult +case object DownloadFailure extends DownloadResult class HttpClient(url: String, username: String, password: String) { - val auth = username + ":" + password - val encodedAuth = Base64.encodeBase64(auth.getBytes(Charset.forName("ISO-8859-1"))) - val authHeader = "Basic " + new String(encodedAuth) + val auth: String = username + ":" + password + val encodedAuth: Array[Byte] = Base64.encodeBase64(auth.getBytes(Charset.forName("ISO-8859-1"))) + val authHeader: String = "Basic " + new String(encodedAuth) def get(path: String): Either[HttpClientError, String] = { - using(HttpClientBuilder.create().build()) { http => - val request = new HttpGet(url + "/rest/api/2" + path) - request.setHeader(HttpHeaders.AUTHORIZATION, authHeader) - val httpResponse = http.execute(request) - httpResponse.getStatusLine.getStatusCode match { + + val closableHttpClient = createHttpClient() + val httpRequest = createHttpGetRequest(url + "/rest/api/2" + path) + + try { + val closableHttpResponse = httpExecute(closableHttpClient, httpRequest) + + closableHttpResponse.getStatusLine.getStatusCode match { case HttpStatus.SC_OK => - using(httpResponse.getEntity.getContent) { inputStream => - val body = io.Source.fromInputStream(inputStream)("UTF-8").getLines.mkString - Right(body) + try { + val content = getContent(closableHttpResponse.getEntity) + Right(content) + } catch { + case e: Throwable => Left(GetContentError(e)) + } finally { + closableHttpResponse.close() } - case HttpStatus.SC_BAD_REQUEST => { - using(httpResponse.getEntity.getContent) { inputStream => - val body = io.Source.fromInputStream(inputStream).getLines.mkString - val errors = JsonParser(body).asJsObject.getFields("errorMessages") match { - case Seq(JsArray(e)) => e.mkString(" ") - case _ => "Bad Request" + case HttpStatus.SC_BAD_REQUEST => + try { + val content = getContent(closableHttpResponse.getEntity) + JsonParser(content).asJsObject.getFields("errorMessages") match { + case Seq(JsArray(e)) => Left(BadRequestError(e.mkString(" "))) + case _ => Left(BadRequestError("Bad Request")) } - Left(BadRequestError(errors)) + } catch { + case e: Throwable => Left(GetContentError(e)) + } finally { + closableHttpResponse.close() } - } - case HttpStatus.SC_NOT_FOUND => Left(ApiNotFoundError(request.getURI.toString)) + case HttpStatus.SC_NOT_FOUND => Left(ApiNotFoundError(httpRequest.getURI.toString)) case HttpStatus.SC_UNAUTHORIZED => Left(AuthenticateFailedError) - case _ => Left(UndefinedError(httpResponse.getStatusLine.getStatusCode)) + case HttpStatus.SC_FORBIDDEN => Left(AuthenticateFailedError) + case statusCode => Left(UndefinedError(statusCode)) } + } catch { + case e: Throwable => Left(ThrowableError(e)) + } finally { + closableHttpClient.close() } } def download(url: String, destinationFilePath: String): DownloadResult = { - using(HttpClientBuilder.create().build()) { http => - val request = new HttpGet(url) - request.setHeader(HttpHeaders.AUTHORIZATION, authHeader) - val httpResponse = http.execute(request) - httpResponse.getStatusLine.getStatusCode match { + + def getChannel(entity: HttpEntity): ReadableByteChannel = + Channels.newChannel(entity.getContent) + + def createOutputStream(channel: ReadableByteChannel): Long = { + val outputStream = new FileOutputStream(new File(destinationFilePath)) + outputStream.getChannel.transferFrom(channel, 0, java.lang.Long.MAX_VALUE) + } + + val closableHttpClient = createHttpClient() + val httpRequest = createHttpGetRequest(url) + + try { + val closableHttpResponse = httpExecute(closableHttpClient, httpRequest) + + closableHttpResponse.getStatusLine.getStatusCode match { case HttpStatus.SC_OK => - using(httpResponse.getEntity.getContent) { inputStream => - using(Channels.newChannel(inputStream)) { rbc => - using(new FileOutputStream(new File(destinationFilePath))) { fos => - fos.getChannel.transferFrom(rbc, 0, java.lang.Long.MAX_VALUE) - Success - } - } + try { + val channel = getChannel(closableHttpResponse.getEntity) + createOutputStream(channel) + DownloadSuccess + } catch { + case _: Throwable => DownloadFailure + } finally { + closableHttpResponse.close() } - case _ => Failure + case _ => + closableHttpResponse.close() + DownloadFailure } + } catch { + case _: Throwable => DownloadFailure + } finally { + closableHttpClient.close() } } - private [this] def using[A <: {def close()}, B](resource: A)(func: A => B): B = { - try { - func(resource) - } finally { - if(resource != null) resource.close() - } + private def createHttpClient(): CloseableHttpClient = + HttpClientBuilder.create().build() + + private def createHttpGetRequest(path: String): HttpGet = + new HttpGet(path) + + private def httpExecute(client: CloseableHttpClient, request: HttpGet): CloseableHttpResponse = { + request.setHeader(HttpHeaders.AUTHORIZATION, authHeader) + client.execute(request) } + + private def getContent(entity: HttpEntity): String = { + val content = entity.getContent + Source.fromInputStream(content)("UTF-8").getLines.mkString + } + } diff --git a/jira-client/src/main/scala/com/nulabinc/jira/client/apis/CommentAPI.scala b/jira-client/src/main/scala/com/nulabinc/jira/client/apis/CommentAPI.scala index 22bde5e4..fe80dcfe 100644 --- a/jira-client/src/main/scala/com/nulabinc/jira/client/apis/CommentAPI.scala +++ b/jira-client/src/main/scala/com/nulabinc/jira/client/apis/CommentAPI.scala @@ -1,21 +1,26 @@ package com.nulabinc.jira.client.apis +import com.netaporter.uri.dsl._ import com.nulabinc.jira.client._ import com.nulabinc.jira.client.domain.CommentResult import spray.json._ -class CommentAPI(httpClient: HttpClient) extends Pagenable { +class CommentAPI(httpClient: HttpClient) extends Pageable { import com.nulabinc.jira.client.json.CommentMappingJsonProtocol._ - def issueComments(id: Long) = fetch(id.toString) + def issueComments(id: Long, startAt: Long, maxResults: Long): Either[JiraRestClientError, CommentResult] = + fetch(id.toString, startAt, maxResults) - def issueComments(projectKey: String) = fetch(projectKey) + def issueComments(projectKey: String, startAt: Long, maxResults: Long): Either[JiraRestClientError, CommentResult] = + fetch(projectKey, startAt, maxResults) - private def fetch(projectIdOrKey: String) = - httpClient.get(s"/issue/$projectIdOrKey/comment") match { - case Right(json) => Right(JsonParser(json).convertTo[CommentResult].comments) - case Left(_: ApiNotFoundError) => Left(ResourceNotFoundError("Component", projectIdOrKey)) - case Left(error) => Left(HttpError(error)) + private def fetch(projectIdOrKey: String, startAt: Long, maxResults: Long): Either[JiraRestClientError, CommentResult] = { + val uri = s"/issue/$projectIdOrKey/comment" ? paginateUri(startAt, maxResults) + httpClient.get(uri.toString) match { + case Right(json) => Right(JsonParser(json).convertTo[CommentResult]) + case Left(_: ApiNotFoundError) => Left(ResourceNotFoundError("Component", projectIdOrKey)) + case Left(error) => Left(HttpError(error)) } + } } diff --git a/jira-client/src/main/scala/com/nulabinc/jira/client/apis/IssueAPI.scala b/jira-client/src/main/scala/com/nulabinc/jira/client/apis/IssueAPI.scala index 9d74329a..df801fa5 100644 --- a/jira-client/src/main/scala/com/nulabinc/jira/client/apis/IssueAPI.scala +++ b/jira-client/src/main/scala/com/nulabinc/jira/client/apis/IssueAPI.scala @@ -8,7 +8,7 @@ import spray.json._ case class IssueResult(total: Int, issues: Seq[Issue]) -class IssueRestClientImpl(httpClient: HttpClient) { +class IssueRestClientImpl(httpClient: HttpClient) extends Pageable { import com.nulabinc.jira.client.json.IssueMappingJsonProtocol._ import com.nulabinc.jira.client.json.IssueResultMappingJsonProtocol._ @@ -18,10 +18,8 @@ class IssueRestClientImpl(httpClient: HttpClient) { def issue(key: String) = fetchIssue(key) - def projectIssues(key: String, startAt: Long = 0, maxResults: Long = 100) = { - val uri = "/search" ? - ("startAt" -> startAt) & - ("maxResults" -> maxResults) & + def projectIssues(key: String, startAt: Long = 0, maxResults: Long = 100): Either[HttpError, Seq[Issue]] = { + val uri = "/search" ? paginateUri(startAt, maxResults) & ("jql" -> s"project=$key") & ("fields" -> "*all") diff --git a/jira-client/src/main/scala/com/nulabinc/jira/client/apis/Pageable.scala b/jira-client/src/main/scala/com/nulabinc/jira/client/apis/Pageable.scala new file mode 100644 index 00000000..eef9a3cf --- /dev/null +++ b/jira-client/src/main/scala/com/nulabinc/jira/client/apis/Pageable.scala @@ -0,0 +1,12 @@ +package com.nulabinc.jira.client.apis + +import com.netaporter.uri.Uri +import com.netaporter.uri.dsl._ + +trait Pageable { + + def paginateUri(startAt: Long, maxResults: Long): Uri = + ("startAt" -> startAt) & + ("maxResults" -> maxResults) + +} diff --git a/jira-client/src/main/scala/com/nulabinc/jira/client/apis/Pagenable.scala b/jira-client/src/main/scala/com/nulabinc/jira/client/apis/Pagenable.scala deleted file mode 100644 index 0121ef27..00000000 --- a/jira-client/src/main/scala/com/nulabinc/jira/client/apis/Pagenable.scala +++ /dev/null @@ -1,6 +0,0 @@ -package com.nulabinc.jira.client.apis - -// TODO: Define pagination -trait Pagenable { - -} diff --git a/jira-client/src/main/scala/com/nulabinc/jira/client/apis/SearchAPI.scala b/jira-client/src/main/scala/com/nulabinc/jira/client/apis/SearchAPI.scala index 78c10400..eac564b0 100644 --- a/jira-client/src/main/scala/com/nulabinc/jira/client/apis/SearchAPI.scala +++ b/jira-client/src/main/scala/com/nulabinc/jira/client/apis/SearchAPI.scala @@ -5,17 +5,14 @@ import com.nulabinc.jira.client._ import com.nulabinc.jira.client.domain.SearchResult import spray.json.JsonParser -class SearchAPI(httpClient: HttpClient) { +class SearchAPI(httpClient: HttpClient) extends Pageable { import com.nulabinc.jira.client.json.SearchResultMappingJsonProtocol._ def searchJql(jql: String): Either[JiraRestClientError, SearchResult] = searchJql(jql, 50, 0) def searchJql(jql: String, maxResults: Int, startAt: Int): Either[JiraRestClientError, SearchResult] = { - val uri = "/search" ? - ("startAt" -> startAt) & - ("maxResults" -> maxResults) - + val uri = "/search" ? paginateUri(startAt, maxResults) httpClient.get(uri.toString + "&jql=" + jql) match { case Right(json) => Right(JsonParser(json).convertTo[SearchResult]) case Left(_: ApiNotFoundError) => Left(ResourceNotFoundError("search", jql)) diff --git a/jira-client/src/main/scala/com/nulabinc/jira/client/apis/UserAPI.scala b/jira-client/src/main/scala/com/nulabinc/jira/client/apis/UserAPI.scala index 2c476d6c..e1c1a218 100644 --- a/jira-client/src/main/scala/com/nulabinc/jira/client/apis/UserAPI.scala +++ b/jira-client/src/main/scala/com/nulabinc/jira/client/apis/UserAPI.scala @@ -11,14 +11,22 @@ class UserAPI(httpClient: HttpClient) { def users: Either[JiraRestClientError, Seq[User]] = chunkUsers(Seq.empty[User]) - def user(name: String): Either[JiraRestClientError, User] = { - httpClient.get(s"/user?key=$name") match { + def findByUsername(name: String): Either[JiraRestClientError, User] = { + httpClient.get(s"/user?username=$name") match { case Right(json) => Right(JsonParser(json).convertTo[User]) case Left(_: ApiNotFoundError) => Left(ResourceNotFoundError("user", name)) case Left(error) => Left(HttpError(error)) } } + def findByKey(key: String): Either[JiraRestClientError, User] = { + httpClient.get(s"/user?key=$key") match { + case Right(json) => Right(JsonParser(json).convertTo[User]) + case Left(_: ApiNotFoundError) => Left(ResourceNotFoundError("user", key)) + case Left(error) => Left(HttpError(error)) + } + } + private [this] def chunkUsers(beforeUsers: Seq[User], startAt: Int = 0): Either[JiraRestClientError, Seq[User]] = { val maxResults = 100 val uri = "/user/search" ? diff --git a/jira-client/src/main/scala/com/nulabinc/jira/client/domain/Comment.scala b/jira-client/src/main/scala/com/nulabinc/jira/client/domain/Comment.scala index 031b31d7..55862701 100644 --- a/jira-client/src/main/scala/com/nulabinc/jira/client/domain/Comment.scala +++ b/jira-client/src/main/scala/com/nulabinc/jira/client/domain/Comment.scala @@ -9,7 +9,7 @@ case class Comment( createdAt: DateTime ) -case class CommentResult( - total: Long, - comments: Seq[Comment] -) \ No newline at end of file +case class CommentResult(startAt: Long, total: Long, comments: Seq[Comment]) { + + def hasPage: Boolean = startAt < total +} \ No newline at end of file diff --git a/jira-client/src/main/scala/com/nulabinc/jira/client/domain/User.scala b/jira-client/src/main/scala/com/nulabinc/jira/client/domain/User.scala index 976bc111..4c3e63ab 100644 --- a/jira-client/src/main/scala/com/nulabinc/jira/client/domain/User.scala +++ b/jira-client/src/main/scala/com/nulabinc/jira/client/domain/User.scala @@ -1,3 +1,3 @@ package com.nulabinc.jira.client.domain -case class User(name: String, displayName: String) +case class User(key: String, name: String, displayName: String, emailAddress: String) diff --git a/jira-client/src/main/scala/com/nulabinc/jira/client/domain/changeLog/ChangeLog.scala b/jira-client/src/main/scala/com/nulabinc/jira/client/domain/changeLog/ChangeLog.scala index 74e1179b..92877737 100644 --- a/jira-client/src/main/scala/com/nulabinc/jira/client/domain/changeLog/ChangeLog.scala +++ b/jira-client/src/main/scala/com/nulabinc/jira/client/domain/changeLog/ChangeLog.scala @@ -23,9 +23,18 @@ case class ChangeLogItem( sealed abstract class ChangeLogItemField(val value: String) case object ComponentChangeLogItemField extends ChangeLogItemField("Component") case object FixVersion extends ChangeLogItemField("Fix Version") -case object Parent extends ChangeLogItemField("Parent") +case object ParentChangeLogItemField extends ChangeLogItemField("Parent") case object AttachmentChangeLogItemField extends ChangeLogItemField("Attachment") case object StatusChangeLogItemField extends ChangeLogItemField("status") +case object DueDateChangeLogItemField extends ChangeLogItemField("duedate") +case object LinkChangeLogItemField extends ChangeLogItemField("Link") +case object LabelsChangeLogItemField extends ChangeLogItemField("labels") +case object TimeOriginalEstimateChangeLogItemField extends ChangeLogItemField("timeoriginalestimate") +case object IssueTypeChangeLogItemField extends ChangeLogItemField("issuetype") +case object TimeSpentChangeLogItemField extends ChangeLogItemField("timespent") +case object WorkIdChangeLogItemField extends ChangeLogItemField("WorklogId") +case object TimeEstimateChangeLogItemField extends ChangeLogItemField("timeestimate") +case object SprintChangeLogItemField extends ChangeLogItemField("Sprint") case class DefaultField(name: String) extends ChangeLogItemField(name) object ChangeLogItemField { @@ -34,7 +43,17 @@ object ChangeLogItemField { case FixVersion.value => FixVersion case AttachmentChangeLogItemField.value => AttachmentChangeLogItemField case StatusChangeLogItemField.value => StatusChangeLogItemField - case v => DefaultField(v) + case DueDateChangeLogItemField.value => DueDateChangeLogItemField + case LinkChangeLogItemField.value => LinkChangeLogItemField + case LabelsChangeLogItemField.value => LabelsChangeLogItemField + case TimeOriginalEstimateChangeLogItemField.value => TimeOriginalEstimateChangeLogItemField + case IssueTypeChangeLogItemField.value => IssueTypeChangeLogItemField + case ParentChangeLogItemField.value => ParentChangeLogItemField + case TimeSpentChangeLogItemField.value => TimeSpentChangeLogItemField + case WorkIdChangeLogItemField.value => WorkIdChangeLogItemField + case TimeEstimateChangeLogItemField.value => TimeEstimateChangeLogItemField + case SprintChangeLogItemField.value => SprintChangeLogItemField + case v => DefaultField(v) } } @@ -46,11 +65,11 @@ object ChangeLogItem { } } -case class ChangeLogResult( - total: Long, - isLast: Boolean, - values: Seq[ChangeLog] -) +case class ChangeLogResult(total: Long, isLast: Boolean, values: Seq[ChangeLog]) { + + def hasPage: Boolean = !isLast + +} sealed abstract class FieldId(val value: String) @@ -66,6 +85,7 @@ case object StatusFieldId extends FieldId("status") case object DueDateFieldId extends FieldId("duedate") case object TimeOriginalEstimateFieldId extends FieldId("timeoriginalestimate") case object TimeEstimateFieldId extends FieldId("timeestimate") +case object TimeSpentFieldId extends FieldId("timespent") case object ResolutionFieldId extends FieldId("resolution") case class CustomFieldFieldId(id: String) extends FieldId(id) case class GeneralFieldId(id: String) extends FieldId(id) @@ -85,6 +105,7 @@ object FieldId { case DueDateFieldId.value => DueDateFieldId case TimeOriginalEstimateFieldId.value => TimeOriginalEstimateFieldId case TimeEstimateFieldId.value => TimeEstimateFieldId + case TimeSpentFieldId.value => TimeSpentFieldId case ResolutionFieldId.value => ResolutionFieldId case v if v.startsWith("customfield_") => CustomFieldFieldId(v) case v => GeneralFieldId(v) diff --git a/jira-client/src/main/scala/com/nulabinc/jira/client/domain/field/FieldCustomType.scala b/jira-client/src/main/scala/com/nulabinc/jira/client/domain/field/FieldCustomType.scala index deea8080..43a9924b 100644 --- a/jira-client/src/main/scala/com/nulabinc/jira/client/domain/field/FieldCustomType.scala +++ b/jira-client/src/main/scala/com/nulabinc/jira/client/domain/field/FieldCustomType.scala @@ -9,6 +9,7 @@ case object Textarea extends FieldCustomType case object Textfield extends FieldCustomType case object RadioButtons extends FieldCustomType case object MultiCheckBoxes extends FieldCustomType +case object CustomLabel extends FieldCustomType object FieldCustomType { @@ -20,6 +21,7 @@ object FieldCustomType { case "com.atlassian.jira.plugin.system.customfieldtypes:textfield" => Some(Textfield) // single line case "com.atlassian.jira.plugin.system.customfieldtypes:radiobuttons" => Some(RadioButtons) case "com.atlassian.jira.plugin.system.customfieldtypes:multicheckboxes" => Some(MultiCheckBoxes) + case "com.atlassian.jira.plugin.system.customfieldtypes:labels" => Some(CustomLabel) // multi select case _ => None } } diff --git a/jira-client/src/main/scala/com/nulabinc/jira/client/json/CommentMappingJsonProtocol.scala b/jira-client/src/main/scala/com/nulabinc/jira/client/json/CommentMappingJsonProtocol.scala index c6f85714..c572a9f0 100644 --- a/jira-client/src/main/scala/com/nulabinc/jira/client/json/CommentMappingJsonProtocol.scala +++ b/jira-client/src/main/scala/com/nulabinc/jira/client/json/CommentMappingJsonProtocol.scala @@ -26,6 +26,6 @@ object CommentMappingJsonProtocol extends DefaultJsonProtocol { } } - implicit val commentResultJsonFormat = jsonFormat2(CommentResult) + implicit val commentResultJsonFormat = jsonFormat3(CommentResult) } diff --git a/jira-client/src/main/scala/com/nulabinc/jira/client/json/UserMappingJsonProtocol.scala b/jira-client/src/main/scala/com/nulabinc/jira/client/json/UserMappingJsonProtocol.scala index a43d2524..cec0b1bc 100644 --- a/jira-client/src/main/scala/com/nulabinc/jira/client/json/UserMappingJsonProtocol.scala +++ b/jira-client/src/main/scala/com/nulabinc/jira/client/json/UserMappingJsonProtocol.scala @@ -4,5 +4,5 @@ import com.nulabinc.jira.client.domain.User import spray.json.DefaultJsonProtocol object UserMappingJsonProtocol extends DefaultJsonProtocol { - implicit val userMappingFormat = jsonFormat2(User) + implicit val userMappingFormat = jsonFormat4(User) } diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/conf/JiraBacklogPaths.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/conf/JiraBacklogPaths.scala new file mode 100644 index 00000000..faeed9a1 --- /dev/null +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/conf/JiraBacklogPaths.scala @@ -0,0 +1,15 @@ +package com.nulabinc.backlog.j2b.jira.conf + +import com.nulabinc.backlog.migration.common.conf.BacklogPaths + +import scalax.file.Path + +class JiraBacklogPaths(backlogProjectKey: String) extends BacklogPaths(backlogProjectKey) { + + def jiraUsersJson: Path = outputPath / "project" / backlogProjectKey / "jiraUsers.json" + + def jiraStatusesJson: Path = outputPath / "project" / backlogProjectKey / "jiraStatuses.json" + + def jiraPrioritiesJson: Path = outputPath / "project" / backlogProjectKey / "jiraPriorities.json" + +} diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/converter/MappingConverter.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/converter/MappingConverter.scala index fcb65383..79d92821 100644 --- a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/converter/MappingConverter.scala +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/converter/MappingConverter.scala @@ -1,8 +1,8 @@ package com.nulabinc.backlog.j2b.jira.converter -import com.nulabinc.backlog.j2b.jira.domain.mapping.Mapping +import com.nulabinc.backlog.j2b.jira.domain.mapping.{Mapping, MappingCollectDatabase} trait MappingConverter { - def convert(userMaps: Seq[Mapping], priorityMaps: Seq[Mapping], statusMaps: Seq[Mapping]): Unit + def convert(database: MappingCollectDatabase, userMaps: Seq[Mapping], priorityMaps: Seq[Mapping], statusMaps: Seq[Mapping]): Unit } diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/converter/UserConverter.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/converter/UserConverter.scala index a97c0c82..a508b13e 100644 --- a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/converter/UserConverter.scala +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/converter/UserConverter.scala @@ -1,12 +1,12 @@ package com.nulabinc.backlog.j2b.jira.converter -import com.nulabinc.backlog.j2b.jira.domain.mapping.Mapping +import com.nulabinc.backlog.j2b.jira.domain.mapping.{Mapping, MappingCollectDatabase} import com.nulabinc.backlog.migration.common.domain.BacklogUser trait UserConverter { - def convert(mappings: Seq[Mapping], user: String): String + def convert(mappingCollectDatabase: MappingCollectDatabase, mappings: Seq[Mapping], user: String): String - def convert(mappings: Seq[Mapping], user: BacklogUser): BacklogUser + def convert(mappingCollectDatabase: MappingCollectDatabase, mappings: Seq[Mapping], user: BacklogUser): BacklogUser } diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/CollectData.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/CollectData.scala index c6ba06a9..51d61875 100644 --- a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/CollectData.scala +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/CollectData.scala @@ -1,9 +1,30 @@ package com.nulabinc.backlog.j2b.jira.domain +import com.nulabinc.backlog.migration.common.utils.IOUtil import com.nulabinc.jira.client.domain.{Priority, Status, User} +import scalax.file.Path + case class CollectData( users: Set[User], statuses: Seq[Status], priorities: Seq[Priority] -) +) { + + import spray.json._ + + def outputJiraUsersToFile(filePath: Path): Unit = { + import com.nulabinc.jira.client.json.UserMappingJsonProtocol._ + IOUtil.output(filePath, users.toJson.prettyPrint) + } + + def outputJiraStatusesToFile(filePath: Path): Unit = { + import com.nulabinc.jira.client.json.StatusMappingJsonProtocol._ + IOUtil.output(filePath, statuses.toJson.prettyPrint) + } + + def outputJiraPrioritiesToFile(filePath: Path): Unit = { + import com.nulabinc.jira.client.json.PriorityMappingJsonProtocol._ + IOUtil.output(filePath, priorities.toJson.prettyPrint) + } +} diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/FieldDefinition.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/FieldDefinition.scala new file mode 100644 index 00000000..b3531143 --- /dev/null +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/FieldDefinition.scala @@ -0,0 +1,6 @@ +package com.nulabinc.backlog.j2b.jira.domain + +import com.nulabinc.backlog.j2b.jira.domain.mapping.CustomFieldRow +import com.nulabinc.jira.client.domain.field.Field + +case class FieldDefinition(fields: Seq[Field], definitions: Seq[CustomFieldRow]) diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/export/Milestone.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/export/Milestone.scala new file mode 100644 index 00000000..3a65f4c4 --- /dev/null +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/export/Milestone.scala @@ -0,0 +1,45 @@ +package com.nulabinc.backlog.j2b.jira.domain.export + +import java.util.Date + +import com.nulabinc.backlog.migration.common.utils.DateUtil + +import scala.util.matching.Regex + +case class Milestone( + id: Long, + name: String, + goal: Option[String], + startDate: Option[String], + endDate: Option[Date] +) + +// com.atlassian.greenhopper.service.sprint.Sprint@2e84f4e0[id=4,rapidViewId=2,state=FUTURE,name=default スプリント 2,goal=,startDate=,endDate=,completeDate=,sequence=4] + +object Milestone { + + val pattern: Regex = """id=(\d+),.*?name=(.+?),.*?goal=(.+?),.*?startDate=(.+?),endDate=(.+?),""".r + + def apply(text: String): Milestone = { + + pattern.findFirstMatchIn(text) match { + case Some(m) => new Milestone( + id = m.group(1).toLong, + name = m.group(2), + goal = m.group(3) match { + case "" => None + case string => Some(string) + }, + startDate = m.group(4) match { + case "" => None + case string => Some(string) + }, + endDate = m.group(5) match { + case "" => None + case string => Some(DateUtil.yyyymmddParse(string)) + } + ) + case None => throw new RuntimeException("Cannot parse milestone. input: " + text) + } + } +} diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/mapping/Mapping.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/mapping/Mapping.scala index 52e2ec66..a79ad627 100644 --- a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/mapping/Mapping.scala +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/mapping/Mapping.scala @@ -8,11 +8,7 @@ case class MappingsWrapper(`//`: String, mappings: Seq[Mapping]) case class MappingInfo(name: String, mail: String) -case class Mapping(info: Option[MappingInfo], mappingType: String, src: String, dst: String) { - - def getMappingType() = MappingType(mappingType) - -} +case class Mapping(info: Option[MappingInfo], src: String, dst: String) object Mapping extends Logging { @@ -21,14 +17,12 @@ object Mapping extends Logging { case Some(userId) => Mapping( info = Some(MappingInfo(name = user.name, mail = user.optMailAddress.getOrElse(""))), - mappingType = MappingType.UserId.toString, src = userId, dst = "" ) case None => Mapping( info = Some(MappingInfo(name = user.name, mail = user.optMailAddress.getOrElse(""))), - mappingType = MappingType.Name.toString, src = user.name, dst = "" ) @@ -37,7 +31,6 @@ object Mapping extends Logging { def create(value: String) = Mapping( info = Some(MappingInfo(name = value, mail = "")), - mappingType = MappingType.Name.toString, src = value, dst = "" ) @@ -46,7 +39,7 @@ object Mapping extends Logging { object MappingJsonProtocol extends DefaultJsonProtocol { implicit val MappingDescriptionFormat = jsonFormat2(MappingInfo) - implicit val MappingFormat = jsonFormat4(Mapping.apply) + implicit val MappingFormat = jsonFormat3(Mapping.apply) implicit val MappingsWrapperFormat = jsonFormat2(MappingsWrapper) } diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/mapping/MappingCollectDatabase.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/mapping/MappingCollectDatabase.scala new file mode 100644 index 00000000..0fe79e56 --- /dev/null +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/mapping/MappingCollectDatabase.scala @@ -0,0 +1,35 @@ +package com.nulabinc.backlog.j2b.jira.domain.mapping + +import com.nulabinc.backlog.j2b.jira.domain.export.Milestone +import com.nulabinc.jira.client.domain.User + +import scala.collection.mutable + +case class CustomFieldRow(fieldId: String, values: mutable.Set[String]) + +trait MappingCollectDatabase { + + def add(user: Option[User]): Boolean + + def add(user: User): User + + def add(name: Option[String]): Option[User] + + def addIgnoreUser(name: Option[String]): Unit + + def existUsers: Set[User] + + def userExistsFromAllUsers(name: Option[String]): Boolean + + def existsByName(name: Option[String]): Boolean + + def findByName(name: Option[String]): Option[User] + + def addCustomField(fieldId: String, value: Option[String]): Option[String] + + def customFieldRows: Seq[CustomFieldRow] + + def addMilestone(milestone: Milestone): Unit + + def milestones: Seq[Milestone] +} diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/mapping/MappingFile.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/mapping/MappingFile.scala index 6e442d1d..4ec0798c 100644 --- a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/mapping/MappingFile.scala +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/domain/mapping/MappingFile.scala @@ -101,7 +101,6 @@ trait MappingFile extends Logging { private[this] def convert(jira: MappingItem): Mapping = Mapping( info = None, - mappingType = "", src = jira.name, dst = matchItem(jira) ) diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/service/IssueService.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/service/IssueService.scala index 42ee0235..2342101d 100644 --- a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/service/IssueService.scala +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/service/IssueService.scala @@ -1,6 +1,7 @@ package com.nulabinc.backlog.j2b.jira.service import com.nulabinc.jira.client.DownloadResult +import com.nulabinc.jira.client.domain.changeLog.ChangeLog import com.nulabinc.jira.client.domain.issue.Issue import scalax.file.Path @@ -11,11 +12,8 @@ trait IssueService { def issues(startAt: Long, maxResults: Long): Seq[Issue] - def injectChangeLogsToIssue(issue: Issue): Issue - - def injectAttachmentsToIssue(issue: Issue): Issue + def changeLogs(issue: Issue): Seq[ChangeLog] def downloadAttachments(attachmentId: Long, destinationPath: Path, fileName: String): DownloadResult - - + } diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/service/MappingFileService.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/service/MappingFileService.scala index 09fcad64..d972375c 100644 --- a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/service/MappingFileService.scala +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/service/MappingFileService.scala @@ -1,14 +1,25 @@ package com.nulabinc.backlog.j2b.jira.service import com.nulabinc.backlog.j2b.jira.domain.mapping.MappingFile +import com.nulabinc.backlog.migration.common.domain.BacklogUser import com.nulabinc.jira.client.domain.{Priority, Status, User} +import com.nulabinc.backlog4j.{Priority => BacklogPriority, Status => BacklogStatus} + +import scalax.file.Path trait MappingFileService { - def createUserMappingFile(users: Set[User]): MappingFile + def createUserMappingFile(users: Set[User], backlogUsers: Seq[BacklogUser]): MappingFile + + def createPriorityMappingFile(priorities: Seq[Priority], backlogPriorities: Seq[BacklogPriority]): MappingFile + + def createStatusMappingFile(statuses: Seq[Status], backlogStatuses: Seq[BacklogStatus]): MappingFile + + def createUserMappingFileFromJson(jiraUsersFilePath: Path, backlogUsers: Seq[BacklogUser]): MappingFile - def createPriorityMappingFile(priorities: Seq[Priority]): MappingFile + def createPrioritiesMappingFileFromJson(jiraPrioritiesFilePath: Path, backlogPriorities: Seq[BacklogPriority]): MappingFile - def createStatusMappingFile(statuses: Seq[Status]): MappingFile + def createStatusesMappingFileFromJson(jiraStatusesFilePath: Path, backlogStatuses: Seq[BacklogStatus]): MappingFile + def usersFromJson(jiraUsersFilePath: Path): Seq[User] } diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/service/UserService.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/service/UserService.scala index e7a6bdd7..c0bf5a10 100644 --- a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/service/UserService.scala +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/service/UserService.scala @@ -8,4 +8,6 @@ trait UserService { def optUserOfKey(key: Option[String]): Option[User] + def optUserOfName(name: Option[String]): Option[User] + } diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/utils/DateChangeLogConverter.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/utils/DateChangeLogConverter.scala new file mode 100644 index 00000000..9af762b8 --- /dev/null +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/utils/DateChangeLogConverter.scala @@ -0,0 +1,32 @@ +package com.nulabinc.backlog.j2b.jira.utils + +import com.nulabinc.jira.client.domain.changeLog.ChangeLog +import com.nulabinc.jira.client.domain.field.{DateSchema, DatetimeSchema, Field} + +trait DateChangeLogConverter extends DatetimeToDateFormatter { + + def convertDateChangeLogs(changeLogs: Seq[ChangeLog], definitions: Seq[Field]): Seq[ChangeLog] = { + changeLogs.map { changeLog => + val items = changeLog.items.map { item => + definitions.find(f => item.fieldId.exists(_.value == f.id)) match { + case Some(field) => field.schema match { + case Some(schema) if schema.schemaType == DateSchema => + item.copy( + fromDisplayString = item.from, + toDisplayString = item.to + ) + case Some(schema) if schema.schemaType == DatetimeSchema => + item.copy( + fromDisplayString = item.from.map(dateTimeStringToDateString), + toDisplayString = item.to.map(dateTimeStringToDateString) + ) + case _ => item + } + case _ => item + } + } + changeLog.copy(items = items) + } + } + +} diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/utils/DatetimeToDateFormatter.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/utils/DatetimeToDateFormatter.scala new file mode 100644 index 00000000..623eb968 --- /dev/null +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/utils/DatetimeToDateFormatter.scala @@ -0,0 +1,27 @@ +package com.nulabinc.backlog.j2b.jira.utils + +import java.text.SimpleDateFormat +import java.util.Locale + +import scala.util.Try + +trait DatetimeToDateFormatter { + + val readFormats = Seq( + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.getDefault()), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()) + ) + + val writeFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + + def dateTimeStringToDateString(dateTime: String): String = { + val date = readFormats.map { readFormat => + Try { readFormat.parse(dateTime) } + }.find(_.isSuccess) match { + case Some(x) => x.get + case _ => throw new Exception(s"Can not parse DateTime to Date: $dateTime") + } + writeFormat.format(date) + } + +} diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/utils/SecondToHourFormatter.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/utils/SecondToHourFormatter.scala new file mode 100644 index 00000000..54283ae1 --- /dev/null +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/utils/SecondToHourFormatter.scala @@ -0,0 +1,10 @@ +package com.nulabinc.backlog.j2b.jira.utils + +trait SecondToHourFormatter { + + def secondsToHours(seconds: Int): Float = { + val formattedText = "%.2f".format(seconds / 3600f) + formattedText.toFloat + } + +} diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/writer/FieldWriter.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/writer/FieldWriter.scala index 72532689..58ad6d51 100644 --- a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/writer/FieldWriter.scala +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/writer/FieldWriter.scala @@ -1,10 +1,11 @@ package com.nulabinc.backlog.j2b.jira.writer +import com.nulabinc.backlog.j2b.jira.domain.mapping.MappingCollectDatabase import com.nulabinc.backlog.migration.common.domain.BacklogCustomFieldSetting import com.nulabinc.jira.client.domain.field.Field trait FieldWriter { - def write(fields: Seq[Field]): Either[WriteError, Seq[BacklogCustomFieldSetting]] + def write(db: MappingCollectDatabase, fields: Seq[Field]): Either[WriteError, Seq[BacklogCustomFieldSetting]] } diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/writer/ProjectUserWriter.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/writer/ProjectUserWriter.scala new file mode 100644 index 00000000..7b92599a --- /dev/null +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/writer/ProjectUserWriter.scala @@ -0,0 +1,9 @@ +package com.nulabinc.backlog.j2b.jira.writer + +import com.nulabinc.backlog.migration.common.domain.BacklogUser + +trait ProjectUserWriter { + + def write(users: Seq[BacklogUser]): Either[WriteError, Seq[BacklogUser]] + +} diff --git a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/writer/VersionWriter.scala b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/writer/VersionWriter.scala index da9d16ce..8d00adc3 100644 --- a/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/writer/VersionWriter.scala +++ b/jira/src/main/scala/com/nulabinc/backlog/j2b/jira/writer/VersionWriter.scala @@ -1,10 +1,11 @@ package com.nulabinc.backlog.j2b.jira.writer +import com.nulabinc.backlog.j2b.jira.domain.export.Milestone import com.nulabinc.backlog.migration.common.domain.BacklogVersion import com.nulabinc.jira.client.domain.Version trait VersionWriter { - def write(versions: Seq[Version]): Either[WriteError, Seq[BacklogVersion]] + def write(versions: Seq[Version], milestones: Seq[Milestone]): Either[WriteError, Seq[BacklogVersion]] } diff --git a/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/MappingCollectDatabaseInMemory.scala b/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/MappingCollectDatabaseInMemory.scala new file mode 100644 index 00000000..30aeb951 --- /dev/null +++ b/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/MappingCollectDatabaseInMemory.scala @@ -0,0 +1,81 @@ +package com.nulabinc.backlog.j2b.mapping.collector + +import com.nulabinc.backlog.j2b.jira.domain.export.Milestone +import com.nulabinc.backlog.j2b.jira.domain.mapping.{CustomFieldRow, MappingCollectDatabase} +import com.nulabinc.jira.client.domain.User + +import scala.collection.mutable + +class MappingCollectDatabaseInMemory extends MappingCollectDatabase { + + private val userSet: mutable.Set[User] = mutable.Set[User]() + private val ignoreUserSet: mutable.Set[String] = mutable.Set[String]() + private val customFieldSet = mutable.Set.empty[CustomFieldRow] + private val milestoneSet = mutable.Set.empty[Milestone] + + override def add(user: Option[User]): Boolean = user match { + case Some(u) => + userSet += u + true + case None => false + } + + override def add(user: User): User = { + userSet += user + user + } + + override def add(optName: Option[String]) = optName match { + case Some(name) => + val user = User(key = name, name = name, displayName = name, emailAddress = "") + userSet += user + Some(user) + case None => None + } + + override def addIgnoreUser(name: Option[String]): Unit = name match { + case Some(n) => ignoreUserSet += n + case None => () + } + + override def existUsers: Set[User] = userSet.toSet + + override def existsByName(name: Option[String]): Boolean = name match { + case Some(n) => userSet.exists(_.name == n) + case None => false + } + + override def userExistsFromAllUsers(name: Option[String]): Boolean = name match { + case Some(n) => + if (existsByName(name)) true + else ignoreUserSet.contains(n) + case None => false + } + + + override def findByName(name: Option[String]): Option[User] = name match { + case Some(_) => userSet.find(_.name == name) + case None => None + } + + + override def addCustomField(fieldId: String, value: Option[String]): Option[String] = value.map { v => + val items = v.split(",").map(_.trim).filter(_.nonEmpty) + customFieldSet.find(_.fieldId == fieldId) match { + case Some(row) => + items.map(str => row.values.add(str)) + v + case None => + customFieldSet += CustomFieldRow(fieldId, mutable.Set() ++ items) + v + } + } + + override def customFieldRows: Seq[CustomFieldRow] = customFieldSet.toSeq + + override def addMilestone(milestone: Milestone): Unit = milestoneSet += milestone + + override def milestones: Seq[Milestone] = milestoneSet.toSeq + + +} diff --git a/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/core/Boot.scala b/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/core/Boot.scala deleted file mode 100644 index 5cdc68ea..00000000 --- a/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/core/Boot.scala +++ /dev/null @@ -1,33 +0,0 @@ -package com.nulabinc.backlog.j2b.mapping.collector.core - -import com.google.inject.Guice -import com.nulabinc.backlog.j2b.jira.conf.JiraApiConfiguration -import com.nulabinc.backlog.j2b.mapping.collector.modules.JiraModule -import com.nulabinc.backlog.j2b.mapping.collector.service.MappingCollector -import com.nulabinc.backlog.migration.common.utils.{ConsoleOut, Logging} -import com.nulabinc.jira.client.domain.User -import com.osinka.i18n.Messages - -import scala.collection.mutable - -object Boot extends Logging { - - def execute(apiConfig: JiraApiConfiguration): MappingData = { - - val injector = Guice.createInjector(new JiraModule(apiConfig)) - - ConsoleOut.println(s""" - |${Messages("cli.project_info.start")} - |-------------------------------------------------- - |""".stripMargin) - - val mappingCollector = injector.getInstance(classOf[MappingCollector]) - val mappingData = mappingCollector.boot() - - ConsoleOut.println(s"""|-------------------------------------------------- - |${Messages("cli.project_info.finish")} - |""".stripMargin) - - mappingData - } -} diff --git a/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/modules/JiraModule.scala b/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/modules/JiraModule.scala deleted file mode 100644 index 58a0ba07..00000000 --- a/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/modules/JiraModule.scala +++ /dev/null @@ -1,8 +0,0 @@ -package com.nulabinc.backlog.j2b.mapping.collector.modules - -import com.nulabinc.backlog.j2b.jira.conf.JiraApiConfiguration -import com.nulabinc.backlog.j2b.modules.DefaultModule - -private [collector] class JiraModule(apiConfig: JiraApiConfiguration) - extends DefaultModule(apiConfig) {} - diff --git a/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/service/CollectService.scala b/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/service/CollectService.scala deleted file mode 100644 index be096856..00000000 --- a/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/service/CollectService.scala +++ /dev/null @@ -1,8 +0,0 @@ -package com.nulabinc.backlog.j2b.mapping.collector.service - -import com.nulabinc.jira.client.domain.issue.Issue - -trait CollectService[T] { - - def collect(issues: Seq[Issue]): Set[T] -} diff --git a/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/service/MappingCollector.scala b/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/service/MappingCollector.scala deleted file mode 100644 index ed805cfc..00000000 --- a/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/service/MappingCollector.scala +++ /dev/null @@ -1,29 +0,0 @@ -package com.nulabinc.backlog.j2b.mapping.collector.service - -import javax.inject.Inject - -import com.nulabinc.backlog.j2b.jira.service.IssueReadService -import com.nulabinc.backlog.j2b.mapping.collector.core._ -import com.nulabinc.backlog.migration.common.utils.Logging -import com.nulabinc.jira.client.domain.{Status, User} - -private [collector] class MappingCollector @Inject()(issueReadService: IssueReadService, - userCollectService: UserCollectService, - statusCollectService: StatusCollectService) - extends Logging { - - def boot() = { - - issueReadService.read("issue.txt") match { - case Right(issues) => { - val users = userCollectService.collect(issues) - val statuses = statusCollectService.collect(issues) - MappingData(users, statuses) - } - case Left(error) => { - logger.error(error.toString) - MappingData(Set.empty[User], Set.empty[Status]) - } - } - } -} diff --git a/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/service/StatusCollectService.scala b/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/service/StatusCollectService.scala deleted file mode 100644 index 1c187ca4..00000000 --- a/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/service/StatusCollectService.scala +++ /dev/null @@ -1,10 +0,0 @@ -package com.nulabinc.backlog.j2b.mapping.collector.service - -import com.nulabinc.jira.client.domain.issue.Issue -import com.nulabinc.jira.client.domain.{Issue, Status} - -class StatusCollectService extends CollectService[Status] { - - override def collect(issues: Seq[Issue]) = ??? - -} diff --git a/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/service/UserCollectService.scala b/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/service/UserCollectService.scala deleted file mode 100644 index 7c755910..00000000 --- a/mapping-collector/src/main/scala/com/nulabinc/backlog/j2b/mapping/collector/service/UserCollectService.scala +++ /dev/null @@ -1,20 +0,0 @@ -package com.nulabinc.backlog.j2b.mapping.collector.service - -import com.nulabinc.backlog.migration.common.utils.Logging -import com.nulabinc.jira.client.domain.User -import com.nulabinc.jira.client.domain.issue.Issue - -class UserCollectService() extends CollectService[User] with Logging { - - override def collect(issues: Seq[Issue]) = { - - - issues.foldLeft(Set.empty[User]) { - case (acc, issue) => issue.assignee match { - case Some(user) => acc + user - case _ => acc - } - } - - } -} diff --git a/mapping-converter/src/main/scala/com/nulabinc/backlog/j2b/mapping/converter/MappingConvertService.scala b/mapping-converter/src/main/scala/com/nulabinc/backlog/j2b/mapping/converter/MappingConvertService.scala index 5740f30d..f8f06647 100644 --- a/mapping-converter/src/main/scala/com/nulabinc/backlog/j2b/mapping/converter/MappingConvertService.scala +++ b/mapping-converter/src/main/scala/com/nulabinc/backlog/j2b/mapping/converter/MappingConvertService.scala @@ -3,7 +3,7 @@ package com.nulabinc.backlog.j2b.mapping.converter import javax.inject.Inject import com.nulabinc.backlog.j2b.jira.converter._ -import com.nulabinc.backlog.j2b.jira.domain.mapping.Mapping +import com.nulabinc.backlog.j2b.jira.domain.mapping.{Mapping, MappingCollectDatabase} import com.nulabinc.backlog.j2b.mapping.converter.writes._ import com.nulabinc.backlog.migration.common.conf.{BacklogConstantValue, BacklogPaths} import com.nulabinc.backlog.migration.common.convert.{BacklogUnmarshaller, Convert} @@ -22,25 +22,25 @@ class MappingConvertService @Inject()(implicit val issueWrites: IssueWrites, backlogPaths: BacklogPaths) extends MappingConverter { - def convert(userMaps: Seq[Mapping], priorityMaps: Seq[Mapping], statusMaps: Seq[Mapping]): Unit = { + def convert(database: MappingCollectDatabase, userMaps: Seq[Mapping], priorityMaps: Seq[Mapping], statusMaps: Seq[Mapping]): Unit = { val paths: Seq[Path] = IOUtil.directoryPaths(backlogPaths.issueDirectoryPath) .flatMap(_.toAbsolute.children().filter(_.isDirectory).toSeq) paths.zipWithIndex.foreach { case (path, index) => - convertIssue(path, index, paths.size, userMaps, priorityMaps, statusMaps) + convertIssue(database, path, index, paths.size, userMaps, priorityMaps, statusMaps) } } - private def convertIssue(path: Path, index: Int, size: Int, userMaps: Seq[Mapping], priorityMaps: Seq[Mapping], statusMaps: Seq[Mapping]) = { + private def convertIssue(database: MappingCollectDatabase, path: Path, index: Int, size: Int, userMaps: Seq[Mapping], priorityMaps: Seq[Mapping], statusMaps: Seq[Mapping]): Unit = { BacklogUnmarshaller.issue(backlogPaths.issueJson(path)) match { case Some(issue: BacklogIssue) => { val converted = issue.copy( - optAssignee = issue.optAssignee.map(userConverter.convert(userMaps, _)), - notifiedUsers = issue.notifiedUsers.map(userConverter.convert(userMaps, _)), + optAssignee = issue.optAssignee.map(userConverter.convert(database, userMaps, _)), + notifiedUsers = issue.notifiedUsers.map(userConverter.convert(database, userMaps, _)), operation = issue.operation.copy( - optCreatedUser = issue.operation.optCreatedUser.map(userConverter.convert(userMaps, _)), - optUpdatedUser = issue.operation.optUpdatedUser.map(userConverter.convert(userMaps, _)) + optCreatedUser = issue.operation.optCreatedUser.map(userConverter.convert(database, userMaps, _)), + optUpdatedUser = issue.operation.optUpdatedUser.map(userConverter.convert(database, userMaps, _)) ), priorityName = priorityConverter.convert(priorityMaps, issue.priorityName), statusName = statusConverter.convert(statusMaps, issue.statusName) @@ -59,8 +59,8 @@ class MappingConvertService @Inject()(implicit val issueWrites: IssueWrites, optNewValue = changeLog.optNewValue.map(priorityConverter.convert(priorityMaps, _)) ) case BacklogConstantValue.ChangeLog.ASSIGNER => changeLog.copy( - optOriginalValue = changeLog.optOriginalValue.map(userConverter.convert(userMaps, _)), - optNewValue = changeLog.optNewValue.map(userConverter.convert(userMaps, _)) + optOriginalValue = changeLog.optOriginalValue.map(userConverter.convert(database, userMaps, _)), + optNewValue = changeLog.optNewValue.map(userConverter.convert(database, userMaps, _)) ) case _ => changeLog } diff --git a/mapping-converter/src/main/scala/com/nulabinc/backlog/j2b/mapping/converter/MappingUserConverter.scala b/mapping-converter/src/main/scala/com/nulabinc/backlog/j2b/mapping/converter/MappingUserConverter.scala index 6e931ee1..9eb55f1d 100644 --- a/mapping-converter/src/main/scala/com/nulabinc/backlog/j2b/mapping/converter/MappingUserConverter.scala +++ b/mapping-converter/src/main/scala/com/nulabinc/backlog/j2b/mapping/converter/MappingUserConverter.scala @@ -3,7 +3,7 @@ package com.nulabinc.backlog.j2b.mapping.converter import javax.inject.Inject import com.nulabinc.backlog.j2b.jira.converter.UserConverter -import com.nulabinc.backlog.j2b.jira.domain.mapping.{Mapping, MappingType} +import com.nulabinc.backlog.j2b.jira.domain.mapping.{Mapping, MappingCollectDatabase, MappingType} import com.nulabinc.backlog.j2b.mapping.converter.writes.UserWrites import com.nulabinc.backlog.migration.common.convert.Convert import com.nulabinc.backlog.migration.common.domain.BacklogUser @@ -13,48 +13,47 @@ import com.osinka.i18n.Messages class MappingUserConverter @Inject()(implicit val userWrites: UserWrites) extends UserConverter with Logging { - override def convert(mappings: Seq[Mapping], user: BacklogUser): BacklogUser = + override def convert(mappingCollectDatabase: MappingCollectDatabase, mappings: Seq[Mapping], user: BacklogUser): BacklogUser = user.optUserId match { case Some(userId) => try { Convert.toBacklog(mappingOfUserId(mappings, userId)) } catch { case _: Throwable => - Convert.toBacklog(mappingOfName(mappings, user.name)) + Convert.toBacklog(mappingOfName(mappingCollectDatabase, mappings, user.name)) } - case _ => Convert.toBacklog(mappingOfName(mappings, user.name)) - } - - override def convert(mappings: Seq[Mapping], user: String) = - mappingOfName(mappings, user).dst - - private def mappingOfUserId(mappings: Seq[Mapping], userId: String): Mapping = { - mappings.filter(_.getMappingType() == MappingType.UserId).find(_.src.trim == userId.trim) match { - case Some(mapping) if mapping.dst.nonEmpty => mapping case _ => - ConsoleOut.error(Messages("convert.user.failed", userId)) - throw new RuntimeException(Messages("convert.user.failed", userId)) + val m = mappingOfName(mappingCollectDatabase, mappings, user.name) + Convert.toBacklog(m) } - } - private def mappingOfName(mappings: Seq[Mapping], userName: String): Mapping = { - mappings.find(_.src.trim == userName.trim) match { - case Some(mapping) if mapping.dst.nonEmpty => mapping - case _ => mappingOfInfoName(mappings, userName) + override def convert(mappingCollectDatabase: MappingCollectDatabase, mappings: Seq[Mapping], user: String) = + mappingOfName(mappingCollectDatabase, mappings, user).dst + + private def mappingOfUserId(mappings: Seq[Mapping], userId: String): Mapping = + mappings.find(_.dst.trim == userId.trim) match { + case Some(mapping) if mapping.dst.nonEmpty => Mapping(None, userId, userId) + case _ => mappings.find( u => u.src.trim == userId.trim) match { + case Some(mapping) if mapping.dst.nonEmpty => mapping + case _ => + ConsoleOut.error(Messages("convert.user.failed", userId)) + throw new RuntimeException(Messages("convert.user.failed", userId)) + } } - } - private def mappingOfInfoName(mappings: Seq[Mapping], userName: String): Mapping = { - mappings.find(_.info.map(_.name).getOrElse("").trim == userName.trim) match { - case Some(mapping) if mapping.dst.nonEmpty => mapping - case _ => mappings.find(_.dst.trim == userName.trim) match { - case Some(user) => user - case _ => { - ConsoleOut.error(Messages("convert.user.failed", userName)) - throw new RuntimeException(Messages("convert.user.failed", userName)) - } + private def mappingOfName(mappingCollectDatabase: MappingCollectDatabase, mappings: Seq[Mapping], userName: String): Mapping = + mappings.find(u => u.dst.trim == userName.trim) match { + case Some(mapping) if mapping.dst.nonEmpty => Mapping(None, mapping.dst, mapping.dst) + case _ => mappings.find(u => u.src.trim == userName.trim) match { + case Some(mapping) if mapping.dst.nonEmpty => Mapping(None, mapping.dst, mapping.dst) + case _ => mappingOfInfoName(mappingCollectDatabase, mappings, userName) } + } + private def mappingOfInfoName(mappingCollectDatabase: MappingCollectDatabase, mappings: Seq[Mapping], userName: String): Mapping = { + mappingCollectDatabase.findByName(Some(userName)) match { + case Some(user) => Mapping(None, user.name, user.name) + case _ => Mapping(None, userName, userName) } } } diff --git a/mapping-converter/src/main/scala/com/nulabinc/backlog/j2b/mapping/converter/writes/MappingUserWrites.scala b/mapping-converter/src/main/scala/com/nulabinc/backlog/j2b/mapping/converter/writes/MappingUserWrites.scala new file mode 100644 index 00000000..3166d397 --- /dev/null +++ b/mapping-converter/src/main/scala/com/nulabinc/backlog/j2b/mapping/converter/writes/MappingUserWrites.scala @@ -0,0 +1,21 @@ +package com.nulabinc.backlog.j2b.mapping.converter.writes + +import javax.inject.Inject + +import com.nulabinc.backlog.j2b.jira.domain.mapping.Mapping +import com.nulabinc.backlog.migration.common.conf.BacklogConstantValue +import com.nulabinc.backlog.migration.common.convert.Writes +import com.nulabinc.backlog.migration.common.domain.BacklogUser + +class MappingUserWrites @Inject()() extends Writes[Mapping, BacklogUser] { + + override def writes(mapping: Mapping): BacklogUser = { + BacklogUser(optId = None, + optUserId = Some(mapping.dst), + optPassword = None, + name = mapping.src, + optMailAddress = None, + roleType = BacklogConstantValue.USER_ROLE) + } + +} diff --git a/mapping-file/src/main/scala/com/nulabinc/backlog/j2b/mapping/file/MappingFileServiceImpl.scala b/mapping-file/src/main/scala/com/nulabinc/backlog/j2b/mapping/file/MappingFileServiceImpl.scala index 8d34a572..d913837f 100644 --- a/mapping-file/src/main/scala/com/nulabinc/backlog/j2b/mapping/file/MappingFileServiceImpl.scala +++ b/mapping-file/src/main/scala/com/nulabinc/backlog/j2b/mapping/file/MappingFileServiceImpl.scala @@ -9,33 +9,45 @@ import com.nulabinc.backlog.j2b.jira.service.MappingFileService import com.nulabinc.backlog.migration.common.modules.{ServiceInjector => BacklogInjector} import com.nulabinc.backlog.migration.common.service.{PriorityService => BacklogPriorityService, StatusService => BacklogStatusService, UserService => BacklogUserService} import com.nulabinc.backlog.migration.common.conf.BacklogApiConfiguration +import com.nulabinc.backlog.migration.common.domain.BacklogUser +import com.nulabinc.backlog.migration.common.utils.IOUtil +import com.nulabinc.backlog4j.{Status => BacklogStatus, Priority => BacklogPriority} + +import scalax.file.Path +import spray.json._ class MappingFileServiceImpl @Inject()(jiraApiConfig: JiraApiConfiguration, backlogApiConfig: BacklogApiConfiguration) extends MappingFileService { - override def createUserMappingFile(users: Set[JiraUser]): MappingFile = { - val injector = BacklogInjector.createInjector(backlogApiConfig) - val userService = injector.getInstance(classOf[BacklogUserService]) - val backlogUsers = userService.allUsers() + override def createUserMappingFile(jiraUsers: Set[JiraUser], backlogUsers: Seq[BacklogUser]): MappingFile = + new UserMappingFile(backlogApiConfig, jiraUsers.toSeq, backlogUsers) - new UserMappingFile(backlogApiConfig, users.toSeq, backlogUsers) - } + override def createPriorityMappingFile(jiraPriorities: Seq[JiraPriority], backlogPriorities: Seq[BacklogPriority]): MappingFile = + new PriorityMappingFile(jiraPriorities, backlogPriorities) - override def createPriorityMappingFile(jiraPriorities: Seq[JiraPriority]): MappingFile = { - val injector = BacklogInjector.createInjector(backlogApiConfig) - val priorityService = injector.getInstance(classOf[BacklogPriorityService]) - val backlogPriorities = priorityService.allPriorities() + override def createStatusMappingFile(jiraStatuses: Seq[JiraStatus], backlogStatuses: Seq[BacklogStatus]): MappingFile = + new StatusMappingFile(jiraStatuses, backlogStatuses) + + override def createUserMappingFileFromJson(jiraUsersFilePath: Path, backlogUsers: Seq[BacklogUser]): MappingFile = + new UserMappingFile(backlogApiConfig, usersFromJson(jiraUsersFilePath), backlogUsers) + + override def createPrioritiesMappingFileFromJson(jiraPrioritiesFilePath: Path, backlogPriorities: Seq[BacklogPriority]): PriorityMappingFile = { + import com.nulabinc.jira.client.json.PriorityMappingJsonProtocol._ + val jiraPriorities = JsonParser(IOUtil.input(jiraPrioritiesFilePath).get).convertTo[Seq[JiraPriority]] new PriorityMappingFile(jiraPriorities, backlogPriorities) } - override def createStatusMappingFile(jiraStatuses: Seq[JiraStatus]): MappingFile = { - val injector = BacklogInjector.createInjector(backlogApiConfig) - val statusService = injector.getInstance(classOf[BacklogStatusService]) - val backlogStatuses = statusService.allStatuses() + override def createStatusesMappingFileFromJson(jiraStatusesFilePath: Path, backlogStatuses: Seq[BacklogStatus]): StatusMappingFile = { + import com.nulabinc.jira.client.json.StatusMappingJsonProtocol._ + val jiraStatuses = JsonParser(IOUtil.input(jiraStatusesFilePath).get).convertTo[Seq[JiraStatus]] new StatusMappingFile(jiraStatuses, backlogStatuses) } + override def usersFromJson(jiraUsersFilePath: Path): Seq[JiraUser] = { + import com.nulabinc.jira.client.json.UserMappingJsonProtocol._ + JsonParser(IOUtil.input(jiraUsersFilePath).get).convertTo[Seq[JiraUser]] + } } diff --git a/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/FieldFileWriter.scala b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/FieldFileWriter.scala index e47ca818..a31424f0 100644 --- a/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/FieldFileWriter.scala +++ b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/FieldFileWriter.scala @@ -3,6 +3,8 @@ package com.nulabinc.backlog.j2b.issue.writer import javax.inject.Inject import com.nulabinc.backlog.j2b.issue.writer.convert.FieldWrites +import com.nulabinc.backlog.j2b.jira.domain.FieldDefinition +import com.nulabinc.backlog.j2b.jira.domain.mapping.MappingCollectDatabase import com.nulabinc.backlog.j2b.jira.writer.FieldWriter import com.nulabinc.backlog.migration.common.conf.BacklogPaths import com.nulabinc.backlog.migration.common.convert.Convert @@ -16,8 +18,9 @@ class FieldFileWriter @Inject()(implicit val fieldWrites: FieldWrites, import com.nulabinc.backlog.migration.common.domain.BacklogJsonProtocol._ - override def write(fields: Seq[Field]) = { - val backlogFields = Convert.toBacklog(fields) + override def write(db: MappingCollectDatabase, fields: Seq[Field]) = { + val fieldDefinitions = FieldDefinition(fields, db.customFieldRows) + val backlogFields = Convert.toBacklog(fieldDefinitions) IOUtil.output(backlogPaths.customFieldSettingsJson, BacklogCustomFieldSettingsWrapper(backlogFields).toJson.prettyPrint) Right(backlogFields) } diff --git a/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/ProjectUserFileWriter.scala b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/ProjectUserFileWriter.scala new file mode 100644 index 00000000..e4f255d0 --- /dev/null +++ b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/ProjectUserFileWriter.scala @@ -0,0 +1,23 @@ +package com.nulabinc.backlog.j2b.issue.writer + +import javax.inject.Inject + +import com.nulabinc.backlog.j2b.issue.writer.convert.UserWrites +import com.nulabinc.backlog.j2b.jira.writer.ProjectUserWriter +import com.nulabinc.backlog.migration.common.conf.BacklogPaths +import com.nulabinc.backlog.migration.common.convert.Convert +import com.nulabinc.backlog.migration.common.domain.{BacklogProjectUsersWrapper, BacklogUser} +import com.nulabinc.backlog.migration.common.utils.IOUtil +import com.nulabinc.jira.client.domain.User +import spray.json._ + +class ProjectUserFileWriter @Inject()(implicit val userWrites: UserWrites, + backlogPaths: BacklogPaths) extends ProjectUserWriter { + + + override def write(users: Seq[BacklogUser]) = { + import com.nulabinc.backlog.migration.common.domain.BacklogJsonProtocol._ + IOUtil.output(backlogPaths.projectUsersJson , BacklogProjectUsersWrapper(users).toJson.prettyPrint) + Right(users) + } +} \ No newline at end of file diff --git a/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/VersionFileWriter.scala b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/VersionFileWriter.scala index 1cfd5852..b038d2d2 100644 --- a/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/VersionFileWriter.scala +++ b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/VersionFileWriter.scala @@ -3,12 +3,14 @@ package com.nulabinc.backlog.j2b.issue.writer import javax.inject.Inject import com.nulabinc.backlog.j2b.issue.writer.convert.VersionWrites +import com.nulabinc.backlog.j2b.jira.domain.export.Milestone import com.nulabinc.backlog.j2b.jira.writer.VersionWriter import com.nulabinc.backlog.migration.common.conf.BacklogPaths import com.nulabinc.backlog.migration.common.convert.Convert import com.nulabinc.backlog.migration.common.domain.BacklogVersionsWrapper import com.nulabinc.backlog.migration.common.utils.IOUtil import com.nulabinc.jira.client.domain.Version +import org.joda.time.DateTime import spray.json._ class VersionFileWriter @Inject()(implicit val versionsWrites: VersionWrites, @@ -16,8 +18,18 @@ class VersionFileWriter @Inject()(implicit val versionsWrites: VersionWrites, import com.nulabinc.backlog.migration.common.domain.BacklogJsonProtocol.BacklogVersionsWrapperFormat - override def write(versions: Seq[Version]) = { - val backlogVersions = Convert.toBacklog(versions) + override def write(versions: Seq[Version], milestones: Seq[Milestone]) = { + val convertedMilestones = milestones.map { milestone => + Version( + id = None, + name = milestone.name, + description = milestone.goal, + archived = false, + released = false, + releaseDate = milestone.endDate.map(new DateTime(_)) + ) + } + val backlogVersions = Convert.toBacklog(versions ++ convertedMilestones) IOUtil.output(backlogPaths.versionsJson, BacklogVersionsWrapper(backlogVersions).toJson.prettyPrint) Right(backlogVersions) } diff --git a/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/ChangelogItemWrites.scala b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/ChangelogItemWrites.scala index 4890389d..b283922d 100644 --- a/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/ChangelogItemWrites.scala +++ b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/ChangelogItemWrites.scala @@ -2,6 +2,7 @@ package com.nulabinc.backlog.j2b.issue.writer.convert import javax.inject.Inject +import com.nulabinc.backlog.j2b.jira.utils.SecondToHourFormatter import com.nulabinc.backlog.migration.common.conf.BacklogConstantValue import com.nulabinc.backlog.migration.common.convert.Writes import com.nulabinc.backlog.migration.common.domain._ @@ -10,7 +11,9 @@ import com.nulabinc.backlog4j.CustomField.FieldType import com.nulabinc.jira.client.domain.field._ import com.nulabinc.jira.client.domain.changeLog._ -class ChangelogItemWrites @Inject()(fields: Seq[Field]) extends Writes[ChangeLogItem, BacklogChangeLog] { +class ChangelogItemWrites @Inject()(fields: Seq[Field]) + extends Writes[ChangeLogItem, BacklogChangeLog] + with SecondToHourFormatter { override def writes(changeLogItem: ChangeLogItem) = BacklogChangeLog( @@ -18,15 +21,33 @@ class ChangelogItemWrites @Inject()(fields: Seq[Field]) extends Writes[ChangeLog optOriginalValue = changeLogItem.fieldId match { case Some(AssigneeFieldId) => changeLogItem.from case Some(DueDateFieldId) => changeLogItem.from - case Some(TimeOriginalEstimateFieldId) => changeLogItem.from.map( sec => (sec.toInt / 3600).toString) - case Some(TimeEstimateFieldId) => changeLogItem.from.map( sec => (sec.toInt / 3600).toString) + case Some(TimeOriginalEstimateFieldId) => changeLogItem.from.map( sec => secondsToHours(sec.toInt).toString) + case Some(TimeEstimateFieldId) => changeLogItem.from.map( sec => secondsToHours(sec.toInt).toString) + case Some(TimeSpentFieldId) => changeLogItem.from.map( sec => secondsToHours(sec.toInt).toString) + case Some(CustomFieldFieldId(id)) => + fields.find(_.id == id) match { + case Some(field) if field.schema.isDefined => + if(field.schema.get.schemaType == UserSchema) changeLogItem.from + else changeLogItem.fromDisplayString + case None => changeLogItem.fromDisplayString + } + case None if changeLogItem.field == ParentChangeLogItemField => changeLogItem.from case _ => changeLogItem.fromDisplayString }, optNewValue = changeLogItem.fieldId match { case Some(AssigneeFieldId) => changeLogItem.to case Some(DueDateFieldId) => changeLogItem.to - case Some(TimeOriginalEstimateFieldId) => changeLogItem.to.map( sec => (sec.toInt / 3600).toString) - case Some(TimeEstimateFieldId) => changeLogItem.to.map( sec => (sec.toInt / 3600).toString) + case Some(TimeOriginalEstimateFieldId) => changeLogItem.to.map( sec => secondsToHours(sec.toInt).toString) + case Some(TimeEstimateFieldId) => changeLogItem.to.map( sec => secondsToHours(sec.toInt).toString) + case Some(TimeSpentFieldId) => changeLogItem.to.map( sec => secondsToHours(sec.toInt).toString) + case Some(CustomFieldFieldId(id)) => + fields.find(_.id == id) match { + case Some(field) if field.schema.isDefined => + if(field.schema.get.schemaType == UserSchema) changeLogItem.to + else changeLogItem.toDisplayString + case None => changeLogItem.toDisplayString + } + case None if changeLogItem.field == ParentChangeLogItemField => changeLogItem.to case _ => changeLogItem.toDisplayString }, optAttachmentInfo = attachmentInfo(changeLogItem), @@ -55,7 +76,8 @@ class ChangelogItemWrites @Inject()(fields: Seq[Field]) extends Writes[ChangeLog case Some(TimeEstimateFieldId) => changeLogItem.field.value case Some(ResolutionFieldId) => BacklogConstantValue.ChangeLog.RESOLUTION case Some(GeneralFieldId(v)) => v - case _ if changeLogItem.field == Parent => BacklogConstantValue.ChangeLog.ISSUE_TYPE + case Some(TimeSpentFieldId) => BacklogConstantValue.ChangeLog.ACTUAL_HOURS + case _ if changeLogItem.field == ParentChangeLogItemField => BacklogConstantValue.ChangeLog.PARENT_ISSUE case None => changeLogItem.field.value } @@ -76,19 +98,20 @@ class ChangelogItemWrites @Inject()(fields: Seq[Field]) extends Writes[ChangeLog case Some(field) => field.schema.map { schema => (schema.schemaType, schema.customType) match { - case (StatusSchema, Some(Textarea)) => Some(FieldType.TextArea.getIntValue) - case (StringSchema, _) => Some(FieldType.Text.getIntValue) - case (NumberSchema, _) => Some(FieldType.Numeric.getIntValue) - case (DateSchema, _) => Some(FieldType.Date.getIntValue) - case (DatetimeSchema, _) => Some(FieldType.Date.getIntValue) - case (ArraySchema, _) => Some(FieldType.MultipleList.getIntValue) - case (UserSchema, _) => Some(FieldType.Text.getIntValue) - case (AnySchema, _) => Some(FieldType.Text.getIntValue) - case (OptionSchema, Some(Select)) => Some(FieldType.SingleList.getIntValue) - case (OptionSchema, Some(MultiCheckBoxes)) => Some(FieldType.MultipleList.getIntValue) - case (OptionSchema, Some(RadioButtons)) => Some(FieldType.SingleList.getIntValue) - case (OptionSchema, _) => Some(FieldType.Text.getIntValue) - case (OptionWithChildSchema, _) => Some(FieldType.MultipleList.getIntValue) + case (StatusSchema, Some(Textarea)) => FieldType.TextArea.getIntValue + case (StringSchema, Some(CustomLabel)) => FieldType.MultipleList.getIntValue + case (StringSchema, _) => FieldType.Text.getIntValue + case (NumberSchema, _) => FieldType.Numeric.getIntValue + case (DateSchema, _) => FieldType.Date.getIntValue + case (DatetimeSchema, _) => FieldType.Date.getIntValue + case (ArraySchema, _) => FieldType.MultipleList.getIntValue + case (UserSchema, _) => FieldType.Text.getIntValue + case (AnySchema, _) => FieldType.Text.getIntValue + case (OptionSchema, Some(Select)) => FieldType.SingleList.getIntValue + case (OptionSchema, Some(MultiCheckBoxes)) => FieldType.MultipleList.getIntValue + case (OptionSchema, Some(RadioButtons)) => FieldType.SingleList.getIntValue + case (OptionSchema, _) => FieldType.Text.getIntValue + case (OptionWithChildSchema, _) => FieldType.MultipleList.getIntValue } } case _ => throw new RuntimeException(s"custom field id not found [${changeLogItem.field}]") diff --git a/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/FieldWrites.scala b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/FieldWrites.scala index 84e5f48f..118e38d9 100644 --- a/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/FieldWrites.scala +++ b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/FieldWrites.scala @@ -1,17 +1,18 @@ package com.nulabinc.backlog.j2b.issue.writer.convert +import com.nulabinc.backlog.j2b.jira.domain.FieldDefinition +import com.nulabinc.backlog.j2b.jira.domain.mapping.CustomFieldRow import com.nulabinc.backlog.migration.common.conf.BacklogConstantValue import com.nulabinc.backlog.migration.common.convert.Writes import com.nulabinc.backlog.migration.common.domain._ import com.nulabinc.backlog.migration.common.utils.Logging import com.nulabinc.backlog4j.CustomField.FieldType import com.nulabinc.jira.client.domain.field._ -import com.osinka.i18n.Messages -class FieldWrites extends Writes[Seq[Field], Seq[BacklogCustomFieldSetting]] with Logging { +class FieldWrites extends Writes[FieldDefinition, Seq[BacklogCustomFieldSetting]] with Logging { - override def writes(fields: Seq[Field]) = { - fields + override def writes(fieldDefinition: FieldDefinition) = { + fieldDefinition.fields .filter(_.schema.isDefined) .filter(_.id.startsWith("customfield_")) .map { field => @@ -23,7 +24,7 @@ class FieldWrites extends Writes[Seq[Field], Seq[BacklogCustomFieldSetting]] wit required = false, applicableIssueTypes = Seq.empty[String], delete = false, - property = property(field.schema.get) + property = property(fieldDefinition.definitions, field.schema.get, field.id) ) } } @@ -48,28 +49,30 @@ class FieldWrites extends Writes[Seq[Field], Seq[BacklogCustomFieldSetting]] wit optMax = None ) - private[this] def multipleProperty(isMultiple: Boolean): BacklogCustomFieldMultipleProperty = { + private[this] def multipleProperty(definitions: Seq[CustomFieldRow], isMultiple: Boolean, id: String): BacklogCustomFieldMultipleProperty = { def multipleTypeId(isMultiple: Boolean): Int = { if (isMultiple) BacklogConstantValue.CustomField.MultipleList else BacklogConstantValue.CustomField.SingleList } + def findCustomFieldValues(fieldId: String): Seq[String] = definitions + .find(_.fieldId == fieldId).map(_.values.toSeq) + .getOrElse(Seq.empty[String]) + // def possibleValues(redmineCustomFieldDefinition: RedmineCustomFieldDefinition): Seq[String] = // redmineCustomFieldDefinition.fieldFormat match { // case RedmineConstantValue.FieldFormat.BOOL => booleanPossibleValues() // case _ => redmineCustomFieldDefinition.possibleValues // } -// def toBacklogItem(name: String): BacklogItem = -// BacklogItem(optId = None, name = name) + def toBacklogItem(name: String): BacklogItem = + BacklogItem(optId = None, name = name) // def booleanPossibleValues() = Seq(Messages("common.no"), Messages("common.yes")) BacklogCustomFieldMultipleProperty( typeId = multipleTypeId(isMultiple), - items = Seq.empty[BacklogItem], - // TODO: get possible values from jira api -// items = possibleValues(redmineCustomFieldDefinition).map(toBacklogItem), + items = findCustomFieldValues(id).map(toBacklogItem), allowAddItem = true, allowInput = false ) @@ -78,6 +81,9 @@ class FieldWrites extends Writes[Seq[Field], Seq[BacklogCustomFieldSetting]] wit private [this] def typeId(schema: FieldSchema): Int = (schema.schemaType, schema.customType) match { case (StatusSchema, Some(Textarea)) => FieldType.TextArea.getIntValue + case (OptionSchema, Some(Select)) => FieldType.SingleList.getIntValue + case (ArraySchema, Some(MultiCheckBoxes)) => FieldType.CheckBox.getIntValue + case (OptionSchema, Some(RadioButtons)) => FieldType.Radio.getIntValue case (StringSchema, _) => FieldType.Text.getIntValue case (NumberSchema, _) => FieldType.Numeric.getIntValue case (DateSchema, _) => FieldType.Date.getIntValue @@ -85,29 +91,26 @@ class FieldWrites extends Writes[Seq[Field], Seq[BacklogCustomFieldSetting]] wit case (ArraySchema, _) => FieldType.MultipleList.getIntValue case (UserSchema, _) => FieldType.Text.getIntValue case (AnySchema, _) => FieldType.Text.getIntValue - case (OptionSchema, Some(Select)) => FieldType.SingleList.getIntValue - case (OptionSchema, Some(MultiCheckBoxes)) => FieldType.MultipleList.getIntValue - case (OptionSchema, Some(RadioButtons)) => FieldType.Radio.getIntValue case (OptionSchema, _) => FieldType.Text.getIntValue case (OptionWithChildSchema, _) => FieldType.MultipleList.getIntValue } - private [this] def property(schema: FieldSchema): BacklogCustomFieldProperty = + private [this] def property(definitions: Seq[CustomFieldRow], schema: FieldSchema, id: String): BacklogCustomFieldProperty = (schema.schemaType, schema.customType) match { case (StatusSchema, Some(Textarea)) => textProperty() case (StringSchema, _) => textProperty() case (NumberSchema, _) => numericProperty() case (DateSchema, _) => dateProperty() case (DatetimeSchema, _) => dateProperty() - case (ArraySchema, _) => multipleProperty(true) + case (ArraySchema, _) => multipleProperty(definitions, true, id) case (UserSchema, _) => textProperty() case (AnySchema, _) => textProperty() - case (OptionSchema, Some(Select)) => multipleProperty(false) - case (OptionSchema, Some(MultiCheckBoxes)) => multipleProperty(true) - case (OptionSchema, Some(RadioButtons)) => multipleProperty(false) + case (OptionSchema, Some(Select)) => multipleProperty(definitions, false, id) + case (OptionSchema, Some(MultiCheckBoxes)) => multipleProperty(definitions, true, id) + case (OptionSchema, Some(RadioButtons)) => multipleProperty(definitions, false, id) case (OptionSchema, _) => textProperty() - case (OptionWithChildSchema, _) => multipleProperty(true) + case (OptionWithChildSchema, _) => multipleProperty(definitions, true, id) } } diff --git a/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/IssueFieldWrites.scala b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/IssueFieldWrites.scala index 5d1eb803..d4af2f30 100644 --- a/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/IssueFieldWrites.scala +++ b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/IssueFieldWrites.scala @@ -1,9 +1,8 @@ package com.nulabinc.backlog.j2b.issue.writer.convert -import java.text.SimpleDateFormat -import java.util.Locale import javax.inject.Inject +import com.nulabinc.backlog.j2b.jira.utils.DatetimeToDateFormatter import com.nulabinc.backlog.migration.common.convert.Writes import com.nulabinc.backlog.migration.common.domain.BacklogCustomField import com.nulabinc.backlog.migration.common.utils.Logging @@ -13,7 +12,8 @@ import com.nulabinc.jira.client.domain.issue._ class IssueFieldWrites @Inject()(customFieldDefinitions: Seq[Field]) extends Writes[IssueField, Option[BacklogCustomField]] - with Logging { + with Logging + with DatetimeToDateFormatter { override def writes(issueField: IssueField) = { customFieldDefinitions.find(_.id == issueField.id) match { @@ -21,6 +21,9 @@ class IssueFieldWrites @Inject()(customFieldDefinitions: Seq[Field]) field.schema.map { schema => (schema.schemaType, schema.customType) match { case (StatusSchema, Some(Textarea)) => toTextAreaCustomField(field, issueField.value.asInstanceOf[StringFieldValue]) + case (OptionSchema, Some(Select)) => toSingleListCustomField(field, issueField.value.asInstanceOf[OptionFieldValue]) + case (ArraySchema, Some(MultiCheckBoxes)) => toCheckBoxCustomField(field, issueField.value.asInstanceOf[ArrayFieldValue]) + case (OptionSchema, Some(RadioButtons)) => toRadioCustomField(field, issueField.value.asInstanceOf[OptionFieldValue]) case (StringSchema, _) => toTextCustomField(field, issueField.value.asInstanceOf[StringFieldValue]) case (NumberSchema, _) => toNumberCustomField(field, issueField.value.asInstanceOf[NumberFieldValue]) case (DateSchema, _) => toDateCustomField(field, issueField.value.asInstanceOf[StringFieldValue]) @@ -28,9 +31,6 @@ class IssueFieldWrites @Inject()(customFieldDefinitions: Seq[Field]) case (ArraySchema, _) => toMultipleListCustomField(field, issueField.value.asInstanceOf[ArrayFieldValue]) case (UserSchema, _) => toUserCustomField(field, issueField.value.asInstanceOf[UserFieldValue]) case (AnySchema, _) => toTextCustomField(field, issueField.value.asInstanceOf[StringFieldValue]) - case (OptionSchema, Some(Select)) => toSingleListCustomField(field, issueField.value.asInstanceOf[OptionFieldValue]) - case (OptionSchema, Some(MultiCheckBoxes)) => toMultipleListCustomField(field, issueField.value.asInstanceOf[ArrayFieldValue]) - case (OptionSchema, Some(RadioButtons)) => toSingleListCustomField(field, issueField.value.asInstanceOf[OptionFieldValue]) case (OptionSchema, _) => toTextCustomField(field, issueField.value.asInstanceOf[StringFieldValue]) case (OptionWithChildSchema, _) => toMultipleListCustomField(field, issueField.value.asInstanceOf[ArrayFieldValue]) } @@ -71,19 +71,14 @@ class IssueFieldWrites @Inject()(customFieldDefinitions: Seq[Field]) values = Seq.empty[String] ) - private def toDateTimeCustomField(field: Field, issueField: StringFieldValue) = { - val readFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.getDefault()) - val writeFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) - - val dateTime = readFormat.parse(issueField.value) - + private def toDateTimeCustomField(field: Field, issueField: StringFieldValue) = BacklogCustomField( name = field.name, fieldTypeId = FieldType.Date.getIntValue, - optValue = Option(writeFormat.format(dateTime)), + optValue = Option(dateTimeStringToDateString(issueField.value)), values = Seq.empty[String] ) - } + private def toMultipleListCustomField(field: Field, issueField: ArrayFieldValue) = BacklogCustomField( @@ -93,6 +88,14 @@ class IssueFieldWrites @Inject()(customFieldDefinitions: Seq[Field]) values = issueField.values.map(_.value) ) + private def toCheckBoxCustomField(field: Field, issueField: ArrayFieldValue) = + BacklogCustomField( + name = field.name, + fieldTypeId = FieldType.CheckBox.getIntValue, + optValue = None, + values = issueField.values.map(_.value) + ) + private def toSingleListCustomField(field: Field, issueField: OptionFieldValue) = BacklogCustomField( name = field.name, @@ -101,6 +104,14 @@ class IssueFieldWrites @Inject()(customFieldDefinitions: Seq[Field]) values = Seq.empty[String] ) + private def toRadioCustomField(field: Field, issueField: OptionFieldValue) = + BacklogCustomField( + name = field.name, + fieldTypeId = FieldType.Radio.getIntValue, + optValue = Option(issueField.value), + values = Seq.empty[String] + ) + private def toUserCustomField(field: Field, issueField: UserFieldValue) = BacklogCustomField( name = field.name, diff --git a/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/IssueWrites.scala b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/IssueWrites.scala index 643c9d4e..e84db60f 100644 --- a/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/IssueWrites.scala +++ b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/IssueWrites.scala @@ -2,6 +2,7 @@ package com.nulabinc.backlog.j2b.issue.writer.convert import javax.inject.Inject +import com.nulabinc.backlog.j2b.jira.utils.SecondToHourFormatter import com.nulabinc.backlog.migration.common.convert.{Convert, Writes} import com.nulabinc.backlog.migration.common.domain._ import com.nulabinc.backlog.migration.common.utils.DateUtil @@ -10,7 +11,8 @@ import com.nulabinc.jira.client.domain.issue.Issue class IssueWrites @Inject()(implicit val userWrites: UserWrites, implicit val issueFieldWrites: IssueFieldWrites, implicit val attachmentWrites: AttachmentWrites) - extends Writes[Issue, BacklogIssue] { + extends Writes[Issue, BacklogIssue] + with SecondToHourFormatter { override def writes(issue: Issue) = BacklogIssue( @@ -22,8 +24,8 @@ class IssueWrites @Inject()(implicit val userWrites: UserWrites, description = issue.description.getOrElse(""), optStartDate = None, optDueDate = issue.dueDate.map(DateUtil.dateFormat), - optEstimatedHours = issue.timeTrack.flatMap(_.originalEstimateSeconds.map(_ / 3600f)), - optActualHours = issue.timeTrack.flatMap(_.timeSpentSeconds.map(_ / 3600f)), + optEstimatedHours = issue.timeTrack.flatMap(_.originalEstimateSeconds.map(secondsToHours)), + optActualHours = issue.timeTrack.flatMap(_.timeSpentSeconds.map(secondsToHours)), optIssueTypeName = Some(issue.issueType.name), statusName = issue.status.name, categoryNames = issue.components.map(_.name), @@ -32,7 +34,7 @@ class IssueWrites @Inject()(implicit val userWrites: UserWrites, priorityName = issue.priority.name, optAssignee = issue.assignee.map(Convert.toBacklog(_)), attachments = issue.attachments.map(Convert.toBacklog(_)), - sharedFiles = Seq.empty[BacklogSharedFile], // TODO: sharedfiles + sharedFiles = Seq.empty[BacklogSharedFile], customFields = issue.issueFields.flatMap(Convert.toBacklog(_)), notifiedUsers = Seq.empty[BacklogUser], operation = toBacklogOperation(issue) diff --git a/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/ProjectWrites.scala b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/ProjectWrites.scala index 1a132e1e..d088ba41 100644 --- a/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/ProjectWrites.scala +++ b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/ProjectWrites.scala @@ -2,17 +2,17 @@ package com.nulabinc.backlog.j2b.issue.writer.convert import javax.inject.Inject -import com.nulabinc.backlog.j2b.jira.domain.JiraProjectKey import com.nulabinc.backlog.migration.common.convert.Writes -import com.nulabinc.backlog.migration.common.domain.BacklogProject +import com.nulabinc.backlog.migration.common.domain.{BacklogProject, BacklogProjectKey} import com.nulabinc.backlog4j.Project.TextFormattingRule import com.nulabinc.jira.client.domain.Project -private [writer] class ProjectWrites @Inject()(projectKey: JiraProjectKey) +private [writer] class ProjectWrites @Inject()(projectKey: BacklogProjectKey) extends Writes[Project, BacklogProject] { override def writes(project: Project) = - BacklogProject(optId = Some(project.id), + BacklogProject( + optId = Some(project.id), name = project.name, key = projectKey.value, isChartEnabled = true, diff --git a/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/UserWrites.scala b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/UserWrites.scala index 01f27e11..8878e12b 100644 --- a/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/UserWrites.scala +++ b/project-writer/src/main/scala/com/nulabinc/backlog/j2b/issue/writer/convert/UserWrites.scala @@ -10,10 +10,10 @@ class UserWrites extends Writes[User, BacklogUser] { override def writes(user: User) = BacklogUser( optId = None, - optUserId = None, + optUserId = Some(user.key), optPassword = None, name = user.name, - optMailAddress = None, + optMailAddress = Some(user.emailAddress), roleType = BacklogConstantValue.USER_ROLE ) } diff --git a/project-writer/src/test/scala/com/nulabinc/backlog/j2b/issue/writer/FileWriterTestHelper.scala b/project-writer/src/test/scala/com/nulabinc/backlog/j2b/issue/writer/FileWriterTestHelper.scala index f15eec12..e155ce1c 100644 --- a/project-writer/src/test/scala/com/nulabinc/backlog/j2b/issue/writer/FileWriterTestHelper.scala +++ b/project-writer/src/test/scala/com/nulabinc/backlog/j2b/issue/writer/FileWriterTestHelper.scala @@ -1,14 +1,14 @@ package com.nulabinc.backlog.j2b.issue.writer import com.nulabinc.backlog.j2b.issue.writer.convert._ -import com.nulabinc.backlog.j2b.jira.domain.JiraProjectKey import com.nulabinc.backlog.migration.common.conf.BacklogPaths +import com.nulabinc.backlog.migration.common.domain.BacklogProjectKey import scalax.file.Path trait FileWriterTestHelper { - val projectKey = new JiraProjectKey("PLAYCMS") + val projectKey = new BacklogProjectKey("PLAYCMS") implicit val projectWrites = new ProjectWrites(projectKey) implicit val issueCategoriesWrites = new ComponentWrites diff --git a/project-writer/src/test/scala/com/nulabinc/backlog/j2b/issue/writer/VersionFileWriterSpec.scala b/project-writer/src/test/scala/com/nulabinc/backlog/j2b/issue/writer/VersionFileWriterSpec.scala index 1ecec9c0..4e32ac53 100644 --- a/project-writer/src/test/scala/com/nulabinc/backlog/j2b/issue/writer/VersionFileWriterSpec.scala +++ b/project-writer/src/test/scala/com/nulabinc/backlog/j2b/issue/writer/VersionFileWriterSpec.scala @@ -1,5 +1,6 @@ package com.nulabinc.backlog.j2b.issue.writer +import com.nulabinc.backlog.j2b.jira.domain.export.Milestone import com.nulabinc.backlog.migration.common.convert.BacklogUnmarshaller import com.nulabinc.jira.client.domain.Version import org.joda.time.DateTime @@ -16,7 +17,7 @@ class VersionFileWriterSpec extends Specification with FileWriterTestHelper { ) // Output to file - new VersionFileWriter().write(versions) + new VersionFileWriter().write(versions, Seq.empty[Milestone]) val actual = BacklogUnmarshaller.versions(paths) diff --git a/project/plugins.sbt b/project/plugins.sbt index 26aa7a9c..058f083b 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -8,4 +8,6 @@ addSbtPlugin("com.sksamuel.scapegoat" %% "sbt-scapegoat" % "1.0.4") addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "0.5.4") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.0") \ No newline at end of file +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.0") + +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.7.0") diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index f07d7fd8..32165e38 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,9 +1,9 @@ - log/backlog-migration-redmine.log + log/backlog-migration-jira.log - log/%d{yyyy-MM-dd}.backlog-migration-redmine.log.zip + log/%d{yyyy-MM-dd}.backlog-migration-jira.log.zip 10 @@ -18,9 +18,9 @@ - log/backlog-migration-redmine-warn.log + log/backlog-migration-jira-warn.log - log/%d{yyyy-MM-dd}.backlog-migration-redmine-warn.log.zip + log/%d{yyyy-MM-dd}.backlog-migration-jira-warn.log.zip 10 diff --git a/src/main/resources/messages.txt b/src/main/resources/messages.txt index 0b9f5060..c1e64255 100644 --- a/src/main/resources/messages.txt +++ b/src/main/resources/messages.txt @@ -14,16 +14,19 @@ common.project=Project common.done_ratio=% done common.comment=Comment common.wikis=wikis +common.labels=Labels +common.sprint=Sprint +common.actual_hours=Actual Hours common.parent_page=Parent page common.parent_issue=Parent issue -common.custom_field=custom field +common.custom_field=Custom field common.project_user=project users -common.category=category +common.category=Category common.issues=issues common.issues_info=issues information -common.version=version +common.version=Version common.groups=groups -common.issue_type=issue type +common.issue_type=Issue type common.result_success=SUCCESSFUL common.result_failed=FAILED:{0,number,#} common.empty=empty @@ -46,7 +49,7 @@ common.url=URL # ----------------------------------------------------------------------------- cli.confirm=Start migration? (y/n [n]): cli.require_java8=The current version of Java is "{0}". Java 8 is required for this program. -cli.help.projectKey=Your Redmine project identifier.(required) Example:--projectKey [your redmine project identifier1]:[your backllog project key] +cli.help.projectKey=Your JIRA project identifier.(required) Example:--projectKey [your jira project identifier1]:[your backllog project key] cli.confirm_recreate=The mapping file {0} already exists. Do you want to overwrite it? (y/n [n]): cli.backlog_project_already_exist=Project "{0}" already exists. Do you want to import issues and wikis to project "{0}"? (Check the README file for details.) (y/n [n]): cli.error.unknown=Unknown error @@ -81,7 +84,8 @@ cli.param.error.auth.not.auth=You do not have the necessary authority to migrate cli.param.error.auth=Accessing {0} failed due to an authorization error.\n Please make sure the API access key is valid and the REST API service is enabled on the server. cli.param.error.auth.backlog=Backlog API key is not administrator authority. Please use API key of administrator authority. cli.param.error.disable.project=The project [{0}] could not be loaded. -cli.param.error.disable.access=The accessing {0} failed. Please check the url or api access key. +cli.param.error.disable.access.backlog=The accessing {0} failed. Please check the url or api access key. +cli.param.error.disable.access.jira=The accessing {0} failed. Please check the url or username/password. cli.param.error.disable.host=The accessing {0} failed. [{1}] is unknown host. cli.param.error.project_key=The project key [{0}] is incorrect.(Uppercase letters (A-Z), numbers (0-9) and underscore (_) can be used.) cli.param.error.client.unknown=Unknown JIRA rest client error.:[{0}] @@ -161,6 +165,8 @@ import.error.failed.comment=Could not register comment on issue [{0}]. : {1} # ----------------------------------------------------------------------------- # Export # ----------------------------------------------------------------------------- +export.start=Start export. +export.finish=Export completed. Next step \n\n1. Edit mapping files.\n2. Execute the following command to import (replace with your JIRA password).\n\n--------------------------------------\n\n{0}\n\n-------------------------------------- export.attachment.empty=Attachment: {0} -> {1} # ----------------------------------------------------------------------------- diff --git a/src/main/resources/messages_ja.txt b/src/main/resources/messages_ja.txt index cb3a8ddf..d7027560 100644 --- a/src/main/resources/messages_ja.txt +++ b/src/main/resources/messages_ja.txt @@ -14,6 +14,9 @@ common.project=プロジェクト common.done_ratio=進捗率 common.comment=コメント common.wikis=Wiki +common.labels=ラベル +common.sprint=スプリント +common.actual_hours=実績時間 common.parent_page=親ページ common.parent_issue=親課題 common.custom_field=カスタムフィールド @@ -82,7 +85,8 @@ cli.param.error.auth.not.auth=JIRAの移行に必要な権限がありません cli.param.error.auth={0}にアクセスできませんでした。認証エラーが発生しました。\n 有効なAPIアクセスキーになっていることと、REST APIサービスがサーバー上で有効になっていることを確認してください。 cli.param.error.auth.backlog=BacklogのAPIキーが管理者権限ではありません。管理者権限のAPIキーを使用してください。 cli.param.error.disable.project=プロジェクト[{0}]を読み込むことができませんでした。 -cli.param.error.disable.access={0}にアクセスできませんでした。URLまたはAPIアクセスキーを確認してください。 +cli.param.error.disable.access.backlog={0}にアクセスできませんでした。URLまたはAPIアクセスキーを確認してください。 +cli.param.error.disable.access.jira={0}にアクセスできませんでした。URLまたはユーザー名/パスワードを確認してください。 cli.param.error.disable.host={0}にアクセスできませんでした。[{1}]は不明なホストです。 cli.param.error.project_key=プロジェクトキー[{0}]が正しくありません。(半角英大文字と半角数字とアンダースコアが使用できます。) cli.param.error.client.unknown=JIRA RESTクライアントで不明なエラーが発生しました。:[{0}] @@ -162,6 +166,8 @@ import.error.failed.comment=コメントを課題[{0}]に登録できません # ----------------------------------------------------------------------------- # Export # ----------------------------------------------------------------------------- +export.start=エクスポートを開始します。 +export.finish=エクスポートが完了しました。 次のステップは \n\n1. マッピングファイルを編集します。\n2. インポートするために以下のコメントを実行します。 (JIRAのパスワードは修正してください)\n\n--------------------------------------\n\n{0}\n\n-------------------------------------- export.attachment.empty=添付ファイル: {0} -> {1} # ----------------------------------------------------------------------------- diff --git a/src/main/scala/com/nulabinc/backlog/j2b/Main.scala b/src/main/scala/com/nulabinc/backlog/j2b/Main.scala index 439f010a..ab336871 100644 --- a/src/main/scala/com/nulabinc/backlog/j2b/Main.scala +++ b/src/main/scala/com/nulabinc/backlog/j2b/Main.scala @@ -50,6 +50,31 @@ class CommandLineInterface(arguments: Seq[String]) extends ScallopConf(arguments verify() } +class NextCommand(args: Seq[String]) extends BacklogConfiguration { + + import com.nulabinc.backlog.j2b.buildinfo.BuildInfo + + private val formattedArgs = args + .filterNot(_ == "export") + .grouped(2) + .collect { + case Seq(k, v) if k.contains("password") => language match { + case "ja" => s" $k JIRAのパスワード" + case "en" => s" $k JIRA_PASSWORD" + case _ => s" $k JIRA_PASSWORD" + } + case Seq(k, v) => s" $k $v" + }.toSeq + + def command(): String = ( + Seq( + "java -jar", + s" ${BuildInfo.name}-${BuildInfo.version}.jar", + " import" + ) ++ formattedArgs + ).mkString(" \\ \n") +} + object J2B extends BacklogConfiguration with Logging { def main(args: Array[String]): Unit = { @@ -72,7 +97,6 @@ object J2B extends BacklogConfiguration with Logging { // DisableSSLCertificateCheckUtil.disableChecks() // TODO: ??? // checkRelease() // TODO: Github repo does not exist - if ( ! ClassVersion.isValid()) { ConsoleOut.error(Messages("cli.require_java8", System.getProperty("java.specification.version"))) exit(1) @@ -80,17 +104,20 @@ object J2B extends BacklogConfiguration with Logging { // Run try { - val cli = new CommandLineInterface(args) - val config = getConfiguration(cli) + val cli = new CommandLineInterface(args) + val config = getConfiguration(cli) + val nextCommand = new NextCommand(args) + cli.subcommand match { case Some(cli.importCommand) => J2BCli.`import`(config) - case Some(cli.exportCommand) => J2BCli.export(config) + case Some(cli.exportCommand) => J2BCli.export(config, nextCommand) case _ => J2BCli.help() } exit(0) } catch { case e: Throwable => logger.error(e.getMessage, e) + ConsoleOut.error(s"${Messages("cli.error.unknown")}:${e.getMessage}") exit(1) } } @@ -103,7 +130,6 @@ object J2B extends BacklogConfiguration with Logging { ConsoleOut.println( s"""-------------------------------------------------- |${Messages("common.jira")} ${Messages("common.username")}[${cli.importCommand.jiraUsername()}] - |${Messages("common.jira")} ${Messages("common.password")}[${cli.importCommand.jiraPassword()}] |${Messages("common.jira")} ${Messages("common.url")}[${cli.importCommand.jiraUrl()}] |${Messages("common.jira")} ${Messages("common.project_key")}[${jira}] |${Messages("common.backlog")} ${Messages("common.url")}[${cli.importCommand.backlogUrl()}] @@ -114,8 +140,8 @@ object J2B extends BacklogConfiguration with Logging { |""".stripMargin) new AppConfiguration( - jiraConfig = new JiraApiConfiguration(username = cli.importCommand.jiraUsername(), password = cli.importCommand.jiraPassword(), cli.importCommand.jiraUrl(), projectKey = jira), - backlogConfig = new BacklogApiConfiguration(url = cli.importCommand.backlogUrl(), key = cli.importCommand.backlogKey(), projectKey = backlog), + jiraConfig = JiraApiConfiguration(username = cli.importCommand.jiraUsername(), password = cli.importCommand.jiraPassword(), cli.importCommand.jiraUrl(), projectKey = jira), + backlogConfig = BacklogApiConfiguration(url = cli.importCommand.backlogUrl(), key = cli.importCommand.backlogKey(), projectKey = backlog), optOut = cli.importCommand.optOut()) } diff --git a/src/main/scala/com/nulabinc/backlog/j2b/cli/ConfigValidator.scala b/src/main/scala/com/nulabinc/backlog/j2b/cli/ConfigValidator.scala index 7ad5609a..f5ca80a1 100644 --- a/src/main/scala/com/nulabinc/backlog/j2b/cli/ConfigValidator.scala +++ b/src/main/scala/com/nulabinc/backlog/j2b/cli/ConfigValidator.scala @@ -10,11 +10,11 @@ trait ConfigValidator extends Logging { def validateConfig(config: AppConfiguration, jiraRestClient: JiraRestClient, - spaceService: SpaceService): Boolean = { + spaceService: SpaceService): Option[Unit] = { val validator = AppConfigValidator(jiraRestClient, spaceService) val errors = validator.validate(config) - if (errors.isEmpty) true + if (errors.isEmpty) Some(()) else { val message = s""" @@ -25,7 +25,7 @@ trait ConfigValidator extends Logging { | """.stripMargin ConsoleOut.error(message) - false + None } } } diff --git a/src/main/scala/com/nulabinc/backlog/j2b/cli/InteractiveConfirm.scala b/src/main/scala/com/nulabinc/backlog/j2b/cli/InteractiveConfirm.scala index 31e74547..fec1593a 100644 --- a/src/main/scala/com/nulabinc/backlog/j2b/cli/InteractiveConfirm.scala +++ b/src/main/scala/com/nulabinc/backlog/j2b/cli/InteractiveConfirm.scala @@ -2,7 +2,6 @@ package com.nulabinc.backlog.j2b.cli import com.nulabinc.backlog.j2b.conf.AppConfiguration import com.nulabinc.backlog.j2b.jira.domain.mapping.MappingFile -import com.nulabinc.backlog.j2b.mapping.file._ import com.nulabinc.backlog.migration.common.service.ProjectService import com.nulabinc.backlog.migration.common.utils.{ConsoleOut, Logging} import com.osinka.i18n.Messages @@ -25,9 +24,9 @@ trait InteractiveConfirm extends Logging { } def finalConfirm(confirmedProjectKeys: ConfirmedProjectKeys, - statusMappingFile: StatusMappingFile, - priorityMappingFile: PriorityMappingFile, - userMappingFile: UserMappingFile): Either[ConfirmError, Unit] = { + statusMappingFile: MappingFile, + priorityMappingFile: MappingFile, + userMappingFile: MappingFile): Either[ConfirmError, Unit] = { def mappingString(mappingFile: MappingFile): String = { mappingFile.unMarshal() match { case Some(mappings) => diff --git a/src/main/scala/com/nulabinc/backlog/j2b/cli/J2BCli.scala b/src/main/scala/com/nulabinc/backlog/j2b/cli/J2BCli.scala index d68660c3..223ce713 100644 --- a/src/main/scala/com/nulabinc/backlog/j2b/cli/J2BCli.scala +++ b/src/main/scala/com/nulabinc/backlog/j2b/cli/J2BCli.scala @@ -1,19 +1,26 @@ package com.nulabinc.backlog.j2b.cli -import com.google.inject.Guice -import com.nulabinc.backlog.j2b.conf.AppConfiguration +import com.google.inject.{Guice, Injector} +import com.nulabinc.backlog.j2b.NextCommand +import com.nulabinc.backlog.j2b.conf.{AppConfigValidator, AppConfiguration, ConfigValidateFailure} import com.nulabinc.backlog.j2b.exporter.Exporter +import com.nulabinc.backlog.j2b.jira.conf.{JiraApiConfiguration, JiraBacklogPaths} import com.nulabinc.backlog.j2b.jira.converter.MappingConverter +import com.nulabinc.backlog.j2b.jira.domain.mapping.MappingCollectDatabase import com.nulabinc.backlog.j2b.jira.service._ -import com.nulabinc.backlog.j2b.mapping.file._ +import com.nulabinc.backlog.j2b.jira.writer.ProjectUserWriter +import com.nulabinc.backlog.j2b.mapping.converter.writes.MappingUserWrites import com.nulabinc.backlog.j2b.modules._ -import com.nulabinc.backlog.migration.common.conf.{BacklogConfiguration, BacklogPaths} -import com.nulabinc.backlog.migration.common.domain.BacklogUser -import com.nulabinc.backlog.migration.common.modules.ServiceInjector -import com.nulabinc.backlog.migration.common.service.{ProjectService, SpaceService, UserService} -import com.nulabinc.backlog.migration.common.utils.Logging +import com.nulabinc.backlog.migration.common.conf.BacklogConfiguration +import com.nulabinc.backlog.migration.common.convert.Convert +import com.nulabinc.backlog.migration.common.modules.{ServiceInjector => BacklogInjector} +import com.nulabinc.backlog.migration.common.service.{ProjectService, SpaceService, PriorityService => BacklogPriorityService, StatusService => BacklogStatusService, UserService => BacklogUserService} +import com.nulabinc.backlog.migration.common.utils.{ConsoleOut, Logging} import com.nulabinc.backlog.migration.importer.core.Boot import com.nulabinc.jira.client.JiraRestClient +import com.osinka.i18n.Messages + +import scala.util.{Failure, Success, Try} object J2BCli extends BacklogConfiguration with Logging @@ -21,32 +28,49 @@ object J2BCli extends BacklogConfiguration with ConfigValidator with MappingValidator with MappingConsole + with ProgressConsole with InteractiveConfirm with Tracker { - def export(config: AppConfiguration): Unit = { + def export(config: AppConfiguration, nextCommand: NextCommand): Unit = { + + def createJiraExportingInjector(config: AppConfiguration): Option[Injector] = + Try(Guice.createInjector(new ExportModule(config))) match { + case Success(jiraInjector) => Some(jiraInjector) + case Failure(error) => + logger.error(error.getMessage) + None + } - val jiraInjector = Guice.createInjector(new ExportModule(config)) - val backlogInjector = ServiceInjector.createInjector(config.backlogConfig) + val backlogInjector = BacklogInjector.createInjector(config.backlogConfig) - val jiraRestClient = jiraInjector.getInstance(classOf[JiraRestClient]) - val spaceService = backlogInjector.getInstance(classOf[SpaceService]) + for { + _ <- checkJiraApiAccessible(config.jiraConfig) + jiraInjector <- createJiraExportingInjector(config) + _ <- validateConfig(config, jiraInjector.getInstance(classOf[JiraRestClient]), backlogInjector.getInstance(classOf[SpaceService])) + } yield { + startExportMessage() - if (validateConfig(config, jiraRestClient, spaceService)) { + val backlogUserService = backlogInjector.getInstance(classOf[BacklogUserService]) + val backlogPriorityService = backlogInjector.getInstance(classOf[BacklogPriorityService]) + val backlogStatusService = backlogInjector.getInstance(classOf[BacklogStatusService]) // Delete old exports - val backlogPaths = backlogInjector.getInstance(classOf[BacklogPaths]) - backlogPaths.outputPath.deleteRecursively(force = true, continueOnFailure = true) + val jiraBacklogPaths = new JiraBacklogPaths(config.backlogConfig.projectKey) + + jiraBacklogPaths.outputPath.deleteRecursively(force = true, continueOnFailure = true) // Export - val exporter = jiraInjector.getInstance(classOf[Exporter]) - val collectData = exporter.export() + val exporter = jiraInjector.getInstance(classOf[Exporter]) + val collectData = exporter.export(jiraBacklogPaths) + + // Mapping file val mappingFileService = jiraInjector.getInstance(classOf[MappingFileService]) List( - mappingFileService.createUserMappingFile(collectData.users), - mappingFileService.createPriorityMappingFile(collectData.priorities), - mappingFileService.createStatusMappingFile(collectData.statuses) + mappingFileService.createUserMappingFile(collectData.users, backlogUserService.allUsers()), + mappingFileService.createPriorityMappingFile(collectData.priorities, backlogPriorityService.allPriorities()), + mappingFileService.createStatusMappingFile(collectData.statuses, backlogStatusService.allStatuses()) ).foreach { mappingFile => if (mappingFile.isExists) { displayMergedMappingFileMessageToConsole(mappingFile) @@ -55,52 +79,95 @@ object J2BCli extends BacklogConfiguration displayCreateMappingFileMessageToConsole(mappingFile) } } + + finishExportMessage(nextCommand) } + } def `import`(config: AppConfiguration): Unit = { - val jiraInjector = Guice.createInjector(new ImportModule(config)) - val backlogInjector = ServiceInjector.createInjector(config.backlogConfig) - - val jiraRestClient = jiraInjector.getInstance(classOf[JiraRestClient]) - val spaceService = backlogInjector.getInstance(classOf[SpaceService]) + def createJiraImportingInjector(config: AppConfiguration): Option[Injector] = + Try(Guice.createInjector(new ImportModule(config))) match { + case Success(jiraInjector) => Some(jiraInjector) + case Failure(error) => + logger.error(error.getMessage) + None + } - if (validateConfig(config, jiraRestClient, spaceService)) { + val backlogInjector = BacklogInjector.createInjector(config.backlogConfig) + val spaceService = backlogInjector.getInstance(classOf[SpaceService]) - import com.nulabinc.backlog4j.{Status => BacklogStatus, Priority => BacklogPriority} - import com.nulabinc.jira.client.domain.{Status => JiraStatus, Priority => JiraPriority, User => JiraUser} + for { + _ <- checkJiraApiAccessible(config.jiraConfig) + jiraInjector <- createJiraImportingInjector(config) + _ <- validateConfig(config, jiraInjector.getInstance(classOf[JiraRestClient]), spaceService) + } yield { + val backlogUserService = backlogInjector.getInstance(classOf[BacklogUserService]) + val backlogPriorityService = backlogInjector.getInstance(classOf[BacklogPriorityService]) + val backlogStatusService = backlogInjector.getInstance(classOf[BacklogStatusService]) - val statusMappingFile = new StatusMappingFile(Seq.empty[JiraStatus], Seq.empty[BacklogStatus]) - val priorityMappingFile = new PriorityMappingFile(Seq.empty[JiraPriority], Seq.empty[BacklogPriority]) - val userMappingFile = new UserMappingFile(config.backlogConfig, Seq.empty[JiraUser], Seq.empty[BacklogUser]) + // Mapping file + val jiraBacklogPaths = new JiraBacklogPaths(config.backlogConfig.projectKey) + val mappingFileService = jiraInjector.getInstance(classOf[MappingFileService]) + val statusMappingFile = mappingFileService.createStatusesMappingFileFromJson(jiraBacklogPaths.jiraStatusesJson, backlogStatusService.allStatuses()) + val priorityMappingFile = mappingFileService.createPrioritiesMappingFileFromJson(jiraBacklogPaths.jiraPrioritiesJson, backlogPriorityService.allPriorities()) + val userMappingFile = mappingFileService.createUserMappingFileFromJson(jiraBacklogPaths.jiraUsersJson, backlogUserService.allUsers()) for { _ <- mappingFileExists(statusMappingFile).right _ <- mappingFileExists(priorityMappingFile).right _ <- mappingFileExists(userMappingFile).right + _ <- validateMapping(statusMappingFile).right + _ <- validateMapping(priorityMappingFile).right + _ <- validateMapping(userMappingFile).right projectKeys <- confirmProject(config, backlogInjector.getInstance(classOf[ProjectService])).right _ <- finalConfirm(projectKeys, statusMappingFile, priorityMappingFile, userMappingFile).right } yield { + + // Collect database + val database = jiraInjector.getInstance(classOf[MappingCollectDatabase]) + mappingFileService.usersFromJson(jiraBacklogPaths.jiraUsersJson).foreach { user => + database.add(user) + } + // Convert val converter = jiraInjector.getInstance(classOf[MappingConverter]) - converter.convert( + database = database, userMaps = userMappingFile.tryUnMarshal(), priorityMaps = priorityMappingFile.tryUnMarshal(), statusMaps = statusMappingFile.tryUnMarshal() ) + // Project users mapping + implicit val mappingUserWrites: MappingUserWrites = new MappingUserWrites + val projectUserWriter = jiraInjector.getInstance(classOf[ProjectUserWriter]) + val projectUsers = userMappingFile.tryUnMarshal().map(Convert.toBacklog(_)) + projectUserWriter.write(projectUsers) + // Import Boot.execute(config.backlogConfig, false) // Tracking if (!config.isOptOut) { - val userService = backlogInjector.getInstance(classOf[UserService]) + val userService = backlogInjector.getInstance(classOf[BacklogUserService]) tracking(config, spaceService, userService) } } } } + private def checkJiraApiAccessible(config: JiraApiConfiguration): Option[Unit] = { + // Check JIRA configuration is correct. Before creating injector. + val jiraClient = JiraRestClient(config.url, config.username, config.password) + AppConfigValidator.validateConfigJira(jiraClient) match { + case ConfigValidateFailure(failure) => + ConsoleOut.println(Messages("cli.param.error.disable.access.jira", Messages("common.jira"))) + logger.error(failure) + None + case _ => Some(()) + } + } + } diff --git a/src/main/scala/com/nulabinc/backlog/j2b/cli/MappingValidator.scala b/src/main/scala/com/nulabinc/backlog/j2b/cli/MappingValidator.scala index b58d98c6..4f035372 100644 --- a/src/main/scala/com/nulabinc/backlog/j2b/cli/MappingValidator.scala +++ b/src/main/scala/com/nulabinc/backlog/j2b/cli/MappingValidator.scala @@ -6,41 +6,42 @@ import com.osinka.i18n.Messages sealed trait MappingValidateError extends CliError case object MappingFileMissing extends MappingValidateError - +case object MappingFileBroken extends MappingValidateError +case object MappingFileNeedFix extends MappingValidateError trait MappingValidator extends Logging { -// def validateMapping(mappingFile: MappingFile): Boolean = { -// if (!mappingFile.isParsed) { -// val error = -// s""" -// |-------------------------------------------------- -// |${Messages("cli.mapping.error.broken_file", mappingFile.itemName)} -// |-------------------------------------------------- -// """.stripMargin -// ConsoleOut.error(error) -// val message = -// s"""|-------------------------------------------------- -// |${Messages("cli.mapping.fix_file", mappingFile.filePath)}""".stripMargin -// ConsoleOut.println(message) -// false -// } else if (!mappingFile.isValid) { -// val error = -// s""" -// |${Messages("cli.mapping.error", mappingFile.itemName)} -// |-------------------------------------------------- -// |${mappingFile.errors.mkString("\n")} -// |--------------------------------------------------""".stripMargin -// ConsoleOut.error(error) -// val message = -// s""" -// |-------------------------------------------------- -// |${Messages("cli.mapping.fix_file", mappingFile.filePath)} -// """.stripMargin -// ConsoleOut.println(message) -// false -// } else true -// } + def validateMapping(mappingFile: MappingFile): Either[MappingValidateError, Unit] = { + if (!mappingFile.isParsed) { + val error = + s""" + |-------------------------------------------------- + |${Messages("cli.mapping.error.broken_file", mappingFile.itemName)} + |-------------------------------------------------- + """.stripMargin + ConsoleOut.error(error) + val message = + s"""|-------------------------------------------------- + |${Messages("cli.mapping.fix_file", mappingFile.filePath)}""".stripMargin + ConsoleOut.println(message) + Left(MappingFileBroken) + } else if (!mappingFile.isValid) { + val error = + s""" + |${Messages("cli.mapping.error", mappingFile.itemName)} + |-------------------------------------------------- + |${mappingFile.errors.mkString("\n")} + |--------------------------------------------------""".stripMargin + ConsoleOut.error(error) + val message = + s""" + |-------------------------------------------------- + |${Messages("cli.mapping.fix_file", mappingFile.filePath)} + """.stripMargin + ConsoleOut.println(message) + Left(MappingFileNeedFix) + } else Right(()) + } def mappingFileExists(mappingFile: MappingFile): Either[MappingValidateError, Unit] = if (mappingFile.isExists) Right(()) diff --git a/src/main/scala/com/nulabinc/backlog/j2b/cli/ProgressConsole.scala b/src/main/scala/com/nulabinc/backlog/j2b/cli/ProgressConsole.scala new file mode 100644 index 00000000..bf57375f --- /dev/null +++ b/src/main/scala/com/nulabinc/backlog/j2b/cli/ProgressConsole.scala @@ -0,0 +1,19 @@ +package com.nulabinc.backlog.j2b.cli + +import com.nulabinc.backlog.j2b.NextCommand +import com.nulabinc.backlog.migration.common.utils.{ConsoleOut, Logging} +import com.osinka.i18n.Messages + +trait ProgressConsole extends Logging { + + def startExportMessage(): Unit = { + ConsoleOut.println(s""" + |${Messages("export.start")} + |--------------------------------------------------""".stripMargin) + } + + def finishExportMessage(nextCommand: NextCommand): Unit = { + ConsoleOut.println(s"""-------------------------------------------------- + |${Messages("export.finish", nextCommand.command())}""".stripMargin) + } +} diff --git a/src/main/scala/com/nulabinc/backlog/j2b/conf/AppConfigValidator.scala b/src/main/scala/com/nulabinc/backlog/j2b/conf/AppConfigValidator.scala index 8be53122..b86a89b2 100644 --- a/src/main/scala/com/nulabinc/backlog/j2b/conf/AppConfigValidator.scala +++ b/src/main/scala/com/nulabinc/backlog/j2b/conf/AppConfigValidator.scala @@ -10,7 +10,9 @@ import com.osinka.i18n.Messages sealed trait ConfigValidateResult case object ConfigValidateSuccess extends ConfigValidateResult -case class ConfigValidateFailure(reason: String) extends ConfigValidateResult +case class ConfigValidateFailure(reason: String) extends ConfigValidateResult { + override def toString: String = reason +} class AppConfigValidator(jiraRestClient: JiraRestClient, spaceService: SpaceService) extends Logging { @@ -18,7 +20,7 @@ class AppConfigValidator(jiraRestClient: JiraRestClient, def validate(config: AppConfiguration): List[ConfigValidateResult] = { List( AppConfigValidator.validateProjectKey(config.backlogProjectKey), - AppConfigValidator.validateConfigJira(jiraRestClient), +// AppConfigValidator.validateConfigJira(jiraRestClient), AppConfigValidator.validateConfigBacklog(spaceService, config.backlogConfig), AppConfigValidator.validateJiraProject(jiraRestClient, config.jiraConfig), AppConfigValidator.validateAuthBacklog(spaceService) @@ -53,7 +55,7 @@ object AppConfigValidator extends Logging { } case Left(e) => logger.error(e.message, e) - ConfigValidateFailure(s"- ${Messages("cli.param.error.disable.access", Messages("common.jira"))}") + ConfigValidateFailure(s"- ${Messages("cli.param.error.disable.access.jira", Messages("common.jira"))}") } } @@ -69,7 +71,7 @@ object AppConfigValidator extends Logging { ConfigValidateFailure(s"- ${Messages("cli.param.error.disable.host", Messages("common.backlog"), config.url)}") case e: Throwable => logger.error(e.getMessage, e) - ConfigValidateFailure(s"- ${Messages("cli.param.error.disable.access", Messages("common.backlog"))}") + ConfigValidateFailure(s"- ${Messages("cli.param.error.disable.access.backlog", Messages("common.backlog"))}") } } diff --git a/src/main/scala/com/nulabinc/backlog/j2b/modules/DefaultModule.scala b/src/main/scala/com/nulabinc/backlog/j2b/modules/DefaultModule.scala index df863ffb..1a85755a 100644 --- a/src/main/scala/com/nulabinc/backlog/j2b/modules/DefaultModule.scala +++ b/src/main/scala/com/nulabinc/backlog/j2b/modules/DefaultModule.scala @@ -5,9 +5,12 @@ import com.nulabinc.backlog.j2b.conf.AppConfiguration import com.nulabinc.backlog.j2b.issue.writer.convert._ import com.nulabinc.backlog.j2b.jira.conf.JiraApiConfiguration import com.nulabinc.backlog.j2b.jira.domain.JiraProjectKey +import com.nulabinc.backlog.j2b.jira.domain.mapping.MappingCollectDatabase import com.nulabinc.backlog.j2b.jira.service._ +import com.nulabinc.backlog.j2b.mapping.collector.MappingCollectDatabaseInMemory import com.nulabinc.backlog.j2b.mapping.file.MappingFileServiceImpl import com.nulabinc.backlog.migration.common.conf.{BacklogApiConfiguration, BacklogPaths} +import com.nulabinc.backlog.migration.common.domain.BacklogProjectKey import com.nulabinc.jira.client.JiraRestClient import com.nulabinc.jira.client.domain.field.Field @@ -27,7 +30,10 @@ class DefaultModule(config: AppConfiguration) extends AbstractModule { // bind(classOf[Project]).toInstance(project) bind(classOf[JiraApiConfiguration]).toInstance(config.jiraConfig) bind(classOf[JiraProjectKey]).toInstance(JiraProjectKey(config.jiraConfig.projectKey)) + bind(classOf[BacklogProjectKey]).toInstance(BacklogProjectKey(config.backlogConfig.projectKey)) bind(classOf[BacklogApiConfiguration]).toInstance(config.backlogConfig) + + // Paths bind(classOf[BacklogPaths]).toInstance(new BacklogPaths(config.backlogProjectKey)) // Mapping-file @@ -44,5 +50,8 @@ class DefaultModule(config: AppConfiguration) extends AbstractModule { bind(classOf[IssueFieldWrites]).toInstance(new IssueFieldWrites(fields)) bind(classOf[ChangelogItemWrites]).toInstance(new ChangelogItemWrites(fields)) bind(classOf[AttachmentWrites]).toInstance(new AttachmentWrites) + + // Collector + bind(classOf[MappingCollectDatabase]).to(classOf[MappingCollectDatabaseInMemory]) } } diff --git a/src/main/scala/com/nulabinc/backlog/j2b/modules/ExportModule.scala b/src/main/scala/com/nulabinc/backlog/j2b/modules/ExportModule.scala index 47852365..ded2f61a 100644 --- a/src/main/scala/com/nulabinc/backlog/j2b/modules/ExportModule.scala +++ b/src/main/scala/com/nulabinc/backlog/j2b/modules/ExportModule.scala @@ -4,8 +4,10 @@ import com.nulabinc.backlog.j2b.conf.AppConfiguration import com.nulabinc.backlog.j2b.exporter.{CommentFileWriter, IssueFileWriter} import com.nulabinc.backlog.j2b.exporter.service._ import com.nulabinc.backlog.j2b.issue.writer._ +import com.nulabinc.backlog.j2b.jira.domain.mapping.MappingCollectDatabase import com.nulabinc.backlog.j2b.jira.service._ import com.nulabinc.backlog.j2b.jira.writer._ +import com.nulabinc.backlog.j2b.mapping.collector.MappingCollectDatabaseInMemory class ExportModule(config: AppConfiguration) extends DefaultModule(config) { diff --git a/src/main/scala/com/nulabinc/backlog/j2b/modules/ImportModule.scala b/src/main/scala/com/nulabinc/backlog/j2b/modules/ImportModule.scala index 51aa6cb8..25fd33a3 100644 --- a/src/main/scala/com/nulabinc/backlog/j2b/modules/ImportModule.scala +++ b/src/main/scala/com/nulabinc/backlog/j2b/modules/ImportModule.scala @@ -1,8 +1,10 @@ package com.nulabinc.backlog.j2b.modules import com.nulabinc.backlog.j2b.conf.AppConfiguration +import com.nulabinc.backlog.j2b.issue.writer.ProjectUserFileWriter import com.nulabinc.backlog.j2b.jira.converter._ import com.nulabinc.backlog.j2b.jira.service.MappingFileService +import com.nulabinc.backlog.j2b.jira.writer.ProjectUserWriter import com.nulabinc.backlog.j2b.mapping.converter._ import com.nulabinc.backlog.j2b.mapping.converter.writes.UserWrites import com.nulabinc.backlog.j2b.mapping.file.MappingFileServiceImpl @@ -22,5 +24,10 @@ class ImportModule(config: AppConfiguration) extends DefaultModule(config) { // Mapping-converter bind(classOf[MappingConverter]).to(classOf[MappingConvertService]) + + // Writer + bind(classOf[ProjectUserWriter]).to(classOf[ProjectUserFileWriter]) + + } } diff --git a/src/test/scala/com/nulabinc/backlog/j2b/CompareSpec.scala b/src/test/scala/com/nulabinc/backlog/j2b/CompareSpec.scala new file mode 100644 index 00000000..4bada080 --- /dev/null +++ b/src/test/scala/com/nulabinc/backlog/j2b/CompareSpec.scala @@ -0,0 +1,326 @@ +package com.nulabinc.backlog.j2b + +import com.nulabinc.backlog.j2b.helper._ +import com.nulabinc.backlog.j2b.jira.conf.JiraApiConfiguration +import com.nulabinc.backlog.j2b.matchers.{DateMatcher, UserMatcher} +import com.nulabinc.backlog.migration.common.conf.{BacklogApiConfiguration, BacklogConstantValue} +import com.nulabinc.backlog.migration.common.convert.Convert +import com.nulabinc.backlog.migration.common.convert.writes.UserWrites +import com.nulabinc.backlog4j.{CustomFieldSetting, IssueComment, ResponseList} +import com.nulabinc.backlog4j.api.option.{GetIssuesParams, QueryParams} +import com.nulabinc.backlog4j.internal.json.customFields._ +import com.nulabinc.jira.client.domain.field._ +import com.nulabinc.jira.client.domain.issue._ +import org.scalatest.{DiagrammedAssertions, FlatSpec, Matchers} + +import scala.collection.JavaConverters._ +import scala.collection.mutable + +class CompareSpec extends FlatSpec + with Matchers + with DiagrammedAssertions + with TestHelper + with DateFormatter + with UserMatcher + with DateMatcher { + + val jiraCustomFieldDefinitions: Seq[Field] = jiraRestApi.fieldAPI.all().right.get + val backlogCustomFieldDefinitions: mutable.Seq[CustomFieldSetting] = backlogApi.getCustomFields(appConfig.backlogConfig.projectKey).asScala + + // -------------------------------------------------------------------------- + testProject(appConfig.jiraConfig, appConfig.backlogConfig) + testProjectUsers(appConfig.backlogConfig) + testVersion(appConfig.jiraConfig, appConfig.backlogConfig) + testIssueType(appConfig.backlogConfig) + testCategory(appConfig.jiraConfig, appConfig.backlogConfig) + testCustomFieldDefinitions(appConfig.backlogConfig) + testIssue(appConfig.jiraConfig, appConfig.backlogConfig) + + def testProject(jiraConfig: JiraApiConfiguration, backlogConfig: BacklogApiConfiguration): Unit = { + "Project" should "match" in { + val jiraProject = jiraRestApi.projectAPI.project(jiraConfig.projectKey) + val backlogProject = backlogApi.getProject(backlogConfig.projectKey) + + backlogProject.getName should equal(jiraProject.right.get.name) + backlogProject.isChartEnabled should be(true) + backlogProject.isSubtaskingEnabled should be(true) + backlogProject.getTextFormattingRule should equal(com.nulabinc.backlog4j.Project.TextFormattingRule.Markdown) + } + } + + def testProjectUsers(backlogConfig: BacklogApiConfiguration): Unit = { + "Project user" should "match" in { + implicit val backlogUserWrites: UserWrites = new UserWrites() + + val backlogUsers = backlogApi.getProjectUsers(backlogConfig.projectKey).asScala + val userMappingFile = mappingFileService.createUserMappingFileFromJson(jiraBacklogPaths.jiraUsersJson, backlogUsers.map(Convert.toBacklog(_))) + val jiraUsers = userMappingFile.tryUnMarshal() + + jiraUsers.foreach { jiraUser => + backlogUsers.exists { backlogUser => + backlogUser.getUserId == jiraUser.dst + } should be(true) + } + } + } + + def testVersion(jiraConfig: JiraApiConfiguration, backlogConfig: BacklogApiConfiguration): Unit = + "Version" should "match" in { + val backlogVersions = backlogApi.getVersions(backlogConfig.projectKey).asScala + val jiraVersions = jiraRestApi.versionsAPI.projectVersions(jiraConfig.projectKey).right.get + jiraVersions.foreach { jiraVersion => + val optBacklogVersion = backlogVersions.find(backlogVersion => jiraVersion.name == backlogVersion.getName) + optBacklogVersion.isDefined should be(true) + for { + backlogVersion <- optBacklogVersion + } yield { + assert(jiraVersion.name == backlogVersion.getName) + assert(jiraVersion.description.get == backlogVersion.getDescription) + } + } + } + + def testIssueType(backlogConfig: BacklogApiConfiguration): Unit = + "Issue type" should "match" in { + val backlogIssueTypes = backlogApi.getIssueTypes(backlogConfig.projectKey).asScala + val jiraIssueTypes = jiraRestApi.issueTypeAPI.allIssueTypes().right.get + jiraIssueTypes.foreach { jiraIssueType => + val backlogIssueType = backlogIssueTypes.find(backlogIssueType => jiraIssueType.name == backlogIssueType.getName).get + jiraIssueType.name should equal(backlogIssueType.getName) + } + } + + def testCategory(jiraConfig: JiraApiConfiguration, backlogConfig: BacklogApiConfiguration): Unit = + "Category" should "match" in { + val backlogComponents = backlogApi.getCategories(backlogConfig.projectKey).asScala + val jiraComponents = jiraRestApi.componentAPI.projectComponents(jiraConfig.projectKey).right.get + jiraComponents.foreach { jiraComponent => + val backlogComponent = backlogComponents.find(backlogComponent => jiraComponent.name == backlogComponent.getName).get + jiraComponent.name should equal(backlogComponent.getName) + } + } + + def testCustomFieldDefinitions(backlogConfig: BacklogApiConfiguration): Unit = + "Custom field definition" should "match" in { + val backlogCustomFields = backlogApi.getCustomFields(backlogConfig.projectKey).asScala + jiraCustomFieldDefinitions.filter(_.id.contains("customfield_")).foreach { jiraCustomField => + val backlogCustomField = backlogCustomFields.find(_.getName == jiraCustomField.name).get + jiraCustomField.name should equal(backlogCustomField.getName) + } + } + + def testIssue(jiraConfig: JiraApiConfiguration, backlogConfig: BacklogApiConfiguration): Unit = { + + val backlogProject = backlogApi.getProject(backlogConfig.projectKey) + val params = new GetIssuesParams(List(Long.box(backlogProject.getId)).asJava) + val backlogIssues = backlogApi.getIssues(params).asScala + + def fetchIssues(startAt: Long, maxResults: Long): Unit = { + val issues = jiraIssueService.issues(startAt, maxResults) + val sprintCustomField = jiraCustomFieldDefinitions.find(_.name == "Sprint").get + + issues.foreach { jiraIssue => + "Issue" should s"match: ${jiraIssue.id} - ${jiraIssue.summary}" in { + + val maybeBacklogIssue = backlogIssues.find(backlogIssue => jiraIssue.summary == backlogIssue.getSummary) + + withClue(s""" + |jira subject:${jiraIssue.summary} + """.stripMargin) { + maybeBacklogIssue should not be None + } + + maybeBacklogIssue.map { backlogIssue => + + // description + jiraIssue.description.getOrElse("") should equal(backlogIssue.getDescription) + + // issue type + jiraIssue.issueType.name should equal(backlogIssue.getIssueType.getName) + + // category + jiraIssue.components.map { jiraCategory => + backlogIssue.getCategory.asScala.find(_.getName == jiraCategory.name) should not be empty + } + + // version + jiraIssue.fixVersions.map { jiraVersion => + backlogIssue.getVersions.asScala.find(_.getName == jiraVersion.name) should not be empty + } + + // milestone + jiraIssue.issueFields.filter(_.id == sprintCustomField.id).map { sprint => + val backlogMilestones = backlogIssue.getMilestone.asScala + sprint.value.asInstanceOf[ArrayFieldValue].values.map { jiraMilestone => + backlogMilestones.find(m => jiraMilestone.value.contains(m.getName)) should not be empty + } + } + + // parent issue + if (jiraIssue.parent.isDefined) backlogIssue.getParentIssueId should not be 0 + else backlogIssue.getParentIssueId should equal(0) + + // due date + dateToOptionDateString(jiraIssue.dueDate) should equal(dateToOptionDateString(Option(backlogIssue.getDueDate))) + + // priority + convertPriority(jiraIssue.priority.name) should equal(backlogIssue.getPriority.getName) + + // status + withClue(s""" + |status: ${jiraIssue.status.name} + |converted:${convertStatus(jiraIssue.status.name)} + |""".stripMargin) { + convertStatus(jiraIssue.status.name) should equal(backlogIssue.getStatus.getName) + } + + // assignee + jiraIssue.assignee.map(assertUser(_, backlogIssue.getAssignee)) + + // actual hours + val spentHours = jiraIssue.timeTrack.flatMap(t => t.timeSpentSeconds).map(s => BigDecimal(s / 3600d).setScale(2, BigDecimal.RoundingMode.HALF_UP)) + val actualHours = Option(backlogIssue.getActualHours).map(s => BigDecimal(s).setScale(2, BigDecimal.RoundingMode.HALF_UP)) + spentHours should equal(actualHours) + + // estimated hours + val jiraHours = jiraIssue.timeTrack.flatMap(t => t.originalEstimateSeconds).map(s => BigDecimal(s / 3600d).setScale(2, BigDecimal.RoundingMode.HALF_UP)) + val backlogHours = Option(backlogIssue.getEstimatedHours).map(s => BigDecimal(s).setScale(2, BigDecimal.RoundingMode.HALF_UP)) + jiraHours should equal(backlogHours) + + // created user + convertUser(jiraIssue.creator.key) should equal(backlogIssue.getCreatedUser.getUserId) + + // created + timestampToString(jiraIssue.createdAt.toDate) should equal(timestampToString(backlogIssue.getCreated)) + + // updated user + withClue(s""" + |JIRA: ${timestampToString(jiraIssue.updatedAt.toDate)} + |backlog:${timestampToString(backlogUpdated(backlogIssue))} + """.stripMargin) { + timestampToString(jiraIssue.updatedAt.toDate) should be(timestampToString(backlogUpdated(backlogIssue))) + } + + // attachment file + val backlogAttachments = backlogIssue.getAttachments.asScala + jiraIssue.attachments.map { jiraAttachment => + val backlogAttachment = backlogAttachments.find(_.getName == jiraAttachment.fileName) + backlogAttachment should not be empty + } + + // custom field + val backlogCustomFields = backlogIssue.getCustomFields.asScala + val rankCustomField = jiraCustomFieldDefinitions.find(_.name == "Rank").get + jiraIssue.issueFields + .filterNot(_.id == sprintCustomField.id) + .filterNot(_.id == rankCustomField.id) + .map { jiraCustomField => + val jiraDefinition = jiraCustomFieldDefinitions.find(_.id == jiraCustomField.id).get + val backlogDefinition = backlogCustomFieldDefinitions.find(_.getName == jiraDefinition.name).get + val backlogCustomField = backlogCustomFields.find(_.getName == jiraDefinition.name).get + + backlogDefinition.getFieldTypeId match { + case BacklogConstantValue.CustomField.MultipleList => + val backlogItems = backlogCustomField.asInstanceOf[MultipleListCustomField].getValue.asScala + jiraCustomField.value.asInstanceOf[ArrayFieldValue].values.map { jiraValue => + backlogItems.find(_.getName == jiraValue.value) should not be empty + } + case BacklogConstantValue.CustomField.Text => + val backlogValue = backlogCustomField.asInstanceOf[TextCustomField] + jiraCustomField.value match { + case UserFieldValue(v) => v.key should equal(backlogValue.getValue) + case StringFieldValue(v) => v should equal(backlogValue.getValue) + } + case BacklogConstantValue.CustomField.TextArea => + val backlogValue = backlogCustomField.asInstanceOf[TextAreaCustomField] + jiraCustomField.value.asInstanceOf[StringFieldValue] should equal(backlogValue.getValue) + case BacklogConstantValue.CustomField.Numeric => + val backlogValue = backlogCustomField.asInstanceOf[NumericCustomField].getValue + jiraCustomField.value.asInstanceOf[NumberFieldValue].v - backlogValue should equal(0) + case BacklogConstantValue.CustomField.Date => + val backlogValue = backlogCustomField.asInstanceOf[DateCustomField].getValue + val jiraValue = jiraCustomField.value.asInstanceOf[StringFieldValue] + jiraDefinition.schema.get.schemaType match { + case DatetimeSchema => assertDate(jiraValue.value, backlogValue) + case DateSchema => assertDateTime(jiraValue.value, backlogValue) + case _ => fail("Custom field type does not match date or datetime") + } + case BacklogConstantValue.CustomField.SingleList => + val backlogValue = backlogCustomField.asInstanceOf[SingleListCustomField].getValue + val jiraValue = jiraCustomField.value.asInstanceOf[OptionFieldValue] + jiraValue.value should equal(backlogValue.getName) + case BacklogConstantValue.CustomField.CheckBox => + val backlogValues = backlogCustomField.asInstanceOf[CheckBoxCustomField].getValue.asScala + jiraCustomField.value.asInstanceOf[ArrayFieldValue].values.map { jiraValue => + backlogValues.find(_.getName == jiraValue.value) should not be empty + } + case BacklogConstantValue.CustomField.Radio => + val backlogValue = backlogCustomField.asInstanceOf[RadioCustomField] + val jiraValue = jiraCustomField.value.asInstanceOf[OptionFieldValue] + jiraValue.value should equal(backlogValue.getValue.getName) + } + } + + // ---------------------------------------------------------------- + // comments + // ---------------------------------------------------------------- + val backlogAllComments = allCommentsOfIssue(backlogIssue.getId) + + // comment + jiraCommentService.issueComments(jiraIssue).filterNot { jiraComment => + attachmentCommentPattern.findFirstIn(jiraComment.body).isDefined + }.map { jiraComment => + val backlogComment = backlogAllComments.find(_.getContent == jiraComment.body) + backlogComment should not be empty + assertUser(jiraComment.author, backlogComment.get.getCreatedUser) + timestampToString(jiraComment.createdAt.toDate) should be(timestampToString(backlogComment.get.getCreated)) + } + + // ----- Change log ----- + // Test + // - creator is same + // - created at is same + jiraIssueService.changeLogs(jiraIssue).map { jiraChangeLog => + val backlogChangelog = backlogAllComments.find { backlogComment => + timestampToString(backlogComment.getCreated) == timestampToString(jiraChangeLog.createdAt.toDate) + } + backlogChangelog should not be empty + assertUser(jiraChangeLog.author, backlogChangelog.get.getCreatedUser) + } + + } + + } + } + + if (issues.nonEmpty) { + fetchIssues(startAt + maxResults, maxResults) + } + } + + fetchIssues(0, 10) + } + + private def allCommentsOfIssue(issueId: Long): Seq[IssueComment] = { + val allCount = backlogApi.getIssueCommentCount(issueId) + + def loop(optMinId: Option[Long], comments: Seq[IssueComment], offset: Long): Seq[IssueComment] = + if (offset < allCount) { + val queryParams = new QueryParams() + for { minId <- optMinId } yield { + queryParams.minId(minId) + } + queryParams.count(100) + queryParams.order(QueryParams.Order.Asc) + val commentsPart = + backlogApi.getIssueComments(issueId, queryParams).asScala + val optLastId = for { lastComment <- commentsPart.lastOption } yield { + lastComment.getId + } + loop(optLastId, comments union commentsPart, offset + 100) + } else comments + + loop(None, Seq.empty[IssueComment], 0).sortWith((c1, c2) => c1.getCreated.before(c2.getCreated)) + } + +} diff --git a/src/test/scala/com/nulabinc/backlog/j2b/helper/DateFormatter.scala b/src/test/scala/com/nulabinc/backlog/j2b/helper/DateFormatter.scala new file mode 100644 index 00000000..0587e29f --- /dev/null +++ b/src/test/scala/com/nulabinc/backlog/j2b/helper/DateFormatter.scala @@ -0,0 +1,21 @@ +package com.nulabinc.backlog.j2b.helper + +import java.util.Date + +import org.joda.time.DateTime + +trait DateFormatter { + + val dateFormat: String = "yyyy-MM-dd" + val timestampFormat: String = "yyyy-MM-dd'T'HH:mm:ssZ" + + def dateToOptionDateString(dateTime: Option[Date]): Option[String] = + dateTime.map(d => new DateTime(d).toString(dateFormat)) + + def dateToDateString(date: Date): String = + new DateTime(date).toString(dateFormat) + + def timestampToString(date: Date): String = + new DateTime(date).toString(timestampFormat) + +} diff --git a/src/test/scala/com/nulabinc/backlog/j2b/helper/TestHelper.scala b/src/test/scala/com/nulabinc/backlog/j2b/helper/TestHelper.scala new file mode 100644 index 00000000..1093f30b --- /dev/null +++ b/src/test/scala/com/nulabinc/backlog/j2b/helper/TestHelper.scala @@ -0,0 +1,140 @@ +package com.nulabinc.backlog.j2b.helper + +import java.io.{File, FileInputStream} +import java.util.{Date, Properties} + +import com.nulabinc.backlog.j2b.conf.AppConfiguration +import com.nulabinc.backlog.j2b.exporter.service.{JiraClientCommentService, JiraClientIssueService} +import com.nulabinc.backlog.j2b.jira.conf.{JiraApiConfiguration, JiraBacklogPaths} +import com.nulabinc.backlog.j2b.jira.domain.JiraProjectKey +import com.nulabinc.backlog.j2b.mapping.collector.MappingCollectDatabaseInMemory +import com.nulabinc.backlog.j2b.mapping.converter.writes.UserWrites +import com.nulabinc.backlog.j2b.mapping.converter.{MappingPriorityConverter, MappingStatusConverter, MappingUserConverter} +import com.nulabinc.backlog.j2b.mapping.file.MappingFileServiceImpl +import com.nulabinc.backlog.migration.common.conf.BacklogApiConfiguration +import com.nulabinc.backlog4j.{IssueComment, Issue => BacklogIssue} +import com.nulabinc.backlog.migration.common.modules.{ServiceInjector => BacklogInjector} +import com.nulabinc.backlog.migration.common.service.{ProjectService, SpaceService, PriorityService => BacklogPriorityService, StatusService => BacklogStatusService, UserService => BacklogUserService} +import com.nulabinc.backlog4j.{BacklogClient, BacklogClientFactory} +import com.nulabinc.backlog4j.conf.{BacklogConfigure, BacklogPackageConfigure} +import com.nulabinc.jira.client.JiraRestClient +import org.joda.time.DateTime + +import scala.collection.JavaConverters._ +import scala.util.matching.Regex + +trait TestHelper { + + + val appConfig: AppConfiguration = getAppConfiguration + val jiraRestApi: JiraRestClient = createJiraRestApi(appConfig.jiraConfig) + val backlogApi: BacklogClient = createBacklogApi(appConfig.backlogConfig) + + // Backlog services + val backlogInjector = BacklogInjector.createInjector(appConfig.backlogConfig) + val backlogUserService = backlogInjector.getInstance(classOf[BacklogUserService]) + val backlogPriorityService = backlogInjector.getInstance(classOf[BacklogPriorityService]) + val backlogStatusService = backlogInjector.getInstance(classOf[BacklogStatusService]) + + // Mapping file + val jiraBacklogPaths = new JiraBacklogPaths(appConfig.backlogConfig.projectKey) + val mappingFileService = new MappingFileServiceImpl(appConfig.jiraConfig, appConfig.backlogConfig) + val statusMappingFile = mappingFileService.createStatusesMappingFileFromJson(jiraBacklogPaths.jiraStatusesJson, backlogStatusService.allStatuses()) + val priorityMappingFile = mappingFileService.createPrioritiesMappingFileFromJson(jiraBacklogPaths.jiraPrioritiesJson, backlogPriorityService.allPriorities()) + val userMappingFile = mappingFileService.createUserMappingFileFromJson(jiraBacklogPaths.jiraUsersJson, backlogUserService.allUsers()) + + // Mappings + val priorityMappings = priorityMappingFile.tryUnMarshal() + val statusMappings = statusMappingFile.tryUnMarshal() + val userMappings = userMappingFile.tryUnMarshal() + + // Mapping converter + implicit val userWrites = new UserWrites + val priorityMappingConverter = new MappingPriorityConverter + val statusMappingConverter = new MappingStatusConverter + val userMappingConverter = new MappingUserConverter() + + // Mapping database + val database = new MappingCollectDatabaseInMemory + mappingFileService.usersFromJson(jiraBacklogPaths.jiraUsersJson).foreach { user => + database.add(user) + } + + // JIRA client service + val jiraCommentService = new JiraClientCommentService(jiraRestApi) + val jiraIssueService = new JiraClientIssueService(appConfig.jiraConfig, JiraProjectKey(appConfig.jiraKey), jiraRestApi, jiraBacklogPaths) + + // Regex + val attachmentCommentPattern: Regex = """\[\^.+?\]""".r + + def createJiraRestApi(config: JiraApiConfiguration) = new JiraRestClient( + url = config.url, + username = config.username, + password = config.password + ) + + def createBacklogApi(config: BacklogApiConfiguration): BacklogClient = { + val backlogPackageConfigure: BacklogPackageConfigure = new BacklogPackageConfigure(config.url) + val configure: BacklogConfigure = backlogPackageConfigure.apiKey(config.key) + new BacklogClientFactory(configure).newClient() + } + + def convertUser(target: String): String = { + userMappingConverter.convert(database, userMappings, target) + } + + def convertStatus(target: String): String = { + statusMappingConverter.convert(statusMappings, target) + } + + def convertPriority(target: String): String = { + priorityMappingConverter.convert(priorityMappings, target) + } + + def backlogUpdated(issue: BacklogIssue): Date = { + val comments = backlogApi.getIssueComments(issue.getId) + if (comments.isEmpty) issue.getUpdated + else { + val comment = comments.asScala.sortWith((c1, c2) => { + val dt1 = new DateTime(c1.getUpdated) + val dt2 = new DateTime(c2.getUpdated) + dt1.isBefore(dt2) + })(comments.size() - 1) + comment.getCreated + } + } + + private def getAppConfiguration: AppConfiguration = { + val file = new File("test.properties") + if (!file.exists()) + throw new RuntimeException("test.properties not found.") + + val prop: Properties = new Properties() + prop.load(new FileInputStream(file)) + val jiraUsername: String = prop.getProperty("jira.username") + val jiraPassword: String = prop.getProperty("jira.password") + val jiraUrl: String = prop.getProperty("jira.url") + val backlogKey: String = prop.getProperty("backlog.key") + val backlogUrl: String = prop.getProperty("backlog.url") + val projectKey: String = prop.getProperty("projectKey") + + val keys: Array[String] = projectKey.split(":") + val jira: String = keys(0) + val backlog: String = if (keys.length == 2) keys(1) else keys(0).toUpperCase.replaceAll("-", "_") + + new AppConfiguration( + jiraConfig = JiraApiConfiguration( + username = jiraUsername, + password = jiraPassword, + url = jiraUrl, + projectKey = jira + ), + backlogConfig = BacklogApiConfiguration( + url = backlogUrl, + key = backlogKey, + projectKey = backlog + ), + optOut = true + ) + } +} diff --git a/src/test/scala/com/nulabinc/backlog/j2b/matchers/DateMatcher.scala b/src/test/scala/com/nulabinc/backlog/j2b/matchers/DateMatcher.scala new file mode 100644 index 00000000..1e63daad --- /dev/null +++ b/src/test/scala/com/nulabinc/backlog/j2b/matchers/DateMatcher.scala @@ -0,0 +1,22 @@ +package com.nulabinc.backlog.j2b.matchers + +import java.util.Date + +import com.nulabinc.backlog.j2b.helper.{DateFormatter, TestHelper} +import com.nulabinc.backlog.j2b.jira.utils.DatetimeToDateFormatter +import org.scalatest.{Assertion, Matchers} + +trait DateMatcher extends TestHelper + with Matchers + with DateFormatter + with DatetimeToDateFormatter { + + def assertDate(jiraDateString: String, backlogDate: Date): Assertion = { + val jiraDate = dateTimeStringToDateString(jiraDateString) + jiraDate should equal(dateToDateString(backlogDate)) + } + + def assertDateTime(jiraDateRimeString: String, backlogDate: Date): Assertion = + jiraDateRimeString should equal(dateToDateString(backlogDate)) + +} diff --git a/src/test/scala/com/nulabinc/backlog/j2b/matchers/UserMatcher.scala b/src/test/scala/com/nulabinc/backlog/j2b/matchers/UserMatcher.scala new file mode 100644 index 00000000..7e49eb8c --- /dev/null +++ b/src/test/scala/com/nulabinc/backlog/j2b/matchers/UserMatcher.scala @@ -0,0 +1,15 @@ +package com.nulabinc.backlog.j2b.matchers + +import com.nulabinc.backlog.j2b.helper.TestHelper +import com.nulabinc.backlog4j.{User => BacklogUser} +import com.nulabinc.jira.client.domain.{User => JiraUser} +import org.scalatest.{Assertion, Matchers} + +trait UserMatcher extends TestHelper with Matchers { + + def assertUser(jiraUser: JiraUser, backlogUser: BacklogUser): Assertion = + convertUser(jiraUser.key) should equal(backlogUser.getUserId) + + def assertUser(jiraUserKey: String, backlogUserId: String): Assertion = + convertUser(jiraUserKey) should equal(backlogUserId) +} diff --git a/src/test/scala/com/nulabinc/backlog/j2b/package.scala b/src/test/scala/com/nulabinc/backlog/j2b/package.scala new file mode 100644 index 00000000..f0cdf562 --- /dev/null +++ b/src/test/scala/com/nulabinc/backlog/j2b/package.scala @@ -0,0 +1,5 @@ +package com.nulabinc.backlog + +package object j2b { + +}