纯java编写
代码
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* A small Postman-like desktop HTTP client built with only Java Swing and JDK APIs.
*
* Compile:
* javac RequestUtilsApp.java
*
* Run:
* java RequestUtilsApp
*/
public class RequestUtilsApp extends JFrame {
private static final String[] METHODS = {"GET", "POST", "PUT", "DELETE"};
private static final String[][] COMMON_HEADER_PRESETS = {
{"Accept", "application/json"},
{"Content-Type", "application/json"},
{"Authorization", "Bearer "},
{"User-Agent", "RequestUtils/1.0"},
{"Cache-Control", "no-cache"},
{"Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"},
{"Cookie", ""},
{"X-Requested-With", "XMLHttpRequest"}
};
private static final int CONNECT_TIMEOUT_MS = 15000;
private static final int READ_TIMEOUT_MS = 30000;
private final JComboBox<String> methodCombo = new JComboBox<String>(METHODS);
private final JComboBox<String> commonHeaderCombo = new JComboBox<String>(commonHeaderNames());
private final JTextField headerNameField = new JTextField();
private final JTextField headerValueField = new JTextField();
private final JTextField urlField = new JTextField();
private final JTextArea headersArea = new JTextArea();
private final JTextArea bodyArea = new JTextArea();
private final JButton sendButton = new JButton("Send");
private final JButton applyHeaderButton = new JButton("Add/Update");
private final JButton importButton = new JButton("Import JSON");
private final JButton exportButton = new JButton("Export JSON");
private final JButton saveResponseButton = new JButton("Save Body");
private final JLabel statusLabel = new JLabel("Ready");
private final JTextArea responseHeadersArea = new JTextArea();
private final JTextArea responseBodyArea = new JTextArea();
private ResponseData latestResponse;
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
setSystemLookAndFeel();
RequestUtilsApp app = new RequestUtilsApp();
app.setVisible(true);
}
});
}
public RequestUtilsApp() {
super("RequestUtils - HTTP Debug Client");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setMinimumSize(new Dimension(960, 680));
setSize(1120, 760);
setLocationRelativeTo(null);
buildUi();
bindEvents();
}
private static String[] commonHeaderNames() {
String[] names = new String[COMMON_HEADER_PRESETS.length];
for (int i = 0; i < COMMON_HEADER_PRESETS.length; i++) {
names[i] = COMMON_HEADER_PRESETS[i][0];
}
return names;
}
private static void setSystemLookAndFeel() {
try {
for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
if ("Nimbus".equals(info.getName())) {
UIManager.setLookAndFeel(info.getClassName());
return;
}
}
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ignored) {
// Swing will keep the default look and feel if Nimbus is not available.
}
}
private void buildUi() {
JPanel root = new JPanel(new BorderLayout(0, 12));
root.setBorder(new EmptyBorder(14, 14, 14, 14));
root.setBackground(new Color(245, 247, 250));
setContentPane(root);
root.add(createRequestBar(), BorderLayout.NORTH);
JTabbedPane requestTabs = new JTabbedPane();
requestTabs.addTab("Headers", createHeadersPanel());
requestTabs.addTab("Request Body", createScrollPane(bodyArea));
JPanel requestPanel = wrapPanel("Request", requestTabs);
JPanel responsePanel = createResponsePanel();
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, requestPanel, responsePanel);
splitPane.setResizeWeight(0.42);
splitPane.setBorder(BorderFactory.createEmptyBorder());
splitPane.setDividerSize(8);
root.add(splitPane, BorderLayout.CENTER);
configureTextAreas();
loadDefaultRequest();
}
private JPanel createRequestBar() {
JPanel bar = new JPanel(new GridBagLayout());
bar.setOpaque(false);
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(0, 0, 0, 8);
c.gridy = 0;
c.fill = GridBagConstraints.HORIZONTAL;
JLabel methodLabel = new JLabel("Method");
methodLabel.setFont(methodLabel.getFont().deriveFont(Font.BOLD));
c.gridx = 0;
c.weightx = 0;
bar.add(methodLabel, c);
methodCombo.setPreferredSize(new Dimension(110, 34));
c.gridx = 1;
bar.add(methodCombo, c);
JLabel urlLabel = new JLabel("URL");
urlLabel.setFont(urlLabel.getFont().deriveFont(Font.BOLD));
c.gridx = 2;
bar.add(urlLabel, c);
urlField.setPreferredSize(new Dimension(300, 34));
urlField.setToolTipText("Example: https://api.example.com/users");
c.gridx = 3;
c.weightx = 1;
bar.add(urlField, c);
stylePrimaryButton(sendButton);
c.gridx = 4;
c.weightx = 0;
bar.add(sendButton, c);
styleSecondaryButton(importButton);
c.gridx = 5;
bar.add(importButton, c);
styleSecondaryButton(exportButton);
c.gridx = 6;
c.insets = new Insets(0, 0, 0, 0);
bar.add(exportButton, c);
return bar;
}
private JPanel createHeadersPanel() {
JPanel panel = new JPanel(new BorderLayout(0, 8));
panel.setOpaque(false);
JPanel editorBar = new JPanel(new GridBagLayout());
editorBar.setOpaque(false);
GridBagConstraints c = new GridBagConstraints();
c.gridy = 0;
c.fill = GridBagConstraints.HORIZONTAL;
c.insets = new Insets(0, 0, 0, 8);
JLabel presetLabel = new JLabel("Common");
presetLabel.setFont(presetLabel.getFont().deriveFont(Font.BOLD));
c.gridx = 0;
c.weightx = 0;
editorBar.add(presetLabel, c);
commonHeaderCombo.setPreferredSize(new Dimension(170, 32));
c.gridx = 1;
editorBar.add(commonHeaderCombo, c);
JLabel nameLabel = new JLabel("Name");
nameLabel.setFont(nameLabel.getFont().deriveFont(Font.BOLD));
c.gridx = 2;
editorBar.add(nameLabel, c);
headerNameField.setPreferredSize(new Dimension(170, 32));
c.gridx = 3;
c.weightx = 0.28;
editorBar.add(headerNameField, c);
JLabel valueLabel = new JLabel("Value");
valueLabel.setFont(valueLabel.getFont().deriveFont(Font.BOLD));
c.gridx = 4;
c.weightx = 0;
editorBar.add(valueLabel, c);
headerValueField.setPreferredSize(new Dimension(260, 32));
c.gridx = 5;
c.weightx = 0.72;
editorBar.add(headerValueField, c);
styleSecondaryButton(applyHeaderButton);
applyHeaderButton.setPreferredSize(new Dimension(120, 32));
c.gridx = 6;
c.weightx = 0;
c.insets = new Insets(0, 0, 0, 0);
editorBar.add(applyHeaderButton, c);
panel.add(editorBar, BorderLayout.NORTH);
panel.add(createScrollPane(headersArea), BorderLayout.CENTER);
return panel;
}
private JPanel createResponsePanel() {
JPanel panel = new JPanel(new BorderLayout(0, 8));
panel.setOpaque(false);
JPanel statusPanel = new JPanel(new BorderLayout());
statusPanel.setOpaque(false);
statusLabel.setFont(statusLabel.getFont().deriveFont(Font.BOLD, 14f));
statusLabel.setBorder(new EmptyBorder(0, 2, 0, 0));
statusPanel.add(statusLabel, BorderLayout.WEST);
styleSecondaryButton(saveResponseButton);
saveResponseButton.setEnabled(false);
statusPanel.add(saveResponseButton, BorderLayout.EAST);
JTabbedPane responseTabs = new JTabbedPane();
responseTabs.addTab("Response Headers", createScrollPane(responseHeadersArea));
responseTabs.addTab("Response Body", createScrollPane(responseBodyArea));
panel.add(statusPanel, BorderLayout.NORTH);
panel.add(responseTabs, BorderLayout.CENTER);
return wrapPanel("Response", panel);
}
private JPanel wrapPanel(String title, java.awt.Component content) {
JPanel panel = new JPanel(new BorderLayout());
panel.setBackground(Color.WHITE);
panel.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(218, 224, 232)),
new EmptyBorder(10, 10, 10, 10)
));
JLabel label = new JLabel(title);
label.setFont(label.getFont().deriveFont(Font.BOLD, 15f));
label.setBorder(new EmptyBorder(0, 2, 8, 0));
panel.add(label, BorderLayout.NORTH);
panel.add(content, BorderLayout.CENTER);
return panel;
}
private JScrollPane createScrollPane(JTextArea textArea) {
JScrollPane scrollPane = new JScrollPane(textArea);
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(224, 229, 236)));
return scrollPane;
}
private void configureTextAreas() {
Font editorFont = new Font(Font.MONOSPACED, Font.PLAIN, 13);
JTextArea[] areas = {headersArea, bodyArea, responseHeadersArea, responseBodyArea};
for (JTextArea area : areas) {
area.setFont(editorFont);
area.setLineWrap(true);
area.setWrapStyleWord(true);
area.setMargin(new Insets(10, 10, 10, 10));
area.setTabSize(2);
}
headersArea.setToolTipText("One header per line. Example: Authorization: Bearer token");
responseHeadersArea.setEditable(false);
responseBodyArea.setEditable(false);
responseHeadersArea.setBackground(new Color(249, 250, 252));
responseBodyArea.setBackground(new Color(249, 250, 252));
}
private void loadDefaultRequest() {
methodCombo.setSelectedItem("GET");
commonHeaderCombo.setSelectedItem("Accept");
fillHeaderEditorFromPreset();
urlField.setText("");
headersArea.setText("Accept: application/json\nContent-Type: application/json");
bodyArea.setText("{\n \"name\": \"demo\"\n}");
}
private void stylePrimaryButton(JButton button) {
button.setPreferredSize(new Dimension(92, 34));
button.setFocusPainted(false);
button.setForeground(Color.WHITE);
button.setBackground(new Color(31, 122, 224));
button.setBorder(BorderFactory.createEmptyBorder(8, 14, 8, 14));
button.setOpaque(true);
}
private void styleSecondaryButton(JButton button) {
button.setPreferredSize(new Dimension(118, 34));
button.setFocusPainted(false);
button.setBackground(Color.WHITE);
button.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(196, 205, 216)),
new EmptyBorder(7, 12, 7, 12)
));
}
private void bindEvents() {
commonHeaderCombo.addActionListener(new java.awt.event.ActionListener() {
@Override
public void actionPerformed(java.awt.event.ActionEvent e) {
fillHeaderEditorFromPreset();
}
});
applyHeaderButton.addActionListener(new java.awt.event.ActionListener() {
@Override
public void actionPerformed(java.awt.event.ActionEvent e) {
applyHeaderFromEditor();
}
});
sendButton.addActionListener(new java.awt.event.ActionListener() {
@Override
public void actionPerformed(java.awt.event.ActionEvent e) {
sendRequest();
}
});
exportButton.addActionListener(new java.awt.event.ActionListener() {
@Override
public void actionPerformed(java.awt.event.ActionEvent e) {
exportConfig();
}
});
importButton.addActionListener(new java.awt.event.ActionListener() {
@Override
public void actionPerformed(java.awt.event.ActionEvent e) {
importConfig();
}
});
saveResponseButton.addActionListener(new java.awt.event.ActionListener() {
@Override
public void actionPerformed(java.awt.event.ActionEvent e) {
saveResponseBody();
}
});
}
private void fillHeaderEditorFromPreset() {
String selected = String.valueOf(commonHeaderCombo.getSelectedItem());
for (int i = 0; i < COMMON_HEADER_PRESETS.length; i++) {
if (COMMON_HEADER_PRESETS[i][0].equals(selected)) {
headerNameField.setText(COMMON_HEADER_PRESETS[i][0]);
headerValueField.setText(COMMON_HEADER_PRESETS[i][1]);
return;
}
}
}
private void applyHeaderFromEditor() {
String name = headerNameField.getText().trim();
String value = headerValueField.getText().trim();
if (name.length() == 0) {
showError("Header name cannot be empty.");
return;
}
if (name.indexOf(':') >= 0 || name.indexOf('\n') >= 0 || name.indexOf('\r') >= 0) {
showError("Header name cannot contain colon or line breaks.");
return;
}
Map<String, String> headers;
try {
headers = parseHeaders(headersArea.getText());
} catch (IllegalArgumentException ex) {
showError(ex.getMessage());
return;
}
String existingKey = findHeaderKey(headers, name);
if (existingKey != null) {
headers.remove(existingKey);
}
headers.put(name, value);
headersArea.setText(formatRequestHeaders(headers));
}
private void sendRequest() {
String urlText = urlField.getText().trim();
if (urlText.length() == 0) {
showError("Please enter a request URL.");
return;
}
final String method = String.valueOf(methodCombo.getSelectedItem());
final Map<String, String> headers;
try {
headers = parseHeaders(headersArea.getText());
} catch (IllegalArgumentException ex) {
showError(ex.getMessage());
return;
}
final String body = bodyArea.getText();
setSending(true);
SwingWorker<ResponseData, Void> worker = new SwingWorker<ResponseData, Void>() {
@Override
protected ResponseData doInBackground() throws Exception {
return executeRequest(urlText, method, headers, body);
}
@Override
protected void done() {
try {
ResponseData response = get();
setSending(false);
showResponse(response);
} catch (Exception ex) {
setSending(false);
Throwable cause = ex.getCause() == null ? ex : ex.getCause();
statusLabel.setText("Request failed");
responseHeadersArea.setText("");
responseBodyArea.setText(cause.getClass().getSimpleName() + ": " + cause.getMessage());
latestResponse = null;
saveResponseButton.setEnabled(false);
}
}
};
worker.execute();
}
private void setSending(boolean sending) {
sendButton.setEnabled(!sending);
applyHeaderButton.setEnabled(!sending);
importButton.setEnabled(!sending);
exportButton.setEnabled(!sending);
saveResponseButton.setEnabled(!sending && latestResponse != null && latestResponse.hasBody());
statusLabel.setText(sending ? "Sending..." : "Ready");
if (sending) {
latestResponse = null;
responseHeadersArea.setText("");
responseBodyArea.setText("");
saveResponseButton.setEnabled(false);
}
}
private ResponseData executeRequest(String urlText, String method, Map<String, String> headers, String body)
throws IOException {
URL url = new URL(urlText);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try {
connection.setRequestMethod(method);
connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
connection.setReadTimeout(READ_TIMEOUT_MS);
connection.setInstanceFollowRedirects(false);
boolean hasRequestBody = !"GET".equals(method) && body != null && body.trim().length() > 0;
if (hasRequestBody && !containsHeader(headers, "Content-Type")) {
headers.put("Content-Type", "application/json; charset=UTF-8");
}
for (Map.Entry<String, String> entry : headers.entrySet()) {
connection.setRequestProperty(entry.getKey(), entry.getValue());
}
if (hasRequestBody) {
byte[] payload = body.getBytes(StandardCharsets.UTF_8);
connection.setDoOutput(true);
connection.setFixedLengthStreamingMode(payload.length);
OutputStream outputStream = connection.getOutputStream();
try {
outputStream.write(payload);
} finally {
outputStream.close();
}
}
int statusCode = connection.getResponseCode();
String statusMessage = connection.getResponseMessage();
Map<String, List<String>> headerFields = connection.getHeaderFields();
String responseHeaders = formatResponseHeaders(headerFields);
String contentType = connection.getHeaderField("Content-Type");
byte[] responseBodyBytes = readResponseBodyBytes(connection, statusCode);
String responsePreview = createResponsePreview(responseBodyBytes, contentType);
String suggestedFileName = suggestFileName(url, headerFields, contentType);
return new ResponseData(statusCode, statusMessage, responseHeaders, responsePreview,
responseBodyBytes, contentType, suggestedFileName);
} finally {
connection.disconnect();
}
}
private byte[] readResponseBodyBytes(HttpURLConnection connection, int statusCode) throws IOException {
InputStream stream = null;
try {
stream = statusCode >= 400 ? connection.getErrorStream() : connection.getInputStream();
} catch (IOException ex) {
stream = connection.getErrorStream();
if (stream == null) {
throw ex;
}
}
if (stream == null) {
return new byte[0];
}
return readAllBytes(stream);
}
private String createResponsePreview(byte[] data, String contentType) {
if (data == null || data.length == 0) {
return "";
}
if (isTextContent(contentType)) {
Charset charset = charsetFromContentType(contentType);
return new String(data, charset);
}
String type = contentType == null ? "unknown content type" : contentType;
return "Binary response body (" + data.length + " bytes, " + type + ").\n"
+ "Use \"Save Body\" to download it as a file.";
}
private byte[] readAllBytes(InputStream stream) throws IOException {
try {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] chunk = new byte[4096];
int read;
while ((read = stream.read(chunk)) != -1) {
buffer.write(chunk, 0, read);
}
return buffer.toByteArray();
} finally {
stream.close();
}
}
private Charset charsetFromContentType(String contentType) {
if (contentType == null) {
return StandardCharsets.UTF_8;
}
String[] parts = contentType.split(";");
for (String part : parts) {
String trimmed = part.trim();
if (trimmed.toLowerCase(Locale.ROOT).startsWith("charset=")) {
String charsetName = trimmed.substring("charset=".length()).trim().replace("\"", "");
try {
return Charset.forName(charsetName);
} catch (Exception ignored) {
return StandardCharsets.UTF_8;
}
}
}
return StandardCharsets.UTF_8;
}
private boolean isTextContent(String contentType) {
if (contentType == null) {
return true;
}
String lower = contentType.toLowerCase(Locale.ROOT);
return lower.startsWith("text/")
|| lower.contains("json")
|| lower.contains("xml")
|| lower.contains("javascript")
|| lower.contains("x-www-form-urlencoded");
}
private void showResponse(ResponseData response) {
latestResponse = response;
statusLabel.setText("Status: " + response.statusCode + " " + nullToEmpty(response.statusMessage));
responseHeadersArea.setText(response.headers);
responseBodyArea.setText(response.previewBody);
responseHeadersArea.setCaretPosition(0);
responseBodyArea.setCaretPosition(0);
saveResponseButton.setEnabled(response.hasBody());
}
private Map<String, String> parseHeaders(String headerText) {
Map<String, String> headers = new LinkedHashMap<String, String>();
String[] lines = headerText.split("\\R");
for (int i = 0; i < lines.length; i++) {
String line = lines[i].trim();
if (line.length() == 0) {
continue;
}
int separator = line.indexOf(':');
if (separator < 0) {
separator = line.indexOf('=');
}
if (separator <= 0) {
throw new IllegalArgumentException("Invalid header at line " + (i + 1)
+ ". Use format: Header-Name: value");
}
String key = line.substring(0, separator).trim();
String value = line.substring(separator + 1).trim();
if (key.length() == 0) {
throw new IllegalArgumentException("Header name cannot be empty at line " + (i + 1) + ".");
}
headers.put(key, value);
}
return headers;
}
private String formatResponseHeaders(Map<String, List<String>> headers) {
StringBuilder builder = new StringBuilder();
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
String key = entry.getKey();
List<String> values = entry.getValue();
String joined = join(values, ", ");
if (key == null) {
builder.append(joined).append('\n');
} else {
builder.append(key).append(": ").append(joined).append('\n');
}
}
return builder.toString();
}
private String join(List<String> values, String separator) {
if (values == null || values.isEmpty()) {
return "";
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < values.size(); i++) {
if (i > 0) {
builder.append(separator);
}
builder.append(values.get(i));
}
return builder.toString();
}
private boolean containsHeader(Map<String, String> headers, String name) {
for (String key : headers.keySet()) {
if (key.equalsIgnoreCase(name)) {
return true;
}
}
return false;
}
private String findHeaderKey(Map<String, String> headers, String name) {
for (String key : headers.keySet()) {
if (key.equalsIgnoreCase(name)) {
return key;
}
}
return null;
}
private String suggestFileName(URL url, Map<String, List<String>> headers, String contentType) {
String fromDisposition = fileNameFromContentDisposition(headers);
if (fromDisposition.length() > 0) {
return sanitizeFileName(fromDisposition);
}
String path = url.getPath();
if (path != null && path.length() > 0) {
int slash = path.lastIndexOf('/');
String lastPart = slash >= 0 ? path.substring(slash + 1) : path;
if (lastPart.length() > 0) {
return sanitizeFileName(lastPart);
}
}
return "response-body" + extensionFromContentType(contentType);
}
private String fileNameFromContentDisposition(Map<String, List<String>> headers) {
String contentDisposition = firstHeaderValue(headers, "Content-Disposition");
if (contentDisposition.length() == 0) {
return "";
}
String[] parts = contentDisposition.split(";");
for (String part : parts) {
String trimmed = part.trim();
String lower = trimmed.toLowerCase(Locale.ROOT);
if (lower.startsWith("filename*=")) {
String value = trimmed.substring("filename*=".length()).trim();
int doubleQuoteIndex = value.indexOf("''");
if (doubleQuoteIndex >= 0) {
value = value.substring(doubleQuoteIndex + 2);
}
return urlDecode(value.replace("\"", ""));
}
if (lower.startsWith("filename=")) {
return trimmed.substring("filename=".length()).trim().replace("\"", "");
}
}
return "";
}
private String firstHeaderValue(Map<String, List<String>> headers, String name) {
if (headers == null) {
return "";
}
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(name)) {
List<String> values = entry.getValue();
if (values != null && !values.isEmpty()) {
return values.get(0);
}
}
}
return "";
}
private String urlDecode(String value) {
try {
return java.net.URLDecoder.decode(value, StandardCharsets.UTF_8.name());
} catch (Exception ex) {
return value;
}
}
private String sanitizeFileName(String fileName) {
String cleaned = nullToEmpty(fileName).trim().replaceAll("[\\\\/:*?\"<>|]", "_");
return cleaned.length() == 0 ? "response-body" : cleaned;
}
private String extensionFromContentType(String contentType) {
if (contentType == null) {
return ".bin";
}
String lower = contentType.toLowerCase(Locale.ROOT);
if (lower.contains("json")) {
return ".json";
}
if (lower.contains("html")) {
return ".html";
}
if (lower.contains("xml")) {
return ".xml";
}
if (lower.startsWith("text/")) {
return ".txt";
}
if (lower.contains("pdf")) {
return ".pdf";
}
if (lower.contains("zip")) {
return ".zip";
}
if (lower.contains("png")) {
return ".png";
}
if (lower.contains("jpeg") || lower.contains("jpg")) {
return ".jpg";
}
return ".bin";
}
private void saveResponseBody() {
if (latestResponse == null || !latestResponse.hasBody()) {
showError("No response body to save.");
return;
}
JFileChooser chooser = new JFileChooser();
chooser.setSelectedFile(new File(latestResponse.suggestedFileName));
if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) {
return;
}
File file = chooser.getSelectedFile();
try {
Files.write(file.toPath(), latestResponse.bodyBytes);
JOptionPane.showMessageDialog(this, "Response body saved:\n" + file.getAbsolutePath(),
"Save Successful", JOptionPane.INFORMATION_MESSAGE);
} catch (IOException ex) {
showError("Failed to save response body: " + ex.getMessage());
}
}
private void exportConfig() {
Map<String, String> headers;
try {
headers = parseHeaders(headersArea.getText());
} catch (IllegalArgumentException ex) {
showError(ex.getMessage());
return;
}
RequestConfig config = new RequestConfig(
urlField.getText().trim(),
String.valueOf(methodCombo.getSelectedItem()),
headers,
bodyArea.getText()
);
JFileChooser chooser = createJsonFileChooser();
chooser.setSelectedFile(new File("request-config.json"));
if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) {
return;
}
File file = ensureJsonExtension(chooser.getSelectedFile());
try {
Files.write(file.toPath(), config.toJson().getBytes(StandardCharsets.UTF_8));
JOptionPane.showMessageDialog(this, "Request configuration exported:\n" + file.getAbsolutePath(),
"Export Successful", JOptionPane.INFORMATION_MESSAGE);
} catch (IOException ex) {
showError("Failed to export JSON: " + ex.getMessage());
}
}
private void importConfig() {
JFileChooser chooser = createJsonFileChooser();
if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) {
return;
}
File file = chooser.getSelectedFile();
try {
String json = readFileAsString(file);
RequestConfig config = RequestConfig.fromJson(json);
applyConfig(config);
JOptionPane.showMessageDialog(this, "Request configuration imported.",
"Import Successful", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
showError("Failed to import JSON: " + ex.getMessage());
}
}
private JFileChooser createJsonFileChooser() {
JFileChooser chooser = new JFileChooser();
chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("JSON files (*.json)", "json"));
return chooser;
}
private File ensureJsonExtension(File file) {
String name = file.getName().toLowerCase(Locale.ROOT);
if (name.endsWith(".json")) {
return file;
}
return new File(file.getParentFile(), file.getName() + ".json");
}
private String readFileAsString(File file) throws IOException {
InputStream inputStream = new FileInputStream(file);
try {
byte[] bytes = readAllBytes(inputStream);
return new String(bytes, StandardCharsets.UTF_8);
} finally {
inputStream.close();
}
}
private void applyConfig(RequestConfig config) {
urlField.setText(config.url);
methodCombo.setSelectedItem(config.method);
headersArea.setText(formatRequestHeaders(config.headers));
bodyArea.setText(config.body);
}
private String formatRequestHeaders(Map<String, String> headers) {
StringBuilder builder = new StringBuilder();
for (Map.Entry<String, String> entry : headers.entrySet()) {
builder.append(entry.getKey()).append(": ").append(entry.getValue()).append('\n');
}
return builder.toString().trim();
}
private void showError(String message) {
JOptionPane.showMessageDialog(this, message, "Error", JOptionPane.ERROR_MESSAGE);
}
private static String nullToEmpty(String value) {
return value == null ? "" : value;
}
private static final class ResponseData {
private final int statusCode;
private final String statusMessage;
private final String headers;
private final String previewBody;
private final byte[] bodyBytes;
private final String contentType;
private final String suggestedFileName;
private ResponseData(int statusCode, String statusMessage, String headers, String previewBody,
byte[] bodyBytes, String contentType, String suggestedFileName) {
this.statusCode = statusCode;
this.statusMessage = statusMessage;
this.headers = headers;
this.previewBody = previewBody;
this.bodyBytes = bodyBytes == null ? new byte[0] : bodyBytes;
this.contentType = nullToEmpty(contentType);
this.suggestedFileName = nullToEmpty(suggestedFileName).length() == 0
? "response-body.bin" : suggestedFileName;
}
private boolean hasBody() {
return bodyBytes.length > 0;
}
}
private static final class RequestConfig {
private final String url;
private final String method;
private final Map<String, String> headers;
private final String body;
private RequestConfig(String url, String method, Map<String, String> headers, String body) {
this.url = nullToEmpty(url);
this.method = normalizeMethod(method);
this.headers = new LinkedHashMap<String, String>(headers);
this.body = nullToEmpty(body);
}
private String toJson() {
StringBuilder builder = new StringBuilder();
builder.append("{\n");
builder.append(" \"url\": ").append(toJsonString(url)).append(",\n");
builder.append(" \"method\": ").append(toJsonString(method)).append(",\n");
builder.append(" \"headers\": {\n");
int index = 0;
for (Map.Entry<String, String> entry : headers.entrySet()) {
builder.append(" ")
.append(toJsonString(entry.getKey()))
.append(": ")
.append(toJsonString(entry.getValue()));
if (index < headers.size() - 1) {
builder.append(',');
}
builder.append('\n');
index++;
}
builder.append(" },\n");
builder.append(" \"body\": ").append(toJsonString(body)).append('\n');
builder.append("}\n");
return builder.toString();
}
private static RequestConfig fromJson(String json) {
Object parsed = SimpleJsonParser.parse(json);
if (!(parsed instanceof Map)) {
throw new IllegalArgumentException("Root JSON value must be an object.");
}
Map<?, ?> root = (Map<?, ?>) parsed;
String url = requireString(root, "url");
String method = normalizeMethod(requireString(root, "method"));
Map<String, String> headers = parseHeaderObject(root.get("headers"));
String body = optionalString(root, "body");
return new RequestConfig(url, method, headers, body);
}
private static String requireString(Map<?, ?> root, String key) {
Object value = root.get(key);
if (!(value instanceof String)) {
throw new IllegalArgumentException("JSON field \"" + key + "\" must be a string.");
}
return (String) value;
}
private static String optionalString(Map<?, ?> root, String key) {
Object value = root.get(key);
if (value == null) {
return "";
}
if (!(value instanceof String)) {
throw new IllegalArgumentException("JSON field \"" + key + "\" must be a string.");
}
return (String) value;
}
private static Map<String, String> parseHeaderObject(Object value) {
Map<String, String> headers = new LinkedHashMap<String, String>();
if (value == null) {
return headers;
}
if (!(value instanceof Map)) {
throw new IllegalArgumentException("JSON field \"headers\" must be an object.");
}
Map<?, ?> rawHeaders = (Map<?, ?>) value;
for (Map.Entry<?, ?> entry : rawHeaders.entrySet()) {
if (!(entry.getKey() instanceof String)) {
throw new IllegalArgumentException("Header names must be strings.");
}
Object rawValue = entry.getValue();
if (rawValue != null && !(rawValue instanceof String)) {
throw new IllegalArgumentException("Header value for \"" + entry.getKey()
+ "\" must be a string.");
}
headers.put((String) entry.getKey(), rawValue == null ? "" : (String) rawValue);
}
return headers;
}
private static String normalizeMethod(String method) {
String normalized = nullToEmpty(method).trim().toUpperCase(Locale.ROOT);
for (String allowed : METHODS) {
if (allowed.equals(normalized)) {
return normalized;
}
}
throw new IllegalArgumentException("Unsupported method: " + method);
}
private static String toJsonString(String value) {
StringBuilder builder = new StringBuilder();
builder.append('"');
String safeValue = nullToEmpty(value);
for (int i = 0; i < safeValue.length(); i++) {
char ch = safeValue.charAt(i);
switch (ch) {
case '"':
builder.append("\\\"");
break;
case '\\':
builder.append("\\\\");
break;
case '\b':
builder.append("\\b");
break;
case '\f':
builder.append("\\f");
break;
case '\n':
builder.append("\\n");
break;
case '\r':
builder.append("\\r");
break;
case '\t':
builder.append("\\t");
break;
default:
if (ch < 0x20) {
builder.append(String.format("\\u%04x", (int) ch));
} else {
builder.append(ch);
}
}
}
builder.append('"');
return builder.toString();
}
}
/**
* Minimal JSON parser for the request configuration format.
* It supports objects, arrays, strings, numbers, booleans, and null.
*/
private static final class SimpleJsonParser {
private final String text;
private int index;
private SimpleJsonParser(String text) {
this.text = text == null ? "" : text;
}
private static Object parse(String text) {
SimpleJsonParser parser = new SimpleJsonParser(text);
Object value = parser.parseValue();
parser.skipWhitespace();
if (!parser.isEnd()) {
throw parser.error("Unexpected trailing content.");
}
return value;
}
private Object parseValue() {
skipWhitespace();
if (isEnd()) {
throw error("Unexpected end of JSON.");
}
char ch = current();
if (ch == '{') {
return parseObject();
}
if (ch == '[') {
return parseArray();
}
if (ch == '"') {
return parseString();
}
if (ch == 't') {
expectLiteral("true");
return Boolean.TRUE;
}
if (ch == 'f') {
expectLiteral("false");
return Boolean.FALSE;
}
if (ch == 'n') {
expectLiteral("null");
return null;
}
if (ch == '-' || Character.isDigit(ch)) {
return parseNumber();
}
throw error("Unexpected character: " + ch);
}
private Map<String, Object> parseObject() {
expect('{');
Map<String, Object> object = new LinkedHashMap<String, Object>();
skipWhitespace();
if (tryConsume('}')) {
return object;
}
while (true) {
skipWhitespace();
if (isEnd() || current() != '"') {
throw error("Object keys must be strings.");
}
String key = parseString();
skipWhitespace();
expect(':');
Object value = parseValue();
object.put(key, value);
skipWhitespace();
if (tryConsume('}')) {
break;
}
expect(',');
}
return object;
}
private List<Object> parseArray() {
expect('[');
List<Object> array = new ArrayList<Object>();
skipWhitespace();
if (tryConsume(']')) {
return array;
}
while (true) {
array.add(parseValue());
skipWhitespace();
if (tryConsume(']')) {
break;
}
expect(',');
}
return array;
}
private String parseString() {
expect('"');
StringBuilder builder = new StringBuilder();
while (!isEnd()) {
char ch = text.charAt(index++);
if (ch == '"') {
return builder.toString();
}
if (ch == '\\') {
if (isEnd()) {
throw error("Incomplete escape sequence.");
}
char escaped = text.charAt(index++);
switch (escaped) {
case '"':
builder.append('"');
break;
case '\\':
builder.append('\\');
break;
case '/':
builder.append('/');
break;
case 'b':
builder.append('\b');
break;
case 'f':
builder.append('\f');
break;
case 'n':
builder.append('\n');
break;
case 'r':
builder.append('\r');
break;
case 't':
builder.append('\t');
break;
case 'u':
builder.append(parseUnicodeEscape());
break;
default:
throw error("Unsupported escape sequence: \\" + escaped);
}
} else {
builder.append(ch);
}
}
throw error("Unterminated string.");
}
private char parseUnicodeEscape() {
if (index + 4 > text.length()) {
throw error("Incomplete unicode escape.");
}
String hex = text.substring(index, index + 4);
try {
index += 4;
return (char) Integer.parseInt(hex, 16);
} catch (NumberFormatException ex) {
throw error("Invalid unicode escape: " + hex);
}
}
private Number parseNumber() {
int start = index;
if (tryConsume('-')) {
// Optional sign already consumed.
}
consumeDigits();
if (tryConsume('.')) {
consumeDigits();
}
if (!isEnd() && (current() == 'e' || current() == 'E')) {
index++;
if (!isEnd() && (current() == '+' || current() == '-')) {
index++;
}
consumeDigits();
}
String numberText = text.substring(start, index);
try {
if (numberText.indexOf('.') >= 0 || numberText.indexOf('e') >= 0 || numberText.indexOf('E') >= 0) {
return Double.valueOf(numberText);
}
return Long.valueOf(numberText);
} catch (NumberFormatException ex) {
throw error("Invalid number: " + numberText);
}
}
private void consumeDigits() {
int start = index;
while (!isEnd() && Character.isDigit(current())) {
index++;
}
if (start == index) {
throw error("Expected digit.");
}
}
private void expectLiteral(String literal) {
if (!text.startsWith(literal, index)) {
throw error("Expected literal: " + literal);
}
index += literal.length();
}
private void expect(char expected) {
skipWhitespace();
if (isEnd() || current() != expected) {
throw error("Expected '" + expected + "'.");
}
index++;
}
private boolean tryConsume(char expected) {
skipWhitespace();
if (!isEnd() && current() == expected) {
index++;
return true;
}
return false;
}
private void skipWhitespace() {
while (!isEnd()) {
char ch = current();
if (ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t') {
index++;
} else {
break;
}
}
}
private char current() {
return text.charAt(index);
}
private boolean isEnd() {
return index >= text.length();
}
private IllegalArgumentException error(String message) {
return new IllegalArgumentException(message + " At position " + index + ".");
}
}
}
效果
先编译
javac RequestUtilsApp.java
启动
java RequestUtilsApp
