You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hop.apache.org by ha...@apache.org on 2026/04/09 12:58:43 UTC
(hop) branch main updated: Replace if instanceof chains with modern switch pattern matching and add unit test (#6977)
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 ce77dbf1a1 Replace if instanceof chains with modern switch pattern matching and add unit test (#6977)
ce77dbf1a1 is described below
commit ce77dbf1a10782bd7716fbb49b16532463c99960
Author: Lance <le...@gmail.com>
AuthorDate: Thu Apr 9 20:58:38 2026 +0800
Replace if instanceof chains with modern switch pattern matching and add unit test (#6977)
* Replace if instanceof chains with modern switch pattern matching and add unit test
Signed-off-by: lance <le...@gmail.com>
* Replace if instanceof chains with modern switch pattern matching and add unit test
Signed-off-by: lance <le...@gmail.com>
---------
Signed-off-by: lance <le...@gmail.com>
---
.../main/java/org/apache/hop/core/row/RowMeta.java | 61 ++++---
.../org/apache/hop/core/row/ValueMetaAndData.java | 65 ++-----
.../hop/core/row/value/ValueMetaFactory.java | 36 ++--
.../java/org/apache/hop/core/row/RowMetaTests.java | 112 ++++++++++++
.../apache/hop/core/row/ValueMetaAndDataTests.java | 106 +++++++++++
.../hop/core/row/value/ValueMetaFactoryTests.java | 120 +++++++++++++
.../hop/avro/transforms/avrodecode/AvroDecode.java | 102 ++++-------
.../transforms/avrodecode/AvroDecodeTests.java | 197 +++++++++++++++++++++
.../hop/pipeline/transforms/sasinput/SasInput.java | 72 ++++----
.../transforms/sasinput/SasInputTests.java | 154 ++++++++++++++++
10 files changed, 822 insertions(+), 203 deletions(-)
diff --git a/core/src/main/java/org/apache/hop/core/row/RowMeta.java b/core/src/main/java/org/apache/hop/core/row/RowMeta.java
index 1e138918f6..4b3deeb5d3 100644
--- a/core/src/main/java/org/apache/hop/core/row/RowMeta.java
+++ b/core/src/main/java/org/apache/hop/core/row/RowMeta.java
@@ -462,43 +462,48 @@ public class RowMeta implements IRowMeta {
* (IRowMeta / IValueMeta) is used. Strings use getBytes().length; other types use fixed
* estimates. Use this when you have only the raw row (e.g. from getRowFrom) and no row meta.
*
- * @param dataRow the row (may be null)
+ * @param dataRow the row (maybe null)
* @return estimated size in bytes, or null if row is null (no data)
*/
public static Long getRowSizeEstimateFromRow(Object[] dataRow) {
if (dataRow == null) {
return null;
}
+
long total = 0L;
for (Object v : dataRow) {
- if (v == null) {
- continue;
- }
- if (v instanceof String s) {
- total += s.getBytes().length;
- } else if (v instanceof byte[] b) {
- total += b.length;
- } else if (v instanceof BigDecimal) {
- total += 32;
- } else if (v instanceof Number) {
- total += 8;
- } else if (v instanceof Date) {
- total += 8;
- } else if (v instanceof Boolean) {
- total += 1;
- } else if (v instanceof UUID) {
- total += 36;
- } else if (v instanceof JsonNode jn) {
- total += jn.toString().length() * 2L;
- } else if (v instanceof GenericRecord gr) {
- total += gr.toString().length() * 2L;
- } else if (v instanceof InetAddress ia) {
- total += ia.getHostAddress().length() * 2L;
- } else {
- total += 64; // Serializable or other unknown types
- }
+ total += estimateSize(v);
+ }
+ return total;
+ }
+
+ /**
+ * Estimates the size in bytes of a row from the Java types of its values only
+ *
+ * @param v data
+ * @return estimated size in bytes
+ */
+ private static long estimateSize(Object v) {
+ if (v == null) {
+ return 0;
}
- return Long.valueOf(total);
+
+ return switch (v) {
+ case String s -> s.getBytes().length;
+ case byte[] b -> b.length;
+ case BigDecimal ignored -> 32;
+ case Number ignored -> 8;
+
+ case Date ignored -> 8;
+ case Boolean ignored -> 1;
+ case UUID ignored -> 36;
+
+ case JsonNode jn -> jn.toString().length() * 2L;
+ case GenericRecord gr -> gr.toString().length() * 2L;
+ case InetAddress ia -> ia.getHostAddress().length() * 2L;
+
+ default -> 64;
+ };
}
/**
diff --git a/core/src/main/java/org/apache/hop/core/row/ValueMetaAndData.java b/core/src/main/java/org/apache/hop/core/row/ValueMetaAndData.java
index ca90362e61..fc00d40bfa 100644
--- a/core/src/main/java/org/apache/hop/core/row/ValueMetaAndData.java
+++ b/core/src/main/java/org/apache/hop/core/row/ValueMetaAndData.java
@@ -19,6 +19,8 @@ package org.apache.hop.core.row;
import java.math.BigDecimal;
import java.util.Date;
+import lombok.Getter;
+import lombok.Setter;
import org.apache.hop.core.Const;
import org.apache.hop.core.exception.HopRuntimeException;
import org.apache.hop.core.exception.HopValueException;
@@ -35,6 +37,8 @@ import org.apache.hop.core.row.value.ValueMetaString;
import org.apache.hop.core.xml.XmlHandler;
import org.w3c.dom.Node;
+@Getter
+@Setter
public class ValueMetaAndData {
public static final String XML_TAG = "value";
@@ -44,8 +48,8 @@ public class ValueMetaAndData {
public ValueMetaAndData() {}
/**
- * @param valueMeta
- * @param valueData
+ * @param valueMeta IValueMeta
+ * @param valueData Object
*/
public ValueMetaAndData(IValueMeta valueMeta, Object valueData) {
this.valueMeta = valueMeta;
@@ -54,23 +58,18 @@ public class ValueMetaAndData {
public ValueMetaAndData(String valueName, Object valueData) {
this.valueData = valueData;
- if (valueData instanceof String) {
- this.valueMeta = new ValueMetaString(valueName);
- } else if (valueData instanceof Double) {
- this.valueMeta = new ValueMetaNumber(valueName);
- } else if (valueData instanceof Long) {
- this.valueMeta = new ValueMetaInteger(valueName);
- } else if (valueData instanceof Date) {
- this.valueMeta = new ValueMetaDate(valueName);
- } else if (valueData instanceof BigDecimal) {
- this.valueMeta = new ValueMetaBigNumber(valueName);
- } else if (valueData instanceof Boolean) {
- this.valueMeta = new ValueMetaBoolean(valueName);
- } else if (valueData instanceof byte[]) {
- this.valueMeta = new ValueMetaBinary(valueName);
- } else {
- this.valueMeta = new ValueMetaSerializable(valueName);
- }
+
+ this.valueMeta =
+ switch (valueData) {
+ case String ignored -> new ValueMetaString(valueName);
+ case Double ignored -> new ValueMetaNumber(valueName);
+ case Long ignored -> new ValueMetaInteger(valueName);
+ case Date ignored -> new ValueMetaDate(valueName);
+ case BigDecimal ignored -> new ValueMetaBigNumber(valueName);
+ case Boolean ignored -> new ValueMetaBoolean(valueName);
+ case byte[] ignored -> new ValueMetaBinary(valueName);
+ default -> new ValueMetaSerializable(valueName);
+ };
}
public ValueMetaAndData(ValueMetaAndData v) {
@@ -203,32 +202,4 @@ public class ValueMetaAndData {
public String toStringMeta() {
return valueMeta.toStringMeta();
}
-
- /**
- * @return the valueData
- */
- public Object getValueData() {
- return valueData;
- }
-
- /**
- * @param valueData the valueData to set
- */
- public void setValueData(Object valueData) {
- this.valueData = valueData;
- }
-
- /**
- * @return the valueMeta
- */
- public IValueMeta getValueMeta() {
- return valueMeta;
- }
-
- /**
- * @param valueMeta the valueMeta to set
- */
- public void setValueMeta(IValueMeta valueMeta) {
- this.valueMeta = valueMeta;
- }
}
diff --git a/core/src/main/java/org/apache/hop/core/row/value/ValueMetaFactory.java b/core/src/main/java/org/apache/hop/core/row/value/ValueMetaFactory.java
index 7f92fb7472..bf4e55e077 100644
--- a/core/src/main/java/org/apache/hop/core/row/value/ValueMetaFactory.java
+++ b/core/src/main/java/org/apache/hop/core/row/value/ValueMetaFactory.java
@@ -192,33 +192,21 @@ public class ValueMetaFactory {
* will be found usable this may be implemented later.
*
* @param object object to guess applicable IValueMeta.
- * @return
+ * @return IValueMeta
* @see IValueMeta if the hop value meta is recognized, null otherwise.
*/
public static IValueMeta guessValueMetaInterface(Object object) {
- if (object instanceof Number) {
- // this is numeric object
- if (object instanceof BigDecimal) {
- return new ValueMetaBigNumber();
- } else if (object instanceof Double) {
- return new ValueMetaNumber();
- } else if (object instanceof Long) {
- return new ValueMetaInteger();
- }
- } else if (object instanceof String) {
- return new ValueMetaString();
- } else if (object instanceof Date) {
- return new ValueMetaDate();
- } else if (object instanceof Boolean) {
- return new ValueMetaBoolean();
- } else if (object instanceof byte[]) {
- return new ValueMetaBinary();
- } else if (object instanceof JsonNode) {
- return new ValueMetaJson();
- }
-
- // ask someone else
- return null;
+ return switch (object) {
+ case BigDecimal ignored -> new ValueMetaBigNumber();
+ case Double ignored -> new ValueMetaNumber();
+ case Long ignored -> new ValueMetaInteger();
+ case String ignored -> new ValueMetaString();
+ case Date ignored -> new ValueMetaDate();
+ case Boolean ignored -> new ValueMetaBoolean();
+ case byte[] ignored -> new ValueMetaBinary();
+ case JsonNode ignored -> new ValueMetaJson();
+ case null, default -> null;
+ };
}
public static IValueMeta loadValueMetaFromJson(JSONObject jValue) throws HopPluginException {
diff --git a/core/src/test/java/org/apache/hop/core/row/RowMetaTests.java b/core/src/test/java/org/apache/hop/core/row/RowMetaTests.java
new file mode 100644
index 0000000000..4406de6291
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/row/RowMetaTests.java
@@ -0,0 +1,112 @@
+/*
+ * 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.core.row;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.math.BigDecimal;
+import java.net.InetAddress;
+import java.util.Date;
+import java.util.UUID;
+import org.apache.avro.Schema;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.hop.junit.rules.RestoreHopEnvironmentExtension;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/** Unit test for {@link RowMeta} */
+@ExtendWith(RestoreHopEnvironmentExtension.class)
+class RowMetaTests {
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ @Test
+ void testNullRow() {
+ assertNull(RowMeta.getRowSizeEstimateFromRow(null));
+ }
+
+ @Test
+ void testAllTypesCoverage() throws Exception {
+ // String,3 bytes (UTF-8)
+ String str = "abc";
+ // byte[]
+ byte[] bytes = new byte[] {1, 2, 3, 4};
+ // BigDecimal
+ BigDecimal bd = new BigDecimal("123.45");
+ // Number
+ int num = 100;
+ // Date
+ Date date = new Date();
+ // Boolean
+ boolean bool = true;
+ // UUID
+ UUID uuid = UUID.randomUUID();
+ // JsonNode
+ JsonNode jsonNode = MAPPER.readTree("{\"a\":1}");
+ // GenericRecord
+ Schema schema =
+ new Schema.Parser()
+ .parse(
+ "{\"type\":\"record\",\"name\":\"Test\",\"fields\":[{\"name\":\"f\",\"type\":\"string\"}]}");
+ GenericRecord rec = new GenericData.Record(schema);
+ rec.put("f", "v");
+ // InetAddress
+ InetAddress ip = InetAddress.getByName("127.0.0.1");
+ // Unknown type
+ Object unknown = new Object();
+
+ Object[] row =
+ new Object[] {null, str, bytes, bd, num, date, bool, uuid, jsonNode, rec, ip, unknown};
+
+ Long result = RowMeta.getRowSizeEstimateFromRow(row);
+ long expected = 0;
+ // String
+ expected += str.getBytes().length;
+ // byte[]
+ expected += bytes.length;
+ // BigDecimal
+ expected += 32;
+ // Number
+ expected += 8;
+ // Date
+ expected += 8;
+ // Boolean
+ expected += 1;
+ // UUID
+ expected += 36;
+ // JsonNode
+ expected += jsonNode.toString().length() * 2L;
+ // GenericRecord
+ expected += rec.toString().length() * 2L;
+ // InetAddress
+ expected += ip.getHostAddress().length() * 2L;
+ // default
+ expected += 64;
+ assertEquals(expected, result);
+ }
+
+ @Test
+ void testOnlyNullElements() {
+ Object[] row = new Object[] {null, null, null};
+ Long result = RowMeta.getRowSizeEstimateFromRow(row);
+ assertEquals(0L, result);
+ }
+}
diff --git a/core/src/test/java/org/apache/hop/core/row/ValueMetaAndDataTests.java b/core/src/test/java/org/apache/hop/core/row/ValueMetaAndDataTests.java
new file mode 100644
index 0000000000..ffa4d3e68b
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/row/ValueMetaAndDataTests.java
@@ -0,0 +1,106 @@
+/*
+ * 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.core.row;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import org.apache.hop.core.row.value.ValueMetaBigNumber;
+import org.apache.hop.core.row.value.ValueMetaBinary;
+import org.apache.hop.core.row.value.ValueMetaBoolean;
+import org.apache.hop.core.row.value.ValueMetaDate;
+import org.apache.hop.core.row.value.ValueMetaInteger;
+import org.apache.hop.core.row.value.ValueMetaNumber;
+import org.apache.hop.core.row.value.ValueMetaSerializable;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.junit.jupiter.api.Test;
+
+/** Unit test for {@link ValueMetaAndData} */
+class ValueMetaAndDataTests {
+
+ @Test
+ void testStringType() {
+ ValueMetaAndData vm = new ValueMetaAndData("f1", "hello");
+
+ assertInstanceOf(ValueMetaString.class, vm.getValueMeta());
+ assertEquals("hello", vm.getValueData());
+ }
+
+ @Test
+ void testDoubleType() {
+ ValueMetaAndData vm = new ValueMetaAndData("f2", 1.23d);
+
+ assertInstanceOf(ValueMetaNumber.class, vm.getValueMeta());
+ assertEquals(1.23d, vm.getValueData());
+ }
+
+ @Test
+ void testLongType() {
+ ValueMetaAndData vm = new ValueMetaAndData("f3", 123L);
+
+ assertInstanceOf(ValueMetaInteger.class, vm.getValueMeta());
+ assertEquals(123L, vm.getValueData());
+ }
+
+ @Test
+ void testDateType() {
+ Date now = new Date();
+ ValueMetaAndData vm = new ValueMetaAndData("f4", now);
+
+ assertInstanceOf(ValueMetaDate.class, vm.getValueMeta());
+ assertEquals(now, vm.getValueData());
+ }
+
+ @Test
+ void testBigDecimalType() {
+ BigDecimal bd = new BigDecimal("123.45");
+ ValueMetaAndData vm = new ValueMetaAndData("f5", bd);
+
+ assertInstanceOf(ValueMetaBigNumber.class, vm.getValueMeta());
+ assertEquals(bd, vm.getValueData());
+ }
+
+ @Test
+ void testBooleanType() {
+ ValueMetaAndData vm = new ValueMetaAndData("f6", true);
+
+ assertInstanceOf(ValueMetaBoolean.class, vm.getValueMeta());
+ assertEquals(true, vm.getValueData());
+ }
+
+ @Test
+ void testByteArrayType() {
+ byte[] bytes = new byte[] {1, 2, 3};
+ ValueMetaAndData vm = new ValueMetaAndData("f7", bytes);
+
+ assertInstanceOf(ValueMetaBinary.class, vm.getValueMeta());
+ assertArrayEquals(bytes, (byte[]) vm.getValueData());
+ }
+
+ @Test
+ void testDefaultSerializableType() {
+ Object obj = new Object();
+ ValueMetaAndData vm = new ValueMetaAndData("f8", obj);
+
+ assertInstanceOf(ValueMetaSerializable.class, vm.getValueMeta());
+ assertEquals(obj, vm.getValueData());
+ }
+}
diff --git a/core/src/test/java/org/apache/hop/core/row/value/ValueMetaFactoryTests.java b/core/src/test/java/org/apache/hop/core/row/value/ValueMetaFactoryTests.java
new file mode 100644
index 0000000000..50af3cfbe7
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/row/value/ValueMetaFactoryTests.java
@@ -0,0 +1,120 @@
+/*
+ * 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.core.row.value;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.hop.core.row.IValueMeta;
+import org.junit.jupiter.api.Test;
+
+/** Unit test for {@link ValueMetaFactory} */
+class ValueMetaFactoryTests {
+
+ @Test
+ void testBigDecimal() {
+ IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(new BigDecimal("10.5"));
+ assertInstanceOf(ValueMetaBigNumber.class, meta);
+ }
+
+ @Test
+ void testDouble() {
+ IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(10.5d);
+ assertInstanceOf(ValueMetaNumber.class, meta);
+ }
+
+ @Test
+ void testLong() {
+ IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(10L);
+ assertInstanceOf(ValueMetaInteger.class, meta);
+ }
+
+ @Test
+ void testInteger_shouldReturnNullOrNeedClarification() {
+ IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(10);
+ assertNull(meta, "Integer is Number but not explicitly handled");
+ }
+
+ @Test
+ void testFloat() {
+ IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(10.5f);
+ assertNull(meta);
+ }
+
+ @Test
+ void testString() {
+ IValueMeta meta = ValueMetaFactory.guessValueMetaInterface("hello");
+ assertInstanceOf(ValueMetaString.class, meta);
+ }
+
+ @Test
+ void testDate() {
+ IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(new Date());
+ assertInstanceOf(ValueMetaDate.class, meta);
+ }
+
+ @Test
+ void testBoolean_true() {
+ IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(true);
+ assertInstanceOf(ValueMetaBoolean.class, meta);
+ }
+
+ @Test
+ void testBoolean_false() {
+ IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(false);
+ assertInstanceOf(ValueMetaBoolean.class, meta);
+ }
+
+ @Test
+ void testByteArray() {
+ IValueMeta meta = ValueMetaFactory.guessValueMetaInterface("abc".getBytes());
+ assertInstanceOf(ValueMetaBinary.class, meta);
+ }
+
+ @Test
+ void testJsonNode() throws Exception {
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode node = mapper.readTree("{\"a\":1}");
+
+ IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(node);
+ assertInstanceOf(ValueMetaJson.class, meta);
+ }
+
+ @Test
+ void testNull() {
+ IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(null);
+ assertNull(meta);
+ }
+
+ @Test
+ void testUnknownObject() {
+ IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(new Object());
+ assertNull(meta);
+ }
+
+ @Test
+ void testAtomicInteger_edgeCase() {
+ IValueMeta meta = ValueMetaFactory.guessValueMetaInterface(new AtomicInteger(5));
+ assertNull(meta);
+ }
+}
diff --git a/plugins/tech/avro/src/main/java/org/apache/hop/avro/transforms/avrodecode/AvroDecode.java b/plugins/tech/avro/src/main/java/org/apache/hop/avro/transforms/avrodecode/AvroDecode.java
index 0988984c34..9d15e22da9 100644
--- a/plugins/tech/avro/src/main/java/org/apache/hop/avro/transforms/avrodecode/AvroDecode.java
+++ b/plugins/tech/avro/src/main/java/org/apache/hop/avro/transforms/avrodecode/AvroDecode.java
@@ -17,10 +17,8 @@
package org.apache.hop.avro.transforms.avrodecode;
-import java.util.Map;
+import com.google.common.annotations.VisibleForTesting;
import org.apache.avro.Schema;
-import org.apache.avro.generic.GenericContainer;
-import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.util.Utf8;
import org.apache.hop.core.exception.HopException;
@@ -107,7 +105,7 @@ public class AvroDecode extends BaseTransform<AvroDecodeMeta, AvroDecodeData> {
return true;
}
- public static final int getStandardHopType(Schema.Field field) throws HopException {
+ public static int getStandardHopType(Schema.Field field) throws HopException {
Schema.Type type = field.schema().getType();
int basicType = getBasicType(type);
if (basicType != 0) {
@@ -139,72 +137,38 @@ public class AvroDecode extends BaseTransform<AvroDecodeMeta, AvroDecodeData> {
};
}
- public static final Object getStandardHopObject(Schema.Field field, Object avroValue)
- throws HopException {
- Object hopValue;
+ @VisibleForTesting
+ public static Object getStandardHopObject(Schema.Field field, Object avroValue) {
if (avroValue == null) {
- hopValue = null;
- } else {
- Schema.Type type = field.schema().getType();
- switch (type) {
- case NULL:
- hopValue = null;
- break;
- case ENUM:
- hopValue = avroValue.toString();
- break;
- case STRING:
- hopValue = ((Utf8) avroValue).toString();
- break;
- case BYTES, LONG, DOUBLE:
- hopValue = avroValue;
- break;
- case INT:
- hopValue = (long) (int) avroValue;
- break;
- case FLOAT:
- hopValue = (double) (float) avroValue;
- break;
- case BOOLEAN:
- hopValue = (boolean) avroValue;
- break;
- case RECORD:
- GenericData.Record record = (GenericData.Record) avroValue;
- hopValue = record.toString();
- break;
- case ARRAY:
- GenericData.Array array = (GenericData.Array) avroValue;
- hopValue = array.toString();
- break;
- case MAP:
- Map<Utf8, Object> map = (Map<Utf8, Object>) avroValue;
- hopValue = map.toString();
- break;
- case UNION:
- // This value can be a set of possible values...
- //
- if (avroValue instanceof Long
- || avroValue instanceof Double
- || avroValue instanceof String
- || avroValue instanceof Boolean
- || avroValue instanceof byte[]) {
- hopValue = avroValue;
- } else if (avroValue instanceof Float) {
- hopValue = Double.valueOf((float) avroValue);
- } else if (avroValue instanceof Integer) {
- hopValue = Integer.valueOf((int) avroValue).longValue();
- } else {
- hopValue = avroValue.toString();
- }
- break;
- case FIXED:
- GenericContainer container = (GenericContainer) avroValue;
- hopValue = container.toString();
- break;
- default:
- throw new HopException("Schema type " + type + " isn't handled yet");
- }
+ return null;
}
- return hopValue;
+
+ return convertBySchemaType(field.schema().getType(), avroValue);
+ }
+
+ private static Object convertBySchemaType(Schema.Type type, Object avroValue) {
+ return switch (type) {
+ case NULL -> null;
+ case ENUM, RECORD, ARRAY, MAP, FIXED -> avroValue.toString();
+ case STRING -> ((Utf8) avroValue).toString();
+ case BYTES, LONG, DOUBLE -> avroValue;
+ case INT -> (long) (int) avroValue;
+ case FLOAT -> (double) (float) avroValue;
+ case BOOLEAN -> (boolean) avroValue;
+ case UNION -> union(avroValue);
+ };
+ }
+
+ private static Object union(Object avroValue) {
+ return switch (avroValue) {
+ case Long l -> l;
+ case Double d -> d;
+ case String s -> s;
+ case Boolean b -> b;
+ case byte[] b -> b;
+ case Float f -> (double) f;
+ case Integer i -> i.longValue();
+ default -> avroValue.toString();
+ };
}
}
diff --git a/plugins/tech/avro/src/test/java/org/apache/hop/avro/transforms/avrodecode/AvroDecodeTests.java b/plugins/tech/avro/src/test/java/org/apache/hop/avro/transforms/avrodecode/AvroDecodeTests.java
new file mode 100644
index 0000000000..8ee5353f1f
--- /dev/null
+++ b/plugins/tech/avro/src/test/java/org/apache/hop/avro/transforms/avrodecode/AvroDecodeTests.java
@@ -0,0 +1,197 @@
+/*
+ * 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.avro.transforms.avrodecode;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.avro.Schema;
+import org.apache.avro.SchemaBuilder;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.avro.util.Utf8;
+import org.junit.jupiter.api.Test;
+
+/** Unit test for {@link AvroDecode} */
+class AvroDecodeTests {
+
+ @Test
+ void testNullValue() {
+ Schema schema = Schema.create(Schema.Type.STRING);
+ Object result = AvroDecode.getStandardHopObject(field("f", schema), null);
+ assertNull(result);
+ }
+
+ @Test
+ void testNullType() {
+ Schema schema = Schema.create(Schema.Type.NULL);
+ Object result = AvroDecode.getStandardHopObject(field("f", schema), "anything");
+ assertNull(result);
+ }
+
+ @Test
+ void testEnum() {
+ Schema schema = SchemaBuilder.enumeration("E").symbols("A", "B");
+ GenericData.EnumSymbol enumVal = new GenericData.EnumSymbol(schema, "A");
+
+ Object result = AvroDecode.getStandardHopObject(field("f", schema), enumVal);
+ assertEquals("A", result);
+ }
+
+ @Test
+ void testString() {
+ Schema schema = Schema.create(Schema.Type.STRING);
+ Utf8 utf8 = new Utf8("abc");
+
+ Object result = AvroDecode.getStandardHopObject(field("f", schema), utf8);
+ assertEquals("abc", result);
+ }
+
+ @Test
+ void testBytes() {
+ Schema schema = Schema.create(Schema.Type.BYTES);
+ byte[] data = new byte[] {1, 2, 3};
+
+ Object result = AvroDecode.getStandardHopObject(field("f", schema), data);
+ assertSame(data, result);
+ }
+
+ @Test
+ void testLongAndDouble() {
+ assertEquals(
+ 123L, AvroDecode.getStandardHopObject(field("f", Schema.create(Schema.Type.LONG)), 123L));
+
+ assertEquals(
+ 1.23, AvroDecode.getStandardHopObject(field("f", Schema.create(Schema.Type.DOUBLE)), 1.23));
+ }
+
+ @Test
+ void testInt() {
+ Object result = AvroDecode.getStandardHopObject(field("f", Schema.create(Schema.Type.INT)), 10);
+
+ assertEquals(10L, result);
+ assertInstanceOf(Long.class, result);
+ }
+
+ @Test
+ void testFloat() {
+ Object result =
+ AvroDecode.getStandardHopObject(field("f", Schema.create(Schema.Type.FLOAT)), 1.5f);
+
+ assertEquals(1.5d, result);
+ assertInstanceOf(Double.class, result);
+ }
+
+ @Test
+ void testBoolean() {
+ Object result =
+ AvroDecode.getStandardHopObject(field("f", Schema.create(Schema.Type.BOOLEAN)), true);
+ assertEquals(true, result);
+ }
+
+ @Test
+ void testRecord() {
+ Schema schema = SchemaBuilder.record("R").fields().requiredString("f").endRecord();
+ GenericRecord rec = new GenericData.Record(schema);
+ rec.put("f", "v");
+
+ Object result = AvroDecode.getStandardHopObject(field("f", schema), rec);
+ assertEquals(rec.toString(), result);
+ }
+
+ @Test
+ void testArray() {
+ Schema schema = Schema.createArray(Schema.create(Schema.Type.STRING));
+ GenericData.Array<String> array = new GenericData.Array<>(schema, List.of("a", "b"));
+
+ Object result = AvroDecode.getStandardHopObject(field("f", schema), array);
+ assertEquals(array.toString(), result);
+ }
+
+ @Test
+ void testMap() {
+ Schema schema = Schema.createMap(Schema.create(Schema.Type.STRING));
+ Map<Utf8, Object> map = new HashMap<>();
+ map.put(new Utf8("k"), "v");
+
+ Object result = AvroDecode.getStandardHopObject(field("f", schema), map);
+ assertEquals(map.toString(), result);
+ }
+
+ @Test
+ void testUnion() {
+ Schema unionSchema =
+ Schema.createUnion(
+ List.of(
+ Schema.create(Schema.Type.NULL),
+ Schema.create(Schema.Type.LONG),
+ Schema.create(Schema.Type.STRING)));
+
+ Schema.Field f = field("f", unionSchema);
+
+ // Long
+ assertEquals(10L, AvroDecode.getStandardHopObject(f, 10L));
+ // Double
+ assertEquals(1.2, AvroDecode.getStandardHopObject(f, 1.2));
+ // String
+ assertEquals("abc", AvroDecode.getStandardHopObject(f, "abc"));
+ // Boolean
+ assertEquals(true, AvroDecode.getStandardHopObject(f, true));
+ // byte[]
+ byte[] bytes = new byte[] {1};
+ assertSame(bytes, AvroDecode.getStandardHopObject(f, bytes));
+
+ // Float → Double
+ Object fResult = AvroDecode.getStandardHopObject(f, 1.5f);
+ assertEquals(1.5d, fResult);
+
+ // Integer → Long
+ Object iResult = AvroDecode.getStandardHopObject(f, 5);
+ assertEquals(5L, iResult);
+
+ // other → toString
+ Object other = new Object();
+ assertEquals(other.toString(), AvroDecode.getStandardHopObject(f, other));
+ }
+
+ @Test
+ void testFixed() {
+ Schema schema = SchemaBuilder.fixed("F").size(4);
+ GenericData.Fixed fixed = new GenericData.Fixed(schema, new byte[] {1, 2, 3, 4});
+
+ Object result = AvroDecode.getStandardHopObject(field("A", schema), fixed);
+ assertEquals(fixed.toString(), result);
+ }
+
+ @Test
+ void testDefaultException() {
+ assertThrows(
+ Exception.class,
+ () -> AvroDecode.getStandardHopObject(field("A", Schema.create(Schema.Type.ENUM)), null));
+ }
+
+ private Schema.Field field(String name, Schema schema) {
+ return new Schema.Field(name, schema, null, null);
+ }
+}
diff --git a/plugins/transforms/sasinput/src/main/java/org/apache/hop/pipeline/transforms/sasinput/SasInput.java b/plugins/transforms/sasinput/src/main/java/org/apache/hop/pipeline/transforms/sasinput/SasInput.java
index b5aad3d605..b7895785be 100644
--- a/plugins/transforms/sasinput/src/main/java/org/apache/hop/pipeline/transforms/sasinput/SasInput.java
+++ b/plugins/transforms/sasinput/src/main/java/org/apache/hop/pipeline/transforms/sasinput/SasInput.java
@@ -20,7 +20,10 @@ package org.apache.hop.pipeline.transforms.sasinput;
import com.epam.parso.Column;
import com.epam.parso.SasFileProperties;
import com.epam.parso.impl.SasFileReaderImpl;
+import com.google.common.annotations.VisibleForTesting;
import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@@ -146,47 +149,20 @@ public class SasInput extends BaseTransform<SasInputMeta, SasInputData> {
field.setPrecision(column.getFormat().getPrecision());
field.setType(SasUtil.getHopDataType(column.getType()));
}
+
Object sasValue = sasRow[index];
- Object value = null;
- IValueMeta inputValueMeta = null;
String fieldName = Const.NVL(field.getRename(), field.getName());
int outputIndex = getInputRowMeta().size() + i;
- if (sasValue instanceof byte[] bytes) {
- inputValueMeta = new ValueMetaString(fieldName);
- if (sasFileProperties.getEncoding() != null) {
- value = new String(bytes, sasFileProperties.getEncoding());
- } else {
- value = new String(bytes);
- }
- }
- if (sasValue instanceof String) {
- inputValueMeta = new ValueMetaString(fieldName);
- value = sasValue;
- }
- if (sasValue instanceof Double) {
- inputValueMeta = new ValueMetaNumber(fieldName);
- value = sasValue;
- }
- if (sasValue instanceof Float) {
- inputValueMeta = new ValueMetaNumber(fieldName);
- value = sasValue;
- }
- if (sasValue instanceof Long) {
- inputValueMeta = new ValueMetaInteger(fieldName);
- value = sasValue;
- }
- if (sasValue instanceof Integer) {
- inputValueMeta = new ValueMetaInteger(fieldName);
- value = (long) (int) sasValue;
- }
- if (sasValue instanceof Date) {
- inputValueMeta = new ValueMetaDate(fieldName);
- value = sasValue;
- }
- if (inputValueMeta != null) {
+
+ ConvertedValue converted = convertSasValue(sasValue, fieldName, sasFileProperties);
+ if (converted != null) {
+ IValueMeta inputValueMeta = converted.meta;
+ Object value = converted.value;
+
inputValueMeta.setLength(field.getLength());
inputValueMeta.setPrecision(field.getPrecision());
inputValueMeta.setConversionMask(field.getConversionMask());
+
IValueMeta outputValueMeta = data.outputRowMeta.getValueMeta(outputIndex);
outputRow[outputIndex] = outputValueMeta.convertData(inputValueMeta, value);
}
@@ -240,4 +216,30 @@ public class SasInput extends BaseTransform<SasInputMeta, SasInputData> {
}
return indexes;
}
+
+ @VisibleForTesting
+ public ConvertedValue convertSasValue(Object object, String field, SasFileProperties property) {
+ return switch (object) {
+ case byte[] bytes -> {
+ Charset charset =
+ property.getEncoding() != null
+ ? Charset.forName(property.getEncoding())
+ : StandardCharsets.UTF_8;
+
+ String value = new String(bytes, charset);
+ yield new ConvertedValue(new ValueMetaString(field), value);
+ }
+
+ case String s -> new ConvertedValue(new ValueMetaString(field), s);
+ case Double d -> new ConvertedValue(new ValueMetaNumber(field), d);
+ case Float f -> new ConvertedValue(new ValueMetaNumber(field), f);
+ case Long l -> new ConvertedValue(new ValueMetaInteger(field), l);
+ case Integer i -> new ConvertedValue(new ValueMetaInteger(field), (long) i);
+ case Date date -> new ConvertedValue(new ValueMetaDate(field), date);
+ case null, default -> null;
+ };
+ }
+
+ /** class valueMeta, object */
+ public record ConvertedValue(IValueMeta meta, Object value) {}
}
diff --git a/plugins/transforms/sasinput/src/test/java/org/apache/hop/pipeline/transforms/sasinput/SasInputTests.java b/plugins/transforms/sasinput/src/test/java/org/apache/hop/pipeline/transforms/sasinput/SasInputTests.java
new file mode 100644
index 0000000000..47b1ce44ae
--- /dev/null
+++ b/plugins/transforms/sasinput/src/test/java/org/apache/hop/pipeline/transforms/sasinput/SasInputTests.java
@@ -0,0 +1,154 @@
+/*
+ * 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.sasinput;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import com.epam.parso.SasFileProperties;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.core.row.value.ValueMetaDate;
+import org.apache.hop.core.row.value.ValueMetaInteger;
+import org.apache.hop.core.row.value.ValueMetaNumber;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.junit.rules.RestoreHopEnvironmentExtension;
+import org.apache.hop.pipeline.Pipeline;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mockito;
+
+/** Unit test for {@link SasInput} */
+@ExtendWith(RestoreHopEnvironmentExtension.class)
+class SasInputTests {
+
+ private SasInput input;
+
+ @BeforeEach
+ void setup() {
+ TransformMockHelper<SasInputMeta, SasInputData> helper =
+ new TransformMockHelper<>("sas_input_test", SasInputMeta.class, SasInputData.class);
+ Mockito.doReturn(helper.iLogChannel)
+ .when(helper.logChannelFactory)
+ .create(any(), any(ILoggingObject.class));
+
+ when(helper.pipeline.isRunning()).thenReturn(true);
+
+ SasInputMeta meta = helper.iTransformMeta;
+ SasInputData data = helper.iTransformData;
+ PipelineMeta plMeta = helper.pipelineMeta;
+ Pipeline pipeline = helper.pipeline;
+ input = new SasInput(helper.transformMeta, meta, data, 0, plMeta, pipeline);
+ }
+
+ @Test
+ void testByteArray_withEncoding() {
+ byte[] bytes = "Bob".getBytes(StandardCharsets.UTF_8);
+ SasInput.ConvertedValue result = input.convertSasValue(bytes, "name", property("UTF-8"));
+
+ assertNotNull(result);
+ assertInstanceOf(ValueMetaString.class, result.meta());
+ assertEquals("Bob", result.value());
+ }
+
+ @Test
+ void testByteArray_defaultEncoding() {
+ byte[] bytes = "world".getBytes(StandardCharsets.UTF_8);
+
+ SasInput.ConvertedValue result = input.convertSasValue(bytes, "field1", property(null));
+ assertNotNull(result);
+ assertEquals("world", result.value());
+ }
+
+ @Test
+ void testString() {
+ SasInput.ConvertedValue result = input.convertSasValue("abc", "field1", property(null));
+
+ assertInstanceOf(ValueMetaString.class, result.meta());
+ assertEquals("abc", result.value());
+ }
+
+ @Test
+ void testDouble() {
+ SasInput.ConvertedValue result = input.convertSasValue(10.5d, "field1", property(null));
+
+ assertInstanceOf(ValueMetaNumber.class, result.meta());
+ assertEquals(10.5d, result.value());
+ }
+
+ @Test
+ void testFloat() {
+ SasInput.ConvertedValue result = input.convertSasValue(3.14f, "field1", property(null));
+
+ assertInstanceOf(ValueMetaNumber.class, result.meta());
+ assertEquals(3.14f, result.value());
+ }
+
+ @Test
+ void testLong() {
+ SasInput.ConvertedValue result = input.convertSasValue(100L, "field1", property(null));
+
+ assertInstanceOf(ValueMetaInteger.class, result.meta());
+ assertEquals(100L, result.value());
+ }
+
+ @Test
+ void testInteger() {
+ SasInput.ConvertedValue result = input.convertSasValue(42, "field1", property(null));
+
+ assertInstanceOf(ValueMetaInteger.class, result.meta());
+ assertEquals(42L, result.value());
+ }
+
+ @Test
+ void testDate() {
+ Date now = new Date();
+ SasInput.ConvertedValue result = input.convertSasValue(now, "field1", property(null));
+
+ assertInstanceOf(ValueMetaDate.class, result.meta());
+ assertEquals(now, result.value());
+ }
+
+ @Test
+ void testNull() {
+ SasInput.ConvertedValue result = input.convertSasValue(null, "field1", property(null));
+
+ assertNull(result);
+ }
+
+ @Test
+ void testDefaultUnknownType() {
+ SasInput.ConvertedValue result = input.convertSasValue(new Object(), "field1", property(null));
+
+ assertNull(result);
+ }
+
+ private SasFileProperties property(String encoding) {
+ SasFileProperties p = new SasFileProperties();
+ p.setEncoding(encoding);
+ return p;
+ }
+}