This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 069fe42094 Add content type to httppost, fixes #4769 (#7374)
069fe42094 is described below

commit 069fe42094ea361c4341aca8370fdc6d0713c27a
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Tue Jun 30 14:57:51 2026 +0200

    Add content type to httppost, fixes #4769 (#7374)
---
 .../ROOT/pages/pipeline/transforms/httppost.adoc   |   1 +
 .../hop/pipeline/transforms/httppost/HttpPost.java |  26 ++--
 .../pipeline/transforms/httppost/HttpPostData.java |   1 +
 .../transforms/httppost/HttpPostDialog.java        |  41 ++++++-
 .../pipeline/transforms/httppost/HttpPostMeta.java |   7 ++
 .../httppost/messages/messages_en_US.properties    |   3 +
 .../transforms/httppost/HttpPostDialogTest.java    | 132 +++++++++++++++++++++
 .../transforms/httppost/HttpPostExecutionTest.java |  14 +++
 8 files changed, 207 insertions(+), 18 deletions(-)

diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/httppost.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/httppost.adoc
index d3b359f2ae..05280d4b81 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/httppost.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/httppost.adoc
@@ -51,6 +51,7 @@ The General tab defines which RSS/Atom URLs you want to use, 
and optionally whic
 |Accept URL from field?|If checked, you must specify which field to retrieve 
the URL from.
 |URL field name|If the previous option is checked, this is where you specify 
the URL field.
 |Encoding|The encoding standard for the files being accessed.
+|Content type|The `Content-Type` header sent with the request body. Defaults 
to `text/xml` for backwards compatibility; set it to e.g. `application/json` 
when posting JSON. A `Content-Type` header parameter defined on the Fields tab 
takes precedence over this value.
 |Request entity field|The name of the field that will contain the POST request.
 When enabled, the Post a file option will retrieve the file named in this 
field, and post the contents of that file.
 |Post a file|If a file is defined in the Request entity field, its contents 
will be posted if this option is checked.
diff --git 
a/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPost.java
 
b/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPost.java
index 9be17eff5d..a404725244 100644
--- 
a/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPost.java
+++ 
b/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPost.java
@@ -479,6 +479,7 @@ public class HttpPost extends BaseTransform<HttpPostMeta, 
HttpPostData> {
         }
       }
       data.realEncoding = resolve(meta.getEncoding());
+      data.realContentType = resolve(meta.getContentType());
     } // end if first
 
     try {
@@ -580,23 +581,16 @@ public class HttpPost extends BaseTransform<HttpPostMeta, 
HttpPostData> {
     // If content encoding is not explicitly specified
     // ISO-8859-1 is assumed by the POSTMethod
     if (!data.contentTypeHeaderOverwrite && !meta.isPostAFile()) {
+      // Use the configured content type, falling back to text/xml for 
backwards compatibility
+      // (e.g. pipelines created before the content type became configurable).
+      String contentType =
+          Utils.isEmpty(data.realContentType) ? CONTENT_TYPE_TEXT_XML : 
data.realContentType;
       // can be overwritten now
-      if (Utils.isEmpty(data.realEncoding)) {
-        post.setHeader(CONTENT_TYPE, CONTENT_TYPE_TEXT_XML);
-        if (isDebug()) {
-          logDebug(
-              BaseMessages.getString(PKG, PKG_HEADER_VALUE, CONTENT_TYPE, 
CONTENT_TYPE_TEXT_XML));
-        }
-      } else {
-        post.setHeader(CONTENT_TYPE, CONTENT_TYPE_TEXT_XML + "; " + 
data.realEncoding);
-        if (isDebug()) {
-          logDebug(
-              BaseMessages.getString(
-                  PKG,
-                  PKG_HEADER_VALUE,
-                  CONTENT_TYPE,
-                  CONTENT_TYPE_TEXT_XML + "; " + data.realEncoding));
-        }
+      String headerValue =
+          Utils.isEmpty(data.realEncoding) ? contentType : contentType + "; " 
+ data.realEncoding;
+      post.setHeader(CONTENT_TYPE, headerValue);
+      if (isDebug()) {
+        logDebug(BaseMessages.getString(PKG, PKG_HEADER_VALUE, CONTENT_TYPE, 
headerValue));
       }
     }
   }
diff --git 
a/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPostData.java
 
b/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPostData.java
index 7e46ee26bb..15a239192a 100644
--- 
a/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPostData.java
+++ 
b/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPostData.java
@@ -27,6 +27,7 @@ public class HttpPostData extends BaseTransformData 
implements ITransformData {
   public IRowMeta outputRowMeta;
   public IRowMeta inputRowMeta;
   public String realEncoding;
+  public String realContentType;
   public int[] header_parameters_nrs;
   public int[] body_parameters_nrs;
   public int[] query_parameters_nrs;
diff --git 
a/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPostDialog.java
 
b/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPostDialog.java
index 8e0747a774..9c88d77d5b 100644
--- 
a/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPostDialog.java
+++ 
b/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPostDialog.java
@@ -116,6 +116,7 @@ public class HttpPostDialog extends BaseTransformDialog {
   private boolean gotPreviousFields = false;
 
   private ComboVar wEncoding;
+  private ComboVar wContentType;
   private Button wMultiPartUpload;
 
   private Button wPostAFile;
@@ -173,6 +174,7 @@ public class HttpPostDialog extends BaseTransformDialog {
     setupIgnoreSslLine(gSettings);
     setupUrlFieldNameLine(lsMod, gSettings);
     setupEncodingLine(lsMod, gSettings);
+    setupContentTypeLine(lsMod, gSettings);
     setupRequestEntityLine(lsMod, gSettings);
     setupMultiPartUpload(gSettings);
     setupPostFileLine(gSettings);
@@ -788,7 +790,7 @@ public class HttpPostDialog extends BaseTransformDialog {
     FormData fdlRequestEntity = new FormData();
     fdlRequestEntity.left = new FormAttachment(0, 0);
     fdlRequestEntity.right = new FormAttachment(middle, -margin);
-    fdlRequestEntity.top = new FormAttachment(wEncoding, margin);
+    fdlRequestEntity.top = new FormAttachment(wContentType, margin);
     wlRequestEntity.setLayoutData(fdlRequestEntity);
 
     wRequestEntity = new ComboVar(variables, gSettings, SWT.BORDER | 
SWT.READ_ONLY);
@@ -797,7 +799,7 @@ public class HttpPostDialog extends BaseTransformDialog {
     wRequestEntity.addModifyListener(lsMod);
     FormData fdRequestEntity = new FormData();
     fdRequestEntity.left = new FormAttachment(middle, 0);
-    fdRequestEntity.top = new FormAttachment(wEncoding, margin);
+    fdRequestEntity.top = new FormAttachment(wContentType, margin);
     fdRequestEntity.right = new FormAttachment(100, -margin);
     wRequestEntity.setLayoutData(fdRequestEntity);
     wRequestEntity.addFocusListener(
@@ -856,6 +858,37 @@ public class HttpPostDialog extends BaseTransformDialog {
         });
   }
 
+  private void setupContentTypeLine(ModifyListener lsMod, Group gSettings) {
+    int middle = props.getMiddlePct();
+    int margin = PropsUi.getMargin();
+    Label wlContentType = new Label(gSettings, SWT.RIGHT);
+    wlContentType.setText(BaseMessages.getString(PKG, 
"HTTPPOSTDialog.ContentType.Label"));
+    wlContentType.setToolTipText(BaseMessages.getString(PKG, 
"HTTPPOSTDialog.ContentType.Tooltip"));
+    PropsUi.setLook(wlContentType);
+    FormData fdlContentType = new FormData();
+    fdlContentType.left = new FormAttachment(0, 0);
+    fdlContentType.top = new FormAttachment(wEncoding, margin);
+    fdlContentType.right = new FormAttachment(middle, -margin);
+    wlContentType.setLayoutData(fdlContentType);
+    wContentType = new ComboVar(variables, gSettings, SWT.BORDER);
+    wContentType.setToolTipText(BaseMessages.getString(PKG, 
"HTTPPOSTDialog.ContentType.Tooltip"));
+    PropsUi.setLook(wContentType);
+    wContentType.addModifyListener(lsMod);
+    FormData fdContentType = new FormData();
+    fdContentType.left = new FormAttachment(middle, 0);
+    fdContentType.top = new FormAttachment(wEncoding, margin);
+    fdContentType.right = new FormAttachment(100, -margin);
+    wContentType.setLayoutData(fdContentType);
+    wContentType.setItems(
+        new String[] {
+          "text/xml",
+          "application/json",
+          "application/xml",
+          "text/plain",
+          "application/x-www-form-urlencoded"
+        });
+  }
+
   private void setupUrlFieldNameLine(ModifyListener lsMod, Group gSettings) {
     // UrlField Line
     int middle = props.getMiddlePct();
@@ -1099,6 +1132,9 @@ public class HttpPostDialog extends BaseTransformDialog {
     if (input.getEncoding() != null) {
       wEncoding.setText(input.getEncoding());
     }
+    if (input.getContentType() != null) {
+      wContentType.setText(input.getContentType());
+    }
     wPostAFile.setSelection(input.isPostAFile());
     wMultiPartUpload.setSelection(input.isMultipartupload());
 
@@ -1187,6 +1223,7 @@ public class HttpPostDialog extends BaseTransformDialog {
     input.setResultFields(listHttpPostResultField);
 
     input.setEncoding(wEncoding.getText());
+    input.setContentType(wContentType.getText());
     input.setPostAFile(wPostAFile.getSelection());
     input.setMultipartupload(wMultiPartUpload.getSelection());
     input.setHttpLogin(wHttpLogin.getText());
diff --git 
a/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPostMeta.java
 
b/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPostMeta.java
index 1a2fc4950d..090451ae0a 100644
--- 
a/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPostMeta.java
+++ 
b/plugins/transforms/httppost/src/main/java/org/apache/hop/pipeline/transforms/httppost/HttpPostMeta.java
@@ -61,6 +61,9 @@ public class HttpPostMeta extends BaseTransformMeta<HttpPost, 
HttpPostData> {
   // the time to wait till a connection is closed (milliseconds)? -1 is no not 
close.
   public static final int DEFAULT_CLOSE_CONNECTIONS_TIME = -1;
 
+  // the default Content-Type header used for the request body (kept for 
backwards compatibility)
+  public static final String DEFAULT_CONTENT_TYPE = "text/xml";
+
   @HopMetadataProperty(injectionKeyDescription = 
"HTTPPOST.Injection.socketTimeout")
   private String socketTimeout;
 
@@ -92,6 +95,9 @@ public class HttpPostMeta extends BaseTransformMeta<HttpPost, 
HttpPostData> {
   @HopMetadataProperty(injectionKeyDescription = "HTTPPOST.Injection.encoding")
   private String encoding;
 
+  @HopMetadataProperty(injectionKeyDescription = 
"HTTPPOST.Injection.contentType")
+  private String contentType;
+
   @HopMetadataProperty(key = "postafile", injectionKeyDescription = 
"HTTPPOST.Injection.postAFile")
   private boolean postAFile;
 
@@ -131,6 +137,7 @@ public class HttpPostMeta extends 
BaseTransformMeta<HttpPost, HttpPostData> {
   @Override
   public void setDefault() {
     encoding = Const.UTF_8;
+    contentType = DEFAULT_CONTENT_TYPE;
     postAFile = false;
     multipartupload = false;
     lookupFields.add(new HttpPostLookupField());
diff --git 
a/plugins/transforms/httppost/src/main/resources/org/apache/hop/pipeline/transforms/httppost/messages/messages_en_US.properties
 
b/plugins/transforms/httppost/src/main/resources/org/apache/hop/pipeline/transforms/httppost/messages/messages_en_US.properties
index 51fb5db610..fca99fe9b6 100644
--- 
a/plugins/transforms/httppost/src/main/resources/org/apache/hop/pipeline/transforms/httppost/messages/messages_en_US.properties
+++ 
b/plugins/transforms/httppost/src/main/resources/org/apache/hop/pipeline/transforms/httppost/messages/messages_en_US.properties
@@ -31,6 +31,7 @@ HTTPPOST.Injection.ArgumentFieldName=Body (Header) Field name
 HTTPPOST.Injection.ArgumentFieldParameter=Body (Header) value 
 HTTPPOST.Injection.closeIdleConnectionsTime=Connection close wait time 
(miliseconds)
 HTTPPOST.Injection.connectionTimeout=Connection timeout value (miliseconds)
+HTTPPOST.Injection.contentType=Content type
 HTTPPOST.Injection.encoding=Encoding
 HTTPPOST.Injection.httpLogin=HTTP Login
 HTTPPOST.Injection.httpPassword=HTTP Password
@@ -74,6 +75,8 @@ HTTPPOSTDialog.ColumnInfo.QueryName=Name
 HTTPPOSTDialog.ColumnInfo.QueryParameter=Value
 HTTPPOSTDialog.ConnectionTimeOut.Label=Connection timeout
 HTTPPOSTDialog.ConnectionTimeOut.Tooltip=The timeout until a connection is 
established (milliseconds)
+HTTPPOSTDialog.ContentType.Label=Content type
+HTTPPOSTDialog.ContentType.Tooltip=The Content-Type header sent with the 
request body. Defaults to text/xml for backwards compatibility; use e.g. 
application/json for JSON payloads. Overridden by a Content-Type header 
parameter on the Fields tab.
 HTTPPOSTDialog.Encoding.Label=Encoding
 HTTPPOSTDialog.FailedToGetFields.DialogMessage=We failed to get fields from 
previous transform\!
 HTTPPOSTDialog.FailedToGetFields.DialogTitle=Error getting fields
diff --git 
a/plugins/transforms/httppost/src/test/java/org/apache/hop/pipeline/transforms/httppost/HttpPostDialogTest.java
 
b/plugins/transforms/httppost/src/test/java/org/apache/hop/pipeline/transforms/httppost/HttpPostDialogTest.java
new file mode 100644
index 0000000000..af45db710b
--- /dev/null
+++ 
b/plugins/transforms/httppost/src/test/java/org/apache/hop/pipeline/transforms/httppost/HttpPostDialogTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.hop.pipeline.transforms.httppost;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.Arrays;
+import org.apache.hop.core.plugins.PluginRegistry;
+import org.apache.hop.core.plugins.TransformPluginType;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.ui.testing.SwtBotTestBase;
+import org.eclipse.swtbot.swt.finder.SWTBot;
+import org.eclipse.swtbot.swt.finder.exceptions.WidgetNotFoundException;
+import org.eclipse.swtbot.swt.finder.widgets.SWTBotCCombo;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * End-to-end SWTBot coverage for the HTTP Post transform's {@link 
HttpPostDialog}, focused on the
+ * configurable Content type field added for issue #4769. The dialog runs its 
own blocking event
+ * loop in {@code open()}, so {@link SwtBotTestBase#withDialog} pumps it on 
the UI thread while the
+ * assertions drive it from a worker thread.
+ *
+ * <p>Tagged {@code uitest} so it is excluded from the normal build (it needs 
a display); run with
+ * {@code mvn -pl plugins/transforms/httppost -Puitest test}.
+ */
+@Tag("uitest")
+class HttpPostDialogTest extends SwtBotTestBase {
+
+  private static final String TRANSFORM_NAME = "httppost";
+  private static final String SHELL_TITLE = "HTTP post";
+
+  @Test
+  void getDataShowsConfiguredContentType() {
+    HttpPostMeta meta = new HttpPostMeta();
+    meta.setDefault(); // contentType defaults to text/xml
+    meta.setContentType("application/json");
+    PipelineMeta pipelineMeta = pipelineWith(meta);
+
+    withDialog(
+        parent -> new HttpPostDialog(parent, new Variables(), meta, 
pipelineMeta).open(),
+        bot -> {
+          SWTBot dialog = bot.shell(SHELL_TITLE).activate().bot();
+          assertEquals("application/json", contentTypeCombo(dialog).getText());
+          dialog.button(buttonLabel("System.Button.Cancel")).click();
+        });
+  }
+
+  @Test
+  void okWritesEditedContentTypeBackToMeta() {
+    HttpPostMeta meta = new HttpPostMeta();
+    meta.setDefault();
+    PipelineMeta pipelineMeta = pipelineWith(meta);
+
+    withDialog(
+        parent -> new HttpPostDialog(parent, new Variables(), meta, 
pipelineMeta).open(),
+        bot -> {
+          SWTBot dialog = bot.shell(SHELL_TITLE).activate().bot();
+          SWTBotCCombo contentType = contentTypeCombo(dialog);
+          // the text/xml default round-trips into the dialog
+          assertEquals("text/xml", contentType.getText());
+          contentType.setText("application/json");
+          dialog.button(buttonLabel("System.Button.OK")).click();
+        });
+
+    assertEquals("application/json", meta.getContentType());
+  }
+
+  @Test
+  void cancelLeavesContentTypeUntouched() {
+    HttpPostMeta meta = new HttpPostMeta();
+    meta.setDefault();
+    PipelineMeta pipelineMeta = pipelineWith(meta);
+
+    withDialog(
+        parent -> new HttpPostDialog(parent, new Variables(), meta, 
pipelineMeta).open(),
+        bot -> {
+          SWTBot dialog = bot.shell(SHELL_TITLE).activate().bot();
+          contentTypeCombo(dialog).setText("application/json"); // edit then 
cancel
+          dialog.button(buttonLabel("System.Button.Cancel")).click();
+        });
+
+    assertEquals(
+        "text/xml", meta.getContentType(), "Cancel must not write the edited 
content type");
+  }
+
+  /**
+   * Finds the content-type combo by content rather than a fixed widget index, 
so the test survives
+   * layout reordering. It is the only {@code CCombo} in the dialog 
pre-populated with media types.
+   */
+  private static SWTBotCCombo contentTypeCombo(SWTBot dialog) {
+    for (int i = 0; i < 16; i++) {
+      try {
+        SWTBotCCombo combo = dialog.ccomboBox(i);
+        if (Arrays.asList(combo.items()).contains("application/json")) {
+          return combo;
+        }
+      } catch (WidgetNotFoundException e) {
+        break; // ran out of combos
+      }
+    }
+    return fail("Content type combo not found in HttpPostDialog");
+  }
+
+  private static PipelineMeta pipelineWith(HttpPostMeta meta) {
+    String pluginId = 
PluginRegistry.getInstance().getPluginId(TransformPluginType.class, meta);
+    assertNotNull(
+        pluginId, "HTTP Post transform plugin must be registered via 
HopEnvironment.init()");
+    PipelineMeta pipelineMeta = new PipelineMeta();
+    pipelineMeta.addTransform(new TransformMeta(pluginId, TRANSFORM_NAME, 
meta));
+    return pipelineMeta;
+  }
+}
diff --git 
a/plugins/transforms/httppost/src/test/java/org/apache/hop/pipeline/transforms/httppost/HttpPostExecutionTest.java
 
b/plugins/transforms/httppost/src/test/java/org/apache/hop/pipeline/transforms/httppost/HttpPostExecutionTest.java
index 14c385561a..8bdf9655f3 100644
--- 
a/plugins/transforms/httppost/src/test/java/org/apache/hop/pipeline/transforms/httppost/HttpPostExecutionTest.java
+++ 
b/plugins/transforms/httppost/src/test/java/org/apache/hop/pipeline/transforms/httppost/HttpPostExecutionTest.java
@@ -342,6 +342,20 @@ class HttpPostExecutionTest {
     assertTrue(lastRequestContentType.get().contains(Const.UTF_8));
   }
 
+  @Test
+  void callHttpPOSTUsesConfiguredContentType() throws Exception {
+    HttpPostMeta meta = bodyOnlyMeta();
+    HttpPostData data = minimalData(baseUrl() + "/echo");
+    data.realContentType = "application/json";
+
+    HttpPost http = newPost(meta, data);
+    http.callHttpPOST(new Object[0]);
+
+    assertNotNull(lastRequestContentType.get());
+    assertTrue(lastRequestContentType.get().contains("application/json"));
+    assertTrue(!lastRequestContentType.get().contains("text/xml"));
+  }
+
   // ---- reflection helpers ----
 
   private static Long getDataVolumeIn(HttpPost http) throws Exception {

Reply via email to