Merge branch 'stable-3.8' into stable-3.9 * stable-3.8: Replace commentLink.html with commentLink.link during init Change-Id: Ia7a302ad9d71a6ce48536ee67739295600ed6025
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraClient.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraClient.java index 5544df2..7c1c35d 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraClient.java +++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraClient.java
@@ -109,6 +109,50 @@ } } + public void invokeProjectRestAPI( + JiraItsServerInfo server, + String projectKey, + String method, + String uri, + int[] passCodes, + String body) + throws IOException { + + log.debug( + "Trying to invoke project {} restapi method {} uri {} status {} body {}", + projectKey, + method, + uri, + Arrays.toString(passCodes), + body); + apiBuilder.getProjects(server).sendPayload(method, projectKey + uri, body, passCodes); + log.debug("reatapi invoked project {}", projectKey); + } + + public void invokeIssueRestAPI( + JiraItsServerInfo server, + String issueKey, + String method, + String uri, + int[] passCodes, + String body) + throws IOException { + + if (issueExists(server, issueKey)) { + log.debug( + "Trying to invoke issue {} restapi method {} uri {} status {} body {}", + issueKey, + method, + uri, + Arrays.toString(passCodes), + body); + apiBuilder.getIssue(server).sendPayload(method, issueKey + uri, body, passCodes); + log.debug("reatapi invoked issue {}", issueKey); + } else { + log.error("Issue {} does not exist or no access permission", issueKey); + } + } + public void createVersion(JiraItsServerInfo server, String projectKey, String version) throws IOException { log.debug("Trying to create version {} on project {}", version, projectKey);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraModule.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraModule.java index 3d59458..4cdda87 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/JiraModule.java
@@ -29,6 +29,8 @@ import com.googlesource.gerrit.plugins.its.base.its.ItsFacade; import com.googlesource.gerrit.plugins.its.base.its.ItsFacadeFactory; import com.googlesource.gerrit.plugins.its.base.workflow.CustomAction; +import com.googlesource.gerrit.plugins.its.jira.workflow.InvokeIssueRestAPI; +import com.googlesource.gerrit.plugins.its.jira.workflow.InvokeProjectRestAPI; import com.googlesource.gerrit.plugins.its.jira.workflow.MarkPropertyAsReleasedVersion; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,6 +64,12 @@ bind(ItsConfig.class); bind(JiraItsServerInfoProvider.class); bind(CustomAction.class) + .annotatedWith(Exports.named(InvokeProjectRestAPI.ACTION_NAME)) + .to(InvokeProjectRestAPI.class); + bind(CustomAction.class) + .annotatedWith(Exports.named(InvokeIssueRestAPI.ACTION_NAME)) + .to(InvokeIssueRestAPI.class); + bind(CustomAction.class) .annotatedWith(Exports.named(MarkPropertyAsReleasedVersion.ACTION_NAME)) .to(MarkPropertyAsReleasedVersion.class); install(new ItsHookModule(pluginName, pluginCfgFactory));
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApi.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApi.java index bcd10a5..af89214 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApi.java +++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApi.java
@@ -13,6 +13,7 @@ // limitations under the License. package com.googlesource.gerrit.plugins.its.jira.restapi; +import static java.nio.charset.StandardCharsets.UTF_8; import com.google.gson.Gson; import com.google.inject.Inject; @@ -72,7 +73,7 @@ public T doGet(String spec, int passCode, int[] failCodes) throws IOException { HttpURLConnection conn = openConnection(spec, "GET", false); try { - if (validateResponse(conn, passCode, failCodes)) { + if (validateResponse(conn, new int[] {passCode}, failCodes)) { readIncomingData(conn); } return data; @@ -117,10 +118,15 @@ private boolean sendPayload(String method, String spec, String jsonInput, int passCode) throws IOException { + return sendPayload(method, spec, jsonInput, new int[] {passCode}); + } + + public boolean sendPayload(String method, String spec, String jsonInput, int[] passCodes) + throws IOException { HttpURLConnection conn = openConnection(spec, method, true); try { writeBodyData(jsonInput, conn); - return validateResponse(conn, passCode, null); + return validateResponse(conn, passCodes, null); } finally { conn.disconnect(); } @@ -130,7 +136,7 @@ private void writeBodyData(String data, HttpURLConnection conn) throws IOException { if (data != null) { try (OutputStream os = conn.getOutputStream()) { - os.write(data.getBytes()); + os.write(data.getBytes(UTF_8)); os.flush(); } } @@ -148,10 +154,10 @@ * IOException exception is thrown. If it was part of the list, then the actual response code is * returned. returns true if valid response is returned, otherwise false */ - private boolean validateResponse(HttpURLConnection conn, int passCode, int[] failCodes) + private boolean validateResponse(HttpURLConnection conn, int[] passCodes, int[] failCodes) throws IOException { responseCode = conn.getResponseCode(); - if (responseCode == passCode) { + if (ArrayUtils.contains(passCodes, responseCode)) { return true; } if ((failCodes == null) || (!ArrayUtils.contains(failCodes, responseCode))) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeIssueRestAPI.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeIssueRestAPI.java new file mode 100644 index 0000000..f798d64 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeIssueRestAPI.java
@@ -0,0 +1,83 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed 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 com.googlesource.gerrit.plugins.its.jira.workflow; + +import com.google.inject.Inject; +import com.googlesource.gerrit.plugins.its.base.ItsPath; +import com.googlesource.gerrit.plugins.its.base.its.ItsFacade; +import com.googlesource.gerrit.plugins.its.base.workflow.ActionRequest; +import com.googlesource.gerrit.plugins.its.base.workflow.ActionType; +import com.googlesource.gerrit.plugins.its.base.workflow.CustomAction; +import com.googlesource.gerrit.plugins.its.jira.JiraClient; +import com.googlesource.gerrit.plugins.its.jira.JiraItsFacade; +import com.googlesource.gerrit.plugins.its.jira.JiraItsServerInfo; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class InvokeIssueRestAPI implements CustomAction { + + private static final Logger log = LoggerFactory.getLogger(InvokeIssueRestAPI.class); + + public static final String ACTION_NAME = "invoke-issue-restapi"; + + private final JiraClient jiraClient; + private final InvokeRestAPIParametersExtractor parametersExtractor; + private final SoyTemplateRenderer soyTemplateRenderer; + + @Inject + public InvokeIssueRestAPI( + @ItsPath Path itsPath, + JiraClient jiraClient, + InvokeRestAPIParametersExtractor parametersExtractor) { + this.jiraClient = jiraClient; + this.parametersExtractor = parametersExtractor; + this.soyTemplateRenderer = new SoyTemplateRenderer(itsPath); + } + + @Override + public void execute( + ItsFacade its, String issueKey, ActionRequest actionRequest, Map<String, String> properties) + throws IOException { + Optional<InvokeRestAPIParameters> _parameters = + parametersExtractor.extract(actionRequest, properties); + if (!_parameters.isPresent()) { + return; + } + InvokeRestAPIParameters parameters = _parameters.get(); + + if (!JiraItsFacade.class.isInstance(its)) { + throw new IllegalArgumentException("Incorrect facade type"); + } + JiraItsFacade jits = JiraItsFacade.class.cast(its); + JiraItsServerInfo jiraItsServerInfo = jits.getJiraServerInstance(); + + jiraClient.invokeIssueRestAPI( + jiraItsServerInfo, + issueKey, + parameters.getMethod(), + parameters.getUri(), + parameters.getPassCodes(), + soyTemplateRenderer.render(parameters.getTemplate(), properties)); + } + + @Override + public ActionType getType() { + return ActionType.ISSUE; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeProjectRestAPI.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeProjectRestAPI.java new file mode 100644 index 0000000..c2cec94 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeProjectRestAPI.java
@@ -0,0 +1,84 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed 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 com.googlesource.gerrit.plugins.its.jira.workflow; + +import com.google.gerrit.entities.Project; +import com.google.inject.Inject; +import com.googlesource.gerrit.plugins.its.base.ItsPath; +import com.googlesource.gerrit.plugins.its.base.its.ItsFacade; +import com.googlesource.gerrit.plugins.its.base.workflow.ActionRequest; +import com.googlesource.gerrit.plugins.its.base.workflow.ActionType; +import com.googlesource.gerrit.plugins.its.base.workflow.CustomAction; +import com.googlesource.gerrit.plugins.its.jira.JiraClient; +import com.googlesource.gerrit.plugins.its.jira.JiraItsServerInfo; +import com.googlesource.gerrit.plugins.its.jira.JiraItsServerInfoProvider; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class InvokeProjectRestAPI implements CustomAction { + + private static final Logger log = LoggerFactory.getLogger(InvokeProjectRestAPI.class); + + public static final String ACTION_NAME = "invoke-project-restapi"; + + private final JiraItsServerInfoProvider serverInfoProvider; + private final JiraClient jiraClient; + private final InvokeRestAPIParametersExtractor parametersExtractor; + private final SoyTemplateRenderer soyTemplateRenderer; + + @Inject + public InvokeProjectRestAPI( + @ItsPath Path itsPath, + JiraItsServerInfoProvider serverInfoProvider, + JiraClient jiraClient, + InvokeRestAPIParametersExtractor parametersExtractor) { + this.serverInfoProvider = serverInfoProvider; + this.jiraClient = jiraClient; + this.parametersExtractor = parametersExtractor; + this.soyTemplateRenderer = new SoyTemplateRenderer(itsPath); + } + + @Override + public void execute( + ItsFacade its, String itsProject, ActionRequest actionRequest, Map<String, String> properties) + throws IOException { + Optional<InvokeRestAPIParameters> _parameters = + parametersExtractor.extract(actionRequest, properties); + if (!_parameters.isPresent()) { + return; + } + InvokeRestAPIParameters parameters = _parameters.get(); + + Project.NameKey projectName = Project.nameKey(properties.get("project")); + JiraItsServerInfo jiraItsServerInfo = serverInfoProvider.get(projectName); + + jiraClient.invokeProjectRestAPI( + jiraItsServerInfo, + itsProject, + parameters.getMethod(), + parameters.getUri(), + parameters.getPassCodes(), + soyTemplateRenderer.render(parameters.getTemplate(), properties)); + } + + @Override + public ActionType getType() { + return ActionType.PROJECT; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeRestAPIParameters.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeRestAPIParameters.java new file mode 100644 index 0000000..ce5313a --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeRestAPIParameters.java
@@ -0,0 +1,47 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed 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 com.googlesource.gerrit.plugins.its.jira.workflow; + +/** Parameters needed by {@link InvokeIssueRestAPI} and {@link InvokeProjectsRestAPI} actions */ +public class InvokeRestAPIParameters { + + private final String method; + private final String uri; + private final int[] passCodes; + private final String template; + + public InvokeRestAPIParameters(String method, String uri, int[] passCodes, String template) { + this.method = method; + this.uri = uri; + this.passCodes = passCodes; + this.template = template; + } + + public String getMethod() { + return method; + } + + public String getUri() { + return uri; + } + + public int[] getPassCodes() { + return passCodes; + } + + public String getTemplate() { + return template; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeRestAPIParametersExtractor.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeRestAPIParametersExtractor.java new file mode 100644 index 0000000..b99141e --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/InvokeRestAPIParametersExtractor.java
@@ -0,0 +1,68 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed 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 com.googlesource.gerrit.plugins.its.jira.workflow; + +import com.google.common.base.Strings; +import com.googlesource.gerrit.plugins.its.base.workflow.ActionRequest; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import javax.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class InvokeRestAPIParametersExtractor { + + private static final Logger log = LoggerFactory.getLogger(InvokeRestAPIParametersExtractor.class); + + @Inject + public InvokeRestAPIParametersExtractor() {} + + public Optional<InvokeRestAPIParameters> extract( + ActionRequest actionRequest, Map<String, String> properties) { + String[] parameters = actionRequest.getParameters(); + if (parameters.length != 4) { + log.error( + "Wrong number of received parameters. Received parameters are {}. Three parameters are" + + " expected, method, uri, passCodes and template.", + Arrays.toString(parameters)); + return Optional.empty(); + } + + String method = parameters[0]; + if (Strings.isNullOrEmpty(method)) { + log.error("Received property id is blank"); + return Optional.empty(); + } + String uri = parameters[1]; + if (Strings.isNullOrEmpty(uri)) { + log.error("Received property uri is blank"); + return Optional.empty(); + } + String passCodesStr = parameters[2]; + if (Strings.isNullOrEmpty(passCodesStr)) { + log.error("Received property passCodes is blank"); + return Optional.empty(); + } + int[] passCodes = Arrays.stream(passCodesStr.split(",")).mapToInt(Integer::parseInt).toArray(); + String template = parameters[3]; + if (Strings.isNullOrEmpty(template)) { + log.error("Received property template is blank"); + return Optional.empty(); + } + + return Optional.of(new InvokeRestAPIParameters(method, uri, passCodes, template)); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/SoyTemplateRenderer.java b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/SoyTemplateRenderer.java new file mode 100644 index 0000000..3463be8 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/its/jira/workflow/SoyTemplateRenderer.java
@@ -0,0 +1,68 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed 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 com.googlesource.gerrit.plugins.its.jira.workflow; + +import com.google.common.io.CharStreams; +import com.google.inject.ProvisionException; +import com.google.template.soy.SoyFileSet; +import com.google.template.soy.jbcsrc.api.SoySauce.Renderer; +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SoyTemplateRenderer { + + private static final Logger log = LoggerFactory.getLogger(SoyTemplateRenderer.class); + + private final Path templateDir; + + public SoyTemplateRenderer(Path itsPath) { + this.templateDir = itsPath.resolve("templates"); + } + + private String soyTextTemplate( + SoyFileSet.Builder builder, String template, Map<String, String> properties) { + + Path templatePath = templateDir.resolve(template + ".soy"); + String content; + + try (Reader r = Files.newBufferedReader(templatePath, StandardCharsets.UTF_8)) { + content = CharStreams.toString(r); + } catch (IOException err) { + throw new ProvisionException( + "Failed to read template file " + templatePath.toAbsolutePath().toString(), err); + } + + builder.add(content, templatePath.toAbsolutePath().toString()); + Renderer renderer = + builder + .build() + .compileTemplates() + .renderTemplate("etc.its.templates." + template) + .setData(properties); + String rendered = renderer.renderText().get(); + log.debug("Rendered template {} to:\n{}", templatePath, rendered); + return rendered; + } + + public String render(String template, Map<String, String> properties) throws IOException { + return soyTextTemplate(SoyFileSet.builder(), template, properties); + } +}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md index 4827dfd..cb34003 100644 --- a/src/main/resources/Documentation/config.md +++ b/src/main/resources/Documentation/config.md
@@ -274,6 +274,67 @@ Specific actions ---------------- +### invoke-issue-restapi + +The `invoke-issue-restapi` invokes RestAPI on the issue, it uses soy template to +generate the request. + +```ini + action = invoke-issue-restapi method uri passCodes template +``` + +For example if you would like to create a link in issues to review, use the +following action: + +```ini + action = invoke-issue-restapi POST /remotelink 200,201 link +``` + +With the follwing `its/templates/link.soy` template: + +``` +{namespace etc.its.templates} +{template .link} + {@param changeUrl: string} + {@param subject: string} + {@param status: string} +{lb} + "globalId": "{$changeUrl}", + "application": {lb} + "type": "com.googlesource.gerrit", + "name": "Gerrit" + {rb}, + "object": {lb} + "url": "{$changeUrl}", + "title": "{$subject}", + "icon": {lb} + "url16x16": "https://www.gerritcodereview.com/images/diffy_logo.png", + "title": "Review" + {rb}, + "status": {lb} + {switch $status} + {case null} + "resolved": false + {case 'NEW'} + "resolved": false + {case 'SUBMITTED'} + "resolved": false + {case 'MERGED'} + "resolved": true + {case 'ABANDONED'} + "resolved": true + {/switch} + {rb} + {rb} +{rb} +{/template} +``` + +### invoke-project-restapi + +The `invoke-project-restapi` is similar to `invoke-issue-restapi`, it invokes +project method instead of issue method. + ### mark-property-as-released-version The `mark-property-as-released-version` action marks a version as released in
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApiTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApiTest.java index 27c5fc7..ce16c0c 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApiTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/its/jira/restapi/JiraRestApiTest.java
@@ -82,8 +82,8 @@ @Test public void testDoPut() throws Exception { - JiraURL url = mock(JiraURL.class); - when(url.resolveUrl(any())).thenReturn(url); + url = mock(JiraURL.class); + when(url.resolveUrl(any(), any(), any())).thenReturn(url); when(url.withSpec(ISSUE_CLASS_PREFIX)).thenReturn(url); HttpURLConnection connection = mock(HttpURLConnection.class);