vlsi commented on code in PR #6736:
URL: https://github.com/apache/jmeter/pull/6736#discussion_r3650891751


##########
src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/visualizers/RequestViewCurl.java:
##########
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jmeter.protocol.http.visualizers;
+
+import java.awt.BorderLayout;
+import java.net.URL;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.swing.JPanel;
+
+import org.apache.jmeter.gui.util.JSyntaxSearchToolBar;
+import org.apache.jmeter.gui.util.JSyntaxTextArea;
+import org.apache.jmeter.gui.util.JTextScrollPane;
+import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult;
+import org.apache.jmeter.protocol.http.util.HTTPConstants;
+import org.apache.jmeter.util.JMeterUtils;
+import org.apache.jmeter.visualizers.RequestView;
+import org.apache.jorphan.util.StringUtilities;
+
+import com.google.auto.service.AutoService;
+
+/**
+ * Panel that renders an HTTP request as a ready-to-run {@code curl} command,
+ * so it can be copied and pasted into a console or shared with a developer.
+ */
+@AutoService(RequestView.class)
+public class RequestViewCurl implements RequestView {
+
+    // Used by Request Panel
+    static final String KEY_LABEL = "view_results_table_request_tab_curl"; 
//$NON-NLS-1$
+
+    private static final String NEWLINE = " \\\n"; //$NON-NLS-1$
+
+    /**
+     * Headers that must not be reproduced in the curl command: curl generates
+     * them itself, or they are connection-specific (hop-by-hop) headers that 
are
+     * forbidden in HTTP/2 and would make the request fail with a protocol 
error.
+     */
+    private static final Set<String> SKIPPED_HEADERS = Set.of(

Review Comment:
   **`X-LocalAddress` should be skipped: it never went on the wire.**
   
   When a sampler has a source address configured, JMeter injects a 
pseudo-header into the result purely for display:
   
   - `HTTPHC4Impl#657` — `request.addHeader(HEADER_LOCAL_ADDRESS, 
localAddress.toString())`, immediately before `res.setRequestHeaders(...)`
   - `HTTPConstantsInterface#75` — `String HEADER_LOCAL_ADDRESS = 
"X-LocalAddress"; // pseudo-header for reporting Local Address`
   
   The curl tab copies it verbatim, so those samples render `-H 
'X-LocalAddress: /10.0.0.5'`. Adding `"x-localaddress"` to `SKIPPED_HEADERS` 
covers it.



##########
src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/visualizers/RequestViewCurl.java:
##########
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jmeter.protocol.http.visualizers;
+
+import java.awt.BorderLayout;
+import java.net.URL;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.swing.JPanel;
+
+import org.apache.jmeter.gui.util.JSyntaxSearchToolBar;
+import org.apache.jmeter.gui.util.JSyntaxTextArea;
+import org.apache.jmeter.gui.util.JTextScrollPane;
+import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult;
+import org.apache.jmeter.protocol.http.util.HTTPConstants;
+import org.apache.jmeter.util.JMeterUtils;
+import org.apache.jmeter.visualizers.RequestView;
+import org.apache.jorphan.util.StringUtilities;
+
+import com.google.auto.service.AutoService;
+
+/**
+ * Panel that renders an HTTP request as a ready-to-run {@code curl} command,
+ * so it can be copied and pasted into a console or shared with a developer.
+ */
+@AutoService(RequestView.class)
+public class RequestViewCurl implements RequestView {
+
+    // Used by Request Panel
+    static final String KEY_LABEL = "view_results_table_request_tab_curl"; 
//$NON-NLS-1$
+
+    private static final String NEWLINE = " \\\n"; //$NON-NLS-1$
+
+    /**
+     * Headers that must not be reproduced in the curl command: curl generates
+     * them itself, or they are connection-specific (hop-by-hop) headers that 
are
+     * forbidden in HTTP/2 and would make the request fail with a protocol 
error.
+     */
+    private static final Set<String> SKIPPED_HEADERS = Set.of(
+            "content-length", //$NON-NLS-1$
+            "connection", //$NON-NLS-1$
+            "keep-alive", //$NON-NLS-1$
+            "proxy-connection", //$NON-NLS-1$
+            "transfer-encoding", //$NON-NLS-1$
+            "upgrade"); //$NON-NLS-1$
+
+    private JSyntaxTextArea curlData;
+
+    private JPanel panel;
+
+    @Override
+    public void init() {
+        panel = new JPanel(new BorderLayout(0, 5));
+        curlData = JSyntaxTextArea.getInstance(20, 80, true);
+        curlData.setEditable(false);
+        curlData.setLineWrap(true);
+        curlData.setWrapStyleWord(true);
+        panel.add(new JSyntaxSearchToolBar(curlData).getToolBar(), 
BorderLayout.NORTH);
+        panel.add(JTextScrollPane.getInstance(curlData), BorderLayout.CENTER);
+    }
+
+    @Override
+    public void clearData() {
+        curlData.setInitialText(""); //$NON-NLS-1$
+    }
+
+    @Override
+    public void setSamplerResult(Object objectResult) {
+        if (objectResult instanceof HTTPSampleResult sampleResult) {
+            curlData.setInitialText(buildCurlCommand(sampleResult));
+            curlData.setCaretPosition(0);
+        } else {
+            // add a message when no http sample (ex. Java request)
+            
curlData.setInitialText(JMeterUtils.getResString("view_results_table_request_http_nohttp"));
 //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * Build a {@code curl} command line that reproduces the given HTTP 
request.
+     *
+     * @param sampleResult the sampled HTTP request
+     * @return the curl command as a string
+     */
+    static String buildCurlCommand(HTTPSampleResult sampleResult) {
+        StringBuilder sb = new StringBuilder(256);
+        sb.append("curl"); //$NON-NLS-1$
+
+        String method = sampleResult.getHTTPMethod();
+        if (StringUtilities.isNotBlank(method)) {
+            sb.append(" -X ").append(quote(method)); //$NON-NLS-1$
+        }
+
+        URL url = sampleResult.getURL();
+        if (url != null) {
+            sb.append(NEWLINE).append("  ").append(quote(url.toString()));
+        }
+
+        boolean hasCookieHeader = false;
+        String requestHeaders = sampleResult.getRequestHeaders();
+        if (StringUtilities.isNotEmpty(requestHeaders)) {
+            LinkedHashMap<String, String> headers = 
JMeterUtils.parseHeaders(requestHeaders);

Review Comment:
   **Repeated headers are silently collapsed, so the command does not reproduce 
the request.**
   
   `JMeterUtils.parseHeaders` returns a `LinkedHashMap<String, String>`, so 
only the last value survives per header name. Repeated headers do reach 
`requestHeaders`: `HeaderManager` entries go through `request.addHeader` 
(`HTTPHC4Impl#1415`), and `getFromHeadersMatchingPredicate` 
(`HTTPHC4Impl#1477`) writes every `Header` instance on its own line.
   
   Verified against this branch:
   
   ```
   input:  X-Trace: a\nX-Trace: b\nAccept: text/html\nAccept: application/json
   
   output: curl -X 'GET' \
             'http://example.com/' \
             -H 'X-Trace: b' \
             -H 'Accept: application/json'
   ```
   
   `X-Trace: a` and `Accept: text/html` are gone. For the HTTP tab that is a 
display quirk; for a command advertised as ready to run it is a different 
request. Splitting `requestHeaders` line by line here, instead of delegating to 
`parseHeaders`, keeps the duplicates.



##########
src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/visualizers/RequestViewCurl.java:
##########
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jmeter.protocol.http.visualizers;
+
+import java.awt.BorderLayout;
+import java.net.URL;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.swing.JPanel;
+
+import org.apache.jmeter.gui.util.JSyntaxSearchToolBar;
+import org.apache.jmeter.gui.util.JSyntaxTextArea;
+import org.apache.jmeter.gui.util.JTextScrollPane;
+import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult;
+import org.apache.jmeter.protocol.http.util.HTTPConstants;
+import org.apache.jmeter.util.JMeterUtils;
+import org.apache.jmeter.visualizers.RequestView;
+import org.apache.jorphan.util.StringUtilities;
+
+import com.google.auto.service.AutoService;
+
+/**
+ * Panel that renders an HTTP request as a ready-to-run {@code curl} command,
+ * so it can be copied and pasted into a console or shared with a developer.
+ */
+@AutoService(RequestView.class)
+public class RequestViewCurl implements RequestView {
+
+    // Used by Request Panel
+    static final String KEY_LABEL = "view_results_table_request_tab_curl"; 
//$NON-NLS-1$
+
+    private static final String NEWLINE = " \\\n"; //$NON-NLS-1$
+
+    /**
+     * Headers that must not be reproduced in the curl command: curl generates
+     * them itself, or they are connection-specific (hop-by-hop) headers that 
are
+     * forbidden in HTTP/2 and would make the request fail with a protocol 
error.
+     */
+    private static final Set<String> SKIPPED_HEADERS = Set.of(
+            "content-length", //$NON-NLS-1$
+            "connection", //$NON-NLS-1$
+            "keep-alive", //$NON-NLS-1$
+            "proxy-connection", //$NON-NLS-1$
+            "transfer-encoding", //$NON-NLS-1$
+            "upgrade"); //$NON-NLS-1$
+
+    private JSyntaxTextArea curlData;
+
+    private JPanel panel;
+
+    @Override
+    public void init() {
+        panel = new JPanel(new BorderLayout(0, 5));
+        curlData = JSyntaxTextArea.getInstance(20, 80, true);
+        curlData.setEditable(false);
+        curlData.setLineWrap(true);
+        curlData.setWrapStyleWord(true);
+        panel.add(new JSyntaxSearchToolBar(curlData).getToolBar(), 
BorderLayout.NORTH);
+        panel.add(JTextScrollPane.getInstance(curlData), BorderLayout.CENTER);
+    }
+
+    @Override
+    public void clearData() {
+        curlData.setInitialText(""); //$NON-NLS-1$
+    }
+
+    @Override
+    public void setSamplerResult(Object objectResult) {
+        if (objectResult instanceof HTTPSampleResult sampleResult) {
+            curlData.setInitialText(buildCurlCommand(sampleResult));
+            curlData.setCaretPosition(0);
+        } else {
+            // add a message when no http sample (ex. Java request)
+            
curlData.setInitialText(JMeterUtils.getResString("view_results_table_request_http_nohttp"));
 //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * Build a {@code curl} command line that reproduces the given HTTP 
request.
+     *
+     * @param sampleResult the sampled HTTP request
+     * @return the curl command as a string
+     */
+    static String buildCurlCommand(HTTPSampleResult sampleResult) {
+        StringBuilder sb = new StringBuilder(256);
+        sb.append("curl"); //$NON-NLS-1$
+
+        String method = sampleResult.getHTTPMethod();
+        if (StringUtilities.isNotBlank(method)) {

Review Comment:
   **`HEAD` samples render a command that fails.**
   
   `-X` is emitted for every method, so a HEAD sample becomes `curl -X 'HEAD' 
'<url>'`. curl sends HEAD but still waits for a response body. Against a local 
server:
   
   ```console
   $ curl -X 'HEAD' 'http://127.0.0.1:18099/' -o /dev/null
   curl: (18) transfer closed with 340 bytes remaining to read   # exit 18
   
   $ curl -I 'http://127.0.0.1:18099/' -o /dev/null              # exit 0
   ```
   
   On a keep-alive connection it hangs until the transfer times out instead of 
failing fast. `HEAD` needs `--head` rather than `-X 'HEAD'`. This overlaps the 
`-X` note on line 108 — both fall out of one branch on the method.



##########
src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/visualizers/RequestViewCurl.java:
##########
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jmeter.protocol.http.visualizers;
+
+import java.awt.BorderLayout;
+import java.net.URL;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.swing.JPanel;
+
+import org.apache.jmeter.gui.util.JSyntaxSearchToolBar;
+import org.apache.jmeter.gui.util.JSyntaxTextArea;
+import org.apache.jmeter.gui.util.JTextScrollPane;
+import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult;
+import org.apache.jmeter.protocol.http.util.HTTPConstants;
+import org.apache.jmeter.util.JMeterUtils;
+import org.apache.jmeter.visualizers.RequestView;
+import org.apache.jorphan.util.StringUtilities;
+
+import com.google.auto.service.AutoService;
+
+/**
+ * Panel that renders an HTTP request as a ready-to-run {@code curl} command,
+ * so it can be copied and pasted into a console or shared with a developer.
+ */
+@AutoService(RequestView.class)
+public class RequestViewCurl implements RequestView {
+
+    // Used by Request Panel
+    static final String KEY_LABEL = "view_results_table_request_tab_curl"; 
//$NON-NLS-1$
+
+    private static final String NEWLINE = " \\\n"; //$NON-NLS-1$
+
+    /**
+     * Headers that must not be reproduced in the curl command: curl generates
+     * them itself, or they are connection-specific (hop-by-hop) headers that 
are
+     * forbidden in HTTP/2 and would make the request fail with a protocol 
error.
+     */
+    private static final Set<String> SKIPPED_HEADERS = Set.of(
+            "content-length", //$NON-NLS-1$
+            "connection", //$NON-NLS-1$
+            "keep-alive", //$NON-NLS-1$
+            "proxy-connection", //$NON-NLS-1$
+            "transfer-encoding", //$NON-NLS-1$
+            "upgrade"); //$NON-NLS-1$
+
+    private JSyntaxTextArea curlData;
+
+    private JPanel panel;
+
+    @Override
+    public void init() {
+        panel = new JPanel(new BorderLayout(0, 5));
+        curlData = JSyntaxTextArea.getInstance(20, 80, true);
+        curlData.setEditable(false);
+        curlData.setLineWrap(true);
+        curlData.setWrapStyleWord(true);
+        panel.add(new JSyntaxSearchToolBar(curlData).getToolBar(), 
BorderLayout.NORTH);
+        panel.add(JTextScrollPane.getInstance(curlData), BorderLayout.CENTER);
+    }
+
+    @Override
+    public void clearData() {
+        curlData.setInitialText(""); //$NON-NLS-1$
+    }
+
+    @Override
+    public void setSamplerResult(Object objectResult) {
+        if (objectResult instanceof HTTPSampleResult sampleResult) {
+            curlData.setInitialText(buildCurlCommand(sampleResult));
+            curlData.setCaretPosition(0);
+        } else {
+            // add a message when no http sample (ex. Java request)
+            
curlData.setInitialText(JMeterUtils.getResString("view_results_table_request_http_nohttp"));
 //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * Build a {@code curl} command line that reproduces the given HTTP 
request.
+     *
+     * @param sampleResult the sampled HTTP request
+     * @return the curl command as a string
+     */
+    static String buildCurlCommand(HTTPSampleResult sampleResult) {
+        StringBuilder sb = new StringBuilder(256);
+        sb.append("curl"); //$NON-NLS-1$
+
+        String method = sampleResult.getHTTPMethod();
+        if (StringUtilities.isNotBlank(method)) {
+            sb.append(" -X ").append(quote(method)); //$NON-NLS-1$
+        }
+
+        URL url = sampleResult.getURL();
+        if (url != null) {
+            sb.append(NEWLINE).append("  ").append(quote(url.toString()));
+        }
+
+        boolean hasCookieHeader = false;
+        String requestHeaders = sampleResult.getRequestHeaders();
+        if (StringUtilities.isNotEmpty(requestHeaders)) {
+            LinkedHashMap<String, String> headers = 
JMeterUtils.parseHeaders(requestHeaders);
+            for (Map.Entry<String, String> entry : headers.entrySet()) {
+                String name = entry.getKey();
+                if (StringUtilities.isBlank(name) || 
SKIPPED_HEADERS.contains(name.toLowerCase(Locale.ROOT))) {
+                    continue;
+                }
+                if (HTTPConstants.HEADER_COOKIE.equalsIgnoreCase(name)) {
+                    hasCookieHeader = true;
+                }
+                sb.append(NEWLINE).append("  -H ").append(quote(name + ": " + 
entry.getValue())); //$NON-NLS-1$

Review Comment:
   **Consider adding `--compressed` when `Accept-Encoding` is reproduced.**
   
   HttpClient4 disables automatic content compression (`HTTPHC4Impl#1150`), so 
`Accept-Encoding` shows up only when a Header Manager sets it — which is 
common, and the recorder template does it. The header is then copied into the 
command without `--compressed`, and curl prints the gzip stream to the terminal.



##########
src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/visualizers/RequestViewCurl.java:
##########
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jmeter.protocol.http.visualizers;
+
+import java.awt.BorderLayout;
+import java.net.URL;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.swing.JPanel;
+
+import org.apache.jmeter.gui.util.JSyntaxSearchToolBar;
+import org.apache.jmeter.gui.util.JSyntaxTextArea;
+import org.apache.jmeter.gui.util.JTextScrollPane;
+import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult;
+import org.apache.jmeter.protocol.http.util.HTTPConstants;
+import org.apache.jmeter.util.JMeterUtils;
+import org.apache.jmeter.visualizers.RequestView;
+import org.apache.jorphan.util.StringUtilities;
+
+import com.google.auto.service.AutoService;
+
+/**
+ * Panel that renders an HTTP request as a ready-to-run {@code curl} command,
+ * so it can be copied and pasted into a console or shared with a developer.
+ */
+@AutoService(RequestView.class)
+public class RequestViewCurl implements RequestView {
+
+    // Used by Request Panel
+    static final String KEY_LABEL = "view_results_table_request_tab_curl"; 
//$NON-NLS-1$
+
+    private static final String NEWLINE = " \\\n"; //$NON-NLS-1$

Review Comment:
   **The output is POSIX-shell syntax, with nothing saying so.**
   
   `NEWLINE` is a backslash line continuation, and `quote()` on line 154 uses 
single quotes with the `'\''` idiom. Neither works in `cmd.exe`, which has no 
single-quote quoting at all, or in PowerShell, which doubles `''` and continues 
lines with a backtick. A large share of View Results Tree users are on Windows.
   
   Two options that both beat the status quo: emit the command on one line, so 
only the quoting differs; or state in `component_reference.xml` that the 
command targets a POSIX-compatible shell.



##########
src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/visualizers/RequestViewCurl.java:
##########
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jmeter.protocol.http.visualizers;
+
+import java.awt.BorderLayout;
+import java.net.URL;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.swing.JPanel;
+
+import org.apache.jmeter.gui.util.JSyntaxSearchToolBar;
+import org.apache.jmeter.gui.util.JSyntaxTextArea;
+import org.apache.jmeter.gui.util.JTextScrollPane;
+import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult;
+import org.apache.jmeter.protocol.http.util.HTTPConstants;
+import org.apache.jmeter.util.JMeterUtils;
+import org.apache.jmeter.visualizers.RequestView;
+import org.apache.jorphan.util.StringUtilities;
+
+import com.google.auto.service.AutoService;
+
+/**
+ * Panel that renders an HTTP request as a ready-to-run {@code curl} command,
+ * so it can be copied and pasted into a console or shared with a developer.
+ */
+@AutoService(RequestView.class)
+public class RequestViewCurl implements RequestView {
+
+    // Used by Request Panel
+    static final String KEY_LABEL = "view_results_table_request_tab_curl"; 
//$NON-NLS-1$
+
+    private static final String NEWLINE = " \\\n"; //$NON-NLS-1$
+
+    /**
+     * Headers that must not be reproduced in the curl command: curl generates
+     * them itself, or they are connection-specific (hop-by-hop) headers that 
are
+     * forbidden in HTTP/2 and would make the request fail with a protocol 
error.
+     */
+    private static final Set<String> SKIPPED_HEADERS = Set.of(
+            "content-length", //$NON-NLS-1$
+            "connection", //$NON-NLS-1$
+            "keep-alive", //$NON-NLS-1$
+            "proxy-connection", //$NON-NLS-1$
+            "transfer-encoding", //$NON-NLS-1$
+            "upgrade"); //$NON-NLS-1$
+
+    private JSyntaxTextArea curlData;
+
+    private JPanel panel;
+
+    @Override
+    public void init() {
+        panel = new JPanel(new BorderLayout(0, 5));
+        curlData = JSyntaxTextArea.getInstance(20, 80, true);
+        curlData.setEditable(false);
+        curlData.setLineWrap(true);
+        curlData.setWrapStyleWord(true);
+        panel.add(new JSyntaxSearchToolBar(curlData).getToolBar(), 
BorderLayout.NORTH);
+        panel.add(JTextScrollPane.getInstance(curlData), BorderLayout.CENTER);
+    }
+
+    @Override
+    public void clearData() {
+        curlData.setInitialText(""); //$NON-NLS-1$
+    }
+
+    @Override
+    public void setSamplerResult(Object objectResult) {
+        if (objectResult instanceof HTTPSampleResult sampleResult) {
+            curlData.setInitialText(buildCurlCommand(sampleResult));
+            curlData.setCaretPosition(0);
+        } else {
+            // add a message when no http sample (ex. Java request)
+            
curlData.setInitialText(JMeterUtils.getResString("view_results_table_request_http_nohttp"));
 //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * Build a {@code curl} command line that reproduces the given HTTP 
request.
+     *
+     * @param sampleResult the sampled HTTP request
+     * @return the curl command as a string
+     */
+    static String buildCurlCommand(HTTPSampleResult sampleResult) {
+        StringBuilder sb = new StringBuilder(256);
+        sb.append("curl"); //$NON-NLS-1$
+
+        String method = sampleResult.getHTTPMethod();
+        if (StringUtilities.isNotBlank(method)) {
+            sb.append(" -X ").append(quote(method)); //$NON-NLS-1$
+        }
+
+        URL url = sampleResult.getURL();
+        if (url != null) {
+            sb.append(NEWLINE).append("  ").append(quote(url.toString()));
+        }
+
+        boolean hasCookieHeader = false;
+        String requestHeaders = sampleResult.getRequestHeaders();
+        if (StringUtilities.isNotEmpty(requestHeaders)) {
+            LinkedHashMap<String, String> headers = 
JMeterUtils.parseHeaders(requestHeaders);
+            for (Map.Entry<String, String> entry : headers.entrySet()) {
+                String name = entry.getKey();
+                if (StringUtilities.isBlank(name) || 
SKIPPED_HEADERS.contains(name.toLowerCase(Locale.ROOT))) {
+                    continue;
+                }
+                if (HTTPConstants.HEADER_COOKIE.equalsIgnoreCase(name)) {
+                    hasCookieHeader = true;
+                }
+                sb.append(NEWLINE).append("  -H ").append(quote(name + ": " + 
entry.getValue())); //$NON-NLS-1$
+            }
+        }
+
+        // Cookies are tracked separately in JMeter; only add them if they were
+        // not already emitted as a Cookie header above.
+        String cookies = sampleResult.getCookies();
+        if (!hasCookieHeader && StringUtilities.isNotEmpty(cookies)) {
+            sb.append(NEWLINE).append("  -b ").append(quote(cookies)); 
//$NON-NLS-1$
+        }
+
+        String body = sampleResult.getQueryString();
+        if (StringUtilities.isNotBlank(body)) {
+            sb.append(NEWLINE).append("  --data-raw ").append(quote(body)); 
//$NON-NLS-1$

Review Comment:
   **Two more paths put a placeholder into `--data-raw`, and neither is 
multipart.**
   
   Extending the multipart note on line 141: filtering on `multipart/form-data` 
alone leaves two cases that produce the same silently wrong command.
   
   - File sent as the request body, where `Content-Type` is the file's own type 
rather than multipart. `HTTPHC4Impl#1545` appends `<actual file content, not 
shown here>` in the `getSendFileAsPostBody` branch.
   - Non-repeatable entity. `HTTPHC4Impl#1619` appends `<Entity was not 
repeatable, cannot view what was sent>`.
   
   Both render as `--data-raw '<actual file content, not shown here>'` and the 
equivalent. Detecting "this body is a rendered placeholder, not the wire body" 
covers all three cases; a multipart check covers one.
   
   Separately, line 140 uses `isNotBlank`, which drops a body made only of 
whitespace. A body of `" "` is a real body with `Content-Length: 1`. 
`isNotEmpty` matches what the header and cookie branches above already use.



##########
src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/visualizers/RequestViewCurl.java:
##########
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jmeter.protocol.http.visualizers;
+
+import java.awt.BorderLayout;
+import java.net.URL;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.swing.JPanel;
+
+import org.apache.jmeter.gui.util.JSyntaxSearchToolBar;
+import org.apache.jmeter.gui.util.JSyntaxTextArea;
+import org.apache.jmeter.gui.util.JTextScrollPane;
+import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult;
+import org.apache.jmeter.protocol.http.util.HTTPConstants;
+import org.apache.jmeter.util.JMeterUtils;
+import org.apache.jmeter.visualizers.RequestView;
+import org.apache.jorphan.util.StringUtilities;
+
+import com.google.auto.service.AutoService;
+
+/**
+ * Panel that renders an HTTP request as a ready-to-run {@code curl} command,
+ * so it can be copied and pasted into a console or shared with a developer.
+ */
+@AutoService(RequestView.class)
+public class RequestViewCurl implements RequestView {
+
+    // Used by Request Panel
+    static final String KEY_LABEL = "view_results_table_request_tab_curl"; 
//$NON-NLS-1$
+
+    private static final String NEWLINE = " \\\n"; //$NON-NLS-1$
+
+    /**
+     * Headers that must not be reproduced in the curl command: curl generates
+     * them itself, or they are connection-specific (hop-by-hop) headers that 
are
+     * forbidden in HTTP/2 and would make the request fail with a protocol 
error.
+     */
+    private static final Set<String> SKIPPED_HEADERS = Set.of(
+            "content-length", //$NON-NLS-1$
+            "connection", //$NON-NLS-1$
+            "keep-alive", //$NON-NLS-1$
+            "proxy-connection", //$NON-NLS-1$
+            "transfer-encoding", //$NON-NLS-1$
+            "upgrade"); //$NON-NLS-1$
+
+    private JSyntaxTextArea curlData;
+
+    private JPanel panel;
+
+    @Override
+    public void init() {
+        panel = new JPanel(new BorderLayout(0, 5));
+        curlData = JSyntaxTextArea.getInstance(20, 80, true);
+        curlData.setEditable(false);
+        curlData.setLineWrap(true);
+        curlData.setWrapStyleWord(true);
+        panel.add(new JSyntaxSearchToolBar(curlData).getToolBar(), 
BorderLayout.NORTH);
+        panel.add(JTextScrollPane.getInstance(curlData), BorderLayout.CENTER);
+    }
+
+    @Override
+    public void clearData() {
+        curlData.setInitialText(""); //$NON-NLS-1$
+    }
+
+    @Override
+    public void setSamplerResult(Object objectResult) {
+        if (objectResult instanceof HTTPSampleResult sampleResult) {
+            curlData.setInitialText(buildCurlCommand(sampleResult));
+            curlData.setCaretPosition(0);
+        } else {
+            // add a message when no http sample (ex. Java request)
+            
curlData.setInitialText(JMeterUtils.getResString("view_results_table_request_http_nohttp"));
 //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * Build a {@code curl} command line that reproduces the given HTTP 
request.
+     *
+     * @param sampleResult the sampled HTTP request
+     * @return the curl command as a string
+     */
+    static String buildCurlCommand(HTTPSampleResult sampleResult) {
+        StringBuilder sb = new StringBuilder(256);
+        sb.append("curl"); //$NON-NLS-1$
+
+        String method = sampleResult.getHTTPMethod();
+        if (StringUtilities.isNotBlank(method)) {
+            sb.append(" -X ").append(quote(method)); //$NON-NLS-1$
+        }
+
+        URL url = sampleResult.getURL();
+        if (url != null) {
+            sb.append(NEWLINE).append("  ").append(quote(url.toString()));
+        }
+
+        boolean hasCookieHeader = false;
+        String requestHeaders = sampleResult.getRequestHeaders();
+        if (StringUtilities.isNotEmpty(requestHeaders)) {
+            LinkedHashMap<String, String> headers = 
JMeterUtils.parseHeaders(requestHeaders);
+            for (Map.Entry<String, String> entry : headers.entrySet()) {
+                String name = entry.getKey();
+                if (StringUtilities.isBlank(name) || 
SKIPPED_HEADERS.contains(name.toLowerCase(Locale.ROOT))) {
+                    continue;
+                }
+                if (HTTPConstants.HEADER_COOKIE.equalsIgnoreCase(name)) {
+                    hasCookieHeader = true;
+                }
+                sb.append(NEWLINE).append("  -H ").append(quote(name + ": " + 
entry.getValue())); //$NON-NLS-1$
+            }
+        }
+
+        // Cookies are tracked separately in JMeter; only add them if they were
+        // not already emitted as a Cookie header above.
+        String cookies = sampleResult.getCookies();
+        if (!hasCookieHeader && StringUtilities.isNotEmpty(cookies)) {

Review Comment:
   **The `hasCookieHeader` branch is unreachable for the shipped HTTP 
implementations.**
   
   `HTTPHC4Impl` and `HTTPJavaImpl` both fill `requestHeaders` from 
`getAllHeadersExceptCookie`, so `Cookie` is stripped by construction and the 
value arrives through `getCookies()` instead. `testCookieHeaderNotDuplicated` 
therefore pins a state that no sampler produces, which reads as coverage it is 
not.
   
   If some implementation does leave `Cookie` in `requestHeaders`, a comment 
naming it would help. Otherwise dropping the flag makes the method shorter.



##########
src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/visualizers/RequestViewCurl.java:
##########
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jmeter.protocol.http.visualizers;
+
+import java.awt.BorderLayout;
+import java.net.URL;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.swing.JPanel;
+
+import org.apache.jmeter.gui.util.JSyntaxSearchToolBar;
+import org.apache.jmeter.gui.util.JSyntaxTextArea;
+import org.apache.jmeter.gui.util.JTextScrollPane;
+import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult;
+import org.apache.jmeter.protocol.http.util.HTTPConstants;
+import org.apache.jmeter.util.JMeterUtils;
+import org.apache.jmeter.visualizers.RequestView;
+import org.apache.jorphan.util.StringUtilities;
+
+import com.google.auto.service.AutoService;
+
+/**
+ * Panel that renders an HTTP request as a ready-to-run {@code curl} command,
+ * so it can be copied and pasted into a console or shared with a developer.
+ */
+@AutoService(RequestView.class)
+public class RequestViewCurl implements RequestView {
+
+    // Used by Request Panel
+    static final String KEY_LABEL = "view_results_table_request_tab_curl"; 
//$NON-NLS-1$
+
+    private static final String NEWLINE = " \\\n"; //$NON-NLS-1$
+
+    /**
+     * Headers that must not be reproduced in the curl command: curl generates
+     * them itself, or they are connection-specific (hop-by-hop) headers that 
are
+     * forbidden in HTTP/2 and would make the request fail with a protocol 
error.
+     */
+    private static final Set<String> SKIPPED_HEADERS = Set.of(
+            "content-length", //$NON-NLS-1$
+            "connection", //$NON-NLS-1$
+            "keep-alive", //$NON-NLS-1$
+            "proxy-connection", //$NON-NLS-1$
+            "transfer-encoding", //$NON-NLS-1$
+            "upgrade"); //$NON-NLS-1$
+
+    private JSyntaxTextArea curlData;
+
+    private JPanel panel;
+
+    @Override
+    public void init() {
+        panel = new JPanel(new BorderLayout(0, 5));
+        curlData = JSyntaxTextArea.getInstance(20, 80, true);
+        curlData.setEditable(false);
+        curlData.setLineWrap(true);
+        curlData.setWrapStyleWord(true);
+        panel.add(new JSyntaxSearchToolBar(curlData).getToolBar(), 
BorderLayout.NORTH);
+        panel.add(JTextScrollPane.getInstance(curlData), BorderLayout.CENTER);
+    }
+
+    @Override
+    public void clearData() {
+        curlData.setInitialText(""); //$NON-NLS-1$
+    }
+
+    @Override
+    public void setSamplerResult(Object objectResult) {
+        if (objectResult instanceof HTTPSampleResult sampleResult) {
+            curlData.setInitialText(buildCurlCommand(sampleResult));
+            curlData.setCaretPosition(0);
+        } else {
+            // add a message when no http sample (ex. Java request)
+            
curlData.setInitialText(JMeterUtils.getResString("view_results_table_request_http_nohttp"));
 //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * Build a {@code curl} command line that reproduces the given HTTP 
request.
+     *
+     * @param sampleResult the sampled HTTP request
+     * @return the curl command as a string
+     */
+    static String buildCurlCommand(HTTPSampleResult sampleResult) {

Review Comment:
   **Consider moving the builder out of the GUI class.**
   
   #6375 asks for "Copy as cURL", and the follow-up most people will want is a 
context-menu action on the sampler itself, not only on a sample that has 
already run. `buildCurlCommand` has no Swing dependency, so it could live next 
to `BasicCurlParser` in `org.apache.jmeter.protocol.http.curl` and be reused by 
such an action later.
   
   That placement also enables a round-trip test the current suite cannot 
express: generate the command, feed it to `BasicCurlParser`, compare the parsed 
request against the sampler. The parser already handles `-X`, `-H`, `-b`, and 
`--data-raw` (`BasicCurlParser#70`, `#551-555`), so the test is cheap — and it 
would have caught the header and body issues above.



##########
src/protocol/http/src/test/java/org/apache/jmeter/protocol/http/visualizers/RequestViewCurlTest.java:
##########
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jmeter.protocol.http.visualizers;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult;
+import org.junit.jupiter.api.Test;
+
+class RequestViewCurlTest {
+
+    private static HTTPSampleResult result(String method, String url) throws 
MalformedURLException {
+        HTTPSampleResult res = new HTTPSampleResult();
+        res.setHTTPMethod(method);
+        if (url != null) {
+            res.setURL(new URL(url));
+        }
+        return res;
+    }
+
+    @Test
+    void testSimpleGet() throws Exception {
+        HTTPSampleResult res = result("GET", "http://example.com/path?a=1";);
+        String curl = RequestViewCurl.buildCurlCommand(res);
+
+        assertTrue(curl.startsWith("curl -X 'GET'"), curl);
+        assertTrue(curl.contains("'http://example.com/path?a=1'"), curl);
+        assertFalse(curl.contains("--data-raw"), curl);
+    }
+
+    @Test
+    void testHeadersAreEmitted() throws Exception {
+        HTTPSampleResult res = result("GET", "http://example.com/";);
+        res.setRequestHeaders("Accept: application/json\nUser-Agent: JMeter");
+        String curl = RequestViewCurl.buildCurlCommand(res);
+
+        assertTrue(curl.contains("-H 'Accept: application/json'"), curl);
+        assertTrue(curl.contains("-H 'User-Agent: JMeter'"), curl);
+    }
+
+    @Test
+    void testPostBody() throws Exception {
+        HTTPSampleResult res = result("POST", "http://example.com/submit";);
+        res.setRequestHeaders("Content-Type: application/json");
+        res.setQueryString("{\"name\":\"value\"}");
+        String curl = RequestViewCurl.buildCurlCommand(res);
+
+        assertTrue(curl.contains("-X 'POST'"), curl);
+        assertTrue(curl.contains("--data-raw '{\"name\":\"value\"}'"), curl);
+    }
+
+    @Test
+    void testConnectionAndAutoHeadersAreSkipped() throws Exception {
+        HTTPSampleResult res = result("POST", "https://example.com/";);
+        res.setRequestHeaders("Connection: keep-alive\n"
+                + "Content-Length: 140\n"
+                + "Transfer-Encoding: chunked\n"
+                + "Content-Type: application/json\n"
+                + "Accept: application/json");
+        res.setQueryString("{}");
+        String curl = RequestViewCurl.buildCurlCommand(res);
+
+        // curl manages these / they are forbidden in HTTP/2, so they must be 
dropped
+        assertFalse(curl.contains("Connection"), curl);

Review Comment:
   **These assertions can pass for the wrong reason.**
   
   `assertFalse(curl.contains("Connection"))` also matches the URL, so the same 
test against `http://connection.example.com/` fails with no regression present. 
The commands are short: asserting the whole string with `assertEquals` is 
stricter and easier to read when it breaks.
   
   Cases worth adding while you are here: repeated header names, `HEAD`, a 
multipart body, `X-LocalAddress`, and a whitespace-only body.



##########
xdocs/changes.xml:
##########
@@ -104,6 +104,7 @@ Summary
   <ul>
     <li><pr>6333</pr>Apply HiDPI mode automatically when setting up the GUI so 
JMeter looks sharp on high-resolution displays. Contributed by Gabriele Coletta 
(github.com/gdmg92)</li>
     <li><pr>6656</pr>Replace the previous feather icon with the new oak leaf 
in the JMeter logo.</li>
+    <li><issue>6375</issue>Add a <code>cURL</code> tab to the Request panel in 
View Results Tree, showing the sampled HTTP request as a ready-to-run 
<code>curl</code> command that can be copied to a console. Contributed by 
Oleksandr Poliakov (github.com/poliakov-alex)</li>

Review Comment:
   Agreed on `<pr>6736</pr>`. One more on the same entry: it sits under 
`Changes -> UI`, but the feature is HTTP-specific and only renders for 
`HTTPSampleResult`. `HTTP Samplers and Test Script Recorder` looks like the 
better home.



##########
xdocs/usermanual/component_reference.xml:
##########
@@ -2798,6 +2798,8 @@ response for any sample.  In addition to showing the 
response, you can see the t
 this response, and some response codes.
 Note that the Request panel only shows the headers added by JMeter.
 It does not show any headers (such as <code>Host</code>) that may be added by 
the HTTP protocol implementation.
+For HTTP samples the Request panel also provides a <code>cURL</code> tab that 
renders the request as a
+ready-to-run <code>curl</code> command, so it can be copied to a console or 
shared with a developer.

Review Comment:
   +1, and two more things worth a sentence there.
   
   The command carries whatever the sample carried, including `Authorization` 
headers, cookies, and API keys. The PR description suggests sharing it with a 
developer, so a note that it contains credentials earns its line.
   
   Also, the new text is added without a `<p>` wrapper, so it runs into the 
preceding paragraph about headers added by JMeter. The surrounding text does 
the same, so this is a question rather than a request: keep it as is, or wrap 
the new sentences?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to