自制请求工具

纯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
相关推荐
scan7241 小时前
短期记忆记忆存储在内存里,一个会话里的多轮对话
开发语言·c#
PersistJiao1 小时前
在项目级别创建 Python 虚拟环境
python·虚拟环境
Ztopcloud极拓云视角1 小时前
微软Build 2026自研MAI模型全接入指南:用Python搭一个多模型路由网关
python·microsoft·flask
香辣西红柿炒蛋1 小时前
pytest框架介绍
python·pytest
风之所往_1 小时前
Python 3.5 新特性全面总结
python
程序员皮皮林1 小时前
Dubbo 的 SPI 和 JDK 的 SPI 有什么区别?
java·开发语言·dubbo
野生的小狗熊1 小时前
【自学Agent开发之路】第二篇—从.NET到Python:Agent开发的本质就是投喂上下文
python
是多巴胺不是尼古丁1 小时前
java‘期末复习--多态
java·开发语言
牵牛花主人2 小时前
【无标题】
python·pandas