/*! ******************************************************************************
*
* Pentaho Data Integration
*
* Copyright (C) 2002-2017 by Pentaho : http://www.pentaho.com
*
*******************************************************************************
*
* Licensed 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.pentaho.di.trans.steps.jsoninput;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import junit.framework.ComparisonFailure;
import org.apache.commons.io.IOUtils;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.pentaho.di.core.KettleClientEnvironment;
import org.pentaho.di.core.RowSet;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.exception.KettleFileException;
import org.pentaho.di.core.exception.KettleStepException;
import org.pentaho.di.core.fileinput.FileInputList;
import org.pentaho.di.core.logging.LogLevel;
import org.pentaho.di.core.logging.LoggingObjectInterface;
import org.pentaho.di.core.row.RowMeta;
import org.pentaho.di.core.row.RowMetaInterface;
import org.pentaho.di.core.row.ValueMetaInterface;
import org.pentaho.di.core.row.value.ValueMetaInteger;
import org.pentaho.di.core.row.value.ValueMetaNumber;
import org.pentaho.di.core.row.value.ValueMetaString;
import org.pentaho.di.core.variables.VariableSpace;
import org.pentaho.di.core.variables.Variables;
import org.pentaho.di.core.vfs.KettleVFS;
import org.pentaho.di.i18n.LanguageChoice;
import org.pentaho.di.trans.step.RowAdapter;
import org.pentaho.di.trans.step.StepErrorMeta;
import org.pentaho.di.trans.step.StepInterface;
import org.pentaho.di.trans.steps.mock.StepMockHelper;
public class JsonInputTest {
protected static final String BASE_RAM_DIR = "ram:/jsonInputTest/";
protected StepMockHelper<JsonInputMeta, JsonInputData> helper;
protected static final String getBasicTestJson() {
try {
// Note: Ultimately this would go in src/test/resources but our project is not setup for that yet.
InputStream is = JsonInputTest.class.getResourceAsStream( "/json/sample.json" );
return IOUtils.toString( is );
} catch ( IOException e ) {
throw new RuntimeException( "Unable to read sample JSON file.", e );
}
}
@BeforeClass
public static void init() throws KettleException {
KettleClientEnvironment.init();
}
@Before
public void setUp() {
helper =
new StepMockHelper<JsonInputMeta, JsonInputData>( "json input test", JsonInputMeta.class, JsonInputData.class );
when( helper.logChannelInterfaceFactory.create( any(), any( LoggingObjectInterface.class ) ) ).thenReturn(
helper.logChannelInterface );
when( helper.trans.isRunning() ).thenReturn( true );
}
@After
public void tearDown() {
helper.cleanUp();
}
@Test
public void testAttrFilter() throws Exception {
final String jsonInputField = getBasicTestJson();
testSimpleJsonPath( "$..book[?(@.isbn)].author", new ValueMetaString( "author w/ isbn" ),
new Object[][] { new Object[] { jsonInputField } },
new Object[][] { new Object[] { jsonInputField, "Herman Melville" },
new Object[] { jsonInputField, "J. R. R. Tolkien" } } );
}
@Test
public void testChildDot() throws Exception {
final String jsonInputField = getBasicTestJson();
testSimpleJsonPath( "$.store.bicycle.color", new ValueMetaString( "bcol" ),
new Object[][] { new Object[] { jsonInputField } },
new Object[][] { new Object[] { jsonInputField, "red" } } );
testSimpleJsonPath( "$.store.bicycle.price", new ValueMetaNumber( "p" ),
new Object[][] { new Object[] { jsonInputField } },
new Object[][] { new Object[] { jsonInputField, 19.95 } } );
}
@Test
public void testChildBrackets() throws Exception {
final String jsonInputField = getBasicTestJson();
testSimpleJsonPath( "$.['store']['bicycle']['color']", new ValueMetaString( "bcol" ),
new Object[][] { new Object[] { jsonInputField } },
new Object[][] { new Object[] { jsonInputField, "red" } } );
}
@Test
public void testChildBracketsNDots() throws Exception {
final String jsonInputField = getBasicTestJson();
testSimpleJsonPath( "$.['store'].['bicycle'].['color']", new ValueMetaString( "bcol" ),
new Object[][] { new Object[] { jsonInputField } },
new Object[][] { new Object[] { jsonInputField, "red" } } );
}
@Test
public void testIndex() throws Exception {
final String jsonInputField = getBasicTestJson();
testSimpleJsonPath( "$..book[2].title", new ValueMetaString( "title" ),
new Object[][] { new Object[] { jsonInputField } },
new Object[][] { new Object[] { jsonInputField, "Moby Dick" } } );
}
@Test
public void testIndexFirst() throws Exception {
final String jsonInputField = getBasicTestJson();
testSimpleJsonPath( "$..book[:2].category", new ValueMetaString( "category" ),
new Object[][] { new Object[] { jsonInputField } },
new Object[][] { new Object[] { jsonInputField, "reference" },
new Object[] { jsonInputField, "fiction" } } );
}
@Test
public void testIndexLastObj() throws Exception {
final String jsonInputField = getBasicTestJson();
JsonInput jsonInput =
createBasicTestJsonInput( "$..book[-1:]", new ValueMetaString( "last book" ), "json",
new Object[] { jsonInputField } );
RowComparatorListener rowComparator = new RowComparatorListener(
new Object[] { jsonInputField,
"{ \"category\": \"fiction\",\n"
+ " \"author\": \"J. R. R. Tolkien\",\n"
+ " \"title\": \"The Lord of the Rings\",\n"
+ " \"isbn\": \"0-395-19395-8\",\n"
+ " \"price\": 22.99\n"
+ "}\n" } );
rowComparator.setComparator( 1, new JsonComparison() );
jsonInput.addRowListener( rowComparator );
processRows( jsonInput, 2 );
Assert.assertEquals( 1, jsonInput.getLinesWritten() );
}
@Test
public void testIndexList() throws Exception {
final String jsonInputField = getBasicTestJson();
testSimpleJsonPath( "$..book[1,3].price", new ValueMetaNumber( "price" ),
new Object[][] { new Object[] { jsonInputField } },
new Object[][] { new Object[] { jsonInputField, 12.99 },
new Object[] { jsonInputField, 22.99 } } );
}
@Test
public void testSingleField() throws Exception {
JsonInputField isbn = new JsonInputField( "isbn" );
isbn.setPath( "$..book[?(@.isbn)].isbn" );
isbn.setType( ValueMetaInterface.TYPE_STRING );
JsonInputMeta meta = createSimpleMeta( "json", isbn );
JsonInput jsonInput = createJsonInput( "json", meta, new Object[] { getBasicTestJson() } );
RowComparatorListener rowComparator = new RowComparatorListener(
new Object[] { null, "0-553-21311-3" },
new Object[] { null, "0-395-19395-8" } );
rowComparator.setComparator( 0, null );
jsonInput.addRowListener( rowComparator );
processRows( jsonInput, 3 );
Assert.assertEquals( "error", 0, jsonInput.getErrors() );
Assert.assertEquals( "lines written", 2, jsonInput.getLinesWritten() );
}
@Test
public void testDualExp() throws Exception {
JsonInputField isbn = new JsonInputField( "isbn" );
isbn.setPath( "$..book[?(@.isbn)].isbn" );
isbn.setType( ValueMetaInterface.TYPE_STRING );
JsonInputField price = new JsonInputField( "price" );
price.setPath( "$..book[?(@.isbn)].price" );
price.setType( ValueMetaInterface.TYPE_NUMBER );
JsonInputMeta meta = createSimpleMeta( "json", isbn, price );
JsonInput jsonInput = createJsonInput( "json", meta, new Object[] { getBasicTestJson() } );
RowComparatorListener rowComparator = new RowComparatorListener(
new Object[] { null, "0-553-21311-3", 8.99 },
new Object[] { null, "0-395-19395-8", 22.99 } );
rowComparator.setComparator( 0, null );
jsonInput.addRowListener( rowComparator );
processRows( jsonInput, 3 );
Assert.assertEquals( "error", 0, jsonInput.getErrors() );
Assert.assertEquals( "lines written", 2, jsonInput.getLinesWritten() );
}
@Test
public void testDualExpMismatchError() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
helper.redirectLog( out, LogLevel.ERROR );
JsonInputField isbn = new JsonInputField( "isbn" );
isbn.setPath( "$..book[?(@.isbn)].isbn" );
isbn.setType( ValueMetaInterface.TYPE_STRING );
JsonInputField price = new JsonInputField( "price" );
price.setPath( "$..book[*].price" );
price.setType( ValueMetaInterface.TYPE_NUMBER );
try ( LocaleChange enUS = new LocaleChange( Locale.US ) ) {
JsonInputMeta meta = createSimpleMeta( "json", isbn, price );
JsonInput jsonInput = createJsonInput( "json", meta, new Object[] { getBasicTestJson() } );
processRows( jsonInput, 3 );
Assert.assertEquals( "error", 1, jsonInput.getErrors() );
Assert.assertEquals( "rows written", 0, jsonInput.getLinesWritten() );
String errors = IOUtils.toString( new ByteArrayInputStream( out.toByteArray() ), StandardCharsets.UTF_8.name() );
String expectedError =
"The data structure is not the same inside the resource!"
+ " We found 4 values for json path [$..book[*].price],"
+ " which is different that the number returned for path [$..book[?(@.isbn)].isbn] (2 values)."
+ " We MUST have the same number of values for all paths.";
Assert.assertTrue( "expected error", errors.contains( expectedError ) );
}
}
@Test
public void testBadExp() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
helper.redirectLog( out, LogLevel.ERROR );
try ( LocaleChange enUS = new LocaleChange( Locale.US ) ) {
ValueMetaString outputMeta = new ValueMetaString( "result" );
JsonInputField jpath = new JsonInputField( outputMeta.getName() );
jpath.setPath( "$..fail" );
jpath.setType( outputMeta.getType() );
JsonInputMeta jsonInputMeta = createSimpleMeta( "json", jpath );
jsonInputMeta.setIgnoreMissingPath( false );
JsonInput jsonInput = createJsonInput( "json", jsonInputMeta, new Object[] { getBasicTestJson() } );
processRows( jsonInput, 2 );
Assert.assertEquals( "errors", 1, jsonInput.getErrors() );
Assert.assertEquals( "rows written", 0, jsonInput.getLinesWritten() );
String expectedError = "We can not find any data with path [$..fail]";
String errors = IOUtils.toString( new ByteArrayInputStream( out.toByteArray() ), StandardCharsets.UTF_8.name() );
Assert.assertTrue( "error", errors.contains( expectedError ) );
}
}
@Test
public void testRemoveSourceField() throws Exception {
final String inCol = "json";
JsonInputField jpath = new JsonInputField( "isbn" );
jpath.setPath( "$..book[*].isbn" );
jpath.setType( ValueMetaInterface.TYPE_STRING );
JsonInputMeta meta = createSimpleMeta( inCol, jpath );
meta.setRemoveSourceField( true );
meta.setIgnoreMissingPath( true );
JsonInput jsonInput = createJsonInput( "json", meta, new Object[] { getBasicTestJson() } );
RowComparatorListener rowComparator = new RowComparatorListener(
new Object[] { "0-553-21311-3" },
new Object[] { "0-395-19395-8" } );
jsonInput.addRowListener( rowComparator );
processRows( jsonInput, 4 );
Assert.assertEquals( "errors", 0, jsonInput.getErrors() );
Assert.assertEquals( "lines written", 2, jsonInput.getLinesWritten() );
}
@Test
public void testRowLimit() throws Exception {
final String inCol = "json";
JsonInputField jpath = new JsonInputField( "isbn" );
jpath.setPath( "$..book[*].isbn" );
jpath.setType( ValueMetaInterface.TYPE_STRING );
JsonInputMeta meta = createSimpleMeta( inCol, jpath );
meta.setRemoveSourceField( true );
meta.setIgnoreMissingPath( true );
meta.setRowLimit( 2 );
JsonInput jsonInput = createJsonInput( "json", meta, new Object[] { getBasicTestJson() } );
processRows( jsonInput, 4 );
Assert.assertEquals( "errors", 0, jsonInput.getErrors() );
Assert.assertEquals( "lines written", 2, jsonInput.getLinesWritten() );
}
@Test
public void testSmallDoubles() throws Exception {
// legacy parser handles these but positive exp would read null
for ( String nbr : new String[] { "1e-20", "1.52999996e-20", "2.05E-20" } ) {
final String ibgNbrInput =
"{ \"number\": " + nbr + " }";
testSimpleJsonPath( "$.number", new ValueMetaNumber( "not so big number" ),
new Object[][] { new Object[] { ibgNbrInput } },
new Object[][] { new Object[] { ibgNbrInput, Double.parseDouble( nbr ) } } );
}
}
@Test
public void testJgdArray() throws Exception {
final String input =
" { \"arr\": [ [ { \"a\": 1, \"b\": 1}, { \"a\": 1, \"b\": 2} ], [ {\"a\": 3, \"b\": 4 } ] ] }";
JsonInput jsonInput =
createBasicTestJsonInput( "$.arr", new ValueMetaString( "array" ), "json", new Object[] { input } );
RowComparatorListener rowComparator =
new RowComparatorListener(
new Object[] { input, "[[{\"a\":1,\"b\":1},{\"a\":1,\"b\":2}],[{\"a\":3,\"b\":4}]]" } );
rowComparator.setComparator( 1, new JsonComparison() );
jsonInput.addRowListener( rowComparator );
processRows( jsonInput, 2 );
Assert.assertEquals( 1, jsonInput.getLinesWritten() );
}
@Test
public void testDefaultLeafToNull() throws Exception {
JsonInputField noPath = new JsonInputField( "price" );
noPath.setPath( "$..price" );
noPath.setType( ValueMetaInterface.TYPE_STRING );
ByteArrayOutputStream out = new ByteArrayOutputStream();
helper.redirectLog( out, LogLevel.ERROR );
JsonInputMeta meta = createSimpleMeta( "json", noPath );
meta.setIgnoreMissingPath( true );
meta.setRemoveSourceField( true );
final String input = getBasicTestJson();
JsonInput jsonInput = createJsonInput( "json", meta, new Object[] { input } );
processRows( jsonInput, 8 );
disposeJsonInput( jsonInput );
Assert.assertEquals( 5, jsonInput.getLinesWritten() );
}
@Test
public void testIfIgnorePathDoNotSkipRowIfInputIsNullOrFieldNotFound() throws Exception {
final String input1 = "{ \"value1\": \"1\",\n"
+ " \"value2\": \"2\",\n"
+ "}";
final String input2 = "{ \"value1\": \"3\""
+ "}";
final String input3 = "{ \"value2\": \"4\""
+ "}";
final String input4 = "{ \"value1\": null,\n"
+ " \"value2\": null,\n"
+ "}";
final String input5 = "{}";
final String input6 = null;
final String inCol = "input";
JsonInputField aField = new JsonInputField();
aField.setName( "a" );
aField.setPath( "$.value1" );
aField.setType( ValueMetaInterface.TYPE_STRING );
JsonInputField bField = new JsonInputField();
bField.setName( "b" );
bField.setPath( "$.value2" );
bField.setType( ValueMetaInterface.TYPE_STRING );
JsonInputMeta meta = createSimpleMeta( inCol, aField, bField );
meta.setIgnoreMissingPath( true );
JsonInput step = createJsonInput( inCol, meta, new Object[] { input1 },
new Object[] { input2 },
new Object[] { input3 },
new Object[] { input4 },
new Object[] { input5 },
new Object[] { input6 }
);
step.addRowListener(
new RowComparatorListener(
new Object[]{ input1, "1", "2" }, new Object[]{ input2, "3", null }, new Object[]{ input3, null, "4" },
new Object[]{ input4, null, null }, new Object[]{ input5, null, null }, new Object[]{ input6, null, null } ) );
processRows( step, 5 );
}
@Test
public void testBfsMatchOrder() throws Exception {
// streaming will be dfs..ref impl is bfs
String input = "{ \"a\": { \"a\" : { \"b\" :2 } , \"b\":1 } }";
JsonInput jsonInput =
createBasicTestJsonInput( "$..a.b", new ValueMetaInteger( "b" ), "in", new Object[] { input } );
RowComparatorListener rowComparator = new RowComparatorListener( jsonInput,
new Object[] { input, 1L },
new Object[] { input, 2L } );
rowComparator.setComparator( 0, null );
processRows( jsonInput, 2 );
Assert.assertEquals( 2, jsonInput.getLinesWritten() );
}
@Test
public void testRepeatFieldSingleObj() throws Exception {
final String input = " { \"items\": [ "
+ "{ \"a\": 1, \"b\": null }, "
+ "{ \"a\":null, \"b\":2 }, "
+ "{ \"a\":3, \"b\":null }, "
+ "{ \"a\":4, \"b\":4 } ] }";
final String inCol = "input";
JsonInputField aField = new JsonInputField();
aField.setName( "a" );
aField.setPath( "$.items[*].a" );
aField.setType( ValueMetaInterface.TYPE_INTEGER );
JsonInputField bField = new JsonInputField();
bField.setName( "b" );
bField.setPath( "$.items[*].b" );
bField.setType( ValueMetaInterface.TYPE_INTEGER );
bField.setRepeated( true );
JsonInputMeta meta = createSimpleMeta( inCol, aField, bField );
meta.setIgnoreMissingPath( true );
JsonInput step = createJsonInput( inCol, meta, new Object[] { input } );
step.addRowListener(
new RowComparatorListener(
new Object[] { input, 1L, null },
new Object[] { input, null, 2L },
new Object[] { input, 3L, 2L },
new Object[] { input, 4L, 4L } ) );
processRows( step, 4 );
Assert.assertEquals( 4, step.getLinesWritten() );
}
@Test
public void testPathMissingIgnore() throws Exception {
final String input = "{ \"value1\": \"1\",\n"
+ " \"value2\": \"2\",\n"
+ "}";
final String inCol = "input";
JsonInputField aField = new JsonInputField();
aField.setName( "a" );
aField.setPath( "$.value1" );
aField.setType( ValueMetaInterface.TYPE_STRING );
JsonInputField bField = new JsonInputField();
bField.setName( "b" );
bField.setPath( "$.value2" );
bField.setType( ValueMetaInterface.TYPE_STRING );
JsonInputField cField = new JsonInputField();
cField.setName( "c" );
cField.setPath( "$.notexistpath.value3" );
cField.setType( ValueMetaInterface.TYPE_STRING );
JsonInputMeta meta = createSimpleMeta( inCol, aField, bField, cField );
meta.setIgnoreMissingPath( true );
JsonInput step = createJsonInput( inCol, meta, new Object[] { input } );
step.addRowListener(
new RowComparatorListener(
new Object[] { input, "1", "2", null } ) );
processRows( step, 1 );
Assert.assertEquals( 1, step.getLinesWritten() );
}
/**
* PDI-10384 Huge numbers causing exception in JSON input step<br>
*/
@Test
public void testLargeDoubles() throws Exception {
// legacy mode yields null for these
for ( String nbr : new String[] { "1e20", "2.05E20", "1.52999996e20" } ) {
final String ibgNbrInput =
"{ \"number\": " + nbr + " }";
testSimpleJsonPath( "$.number", new ValueMetaNumber( "not so big number" ),
new Object[][] { new Object[] { ibgNbrInput } },
new Object[][] { new Object[] { ibgNbrInput, Double.parseDouble( nbr ) } } );
}
}
@Test
public void testNullProp() throws Exception {
final String input = "{ \"obj\": [ { \"nval\": null, \"val\": 2 }, { \"val\": 1 } ] }";
JsonInput jsonInput =
createBasicTestJsonInput( "$.obj[?(@.nval)].val", new ValueMetaString( "obj" ), "json", new Object[] { input } );
RowComparatorListener rowComparator = new RowComparatorListener(
new Object[] { input, "2" } );
rowComparator.setComparator( 1, new JsonComparison() );
jsonInput.addRowListener( rowComparator );
processRows( jsonInput, 2 );
// in jsonpath 2.0->2.1, null value properties started being counted as existing
Assert.assertEquals( 1, jsonInput.getLinesWritten() );
}
@Test
public void testDualExpMismatchPathLeafToNull() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
helper.redirectLog( out, LogLevel.ERROR );
JsonInputField isbn = new JsonInputField( "isbn" );
isbn.setPath( "$..book[*].isbn" );
isbn.setType( ValueMetaInterface.TYPE_STRING );
JsonInputField price = new JsonInputField( "price" );
price.setPath( "$..book[*].price" );
price.setType( ValueMetaInterface.TYPE_NUMBER );
JsonInputMeta meta = createSimpleMeta( "json", isbn, price );
meta.setIgnoreMissingPath( true );
meta.setRemoveSourceField( true );
JsonInput jsonInput = createJsonInput( "json", meta, new Object[] { getBasicTestJson() } );
RowComparatorListener rowComparator = new RowComparatorListener(
new Object[] { null, 8.95d },
new Object[] { null, 12.99d },
new Object[] { "0-553-21311-3", 8.99d },
new Object[] { "0-395-19395-8", 22.99d } );
jsonInput.addRowListener( rowComparator );
processRows( jsonInput, 5 );
Assert.assertEquals( out.toString(), 0, jsonInput.getErrors() );
Assert.assertEquals( "rows written", 4, jsonInput.getLinesWritten() );
}
@Test
public void testSingleObjPred() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
helper.redirectLog( out, LogLevel.ERROR );
JsonInputField bic = new JsonInputField( "color" );
bic.setPath( "$.store.bicycle[?(@.price)].color" );
bic.setType( ValueMetaInterface.TYPE_STRING );
JsonInputMeta meta = createSimpleMeta( "json", bic );
meta.setRemoveSourceField( true );
JsonInput jsonInput = createJsonInput( "json", meta, new Object[] { getBasicTestJson() } );
RowComparatorListener rowComparator = new RowComparatorListener(
new Object[] { "red" } );
jsonInput.addRowListener( rowComparator );
processRows( jsonInput, 2 );
Assert.assertEquals( out.toString(), 0, jsonInput.getErrors() );
Assert.assertEquals( "rows written", 1, jsonInput.getLinesWritten() );
}
@Test
public void testArrayOut() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
helper.redirectLog( out, LogLevel.ERROR );
JsonInputField byc = new JsonInputField( "books (array)" );
byc.setPath( "$.store.book" );
byc.setType( ValueMetaInterface.TYPE_STRING );
JsonInputMeta meta = createSimpleMeta( "json", byc );
meta.setRemoveSourceField( true );
JsonInput jsonInput = createJsonInput( "json", meta, new Object[] { getBasicTestJson() } );
RowComparatorListener rowComparator = new RowComparatorListener(
new Object[] {
"[{\"category\":\"reference\",\"author\":\"Nigel Rees\",\"title\":\"Sayings of the Century\",\"price\":8.95},"
+ "{\"category\":\"fiction\",\"author\":\"Evelyn Waugh\",\"title\":\"Sword of Honour\",\"price\":12.99},"
+ "{\"category\":\"fiction\",\"author\":\"Herman Melville\",\"title\":\"Moby Dick\","
+ "\"isbn\":\"0-553-21311-3\",\"price\":8.99},{\"category\":\"fiction\",\"author\":\"J. R. R. Tolkien\","
+ "\"title\":\"The Lord of the Rings\",\"isbn\":\"0-395-19395-8\",\"price\":22.99}]" } );
jsonInput.addRowListener( rowComparator );
processRows( jsonInput, 2 );
Assert.assertEquals( out.toString(), 0, jsonInput.getErrors() );
Assert.assertEquals( "rows written", 1, jsonInput.getLinesWritten() );
}
@Test
public void testObjectOut() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
helper.redirectLog( out, LogLevel.ERROR );
JsonInputField bic = new JsonInputField( "the bicycle (obj)" );
bic.setPath( "$.store.bicycle" );
bic.setType( ValueMetaInterface.TYPE_STRING );
JsonInputMeta meta = createSimpleMeta( "json", bic );
meta.setRemoveSourceField( true );
JsonInput jsonInput = createJsonInput( "json", meta, new Object[] { getBasicTestJson() } );
RowComparatorListener rowComparator = new RowComparatorListener(
new Object[] { "{\"color\":\"red\",\"price\":19.95}" } );
jsonInput.addRowListener( rowComparator );
processRows( jsonInput, 2 );
Assert.assertEquals( out.toString(), 0, jsonInput.getErrors() );
Assert.assertEquals( "rows written", 1, jsonInput.getLinesWritten() );
}
@Test
public void testBicycleAsterisk() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
helper.redirectLog( out, LogLevel.ERROR );
JsonInputField byc = new JsonInputField( "badger" );
byc.setPath( "$.store.bicycle[*]" );
byc.setType( ValueMetaInterface.TYPE_STRING );
JsonInputMeta meta = createSimpleMeta( "json", byc );
meta.setRemoveSourceField( true );
JsonInput jsonInput = createJsonInput( "json", meta, new Object[] { getBasicTestJson() } );
RowComparatorListener rowComparator = new RowComparatorListener(
new Object[] { "red" },
new Object[] { "19.95" } );
jsonInput.addRowListener( rowComparator );
processRows( jsonInput, 2 );
assertEquals( out.toString(), 0, jsonInput.getErrors() );
assertEquals( "rows written", 2, jsonInput.getLinesWritten() );
}
@Test
public void testNullInputs() throws Exception {
final String jsonInputField = getBasicTestJson();
testSimpleJsonPath( "$..book[?(@.isbn)].author", new ValueMetaString( "author w/ isbn" ),
new Object[][] {
new Object[] { null },
new Object[] { jsonInputField },
new Object[] { null } },
new Object[][] {
new Object[] { null, null },
new Object[] { jsonInputField, "Herman Melville" },
new Object[] { jsonInputField, "J. R. R. Tolkien" },
new Object[] { null, null }
} );
}
/**
* File tests
*/
@Test
public void testNullFileList() throws Exception {
ByteArrayOutputStream err = new ByteArrayOutputStream();
helper.redirectLog( err, LogLevel.ERROR );
try {
JsonInputField price = new JsonInputField();
price.setName( "price" );
price.setType( ValueMetaInterface.TYPE_NUMBER );
price.setPath( "$..book[*].price" );
List<FileObject> fileList = Arrays.asList( null, null );
JsonInputMeta meta = createFileListMeta( fileList );
meta.setInputFields( new JsonInputField[] { price } );
meta.setIncludeRowNumber( true );
meta.setRowNumberField( "rownbr" );
meta.setShortFileNameField( "fname" );
JsonInput jsonInput = createJsonInput( meta );
processRows( jsonInput, 5 );
disposeJsonInput( jsonInput );
assertEquals( err.toString(), 2, jsonInput.getErrors() );
} finally {
deleteFiles();
}
}
@Test
public void testFileList() throws Exception {
ByteArrayOutputStream err = new ByteArrayOutputStream();
helper.redirectLog( err, LogLevel.ERROR );
final String input1 = getBasicTestJson();
final String input2 = "{ \"store\": { \"book\": [ { \"price\": 9.99 } ] } }";
try ( FileObject fileObj1 = KettleVFS.getFileObject( BASE_RAM_DIR + "test1.json" );
FileObject fileObj2 = KettleVFS.getFileObject( BASE_RAM_DIR + "test2.json" ) ) {
try ( OutputStream out = fileObj1.getContent().getOutputStream() ) {
out.write( input1.getBytes() );
}
try ( OutputStream out = fileObj2.getContent().getOutputStream() ) {
out.write( input2.getBytes() );
}
JsonInputField price = new JsonInputField();
price.setName( "price" );
price.setType( ValueMetaInterface.TYPE_NUMBER );
price.setPath( "$..book[*].price" );
List<FileObject> fileList = Arrays.asList( fileObj1, fileObj2 );
JsonInputMeta meta = createFileListMeta( fileList );
meta.setInputFields( new JsonInputField[] { price } );
meta.setIncludeRowNumber( true );
meta.setRowNumberField( "rownbr" );
meta.setShortFileNameField( "fname" );
JsonInput jsonInput = createJsonInput( meta );
RowComparatorListener rowComparator = new RowComparatorListener(
new Object[] { 8.95d, 1L, "test1.json" },
new Object[] { 12.99d, 2L, "test1.json" },
new Object[] { 8.99d, 3L, "test1.json" },
new Object[] { 22.99d, 4L, "test1.json" },
new Object[] { 9.99d, 5L, "test2.json" } );
jsonInput.addRowListener( rowComparator );
processRows( jsonInput, 5 );
disposeJsonInput( jsonInput );
assertEquals( err.toString(), 0, jsonInput.getErrors() );
} finally {
deleteFiles();
}
}
@Test
public void testNoFilesInListError() throws Exception {
ByteArrayOutputStream err = new ByteArrayOutputStream();
helper.redirectLog( err, LogLevel.ERROR );
JsonInputMeta meta = createFileListMeta( Collections.<FileObject>emptyList() );
meta.setDoNotFailIfNoFile( false );
JsonInputField price = new JsonInputField();
price.setName( "price" );
price.setType( ValueMetaInterface.TYPE_NUMBER );
price.setPath( "$..book[*].price" );
meta.setInputFields( new JsonInputField[] { price } );
try ( LocaleChange enUS = new LocaleChange( Locale.US ) ) {
JsonInput jsonInput = createJsonInput( meta );
processRows( jsonInput, 1 );
}
String errMsgs = err.toString();
assertTrue( errMsgs, errMsgs.contains( "No file(s) specified!" ) );
}
@Test
public void testZipFileInput() throws Exception {
ByteArrayOutputStream err = new ByteArrayOutputStream();
helper.redirectLog( err, LogLevel.ERROR );
final String input = getBasicTestJson();
try ( FileObject fileObj = KettleVFS.getFileObject( BASE_RAM_DIR + "test.zip" ) ) {
fileObj.createFile();
try ( OutputStream out = fileObj.getContent().getOutputStream() ) {
try ( ZipOutputStream zipOut = new ZipOutputStream( out ) ) {
ZipEntry jsonFile = new ZipEntry( "test.json" );
zipOut.putNextEntry( jsonFile );
zipOut.write( input.getBytes() );
zipOut.closeEntry();
zipOut.flush();
}
}
JsonInputField price = new JsonInputField();
price.setName( "price" );
price.setType( ValueMetaInterface.TYPE_NUMBER );
price.setPath( "$..book[*].price" );
JsonInputMeta meta = createSimpleMeta( "in file", price );
meta.setIsAFile( true );
meta.setRemoveSourceField( true );
JsonInput jsonInput = createJsonInput( "in file", meta, new Object[][] {
new Object[] { "zip:" + BASE_RAM_DIR + "test.zip!/test.json" }
} );
RowComparatorListener rowComparator = new RowComparatorListener(
new Object[] { 8.95d },
new Object[] { 12.99d },
new Object[] { 8.99d },
new Object[] { 22.99d } );
jsonInput.addRowListener( rowComparator );
processRows( jsonInput, 5 );
Assert.assertEquals( err.toString(), 0, jsonInput.getErrors() );
} finally {
deleteFiles();
}
}
@Test
public void testExtraFileFields() throws Exception {
ByteArrayOutputStream err = new ByteArrayOutputStream();
helper.redirectLog( err, LogLevel.ERROR );
final String input1 = getBasicTestJson();
final String input2 = "{ \"store\": { \"bicycle\": { \"color\": \"blue\" } } }";
final String path1 = BASE_RAM_DIR + "test1.json";
final String path2 = BASE_RAM_DIR + "test2.js";
try ( FileObject fileObj1 = KettleVFS.getFileObject( path1 );
FileObject fileObj2 = KettleVFS.getFileObject( path2 ) ) {
try ( OutputStream out = fileObj1.getContent().getOutputStream() ) {
out.write( input1.getBytes() );
}
try ( OutputStream out = fileObj2.getContent().getOutputStream() ) {
out.write( input2.getBytes() );
}
JsonInputField color = new JsonInputField();
color.setName( "color" );
color.setType( ValueMetaInterface.TYPE_STRING );
color.setPath( "$.store.bicycle.color" );
JsonInputMeta meta = createSimpleMeta( "in file", color );
meta.setInFields( true );
meta.setIsAFile( true );
meta.setRemoveSourceField( true );
meta.setExtensionField( "extension" );
meta.setPathField( "dir path" );
meta.setSizeField( "size" );
meta.setIsHiddenField( "hidden?" );
meta.setLastModificationDateField( "last modified" );
meta.setUriField( "URI" );
meta.setRootUriField( "root URI" );
// custom checkers for size and last modified
RowComparatorListener rowComparator = new RowComparatorListener(
new Object[] { "red",
"json", "ram:///jsonInputTest", -1L, false, new Date( 0 ), "ram:///jsonInputTest/test1.json", "ram:///" },
new Object[] { "blue",
"js", "ram:///jsonInputTest", -1L, false, new Date( 0 ), "ram:///jsonInputTest/test2.js", "ram:///" } );
rowComparator.setComparator( 3, new RowComparatorListener.Comparison<Object>() {
@Override
public boolean equals( Object expected, Object actual ) throws Exception {
// just want a valid size
return ( (long) actual ) > 0L;
}
} );
rowComparator.setComparator( 5, new RowComparatorListener.Comparison<Object>() {
@Override
public boolean equals( Object expected, Object actual ) throws Exception {
return ( (Date) actual ).after( new Date( 0 ) );
}
} );
JsonInput jsonInput = createJsonInput( "in file", meta, new Object[][] {
new Object[] { path1 },
new Object[] { path2 }
} );
jsonInput.addRowListener( rowComparator );
processRows( jsonInput, 3 );
Assert.assertEquals( err.toString(), 0, jsonInput.getErrors() );
} finally {
deleteFiles();
}
}
@Test
public void testZeroSizeFile() throws Exception {
ByteArrayOutputStream log = new ByteArrayOutputStream();
helper.redirectLog( log, LogLevel.BASIC );
try ( FileObject fileObj = KettleVFS.getFileObject( BASE_RAM_DIR + "test.json" );
LocaleChange enUs = new LocaleChange( Locale.US ); ) {
fileObj.createFile();
JsonInputField price = new JsonInputField();
price.setName( "price" );
price.setType( ValueMetaInterface.TYPE_NUMBER );
price.setPath( "$..book[*].price" );
JsonInputMeta meta = createSimpleMeta( "in file", price );
meta.setIsAFile( true );
meta.setRemoveSourceField( true );
meta.setIgnoreEmptyFile( false );
JsonInput jsonInput = createJsonInput( "in file", meta, new Object[][] {
new Object[] { BASE_RAM_DIR + "test.json" }
} );
processRows( jsonInput, 1 );
String logMsgs = log.toString();
assertTrue( logMsgs, logMsgs.contains( "is empty!" ) );
} finally {
deleteFiles();
}
}
/**
* PDI-13859
*/
@Test
public void testBracketEscape() throws Exception {
String input = "{\"a\":1,\"b(1)\":2}";
testSimpleJsonPath( "$.['b(1)']", new ValueMetaInteger( "b(1)" ),
new Object[][] { new Object[] { input } },
new Object[][] { new Object[] { input, 2L } } );
}
@Test
public void testBadInput() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
helper.redirectLog( out, LogLevel.ERROR );
JsonInputField isbn = new JsonInputField( "isbn" );
isbn.setPath( "$..book[?(@.isbn)].isbn" );
isbn.setType( ValueMetaInterface.TYPE_STRING );
String input = "{{";
try ( LocaleChange enUS = new LocaleChange( Locale.US ) ) {
JsonInputMeta meta = createSimpleMeta( "json", isbn );
JsonInput jsonInput = createJsonInput( "json", meta, new Object[] { input } );
processRows( jsonInput, 3 );
Assert.assertEquals( "error", 1, jsonInput.getErrors() );
Assert.assertEquals( "rows written", 0, jsonInput.getLinesWritten() );
String errors = IOUtils.toString( new ByteArrayInputStream( out.toByteArray() ), StandardCharsets.UTF_8.name() );
Assert.assertTrue( "expected error", errors.contains( "Error parsing string" ) );
}
}
@Test
public void testErrorRedirect() throws Exception {
JsonInputField field = new JsonInputField( "value" );
field.setPath( "$.value" );
field.setType( ValueMetaInterface.TYPE_STRING );
String input1 = "{{";
String input2 = "{ \"value\": \"ok\" }";
JsonInputMeta meta = createSimpleMeta( "json", field );
meta.setRemoveSourceField( true );
when( helper.stepMeta.isDoingErrorHandling() ).thenReturn( true );
JsonInput jsonInput = createJsonInput( "json", meta, new Object[] { input1 }, new Object[] { input2 } );
StepErrorMeta errMeta = new StepErrorMeta( jsonInput, helper.stepMeta );
errMeta.setEnabled( true );
errMeta.setErrorFieldsValuename( "err field" );
when( helper.stepMeta.getStepErrorMeta() ).thenReturn( errMeta );
final List<Object[]> errorLines = new ArrayList<>();
jsonInput.addRowListener( new RowComparatorListener( new Object[] { "ok" } ) {
@Override
public void errorRowWrittenEvent( RowMetaInterface rowMeta, Object[] row ) throws KettleStepException {
errorLines.add( row );
}
} );
processRows( jsonInput, 3 );
Assert.assertEquals( "fwd error", 1, errorLines.size() );
Assert.assertEquals( "input in err line", input1, errorLines.get( 0 )[ 0 ] );
Assert.assertEquals( "rows written", 1, jsonInput.getLinesWritten() );
}
@Test
public void testUrlInput() throws Exception {
JsonInputField field = new JsonInputField( "value" );
field.setPath( "$.value" );
field.setType( ValueMetaInterface.TYPE_STRING );
String input1 = "http://localhost/test.json";
JsonInputMeta meta = createSimpleMeta( "json", field );
meta.setReadUrl( true );
JsonInput jsonInput = createJsonInput( "json", meta, new Object[] { input1 } );
processRows( jsonInput, 3 );
Assert.assertEquals( 1, jsonInput.getErrors() );
}
protected JsonInputMeta createSimpleMeta( String inputColumn, JsonInputField... jsonPathFields ) {
JsonInputMeta jsonInputMeta = new JsonInputMeta();
jsonInputMeta.setDefault();
jsonInputMeta.setInFields( true );
jsonInputMeta.setFieldValue( inputColumn );
jsonInputMeta.setInputFields( jsonPathFields );
jsonInputMeta.setIgnoreMissingPath( true );
return jsonInputMeta;
}
private void deleteFiles() throws FileSystemException, KettleFileException {
try ( FileObject baseDir = KettleVFS.getFileObject( BASE_RAM_DIR ) ) {
baseDir.deleteAll();
}
}
protected JsonInput createJsonInput( JsonInputMeta meta ) {
JsonInputData data = new JsonInputData();
JsonInput jsonInput = new JsonInput( helper.stepMeta, helper.stepDataInterface, 0, helper.transMeta, helper.trans );
jsonInput.init( meta, data );
return jsonInput;
}
protected void disposeJsonInput( JsonInput jsonInput ) {
jsonInput.dispose( null, helper.stepDataInterface );
}
protected JsonInputMeta createFileListMeta( final List<FileObject> files ) {
JsonInputMeta meta = new JsonInputMeta() {
@Override
public FileInputList getFileInputList( VariableSpace space ) {
return new FileInputList() {
@Override
public List<FileObject> getFiles() {
return files;
}
@Override
public int nrOfFiles() {
return files.size();
}
};
}
};
meta.setDefault();
meta.setInFields( false );
meta.setIgnoreMissingPath( false );
return meta;
}
protected void testSimpleJsonPath( String jsonPath,
ValueMetaInterface outputMeta,
Object[][] inputRows, Object[][] outputRows ) throws Exception {
final String inCol = "in";
JsonInput jsonInput = createBasicTestJsonInput( jsonPath, outputMeta, inCol, inputRows );
RowComparatorListener rowComparator = new RowComparatorListener( outputRows );
jsonInput.addRowListener( rowComparator );
processRows( jsonInput, outputRows.length + 1 );
Assert.assertEquals( "rows written", outputRows.length, jsonInput.getLinesWritten() );
Assert.assertEquals( "errors", 0, jsonInput.getErrors() );
}
protected void processRows( StepInterface step, final int maxCalls ) throws Exception {
for ( int outRowIdx = 0; outRowIdx < maxCalls; outRowIdx++ ) {
if ( !step.processRow( helper.processRowsStepMetaInterface, helper.processRowsStepDataInterface ) ) {
break;
}
}
}
protected JsonInput createBasicTestJsonInput( String jsonPath, ValueMetaInterface outputMeta, final String inCol,
Object[]... inputRows ) {
JsonInputField jpath = new JsonInputField( outputMeta.getName() );
jpath.setPath( jsonPath );
jpath.setType( outputMeta.getType() );
JsonInputMeta meta = createSimpleMeta( inCol, jpath );
return createJsonInput( inCol, meta, inputRows );
}
protected JsonInput createJsonInput( final String inCol, JsonInputMeta meta, Object[]... inputRows ) {
return createJsonInput( inCol, meta, null, inputRows );
}
protected JsonInput createJsonInput( final String inCol, JsonInputMeta meta, VariableSpace variables, Object[]... inputRows ) {
JsonInputData data = new JsonInputData();
JsonInput jsonInput = new JsonInput( helper.stepMeta, helper.stepDataInterface, 0, helper.transMeta, helper.trans );
RowSet input = helper.getMockInputRowSet( inputRows );
RowMetaInterface rowMeta = createRowMeta( new ValueMetaString( inCol ) );
input.setRowMeta( rowMeta );
jsonInput.getInputRowSets().add( input );
jsonInput.setInputRowMeta( rowMeta );
jsonInput.initializeVariablesFrom( variables );
jsonInput.init( meta, data );
return jsonInput;
}
protected static class RowComparatorListener extends RowAdapter {
Object[][] data;
int rowNbr = 0;
private Map<Integer, Comparison<Object>> comparators = new HashMap<>();
public RowComparatorListener( Object[]... data ) {
this.data = data;
}
public RowComparatorListener( StepInterface step, Object[]... data ) {
this.data = data;
step.addRowListener( this );
}
/**
* @param colIdx
* @param comparator
*/
public void setComparator( int colIdx, Comparison<Object> comparator ) {
comparators.put( colIdx, comparator );
}
@Override
public void rowWrittenEvent( RowMetaInterface rowMeta, Object[] row ) throws KettleStepException {
if ( rowNbr >= data.length ) {
throw new ComparisonFailure( "too many output rows", "" + data.length, "" + rowNbr + 1 );
} else {
for ( int i = 0; i < data[ rowNbr ].length; i++ ) {
try {
boolean eq = true;
if ( comparators.containsKey( i ) ) {
Comparison<Object> comp = comparators.get( i );
if ( comp != null ) {
eq = comp.equals( data[ rowNbr ][ i ], row[ i ] );
}
} else {
ValueMetaInterface valueMeta = rowMeta.getValueMeta( i );
eq = valueMeta.compare( data[ rowNbr ][ i ], row[ i ] ) == 0;
}
if ( !eq ) {
throw new ComparisonFailure( String.format( "Mismatch row %d, column %d", rowNbr, i ), rowMeta
.getString( data[ rowNbr ] ), rowMeta.getString( row ) );
}
} catch ( Exception e ) {
throw new AssertionError( String.format( "Value type at row %d, column %d", rowNbr, i ), e );
}
}
rowNbr++;
}
}
protected static interface Comparison<T> {
public boolean equals( T expected, T actual ) throws Exception;
}
}
protected static class JsonComparison implements RowComparatorListener.Comparison<Object> {
@Override
public boolean equals( Object expected, Object actual ) throws Exception {
return jsonEquals( (String) expected, (String) actual );
}
}
protected static class LocaleChange implements AutoCloseable {
private Locale original;
public LocaleChange( Locale newLocale ) {
original = LanguageChoice.getInstance().getDefaultLocale();
LanguageChoice.getInstance().setDefaultLocale( newLocale );
}
@Override
public void close() throws Exception {
LanguageChoice.getInstance().setDefaultLocale( original );
}
}
/**
* compare json (deep equals ignoring order)
*/
protected static final boolean jsonEquals( String json1, String json2 ) throws Exception {
ObjectMapper om = new ObjectMapper();
JsonNode parsedJson1 = om.readTree( json1 );
JsonNode parsedJson2 = om.readTree( json2 );
return parsedJson1.equals( parsedJson2 );
}
protected static RowMetaInterface createRowMeta( ValueMetaInterface... valueMetas ) {
RowMeta rowMeta = new RowMeta();
rowMeta.setValueMetaList( Arrays.asList( valueMetas ) );
return rowMeta;
}
@Test
public void testJsonInputMetaInputFieldsNotOverwritten() throws Exception {
JsonInputField inputField = new JsonInputField();
final String PATH = "$..book[?(@.category=='${category}')].price";
inputField.setPath( PATH );
inputField.setType( ValueMetaInterface.TYPE_STRING );
final JsonInputMeta inputMeta = createSimpleMeta( "json", inputField );
VariableSpace variables = new Variables();
variables.setVariable( "category", "fiction" );
JsonInput jsonInput = createJsonInput( "json", inputMeta, variables, new Object[] { getBasicTestJson() } );
processRows( jsonInput, 2 );
assertEquals( "Meta input fields paths should be the same after processRows", PATH, inputMeta.getInputFields()[0].getPath() );
}
}