/*! ******************************************************************************
*
* Pentaho Data Integration
*
* Copyright (C) 2002-2016 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.jsonoutput;
import java.io.File;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import junit.framework.TestCase;
import org.apache.commons.io.FileUtils;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.json.simple.JSONObject;
import org.junit.Assert;
import org.pentaho.di.TestUtilities;
import org.pentaho.di.core.util.Utils;
import org.pentaho.di.core.KettleEnvironment;
import org.pentaho.di.core.RowMetaAndData;
import org.pentaho.di.core.logging.LoggingObjectInterface;
import org.pentaho.di.core.plugins.PluginRegistry;
import org.pentaho.di.core.plugins.StepPluginType;
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.ValueMetaString;
import org.pentaho.di.trans.RowStepCollector;
import org.pentaho.di.trans.Trans;
import org.pentaho.di.trans.TransHopMeta;
import org.pentaho.di.trans.TransMeta;
import org.pentaho.di.trans.step.StepInterface;
import org.pentaho.di.trans.step.StepMeta;
import org.pentaho.di.trans.steps.dummytrans.DummyTransMeta;
import org.pentaho.di.trans.steps.mock.StepMockHelper;
import org.pentaho.di.trans.steps.rowgenerator.RowGeneratorMeta;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
/**
* This class was a "copy and modification" of Kettle's JsonOutputTests.
*
* @author Hendy Irawan <hendy@soluvas.com> Modified by Sean Flatley, removing dependency on external text file to hold
* expected results and modifying code to handle "Compatibility Mode".
*/
public class JsonOutputTest extends TestCase {
private static final String EXPECTED_NON_COMPATIBILITY_JSON =
"{\"data\":[{\"id\":1,\"state\":\"Florida\",\"city\":\"Orlando\"},"
+ "{\"id\":1,\"state\":\"Florida\",\"city\":\"Orlando\"},"
+ "{\"id\":1,\"state\":\"Florida\",\"city\":\"Orlando\"},"
+ "{\"id\":1,\"state\":\"Florida\",\"city\":\"Orlando\"},"
+ "{\"id\":1,\"state\":\"Florida\",\"city\":\"Orlando\"},"
+ "{\"id\":1,\"state\":\"Florida\",\"city\":\"Orlando\"},"
+ "{\"id\":1,\"state\":\"Florida\",\"city\":\"Orlando\"},"
+ "{\"id\":1,\"state\":\"Florida\",\"city\":\"Orlando\"},"
+ "{\"id\":1,\"state\":\"Florida\",\"city\":\"Orlando\"},"
+ "{\"id\":1,\"state\":\"Florida\",\"city\":\"Orlando\"}]}";
private static final String EXPECTED_COMPATIBILITY_MODE_JSON =
"{\"data\":[{\"id\":1},{\"state\":\"Florida\"},{\"city\":\"Orlando\"},{\"id\":1},{\"state\":\"Florida\"},"
+ "{\"city\":\"Orlando\"},{\"id\":1},{\"state\":\"Florida\"},{\"city\":\"Orlando\"},{\"id\":1},"
+ "{\"state\":\"Florida\"},{\"city\":\"Orlando\"},{\"id\":1},{\"state\":\"Florida\"},"
+ "{\"city\":\"Orlando\"},{\"id\":1},{\"state\":\"Florida\"},{\"city\":\"Orlando\"},{\"id\":1},"
+ "{\"state\":\"Florida\"},{\"city\":\"Orlando\"},{\"id\":1},{\"state\":\"Florida\"},"
+ "{\"city\":\"Orlando\"},{\"id\":1},{\"state\":\"Florida\"},{\"city\":\"Orlando\"},{\"id\":1},"
+ "{\"state\":\"Florida\"},{\"city\":\"Orlando\"}]}";
/**
* Creates a row generator step for this class..
*
* @param name
* @param registry
* @return
*/
private StepMeta createRowGeneratorStep( String name, PluginRegistry registry ) {
// Default the name if it is empty
String testFileOutputName = ( Utils.isEmpty( name ) ? "generate rows" : name );
// create the RowGenerator and Step Meta
RowGeneratorMeta rowGeneratorMeta = new RowGeneratorMeta();
String rowGeneratorPid = registry.getPluginId( StepPluginType.class, rowGeneratorMeta );
StepMeta generateRowsStep = new StepMeta( rowGeneratorPid, testFileOutputName, rowGeneratorMeta );
// Set the field names, types and values
rowGeneratorMeta.setFieldName( new String[] { "Id", "State", "City" } );
rowGeneratorMeta.setFieldType( new String[] { "Integer", "String", "String" } );
rowGeneratorMeta.setValue( new String[] { "1", "Florida", "Orlando" } );
rowGeneratorMeta.setFieldLength( new int[] { -1, -1, -1 } );
rowGeneratorMeta.setFieldPrecision( new int[] { -1, -1, -1 } );
rowGeneratorMeta.setGroup( new String[] { "", "", "" } );
rowGeneratorMeta.setDecimal( new String[] { "", "", "" } );
rowGeneratorMeta.setCurrency( new String[] { "", "", "" } );
rowGeneratorMeta.setFieldFormat( new String[] { "", "", "" } );
rowGeneratorMeta.setRowLimit( "10" );
// return the step meta
return generateRowsStep;
}
/**
* Create a dummy step for this class.
*
* @param name
* @param registry
* @return
*/
private StepMeta createDummyStep( String name, PluginRegistry registry ) {
// Create a dummy step 1 and add it to the tranMeta
String dummyStepName = "dummy step";
DummyTransMeta dm1 = new DummyTransMeta();
String dummyPid1 = registry.getPluginId( StepPluginType.class, dm1 );
StepMeta dummyStep = new StepMeta( dummyPid1, dummyStepName, dm1 );
return dummyStep;
}
/**
* Create result data for test case 1. Each Object array in element in list should mirror the data written by the row
* generator created by the createRowGenerator method.
*
* @return list of metadata/data couples of how the result should look like.
*/
public List<RowMetaAndData> createResultData1() {
List<RowMetaAndData> list = new ArrayList<RowMetaAndData>();
RowMetaInterface rowMetaInterface = createResultRowMetaInterface();
Object[] r1 = new Object[] { new Long( 1L ), "Orlando", "Florida" };
Object[] r2 = new Object[] { new Long( 1L ), "Orlando", "Florida" };
Object[] r3 = new Object[] { new Long( 1L ), "Orlando", "Florida" };
Object[] r4 = new Object[] { new Long( 1L ), "Orlando", "Florida" };
Object[] r5 = new Object[] { new Long( 1L ), "Orlando", "Florida" };
Object[] r6 = new Object[] { new Long( 1L ), "Orlando", "Florida" };
Object[] r7 = new Object[] { new Long( 1L ), "Orlando", "Florida" };
Object[] r8 = new Object[] { new Long( 1L ), "Orlando", "Florida" };
Object[] r9 = new Object[] { new Long( 1L ), "Orlando", "Florida" };
Object[] r10 = new Object[] { new Long( 1L ), "Orlando", "Florida" };
list.add( new RowMetaAndData( rowMetaInterface, r1 ) );
list.add( new RowMetaAndData( rowMetaInterface, r2 ) );
list.add( new RowMetaAndData( rowMetaInterface, r3 ) );
list.add( new RowMetaAndData( rowMetaInterface, r4 ) );
list.add( new RowMetaAndData( rowMetaInterface, r5 ) );
list.add( new RowMetaAndData( rowMetaInterface, r6 ) );
list.add( new RowMetaAndData( rowMetaInterface, r7 ) );
list.add( new RowMetaAndData( rowMetaInterface, r8 ) );
list.add( new RowMetaAndData( rowMetaInterface, r9 ) );
list.add( new RowMetaAndData( rowMetaInterface, r10 ) );
return list;
}
/**
* Creates a RowMetaInterface with a ValueMetaInterface with the name "filename".
*
* @return
*/
public RowMetaInterface createRowMetaInterface() {
RowMetaInterface rowMetaInterface = new RowMeta();
ValueMetaInterface[] valuesMeta = { new ValueMetaString( "filename" ), };
for ( int i = 0; i < valuesMeta.length; i++ ) {
rowMetaInterface.addValueMeta( valuesMeta[i] );
}
return rowMetaInterface;
}
/**
* Creates data... Will add more as I figure what the data is.
*
* @param fileName
* @return
*/
public List<RowMetaAndData> createData() {
List<RowMetaAndData> list = new ArrayList<RowMetaAndData>();
RowMetaInterface rowMetaInterface = createRowMetaInterface();
Object[] r1 = new Object[] {};
list.add( new RowMetaAndData( rowMetaInterface, r1 ) );
return list;
}
/**
* Creates a row meta interface for the fields that are defined by performing a getFields and by checking "Result
* filenames - Add filenames to result from "Text File Input" dialog.
*
* @return
*/
public RowMetaInterface createResultRowMetaInterface() {
RowMetaInterface rowMetaInterface = new RowMeta();
ValueMetaInterface[] valuesMeta =
{ new ValueMetaInteger( "Id" ), new ValueMetaString( "State" ), new ValueMetaString( "City" ) };
for ( int i = 0; i < valuesMeta.length; i++ ) {
rowMetaInterface.addValueMeta( valuesMeta[i] );
}
return rowMetaInterface;
}
private StepMeta createJsonOutputStep( String name, String jsonFileName, PluginRegistry registry ) {
// Create a Text File Output step
String testFileOutputName = name;
JsonOutputMeta jsonOutputMeta = new JsonOutputMeta();
String textFileInputPid = registry.getPluginId( StepPluginType.class, jsonOutputMeta );
StepMeta jsonOutputStep = new StepMeta( textFileInputPid, testFileOutputName, jsonOutputMeta );
// initialize the fields
JsonOutputField[] fields = new JsonOutputField[3];
for ( int idx = 0; idx < fields.length; idx++ ) {
fields[idx] = new JsonOutputField();
}
// populate the fields
// it is important that the setPosition(int)
// is invoked with the correct position as
// we are testing the reading of a delimited file.
fields[0].setFieldName( "id" );
fields[0].setElementName( "id" );
fields[1].setFieldName( "state" );
fields[1].setElementName( "state" );
fields[2].setFieldName( "city" );
fields[2].setElementName( "city" );
// call this to allocate the number of fields
jsonOutputMeta.allocate( fields.length );
jsonOutputMeta.setOutputFields( fields );
// set meta properties- these were determined by running Spoon
// and setting up the transformation we are setting up here.
// i.e. - the dialog told me what I had to set to avoid
// NPEs during the transformation.
// We need a file name so we will generate a temp file
jsonOutputMeta.setOperationType( JsonOutputMeta.OPERATION_TYPE_WRITE_TO_FILE );
jsonOutputMeta.setOutputValue( "data" );
jsonOutputMeta.setFileName( jsonFileName );
jsonOutputMeta.setExtension( "js" );
jsonOutputMeta.setNrRowsInBloc( "0" ); // a single "data" contains an array of all records
jsonOutputMeta.setJsonBloc( "data" );
return jsonOutputStep;
}
public String test( boolean compatibilityMode ) throws Exception {
KettleEnvironment.init();
// Create a new transformation...
//
TransMeta transMeta = new TransMeta();
transMeta.setName( "testJsonOutput" );
PluginRegistry registry = PluginRegistry.getInstance();
// create an injector step
String injectorStepName = "injector step";
StepMeta injectorStep = TestUtilities.createInjectorStep( injectorStepName, registry );
transMeta.addStep( injectorStep );
// create a row generator step
StepMeta rowGeneratorStep = createRowGeneratorStep( "Create rows for testJsonOutput1", registry );
transMeta.addStep( rowGeneratorStep );
// create a TransHopMeta for injector and add it to the transMeta
TransHopMeta hop_injectory_rowGenerator = new TransHopMeta( injectorStep, rowGeneratorStep );
transMeta.addTransHop( hop_injectory_rowGenerator );
// create the json output step
// but first lets get a filename
String jsonFileName = TestUtilities.createEmptyTempFile( "testJsonOutput1_" );
StepMeta jsonOutputStep = createJsonOutputStep( "json output step", jsonFileName, registry );
( (JsonOutputMeta) jsonOutputStep.getStepMetaInterface() ).setCompatibilityMode( compatibilityMode );
transMeta.addStep( jsonOutputStep );
// create a TransHopMeta for jsonOutputStep and add it to the transMeta
TransHopMeta hop_RowGenerator_outputTextFile = new TransHopMeta( rowGeneratorStep, jsonOutputStep );
transMeta.addTransHop( hop_RowGenerator_outputTextFile );
// Create a dummy step and add it to the tranMeta
String dummyStepName = "dummy step";
StepMeta dummyStep = createDummyStep( dummyStepName, registry );
transMeta.addStep( dummyStep );
// create a TransHopMeta for the
TransHopMeta hop_outputJson_dummyStep = new TransHopMeta( jsonOutputStep, dummyStep );
transMeta.addTransHop( hop_outputJson_dummyStep );
// Now execute the transformation...
Trans trans = new Trans( transMeta );
trans.prepareExecution( null );
// Create a row collector and add it to the dummy step interface
StepInterface dummyStepInterface = trans.getStepInterface( dummyStepName, 0 );
RowStepCollector dummyRowCollector = new RowStepCollector();
dummyStepInterface.addRowListener( dummyRowCollector );
// RowProducer rowProducer = trans.addRowProducer(injectorStepName, 0);
trans.startThreads();
trans.waitUntilFinished();
// get the results and return it
File outputFile = new File( jsonFileName + ".js" );
String jsonStructure = FileUtils.readFileToString( outputFile );
return jsonStructure;
}
// The actual tests
public void testNonCompatibilityMode() throws Exception {
String jsonStructure = test( false );
Assert.assertTrue( jsonEquals( EXPECTED_NON_COMPATIBILITY_JSON, jsonStructure ) );
}
public void testCompatibilityMode() throws Exception {
String jsonStructure = test( true );
Assert.assertEquals( EXPECTED_COMPATIBILITY_MODE_JSON, jsonStructure );
}
/* PDI-7243 */
public void testNpeIsNotThrownOnNullInput() throws Exception {
StepMockHelper<JsonOutputMeta, JsonOutputData> mockHelper =
new StepMockHelper<JsonOutputMeta, JsonOutputData>( "jsonOutput", JsonOutputMeta.class, JsonOutputData.class );
when( mockHelper.logChannelInterfaceFactory.create( any(), any( LoggingObjectInterface.class ) ) ).thenReturn(
mockHelper.logChannelInterface );
when( mockHelper.trans.isRunning() ).thenReturn( true );
when( mockHelper.stepMeta.getStepMetaInterface() ).thenReturn( new JsonOutputMeta() );
JsonOutput step =
new JsonOutput( mockHelper.stepMeta, mockHelper.stepDataInterface, 0, mockHelper.transMeta, mockHelper.trans );
step = spy( step );
doReturn( null ).when( step ).getRow();
step.processRow( mockHelper.processRowsStepMetaInterface, mockHelper.processRowsStepDataInterface );
}
public void testEmptyDoesntWriteToFile() throws Exception {
StepMockHelper<JsonOutputMeta, JsonOutputData> mockHelper =
new StepMockHelper<JsonOutputMeta, JsonOutputData>( "jsonOutput", JsonOutputMeta.class, JsonOutputData.class );
when( mockHelper.logChannelInterfaceFactory.create( any(), any( LoggingObjectInterface.class ) ) ).thenReturn(
mockHelper.logChannelInterface );
when( mockHelper.trans.isRunning() ).thenReturn( true );
when( mockHelper.stepMeta.getStepMetaInterface() ).thenReturn( new JsonOutputMeta() );
JsonOutputData stepData = new JsonOutputData();
stepData.writeToFile = true;
JsonOutput step =
new JsonOutput( mockHelper.stepMeta, stepData, 0, mockHelper.transMeta, mockHelper.trans );
step = spy( step );
doReturn( null ).when( step ).getRow();
doReturn( true ).when( step ).openNewFile();
doReturn( true ).when( step ).closeFile();
step.processRow( mockHelper.processRowsStepMetaInterface, stepData );
verify( step, times( 0 ) ).openNewFile();
verify( step, times( 0 ) ).closeFile();
}
@SuppressWarnings( "unchecked" )
public void testWriteToFile() throws Exception {
StepMockHelper<JsonOutputMeta, JsonOutputData> mockHelper =
new StepMockHelper<JsonOutputMeta, JsonOutputData>( "jsonOutput", JsonOutputMeta.class, JsonOutputData.class );
when( mockHelper.logChannelInterfaceFactory.create( any(), any( LoggingObjectInterface.class ) ) ).thenReturn(
mockHelper.logChannelInterface );
when( mockHelper.trans.isRunning() ).thenReturn( true );
when( mockHelper.stepMeta.getStepMetaInterface() ).thenReturn( new JsonOutputMeta() );
JsonOutputData stepData = new JsonOutputData();
stepData.writeToFile = true;
JSONObject jsonObject = new JSONObject();
jsonObject.put( "key", "value" );
stepData.ja.add( jsonObject );
stepData.writer = mock( Writer.class );
JsonOutput step =
new JsonOutput( mockHelper.stepMeta, stepData, 0, mockHelper.transMeta, mockHelper.trans );
step = spy( step );
doReturn( null ).when( step ).getRow();
doReturn( true ).when( step ).openNewFile();
doReturn( true ).when( step ).closeFile();
doNothing().when( stepData.writer ).write( anyString() );
step.processRow( mockHelper.processRowsStepMetaInterface, stepData );
verify( step ).openNewFile();
verify( step ).closeFile();
}
/**
* compare json (deep equals ignoring order)
*/
protected 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 );
}
}