This is an automated email from the ASF dual-hosted git repository.

abeizn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 1145cc954 Feat(sonarqube): projects (#4269)
1145cc954 is described below

commit 1145cc954b33bef7f50ccb2013e0599de0c60b6c
Author: Warren Chen <[email protected]>
AuthorDate: Mon Jan 30 16:41:39 2023 +0800

    Feat(sonarqube): projects (#4269)
    
    * feat(sonarqube): init
    
    * feat(sonarqube): projects
    
    * fix(sonarqube): rebase to main
    
    * fix(sonarqube): fix lint
    
    * fix(generator): fix for review
---
 backend/generator/cmd/create_plugin.go             |  1 +
 .../template/plugin/api/blueprint.go-template      |  2 +-
 .../template/plugin/api/connection.go-template     |  2 +-
 .../generator/template/plugin/api/init.go-template |  2 +-
 .../plugin/impl/impl_complete_plugin.go-template   | 23 ++++--
 .../template/plugin/models/connection.go-template  |  2 +-
 .../template/plugin/tasks/api_client.go-template   |  2 +-
 .../plugin/tasks/api_collector.go-template         | 23 +++---
 .../template/plugin/tasks/extractor.go-template    |  9 ++-
 .../template/plugin/tasks/shared.go-template}      | 33 +++++----
 .../template/plugin/tasks/task_data.go-template    |  2 +-
 .../tasks/task_data_complete_plugin.go-template    | 25 +++++--
 backend/plugins/icla/tasks/task_data.go            |  2 +-
 backend/plugins/sonarqube/api/connection.go        | 19 ++++-
 backend/plugins/sonarqube/impl/impl.go             | 24 +++++--
 backend/plugins/sonarqube/models/connection.go     | 42 +++++++----
 .../migrationscripts/20230111_add_init_tables.go   |  3 +-
 .../sonarqube_project.go}                          | 31 ++++-----
 ...111_add_init_tables.go => sonarqube_project.go} | 31 ++++-----
 backend/plugins/sonarqube/sonarqube.go             |  2 +-
 backend/plugins/sonarqube/tasks/api_client.go      |  4 +-
 .../plugins/sonarqube/tasks/projects_collector.go  | 81 ++++++++++++++++++++++
 .../plugins/sonarqube/tasks/projects_extractor.go  | 55 +++++++++++++++
 backend/plugins/sonarqube/tasks/shared.go          | 65 +++++++++++++++++
 backend/plugins/sonarqube/tasks/task_data.go       | 19 +++--
 backend/plugins/zentao/tasks/task_data.go          |  2 +-
 26 files changed, 390 insertions(+), 116 deletions(-)

diff --git a/backend/generator/cmd/create_plugin.go 
b/backend/generator/cmd/create_plugin.go
index 3a6a4fa23..22f8adedf 100644
--- a/backend/generator/cmd/create_plugin.go
+++ b/backend/generator/cmd/create_plugin.go
@@ -127,6 +127,7 @@ Type in what the name of plugin is, then generator will 
create a new plugin in p
                                `impl/impl.go`:                   
util.ReadTemplate("generator/template/plugin/impl/impl_complete_plugin.go-template"),
                                `tasks/api_client.go`:            
util.ReadTemplate("generator/template/plugin/tasks/api_client.go-template"),
                                `tasks/task_data.go`:             
util.ReadTemplate("generator/template/plugin/tasks/task_data_complete_plugin.go-template"),
+                               `tasks/shared.go`:                
util.ReadTemplate("generator/template/plugin/tasks/shared.go-template"),
                                `api/connection.go`:              
util.ReadTemplate("generator/template/plugin/api/connection.go-template"),
                                `models/connection.go`:           
util.ReadTemplate("generator/template/plugin/models/connection.go-template"),
                                
fmt.Sprintf("models/migrationscripts/%s_add_init_tables.go", versionTimestamp): 
util.ReadTemplate("generator/template/migrationscripts/add_init_tables.go-template"),
diff --git a/backend/generator/template/plugin/api/blueprint.go-template 
b/backend/generator/template/plugin/api/blueprint.go-template
index 3429d34ad..054caac4e 100644
--- a/backend/generator/template/plugin/api/blueprint.go-template
+++ b/backend/generator/template/plugin/api/blueprint.go-template
@@ -21,7 +21,7 @@ import (
        "encoding/json"
        "github.com/apache/incubator-devlake/core/errors"
        core "github.com/apache/incubator-devlake/core/plugin"
-       "github.com/apache/incubator-devlake/plugins/helper"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
        "github.com/apache/incubator-devlake/plugins/{{ .plugin_name }}/tasks"
 )
 
diff --git a/backend/generator/template/plugin/api/connection.go-template 
b/backend/generator/template/plugin/api/connection.go-template
index 07c2f5b4f..8d3b1d3af 100644
--- a/backend/generator/template/plugin/api/connection.go-template
+++ b/backend/generator/template/plugin/api/connection.go-template
@@ -25,7 +25,7 @@ import (
        "time"
 
        core "github.com/apache/incubator-devlake/core/plugin"
-       "github.com/apache/incubator-devlake/plugins/helper"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
        "github.com/apache/incubator-devlake/plugins/{{ .plugin_name }}/models"
 )
 
diff --git a/backend/generator/template/plugin/api/init.go-template 
b/backend/generator/template/plugin/api/init.go-template
index f81181c0d..982c1d388 100644
--- a/backend/generator/template/plugin/api/init.go-template
+++ b/backend/generator/template/plugin/api/init.go-template
@@ -19,7 +19,7 @@ package api
 
 import (
        core "github.com/apache/incubator-devlake/core/plugin"
-       "github.com/apache/incubator-devlake/plugins/helper"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
        "github.com/go-playground/validator/v10"
 )
 
diff --git 
a/backend/generator/template/plugin/impl/impl_complete_plugin.go-template 
b/backend/generator/template/plugin/impl/impl_complete_plugin.go-template
index a6e81e147..defafca36 100644
--- a/backend/generator/template/plugin/impl/impl_complete_plugin.go-template
+++ b/backend/generator/template/plugin/impl/impl_complete_plugin.go-template
@@ -26,7 +26,7 @@ import (
     "github.com/apache/incubator-devlake/plugins/{{ .plugin_name }}/models"
     "github.com/apache/incubator-devlake/plugins/{{ .plugin_name 
}}/models/migrationscripts"
        "github.com/apache/incubator-devlake/plugins/{{ .plugin_name }}/tasks"
-       "github.com/apache/incubator-devlake/plugins/helper"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
        "github.com/spf13/viper"
 )
 
@@ -76,11 +76,22 @@ func (p {{ .PluginName }}) PrepareTaskData(taskCtx 
plugin.TaskContext, options m
     if err != nil {
         return nil, errors.Default.Wrap(err, "unable to get {{ .PluginName }} 
API client instance")
     }
-
-    return &tasks.{{ .PluginName }}TaskData{
-        Options:   op,
-        ApiClient: apiClient,
-    }, nil
+       taskData := &tasks.{{ .PluginName }}TaskData{
+               Options:   op,
+               ApiClient: apiClient,
+       }
+       var createdDateAfter time.Time
+       if op.CreatedDateAfter != "" {
+               createdDateAfter, err = 
errors.Convert01(time.Parse(time.RFC3339, op.CreatedDateAfter))
+               if err != nil {
+                       return nil, errors.BadInput.Wrap(err, "invalid value 
for `createdDateAfter`")
+               }
+       }
+       if !createdDateAfter.IsZero() {
+               taskData.CreatedDateAfter = &createdDateAfter
+               logger.Debug("collect data updated createdDateAfter %s", 
createdDateAfter)
+       }
+       return taskData, nil
 }
 
 // PkgPath information lost when compiled as plugin(.so)
diff --git a/backend/generator/template/plugin/models/connection.go-template 
b/backend/generator/template/plugin/models/connection.go-template
index 72133f103..a7d7a20b7 100644
--- a/backend/generator/template/plugin/models/connection.go-template
+++ b/backend/generator/template/plugin/models/connection.go-template
@@ -17,7 +17,7 @@ limitations under the License.
 
 package models
 
-import "github.com/apache/incubator-devlake/plugins/helper"
+import helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
 
 //TODO Please modify the following code to fit your needs
 // This object conforms to what the frontend currently sends.
diff --git a/backend/generator/template/plugin/tasks/api_client.go-template 
b/backend/generator/template/plugin/tasks/api_client.go-template
index c56dfd602..6a0d77ffb 100644
--- a/backend/generator/template/plugin/tasks/api_client.go-template
+++ b/backend/generator/template/plugin/tasks/api_client.go-template
@@ -26,7 +26,7 @@ import (
        "github.com/apache/incubator-devlake/plugins/{{ .plugin_name }}/models"
        core "github.com/apache/incubator-devlake/core/plugin"
        "github.com/apache/incubator-devlake/core/errors"
-       "github.com/apache/incubator-devlake/plugins/helper"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
 )
 
 func New{{ .PluginName }}ApiClient(taskCtx plugin.TaskContext, connection 
*models.{{ .PluginName }}Connection) (*api.ApiAsyncClient, errors.Error) {
diff --git a/backend/generator/template/plugin/tasks/api_collector.go-template 
b/backend/generator/template/plugin/tasks/api_collector.go-template
index eb344436e..5526314ea 100644
--- a/backend/generator/template/plugin/tasks/api_collector.go-template
+++ b/backend/generator/template/plugin/tasks/api_collector.go-template
@@ -25,7 +25,7 @@ import (
 
        core "github.com/apache/incubator-devlake/core/plugin"
        "github.com/apache/incubator-devlake/core/errors"
-       "github.com/apache/incubator-devlake/plugins/helper"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
 )
 
 const RAW_{{ .COLLECTOR_DATA_NAME }}_TABLE = "{{ .plugin_name }}_{{ 
.collector_data_name }}"
@@ -34,21 +34,19 @@ var _ core.SubTaskEntryPoint = Collect{{ .CollectorDataName 
}}
 
 func Collect{{ .CollectorDataName }}(taskCtx core.SubTaskContext) errors.Error 
{
        data := taskCtx.GetData().(*{{ .PluginName }}TaskData)
-       iterator, err := helper.NewDateIterator(365)
+       rawDataSubTaskArgs, data := CreateRawDataSubTaskArgs(taskCtx, RAW_{{ 
.COLLECTOR_DATA_NAME }}_TABLE)
+       logger := taskCtx.GetLogger()
+
+    collectorWithState, err := 
helper.NewApiCollectorWithState(*rawDataSubTaskArgs, data.CreatedDateAfter)
        if err != nil {
                return err
        }
+       incremental := collectorWithState.IsIncremental()
 
-       collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
-               RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
-                       Ctx: taskCtx,
-                       Params: {{ .PluginName }}ApiParams{
-                       },
-                       Table: RAW_{{ .COLLECTOR_DATA_NAME }}_TABLE,
-               },
+       err = collectorWithState.InitCollector(helper.ApiCollectorArgs{
+               Incremental: incremental,
                ApiClient:   data.ApiClient,
-               Incremental: false,
-               Input:       iterator,
+               // PageSize:    100,
                // TODO write which api would you want request
                UrlTemplate: "{{ .HttpPath }}",
                Query: func(reqData *helper.RequestData) (url.Values, 
errors.Error) {
@@ -66,8 +64,7 @@ func Collect{{ .CollectorDataName }}(taskCtx 
core.SubTaskContext) errors.Error {
        if err != nil {
                return err
        }
-
-       return collector.Execute()
+       return collectorWithState.Execute()
 }
 
 var Collect{{ .CollectorDataName }}Meta = plugin.SubTaskMeta{
diff --git a/backend/generator/template/plugin/tasks/extractor.go-template 
b/backend/generator/template/plugin/tasks/extractor.go-template
index 9991a4b19..0457dfaa5 100644
--- a/backend/generator/template/plugin/tasks/extractor.go-template
+++ b/backend/generator/template/plugin/tasks/extractor.go-template
@@ -19,14 +19,13 @@ package tasks
 
 import (
        "github.com/apache/incubator-devlake/core/errors"
-       core "github.com/apache/incubator-devlake/core/plugin"
-       "github.com/apache/incubator-devlake/core/errors"
-       "github.com/apache/incubator-devlake/plugins/helper"
+       "github.com/apache/incubator-devlake/core/plugin"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
 )
 
-var _ core.SubTaskEntryPoint = Extract{{ .ExtractorDataName }}
+var _ plugin.SubTaskEntryPoint = Extract{{ .ExtractorDataName }}
 
-func Extract{{ .ExtractorDataName }}(taskCtx core.SubTaskContext) errors.Error 
{
+func Extract{{ .ExtractorDataName }}(taskCtx plugin.SubTaskContext) 
errors.Error {
     extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
                RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
                        Ctx: taskCtx,
diff --git a/backend/plugins/icla/tasks/task_data.go 
b/backend/generator/template/plugin/tasks/shared.go-template
similarity index 51%
copy from backend/plugins/icla/tasks/task_data.go
copy to backend/generator/template/plugin/tasks/shared.go-template
index a420a013a..ee2ac613d 100644
--- a/backend/plugins/icla/tasks/task_data.go
+++ b/backend/generator/template/plugin/tasks/shared.go-template
@@ -18,21 +18,24 @@ limitations under the License.
 package tasks
 
 import (
-       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
 )
 
-type IclaApiParams struct {
-}
-
-type IclaOptions struct {
-       // TODO add some custom options here if necessary
-       // options means some custom params required by plugin running.
-       // Such As How many rows do your want
-       // You can use it in sub tasks and you need pass it in main.go and 
pipelines.
-       Tasks []string `json:"tasks,omitempty"`
-}
-
-type IclaTaskData struct {
-       Options   *IclaOptions
-       ApiClient *helper.ApiAsyncClient
+func CreateRawDataSubTaskArgs(taskCtx plugin.SubTaskContext, rawTable string) 
(*api.RawDataSubTaskArgs, *{{ .PluginName }}TaskData) {
+       data := taskCtx.GetData().(*{{ .PluginName }}TaskData)
+       filteredData := *data
+       filteredData.Options = &{{ .PluginName }}Options{}
+       *filteredData.Options = *data.Options
+       var params = {{ .PluginName }}ApiParams{
+               ConnectionId: data.Options.ConnectionId,
+               ProjectKey:   data.Options.ProjectKey,
+               HotspotKey:   data.Options.HotspotKey,
+       }
+       rawDataSubTaskArgs := &api.RawDataSubTaskArgs{
+               Ctx:    taskCtx,
+               Params: params,
+               Table:  rawTable,
+       }
+       return rawDataSubTaskArgs, &filteredData
 }
diff --git a/backend/generator/template/plugin/tasks/task_data.go-template 
b/backend/generator/template/plugin/tasks/task_data.go-template
index 50fdbd058..fbb0250c3 100644
--- a/backend/generator/template/plugin/tasks/task_data.go-template
+++ b/backend/generator/template/plugin/tasks/task_data.go-template
@@ -24,7 +24,7 @@ type {{ .PluginName }}Options struct {
        // TODO add some custom options here if necessary
        // options means some custom params required by plugin running.
        // Such As How many rows do your want
-       // You can use it in sub tasks and you need pass it in main.go and 
pipelines.
+       // You can use it in subtasks, and you need to pass it to main.go and 
pipelines.
 }
 
 type {{ .PluginName }}TaskData struct {
diff --git 
a/backend/generator/template/plugin/tasks/task_data_complete_plugin.go-template 
b/backend/generator/template/plugin/tasks/task_data_complete_plugin.go-template
index 49f2098ba..dcee683af 100644
--- 
a/backend/generator/template/plugin/tasks/task_data_complete_plugin.go-template
+++ 
b/backend/generator/template/plugin/tasks/task_data_complete_plugin.go-template
@@ -20,7 +20,7 @@ package tasks
 import (
        "fmt"
        "github.com/apache/incubator-devlake/core/errors"
-       "github.com/apache/incubator-devlake/plugins/helper"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
 )
 
 type {{ .PluginName }}ApiParams struct {
@@ -30,15 +30,16 @@ type {{ .PluginName }}Options struct {
        // TODO add some custom options here if necessary
        // options means some custom params required by plugin running.
        // Such As How many rows do your want
-       // You can use it in sub tasks and you need pass it in main.go and 
pipelines.
+       // You can use it in subtasks, and you need to pass it to main.go and 
pipelines.
     ConnectionId               uint64   `json:"connectionId"`
     Tasks                      []string `json:"tasks,omitempty"`
-    Since                      string
+    CreatedDateAfter string   `json:"createdDateAfter" 
mapstructure:"createdDateAfter,omitempty"`
 }
 
 type {{ .PluginName }}TaskData struct {
        Options   *{{ .PluginName }}Options
        ApiClient *api.ApiAsyncClient
+       CreatedDateAfter *time.Time
 }
 
 func DecodeAndValidateTaskOptions(options map[string]interface{}) (*{{ 
.PluginName }}Options, errors.Error) {
@@ -50,4 +51,20 @@ func DecodeAndValidateTaskOptions(options 
map[string]interface{}) (*{{ .PluginNa
                return nil, errors.Default.New("connectionId is invalid")
        }
        return &op, nil
-}
\ No newline at end of file
+}
+
+func CreateRawDataSubTaskArgs(taskCtx plugin.SubTaskContext, rawTable string) 
(*api.RawDataSubTaskArgs, *{{ .PluginName }}TaskData) {
+       data := taskCtx.GetData().(*{{ .PluginName }}TaskData)
+       filteredData := *data
+       filteredData.Options = &{{ .PluginName }}Options{}
+       *filteredData.Options = *data.Options
+       var params = {{ .PluginName }}ApiParams{
+               ConnectionId: data.Options.ConnectionId,
+       }
+       rawDataSubTaskArgs := &api.RawDataSubTaskArgs{
+               Ctx:    taskCtx,
+               Params: params,
+               Table:  rawTable,
+       }
+       return rawDataSubTaskArgs, &filteredData
+}
diff --git a/backend/plugins/icla/tasks/task_data.go 
b/backend/plugins/icla/tasks/task_data.go
index a420a013a..dfa35a0ea 100644
--- a/backend/plugins/icla/tasks/task_data.go
+++ b/backend/plugins/icla/tasks/task_data.go
@@ -28,7 +28,7 @@ type IclaOptions struct {
        // TODO add some custom options here if necessary
        // options means some custom params required by plugin running.
        // Such As How many rows do your want
-       // You can use it in sub tasks and you need pass it in main.go and 
pipelines.
+       // You can use it in subtasks, and you need to pass it to main.go and 
pipelines.
        Tasks []string `json:"tasks,omitempty"`
 }
 
diff --git a/backend/plugins/sonarqube/api/connection.go 
b/backend/plugins/sonarqube/api/connection.go
index 0dea3fcbf..bf518c0ad 100644
--- a/backend/plugins/sonarqube/api/connection.go
+++ b/backend/plugins/sonarqube/api/connection.go
@@ -28,10 +28,14 @@ import (
        "time"
 )
 
+type validation struct {
+       Valid bool `json:"valid"`
+}
+
 func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
        // decode
        var err errors.Error
-       var connection models.TestConnectionRequest
+       var connection models.SonarqubeConn
        if err = api.Decode(input.Body, &connection, vld); err != nil {
                return nil, err
        }
@@ -40,7 +44,7 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
                context.TODO(),
                connection.Endpoint,
                map[string]string{
-                       "Authorization": fmt.Sprintf("%s:", connection.Token),
+                       "Authorization": fmt.Sprintf("Basic %s", 
connection.GetEncodedToken()),
                },
                3*time.Second,
                connection.Proxy,
@@ -50,13 +54,22 @@ func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput,
                return nil, err
        }
 
-       res, err := apiClient.Get("server/version", nil, nil)
+       res, err := apiClient.Get("authentication/validate", nil, nil)
        if err != nil {
                return nil, err
        }
        if res.StatusCode != http.StatusOK {
                return nil, 
errors.HttpStatus(res.StatusCode).New(fmt.Sprintf("unexpected status code: %d", 
res.StatusCode))
        }
+
+       body := &validation{}
+       err = api.UnmarshalResponse(res, body)
+       if err != nil {
+               return nil, err
+       }
+       if !body.Valid {
+               return nil, errors.Default.New("Authentication failed, please 
check your access token.")
+       }
        return nil, nil
 }
 
diff --git a/backend/plugins/sonarqube/impl/impl.go 
b/backend/plugins/sonarqube/impl/impl.go
index 04679ab82..82b7c55ad 100644
--- a/backend/plugins/sonarqube/impl/impl.go
+++ b/backend/plugins/sonarqube/impl/impl.go
@@ -23,6 +23,7 @@ import (
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/plugin"
        helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "time"
 
        "github.com/apache/incubator-devlake/plugins/sonarqube/api"
        "github.com/apache/incubator-devlake/plugins/sonarqube/models"
@@ -50,10 +51,14 @@ func (p Sonarqube) Init(br context.BasicRes) errors.Error {
 }
 
 func (p Sonarqube) SubTaskMetas() []plugin.SubTaskMeta {
-       return []plugin.SubTaskMeta{}
+       return []plugin.SubTaskMeta{
+               tasks.CollectProjectsMeta,
+               tasks.ExtractProjectsMeta,
+       }
 }
 
 func (p Sonarqube) PrepareTaskData(taskCtx plugin.TaskContext, options 
map[string]interface{}) (interface{}, errors.Error) {
+       logger := taskCtx.GetLogger()
        op, err := tasks.DecodeAndValidateTaskOptions(options)
        if err != nil {
                return nil, err
@@ -72,11 +77,22 @@ func (p Sonarqube) PrepareTaskData(taskCtx 
plugin.TaskContext, options map[strin
        if err != nil {
                return nil, errors.Default.Wrap(err, "unable to get Sonarqube 
API client instance")
        }
-
-       return &tasks.SonarqubeTaskData{
+       taskData := &tasks.SonarqubeTaskData{
                Options:   op,
                ApiClient: apiClient,
-       }, nil
+       }
+       var createdDateAfter time.Time
+       if op.CreatedDateAfter != "" {
+               createdDateAfter, err = 
errors.Convert01(time.Parse(time.RFC3339, op.CreatedDateAfter))
+               if err != nil {
+                       return nil, errors.BadInput.Wrap(err, "invalid value 
for `createdDateAfter`")
+               }
+       }
+       if !createdDateAfter.IsZero() {
+               taskData.CreatedDateAfter = &createdDateAfter
+               logger.Debug("collect data updated createdDateAfter %s", 
createdDateAfter)
+       }
+       return taskData, nil
 }
 
 // PkgPath information lost when compiled as plugin(.so)
diff --git a/backend/plugins/sonarqube/models/connection.go 
b/backend/plugins/sonarqube/models/connection.go
index fe60b8179..422d88d51 100644
--- a/backend/plugins/sonarqube/models/connection.go
+++ b/backend/plugins/sonarqube/models/connection.go
@@ -17,20 +17,44 @@ limitations under the License.
 
 package models
 
-import helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+import (
+       "encoding/base64"
+       "fmt"
+       "github.com/apache/incubator-devlake/core/errors"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       
"github.com/apache/incubator-devlake/helpers/pluginhelper/api/apihelperabstract"
+       "net/http"
+)
+
+type SonarqubeAccessToken helper.AccessToken
+
+// SetupAuthentication sets up the HTTP Request Authentication
+func (sat SonarqubeAccessToken) SetupAuthentication(req *http.Request) 
errors.Error {
+       req.Header.Set("Authorization", fmt.Sprintf("Basic %s", 
sat.GetEncodedToken()))
+       return nil
+}
+
+func (sat SonarqubeAccessToken) GetAccessTokenAuthenticator() 
apihelperabstract.ApiAuthenticator {
+       return sat
+}
+
+// GetEncodedToken returns encoded bearer token for HTTP Basic Authentication
+func (sat SonarqubeAccessToken) GetEncodedToken() string {
+       return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%v:", 
sat.Token)))
+}
 
 // This object conforms to what the frontend currently sends.
 type SonarqubeConnection struct {
        helper.BaseConnection `mapstructure:",squash"`
        helper.RestConnection `mapstructure:",squash"`
        // For sonarqube, we can `use user_token:`
-       helper.AccessToken `mapstructure:",squash"`
+       SonarqubeAccessToken `mapstructure:",squash"`
 }
 
-type TestConnectionRequest struct {
-       Endpoint           string `json:"endpoint"`
-       Proxy              string `json:"proxy"`
-       helper.AccessToken `mapstructure:",squash"`
+// SonarqubeConn holds the essential information to connect to the sonarqube 
API
+type SonarqubeConn struct {
+       helper.RestConnection `mapstructure:",squash"`
+       SonarqubeAccessToken  `mapstructure:",squash"`
 }
 
 // This object conforms to what the frontend currently expects.
@@ -40,12 +64,6 @@ type SonarqubeResponse struct {
        SonarqubeConnection
 }
 
-// Using User because it requires authentication.
-type ApiUserResponse struct {
-       Id   int
-       Name string `json:"name"`
-}
-
 func (SonarqubeConnection) TableName() string {
        return "_tool_sonarqube_connections"
 }
diff --git 
a/backend/plugins/sonarqube/models/migrationscripts/20230111_add_init_tables.go 
b/backend/plugins/sonarqube/models/migrationscripts/20230111_add_init_tables.go
index bb3854365..3511113e6 100644
--- 
a/backend/plugins/sonarqube/models/migrationscripts/20230111_add_init_tables.go
+++ 
b/backend/plugins/sonarqube/models/migrationscripts/20230111_add_init_tables.go
@@ -30,11 +30,12 @@ func (*addInitTables) Up(basicRes context.BasicRes) 
errors.Error {
        return migrationhelper.AutoMigrateTables(
                basicRes,
                &archived.SonarqubeConnection{},
+               &archived.SonarqubeProject{},
        )
 }
 
 func (*addInitTables) Version() uint64 {
-       return 20230111000011
+       return 20230130000011
 }
 
 func (*addInitTables) Name() string {
diff --git 
a/backend/plugins/sonarqube/models/migrationscripts/20230111_add_init_tables.go 
b/backend/plugins/sonarqube/models/migrationscripts/archived/sonarqube_project.go
similarity index 54%
copy from 
backend/plugins/sonarqube/models/migrationscripts/20230111_add_init_tables.go
copy to 
backend/plugins/sonarqube/models/migrationscripts/archived/sonarqube_project.go
index bb3854365..fa2473dd8 100644
--- 
a/backend/plugins/sonarqube/models/migrationscripts/20230111_add_init_tables.go
+++ 
b/backend/plugins/sonarqube/models/migrationscripts/archived/sonarqube_project.go
@@ -15,28 +15,23 @@ See the License for the specific language governing 
permissions and
 limitations under the License.
 */
 
-package migrationscripts
+package archived
 
 import (
-       "github.com/apache/incubator-devlake/core/context"
-       "github.com/apache/incubator-devlake/core/errors"
-       "github.com/apache/incubator-devlake/helpers/migrationhelper"
-       
"github.com/apache/incubator-devlake/plugins/sonarqube/models/migrationscripts/archived"
+       
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+       "time"
 )
 
-type addInitTables struct{}
-
-func (*addInitTables) Up(basicRes context.BasicRes) errors.Error {
-       return migrationhelper.AutoMigrateTables(
-               basicRes,
-               &archived.SonarqubeConnection{},
-       )
-}
-
-func (*addInitTables) Version() uint64 {
-       return 20230111000011
+type SonarqubeProject struct {
+       archived.NoPKModel
+       Key              string     `json:"key" 
gorm:"type:varchar(64);primaryKey"`
+       Name             string     `json:"name" gorm:"type:varchar(255)"`
+       Qualifier        string     `json:"qualifier" gorm:"type:varchar(255)"`
+       Visibility       string     `json:"visibility" gorm:"type:varchar(64)"`
+       LastAnalysisDate *time.Time `json:"lastAnalysisDate"`
+       Revision         string     `json:"revision" gorm:"type:varchar(128)"`
 }
 
-func (*addInitTables) Name() string {
-       return "sonarqube init schemas"
+func (SonarqubeProject) TableName() string {
+       return "_tool_sonarqube_projects"
 }
diff --git 
a/backend/plugins/sonarqube/models/migrationscripts/20230111_add_init_tables.go 
b/backend/plugins/sonarqube/models/sonarqube_project.go
similarity index 52%
copy from 
backend/plugins/sonarqube/models/migrationscripts/20230111_add_init_tables.go
copy to backend/plugins/sonarqube/models/sonarqube_project.go
index bb3854365..d549aca6b 100644
--- 
a/backend/plugins/sonarqube/models/migrationscripts/20230111_add_init_tables.go
+++ b/backend/plugins/sonarqube/models/sonarqube_project.go
@@ -15,28 +15,23 @@ See the License for the specific language governing 
permissions and
 limitations under the License.
 */
 
-package migrationscripts
+package models
 
 import (
-       "github.com/apache/incubator-devlake/core/context"
-       "github.com/apache/incubator-devlake/core/errors"
-       "github.com/apache/incubator-devlake/helpers/migrationhelper"
-       
"github.com/apache/incubator-devlake/plugins/sonarqube/models/migrationscripts/archived"
+       "github.com/apache/incubator-devlake/core/models/common"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
 )
 
-type addInitTables struct{}
-
-func (*addInitTables) Up(basicRes context.BasicRes) errors.Error {
-       return migrationhelper.AutoMigrateTables(
-               basicRes,
-               &archived.SonarqubeConnection{},
-       )
-}
-
-func (*addInitTables) Version() uint64 {
-       return 20230111000011
+type SonarqubeProject struct {
+       common.NoPKModel
+       Key              string           `json:"key" 
gorm:"type:varchar(64);primaryKey"`
+       Name             string           `json:"name" gorm:"type:varchar(255)"`
+       Qualifier        string           `json:"qualifier" 
gorm:"type:varchar(255)"`
+       Visibility       string           `json:"visibility" 
gorm:"type:varchar(64)"`
+       LastAnalysisDate *api.Iso8601Time `json:"lastAnalysisDate"`
+       Revision         string           `json:"revision" 
gorm:"type:varchar(128)"`
 }
 
-func (*addInitTables) Name() string {
-       return "sonarqube init schemas"
+func (SonarqubeProject) TableName() string {
+       return "_tool_sonarqube_projects"
 }
diff --git a/backend/plugins/sonarqube/sonarqube.go 
b/backend/plugins/sonarqube/sonarqube.go
index f5b88d01e..927398a3c 100644
--- a/backend/plugins/sonarqube/sonarqube.go
+++ b/backend/plugins/sonarqube/sonarqube.go
@@ -33,7 +33,7 @@ func main() {
        projectName := cmd.Flags().StringP("projectName", "o", "", "sonarqube 
projectName")
        createdDateAfter := cmd.Flags().StringP("createdDateAfter", "a", "", 
"collect data that are created after specified time, ie 2006-05-06T07:08:09Z")
        _ = cmd.MarkFlagRequired("connectionId")
-       _ = cmd.MarkFlagRequired("projectName")
+       //_ = cmd.MarkFlagRequired("projectName")
 
        cmd.Run = func(cmd *cobra.Command, args []string) {
                runner.DirectRun(cmd, args, PluginEntry, map[string]interface{}{
diff --git a/backend/plugins/sonarqube/tasks/api_client.go 
b/backend/plugins/sonarqube/tasks/api_client.go
index 0fff56b32..e7864f4a7 100644
--- a/backend/plugins/sonarqube/tasks/api_client.go
+++ b/backend/plugins/sonarqube/tasks/api_client.go
@@ -32,9 +32,9 @@ import (
 func NewSonarqubeApiClient(taskCtx plugin.TaskContext, connection 
*models.SonarqubeConnection) (*api.ApiAsyncClient, errors.Error) {
        // create synchronize api client so we can calculate api rate limit 
dynamically
        headers := map[string]string{
-               "Authorization": fmt.Sprintf("%s:", connection.Token),
+               "Authorization": fmt.Sprintf("Basic %s", 
connection.GetEncodedToken()),
        }
-       apiClient, err := api.NewApiClient(taskCtx.GetContext(), 
connection.Endpoint, headers, 0, connection.Proxy, taskCtx)
+       apiClient, err := api.NewApiClient(taskCtx.GetContext(), 
connection.Endpoint, headers, 30*time.Second, connection.Proxy, taskCtx)
        if err != nil {
                return nil, err
        }
diff --git a/backend/plugins/sonarqube/tasks/projects_collector.go 
b/backend/plugins/sonarqube/tasks/projects_collector.go
new file mode 100644
index 000000000..da8bd87b4
--- /dev/null
+++ b/backend/plugins/sonarqube/tasks/projects_collector.go
@@ -0,0 +1,81 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "encoding/json"
+       "fmt"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "net/http"
+       "net/url"
+)
+
+const RAW_PROJECTS_TABLE = "sonarqube_projects"
+
+var _ plugin.SubTaskEntryPoint = CollectProjects
+
+func CollectProjects(taskCtx plugin.SubTaskContext) errors.Error {
+       rawDataSubTaskArgs, data := CreateRawDataSubTaskArgs(taskCtx, 
RAW_PROJECTS_TABLE)
+       logger := taskCtx.GetLogger()
+       logger.Info("collect projects")
+
+       collectorWithState, err := 
helper.NewApiCollectorWithState(*rawDataSubTaskArgs, data.CreatedDateAfter)
+       if err != nil {
+               return err
+       }
+       err = collectorWithState.InitCollector(helper.ApiCollectorArgs{
+               ApiClient:   data.ApiClient,
+               PageSize:    100,
+               UrlTemplate: "projects/search",
+               Query: func(reqData *helper.RequestData) (url.Values, 
errors.Error) {
+                       query := url.Values{}
+                       if data.CreatedDateAfter != nil {
+                               query.Set("analyzedBefore",
+                                       
data.CreatedDateAfter.Format("2006-01-02"))
+                       }
+                       if data.Options.ProjectKey != "" {
+                               query.Set("q", data.Options.ProjectKey)
+                       }
+                       query.Set("p", fmt.Sprintf("%v", reqData.Pager.Page))
+                       query.Set("ps", fmt.Sprintf("%v", reqData.Pager.Size))
+                       return query, nil
+               },
+               GetTotalPages: GetTotalPagesFromResponse,
+               ResponseParser: func(res *http.Response) ([]json.RawMessage, 
errors.Error) {
+                       var resData struct {
+                               Data []json.RawMessage `json:"components"`
+                       }
+                       err = helper.UnmarshalResponse(res, &resData)
+                       return resData.Data, err
+               },
+       })
+       if err != nil {
+               return err
+       }
+
+       return collectorWithState.Execute()
+}
+
+var CollectProjectsMeta = plugin.SubTaskMeta{
+       Name:             "CollectProjects",
+       EntryPoint:       CollectProjects,
+       EnabledByDefault: true,
+       Description:      "Collect Projects data from Sonarqube api",
+}
diff --git a/backend/plugins/sonarqube/tasks/projects_extractor.go 
b/backend/plugins/sonarqube/tasks/projects_extractor.go
new file mode 100644
index 000000000..bd94f5007
--- /dev/null
+++ b/backend/plugins/sonarqube/tasks/projects_extractor.go
@@ -0,0 +1,55 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "encoding/json"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/sonarqube/models"
+)
+
+var _ plugin.SubTaskEntryPoint = ExtractProjects
+
+func ExtractProjects(taskCtx plugin.SubTaskContext) errors.Error {
+       rawDataSubTaskArgs, _ := CreateRawDataSubTaskArgs(taskCtx, 
RAW_PROJECTS_TABLE)
+       extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+               RawDataSubTaskArgs: *rawDataSubTaskArgs,
+               Extract: func(resData *helper.RawData) ([]interface{}, 
errors.Error) {
+                       body := &models.SonarqubeProject{}
+                       err := errors.Convert(json.Unmarshal(resData.Data, 
body))
+                       if err != nil {
+                               return nil, err
+                       }
+                       return []interface{}{body}, nil
+               },
+       })
+       if err != nil {
+               return err
+       }
+
+       return extractor.Execute()
+}
+
+var ExtractProjectsMeta = plugin.SubTaskMeta{
+       Name:             "ExtractProjects",
+       EntryPoint:       ExtractProjects,
+       EnabledByDefault: true,
+       Description:      "Extract raw data into tool layer table 
sonarqube_projects",
+}
diff --git a/backend/plugins/sonarqube/tasks/shared.go 
b/backend/plugins/sonarqube/tasks/shared.go
new file mode 100644
index 000000000..316ef36c7
--- /dev/null
+++ b/backend/plugins/sonarqube/tasks/shared.go
@@ -0,0 +1,65 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "net/http"
+)
+
+func CreateRawDataSubTaskArgs(taskCtx plugin.SubTaskContext, rawTable string) 
(*api.RawDataSubTaskArgs, *SonarqubeTaskData) {
+       data := taskCtx.GetData().(*SonarqubeTaskData)
+       filteredData := *data
+       filteredData.Options = &SonarqubeOptions{}
+       *filteredData.Options = *data.Options
+       var params = SonarqubeApiParams{
+               ConnectionId: data.Options.ConnectionId,
+               ProjectKey:   data.Options.ProjectKey,
+               HotspotKey:   data.Options.HotspotKey,
+       }
+       rawDataSubTaskArgs := &api.RawDataSubTaskArgs{
+               Ctx:    taskCtx,
+               Params: params,
+               Table:  rawTable,
+       }
+       return rawDataSubTaskArgs, &filteredData
+}
+
+func GetTotalPagesFromResponse(res *http.Response, args *api.ApiCollectorArgs) 
(int, errors.Error) {
+       body := &SonarqubePagination{}
+       err := api.UnmarshalResponse(res, body)
+       if err != nil {
+               return 0, err
+       }
+       pages := body.Paging.Total / args.PageSize
+       if body.Paging.Total%args.PageSize > 0 {
+               pages++
+       }
+       return pages, nil
+}
+
+type SonarqubePagination struct {
+       Paging Paging `json:"paging"`
+}
+type Paging struct {
+       PageIndex int `json:"pageIndex"`
+       PageSize  int `json:"pageSize"`
+       Total     int `json:"total"`
+}
diff --git a/backend/plugins/sonarqube/tasks/task_data.go 
b/backend/plugins/sonarqube/tasks/task_data.go
index 67dbdbd01..c409340e7 100644
--- a/backend/plugins/sonarqube/tasks/task_data.go
+++ b/backend/plugins/sonarqube/tasks/task_data.go
@@ -20,23 +20,30 @@ package tasks
 import (
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "time"
 )
 
 type SonarqubeApiParams struct {
+       ConnectionId uint64 `json:"connectionId"`
+       ProjectKey   string
+       HotspotKey   string
 }
 
 type SonarqubeOptions struct {
        // options means some custom params required by plugin running.
        // Such As How many rows do your want
-       // You can use it in sub tasks and you need pass it in main.go and 
pipelines.
-       ConnectionId uint64   `json:"connectionId"`
-       Tasks        []string `json:"tasks,omitempty"`
-       Since        string
+       // You can use it in subtasks, and you need to pass it to main.go and 
pipelines.
+       ConnectionId     uint64   `json:"connectionId"`
+       ProjectKey       string   `json:"projectKey"`
+       HotspotKey       string   `json:"hotspotKey"`
+       CreatedDateAfter string   `json:"createdDateAfter" 
mapstructure:"createdDateAfter,omitempty"`
+       Tasks            []string `json:"tasks,omitempty"`
 }
 
 type SonarqubeTaskData struct {
-       Options   *SonarqubeOptions
-       ApiClient *api.ApiAsyncClient
+       Options          *SonarqubeOptions
+       ApiClient        *api.ApiAsyncClient
+       CreatedDateAfter *time.Time
 }
 
 func DecodeAndValidateTaskOptions(options map[string]interface{}) 
(*SonarqubeOptions, errors.Error) {
diff --git a/backend/plugins/zentao/tasks/task_data.go 
b/backend/plugins/zentao/tasks/task_data.go
index 81b9936da..da07c0a54 100644
--- a/backend/plugins/zentao/tasks/task_data.go
+++ b/backend/plugins/zentao/tasks/task_data.go
@@ -33,7 +33,7 @@ type ZentaoApiParams struct {
 type ZentaoOptions struct {
        // options means some custom params required by plugin running.
        // Such As How many rows do your want
-       // You can use it in sub tasks and you need pass it in main.go and 
pipelines.
+       // You can use it in subtasks, and you need to pass it to main.go and 
pipelines.
        ConnectionId uint64 `json:"connectionId"`
        ProductId    int64
        ExecutionId  int64


Reply via email to