From 9e1618a104e1c5b67c651fb0dd0c1ff2797b3701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A6=82=E6=A2=A6=E6=8A=80=E6=9C=AF?= <596392912@qq.com> Date: Mon, 17 Nov 2025 11:29:14 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(http):=20=E6=96=B0=E5=A2=9EHTTP?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=AF=BC=E5=85=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了HttpFileParser类,用于解析.http文件格式 - 添加了对REST Client格式的完整支持,包括请求分隔符、HTTP方法、URL、请求头和请求体 - 支持多种HTTP方法(GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS) - 实现了Content-Type识别和不同body类型的处理(raw、urlencoded) - 支持Basic和Bearer认证方式的解析 - 添加了对SSE和WebSocket协议的自动识别 - 在左侧集合面板中增加了HTTP文件导入菜单项 - 实现了文件选择对话框,限制只显示.http文件 - 添加了完整的单元测试覆盖各种解析场景 - 增加了国际化消息键值和英文翻译 - 升级 lombok.version 到 1.18.42,支持高版本 jdk --- pom.xml | 3 +- .../panel/collections/left/LeftTopPanel.java | 46 ++ .../service/httpfile/HttpFileParser.java | 317 ++++++++++ .../com/laker/postman/util/MessageKeys.java | 5 +- src/main/resources/messages_en.properties | 3 + src/main/resources/messages_zh.properties | 3 + .../service/httpfile/HttpFileParserTest.java | 586 ++++++++++++++++++ 7 files changed, 961 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/laker/postman/service/httpfile/HttpFileParser.java create mode 100644 src/test/java/com/laker/postman/service/httpfile/HttpFileParserTest.java diff --git a/pom.xml b/pom.xml index 0a223be0..0ed0834f 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,7 @@ 4.12.0 24.2.1 7.3.0.202506031305-r + 1.18.42 @@ -72,7 +73,7 @@ org.projectlombok lombok - 1.18.34 + ${lombok.version} provided diff --git a/src/main/java/com/laker/postman/panel/collections/left/LeftTopPanel.java b/src/main/java/com/laker/postman/panel/collections/left/LeftTopPanel.java index 4aeca2d8..c6e09813 100644 --- a/src/main/java/com/laker/postman/panel/collections/left/LeftTopPanel.java +++ b/src/main/java/com/laker/postman/panel/collections/left/LeftTopPanel.java @@ -17,6 +17,7 @@ import com.laker.postman.service.curl.CurlParser; import com.laker.postman.service.har.HarParser; import com.laker.postman.service.http.HttpUtil; +import com.laker.postman.service.httpfile.HttpFileParser; import com.laker.postman.service.postman.PostmanCollectionParser; import com.laker.postman.util.I18nUtil; import com.laker.postman.util.MessageKeys; @@ -149,12 +150,16 @@ private JPopupMenu getImportMenu() { JMenuItem importHarItem = new JMenuItem(I18nUtil.getMessage(MessageKeys.COLLECTIONS_IMPORT_HAR), new FlatSVGIcon("icons/har.svg", 20, 20)); importHarItem.addActionListener(e -> importHarCollection()); + JMenuItem importHttpItem = new JMenuItem(I18nUtil.getMessage(MessageKeys.COLLECTIONS_IMPORT_HTTP), + new FlatSVGIcon("icons/http.svg", 20, 20)); + importHttpItem.addActionListener(e -> importHttpFile()); JMenuItem importCurlItem = new JMenuItem(I18nUtil.getMessage(MessageKeys.COLLECTIONS_IMPORT_CURL), new FlatSVGIcon("icons/curl.svg", 20, 20)); importCurlItem.addActionListener(e -> importCurlToCollection(null)); importMenu.add(importEasyToolsItem); importMenu.add(importPostmanItem); importMenu.add(importHarItem); + importMenu.add(importHttpItem); importMenu.add(importCurlItem); return importMenu; } @@ -254,6 +259,47 @@ private void importHarCollection() { } } + // 导入HTTP文件 + private void importHttpFile() { + RequestCollectionsLeftPanel leftPanel = SingletonFactory.getInstance(RequestCollectionsLeftPanel.class); + MainFrame mainFrame = SingletonFactory.getInstance(MainFrame.class); + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setDialogTitle(I18nUtil.getMessage(MessageKeys.COLLECTIONS_IMPORT_HTTP_DIALOG_TITLE)); + // 设置文件过滤器,只显示 .http 文件 + javax.swing.filechooser.FileFilter httpFilter = new javax.swing.filechooser.FileFilter() { + @Override + public boolean accept(File f) { + return f.isDirectory() || f.getName().toLowerCase().endsWith(".http"); + } + + @Override + public String getDescription() { + return "HTTP Files (*.http)"; + } + }; + fileChooser.setFileFilter(httpFilter); + int userSelection = fileChooser.showOpenDialog(mainFrame); + if (userSelection == JFileChooser.APPROVE_OPTION) { + File fileToOpen = fileChooser.getSelectedFile(); + try { + String content = FileUtil.readString(fileToOpen, StandardCharsets.UTF_8); + DefaultMutableTreeNode collectionNode = HttpFileParser.parseHttpFile(content); + if (collectionNode != null) { + leftPanel.getRootTreeNode().add(collectionNode); + leftPanel.getTreeModel().reload(); + leftPanel.getPersistence().saveRequestGroups(); + leftPanel.getRequestTree().expandPath(new TreePath(collectionNode.getPath())); + NotificationUtil.showSuccess(I18nUtil.getMessage(MessageKeys.COLLECTIONS_IMPORT_SUCCESS)); + } else { + NotificationUtil.showError(I18nUtil.getMessage(MessageKeys.COLLECTIONS_IMPORT_HTTP_INVALID)); + } + } catch (Exception ex) { + log.error("Import HTTP file error", ex); + NotificationUtil.showError(I18nUtil.getMessage(MessageKeys.COLLECTIONS_IMPORT_FAIL, ex.getMessage())); + } + } + } + private void importCurlToCollection(String defaultCurl) { MainFrame mainFrame = SingletonFactory.getInstance(MainFrame.class); String curlText; diff --git a/src/main/java/com/laker/postman/service/httpfile/HttpFileParser.java b/src/main/java/com/laker/postman/service/httpfile/HttpFileParser.java new file mode 100644 index 00000000..0ef3276e --- /dev/null +++ b/src/main/java/com/laker/postman/service/httpfile/HttpFileParser.java @@ -0,0 +1,317 @@ +package com.laker.postman.service.httpfile; + +import com.laker.postman.model.*; +import com.laker.postman.service.http.HttpUtil; +import lombok.extern.slf4j.Slf4j; + +import javax.swing.tree.DefaultMutableTreeNode; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.laker.postman.panel.collections.right.request.sub.AuthTabPanel.*; + +/** + * HTTP 文件解析器 + * 负责解析 .http 文件格式(REST Client 格式),转换为内部数据结构 + */ +@Slf4j +public class HttpFileParser { + // 常量定义 + private static final String GROUP = "group"; + private static final String REQUEST = "request"; + + // 匹配请求分隔符:### 开头的注释 + private static final Pattern REQUEST_SEPARATOR_PATTERN = Pattern.compile("^###\\s*(.+)$"); + // 匹配 HTTP 方法 + URL:GET/POST/PUT/DELETE/PATCH + URL + private static final Pattern HTTP_METHOD_PATTERN = Pattern.compile("^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\\s+(.+)$"); + // 匹配头部:Key: Value + private static final Pattern HEADER_PATTERN = Pattern.compile("^([^:]+):\\s*(.+)$"); + + /** + * 私有构造函数,防止实例化 + */ + private HttpFileParser() { + throw new UnsupportedOperationException("Utility class cannot be instantiated"); + } + + /** + * 解析 HTTP 文件,返回根节点 + * + * @param content HTTP 文件内容 + * @return 集合根节点,如果解析失败返回 null + */ + public static DefaultMutableTreeNode parseHttpFile(String content) { + try { + if (content == null || content.trim().isEmpty()) { + log.error("HTTP文件内容为空"); + return null; + } + + // 创建分组节点 + String groupName = "HTTP Import " + System.currentTimeMillis(); + RequestGroup collectionGroup = new RequestGroup(groupName); + DefaultMutableTreeNode collectionNode = new DefaultMutableTreeNode(new Object[]{GROUP, collectionGroup}); + + // 按行分割 + String[] lines = content.split("\n"); + List requests = parseHttpRequests(lines); + + // 将解析的请求添加到树节点 + for (HttpRequestItem request : requests) { + if (request != null && request.getUrl() != null && !request.getUrl().isEmpty()) { + DefaultMutableTreeNode requestNode = new DefaultMutableTreeNode(new Object[]{REQUEST, request}); + collectionNode.add(requestNode); + } + } + + if (collectionNode.getChildCount() == 0) { + log.warn("HTTP文件中没有解析到有效的请求"); + return null; + } + + return collectionNode; + } catch (Exception e) { + log.error("解析HTTP文件失败", e); + return null; + } + } + + /** + * 解析 HTTP 请求列表 + */ + private static List parseHttpRequests(String[] lines) { + List requests = new ArrayList<>(); + HttpRequestItem currentRequest = null; + StringBuilder bodyBuilder = null; + boolean inBody = false; + String contentType = null; + + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + String trimmedLine = line.trim(); + + // 跳过空行 + if (trimmedLine.isEmpty()) { + if (inBody && bodyBuilder != null) { + // 空行可能是 body 的一部分 + bodyBuilder.append("\n"); + } + continue; + } + + // 检查是否是请求分隔符(### 开头的注释) + Matcher separatorMatcher = REQUEST_SEPARATOR_PATTERN.matcher(trimmedLine); + if (separatorMatcher.matches()) { + // 保存上一个请求 + if (currentRequest != null) { + finishRequest(currentRequest, bodyBuilder, contentType); + requests.add(currentRequest); + } + // 开始新请求 + String requestName = separatorMatcher.group(1).trim(); + currentRequest = new HttpRequestItem(); + currentRequest.setId(UUID.randomUUID().toString()); + currentRequest.setName(requestName.isEmpty() ? "未命名请求" : requestName); + bodyBuilder = new StringBuilder(); + inBody = false; + contentType = null; + continue; + } + + // 检查是否是 HTTP 方法行(如果没有当前请求,自动创建一个) + Matcher methodMatcher = HTTP_METHOD_PATTERN.matcher(trimmedLine); + if (methodMatcher.matches()) { + // 如果还没有当前请求,先创建一个(支持没有 ### 分隔符的情况) + if (currentRequest == null) { + currentRequest = new HttpRequestItem(); + currentRequest.setId(UUID.randomUUID().toString()); + currentRequest.setName("未命名请求"); + bodyBuilder = new StringBuilder(); + inBody = false; + contentType = null; + } + String method = methodMatcher.group(1).toUpperCase(); + String url = methodMatcher.group(2).trim(); + currentRequest.setMethod(method); + currentRequest.setUrl(url); + continue; + } + + // 如果没有当前请求,跳过 + if (currentRequest == null) { + continue; + } + + // 检查是否是 Content-Type 头部(可能影响 body 解析) + if (trimmedLine.toLowerCase().startsWith("content-type:")) { + Matcher contentTypeMatcher = HEADER_PATTERN.matcher(trimmedLine); + if (contentTypeMatcher.matches()) { + contentType = contentTypeMatcher.group(2).trim().toLowerCase(); + if (currentRequest.getHeadersList() == null) { + currentRequest.setHeadersList(new ArrayList<>()); + } + currentRequest.getHeadersList().add(new HttpHeader(true, "Content-Type", contentTypeMatcher.group(2).trim())); + continue; + } + } + + // 检查是否是头部行 + Matcher headerMatcher = HEADER_PATTERN.matcher(trimmedLine); + if (headerMatcher.matches() && !inBody) { + String headerName = headerMatcher.group(1).trim(); + String headerValue = headerMatcher.group(2).trim(); + // 处理 Authorization 头部 + if ("Authorization".equalsIgnoreCase(headerName)) { + parseAuthorization(currentRequest, headerValue); + } else { + // 添加到头部列表 + if (currentRequest.getHeadersList() == null) { + currentRequest.setHeadersList(new ArrayList<>()); + } + currentRequest.getHeadersList().add(new HttpHeader(true, headerName, headerValue)); + } + continue; + } + + // 其他情况视为 body 内容 + if (currentRequest.getMethod() != null && + ("POST".equals(currentRequest.getMethod()) || + "PUT".equals(currentRequest.getMethod()) || + "PATCH".equals(currentRequest.getMethod()) || + "DELETE".equals(currentRequest.getMethod()))) { + inBody = true; + if (bodyBuilder != null && !bodyBuilder.isEmpty()) { + bodyBuilder.append("\n"); + } + if (bodyBuilder == null) { + bodyBuilder = new StringBuilder(); + } + bodyBuilder.append(line); + } + } + + // 保存最后一个请求 + if (currentRequest != null) { + finishRequest(currentRequest, bodyBuilder, contentType); + requests.add(currentRequest); + } + + return requests; + } + + /** + * 完成请求解析,设置 body 和其他属性 + */ + private static void finishRequest(HttpRequestItem request, StringBuilder bodyBuilder, String contentType) { + // 设置协议类型 + if (HttpUtil.isSSERequest(request.getHeaders())) { + request.setProtocol(RequestItemProtocolEnum.SSE); + } else if (HttpUtil.isWebSocketRequest(request.getUrl())) { + request.setProtocol(RequestItemProtocolEnum.WEBSOCKET); + } else { + request.setProtocol(RequestItemProtocolEnum.HTTP); + } + + // 处理 body + if (bodyBuilder != null && !bodyBuilder.isEmpty()) { + String body = bodyBuilder.toString().trim(); + if (!body.isEmpty()) { + // 根据 Content-Type 处理 body + if (contentType != null && contentType.contains("application/x-www-form-urlencoded")) { + // 解析 urlencoded 格式 + List urlencodedList = new ArrayList<>(); + String[] pairs = body.split("&"); + for (String pair : pairs) { + String[] kv = pair.split("=", 2); + String name = kv.length > 0 ? kv[0].trim() : ""; + String value = kv.length > 1 ? kv[1].trim() : ""; + if (!name.isEmpty()) { + urlencodedList.add(new HttpFormUrlencoded(true, name, value)); + } + } + request.setUrlencodedList(urlencodedList); + request.setBodyType("urlencoded"); + } else { + // 默认作为 raw body + request.setBody(body); + request.setBodyType("raw"); + } + } + } + + // 如果没有设置名称,使用 URL + if (request.getName() == null || request.getName().isEmpty() || "未命名请求".equals(request.getName())) { + try { + java.net.URI uri = new java.net.URI(request.getUrl()); + String path = uri.getPath(); + if (path != null && !path.isEmpty()) { + String[] parts = path.split("/"); + String name = parts[parts.length - 1]; + if (name.isEmpty() && parts.length > 1) { + name = parts[parts.length - 2]; + } + request.setName(name.isEmpty() ? request.getUrl() : name); + } else { + request.setName(request.getUrl()); + } + } catch (Exception e) { + request.setName(request.getUrl()); + } + } + } + + /** + * 解析 Authorization 头部 + */ + private static void parseAuthorization(HttpRequestItem request, String authValue) { + if (authValue.startsWith("Basic ")) { + request.setAuthType(AUTH_TYPE_BASIC); + String credentials = authValue.substring(6).trim(); + // 检查是否是变量占位符格式:Basic {{username}} {{password}} + if (credentials.contains("{{") && credentials.contains("}}")) { + // 变量格式,尝试提取用户名和密码变量 + // 格式可能是 "Basic {{username}} {{password}}" 或 "Basic {{username}}:{{password}}" + String[] parts = credentials.split("\\s+"); + if (parts.length >= 2) { + // 格式:Basic {{username}} {{password}} + request.setAuthUsername(parts[0]); + request.setAuthPassword(parts[1]); + } else { + // 格式:Basic {{username}}:{{password}} + int colonIndex = credentials.indexOf(':'); + if (colonIndex > 0) { + request.setAuthUsername(credentials.substring(0, colonIndex)); + request.setAuthPassword(credentials.substring(colonIndex + 1)); + } else { + request.setAuthUsername(credentials); + request.setAuthPassword(""); + } + } + } else { + // Base64 编码格式 + try { + String decoded = new String(Base64.getDecoder().decode(credentials)); + String[] parts = decoded.split(":", 2); + if (parts.length == 2) { + request.setAuthUsername(parts[0]); + request.setAuthPassword(parts[1]); + } + } catch (Exception e) { + log.warn("解析 Basic 认证失败,可能是变量格式", e); + // 如果解析失败,可能是变量格式,直接设置 + request.setAuthUsername(credentials); + request.setAuthPassword(""); + } + } + } else if (authValue.startsWith("Bearer ")) { + request.setAuthType(AUTH_TYPE_BEARER); + request.setAuthToken(authValue.substring(7)); + } + } + +} + diff --git a/src/main/java/com/laker/postman/util/MessageKeys.java b/src/main/java/com/laker/postman/util/MessageKeys.java index fe2d9712..fa78e0f3 100644 --- a/src/main/java/com/laker/postman/util/MessageKeys.java +++ b/src/main/java/com/laker/postman/util/MessageKeys.java @@ -470,8 +470,9 @@ private MessageKeys() { public static final String COLLECTIONS_IMPORT_CURL_TITLE = "collections.import.curl.title"; public static final String COLLECTIONS_IMPORT_EASY = "collections.import.easy"; public static final String COLLECTIONS_IMPORT_POSTMAN = "collections.import.postman"; - public static final String COLLECTIONS_IMPORT_HAR = "collections.import.har"; public static final String COLLECTIONS_IMPORT_CURL = "collections.import.curl"; + public static final String COLLECTIONS_IMPORT_HAR = "collections.import.har"; + public static final String COLLECTIONS_IMPORT_HTTP = "collections.import.http"; // ============ 集合菜单相关 ============ public static final String COLLECTIONS_MENU_ADD_GROUP = "collections.menu.add_group"; @@ -508,6 +509,8 @@ private MessageKeys() { public static final String COLLECTIONS_IMPORT_CURL_DIALOG_PROMPT = "collections.import.curl.dialog_prompt"; public static final String COLLECTIONS_IMPORT_CURL_PARSE_FAIL = "collections.import.curl.parse_fail"; public static final String COLLECTIONS_IMPORT_CURL_PARSE_ERROR = "collections.import.curl.parse_error"; + public static final String COLLECTIONS_IMPORT_HTTP_DIALOG_TITLE = "collections.import.http.dialog_title"; + public static final String COLLECTIONS_IMPORT_HTTP_INVALID = "collections.import.http.invalid"; // ============ 集合对话框相关 ============ public static final String COLLECTIONS_DIALOG_ADD_GROUP_PROMPT = "collections.dialog.add_group.prompt"; diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index b29f3f12..69f7da78 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -362,6 +362,7 @@ collections.import.curl.title=Import cURL collections.import.easy=Import from EasyPostman collections.import.postman=Import from Postman v2.1 collections.import.har=Import from HAR file +collections.import.http=Import from HTTP File collections.import.curl=Import from cURL collections.menu.add_group=Add Group collections.menu.add_root_group=Add Root Group @@ -395,6 +396,8 @@ collections.import.curl.dialog_title=Import from cURL collections.import.curl.dialog_prompt=Please enter cURL command: collections.import.curl.parse_fail=Failed to parse cURL command collections.import.curl.parse_error=Failed to parse cURL: {0} +collections.import.http.dialog_title=Import from HTTP File +collections.import.http.invalid=Not a valid HTTP file collections.dialog.add_group.prompt=Please enter collection name: collections.dialog.add_request.title=Add New Request collections.dialog.add_request.name=Request Name: diff --git a/src/main/resources/messages_zh.properties b/src/main/resources/messages_zh.properties index 97fdf816..6cdb4bd9 100644 --- a/src/main/resources/messages_zh.properties +++ b/src/main/resources/messages_zh.properties @@ -362,6 +362,7 @@ collections.import.curl.title=导入cURL collections.import.easy=从EasyPostman导入 collections.import.postman=从Postman v2.1导入 collections.import.har=从HAR文件导入 +collections.import.http=从HTTP文件导入 collections.import.curl=从cURL导入 collections.menu.add_group=新增分组 collections.menu.add_root_group=新增Root分组 @@ -395,6 +396,8 @@ collections.import.curl.dialog_title=从cURL导入 collections.import.curl.dialog_prompt=请输入cURL命令: collections.import.curl.parse_fail=无法解析cURL命令 collections.import.curl.parse_error=解析cURL出错: {0} +collections.import.http.dialog_title=从HTTP文件导入 +collections.import.http.invalid=不是有效的HTTP文件 collections.dialog.add_group.prompt=请输入集合名称: collections.dialog.add_request.title=新增请求 collections.dialog.add_request.name=请求名称: diff --git a/src/test/java/com/laker/postman/service/httpfile/HttpFileParserTest.java b/src/test/java/com/laker/postman/service/httpfile/HttpFileParserTest.java new file mode 100644 index 00000000..972bf7c0 --- /dev/null +++ b/src/test/java/com/laker/postman/service/httpfile/HttpFileParserTest.java @@ -0,0 +1,586 @@ +package com.laker.postman.service.httpfile; + +import java.util.Base64; + +import javax.swing.tree.DefaultMutableTreeNode; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; +import org.testng.annotations.Test; + +import com.laker.postman.model.HttpRequestItem; +import com.laker.postman.model.RequestGroup; +import com.laker.postman.model.RequestItemProtocolEnum; +import static com.laker.postman.panel.collections.right.request.sub.AuthTabPanel.AUTH_TYPE_BASIC; +import static com.laker.postman.panel.collections.right.request.sub.AuthTabPanel.AUTH_TYPE_BEARER; + +/** + * HttpFileParser 单元测试类 + * 测试 HTTP 文件解析器的各种功能 + */ +public class HttpFileParserTest { + + @Test(description = "测试解析空内容") + public void testParseEmptyContent() { + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(null); + assertNull(result, "null 内容应该返回 null"); + + result = HttpFileParser.parseHttpFile(""); + assertNull(result, "空字符串应该返回 null"); + + result = HttpFileParser.parseHttpFile(" "); + assertNull(result, "空白字符串应该返回 null"); + } + + @Test(description = "测试解析简单的 GET 请求") + public void testParseSimpleGetRequest() { + String content = "GET https://api.example.com/users"; + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + + assertNotNull(result, "应该成功解析"); + assertEquals(result.getChildCount(), 1, "应该有一个请求节点"); + + DefaultMutableTreeNode requestNode = (DefaultMutableTreeNode) result.getChildAt(0); + Object[] userObject = (Object[]) requestNode.getUserObject(); + assertEquals(userObject[0], "request", "节点类型应该是 request"); + + HttpRequestItem request = (HttpRequestItem) userObject[1]; + assertEquals(request.getMethod(), "GET"); + assertEquals(request.getUrl(), "https://api.example.com/users"); + assertNotNull(request.getName(), "应该有请求名称"); + } + + @Test(description = "测试解析带名称的请求") + public void testParseRequestWithName() { + String content = """ + ### 获取用户列表 + GET https://api.example.com/users + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + assertEquals(result.getChildCount(), 1); + + DefaultMutableTreeNode requestNode = (DefaultMutableTreeNode) result.getChildAt(0); + HttpRequestItem request = (HttpRequestItem) ((Object[]) requestNode.getUserObject())[1]; + assertEquals(request.getName(), "获取用户列表"); + assertEquals(request.getMethod(), "GET"); + assertEquals(request.getUrl(), "https://api.example.com/users"); + } + + @Test(description = "测试解析多个请求") + public void testParseMultipleRequests() { + String content = """ + ### 获取用户列表 + GET https://api.example.com/users + + ### 创建用户 + POST https://api.example.com/users + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + assertEquals(result.getChildCount(), 2, "应该有两个请求节点"); + + // 验证第一个请求 + DefaultMutableTreeNode requestNode1 = (DefaultMutableTreeNode) result.getChildAt(0); + HttpRequestItem request1 = (HttpRequestItem) ((Object[]) requestNode1.getUserObject())[1]; + assertEquals(request1.getName(), "获取用户列表"); + assertEquals(request1.getMethod(), "GET"); + + // 验证第二个请求 + DefaultMutableTreeNode requestNode2 = (DefaultMutableTreeNode) result.getChildAt(1); + HttpRequestItem request2 = (HttpRequestItem) ((Object[]) requestNode2.getUserObject())[1]; + assertEquals(request2.getName(), "创建用户"); + assertEquals(request2.getMethod(), "POST"); + } + + @Test(description = "测试解析 POST 请求带 JSON body") + public void testParsePostRequestWithJsonBody() { + String content = """ + ### 创建用户 + POST https://api.example.com/users + Content-Type: application/json + + { + "name": "John Doe", + "email": "john@example.com" + } + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + assertEquals(result.getChildCount(), 1); + + HttpRequestItem request = getRequestFromNode(result, 0); + assertEquals(request.getMethod(), "POST"); + assertEquals(request.getUrl(), "https://api.example.com/users"); + assertEquals(request.getBodyType(), "raw"); + assertTrue(request.getBody().contains("John Doe")); + assertTrue(request.getBody().contains("john@example.com")); + + // 验证 Content-Type 头部 + assertNotNull(request.getHeadersList()); + boolean hasContentType = request.getHeadersList().stream() + .anyMatch(h -> "Content-Type".equals(h.getKey()) && + h.getValue().contains("application/json")); + assertTrue(hasContentType, "应该有 Content-Type 头部"); + } + + @Test(description = "测试解析带多个请求头的请求") + public void testParseRequestWithMultipleHeaders() { + String content = """ + ### API 请求 + GET https://api.example.com/data + Accept: application/json + User-Agent: EasyPostman/1.0 + X-Custom-Header: custom-value + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + + HttpRequestItem request = getRequestFromNode(result, 0); + assertNotNull(request.getHeadersList()); + assertEquals(request.getHeadersList().size(), 3, "应该有 3 个请求头"); + + // 验证请求头 + assertTrue(hasHeader(request, "Accept", "application/json")); + assertTrue(hasHeader(request, "User-Agent", "EasyPostman/1.0")); + assertTrue(hasHeader(request, "X-Custom-Header", "custom-value")); + } + + @Test(description = "测试解析 Basic 认证") + public void testParseBasicAuth() { + String content = """ + ### 需要认证的请求 + GET https://api.example.com/protected + Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + + HttpRequestItem request = getRequestFromNode(result, 0); + assertEquals(request.getAuthType(), AUTH_TYPE_BASIC); + + // 验证 Base64 解码 + String decoded = new String(Base64.getDecoder().decode("dXNlcm5hbWU6cGFzc3dvcmQ=")); + String[] parts = decoded.split(":", 2); + assertEquals(request.getAuthUsername(), parts[0]); + assertEquals(request.getAuthPassword(), parts[1]); + } + + @Test(description = "测试解析 Basic 认证(变量格式)") + public void testParseBasicAuthWithVariables() { + String content = """ + ### 需要认证的请求 + GET https://api.example.com/protected + Authorization: Basic {{username}} {{password}} + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + + HttpRequestItem request = getRequestFromNode(result, 0); + assertEquals(request.getAuthType(), AUTH_TYPE_BASIC); + assertEquals(request.getAuthUsername(), "{{username}}"); + assertEquals(request.getAuthPassword(), "{{password}}"); + } + + @Test(description = "测试解析 Bearer 认证") + public void testParseBearerAuth() { + String content = """ + ### 需要 Bearer Token 的请求 + GET https://api.example.com/protected + Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + + HttpRequestItem request = getRequestFromNode(result, 0); + assertEquals(request.getAuthType(), AUTH_TYPE_BEARER); + assertEquals(request.getAuthToken(), "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"); + } + + @Test(description = "测试解析 urlencoded body") + public void testParseUrlencodedBody() { + String content = """ + ### 提交表单 + POST https://api.example.com/submit + Content-Type: application/x-www-form-urlencoded + + name=John+Doe&email=john%40example.com&age=30 + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + + HttpRequestItem request = getRequestFromNode(result, 0); + assertEquals(request.getMethod(), "POST"); + assertEquals(request.getBodyType(), "urlencoded"); + assertNotNull(request.getUrlencodedList()); + assertEquals(request.getUrlencodedList().size(), 3, "应该有 3 个 urlencoded 字段"); + + // 验证字段值 + assertTrue(hasUrlencodedField(request, "name", "John+Doe")); + assertTrue(hasUrlencodedField(request, "email", "john%40example.com")); + assertTrue(hasUrlencodedField(request, "age", "30")); + } + + @Test(description = "测试解析各种 HTTP 方法") + public void testParseVariousHttpMethods() { + String content = """ + ### GET 请求 + GET https://api.example.com/get + + ### POST 请求 + POST https://api.example.com/post + + ### PUT 请求 + PUT https://api.example.com/put + + ### DELETE 请求 + DELETE https://api.example.com/delete + + ### PATCH 请求 + PATCH https://api.example.com/patch + + ### HEAD 请求 + HEAD https://api.example.com/head + + ### OPTIONS 请求 + OPTIONS https://api.example.com/options + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + assertEquals(result.getChildCount(), 7, "应该有 7 个请求"); + + assertEquals(getRequestFromNode(result, 0).getMethod(), "GET"); + assertEquals(getRequestFromNode(result, 1).getMethod(), "POST"); + assertEquals(getRequestFromNode(result, 2).getMethod(), "PUT"); + assertEquals(getRequestFromNode(result, 3).getMethod(), "DELETE"); + assertEquals(getRequestFromNode(result, 4).getMethod(), "PATCH"); + assertEquals(getRequestFromNode(result, 5).getMethod(), "HEAD"); + assertEquals(getRequestFromNode(result, 6).getMethod(), "OPTIONS"); + } + + @Test(description = "测试解析 POST 请求带多行 body") + public void testParsePostRequestWithMultilineBody() { + String content = """ + ### 创建复杂对象 + POST https://api.example.com/complex + Content-Type: application/json + + { + "name": "John", + "address": { + "street": "123 Main St", + "city": "New York" + }, + "tags": ["tag1", "tag2"] + } + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + + HttpRequestItem request = getRequestFromNode(result, 0); + assertEquals(request.getMethod(), "POST"); + assertEquals(request.getBodyType(), "raw"); + assertNotNull(request.getBody()); + assertTrue(request.getBody().contains("\"name\": \"John\"")); + assertTrue(request.getBody().contains("\"street\": \"123 Main St\"")); + assertTrue(request.getBody().contains("\"tags\":")); + } + + @Test(description = "测试解析没有名称的请求(使用 URL 作为名称)") + public void testParseRequestWithoutName() { + String content = """ + ### + GET https://api.example.com/users/profile + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + + HttpRequestItem request = getRequestFromNode(result, 0); + // 如果没有名称,应该使用 URL 的路径部分作为名称 + assertNotNull(request.getName()); + assertFalse(request.getName().isEmpty()); + } + + @Test(description = "测试解析 SSE 请求") + public void testParseSSERequest() { + String content = """ + ### SSE 流式请求 + GET https://api.example.com/events + Accept: text/event-stream + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + + HttpRequestItem request = getRequestFromNode(result, 0); + assertEquals(request.getProtocol(), RequestItemProtocolEnum.SSE); + assertTrue(hasHeader(request, "Accept", "text/event-stream")); + } + + @Test(description = "测试解析 WebSocket 请求") + public void testParseWebSocketRequest() { + String content = """ + ### WebSocket 连接 + GET wss://echo-websocket.hoppscotch.io/ + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + + HttpRequestItem request = getRequestFromNode(result, 0); + assertEquals(request.getProtocol(), RequestItemProtocolEnum.WEBSOCKET); + assertEquals(request.getUrl(), "wss://echo-websocket.hoppscotch.io/"); + } + + @Test(description = "测试解析包含空行的请求") + public void testParseRequestWithEmptyLines() { + String content = """ + ### 带空行的请求 + POST https://api.example.com/data + Content-Type: application/json + + + { + "data": "value" + } + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + + HttpRequestItem request = getRequestFromNode(result, 0); + assertEquals(request.getMethod(), "POST"); + assertNotNull(request.getBody()); + assertTrue(request.getBody().contains("\"data\": \"value\"")); + } + + @Test(description = "测试解析没有分隔符的单个请求") + public void testParseSingleRequestWithoutSeparator() { + String content = "GET https://api.example.com/users"; + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + + assertNotNull(result); + assertEquals(result.getChildCount(), 1); + + HttpRequestItem request = getRequestFromNode(result, 0); + assertEquals(request.getMethod(), "GET"); + assertEquals(request.getUrl(), "https://api.example.com/users"); + } + + @Test(description = "测试解析带查询参数的 URL") + public void testParseRequestWithQueryParams() { + String content = """ + ### 搜索请求 + GET https://api.example.com/search?q=test&limit=10&page=1 + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + + HttpRequestItem request = getRequestFromNode(result, 0); + assertEquals(request.getUrl(), "https://api.example.com/search?q=test&limit=10&page=1"); + } + + @Test(description = "测试解析分组节点") + public void testParseGroupNode() { + String content = """ + ### 第一个请求 + GET https://api.example.com/one + + ### 第二个请求 + POST https://api.example.com/two + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + + // 验证根节点是分组节点 + Object[] rootUserObject = (Object[]) result.getUserObject(); + assertEquals(rootUserObject[0], "group"); + assertTrue(rootUserObject[1] instanceof RequestGroup); + + RequestGroup group = (RequestGroup) rootUserObject[1]; + assertNotNull(group.getName()); + assertTrue(group.getName().startsWith("HTTP Import")); + } + + @Test(description = "测试解析无效请求(没有 URL)") + public void testParseInvalidRequestWithoutUrl() { + String content = """ + ### 无效请求 + GET + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + // 没有有效 URL 的请求不应该被添加到结果中 + assertNull(result, "没有有效 URL 的请求应该返回 null"); + } + + @Test(description = "测试解析带特殊字符的请求头值") + public void testParseHeaderWithSpecialCharacters() { + String content = """ + ### 特殊字符测试 + GET https://api.example.com/test + X-Custom: value:with:colons + X-Another: value with spaces + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + + HttpRequestItem request = getRequestFromNode(result, 0); + assertTrue(hasHeader(request, "X-Custom", "value:with:colons")); + assertTrue(hasHeader(request, "X-Another", "value with spaces")); + } + + @Test(description = "测试解析 DELETE 请求带 body") + public void testParseDeleteRequestWithBody() { + String content = """ + ### 删除资源 + DELETE https://api.example.com/resource/123 + Content-Type: application/json + + { + "reason": "No longer needed" + } + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + + HttpRequestItem request = getRequestFromNode(result, 0); + assertEquals(request.getMethod(), "DELETE"); + assertNotNull(request.getBody()); + assertTrue(request.getBody().contains("\"reason\"")); + } + + @Test(description = "测试解析 PUT 请求") + public void testParsePutRequest() { + String content = """ + ### 更新资源 + PUT https://api.example.com/resource/123 + Content-Type: application/json + + { + "name": "Updated Name" + } + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + + HttpRequestItem request = getRequestFromNode(result, 0); + assertEquals(request.getMethod(), "PUT"); + assertEquals(request.getBodyType(), "raw"); + assertTrue(request.getBody().contains("Updated Name")); + } + + @Test(description = "测试解析 urlencoded body 带空值") + public void testParseUrlencodedWithEmptyValues() { + String content = """ + ### 表单提交 + POST https://api.example.com/form + Content-Type: application/x-www-form-urlencoded + + name=John&email=&age=30&empty= + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + + HttpRequestItem request = getRequestFromNode(result, 0); + assertEquals(request.getBodyType(), "urlencoded"); + assertTrue(hasUrlencodedField(request, "name", "John")); + assertTrue(hasUrlencodedField(request, "email", "")); + assertTrue(hasUrlencodedField(request, "age", "30")); + assertTrue(hasUrlencodedField(request, "empty", "")); + } + + @Test(description = "测试解析复杂场景:多个请求带各种配置") + public void testParseComplexScenario() { + String content = """ + ### 获取用户 + GET https://api.example.com/users + Accept: application/json + Authorization: Bearer token123 + + ### 创建用户 + POST https://api.example.com/users + Content-Type: application/json + Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= + + { + "name": "John", + "email": "john@example.com" + } + + ### 提交表单 + POST https://api.example.com/form + Content-Type: application/x-www-form-urlencoded + + name=John&email=john@example.com + """; + + DefaultMutableTreeNode result = HttpFileParser.parseHttpFile(content); + assertNotNull(result); + assertEquals(result.getChildCount(), 3, "应该有 3 个请求"); + + // 验证第一个请求(GET with Bearer) + HttpRequestItem req1 = getRequestFromNode(result, 0); + assertEquals(req1.getMethod(), "GET"); + assertEquals(req1.getAuthType(), AUTH_TYPE_BEARER); + assertEquals(req1.getAuthToken(), "token123"); + + // 验证第二个请求(POST with JSON and Basic Auth) + HttpRequestItem req2 = getRequestFromNode(result, 1); + assertEquals(req2.getMethod(), "POST"); + assertEquals(req2.getAuthType(), AUTH_TYPE_BASIC); + assertEquals(req2.getBodyType(), "raw"); + assertTrue(req2.getBody().contains("John")); + + // 验证第三个请求(POST with urlencoded) + HttpRequestItem req3 = getRequestFromNode(result, 2); + assertEquals(req3.getMethod(), "POST"); + assertEquals(req3.getBodyType(), "urlencoded"); + assertTrue(hasUrlencodedField(req3, "name", "John")); + } + + // 辅助方法:从节点获取 HttpRequestItem + private HttpRequestItem getRequestFromNode(DefaultMutableTreeNode root, int index) { + DefaultMutableTreeNode requestNode = (DefaultMutableTreeNode) root.getChildAt(index); + Object[] userObject = (Object[]) requestNode.getUserObject(); + return (HttpRequestItem) userObject[1]; + } + + // 辅助方法:检查请求是否有指定的请求头 + private boolean hasHeader(HttpRequestItem request, String key, String value) { + if (request.getHeadersList() == null) { + return false; + } + return request.getHeadersList().stream() + .anyMatch(h -> key.equals(h.getKey()) && value.equals(h.getValue())); + } + + // 辅助方法:检查请求是否有指定的 urlencoded 字段 + private boolean hasUrlencodedField(HttpRequestItem request, String key, String value) { + if (request.getUrlencodedList() == null) { + return false; + } + return request.getUrlencodedList().stream() + .anyMatch(f -> key.equals(f.getKey()) && value.equals(f.getValue())); + } +} From 05662db7b1948b6bb8048c3b1db44aa5e5c035b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A6=82=E6=A2=A6=E6=8A=80=E6=9C=AF?= <596392912@qq.com> Date: Wed, 19 Nov 2025 10:36:15 +0800 Subject: [PATCH 2/2] =?UTF-8?q?chore(deps):=20=E7=A7=BB=E9=99=A4=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E4=BD=BF=E7=94=A8=E5=88=B0=E7=9A=84=20hutool-http=20?= =?UTF-8?q?=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pom.xml b/pom.xml index 0ed0834f..5c42e2fa 100644 --- a/pom.xml +++ b/pom.xml @@ -59,11 +59,6 @@ hutool-crypto ${hutool.version} - - cn.hutool - hutool-http - ${hutool.version} - cn.hutool hutool-system