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 ccfd55021d issue #7308 : OData Input (#7362)
ccfd55021d is described below

commit ccfd55021d578c0df2c0422e58ba95feaea23328
Author: Matt Casters <[email protected]>
AuthorDate: Mon Jun 29 16:07:24 2026 +0200

    issue #7308 : OData Input (#7362)
---
 assemblies/plugins/pom.xml                         |   6 +
 .../integration-tests/integration-tests-odata.yaml |  39 ++
 .../transforms/odata-input-connection-tab.png      | Bin 0 -> 74195 bytes
 .../images/transforms/odata-input-fields-tab.png   | Bin 0 -> 44162 bytes
 .../images/transforms/odata-input-query-tab.png    | Bin 0 -> 54801 bytes
 docs/hop-user-manual/modules/ROOT/nav.adoc         |   1 +
 .../modules/ROOT/pages/pipeline/transforms.adoc    |   1 +
 .../pages/pipeline/transforms/odata-input.adoc     | 104 +++
 integration-tests/odata/0001-odata-query.hpl       | 163 +++++
 integration-tests/odata/dev-env-config.json        |   9 +
 integration-tests/odata/hop-config.json            |  34 +
 .../odata/import/initializerJson.json              |  58 ++
 integration-tests/odata/import/metadata.xml        |  35 +
 integration-tests/odata/main-0001-odata-query.hwf  | 106 +++
 .../metadata/pipeline-run-configuration/local.json |  18 +
 .../metadata/workflow-run-configuration/local.json |  10 +
 integration-tests/odata/project-config.json        |  13 +
 plugins/transforms/odata/pom.xml                   |  32 +
 plugins/transforms/odata/src/assembly/assembly.xml |  44 ++
 .../pipeline/transforms/odata/ODataAuthType.java   |  65 ++
 .../hop/pipeline/transforms/odata/ODataField.java  | 100 +++
 .../hop/pipeline/transforms/odata/ODataInput.java  | 312 +++++++++
 .../pipeline/transforms/odata/ODataInputData.java  |  41 ++
 .../transforms/odata/ODataInputDialog.java         | 730 +++++++++++++++++++++
 .../pipeline/transforms/odata/ODataInputMeta.java  | 174 +++++
 .../odata/src/main/resources/dependencies.xml      |  21 +
 .../transforms/odata/src/main/resources/odata.svg  |  40 ++
 .../odata/messages/messages_en_US.properties       |  52 ++
 .../odata/src/main/resources/version.xml           |  20 +
 plugins/transforms/pom.xml                         |   1 +
 30 files changed, 2229 insertions(+)

diff --git a/assemblies/plugins/pom.xml b/assemblies/plugins/pom.xml
index c53aac3253..ee02975c13 100644
--- a/assemblies/plugins/pom.xml
+++ b/assemblies/plugins/pom.xml
@@ -1291,6 +1291,12 @@
             <version>${project.version}</version>
             <type>zip</type>
         </dependency>
+        <dependency>
+            <groupId>org.apache.hop</groupId>
+            <artifactId>hop-transform-odata</artifactId>
+            <version>${project.version}</version>
+            <type>zip</type>
+        </dependency>
         <dependency>
             <groupId>org.apache.hop</groupId>
             <artifactId>hop-transform-pgp</artifactId>
diff --git a/docker/integration-tests/integration-tests-odata.yaml 
b/docker/integration-tests/integration-tests-odata.yaml
new file mode 100644
index 0000000000..0a61c9227e
--- /dev/null
+++ b/docker/integration-tests/integration-tests-odata.yaml
@@ -0,0 +1,39 @@
+# 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.
+
+services:
+  integration_test_database:
+    extends:
+      file: integration-tests-base.yaml
+      service: integration_test
+    depends_on:
+      odata_mock:
+        condition: service_started
+    links:
+      - odata_mock
+
+  odata_mock:
+    image: mockserver/mockserver:5.15.0
+    hostname: odata_mock
+    ports:
+      - "1080"
+    environment:
+      - MOCKSERVER_INITIALIZATION_JSON_PATH=/config/initializerJson.json
+      - MOCKSERVER_LOG_LEVEL=WARN
+    volumes:
+      - 
../../integration-tests/odata/import/initializerJson.json:/config/initializerJson.json
+      - ../../integration-tests/odata/import/metadata.xml:/config/metadata.xml
diff --git 
a/docs/hop-user-manual/modules/ROOT/assets/images/transforms/odata-input-connection-tab.png
 
b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/odata-input-connection-tab.png
new file mode 100644
index 0000000000..53f7bb8191
Binary files /dev/null and 
b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/odata-input-connection-tab.png
 differ
diff --git 
a/docs/hop-user-manual/modules/ROOT/assets/images/transforms/odata-input-fields-tab.png
 
b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/odata-input-fields-tab.png
new file mode 100644
index 0000000000..d54f81ac52
Binary files /dev/null and 
b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/odata-input-fields-tab.png
 differ
diff --git 
a/docs/hop-user-manual/modules/ROOT/assets/images/transforms/odata-input-query-tab.png
 
b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/odata-input-query-tab.png
new file mode 100644
index 0000000000..edaf6fa550
Binary files /dev/null and 
b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/odata-input-query-tab.png
 differ
diff --git a/docs/hop-user-manual/modules/ROOT/nav.adoc 
b/docs/hop-user-manual/modules/ROOT/nav.adoc
index e8d04fc058..972aeac08d 100644
--- a/docs/hop-user-manual/modules/ROOT/nav.adoc
+++ b/docs/hop-user-manual/modules/ROOT/nav.adoc
@@ -209,6 +209,7 @@ under the License.
 *** xref:pipeline/transforms/neo4j-split-graph.adoc[Neo4j Split Graph]
 *** xref:pipeline/transforms/nullif.adoc[Null If]
 *** xref:pipeline/transforms/numberrange.adoc[Number range]
+*** xref:pipeline/transforms/odata-input.adoc[OData Input]
 *** xref:pipeline/transforms/orabulkloader.adoc[Oracle Bulk Loader]
 *** xref:pipeline/transforms/parquet-file-input.adoc[Parquet File Input]
 *** xref:pipeline/transforms/parquet-file-output.adoc[Parquet File Output]
diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc
index 4933b31be7..40e598f0b1 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc
@@ -166,6 +166,7 @@ The pages nested under this topic contain information on 
how to use the transfor
 * xref:pipeline/transforms/neo4j-split-graph.adoc[Neo4j Split Graph]
 * xref:pipeline/transforms/nullif.adoc[Null If]
 * xref:pipeline/transforms/numberrange.adoc[Number range]
+* xref:pipeline/transforms/odata-input.adoc[OData Input]
 * xref:pipeline/transforms/orabulkloader.adoc[Oracle Bulk Loader]
 * xref:pipeline/transforms/parquet-file-input.adoc[Parquet File Input]
 * xref:pipeline/transforms/parquet-file-output.adoc[Parquet File Output]
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/odata-input.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/odata-input.adoc
new file mode 100644
index 0000000000..55783c0c7e
--- /dev/null
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/odata-input.adoc
@@ -0,0 +1,104 @@
+////
+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.
+////
+:documentationPath: /pipeline/transforms/
+:language: en_US
+:description: The OData Input transform retrieves data from OData V2 and V4 
service endpoints.
+
+= image:transforms/icons/odata.svg[OData Input transform Icon, 
role="image-doc-icon"] OData Input
+
+[%noheader,cols="3a,1a", role="table-no-borders" ]
+|===
+|
+== Description
+
+The OData Input transform enables you to query OData (Open Data Protocol) 
services. OData is an OASIS standard that defines a set of best practices for 
building and consuming RESTful APIs. For more information, visit the official 
link:https://www.odata.org/[OData Documentation].
+
+This transform performs GET requests against OData Entity Sets, maps JSON 
properties to Apache Hop rows, and automatically handles relative pagination 
using standard `@odata.nextLink` (OData V4) and `__next` (OData V2) metadata 
attributes.
+
+|
+== Supported Engines
+[%noheader,cols="2,1a",frame=none, role="table-supported-engines"]
+!===
+!Hop Engine! image:check_mark.svg[Supported, 24]
+!Spark! image:question_mark.svg[Maybe Supported, 24]
+!Flink! image:question_mark.svg[Maybe Supported, 24]
+!Dataflow! image:question_mark.svg[Maybe Supported, 24]
+!===
+|===
+
+== Connection Options
+
+The Connection tab allows you to configure the service endpoint and optional 
credentials.
+
+image::transforms/odata-input-connection-tab.png[OData Input Connection Tab, 
align="center"]
+
+[options="header"]
+|===
+|Option|Description
+|Transform name|Name of this transform as it appears in the pipeline workspace.
+|OData Service Root URL|The base URL of the OData service (e.g., 
`https://services.odata.org/V4/TripPinServiceRW/`).
+|Authentication Type|The authentication type to use:
+* `No Authentication`: Anonymous connection.
+* `Basic Authentication`: Use standard Username/Password credentials.
+* `Bearer Token`: Use a Bearer token in the `Authorization` header.
+|Username (Basic Auth)|The username for Basic authentication.
+|Password (Basic Auth)|The password for Basic authentication.
+|Token (Bearer / OAuth2)|The bearer token value.
+|OData Entity Set|The OData entity set to query (e.g., `People` or `Airports`).
+|Get Entity Sets|Retrieves and displays a drop-down list of available entity 
sets from the service endpoint.
+|===
+
+== Query Options
+
+The OData Query tab allows you to configure filtering, ordering, and field 
selection properties.
+
+image::transforms/odata-input-query-tab.png[OData Input Query Tab, 
align="center"]
+
+[options="header"]
+|===
+|Option|Description
+|$select|A comma-separated list of properties to select (e.g., 
`UserName,FirstName,LastName`).
+|$filter|An OData expression to filter matching entities (e.g., `Gender eq 
'Male'`).
+|$orderby|Specifies the sorting order of the results (e.g., `LastName asc, 
FirstName desc`).
+|$top|Limits the number of entities returned on each page request (e.g., `50`).
+|$skip|Specifies the number of entities to skip from the beginning of the list.
+|===
+
+== Fields Mapping
+
+The Fields tab defines how properties in the returned OData JSON response map 
to Apache Hop row fields.
+
+image::transforms/odata-input-fields-tab.png[OData Input Fields Tab, 
align="center"]
+
+[options="header"]
+|===
+|Option|Description
+|Name in Hop|The name of the field as it will be produced in the Apache Hop 
pipeline.
+|OData Path|The JSON property path of the field within the entity JSON object 
(e.g., `UserName`, `AddressInfo/0/City/Name` for nested structures).
+|Type|The Hop data type (String, Integer, Number, Date, Boolean, BigNumber).
+|Format|Optional parsing format for date or numeric fields.
+|Get Fields|Connects to the `$metadata` endpoint of the OData service and 
automatically populates the fields list based on the selected Entity Set's 
schema properties.
+|===
+
+== Example
+
+To query a public OData service:
+1. Set the **Service Root URL** to 
`https://services.odata.org/V4/TripPinServiceRW/`.
+2. Set the **Entity Set** to `People`.
+3. Go to the **Fields** tab and click **Get Fields** to pull all metadata 
fields (`UserName`, `FirstName`, `LastName`, etc.).
+4. (Optional) In the **OData Query** tab, add a `$filter` such as `FirstName 
eq 'John'` or a `$select` of `UserName,Emails`.
+5. Run the pipeline to stream entities into downstream transforms.
diff --git a/integration-tests/odata/0001-odata-query.hpl 
b/integration-tests/odata/0001-odata-query.hpl
new file mode 100644
index 0000000000..85c89395a6
--- /dev/null
+++ b/integration-tests/odata/0001-odata-query.hpl
@@ -0,0 +1,163 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~
+  -->
+<pipeline>
+  <info>
+    <name>0001-odata-query</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+  </info>
+  <order>
+    <hop>
+      <from>OData Input</from>
+      <to>count</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>count</from>
+      <to>check count</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>check count</from>
+      <to>Abort</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>OData Input</name>
+    <type>ODataInput</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <url>http://odata_mock:1080/odata</url>
+    <entity_set>Customers</entity_set>
+    <auth_type>NONE</auth_type>
+    <fields>
+      <field>
+        <name>Id</name>
+        <path>Id</path>
+        <type>Integer</type>
+      </field>
+      <field>
+        <name>Name</name>
+        <path>Name</path>
+        <type>String</type>
+      </field>
+      <field>
+        <name>Balance</name>
+        <path>Balance</path>
+        <type>Number</type>
+      </field>
+    </fields>
+    <GUI>
+      <xloc>100</xloc>
+      <yloc>100</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>count</name>
+    <type>MemoryGroupBy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <aggregate>count</aggregate>
+        <type>COUNT_ANY</type>
+      </field>
+    </fields>
+    <give_back_row>N</give_back_row>
+    <group>
+    </group>
+    <GUI>
+      <xloc>250</xloc>
+      <yloc>100</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>check count</name>
+    <type>FilterRows</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <compare>
+      <condition>
+        <conditions>
+        </conditions>
+        <function>&lt;&gt;</function>
+        <leftvalue>count</leftvalue>
+        <negated>N</negated>
+        <operator>-</operator>
+        <value>
+          <isnull>N</isnull>
+          <length>-1</length>
+          <mask>####0;-####0</mask>
+          <name>constant</name>
+          <precision>0</precision>
+          <text>3</text>
+          <type>Integer</type>
+        </value>
+      </condition>
+    </compare>
+    <GUI>
+      <xloc>400</xloc>
+      <yloc>100</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Abort</name>
+    <type>Abort</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <abort_option>ABORT_WITH_ERROR</abort_option>
+    <always_log_rows>Y</always_log_rows>
+    <message>Incorrect number of OData records received</message>
+    <row_threshold>0</row_threshold>
+    <GUI>
+      <xloc>550</xloc>
+      <yloc>100</yloc>
+    </GUI>
+  </transform>
+</pipeline>
diff --git a/integration-tests/odata/dev-env-config.json 
b/integration-tests/odata/dev-env-config.json
new file mode 100644
index 0000000000..41e7cbaee0
--- /dev/null
+++ b/integration-tests/odata/dev-env-config.json
@@ -0,0 +1,9 @@
+{
+  "variables" : [
+    {
+      "name" : "HOSTNAME",
+      "value" : "odata_mock",
+      "description" : "hostname of odata_mock container"
+    }
+  ]
+}
diff --git a/integration-tests/odata/hop-config.json 
b/integration-tests/odata/hop-config.json
new file mode 100644
index 0000000000..4ae5d9cd60
--- /dev/null
+++ b/integration-tests/odata/hop-config.json
@@ -0,0 +1,34 @@
+{
+  "variables": [],
+  "LocaleDefault": "en_US",
+  "guiProperties": {
+    "DarkMode": "Y"
+  },
+  "projectsConfig": {
+    "enabled": true,
+    "projectMandatory": true,
+    "environmentMandatory": false,
+    "defaultProject": "odata",
+    "defaultEnvironment": null,
+    "standardParentProject": "odata",
+    "standardProjectsFolder": null,
+    "projectConfigurations": [
+      {
+        "projectName": "odata",
+        "projectHome": "${HOP_CONFIG_FOLDER}",
+        "configFilename": "project-config.json"
+      }
+    ],
+    "lifecycleEnvironments": [
+      {
+        "name": "dev",
+        "purpose": "Testing",
+        "projectName": "odata",
+        "configurationFiles": [
+          "${PROJECT_HOME}/dev-env-config.json"
+        ]
+      }
+    ],
+    "projectLifecycles": []
+  }
+}
diff --git a/integration-tests/odata/import/initializerJson.json 
b/integration-tests/odata/import/initializerJson.json
new file mode 100644
index 0000000000..ae6d4893c3
--- /dev/null
+++ b/integration-tests/odata/import/initializerJson.json
@@ -0,0 +1,58 @@
+[
+  {
+    "httpRequest": {
+      "method": "GET",
+      "path": "/odata"
+    },
+    "httpResponse": {
+      "statusCode": 200,
+      "headers": {
+        "Content-Type": ["application/json; charset=utf-8"]
+      },
+      "body": "{\"value\": [{\"name\": \"Customers\", \"kind\": \"EntitySet\", 
\"url\": \"Customers\"}]}"
+    }
+  },
+  {
+    "httpRequest": {
+      "method": "GET",
+      "path": "/odata/$metadata"
+    },
+    "httpResponse": {
+      "statusCode": 200,
+      "headers": {
+        "Content-Type": ["application/xml; charset=utf-8"]
+      },
+      "body": "<?xml version=\"1.0\" encoding=\"utf-8\"?><edmx:Edmx 
Version=\"4.0\" 
xmlns:edmx=\"http://docs.oasis-open.org/odata/ns/edmx\";><edmx:DataServices><Schema
 Namespace=\"Hop.Test\" 
xmlns=\"http://docs.oasis-open.org/odata/ns/edm\";><EntityType 
Name=\"Customer\"><Key><PropertyRef Name=\"Id\"/></Key><Property Name=\"Id\" 
Type=\"Edm.Int32\" Nullable=\"false\"/><Property Name=\"Name\" 
Type=\"Edm.String\" Nullable=\"false\"/><Property Name=\"Balance\" 
Type=\"Edm.Decimal\"/></EntityTyp [...]
+    }
+  },
+  {
+    "httpRequest": {
+      "method": "GET",
+      "path": "/odata/Customers"
+    },
+    "httpResponse": {
+      "statusCode": 200,
+      "headers": {
+        "Content-Type": ["application/json; charset=utf-8"]
+      },
+      "body": "{\"value\": [{\"Id\": 1, \"Name\": \"Alice\", \"Balance\": 
100.50}, {\"Id\": 2, \"Name\": \"Bob\", \"Balance\": 250.00}], 
\"@odata.nextLink\": \"http://odata_mock:1080/odata/Customers?%24skip=2\"}";
+    }
+  },
+  {
+    "priority": 10,
+    "httpRequest": {
+      "method": "GET",
+      "path": "/odata/Customers",
+      "queryStringParameters": {
+        "$skip": ["2"]
+      }
+    },
+    "httpResponse": {
+      "statusCode": 200,
+      "headers": {
+        "Content-Type": ["application/json; charset=utf-8"]
+      },
+      "body": "{\"value\": [{\"Id\": 3, \"Name\": \"Charlie\", \"Balance\": 
0.00}]}"
+    }
+  }
+]
diff --git a/integration-tests/odata/import/metadata.xml 
b/integration-tests/odata/import/metadata.xml
new file mode 100644
index 0000000000..5582adc51d
--- /dev/null
+++ b/integration-tests/odata/import/metadata.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  ~
+  -->
+<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx";>
+  <edmx:DataServices>
+    <Schema Namespace="Hop.Test" 
xmlns="http://docs.oasis-open.org/odata/ns/edm";>
+      <EntityType Name="Customer">
+        <Key>
+          <PropertyRef Name="Id"/>
+        </Key>
+        <Property Name="Id" Type="Edm.Int32" Nullable="false"/>
+        <Property Name="Name" Type="Edm.String" Nullable="false"/>
+        <Property Name="Balance" Type="Edm.Decimal"/>
+      </EntityType>
+      <EntityContainer Name="DefaultContainer">
+        <EntitySet Name="Customers" EntityType="Hop.Test.Customer"/>
+      </EntityContainer>
+    </Schema>
+  </edmx:DataServices>
+</edmx:Edmx>
diff --git a/integration-tests/odata/main-0001-odata-query.hwf 
b/integration-tests/odata/main-0001-odata-query.hwf
new file mode 100644
index 0000000000..8e4034574b
--- /dev/null
+++ b/integration-tests/odata/main-0001-odata-query.hwf
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~
+  -->
+<workflow>
+  <name>main-0001-odata-query</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description/>
+  <extended_description/>
+  <workflow_version/>
+  <workflow_status>0</workflow_status>
+  <parameters>
+  </parameters>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <intervalSeconds>0</intervalSeconds>
+      <intervalMinutes>60</intervalMinutes>
+      <hour>12</hour>
+      <minutes>0</minutes>
+      <weekDay>1</weekDay>
+      <DayOfMonth>1</DayOfMonth>
+      <parallel>N</parallel>
+      <xloc>50</xloc>
+      <yloc>50</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>0001-odata-query.hpl</name>
+      <description/>
+      <type>PIPELINE</type>
+      <attributes/>
+      <filename>${PROJECT_HOME}/0001-odata-query.hpl</filename>
+      <params_from_previous>N</params_from_previous>
+      <exec_per_row>N</exec_per_row>
+      <clear_rows>N</clear_rows>
+      <clear_files>N</clear_files>
+      <set_logfile>N</set_logfile>
+      <logfile/>
+      <logext/>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <loglevel>Basic</loglevel>
+      <set_append_logfile>N</set_append_logfile>
+      <wait_until_finished>Y</wait_until_finished>
+      <follow_abort_remote>N</follow_abort_remote>
+      <create_parent_folder>N</create_parent_folder>
+      <run_configuration>local</run_configuration>
+      <parameters>
+        <pass_all_parameters>Y</pass_all_parameters>
+      </parameters>
+      <parallel>N</parallel>
+      <xloc>200</xloc>
+      <yloc>50</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Abort workflow</name>
+      <description/>
+      <type>ABORT</type>
+      <attributes/>
+      <parallel>N</parallel>
+      <xloc>200</xloc>
+      <yloc>180</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>0001-odata-query.hpl</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+    <hop>
+      <from>0001-odata-query.hpl</from>
+      <to>Abort workflow</to>
+      <enabled>Y</enabled>
+      <evaluation>N</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git 
a/integration-tests/odata/metadata/pipeline-run-configuration/local.json 
b/integration-tests/odata/metadata/pipeline-run-configuration/local.json
new file mode 100644
index 0000000000..5d38d84e25
--- /dev/null
+++ b/integration-tests/odata/metadata/pipeline-run-configuration/local.json
@@ -0,0 +1,18 @@
+{
+  "engineRunConfiguration": {
+    "Local": {
+      "feedback_size": "50000",
+      "sample_size": "100",
+      "sample_type_in_gui": "Last",
+      "rowset_size": "10000",
+      "safe_mode": false,
+      "show_feedback": false,
+      "topo_sort": false,
+      "gather_metrics": false,
+      "transactional": false
+    }
+  },
+  "configurationVariables": [],
+  "name": "local",
+  "description": "Runs your pipelines locally with the standard local Hop 
pipeline engine"
+}
diff --git 
a/integration-tests/odata/metadata/workflow-run-configuration/local.json 
b/integration-tests/odata/metadata/workflow-run-configuration/local.json
new file mode 100644
index 0000000000..eec94f94e8
--- /dev/null
+++ b/integration-tests/odata/metadata/workflow-run-configuration/local.json
@@ -0,0 +1,10 @@
+{
+  "engineRunConfiguration": {
+    "Local": {
+      "safe_mode": false,
+      "transactional": false
+    }
+  },
+  "name": "local",
+  "description": "Runs your workflows locally with the standard local Hop 
workflow engine"
+}
diff --git a/integration-tests/odata/project-config.json 
b/integration-tests/odata/project-config.json
new file mode 100644
index 0000000000..793afe289e
--- /dev/null
+++ b/integration-tests/odata/project-config.json
@@ -0,0 +1,13 @@
+{
+  "metadataBaseFolder" : "${PROJECT_HOME}/metadata",
+  "unitTestsBasePath" : "${PROJECT_HOME}",
+  "dataSetsCsvFolder" : "${PROJECT_HOME}/datasets",
+  "enforcingExecutionInHome" : true,
+  "config" : {
+    "variables" : [ {
+      "name" : "HOP_LICENSE_HEADER_FILE",
+      "value" : "${PROJECT_HOME}/../asf-header.txt",
+      "description" : "This will automatically serialize the ASF license 
header into pipelines and workflows in the integration test projects"
+    } ]
+  }
+}
diff --git a/plugins/transforms/odata/pom.xml b/plugins/transforms/odata/pom.xml
new file mode 100644
index 0000000000..7eb4841124
--- /dev/null
+++ b/plugins/transforms/odata/pom.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.hop</groupId>
+        <artifactId>hop-plugins-transforms</artifactId>
+        <version>2.19.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>hop-transform-odata</artifactId>
+    <packaging>jar</packaging>
+    <name>Hop Plugins Transforms OData</name>
+
+</project>
diff --git a/plugins/transforms/odata/src/assembly/assembly.xml 
b/plugins/transforms/odata/src/assembly/assembly.xml
new file mode 100644
index 0000000000..5d6b0b0869
--- /dev/null
+++ b/plugins/transforms/odata/src/assembly/assembly.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~
+  -->
+
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0";
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+    xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 
http://maven.apache.org/xsd/assembly-2.2.0.xsd";>
+    <id>hop-transform-odata</id>
+    <formats>
+        <format>zip</format>
+    </formats>
+    <baseDirectory>.</baseDirectory>
+    <files>
+        <file>
+            <source>${project.basedir}/src/main/resources/version.xml</source>
+            <outputDirectory>${hop.plugin.libdir}</outputDirectory>
+            <filtered>true</filtered>
+        </file>
+        <file>
+            
<source>${project.basedir}/src/main/resources/dependencies.xml</source>
+            <outputDirectory>${hop.plugin.libdir}</outputDirectory>
+            <filtered>true</filtered>
+        </file>
+    </files>
+
+    <componentDescriptors>
+        
<componentDescriptor>${maven.multiModuleProjectDirectory}/assemblies/shared/hop-plugin-libs.xml</componentDescriptor>
+    </componentDescriptors>
+</assembly>
diff --git 
a/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataAuthType.java
 
b/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataAuthType.java
new file mode 100644
index 0000000000..fc22c60508
--- /dev/null
+++ 
b/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataAuthType.java
@@ -0,0 +1,65 @@
+/*
+ * 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.odata;
+
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.metadata.api.IEnumHasCodeAndDescription;
+
+public enum ODataAuthType implements IEnumHasCodeAndDescription {
+  NONE("NONE", "ODataAuthType.None.Description"),
+  BASIC("BASIC", "ODataAuthType.Basic.Description"),
+  BEARER("BEARER", "ODataAuthType.Bearer.Description");
+
+  private static final Class<?> PKG = ODataAuthType.class;
+
+  private final String code;
+  private final String descriptionKey;
+
+  ODataAuthType(String code, String descriptionKey) {
+    this.code = code;
+    this.descriptionKey = descriptionKey;
+  }
+
+  @Override
+  public String getCode() {
+    return code;
+  }
+
+  @Override
+  public String getDescription() {
+    return BaseMessages.getString(PKG, descriptionKey);
+  }
+
+  public static String[] getDescriptions() {
+    return IEnumHasCodeAndDescription.getDescriptions(ODataAuthType.class);
+  }
+
+  public static ODataAuthType lookupDescription(String description) {
+    return IEnumHasCodeAndDescription.lookupDescription(
+        ODataAuthType.class, description, ODataAuthType.NONE);
+  }
+
+  public static ODataAuthType lookupCode(String code) {
+    for (ODataAuthType val : values()) {
+      if (val.getCode().equalsIgnoreCase(code)) {
+        return val;
+      }
+    }
+    return NONE;
+  }
+}
diff --git 
a/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataField.java
 
b/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataField.java
new file mode 100644
index 0000000000..241e9adc59
--- /dev/null
+++ 
b/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataField.java
@@ -0,0 +1,100 @@
+/*
+ * 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.odata;
+
+import java.util.Objects;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.core.exception.HopPluginException;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.value.ValueMetaBase;
+import org.apache.hop.core.row.value.ValueMetaFactory;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+
+@Getter
+@Setter
+public class ODataField {
+  @HopMetadataProperty(key = "name")
+  private String name;
+
+  @HopMetadataProperty(key = "path")
+  private String path;
+
+  @HopMetadataProperty(key = "type", intCodeConverter = 
ValueMetaBase.ValueTypeCodeConverter.class)
+  private int type;
+
+  @HopMetadataProperty(key = "format")
+  private String format;
+
+  public ODataField() {
+    this.name = "";
+    this.path = "";
+    this.type = IValueMeta.TYPE_STRING;
+    this.format = "";
+  }
+
+  public ODataField(String name, String path, int type, String format) {
+    this.name = name;
+    this.path = path;
+    this.type = type;
+    this.format = format;
+  }
+
+  public ODataField(ODataField other) {
+    this.name = other.name;
+    this.path = other.path;
+    this.type = other.type;
+    this.format = other.format;
+  }
+
+  @Override
+  public ODataField clone() {
+    return new ODataField(this);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof ODataField other)) {
+      return false;
+    }
+    return Objects.equals(name, other.name)
+        && Objects.equals(path, other.path)
+        && type == other.type
+        && Objects.equals(format, other.format);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(name, path, type, format);
+  }
+
+  public IValueMeta toValueMeta(String originTransformName, IVariables 
variables)
+      throws HopPluginException {
+    int hopType = getType();
+    if (hopType == IValueMeta.TYPE_NONE) {
+      hopType = IValueMeta.TYPE_STRING;
+    }
+    IValueMeta v =
+        ValueMetaFactory.createValueMeta(
+            variables != null ? variables.resolve(getName()) : getName(), 
hopType);
+    v.setOrigin(originTransformName);
+    v.setConversionMask(getFormat());
+    return v;
+  }
+}
diff --git 
a/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataInput.java
 
b/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataInput.java
new file mode 100644
index 0000000000..261b45c024
--- /dev/null
+++ 
b/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataInput.java
@@ -0,0 +1,312 @@
+/*
+ * 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.odata;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.RowDataUtil;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.util.HttpClientManager;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.pipeline.Pipeline;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.BaseTransform;
+import org.apache.hop.pipeline.transform.TransformMeta;
+
+public class ODataInput extends BaseTransform<ODataInputMeta, ODataInputData> {
+  private static final Class<?> PKG = ODataInputMeta.class;
+
+  public ODataInput(
+      TransformMeta transformMeta,
+      ODataInputMeta meta,
+      ODataInputData data,
+      int copyNr,
+      PipelineMeta pipelineMeta,
+      Pipeline pipeline) {
+    super(transformMeta, meta, data, copyNr, pipelineMeta, pipeline);
+  }
+
+  @Override
+  public boolean init() {
+    if (super.init()) {
+      try {
+        HttpClientManager.HttpClientBuilderFacade builder =
+            HttpClientManager.getInstance().createBuilder();
+        if ("BASIC".equalsIgnoreCase(meta.getAuthType()) && 
!Utils.isEmpty(meta.getUsername())) {
+          builder.setCredentials(resolve(meta.getUsername()), 
resolve(meta.getPassword()));
+        }
+        data.httpClient = builder.build();
+
+        String serviceUrl = resolve(meta.getUrl());
+        String entitySet = resolve(meta.getEntitySet());
+        if (Utils.isEmpty(serviceUrl)) {
+          logError("Service URL is empty.");
+          return false;
+        }
+        if (Utils.isEmpty(entitySet)) {
+          logError("Entity Set is empty.");
+          return false;
+        }
+
+        String rootUrl = serviceUrl;
+        if (!rootUrl.endsWith("/")) {
+          rootUrl += "/";
+        }
+        String requestUrl = rootUrl + entitySet;
+
+        List<String> queryParams = new ArrayList<>();
+        if (!Utils.isEmpty(meta.getQuerySelect())) {
+          queryParams.add(
+              "$select="
+                  + URLEncoder.encode(
+                      resolve(meta.getQuerySelect()), 
StandardCharsets.UTF_8.name()));
+        }
+        if (!Utils.isEmpty(meta.getQueryFilter())) {
+          queryParams.add(
+              "$filter="
+                  + URLEncoder.encode(
+                      resolve(meta.getQueryFilter()), 
StandardCharsets.UTF_8.name()));
+        }
+        if (!Utils.isEmpty(meta.getQueryOrder())) {
+          queryParams.add(
+              "$orderby="
+                  + URLEncoder.encode(
+                      resolve(meta.getQueryOrder()), 
StandardCharsets.UTF_8.name()));
+        }
+        if (!Utils.isEmpty(meta.getQueryTop())) {
+          queryParams.add(
+              "$top="
+                  + URLEncoder.encode(resolve(meta.getQueryTop()), 
StandardCharsets.UTF_8.name()));
+        }
+        if (!Utils.isEmpty(meta.getQuerySkip())) {
+          queryParams.add(
+              "$skip="
+                  + URLEncoder.encode(resolve(meta.getQuerySkip()), 
StandardCharsets.UTF_8.name()));
+        }
+
+        if (!queryParams.isEmpty()) {
+          requestUrl += "?" + String.join("&", queryParams);
+        }
+        data.nextPageUrl = requestUrl;
+        data.isFinishedReading = false;
+
+        return true;
+      } catch (Exception e) {
+        logError("Error initializing OData HTTP Client", e);
+        return false;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public boolean processRow() throws HopException {
+    if (first) {
+      first = false;
+      data.outputRowMeta = new RowMeta();
+      meta.getFields(data.outputRowMeta, getTransformName(), null, null, this, 
metadataProvider);
+    }
+
+    if (data.recordIndex < data.recordBuffer.size()) {
+      Object[] row = data.recordBuffer.get(data.recordIndex++);
+      putRow(data.outputRowMeta, row);
+      return true;
+    }
+
+    if (data.isFinishedReading && data.recordIndex >= 
data.recordBuffer.size()) {
+      setOutputDone();
+      return false;
+    }
+
+    // Fetch next page of data
+    fetchNextPage();
+
+    if (data.recordBuffer.isEmpty()) {
+      setOutputDone();
+      return false;
+    }
+
+    Object[] row = data.recordBuffer.get(data.recordIndex++);
+    putRow(data.outputRowMeta, row);
+    return true;
+  }
+
+  private void fetchNextPage() throws HopException {
+    if (data.nextPageUrl == null || data.nextPageUrl.isEmpty()) {
+      data.isFinishedReading = true;
+      return;
+    }
+
+    try {
+      HttpGet get = new HttpGet(data.nextPageUrl);
+      get.setHeader("Accept", "application/json");
+      if ("BEARER".equalsIgnoreCase(meta.getAuthType()) && 
!Utils.isEmpty(meta.getToken())) {
+        get.setHeader("Authorization", "Bearer " + resolve(meta.getToken()));
+      }
+
+      try (CloseableHttpResponse response = data.httpClient.execute(get)) {
+        int statusCode = response.getCode();
+        if (statusCode != 200) {
+          throw new HopException("OData service returned non-200 status code: 
" + statusCode);
+        }
+
+        HttpEntity entity = response.getEntity();
+        String body = entity != null ? EntityUtils.toString(entity, 
StandardCharsets.UTF_8) : "";
+
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode rootNode = mapper.readTree(body);
+
+        JsonNode recordsNode = null;
+        String nextLink = null;
+
+        if (rootNode.has("value")) {
+          // OData V4
+          recordsNode = rootNode.get("value");
+          if (rootNode.has("@odata.nextLink")) {
+            nextLink = rootNode.get("@odata.nextLink").asText();
+          }
+        } else if (rootNode.has("d")) {
+          // OData V2
+          JsonNode dNode = rootNode.get("d");
+          if (dNode.has("results")) {
+            recordsNode = dNode.get("results");
+          } else if (dNode.isArray()) {
+            recordsNode = dNode;
+          } else {
+            // Single object wrapper
+            recordsNode = mapper.createArrayNode();
+            ((com.fasterxml.jackson.databind.node.ArrayNode) 
recordsNode).add(dNode);
+          }
+
+          if (dNode.has("__next")) {
+            nextLink = dNode.get("__next").asText();
+          }
+        } else {
+          // General json backup
+          if (rootNode.isArray()) {
+            recordsNode = rootNode;
+          } else {
+            recordsNode = mapper.createArrayNode();
+            ((com.fasterxml.jackson.databind.node.ArrayNode) 
recordsNode).add(rootNode);
+          }
+        }
+
+        data.recordBuffer.clear();
+        data.recordIndex = 0;
+
+        if (recordsNode != null && recordsNode.isArray()) {
+          for (JsonNode record : recordsNode) {
+            Object[] row = 
RowDataUtil.allocateRowData(data.outputRowMeta.size());
+            for (int i = 0; i < meta.getFields().size(); i++) {
+              ODataField field = meta.getFields().get(i);
+              String path = resolve(field.getPath());
+              JsonNode valueNode = getValueByPath(record, path);
+              IValueMeta valueMeta = data.outputRowMeta.getValueMeta(i);
+
+              Object val = null;
+              if (valueNode != null && !valueNode.isNull()) {
+                if (valueMeta.isBoolean()) {
+                  val = valueNode.asBoolean();
+                } else if (valueMeta.isInteger()) {
+                  val = valueNode.asLong();
+                } else if (valueMeta.isNumber()) {
+                  val = valueNode.asDouble();
+                } else if (valueMeta.isDate()) {
+                  String text = valueNode.asText();
+                  try {
+                    val = valueMeta.convertDataFromString(text, null, null, 
null, 0);
+                  } catch (Exception ex) {
+                    logError("Failed to parse date: " + text + ", field: " + 
field.getName(), ex);
+                  }
+                } else {
+                  val = valueNode.asText();
+                }
+              }
+              row[i] = val;
+            }
+            data.recordBuffer.add(row);
+          }
+        }
+
+        if (nextLink != null && !nextLink.isEmpty()) {
+          // Resolve nextLink relative to base if needed
+          if (!nextLink.startsWith("http://";) && 
!nextLink.startsWith("https://";)) {
+            URI uri = URI.create(data.nextPageUrl);
+            String portStr = uri.getPort() != -1 ? ":" + uri.getPort() : "";
+            String base = uri.getScheme() + "://" + uri.getHost() + portStr;
+            if (nextLink.startsWith("/")) {
+              nextLink = base + nextLink;
+            } else {
+              String path = uri.getPath();
+              int idx = path.lastIndexOf("/");
+              if (idx != -1) {
+                nextLink = base + path.substring(0, idx + 1) + nextLink;
+              } else {
+                nextLink = base + "/" + nextLink;
+              }
+            }
+          }
+          data.nextPageUrl = nextLink;
+        } else {
+          data.nextPageUrl = null;
+          data.isFinishedReading = true;
+        }
+      }
+    } catch (Exception e) {
+      throw new HopException("Error requesting OData data page", e);
+    }
+  }
+
+  private JsonNode getValueByPath(JsonNode root, String path) {
+    if (root == null || path == null || path.isEmpty()) {
+      return root;
+    }
+    String[] parts = path.split("/");
+    JsonNode current = root;
+    for (String part : parts) {
+      if (current == null) {
+        return null;
+      }
+      current = current.get(part);
+    }
+    return current;
+  }
+
+  @Override
+  public void dispose() {
+    if (data.httpClient != null) {
+      try {
+        data.httpClient.close();
+      } catch (Exception e) {
+        logError("Error closing OData HTTP Client", e);
+      }
+    }
+    super.dispose();
+  }
+}
diff --git 
a/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataInputData.java
 
b/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataInputData.java
new file mode 100644
index 0000000000..566527bda9
--- /dev/null
+++ 
b/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataInputData.java
@@ -0,0 +1,41 @@
+/*
+ * 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.odata;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.pipeline.transform.BaseTransformData;
+import org.apache.hop.pipeline.transform.ITransformData;
+
+public class ODataInputData extends BaseTransformData implements 
ITransformData {
+  public IRowMeta outputRowMeta;
+  public CloseableHttpClient httpClient;
+  public String nextPageUrl;
+  public List<Object[]> recordBuffer;
+  public int recordIndex;
+  public boolean isFinishedReading;
+
+  public ODataInputData() {
+    super();
+    recordBuffer = new ArrayList<>();
+    recordIndex = 0;
+    isFinishedReading = false;
+  }
+}
diff --git 
a/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataInputDialog.java
 
b/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataInputDialog.java
new file mode 100644
index 0000000000..dcf3acadf0
--- /dev/null
+++ 
b/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataInputDialog.java
@@ -0,0 +1,730 @@
+/*
+ * 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.odata;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.Props;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.row.value.ValueMetaFactory;
+import org.apache.hop.core.util.HttpClientManager;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
+import org.apache.hop.ui.core.widget.ColumnInfo;
+import org.apache.hop.ui.core.widget.ComboVar;
+import org.apache.hop.ui.core.widget.PasswordTextVar;
+import org.apache.hop.ui.core.widget.TableView;
+import org.apache.hop.ui.core.widget.TextVar;
+import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CTabFolder;
+import org.eclipse.swt.custom.CTabItem;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.TableItem;
+import org.eclipse.swt.widgets.Text;
+
+public class ODataInputDialog extends BaseTransformDialog {
+  private static final Class<?> PKG = ODataInputMeta.class;
+
+  private final ODataInputMeta input;
+
+  private TextVar wUrl;
+  private ComboVar wEntitySet;
+  private ComboVar wAuthType;
+  private TextVar wUsername;
+  private PasswordTextVar wPassword;
+  private PasswordTextVar wToken;
+
+  private TextVar wQuerySelect;
+  private TextVar wQueryFilter;
+  private TextVar wQueryOrder;
+  private TextVar wQueryTop;
+  private TextVar wQuerySkip;
+
+  private TableView wFields;
+
+  public ODataInputDialog(
+      Shell parent, IVariables variables, ODataInputMeta transformMeta, 
PipelineMeta pipelineMeta) {
+    super(parent, variables, transformMeta, pipelineMeta);
+    input = transformMeta;
+  }
+
+  @Override
+  public String open() {
+    Shell parent = getParent();
+    shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MAX | 
SWT.MIN);
+    PropsUi.setLook(shell);
+    setShellImage(shell, input);
+
+    ModifyListener lsMod = e -> input.setChanged();
+    changed = input.hasChanged();
+
+    FormLayout formLayout = new FormLayout();
+    formLayout.marginWidth = PropsUi.getFormMargin();
+    formLayout.marginHeight = PropsUi.getFormMargin();
+
+    shell.setLayout(formLayout);
+    shell.setText(BaseMessages.getString(PKG, "ODataInput.Name"));
+
+    middle = props.getMiddlePct();
+    margin = PropsUi.getFormMargin();
+
+    // TransformName line
+    wlTransformName = new Label(shell, SWT.RIGHT);
+    wlTransformName.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.TransformName.Label"));
+    PropsUi.setLook(wlTransformName);
+    fdlTransformName = new FormData();
+    fdlTransformName.left = new FormAttachment(0, 0);
+    fdlTransformName.right = new FormAttachment(middle, -margin);
+    fdlTransformName.top = new FormAttachment(0, margin);
+    wlTransformName.setLayoutData(fdlTransformName);
+    wTransformName = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+    wTransformName.setText(transformName);
+    PropsUi.setLook(wTransformName);
+    wTransformName.addModifyListener(lsMod);
+    fdTransformName = new FormData();
+    fdTransformName.left = new FormAttachment(middle, 0);
+    fdTransformName.top = new FormAttachment(0, margin);
+    fdTransformName.right = new FormAttachment(100, 0);
+    wTransformName.setLayoutData(fdTransformName);
+
+    // Button bar at the bottom
+    wOk = new Button(shell, SWT.PUSH);
+    wOk.setText(BaseMessages.getString(PKG, "ODataInputDialog.Button.OK"));
+    wOk.addListener(SWT.Selection, e -> ok());
+    wCancel = new Button(shell, SWT.PUSH);
+    wCancel.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.Button.Cancel"));
+    wCancel.addListener(SWT.Selection, e -> cancel());
+    setButtonPositions(new Button[] {wOk, wCancel}, margin, null);
+
+    // Tab Folder
+    CTabFolder wTabFolder = new CTabFolder(shell, SWT.BORDER);
+    PropsUi.setLook(wTabFolder, Props.WIDGET_STYLE_TAB);
+
+    // ////////////////////////
+    // START OF CONNECTION TAB
+    // ////////////////////////
+    CTabItem wGeneralTab = new CTabItem(wTabFolder, SWT.NONE);
+    wGeneralTab.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.GeneralTab.Title"));
+    Composite wGeneralComp = new Composite(wTabFolder, SWT.NONE);
+    PropsUi.setLook(wGeneralComp);
+    FormLayout generalLayout = new FormLayout();
+    generalLayout.marginWidth = margin;
+    generalLayout.marginHeight = margin;
+    wGeneralComp.setLayout(generalLayout);
+
+    // URL line
+    Label wlUrl = new Label(wGeneralComp, SWT.RIGHT);
+    wlUrl.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.ServiceUrl.Label"));
+    PropsUi.setLook(wlUrl);
+    FormData fdlUrl = new FormData();
+    fdlUrl.left = new FormAttachment(0, 0);
+    fdlUrl.right = new FormAttachment(middle, -margin);
+    fdlUrl.top = new FormAttachment(0, margin);
+    wlUrl.setLayoutData(fdlUrl);
+    wUrl = new TextVar(variables, wGeneralComp, SWT.SINGLE | SWT.LEFT | 
SWT.BORDER);
+    PropsUi.setLook(wUrl);
+    wUrl.addModifyListener(lsMod);
+    FormData fdUrl = new FormData();
+    fdUrl.left = new FormAttachment(middle, 0);
+    fdUrl.right = new FormAttachment(100, 0);
+    fdUrl.top = new FormAttachment(0, margin);
+    wUrl.setLayoutData(fdUrl);
+
+    // AuthType line
+    Label wlAuthType = new Label(wGeneralComp, SWT.RIGHT);
+    wlAuthType.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.AuthType.Label"));
+    PropsUi.setLook(wlAuthType);
+    FormData fdlAuthType = new FormData();
+    fdlAuthType.left = new FormAttachment(0, 0);
+    fdlAuthType.right = new FormAttachment(middle, -margin);
+    fdlAuthType.top = new FormAttachment(wUrl, margin);
+    wlAuthType.setLayoutData(fdlAuthType);
+    wAuthType = new ComboVar(variables, wGeneralComp, SWT.SINGLE | SWT.LEFT | 
SWT.BORDER);
+    wAuthType.setItems(ODataAuthType.getDescriptions());
+    PropsUi.setLook(wAuthType);
+    wAuthType.addModifyListener(lsMod);
+    FormData fdAuthType = new FormData();
+    fdAuthType.left = new FormAttachment(middle, 0);
+    fdAuthType.right = new FormAttachment(100, 0);
+    fdAuthType.top = new FormAttachment(wUrl, margin);
+    wAuthType.setLayoutData(fdAuthType);
+
+    // Username line
+    Label wlUsername = new Label(wGeneralComp, SWT.RIGHT);
+    wlUsername.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.Username.Label"));
+    PropsUi.setLook(wlUsername);
+    FormData fdlUsername = new FormData();
+    fdlUsername.left = new FormAttachment(0, 0);
+    fdlUsername.right = new FormAttachment(middle, -margin);
+    fdlUsername.top = new FormAttachment(wAuthType, margin);
+    wlUsername.setLayoutData(fdlUsername);
+    wUsername = new TextVar(variables, wGeneralComp, SWT.SINGLE | SWT.LEFT | 
SWT.BORDER);
+    PropsUi.setLook(wUsername);
+    wUsername.addModifyListener(lsMod);
+    FormData fdUsername = new FormData();
+    fdUsername.left = new FormAttachment(middle, 0);
+    fdUsername.right = new FormAttachment(100, 0);
+    fdUsername.top = new FormAttachment(wAuthType, margin);
+    wUsername.setLayoutData(fdUsername);
+
+    // Password line
+    Label wlPassword = new Label(wGeneralComp, SWT.RIGHT);
+    wlPassword.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.Password.Label"));
+    PropsUi.setLook(wlPassword);
+    FormData fdlPassword = new FormData();
+    fdlPassword.left = new FormAttachment(0, 0);
+    fdlPassword.right = new FormAttachment(middle, -margin);
+    fdlPassword.top = new FormAttachment(wUsername, margin);
+    wlPassword.setLayoutData(fdlPassword);
+    wPassword = new PasswordTextVar(variables, wGeneralComp, SWT.SINGLE | 
SWT.LEFT | SWT.BORDER);
+    PropsUi.setLook(wPassword);
+    wPassword.addModifyListener(lsMod);
+    FormData fdPassword = new FormData();
+    fdPassword.left = new FormAttachment(middle, 0);
+    fdPassword.right = new FormAttachment(100, 0);
+    fdPassword.top = new FormAttachment(wUsername, margin);
+    wPassword.setLayoutData(fdPassword);
+
+    // Token line
+    Label wlToken = new Label(wGeneralComp, SWT.RIGHT);
+    wlToken.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.Token.Label"));
+    PropsUi.setLook(wlToken);
+    FormData fdlToken = new FormData();
+    fdlToken.left = new FormAttachment(0, 0);
+    fdlToken.right = new FormAttachment(middle, -margin);
+    fdlToken.top = new FormAttachment(wPassword, margin);
+    wlToken.setLayoutData(fdlToken);
+    wToken = new PasswordTextVar(variables, wGeneralComp, SWT.SINGLE | 
SWT.LEFT | SWT.BORDER);
+    PropsUi.setLook(wToken);
+    wToken.addModifyListener(lsMod);
+    FormData fdToken = new FormData();
+    fdToken.left = new FormAttachment(middle, 0);
+    fdToken.right = new FormAttachment(100, 0);
+    fdToken.top = new FormAttachment(wPassword, margin);
+    wToken.setLayoutData(fdToken);
+
+    // EntitySet line
+    Label wlEntitySet = new Label(wGeneralComp, SWT.RIGHT);
+    wlEntitySet.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.EntitySet.Label"));
+    PropsUi.setLook(wlEntitySet);
+    FormData fdlEntitySet = new FormData();
+    fdlEntitySet.left = new FormAttachment(0, 0);
+    fdlEntitySet.right = new FormAttachment(middle, -margin);
+    fdlEntitySet.top = new FormAttachment(wToken, margin);
+    wlEntitySet.setLayoutData(fdlEntitySet);
+
+    Button wGetEntitySets = new Button(wGeneralComp, SWT.PUSH);
+    wGetEntitySets.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.Button.GetEntitySets"));
+    wGetEntitySets.addListener(SWT.Selection, e -> getEntitySets());
+    FormData fdGetEntitySets = new FormData();
+    fdGetEntitySets.right = new FormAttachment(100, 0);
+    fdGetEntitySets.top = new FormAttachment(wToken, margin);
+    wGetEntitySets.setLayoutData(fdGetEntitySets);
+
+    wEntitySet = new ComboVar(variables, wGeneralComp, SWT.SINGLE | SWT.LEFT | 
SWT.BORDER);
+    PropsUi.setLook(wEntitySet);
+    wEntitySet.addModifyListener(lsMod);
+    FormData fdEntitySet = new FormData();
+    fdEntitySet.left = new FormAttachment(middle, 0);
+    fdEntitySet.right = new FormAttachment(wGetEntitySets, -margin);
+    fdEntitySet.top = new FormAttachment(wToken, margin);
+    wEntitySet.setLayoutData(fdEntitySet);
+
+    FormData fdGeneralComp = new FormData();
+    fdGeneralComp.left = new FormAttachment(0, 0);
+    fdGeneralComp.right = new FormAttachment(100, 0);
+    fdGeneralComp.top = new FormAttachment(0, 0);
+    fdGeneralComp.bottom = new FormAttachment(100, 0);
+    wGeneralComp.setLayoutData(fdGeneralComp);
+    wGeneralComp.layout();
+    wGeneralTab.setControl(wGeneralComp);
+
+    // ////////////////////////
+    // START OF QUERY TAB
+    // ////////////////////////
+    CTabItem wQueryTab = new CTabItem(wTabFolder, SWT.NONE);
+    wQueryTab.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.QueryTab.Title"));
+    Composite wQueryComp = new Composite(wTabFolder, SWT.NONE);
+    PropsUi.setLook(wQueryComp);
+    FormLayout queryLayout = new FormLayout();
+    queryLayout.marginWidth = margin;
+    queryLayout.marginHeight = margin;
+    wQueryComp.setLayout(queryLayout);
+
+    // Select line
+    Label wlQuerySelect = new Label(wQueryComp, SWT.RIGHT);
+    wlQuerySelect.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.QuerySelect.Label"));
+    PropsUi.setLook(wlQuerySelect);
+    FormData fdlQuerySelect = new FormData();
+    fdlQuerySelect.left = new FormAttachment(0, 0);
+    fdlQuerySelect.right = new FormAttachment(middle, -margin);
+    fdlQuerySelect.top = new FormAttachment(0, margin);
+    wlQuerySelect.setLayoutData(fdlQuerySelect);
+    wQuerySelect = new TextVar(variables, wQueryComp, SWT.SINGLE | SWT.LEFT | 
SWT.BORDER);
+    PropsUi.setLook(wQuerySelect);
+    wQuerySelect.addModifyListener(lsMod);
+    FormData fdQuerySelect = new FormData();
+    fdQuerySelect.left = new FormAttachment(middle, 0);
+    fdQuerySelect.right = new FormAttachment(100, 0);
+    fdQuerySelect.top = new FormAttachment(0, margin);
+    wQuerySelect.setLayoutData(fdQuerySelect);
+
+    // Filter line
+    Label wlQueryFilter = new Label(wQueryComp, SWT.RIGHT);
+    wlQueryFilter.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.QueryFilter.Label"));
+    PropsUi.setLook(wlQueryFilter);
+    FormData fdlQueryFilter = new FormData();
+    fdlQueryFilter.left = new FormAttachment(0, 0);
+    fdlQueryFilter.right = new FormAttachment(middle, -margin);
+    fdlQueryFilter.top = new FormAttachment(wQuerySelect, margin);
+    wlQueryFilter.setLayoutData(fdlQueryFilter);
+    wQueryFilter = new TextVar(variables, wQueryComp, SWT.SINGLE | SWT.LEFT | 
SWT.BORDER);
+    PropsUi.setLook(wQueryFilter);
+    wQueryFilter.addModifyListener(lsMod);
+    FormData fdQueryFilter = new FormData();
+    fdQueryFilter.left = new FormAttachment(middle, 0);
+    fdQueryFilter.right = new FormAttachment(100, 0);
+    fdQueryFilter.top = new FormAttachment(wQuerySelect, margin);
+    wQueryFilter.setLayoutData(fdQueryFilter);
+
+    // OrderBy line
+    Label wlQueryOrder = new Label(wQueryComp, SWT.RIGHT);
+    wlQueryOrder.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.QueryOrder.Label"));
+    PropsUi.setLook(wlQueryOrder);
+    FormData fdlQueryOrder = new FormData();
+    fdlQueryOrder.left = new FormAttachment(0, 0);
+    fdlQueryOrder.right = new FormAttachment(middle, -margin);
+    fdlQueryOrder.top = new FormAttachment(wQueryFilter, margin);
+    wlQueryOrder.setLayoutData(fdlQueryOrder);
+    wQueryOrder = new TextVar(variables, wQueryComp, SWT.SINGLE | SWT.LEFT | 
SWT.BORDER);
+    PropsUi.setLook(wQueryOrder);
+    wQueryOrder.addModifyListener(lsMod);
+    FormData fdQueryOrder = new FormData();
+    fdQueryOrder.left = new FormAttachment(middle, 0);
+    fdQueryOrder.right = new FormAttachment(100, 0);
+    fdQueryOrder.top = new FormAttachment(wQueryFilter, margin);
+    wQueryOrder.setLayoutData(fdQueryOrder);
+
+    // Top line
+    Label wlQueryTop = new Label(wQueryComp, SWT.RIGHT);
+    wlQueryTop.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.QueryTop.Label"));
+    PropsUi.setLook(wlQueryTop);
+    FormData fdlQueryTop = new FormData();
+    fdlQueryTop.left = new FormAttachment(0, 0);
+    fdlQueryTop.right = new FormAttachment(middle, -margin);
+    fdlQueryTop.top = new FormAttachment(wQueryOrder, margin);
+    wlQueryTop.setLayoutData(fdlQueryTop);
+    wQueryTop = new TextVar(variables, wQueryComp, SWT.SINGLE | SWT.LEFT | 
SWT.BORDER);
+    PropsUi.setLook(wQueryTop);
+    wQueryTop.addModifyListener(lsMod);
+    FormData fdQueryTop = new FormData();
+    fdQueryTop.left = new FormAttachment(middle, 0);
+    fdQueryTop.right = new FormAttachment(100, 0);
+    fdQueryTop.top = new FormAttachment(wQueryOrder, margin);
+    wQueryTop.setLayoutData(fdQueryTop);
+
+    // Skip line
+    Label wlQuerySkip = new Label(wQueryComp, SWT.RIGHT);
+    wlQuerySkip.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.QuerySkip.Label"));
+    PropsUi.setLook(wlQuerySkip);
+    FormData fdlQuerySkip = new FormData();
+    fdlQuerySkip.left = new FormAttachment(0, 0);
+    fdlQuerySkip.right = new FormAttachment(middle, -margin);
+    fdlQuerySkip.top = new FormAttachment(wQueryTop, margin);
+    wlQuerySkip.setLayoutData(fdlQuerySkip);
+    wQuerySkip = new TextVar(variables, wQueryComp, SWT.SINGLE | SWT.LEFT | 
SWT.BORDER);
+    PropsUi.setLook(wQuerySkip);
+    wQuerySkip.addModifyListener(lsMod);
+    FormData fdQuerySkip = new FormData();
+    fdQuerySkip.left = new FormAttachment(middle, 0);
+    fdQuerySkip.right = new FormAttachment(100, 0);
+    fdQuerySkip.top = new FormAttachment(wQueryTop, margin);
+    wQuerySkip.setLayoutData(fdQuerySkip);
+
+    FormData fdQueryComp = new FormData();
+    fdQueryComp.left = new FormAttachment(0, 0);
+    fdQueryComp.right = new FormAttachment(100, 0);
+    fdQueryComp.top = new FormAttachment(0, 0);
+    fdQueryComp.bottom = new FormAttachment(100, 0);
+    wQueryComp.setLayoutData(fdQueryComp);
+    wQueryComp.layout();
+    wQueryTab.setControl(wQueryComp);
+
+    // ////////////////////////
+    // START OF FIELDS TAB
+    // ////////////////////////
+    CTabItem wFieldsTab = new CTabItem(wTabFolder, SWT.NONE);
+    wFieldsTab.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.FieldsTab.Title"));
+    Composite wFieldsComp = new Composite(wTabFolder, SWT.NONE);
+    PropsUi.setLook(wFieldsComp);
+    FormLayout fieldsLayout = new FormLayout();
+    fieldsLayout.marginWidth = margin;
+    fieldsLayout.marginHeight = margin;
+    wFieldsComp.setLayout(fieldsLayout);
+
+    Button wGetFields = new Button(wFieldsComp, SWT.PUSH);
+    wGetFields.setText(BaseMessages.getString(PKG, 
"ODataInputDialog.Button.GetFields"));
+    wGetFields.addListener(SWT.Selection, e -> getFields());
+    FormData fdGetFields = new FormData();
+    fdGetFields.right = new FormAttachment(100, 0);
+    fdGetFields.top = new FormAttachment(0, margin);
+    wGetFields.setLayoutData(fdGetFields);
+
+    ColumnInfo[] colinf =
+        new ColumnInfo[] {
+          new ColumnInfo(
+              BaseMessages.getString(PKG, 
"ODataInputDialog.Fields.Column.HopName"),
+              ColumnInfo.COLUMN_TYPE_TEXT,
+              false),
+          new ColumnInfo(
+              BaseMessages.getString(PKG, 
"ODataInputDialog.Fields.Column.ODataPath"),
+              ColumnInfo.COLUMN_TYPE_TEXT,
+              false),
+          new ColumnInfo(
+              BaseMessages.getString(PKG, 
"ODataInputDialog.Fields.Column.HopType"),
+              ColumnInfo.COLUMN_TYPE_CCOMBO,
+              ValueMetaFactory.getValueMetaNames()),
+          new ColumnInfo(
+              BaseMessages.getString(PKG, 
"ODataInputDialog.Fields.Column.Format"),
+              ColumnInfo.COLUMN_TYPE_FORMAT,
+              3)
+        };
+
+    wFields =
+        new TableView(
+            variables,
+            wFieldsComp,
+            SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI,
+            colinf,
+            1,
+            lsMod,
+            props);
+    FormData fdFields = new FormData();
+    fdFields.left = new FormAttachment(0, 0);
+    fdFields.right = new FormAttachment(100, 0);
+    fdFields.top = new FormAttachment(wGetFields, margin);
+    fdFields.bottom = new FormAttachment(100, 0);
+    wFields.setLayoutData(fdFields);
+
+    FormData fdFieldsComp = new FormData();
+    fdFieldsComp.left = new FormAttachment(0, 0);
+    fdFieldsComp.right = new FormAttachment(100, 0);
+    fdFieldsComp.top = new FormAttachment(0, 0);
+    fdFieldsComp.bottom = new FormAttachment(100, 0);
+    wFieldsComp.setLayoutData(fdFieldsComp);
+    wFieldsComp.layout();
+    wFieldsTab.setControl(wFieldsComp);
+
+    // Layout Tab Folder
+    FormData fdTabFolder = new FormData();
+    fdTabFolder.left = new FormAttachment(0, 0);
+    fdTabFolder.right = new FormAttachment(100, 0);
+    fdTabFolder.top = new FormAttachment(wTransformName, margin);
+    fdTabFolder.bottom = new FormAttachment(wOk, -margin);
+    wTabFolder.setLayoutData(fdTabFolder);
+
+    wTabFolder.setSelection(0);
+
+    // Populate data
+    getData();
+    input.setChanged(changed);
+
+    shell.open();
+    Point size = shell.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+    shell.setSize(Math.max(600, size.x), Math.max(500, size.y));
+    while (!shell.isDisposed()) {
+      if (!shell.getDisplay().readAndDispatch()) {
+        shell.getDisplay().sleep();
+      }
+    }
+    return transformName;
+  }
+
+  private void getData() {
+    wUrl.setText(Const.NVL(input.getUrl(), ""));
+    wEntitySet.setText(Const.NVL(input.getEntitySet(), ""));
+    
wAuthType.setText(ODataAuthType.lookupCode(input.getAuthType()).getDescription());
+    wUsername.setText(Const.NVL(input.getUsername(), ""));
+    wPassword.setText(Const.NVL(input.getPassword(), ""));
+    wToken.setText(Const.NVL(input.getToken(), ""));
+
+    wQuerySelect.setText(Const.NVL(input.getQuerySelect(), ""));
+    wQueryFilter.setText(Const.NVL(input.getQueryFilter(), ""));
+    wQueryOrder.setText(Const.NVL(input.getQueryOrder(), ""));
+    wQueryTop.setText(Const.NVL(input.getQueryTop(), ""));
+    wQuerySkip.setText(Const.NVL(input.getQuerySkip(), ""));
+
+    for (int i = 0; i < input.getFields().size(); i++) {
+      ODataField field = input.getFields().get(i);
+      TableItem item = new TableItem(wFields.table, SWT.NONE);
+      item.setText(1, Const.NVL(field.getName(), ""));
+      item.setText(2, Const.NVL(field.getPath(), ""));
+      item.setText(3, 
Const.NVL(ValueMetaFactory.getValueMetaName(field.getType()), ""));
+      item.setText(4, Const.NVL(field.getFormat(), ""));
+    }
+    wFields.removeEmptyRows();
+    wFields.setRowNums();
+    wFields.optWidth(true);
+  }
+
+  private void cancel() {
+    transformName = null;
+    input.setChanged(changed);
+    dispose();
+  }
+
+  private void ok() {
+    if (Utils.isEmpty(wTransformName.getText())) {
+      return;
+    }
+    transformName = wTransformName.getText();
+
+    input.setUrl(wUrl.getText());
+    input.setEntitySet(wEntitySet.getText());
+    
input.setAuthType(ODataAuthType.lookupDescription(wAuthType.getText()).getCode());
+    input.setUsername(wUsername.getText());
+    input.setPassword(wPassword.getText());
+    input.setToken(wToken.getText());
+
+    input.setQuerySelect(wQuerySelect.getText());
+    input.setQueryFilter(wQueryFilter.getText());
+    input.setQueryOrder(wQueryOrder.getText());
+    input.setQueryTop(wQueryTop.getText());
+    input.setQuerySkip(wQuerySkip.getText());
+
+    input.getFields().clear();
+    int nrFields = wFields.nrNonEmpty();
+    for (int i = 0; i < nrFields; i++) {
+      TableItem item = wFields.getNonEmpty(i);
+      String name = item.getText(1);
+      String path = item.getText(2);
+      int type = ValueMetaFactory.getIdForValueMeta(item.getText(3));
+      String format = item.getText(4);
+      input.getFields().add(new ODataField(name, path, type, format));
+    }
+
+    input.setChanged();
+    dispose();
+  }
+
+  private void getEntitySets() {
+    String urlStr = wUrl.getText();
+    if (Utils.isEmpty(urlStr)) {
+      return;
+    }
+    urlStr = variables.resolve(urlStr);
+    try {
+      HttpClientManager.HttpClientBuilderFacade builder =
+          HttpClientManager.getInstance().createBuilder();
+      ODataAuthType authType = 
ODataAuthType.lookupDescription(wAuthType.getText());
+      if (ODataAuthType.BASIC == authType && 
!Utils.isEmpty(wUsername.getText())) {
+        builder.setCredentials(
+            variables.resolve(wUsername.getText()), 
variables.resolve(wPassword.getText()));
+      }
+      try (org.apache.hc.client5.http.impl.classic.CloseableHttpClient client 
= builder.build()) {
+        HttpGet get = new HttpGet(urlStr);
+        get.setHeader("Accept", "application/json");
+        if (ODataAuthType.BEARER == authType && 
!Utils.isEmpty(wToken.getText())) {
+          get.setHeader("Authorization", "Bearer " + 
variables.resolve(wToken.getText()));
+        }
+        try (CloseableHttpResponse response = client.execute(get)) {
+          int code = response.getCode();
+          if (code != 200) {
+            throw new HopException("HTTP " + code);
+          }
+          String body = EntityUtils.toString(response.getEntity(), 
StandardCharsets.UTF_8);
+          ObjectMapper mapper = new ObjectMapper();
+          JsonNode root = mapper.readTree(body);
+          List<String> list = new ArrayList<>();
+          if (root.has("value")) {
+            JsonNode val = root.get("value");
+            if (val.isArray()) {
+              for (JsonNode it : val) {
+                if (it.has("name")) {
+                  list.add(it.get("name").asText());
+                }
+              }
+            }
+          } else if (root.has("d")) {
+            JsonNode d = root.get("d");
+            if (d.has("EntitySets") && d.get("EntitySets").isArray()) {
+              for (JsonNode it : d.get("EntitySets")) {
+                list.add(it.asText());
+              }
+            } else if (d.isArray()) {
+              for (JsonNode it : d) {
+                if (it.has("name")) {
+                  list.add(it.get("name").asText());
+                }
+              }
+            } else {
+              d.fieldNames()
+                  .forEachRemaining(
+                      name -> {
+                        if (!name.startsWith("__")) {
+                          list.add(name);
+                        }
+                      });
+            }
+          }
+          if (!list.isEmpty()) {
+            wEntitySet.setItems(list.toArray(new String[0]));
+          }
+        }
+      }
+    } catch (Exception e) {
+      new ErrorDialog(shell, "Error", "Error fetching entity sets", e);
+    }
+  }
+
+  private void getFields() {
+    String urlStr = wUrl.getText();
+    String entitySetName = wEntitySet.getText();
+    if (Utils.isEmpty(urlStr) || Utils.isEmpty(entitySetName)) {
+      return;
+    }
+    urlStr = variables.resolve(urlStr);
+    entitySetName = variables.resolve(entitySetName);
+    if (!urlStr.endsWith("/")) {
+      urlStr += "/";
+    }
+    String metadataUrl = urlStr + "$metadata";
+    try {
+      HttpClientManager.HttpClientBuilderFacade builder =
+          HttpClientManager.getInstance().createBuilder();
+      ODataAuthType authType = 
ODataAuthType.lookupDescription(wAuthType.getText());
+      if (ODataAuthType.BASIC == authType && 
!Utils.isEmpty(wUsername.getText())) {
+        builder.setCredentials(
+            variables.resolve(wUsername.getText()), 
variables.resolve(wPassword.getText()));
+      }
+      try (org.apache.hc.client5.http.impl.classic.CloseableHttpClient client 
= builder.build()) {
+        HttpGet get = new HttpGet(metadataUrl);
+        if (ODataAuthType.BEARER == authType && 
!Utils.isEmpty(wToken.getText())) {
+          get.setHeader("Authorization", "Bearer " + 
variables.resolve(wToken.getText()));
+        }
+        try (CloseableHttpResponse response = client.execute(get)) {
+          int code = response.getCode();
+          if (code != 200) {
+            throw new HopException("HTTP " + code);
+          }
+          String body = EntityUtils.toString(response.getEntity(), 
StandardCharsets.UTF_8);
+
+          javax.xml.parsers.DocumentBuilderFactory factory =
+              javax.xml.parsers.DocumentBuilderFactory.newInstance();
+          factory.setNamespaceAware(true);
+          javax.xml.parsers.DocumentBuilder db = factory.newDocumentBuilder();
+          org.w3c.dom.Document doc =
+              db.parse(new 
java.io.ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)));
+
+          org.w3c.dom.NodeList entitySets = doc.getElementsByTagNameNS("*", 
"EntitySet");
+          String entityType = null;
+          for (int i = 0; i < entitySets.getLength(); i++) {
+            org.w3c.dom.Element set = (org.w3c.dom.Element) entitySets.item(i);
+            if (entitySetName.equalsIgnoreCase(set.getAttribute("Name"))) {
+              entityType = set.getAttribute("EntityType");
+              break;
+            }
+          }
+
+          if (entityType == null) {
+            throw new HopException("Entity Set not found in metadata: " + 
entitySetName);
+          }
+
+          String entityTypeName = entityType;
+          int lastDot = entityType.lastIndexOf(".");
+          if (lastDot != -1) {
+            entityTypeName = entityType.substring(lastDot + 1);
+          }
+
+          org.w3c.dom.NodeList entityTypes = doc.getElementsByTagNameNS("*", 
"EntityType");
+          org.w3c.dom.Element targetEntityType = null;
+          for (int i = 0; i < entityTypes.getLength(); i++) {
+            org.w3c.dom.Element type = (org.w3c.dom.Element) 
entityTypes.item(i);
+            if (entityTypeName.equalsIgnoreCase(type.getAttribute("Name"))) {
+              targetEntityType = type;
+              break;
+            }
+          }
+
+          if (targetEntityType == null) {
+            throw new HopException(
+                "EntityType definition not found in metadata: " + 
entityTypeName);
+          }
+
+          org.w3c.dom.NodeList properties =
+              targetEntityType.getElementsByTagNameNS("*", "Property");
+          wFields.table.removeAll();
+          for (int i = 0; i < properties.getLength(); i++) {
+            org.w3c.dom.Element prop = (org.w3c.dom.Element) 
properties.item(i);
+            String name = prop.getAttribute("Name");
+            String type = prop.getAttribute("Type");
+
+            String hopType = "String";
+            if (type != null) {
+              if (type.contains("Int16")
+                  || type.contains("Int32")
+                  || type.contains("Int64")
+                  || type.contains("Byte")) {
+                hopType = "Integer";
+              } else if (type.contains("Decimal")
+                  || type.contains("Double")
+                  || type.contains("Single")) {
+                hopType = "Number";
+              } else if (type.contains("Boolean")) {
+                hopType = "Boolean";
+              } else if (type.contains("DateTime") || type.contains("Date")) {
+                hopType = "Date";
+              }
+            }
+
+            TableItem item = new TableItem(wFields.table, SWT.NONE);
+            item.setText(1, name);
+            item.setText(2, name);
+            item.setText(3, hopType);
+            item.setText(4, "");
+          }
+          wFields.removeEmptyRows();
+          wFields.setRowNums();
+          wFields.optWidth(true);
+        }
+      }
+    } catch (Exception e) {
+      new ErrorDialog(shell, "Error", "Error fetching fields metadata", e);
+    }
+  }
+}
diff --git 
a/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataInputMeta.java
 
b/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataInputMeta.java
new file mode 100644
index 0000000000..14d5a185e5
--- /dev/null
+++ 
b/plugins/transforms/odata/src/main/java/org/apache/hop/pipeline/transforms/odata/ODataInputMeta.java
@@ -0,0 +1,174 @@
+/*
+ * 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.odata;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.core.CheckResult;
+import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.annotations.Transform;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.BaseTransformMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+
+@Getter
+@Setter
+@Transform(
+    id = "ODataInput",
+    image = "odata.svg",
+    name = "i18n::ODataInput.Name",
+    description = "i18n::ODataInput.Description",
+    categoryDescription = 
"i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.Input",
+    keywords = "i18n::ODataInputMeta.keyword",
+    documentationUrl = "/pipeline/transforms/odata-input.html")
+public class ODataInputMeta extends BaseTransformMeta<ODataInput, 
ODataInputData> {
+  private static final Class<?> PKG = ODataInputMeta.class;
+
+  @HopMetadataProperty(key = "url")
+  private String url;
+
+  @HopMetadataProperty(key = "entity_set")
+  private String entitySet;
+
+  @HopMetadataProperty(key = "auth_type")
+  private String authType;
+
+  @HopMetadataProperty(key = "username")
+  private String username;
+
+  @HopMetadataProperty(key = "password", password = true)
+  private String password;
+
+  @HopMetadataProperty(key = "token", password = true)
+  private String token;
+
+  @HopMetadataProperty(key = "query_select")
+  private String querySelect;
+
+  @HopMetadataProperty(key = "query_filter")
+  private String queryFilter;
+
+  @HopMetadataProperty(key = "query_order")
+  private String queryOrder;
+
+  @HopMetadataProperty(key = "query_top")
+  private String queryTop;
+
+  @HopMetadataProperty(key = "query_skip")
+  private String querySkip;
+
+  @HopMetadataProperty(key = "field", groupKey = "fields")
+  private List<ODataField> fields;
+
+  public ODataInputMeta() {
+    super();
+    fields = new ArrayList<>();
+    authType = "NONE";
+  }
+
+  public ODataInputMeta(ODataInputMeta other) {
+    this();
+    this.url = other.url;
+    this.entitySet = other.entitySet;
+    this.authType = other.authType;
+    this.username = other.username;
+    this.password = other.password;
+    this.token = other.token;
+    this.querySelect = other.querySelect;
+    this.queryFilter = other.queryFilter;
+    this.queryOrder = other.queryOrder;
+    this.queryTop = other.queryTop;
+    this.querySkip = other.querySkip;
+    if (other.fields != null) {
+      for (ODataField f : other.fields) {
+        this.fields.add(new ODataField(f));
+      }
+    }
+  }
+
+  @Override
+  public void setDefault() {
+    this.url = "";
+    this.entitySet = "";
+    this.authType = "NONE";
+    this.username = "";
+    this.password = "";
+    this.token = "";
+    this.querySelect = "";
+    this.queryFilter = "";
+    this.queryOrder = "";
+    this.queryTop = "";
+    this.querySkip = "";
+    this.fields = new ArrayList<>();
+  }
+
+  @Override
+  public void getFields(
+      IRowMeta rowMeta,
+      String name,
+      IRowMeta[] info,
+      TransformMeta nextTransform,
+      IVariables variables,
+      IHopMetadataProvider metadataProvider)
+      throws HopTransformException {
+    for (ODataField field : fields) {
+      try {
+        rowMeta.addValueMeta(field.toValueMeta(name, variables));
+      } catch (Exception e) {
+        throw new HopTransformException(
+            "Error generating value meta for field: " + field.getName(), e);
+      }
+    }
+  }
+
+  @Override
+  public void check(
+      List<ICheckResult> remarks,
+      PipelineMeta pipelineMeta,
+      TransformMeta transformMeta,
+      IRowMeta prev,
+      String[] input,
+      String[] output,
+      IRowMeta info,
+      IVariables variables,
+      IHopMetadataProvider metadataProvider) {
+    if (url == null || url.trim().isEmpty()) {
+      remarks.add(
+          new CheckResult(
+              ICheckResult.TYPE_RESULT_ERROR, "Service URL is missing.", 
transformMeta));
+    } else {
+      remarks.add(
+          new CheckResult(ICheckResult.TYPE_RESULT_OK, "Service URL is 
specified.", transformMeta));
+    }
+
+    if (entitySet == null || entitySet.trim().isEmpty()) {
+      remarks.add(
+          new CheckResult(ICheckResult.TYPE_RESULT_ERROR, "Entity Set is 
missing.", transformMeta));
+    } else {
+      remarks.add(
+          new CheckResult(ICheckResult.TYPE_RESULT_OK, "Entity Set is 
specified.", transformMeta));
+    }
+  }
+}
diff --git a/plugins/transforms/odata/src/main/resources/dependencies.xml 
b/plugins/transforms/odata/src/main/resources/dependencies.xml
new file mode 100644
index 0000000000..4e93773f89
--- /dev/null
+++ b/plugins/transforms/odata/src/main/resources/dependencies.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~
+  -->
+
+<dependencies>
+</dependencies>
diff --git a/plugins/transforms/odata/src/main/resources/odata.svg 
b/plugins/transforms/odata/src/main/resources/odata.svg
new file mode 100644
index 0000000000..cde9c9b82b
--- /dev/null
+++ b/plugins/transforms/odata/src/main/resources/odata.svg
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+  ~ 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.
+  ~
+  -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   version="1.1"
+   id="Layer_1"
+   x="0px"
+   y="0px"
+   viewBox="0 0 45 45"
+   style="enable-background:new 0 0 45 45;"
+   xml:space="preserve"
+   width="100%"
+   height="100%">
+   <g>
+     <!-- Circle background -->
+     <circle cx="22.5" cy="22.5" r="20" fill="#0B79B5" stroke="#FFFFFF" 
stroke-width="2"/>
+     <!-- OData text shape or stylized 'O' and 'D' -->
+     <text x="22.5" y="29" font-family="'Helvetica Neue', Helvetica, Arial, 
sans-serif" font-size="18" font-weight="bold" fill="#FFFFFF" 
text-anchor="middle">OD</text>
+   </g>
+</svg>
diff --git 
a/plugins/transforms/odata/src/main/resources/org/apache/hop/pipeline/transforms/odata/messages/messages_en_US.properties
 
b/plugins/transforms/odata/src/main/resources/org/apache/hop/pipeline/transforms/odata/messages/messages_en_US.properties
new file mode 100644
index 0000000000..854ab75509
--- /dev/null
+++ 
b/plugins/transforms/odata/src/main/resources/org/apache/hop/pipeline/transforms/odata/messages/messages_en_US.properties
@@ -0,0 +1,52 @@
+#
+# 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.
+#
+
+ODataInput.Name=OData Input
+ODataInput.Description=Retrieve data from an OData service (V2 and V4).
+ODataInputMeta.keyword=odata,input,rest,service,sap,microsoft
+
+ODataInputDialog.TransformName.Label=Transform name
+ODataInputDialog.GeneralTab.Title=Connection
+ODataInputDialog.QueryTab.Title=OData Query
+ODataInputDialog.FieldsTab.Title=Fields
+
+ODataInputDialog.ServiceUrl.Label=OData Service Root URL
+ODataInputDialog.EntitySet.Label=OData Entity Set
+ODataInputDialog.AuthType.Label=Authentication Type
+ODataInputDialog.Username.Label=Username (Basic Auth)
+ODataInputDialog.Password.Label=Password (Basic Auth)
+ODataInputDialog.Token.Label=Token (Bearer / OAuth2)
+
+ODataInputDialog.QuerySelect.Label=$select (comma separated fields)
+ODataInputDialog.QueryFilter.Label=$filter condition
+ODataInputDialog.QueryOrder.Label=$orderby field desc/asc
+ODataInputDialog.QueryTop.Label=$top count
+ODataInputDialog.QuerySkip.Label=$skip count
+
+ODataInputDialog.Fields.Column.HopName=Name in Hop
+ODataInputDialog.Fields.Column.ODataPath=OData Path
+ODataInputDialog.Fields.Column.HopType=Type
+ODataInputDialog.Fields.Column.Format=Format
+
+ODataInputDialog.Button.GetEntitySets=Get Entity Sets
+ODataInputDialog.Button.GetFields=Get Fields
+ODataInputDialog.Button.OK=&OK
+ODataInputDialog.Button.Cancel=&Cancel
+
+ODataAuthType.None.Description=No Authentication
+ODataAuthType.Basic.Description=Basic Authentication (Username / Password)
+ODataAuthType.Bearer.Description=Bearer Token (Authorization Header)
diff --git a/plugins/transforms/odata/src/main/resources/version.xml 
b/plugins/transforms/odata/src/main/resources/version.xml
new file mode 100644
index 0000000000..36ab20e22e
--- /dev/null
+++ b/plugins/transforms/odata/src/main/resources/version.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~
+  -->
+
+<version>${project.version}</version>
diff --git a/plugins/transforms/pom.xml b/plugins/transforms/pom.xml
index a481cedd4a..ab621dd3f2 100644
--- a/plugins/transforms/pom.xml
+++ b/plugins/transforms/pom.xml
@@ -112,6 +112,7 @@
         <module>normaliser</module>
         <module>nullif</module>
         <module>numberrange</module>
+        <module>odata</module>
         <module>pgp</module>
         <module>pipelineexecutor</module>
         <module>processfiles</module>


Reply via email to