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