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 {