Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,14 @@ You can use Regex in the Path expression.

*Example Path to filter on those that start with the letter 'a':* $.data[?(@=~/a.*/i)]

*JsonPath functions*

The aggregation functions provided by JayWay can be used at the end of a Path expression, e.g. `length()` (or its alias `size()`), `sum()`, `avg()`, `min()`, `max()`, `stddev()`, `keys()`, `first()`, `last()` and `index()`.

*Example Path to retrieve the number of elements of an array:* $.someArray.length()

A function always produces a single value, and therefore a single output row. Combining a function field with a field whose Path matches several elements is not supported: the transform reports a structure error because the two Paths do not yield the same number of rows. Read such a value in a dedicated JSON Input transform when the other fields return multiple rows.


=== Additional output fields tab

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.io.InputStream;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.RandomAccess;
Expand Down Expand Up @@ -299,7 +300,7 @@ private List<List<?>> evalCombinedResult() throws JsonInputException {
int i = 0;
for (JsonPath path : paths) {
Object raw = getReadContext().read(path);
List<Object> result = normalizeJsonPathResult(raw);
List<Object> result = raw == null ? readFunctionPath(path) : normalizeJsonPathResult(raw);
if (result.size() != lastSize && lastSize > 0 && !result.isEmpty()) {
throw new JsonInputException(
BaseMessages.getString(
Expand All @@ -322,6 +323,27 @@ private List<List<?>> evalCombinedResult() throws JsonInputException {
return results;
}

/**
* JsonPath never evaluates a function path (length(), sum(), keys(), ...) while
* ALWAYS_RETURN_LIST is set: combined with SUPPRESS_EXCEPTIONS it silently yields null. Such a
* path is re-evaluated here without that option, and the scalar it produces is exposed as a
* single row.
*/
private List<Object> readFunctionPath(JsonPath path) throws JsonInputException {
ReadContext context = getReadContext();
EnumSet<Option> options = EnumSet.noneOf(Option.class);
options.addAll(context.configuration().getOptions());
options.remove(Option.ALWAYS_RETURN_LIST);
Configuration functionConfiguration =
context.configuration().setOptions(options.toArray(new Option[0]));
Object document = context.json();
Object value = path.read(document, functionConfiguration);
if (value instanceof List<?> || value instanceof ArrayNode) {
return normalizeJsonPathResult(value);
}
return Collections.singletonList(value);
}

public static boolean isAllNull(Iterable<?> list) {
for (Object obj : list) {
if (obj != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,20 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.Option;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import org.apache.hop.core.IRowSet;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.logging.ILogChannel;
import org.apache.hop.pipeline.transforms.jsoninput.JsonInputField;
Expand Down Expand Up @@ -110,4 +115,57 @@ void testFastJsonReaderGetMaxRowSize() {
mainList.add(l3);
assertEquals(3, FastJsonReader.getMaxRowSize(Collections.singletonList(mainList)));
}

private static final String ARRAY_JSON = "{\"someArray\":[{\"x\":1},{\"x\":2}]}";

private static IRowSet readString(String json, String path) throws HopException {
JsonInputField field = new JsonInputField("value");
field.setPath(path);
FastJsonReader reader =
new FastJsonReader(new JsonInputField[] {field}, mock(ILogChannel.class));
return reader.parseStringValue(new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)));
}

@Test
void testLengthFunctionPathReturnsNumberOfArrayElements() throws Exception {
IRowSet rowSet = readString(ARRAY_JSON, "$.someArray.length()");
Object[] row = rowSet.getRow();
assertNotNull(row);
assertEquals(2, ((Number) row[0]).intValue());
}

@Test
void testSumFunctionPathIsEvaluated() throws Exception {
IRowSet rowSet = readString("{\"values\":[1,2,4]}", "$.values.sum()");
Object[] row = rowSet.getRow();
assertNotNull(row);
assertEquals(7, ((Number) row[0]).intValue());
}

@Test
void testFunctionPathReturnsSingleRow() throws Exception {
IRowSet rowSet = readString(ARRAY_JSON, "$.someArray.length()");
assertNotNull(rowSet.getRow());
assertNull(rowSet.getRow());
}

@Test
void testLengthFunctionPathOnJsonNodeInput() throws Exception {
JsonInputField field = new JsonInputField("value");
field.setPath("$.someArray.length()");
FastJsonReader reader =
new FastJsonReader(new JsonInputField[] {field}, mock(ILogChannel.class));
IRowSet rowSet = reader.parseJsonNodeValue(new ObjectMapper().readTree(ARRAY_JSON));
Object[] row = rowSet.getRow();
assertNotNull(row);
assertEquals(2, ((Number) row[0]).intValue());
}

@Test
void testRegularPathStillReturnsOneRowPerMatch() throws Exception {
IRowSet rowSet = readString(ARRAY_JSON, "$.someArray[*].x");
assertNotNull(rowSet.getRow());
assertNotNull(rowSet.getRow());
assertNull(rowSet.getRow());
}
}
Loading