From 2b1b3479b72fedb85c219be54dc6d86478bc3a62 Mon Sep 17 00:00:00 2001
From: Manuel VACELET <manuel.vacelet@enalean.com>
Date: Wed, 28 Jul 2021 10:13:25 +0200
Subject: [PATCH] request #22580 Get list of jira projects doesnt work with
 Jira Server

The while both APIs seems to be available on Jira Cloud, the design
selected was to clearly separate the 2 ways to get the information

The main concern is that Jira Server API is considered as deprecated:
https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-projects/#api-rest-api-2-project-get

Hence, while this should not be an issue with import coming from Jira
Server itself it might no longer be supported in the future for people
wanted to ditch Jira Cloud.

In order to test with a Jira Cloud instance you must force your
platform to use Jira Server APIs with:

    tuleap config-set tracker_force_jira_server 1

Change-Id: I595bf5b238b2cbc8b1239475c43b7b0b1f372c4c
---
 .../Creation/JiraImporter/ClientWrapper.php   | 27 ++++++++++++
 .../Creation/JiraImporter/JiraClient.php      |  2 +
 .../JiraImporter/JiraConnectionException.php  | 32 +++++++-------
 .../JiraImporter/JiraProjectBuilder.php       | 42 ++++++++++++++++++-
 .../JiraImporter/JiraProjectCollection.php    |  3 +-
 .../JiraProjectListController.php             |  4 +-
 plugins/tracker/include/trackerPlugin.php     |  2 +-
 7 files changed, 92 insertions(+), 20 deletions(-)

diff --git a/plugins/tracker/include/Tracker/Creation/JiraImporter/ClientWrapper.php b/plugins/tracker/include/Tracker/Creation/JiraImporter/ClientWrapper.php
index 1844ea16f1..067092843c 100644
--- a/plugins/tracker/include/Tracker/Creation/JiraImporter/ClientWrapper.php
+++ b/plugins/tracker/include/Tracker/Creation/JiraImporter/ClientWrapper.php
@@ -33,6 +33,15 @@ use Tuleap\Http\HTTPFactoryBuilder;
 
 class ClientWrapper implements JiraClient
 {
+    /**
+     * Force Tuleap to use Jira Server APIs even when remote instance is Jira Cloud
+     *
+     * This is mainly useful in development to test Jira Server REST calls against Jira Cloud
+     *
+     * @tlp-config-key
+     */
+    public const CONFIG_KEY_FORCE_JIRA_SERVER = 'tracker_force_jira_server';
+
     /**
      * According to [1] the v3 API is only available on Jira Cloud with no forseen implementation on Jira Server (on prem)
      * So we stick to v2 because this code should run against on prem instances.
@@ -41,6 +50,8 @@ class ClientWrapper implements JiraClient
      */
     public const JIRA_CORE_BASE_URL = '/rest/api/2';
 
+    private const DEPLOYMENT_TYPE_CLOUD = 'Cloud';
+
     /**
      * @var ClientInterface
      */
@@ -61,6 +72,10 @@ class ClientWrapper implements JiraClient
      * @var ?string
      */
     private $log_file;
+    /**
+     * @var ?bool
+     */
+    private $is_jira_cloud;
 
     public function __construct(ClientInterface $client, RequestFactoryInterface $factory, string $base_url)
     {
@@ -126,4 +141,16 @@ class ClientWrapper implements JiraClient
 
         return json_decode($body_contents, true, 512, JSON_OBJECT_AS_ARRAY & JSON_THROW_ON_ERROR);
     }
+
+    public function isJiraCloud(): bool
+    {
+        if (\ForgeConfig::get(self::CONFIG_KEY_FORCE_JIRA_SERVER) === '1') {
+            return false;
+        }
+        if ($this->is_jira_cloud === null) {
+            $json                = $this->getUrl(self::JIRA_CORE_BASE_URL . '/serverInfo');
+            $this->is_jira_cloud = isset($json['deploymentType']) && $json['deploymentType'] === self::DEPLOYMENT_TYPE_CLOUD;
+        }
+        return $this->is_jira_cloud;
+    }
 }
diff --git a/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraClient.php b/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraClient.php
index b4e02ce296..713e5f5702 100644
--- a/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraClient.php
+++ b/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraClient.php
@@ -29,4 +29,6 @@ interface JiraClient
      * @throws JiraConnectionException|\JsonException
      */
     public function getUrl(string $url): ?array;
+
+    public function isJiraCloud(): bool;
 }
diff --git a/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraConnectionException.php b/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraConnectionException.php
index 70b0ee5703..46dc6ae658 100644
--- a/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraConnectionException.php
+++ b/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraConnectionException.php
@@ -86,25 +86,27 @@ class JiraConnectionException extends \Exception
                 $jira_warnings = $body['warningMessages'];
             }
         } catch (\JsonException $exception) {
+            $message = sprintf(
+                'Query was not successful (code: %d, message: "%s"). Error response cannot be read, invalid json for %s',
+                $response->getStatusCode(),
+                $response->getReasonPhrase(),
+                (string) $request->getUri(),
+            );
             return new self(
-                sprintf(
-                    'Query was not successful (code: %d, message: "%s"). Error response cannot be read, invalid json for %s',
-                    $response->getStatusCode(),
-                    $response->getReasonPhrase(),
-                    (string) $request->getUri(),
-                ),
-                ''
+                $message,
+                $message
             );
         }
+        $message = sprintf(
+            'Query was not successful (code: %d, message: "%s"). Jira errors:' . PHP_EOL . '%s' . PHP_EOL . 'Jira warnings:' . PHP_EOL . '%s',
+            $response->getStatusCode(),
+            $response->getReasonPhrase(),
+            implode(PHP_EOL, $jira_errors),
+            implode(PHP_EOL, $jira_warnings),
+        );
         return new self(
-            sprintf(
-                'Query was not successful (code: %d, message: "%s"). Jira errors:' . PHP_EOL . '%s' . PHP_EOL . 'Jira warnings:' . PHP_EOL . '%s',
-                $response->getStatusCode(),
-                $response->getReasonPhrase(),
-                implode(PHP_EOL, $jira_errors),
-                implode(PHP_EOL, $jira_warnings),
-            ),
-            ''
+            $message,
+            $message
         );
     }
 
diff --git a/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraProjectBuilder.php b/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraProjectBuilder.php
index f8609d81b1..d19c37d7aa 100644
--- a/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraProjectBuilder.php
+++ b/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraProjectBuilder.php
@@ -28,10 +28,24 @@ use Psr\Log\LoggerInterface;
 class JiraProjectBuilder
 {
     /**
-     * @throws JiraConnectionException
      * @return array<array{id: string, label: string}>
+     * @throws UnexpectedFormatException
+     * @throws JiraConnectionException
+     * @throws \JsonException
      */
     public function build(JiraClient $jira_client, LoggerInterface $logger): array
+    {
+        if ($jira_client->isJiraCloud()) {
+            return $this->getCollectionFromJiraCloud($jira_client, $logger);
+        }
+        return $this->getCollectionFromJiraServer($jira_client);
+    }
+
+    /**
+     * @return array<array{id: string, label: string}>
+     * @throws UnexpectedFormatException
+     */
+    private function getCollectionFromJiraCloud(JiraClient $jira_client, LoggerInterface $logger): array
     {
         $iterator           = JiraCollectionBuilder::iterateUntilIsLast(
             $jira_client,
@@ -53,4 +67,30 @@ class JiraProjectBuilder
         }
         return $project_collection->getJiraProjects();
     }
+
+    /**
+     * @throws UnexpectedFormatException
+     * @throws JiraConnectionException
+     * @throws \JsonException
+     */
+    private function getCollectionFromJiraServer(JiraClient $jira_client): array
+    {
+        $json = $jira_client->getUrl(ClientWrapper::JIRA_CORE_BASE_URL . '/project');
+        if ($json === null) {
+            throw new UnexpectedFormatException(ClientWrapper::JIRA_CORE_BASE_URL . '/project is supposed to return a collection of projects, null received');
+        }
+        $project_collection = new JiraProjectCollection();
+        foreach ($json as $project_entry) {
+            if (! isset($project_entry['key'], $project_entry['name'])) {
+                throw new UnexpectedFormatException('`key` or `name` has not been founded in jira_representation');
+            }
+            $project_collection->addProject(
+                [
+                    'id' => (string) $project_entry['key'],
+                    'label' => (string) $project_entry['name']
+                ]
+            );
+        }
+        return $project_collection->getJiraProjects();
+    }
 }
diff --git a/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraProjectCollection.php b/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraProjectCollection.php
index e4cca630f1..5eb7f477ac 100644
--- a/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraProjectCollection.php
+++ b/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraProjectCollection.php
@@ -33,9 +33,10 @@ class JiraProjectCollection
     /**
      * @param array{id: string, label: string} $project
      */
-    public function addProject(array $project): void
+    public function addProject(array $project): self
     {
         $this->jira_projects[] = $project;
+        return $this;
     }
 
     /**
diff --git a/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraProjectListController.php b/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraProjectListController.php
index 20c9a931fc..2f2cdd7b5f 100644
--- a/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraProjectListController.php
+++ b/plugins/tracker/include/Tracker/Creation/JiraImporter/JiraProjectListController.php
@@ -79,9 +79,9 @@ class JiraProjectListController implements DispatchableWithRequest, Dispatchable
             $projects = $this->jira_project_builder->build($wrapper, $this->logger);
             $layout->sendJSON($projects);
         } catch (JiraConnectionException $exception) {
-            $layout->send400JSONErrors(['error' => $exception->getI18nMessage()]);
+            $layout->send400JSONErrors(['error' => $exception->getI18nMessage(), 'type' => get_class($exception)]);
         } catch (\Exception $exception) {
-            $layout->send400JSONErrors(['error' => $exception->getMessage()]);
+            $layout->send400JSONErrors(['error' => $exception->getMessage(), 'type' => get_class($exception)]);
         }
     }
 
diff --git a/plugins/tracker/include/trackerPlugin.php b/plugins/tracker/include/trackerPlugin.php
index f3a41797c5..0ffd9b1af9 100644
--- a/plugins/tracker/include/trackerPlugin.php
+++ b/plugins/tracker/include/trackerPlugin.php
@@ -2357,8 +2357,8 @@ class trackerPlugin extends Plugin //phpcs:ignore PSR1.Classes.ClassDeclaration.
 
     public function getWhitelistedKeys(GetWhitelistedKeys $event): void
     {
-        $event->addConfigClass(JiraRunner::class);
         $event->addConfigClass(ListPickerIncluder::class);
+        $event->addConfigClass(\Tuleap\Tracker\Creation\JiraImporter\ClientWrapper::class);
     }
 
     private function getMailNotificationBuilder(): MailNotificationBuilder
-- 
2.31.1

