diff --git a/pom.xml b/pom.xml index f8f25a2..0a29829 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,11 @@ commons-lang 2.6 + + commons-codec + commons-codec + 1.10 + org.eclipse.jetty jetty-util diff --git a/src/main/java/com/aliyun/api/gateway/demo/Request.java b/src/main/java/com/aliyun/api/gateway/demo/Request.java index b89bfa4..25a44db 100644 --- a/src/main/java/com/aliyun/api/gateway/demo/Request.java +++ b/src/main/java/com/aliyun/api/gateway/demo/Request.java @@ -20,6 +20,7 @@ import com.aliyun.api.gateway.demo.enums.Method; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -195,4 +196,7 @@ public List getSignHeaderPrefixList() { public void setSignHeaderPrefixList(List signHeaderPrefixList) { this.signHeaderPrefixList = signHeaderPrefixList; } + public void setSignHeaderPrefixList(String signHeaderPrefixList) { + this.signHeaderPrefixList = Arrays.asList(signHeaderPrefixList.split(",")); + } } diff --git a/src/main/java/com/aliyun/api/gateway/demo/service/JiraConfig.java b/src/main/java/com/aliyun/api/gateway/demo/service/JiraConfig.java new file mode 100644 index 0000000..d42ee3e --- /dev/null +++ b/src/main/java/com/aliyun/api/gateway/demo/service/JiraConfig.java @@ -0,0 +1,95 @@ +/* + * 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 com.aliyun.api.gateway.demo.service; + +/** + * Jira 配置类 + */ +public class JiraConfig { + /** + * Jira 服务器地址(例如:https://your-domain.atlassian.net) + */ + private String jiraBaseUrl; + + /** + * Jira 用户邮箱 + */ + private String username; + + /** + * Jira API Token + */ + private String apiToken; + + /** + * 请求超时时间(毫秒) + */ + private int timeout; + + public JiraConfig() { + this.timeout = 30000; // 默认30秒 + } + + public JiraConfig(String jiraBaseUrl, String username, String apiToken) { + this.jiraBaseUrl = jiraBaseUrl; + this.username = username; + this.apiToken = apiToken; + this.timeout = 30000; + } + + public JiraConfig(String jiraBaseUrl, String username, String apiToken, int timeout) { + this.jiraBaseUrl = jiraBaseUrl; + this.username = username; + this.apiToken = apiToken; + this.timeout = timeout; + } + + public String getJiraBaseUrl() { + return jiraBaseUrl; + } + + public void setJiraBaseUrl(String jiraBaseUrl) { + this.jiraBaseUrl = jiraBaseUrl; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getApiToken() { + return apiToken; + } + + public void setApiToken(String apiToken) { + this.apiToken = apiToken; + } + + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } +} + diff --git a/src/main/java/com/aliyun/api/gateway/demo/service/JiraConfigLoader.java b/src/main/java/com/aliyun/api/gateway/demo/service/JiraConfigLoader.java new file mode 100644 index 0000000..3e653f9 --- /dev/null +++ b/src/main/java/com/aliyun/api/gateway/demo/service/JiraConfigLoader.java @@ -0,0 +1,138 @@ +/* + * 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 com.aliyun.api.gateway.demo.service; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * Jira 配置加载器 + * 从配置文件或环境变量中加载 Jira 配置 + */ +public class JiraConfigLoader { + + private static final String DEFAULT_CONFIG_FILE = "jira-config.properties"; + + /** + * 从默认配置文件加载配置 + * @return JiraConfig 对象 + * @throws IOException + */ + public static JiraConfig loadFromFile() throws IOException { + return loadFromFile(DEFAULT_CONFIG_FILE); + } + + /** + * 从指定配置文件加载配置 + * @param configFile 配置文件路径 + * @return JiraConfig 对象 + * @throws IOException + */ + public static JiraConfig loadFromFile(String configFile) throws IOException { + Properties props = new Properties(); + + // 尝试从文件系统加载 + try (InputStream input = new FileInputStream(configFile)) { + props.load(input); + } catch (IOException e) { + // 尝试从 classpath 加载 + try (InputStream input = JiraConfigLoader.class.getClassLoader() + .getResourceAsStream(configFile)) { + if (input == null) { + throw new IOException("无法找到配置文件: " + configFile); + } + props.load(input); + } + } + + return createConfigFromProperties(props); + } + + /** + * 从环境变量加载配置 + * @return JiraConfig 对象 + */ + public static JiraConfig loadFromEnv() { + String baseUrl = System.getenv("JIRA_BASE_URL"); + String username = System.getenv("JIRA_USERNAME"); + String apiToken = System.getenv("JIRA_API_TOKEN"); + String timeoutStr = System.getenv("JIRA_TIMEOUT"); + + if (baseUrl == null || username == null || apiToken == null) { + throw new IllegalStateException( + "缺少必要的环境变量: JIRA_BASE_URL, JIRA_USERNAME, JIRA_API_TOKEN" + ); + } + + int timeout = 30000; // 默认30秒 + if (timeoutStr != null && !timeoutStr.isEmpty()) { + try { + timeout = Integer.parseInt(timeoutStr); + } catch (NumberFormatException e) { + System.err.println("无效的超时时间配置,使用默认值: 30000ms"); + } + } + + return new JiraConfig(baseUrl, username, apiToken, timeout); + } + + /** + * 从 Properties 对象创建 JiraConfig + * @param props Properties 对象 + * @return JiraConfig 对象 + */ + private static JiraConfig createConfigFromProperties(Properties props) { + String baseUrl = props.getProperty("jira.base.url"); + String username = props.getProperty("jira.username"); + String apiToken = props.getProperty("jira.api.token"); + String timeoutStr = props.getProperty("jira.timeout", "30000"); + + if (baseUrl == null || username == null || apiToken == null) { + throw new IllegalStateException( + "配置文件缺少必要的属性: jira.base.url, jira.username, jira.api.token" + ); + } + + int timeout = 30000; + try { + timeout = Integer.parseInt(timeoutStr); + } catch (NumberFormatException e) { + System.err.println("无效的超时时间配置,使用默认值: 30000ms"); + } + + return new JiraConfig(baseUrl, username, apiToken, timeout); + } + + /** + * 优先从环境变量加载,如果失败则从配置文件加载 + * @return JiraConfig 对象 + * @throws IOException + */ + public static JiraConfig load() throws IOException { + try { + return loadFromEnv(); + } catch (IllegalStateException e) { + System.out.println("从环境变量加载失败,尝试从配置文件加载..."); + return loadFromFile(); + } + } +} + diff --git a/src/main/java/com/aliyun/api/gateway/demo/service/JiraService.java b/src/main/java/com/aliyun/api/gateway/demo/service/JiraService.java new file mode 100644 index 0000000..f4e65f6 --- /dev/null +++ b/src/main/java/com/aliyun/api/gateway/demo/service/JiraService.java @@ -0,0 +1,323 @@ +/* + * 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 com.aliyun.api.gateway.demo.service; + +import com.alibaba.fastjson.JSON; +import com.aliyun.api.gateway.demo.service.dto.JiraIssue; +import com.aliyun.api.gateway.demo.service.dto.JiraIssueCreateRequest; +import com.aliyun.api.gateway.demo.service.dto.JiraIssueUpdateRequest; +import org.apache.commons.codec.binary.Base64; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.*; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.util.EntityUtils; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +/** + * Jira API 服务类 + * 提供与 Jira REST API 交互的方法 + */ +public class JiraService { + + private final JiraConfig config; + private final String authHeader; + + private static final String API_VERSION = "/rest/api/3"; + + /** + * 构造函数 + * @param config Jira配置对象 + */ + public JiraService(JiraConfig config) { + this.config = config; + // 创建 Basic Auth 认证头 + String auth = config.getUsername() + ":" + config.getApiToken(); + byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(StandardCharsets.UTF_8)); + this.authHeader = "Basic " + new String(encodedAuth); + } + + /** + * 创建 Issue + * @param request 创建 Issue 的请求对象 + * @return 创建的 Issue 信息 + * @throws Exception + */ + public JiraIssue createIssue(JiraIssueCreateRequest request) throws Exception { + String url = config.getJiraBaseUrl() + API_VERSION + "/issue"; + String jsonBody = JSON.toJSONString(request); + + String response = executePost(url, jsonBody); + return JSON.parseObject(response, JiraIssue.class); + } + + /** + * 获取 Issue 详情 + * @param issueIdOrKey Issue ID 或 Key(例如:PROJ-123) + * @return Issue 详情 + * @throws Exception + */ + public JiraIssue getIssue(String issueIdOrKey) throws Exception { + String url = config.getJiraBaseUrl() + API_VERSION + "/issue/" + issueIdOrKey; + String response = executeGet(url); + return JSON.parseObject(response, JiraIssue.class); + } + + /** + * 更新 Issue + * @param issueIdOrKey Issue ID 或 Key + * @param request 更新请求对象 + * @return 是否更新成功 + * @throws Exception + */ + public boolean updateIssue(String issueIdOrKey, JiraIssueUpdateRequest request) throws Exception { + String url = config.getJiraBaseUrl() + API_VERSION + "/issue/" + issueIdOrKey; + String jsonBody = JSON.toJSONString(request); + + executePut(url, jsonBody); + return true; + } + + /** + * 删除 Issue + * @param issueIdOrKey Issue ID 或 Key + * @return 是否删除成功 + * @throws Exception + */ + public boolean deleteIssue(String issueIdOrKey) throws Exception { + String url = config.getJiraBaseUrl() + API_VERSION + "/issue/" + issueIdOrKey; + executeDelete(url); + return true; + } + + /** + * 搜索 Issues + * @param jql JQL 查询语句 + * @param maxResults 最大返回结果数 + * @return 搜索结果的 JSON 字符串 + * @throws Exception + */ + public String searchIssues(String jql, int maxResults) throws Exception { + String url = config.getJiraBaseUrl() + API_VERSION + "/search"; + + Map requestBody = new HashMap<>(); + requestBody.put("jql", jql); + requestBody.put("maxResults", maxResults); + + String jsonBody = JSON.toJSONString(requestBody); + return executePost(url, jsonBody); + } + + /** + * 为 Issue 添加评论 + * @param issueIdOrKey Issue ID 或 Key + * @param comment 评论内容 + * @return 评论结果的 JSON 字符串 + * @throws Exception + */ + public String addComment(String issueIdOrKey, String comment) throws Exception { + String url = config.getJiraBaseUrl() + API_VERSION + "/issue/" + issueIdOrKey + "/comment"; + + Map requestBody = new HashMap<>(); + Map body = new HashMap<>(); + body.put("type", "doc"); + body.put("version", 1); + + Map content = new HashMap<>(); + content.put("type", "paragraph"); + + Map text = new HashMap<>(); + text.put("type", "text"); + text.put("text", comment); + + content.put("content", new Object[]{text}); + body.put("content", new Object[]{content}); + + requestBody.put("body", body); + + String jsonBody = JSON.toJSONString(requestBody); + return executePost(url, jsonBody); + } + + /** + * 获取所有项目 + * @return 项目列表的 JSON 字符串 + * @throws Exception + */ + public String getAllProjects() throws Exception { + String url = config.getJiraBaseUrl() + API_VERSION + "/project"; + return executeGet(url); + } + + /** + * 获取项目详情 + * @param projectIdOrKey 项目 ID 或 Key + * @return 项目详情的 JSON 字符串 + * @throws Exception + */ + public String getProject(String projectIdOrKey) throws Exception { + String url = config.getJiraBaseUrl() + API_VERSION + "/project/" + projectIdOrKey; + return executeGet(url); + } + + /** + * 转换 Issue 状态 + * @param issueIdOrKey Issue ID 或 Key + * @param transitionId 转换 ID + * @return 是否转换成功 + * @throws Exception + */ + public boolean transitionIssue(String issueIdOrKey, String transitionId) throws Exception { + String url = config.getJiraBaseUrl() + API_VERSION + "/issue/" + issueIdOrKey + "/transitions"; + + Map requestBody = new HashMap<>(); + Map transition = new HashMap<>(); + transition.put("id", transitionId); + requestBody.put("transition", transition); + + String jsonBody = JSON.toJSONString(requestBody); + executePost(url, jsonBody); + return true; + } + + /** + * 执行 GET 请求 + */ + private String executeGet(String url) throws Exception { + HttpClient httpClient = new DefaultHttpClient(); + HttpGet httpGet = new HttpGet(url); + + // 设置请求头 + httpGet.setHeader("Authorization", authHeader); + httpGet.setHeader("Content-Type", "application/json"); + httpGet.setHeader("Accept", "application/json"); + + try { + HttpResponse response = httpClient.execute(httpGet); + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8"); + + if (statusCode >= 200 && statusCode < 300) { + return responseBody; + } else { + throw new Exception("Jira API Error: " + statusCode + " - " + responseBody); + } + } finally { + httpClient.getConnectionManager().shutdown(); + } + } + + /** + * 执行 POST 请求 + */ + private String executePost(String url, String jsonBody) throws Exception { + HttpClient httpClient = new DefaultHttpClient(); + HttpPost httpPost = new HttpPost(url); + + // 设置请求头 + httpPost.setHeader("Authorization", authHeader); + httpPost.setHeader("Content-Type", "application/json"); + httpPost.setHeader("Accept", "application/json"); + + // 设置请求体 + if (jsonBody != null && !jsonBody.isEmpty()) { + StringEntity entity = new StringEntity(jsonBody, "UTF-8"); + httpPost.setEntity(entity); + } + + try { + HttpResponse response = httpClient.execute(httpPost); + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8"); + + if (statusCode >= 200 && statusCode < 300) { + return responseBody; + } else { + throw new Exception("Jira API Error: " + statusCode + " - " + responseBody); + } + } finally { + httpClient.getConnectionManager().shutdown(); + } + } + + /** + * 执行 PUT 请求 + */ + private String executePut(String url, String jsonBody) throws Exception { + HttpClient httpClient = new DefaultHttpClient(); + HttpPut httpPut = new HttpPut(url); + + // 设置请求头 + httpPut.setHeader("Authorization", authHeader); + httpPut.setHeader("Content-Type", "application/json"); + httpPut.setHeader("Accept", "application/json"); + + // 设置请求体 + if (jsonBody != null && !jsonBody.isEmpty()) { + StringEntity entity = new StringEntity(jsonBody, "UTF-8"); + httpPut.setEntity(entity); + } + + try { + HttpResponse response = httpClient.execute(httpPut); + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8"); + + if (statusCode >= 200 && statusCode < 300) { + return responseBody; + } else { + throw new Exception("Jira API Error: " + statusCode + " - " + responseBody); + } + } finally { + httpClient.getConnectionManager().shutdown(); + } + } + + /** + * 执行 DELETE 请求 + */ + private String executeDelete(String url) throws Exception { + HttpClient httpClient = new DefaultHttpClient(); + HttpDelete httpDelete = new HttpDelete(url); + + // 设置请求头 + httpDelete.setHeader("Authorization", authHeader); + httpDelete.setHeader("Content-Type", "application/json"); + httpDelete.setHeader("Accept", "application/json"); + + try { + HttpResponse response = httpClient.execute(httpDelete); + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8"); + + if (statusCode >= 200 && statusCode < 300) { + return responseBody; + } else { + throw new Exception("Jira API Error: " + statusCode + " - " + responseBody); + } + } finally { + httpClient.getConnectionManager().shutdown(); + } + } +} + diff --git a/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssue.java b/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssue.java new file mode 100644 index 0000000..204a985 --- /dev/null +++ b/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssue.java @@ -0,0 +1,92 @@ +/* + * 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 com.aliyun.api.gateway.demo.service.dto; + +import java.util.Map; + +/** + * Jira Issue 响应对象 + */ +public class JiraIssue { + /** + * Issue ID + */ + private String id; + + /** + * Issue Key (例如:PROJ-123) + */ + private String key; + + /** + * Issue 的 URL + */ + private String self; + + /** + * Issue 的字段信息 + */ + private Map fields; + + public JiraIssue() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getSelf() { + return self; + } + + public void setSelf(String self) { + this.self = self; + } + + public Map getFields() { + return fields; + } + + public void setFields(Map fields) { + this.fields = fields; + } + + @Override + public String toString() { + return "JiraIssue{" + + "id='" + id + '\'' + + ", key='" + key + '\'' + + ", self='" + self + '\'' + + ", fields=" + fields + + '}'; + } +} + diff --git a/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssueCreateRequest.java b/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssueCreateRequest.java new file mode 100644 index 0000000..a1e0c74 --- /dev/null +++ b/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssueCreateRequest.java @@ -0,0 +1,131 @@ +/* + * 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 com.aliyun.api.gateway.demo.service.dto; + +import java.util.HashMap; +import java.util.Map; + +/** + * Jira Issue 创建请求对象 + */ +public class JiraIssueCreateRequest { + + private Map fields; + + public JiraIssueCreateRequest() { + this.fields = new HashMap<>(); + } + + /** + * 设置项目 + * @param projectKey 项目 Key + */ + public void setProject(String projectKey) { + Map project = new HashMap<>(); + project.put("key", projectKey); + fields.put("project", project); + } + + /** + * 设置标题 + * @param summary 标题 + */ + public void setSummary(String summary) { + fields.put("summary", summary); + } + + /** + * 设置描述 + * @param description 描述内容 + */ + public void setDescription(String description) { + // Jira Cloud 使用 Atlassian Document Format + Map descDoc = new HashMap<>(); + descDoc.put("type", "doc"); + descDoc.put("version", 1); + + Map paragraph = new HashMap<>(); + paragraph.put("type", "paragraph"); + + Map text = new HashMap<>(); + text.put("type", "text"); + text.put("text", description); + + paragraph.put("content", new Object[]{text}); + descDoc.put("content", new Object[]{paragraph}); + + fields.put("description", descDoc); + } + + /** + * 设置 Issue 类型 + * @param issueTypeName Issue 类型名称(例如:Task, Bug, Story) + */ + public void setIssueType(String issueTypeName) { + Map issueType = new HashMap<>(); + issueType.put("name", issueTypeName); + fields.put("issuetype", issueType); + } + + /** + * 设置优先级 + * @param priorityName 优先级名称(例如:High, Medium, Low) + */ + public void setPriority(String priorityName) { + Map priority = new HashMap<>(); + priority.put("name", priorityName); + fields.put("priority", priority); + } + + /** + * 设置经办人 + * @param accountId 用户账号 ID + */ + public void setAssignee(String accountId) { + Map assignee = new HashMap<>(); + assignee.put("accountId", accountId); + fields.put("assignee", assignee); + } + + /** + * 设置标签 + * @param labels 标签数组 + */ + public void setLabels(String[] labels) { + fields.put("labels", labels); + } + + /** + * 添加自定义字段 + * @param fieldName 字段名称 + * @param value 字段值 + */ + public void addCustomField(String fieldName, Object value) { + fields.put(fieldName, value); + } + + public Map getFields() { + return fields; + } + + public void setFields(Map fields) { + this.fields = fields; + } +} + diff --git a/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssueUpdateRequest.java b/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssueUpdateRequest.java new file mode 100644 index 0000000..21816d9 --- /dev/null +++ b/src/main/java/com/aliyun/api/gateway/demo/service/dto/JiraIssueUpdateRequest.java @@ -0,0 +1,111 @@ +/* + * 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 com.aliyun.api.gateway.demo.service.dto; + +import java.util.HashMap; +import java.util.Map; + +/** + * Jira Issue 更新请求对象 + */ +public class JiraIssueUpdateRequest { + + private Map fields; + + public JiraIssueUpdateRequest() { + this.fields = new HashMap<>(); + } + + /** + * 更新标题 + * @param summary 新标题 + */ + public void setSummary(String summary) { + fields.put("summary", summary); + } + + /** + * 更新描述 + * @param description 新描述 + */ + public void setDescription(String description) { + // Jira Cloud 使用 Atlassian Document Format + Map descDoc = new HashMap<>(); + descDoc.put("type", "doc"); + descDoc.put("version", 1); + + Map paragraph = new HashMap<>(); + paragraph.put("type", "paragraph"); + + Map text = new HashMap<>(); + text.put("type", "text"); + text.put("text", description); + + paragraph.put("content", new Object[]{text}); + descDoc.put("content", new Object[]{paragraph}); + + fields.put("description", descDoc); + } + + /** + * 更新优先级 + * @param priorityName 优先级名称 + */ + public void setPriority(String priorityName) { + Map priority = new HashMap<>(); + priority.put("name", priorityName); + fields.put("priority", priority); + } + + /** + * 更新经办人 + * @param accountId 用户账号 ID + */ + public void setAssignee(String accountId) { + Map assignee = new HashMap<>(); + assignee.put("accountId", accountId); + fields.put("assignee", assignee); + } + + /** + * 更新标签 + * @param labels 标签数组 + */ + public void setLabels(String[] labels) { + fields.put("labels", labels); + } + + /** + * 添加自定义字段 + * @param fieldName 字段名称 + * @param value 字段值 + */ + public void addCustomField(String fieldName, Object value) { + fields.put(fieldName, value); + } + + public Map getFields() { + return fields; + } + + public void setFields(Map fields) { + this.fields = fields; + } +} + diff --git a/src/main/java/com/aliyun/api/gateway/demo/util/SignUtil.java b/src/main/java/com/aliyun/api/gateway/demo/util/SignUtil.java index b2beb0e..a5f33cf 100644 --- a/src/main/java/com/aliyun/api/gateway/demo/util/SignUtil.java +++ b/src/main/java/com/aliyun/api/gateway/demo/util/SignUtil.java @@ -1,37 +1,79 @@ package com.aliyun.api.gateway.demo.util; -import java.util.*; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; + import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.StringUtils; + import com.aliyun.api.gateway.demo.constant.Constants; import com.aliyun.api.gateway.demo.constant.HttpHeader; import com.aliyun.api.gateway.demo.constant.SystemHeader; +/** + * 签名工具类 + * 用于生成阿里云API网关请求签名 + */ public class SignUtil { + private SignUtil() { + // 工具类,禁止实例化 + throw new IllegalStateException("Utility class"); + } + + /** + * 生成请求签名 + * + * @param secret 密钥 + * @param method HTTP方法 + * @param path 请求路径 + * @param headers 请求头 + * @param querys 查询参数 + * @param bodys 请求体参数 + * @param signHeaderPrefixList 需要签名的请求头前缀列表 + * @return Base64编码的签名字符串 + * @throws RuntimeException 签名生成失败时抛出 + */ public static String sign(String secret, String method, String path, - Map headers, - Map querys, - Map bodys, - List signHeaderPrefixList) { + Map headers, + Map querys, + Map bodys, + List signHeaderPrefixList) { try { Mac hmacSha256 = Mac.getInstance(Constants.HMAC_SHA256); - hmacSha256.init(new SecretKeySpec(secret.getBytes(Constants.ENCODING), Constants.HMAC_SHA256)); + byte[] keyBytes = secret.getBytes(Constants.ENCODING); + SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, Constants.HMAC_SHA256); + hmacSha256.init(secretKeySpec); String stringToSign = buildStringToSign(method, path, headers, querys, bodys, signHeaderPrefixList); - return Base64.encodeBase64String(hmacSha256.doFinal(stringToSign.getBytes(Constants.ENCODING))); + byte[] signBytes = hmacSha256.doFinal(stringToSign.getBytes(Constants.ENCODING)); + return Base64.encodeBase64String(signBytes); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new RuntimeException("Error while generating signature: " + e.getMessage(), e); } catch (Exception e) { - throw new RuntimeException("Error while generating signature", e); + throw new RuntimeException("Unexpected error during signature generation", e); } } + /** + * 构建待签名字符串 + */ private static String buildStringToSign(String method, String path, - Map headers, - Map querys, - Map bodys, - List signHeaderPrefixList) { + Map headers, + Map querys, + Map bodys, + List signHeaderPrefixList) { StringBuilder sb = new StringBuilder(); sb.append(method.toUpperCase()).append(Constants.LF); @@ -44,40 +86,69 @@ private static String buildStringToSign(String method, String path, .append(Constants.LF) .append(getHeaderOrEmpty(headers, HttpHeader.HTTP_HEADER_DATE)) .append(Constants.LF); + } else { + // headers为null时,添加4个换行符 + sb.append(Constants.LF).append(Constants.LF) + .append(Constants.LF).append(Constants.LF); } + sb.append(buildHeaders(headers, signHeaderPrefixList)) .append(buildResource(path, querys, bodys)); return sb.toString(); } + /** + * 构建资源路径字符串(包含查询参数和body参数) + */ private static String buildResource(String path, Map querys, Map bodys) { StringBuilder sb = new StringBuilder(path != null ? path : ""); Map sortMap = new TreeMap<>(); - if (querys != null) sortMap.putAll(querys); - if (bodys != null) sortMap.putAll(bodys); + if (querys != null) { + sortMap.putAll(querys); + } + if (bodys != null) { + sortMap.putAll(bodys); + } + + if (sortMap.isEmpty()) { + return sb.toString(); + } String paramString = sortMap.entrySet().stream() .filter(entry -> !StringUtils.isBlank(entry.getKey())) - .map(entry -> entry.getKey() + (StringUtils.isBlank(entry.getValue()) ? "" : Constants.SPE4 + entry.getValue())) - .reduce((a, b) -> a + Constants.SPE3 + b) - .orElse(""); + .map(entry -> { + String key = entry.getKey(); + String value = entry.getValue(); + return StringUtils.isBlank(value) ? key : key + Constants.SPE4 + value; + }) + .collect(Collectors.joining(Constants.SPE3)); if (!paramString.isEmpty()) { sb.append(Constants.SPE5).append(paramString); } + return sb.toString(); } + /** + * 构建需要签名的请求头字符串 + */ private static String buildHeaders(Map headers, List signHeaderPrefixList) { if (headers == null || signHeaderPrefixList == null) { return ""; } + // 过滤掉不需要的请求头前缀 List filteredPrefixes = new ArrayList<>(signHeaderPrefixList); - filteredPrefixes.removeAll(Arrays.asList(SystemHeader.X_CA_SIGNATURE, HttpHeader.HTTP_HEADER_ACCEPT, - HttpHeader.HTTP_HEADER_CONTENT_MD5, HttpHeader.HTTP_HEADER_CONTENT_TYPE, HttpHeader.HTTP_HEADER_DATE)); + filteredPrefixes.removeAll(Arrays.asList( + SystemHeader.X_CA_SIGNATURE, + HttpHeader.HTTP_HEADER_ACCEPT, + HttpHeader.HTTP_HEADER_CONTENT_MD5, + HttpHeader.HTTP_HEADER_CONTENT_TYPE, + HttpHeader.HTTP_HEADER_DATE + )); Collections.sort(filteredPrefixes); Map sortedHeaders = new TreeMap<>(headers); @@ -85,76 +156,42 @@ private static String buildHeaders(Map headers, List sig StringBuilder signHeaders = new StringBuilder(); for (Map.Entry entry : sortedHeaders.entrySet()) { - if (isHeaderToSign(entry.getKey(), filteredPrefixes)) { - sb.append(entry.getKey()).append(Constants.SPE2).append(entry.getValue() == null ? "" : entry.getValue()).append(Constants.LF); - if (signHeaders.length() > 0) signHeaders.append(Constants.SPE1); - signHeaders.append(entry.getKey()); + String headerName = entry.getKey(); + if (isHeaderToSign(headerName, filteredPrefixes)) { + String headerValue = entry.getValue() != null ? entry.getValue() : ""; + sb.append(headerName) + .append(Constants.SPE2) + .append(headerValue) + .append(Constants.LF); + + if (signHeaders.length() > 0) { + signHeaders.append(Constants.SPE1); + } + signHeaders.append(headerName); } } + headers.put(SystemHeader.X_CA_SIGNATURE_HEADERS, signHeaders.toString()); return sb.toString(); } + /** + * 判断请求头是否需要参与签名 + */ private static boolean isHeaderToSign(String headerName, List signHeaderPrefixList) { - return !StringUtils.isBlank(headerName) && - (headerName.startsWith(Constants.CA_HEADER_TO_SIGN_PREFIX_SYSTEM) || - signHeaderPrefixList.stream().anyMatch(headerName::equalsIgnoreCase)); - } - - private static String getHeaderOrEmpty(Map headers, String key) { - return headers.getOrDefault(key, ""); - } - - public static void ArrayIndexOutOfBoundsExample(String[] args) { - String[] array = { "Apple", "Banana", "Cherry" }; - System.out.println(array[3]); // ArrayIndexOutOfBoundsException - } - - public static void NullPointerExceptionExample(String[] args) { - String str = null; - System.out.println(str.length()); // NullPointerException - } - - public static void InfiniteLoopExample(String[] args) { - int count = 0; - while (count >= 0) { // Infinite loop - System.out.println("Looping..."); - count++; + if (StringUtils.isBlank(headerName)) { + return false; } - } - public static void MemoryLeakExample(String[] args) { - List list = new ArrayList<>(); - while (true) { - list.add("A new object"); - } + // 系统请求头或匹配前缀列表的请求头需要签名 + return headerName.startsWith(Constants.CA_HEADER_TO_SIGN_PREFIX_SYSTEM) || + signHeaderPrefixList.stream().anyMatch(headerName::equalsIgnoreCase); } - public static void WrongThreadPoolUsageExample(String[] args) { - // ❌ 错误用法:使用 Executors.newCachedThreadPool() - // 该线程池会无限创建线程,在高并发场景下容易 OOM - ExecutorService executor = Executors.newCachedThreadPool(); - - // ❌ 提交过多任务,任务中还有阻塞操作 - for (int i = 0; i < 100000; i++) { - final int taskId = i; - executor.submit(() -> { - try { - // 模拟长时间阻塞 - Thread.sleep(10000); - System.out.println("Task " + taskId + " finished."); - } catch (InterruptedException e) { - e.printStackTrace(); - } - }); + /** + * 获取请求头的值,不存在时返回空字符串 + */ + private static String getHeaderOrEmpty(Map headers, String key) { + return headers.getOrDefault(key, ""); } - - // ❌ 忘记调用 shutdown() 或 shutdownNow() - // 线程池将一直运行,导致进程无法正常退出 - // executor.shutdown(); - - // ❌ 在主线程中直接退出可能导致部分任务丢失 - System.out.println("Main thread finished, but thread pool still running..."); -} - } \ No newline at end of file