/*! ****************************************************************************** * * 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.filterrows; import org.pentaho.di.core.CheckResult; import org.pentaho.di.core.CheckResultInterface; import org.pentaho.di.core.Condition; import org.pentaho.di.core.Const; import org.pentaho.di.core.database.DatabaseMeta; import org.pentaho.di.core.exception.KettleException; import org.pentaho.di.core.exception.KettleStepException; import org.pentaho.di.core.exception.KettleValueException; import org.pentaho.di.core.exception.KettleXMLException; import org.pentaho.di.core.injection.Injection; import org.pentaho.di.core.injection.InjectionSupported; import org.pentaho.di.core.row.RowMetaInterface; import org.pentaho.di.core.row.ValueMetaAndData; import org.pentaho.di.core.row.ValueMetaInterface; import org.pentaho.di.core.util.Utils; import org.pentaho.di.core.variables.VariableSpace; import org.pentaho.di.core.xml.XMLHandler; import org.pentaho.di.i18n.BaseMessages; import org.pentaho.di.repository.ObjectId; import org.pentaho.di.repository.Repository; import org.pentaho.di.trans.Trans; import org.pentaho.di.trans.TransMeta; import org.pentaho.di.trans.step.BaseStepMeta; import org.pentaho.di.trans.step.StepDataInterface; import org.pentaho.di.trans.step.StepIOMeta; import org.pentaho.di.trans.step.StepIOMetaInterface; import org.pentaho.di.trans.step.StepInterface; import org.pentaho.di.trans.step.StepMeta; import org.pentaho.di.trans.step.StepMetaInterface; import org.pentaho.di.trans.step.errorhandling.Stream; import org.pentaho.di.trans.step.errorhandling.StreamIcon; import org.pentaho.di.trans.step.errorhandling.StreamInterface; import org.pentaho.di.trans.step.errorhandling.StreamInterface.StreamType; import org.pentaho.metastore.api.IMetaStore; import org.w3c.dom.Node; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; /* * Created on 02-jun-2003 * */ @InjectionSupported( localizationPrefix = "FilterRowsMeta.Injection." ) public class FilterRowsMeta extends BaseStepMeta implements StepMetaInterface { private static Class<?> PKG = FilterRowsMeta.class; // for i18n purposes, needed by Translator2!! /** * This is the main condition for the complete filter. * * @since version 2.1 */ private Condition condition; private String conditionXML; public FilterRowsMeta() { super(); // allocate BaseStepMeta condition = new Condition(); } public void loadXML( Node stepnode, List<DatabaseMeta> databases, IMetaStore metaStore ) throws KettleXMLException { readData( stepnode ); } /** * @return Returns the condition. */ public Condition getCondition() { return condition; } /** * @param condition * The condition to set. */ public void setCondition( Condition condition ) { this.condition = condition; } public void allocate() { condition = new Condition(); } public Object clone() { FilterRowsMeta retval = (FilterRowsMeta) super.clone(); retval.setTrueStepname( getTrueStepname() ); retval.setFalseStepname( getFalseStepname() ); if ( condition != null ) { retval.condition = (Condition) condition.clone(); } else { retval.condition = null; } return retval; } public String getXML() throws KettleException { StringBuilder retval = new StringBuilder( 200 ); retval.append( XMLHandler.addTagValue( "send_true_to", getTrueStepname() ) ); retval.append( XMLHandler.addTagValue( "send_false_to", getFalseStepname() ) ); retval.append( " <compare>" ).append( Const.CR ); if ( condition != null ) { retval.append( condition.getXML() ); } retval.append( " </compare>" ).append( Const.CR ); return retval.toString(); } private void readData( Node stepnode ) throws KettleXMLException { try { setTrueStepname( XMLHandler.getTagValue( stepnode, "send_true_to" ) ); setFalseStepname( XMLHandler.getTagValue( stepnode, "send_false_to" ) ); Node compare = XMLHandler.getSubNode( stepnode, "compare" ); Node condnode = XMLHandler.getSubNode( compare, "condition" ); // The new situation... if ( condnode != null ) { condition = new Condition( condnode ); } else { // Old style condition: Line1 OR Line2 OR Line3: @deprecated! condition = new Condition(); int nrkeys = XMLHandler.countNodes( compare, "key" ); if ( nrkeys == 1 ) { Node knode = XMLHandler.getSubNodeByNr( compare, "key", 0 ); String key = XMLHandler.getTagValue( knode, "name" ); String value = XMLHandler.getTagValue( knode, "value" ); String field = XMLHandler.getTagValue( knode, "field" ); String comparator = XMLHandler.getTagValue( knode, "condition" ); condition.setOperator( Condition.OPERATOR_NONE ); condition.setLeftValuename( key ); condition.setFunction( Condition.getFunction( comparator ) ); condition.setRightValuename( field ); condition.setRightExact( new ValueMetaAndData( "value", value ) ); } else { for ( int i = 0; i < nrkeys; i++ ) { Node knode = XMLHandler.getSubNodeByNr( compare, "key", i ); String key = XMLHandler.getTagValue( knode, "name" ); String value = XMLHandler.getTagValue( knode, "value" ); String field = XMLHandler.getTagValue( knode, "field" ); String comparator = XMLHandler.getTagValue( knode, "condition" ); Condition subc = new Condition(); if ( i > 0 ) { subc.setOperator( Condition.OPERATOR_OR ); } else { subc.setOperator( Condition.OPERATOR_NONE ); } subc.setLeftValuename( key ); subc.setFunction( Condition.getFunction( comparator ) ); subc.setRightValuename( field ); subc.setRightExact( new ValueMetaAndData( "value", value ) ); condition.addCondition( subc ); } } } } catch ( Exception e ) { throw new KettleXMLException( BaseMessages.getString( PKG, "FilterRowsMeta.Exception..UnableToLoadStepInfoFromXML" ), e ); } } public void setDefault() { allocate(); } public void readRep( Repository rep, IMetaStore metaStore, ObjectId id_step, List<DatabaseMeta> databases ) throws KettleException { try { allocate(); setTrueStepname( rep.getStepAttributeString( id_step, "send_true_to" ) ); setFalseStepname( rep.getStepAttributeString( id_step, "send_false_to" ) ); condition = rep.loadConditionFromStepAttribute( id_step, "id_condition" ); } catch ( Exception e ) { throw new KettleException( BaseMessages.getString( PKG, "FilterRowsMeta.Exception.UnexpectedErrorInReadingStepInfoFromRepository" ), e ); } } @Override public void searchInfoAndTargetSteps( List<StepMeta> steps ) { for ( StreamInterface stream : getStepIOMeta().getTargetStreams() ) { stream.setStepMeta( StepMeta.findStep( steps, (String) stream.getSubject() ) ); } } public void saveRep( Repository rep, IMetaStore metaStore, ObjectId id_transformation, ObjectId id_step ) throws KettleException { try { if ( condition != null ) { rep.saveConditionStepAttribute( id_transformation, id_step, "id_condition", condition ); rep.saveStepAttribute( id_transformation, id_step, "send_true_to", getTrueStepname() ); rep.saveStepAttribute( id_transformation, id_step, "send_false_to", getFalseStepname() ); } } catch ( Exception e ) { throw new KettleException( BaseMessages.getString( PKG, "FilterRowsMeta.Exception.UnableToSaveStepInfoToRepository" ) + id_step, e ); } } public void getFields( RowMetaInterface rowMeta, String origin, RowMetaInterface[] info, StepMeta nextStep, VariableSpace space, Repository repository, IMetaStore metaStore ) throws KettleStepException { // Clear the sortedDescending flag on fields used within the condition - otherwise the comparisons will be // inverted!! String[] conditionField = condition.getUsedFields(); for ( int i = 0; i < conditionField.length; i++ ) { int idx = rowMeta.indexOfValue( conditionField[i] ); if ( idx >= 0 ) { ValueMetaInterface valueMeta = rowMeta.getValueMeta( idx ); valueMeta.setSortedDescending( false ); } } } public void check( List<CheckResultInterface> remarks, TransMeta transMeta, StepMeta stepMeta, RowMetaInterface prev, String[] input, String[] output, RowMetaInterface info, VariableSpace space, Repository repository, IMetaStore metaStore ) { CheckResult cr; String error_message = ""; checkTarget( stepMeta, "true", getTrueStepname(), output ).ifPresent( remarks::add ); checkTarget( stepMeta, "false", getFalseStepname(), output ).ifPresent( remarks::add ); if ( condition.isEmpty() ) { cr = new CheckResult( CheckResultInterface.TYPE_RESULT_ERROR, BaseMessages.getString( PKG, "FilterRowsMeta.CheckResult.NoConditionSpecified" ), stepMeta ); } else { cr = new CheckResult( CheckResultInterface.TYPE_RESULT_OK, BaseMessages.getString( PKG, "FilterRowsMeta.CheckResult.ConditionSpecified" ), stepMeta ); } remarks.add( cr ); // Look up fields in the input stream <prev> if ( prev != null && prev.size() > 0 ) { cr = new CheckResult( CheckResultInterface.TYPE_RESULT_OK, BaseMessages.getString( PKG, "FilterRowsMeta.CheckResult.StepReceivingFields", prev.size() + "" ), stepMeta ); remarks.add( cr ); List<String> orphanFields = getOrphanFields( condition, prev ); if ( orphanFields.size() > 0 ) { error_message = BaseMessages.getString( PKG, "FilterRowsMeta.CheckResult.FieldsNotFoundFromPreviousStep" ) + Const.CR; for ( String field : orphanFields ) { error_message += "\t\t" + field + Const.CR; } cr = new CheckResult( CheckResultInterface.TYPE_RESULT_ERROR, error_message, stepMeta ); } else { cr = new CheckResult( CheckResultInterface.TYPE_RESULT_OK, BaseMessages.getString( PKG, "FilterRowsMeta.CheckResult.AllFieldsFoundInInputStream" ), stepMeta ); } remarks.add( cr ); } else { error_message = BaseMessages.getString( PKG, "FilterRowsMeta.CheckResult.CouldNotReadFieldsFromPreviousStep" ) + Const.CR; cr = new CheckResult( CheckResultInterface.TYPE_RESULT_ERROR, error_message, stepMeta ); remarks.add( cr ); } // See if we have input streams leading to this step! if ( input.length > 0 ) { cr = new CheckResult( CheckResultInterface.TYPE_RESULT_OK, BaseMessages.getString( PKG, "FilterRowsMeta.CheckResult.StepReceivingInfoFromOtherSteps" ), stepMeta ); remarks.add( cr ); } else { cr = new CheckResult( CheckResultInterface.TYPE_RESULT_ERROR, BaseMessages.getString( PKG, "FilterRowsMeta.CheckResult.NoInputReceivedFromOtherSteps" ), stepMeta ); remarks.add( cr ); } } private Optional<CheckResult> checkTarget( StepMeta stepMeta, String target, String targetStepName, String[] output ) { if ( targetStepName != null ) { int trueTargetIdx = Const.indexOfString( targetStepName, output ); if ( trueTargetIdx < 0 ) { return Optional.of( new CheckResult( CheckResultInterface.TYPE_RESULT_ERROR, BaseMessages.getString( PKG, "FilterRowsMeta.CheckResult.TargetStepInvalid", target, targetStepName ), stepMeta ) ); } } return Optional.empty(); } public StepInterface getStep( StepMeta stepMeta, StepDataInterface stepDataInterface, int cnr, TransMeta tr, Trans trans ) { return new FilterRows( stepMeta, stepDataInterface, cnr, tr, trans ); } public StepDataInterface getStepData() { return new FilterRowsData(); } /** * Returns the Input/Output metadata for this step. */ public StepIOMetaInterface getStepIOMeta() { if ( ioMeta == null ) { ioMeta = new StepIOMeta( true, true, false, false, false, false ); ioMeta.addStream( new Stream( StreamType.TARGET, null, BaseMessages.getString( PKG, "FilterRowsMeta.InfoStream.True.Description" ), StreamIcon.TRUE, null ) ); ioMeta.addStream( new Stream( StreamType.TARGET, null, BaseMessages.getString( PKG, "FilterRowsMeta.InfoStream.False.Description" ), StreamIcon.FALSE, null ) ); } return ioMeta; } @Override public void resetStepIoMeta() { } /** * When an optional stream is selected, this method is called to handled the ETL metadata implications of that. * * @param stream * The optional stream to handle. */ public void handleStreamSelection( StreamInterface stream ) { // This step targets another step. // Make sure that we don't specify the same step for true and false... // If the user requests false, we blank out true and vice versa // List<StreamInterface> targets = getStepIOMeta().getTargetStreams(); int index = targets.indexOf( stream ); if ( index == 0 ) { // True // StepMeta falseStep = targets.get( 1 ).getStepMeta(); if ( falseStep != null && falseStep.equals( stream.getStepMeta() ) ) { targets.get( 1 ).setStepMeta( null ); } } if ( index == 1 ) { // False // StepMeta trueStep = targets.get( 0 ).getStepMeta(); if ( trueStep != null && trueStep.equals( stream.getStepMeta() ) ) { targets.get( 0 ).setStepMeta( null ); } } } @Override public boolean excludeFromCopyDistributeVerification() { return true; } /** * Get non-existing referenced input fields * @param condition * @param prev * @return */ public List<String> getOrphanFields( Condition condition, RowMetaInterface prev ) { List<String> orphans = new ArrayList<String>( ); if ( condition == null || prev == null ) { return orphans; } String[] key = condition.getUsedFields(); for ( int i = 0; i < key.length; i++ ) { if ( Utils.isEmpty( key[i] ) ) { continue; } ValueMetaInterface v = prev.searchValueMeta( key[i] ); if ( v == null ) { orphans.add( key[i] ); } } return orphans; } public String getTrueStepname() { return getTargetStepName( 0 ); } @Injection( name = "SEND_TRUE_STEP" ) public void setTrueStepname( String trueStepname ) { getStepIOMeta().getTargetStreams().get( 0 ).setSubject( trueStepname ); } public String getFalseStepname() { return getTargetStepName( 1 ); } @Injection( name = "SEND_FALSE_STEP" ) public void setFalseStepname( String falseStepname ) { getStepIOMeta().getTargetStreams().get( 1 ).setSubject( falseStepname ); } private String getTargetStepName( int streamIndex ) { StreamInterface stream = getStepIOMeta().getTargetStreams().get( streamIndex ); return java.util.stream.Stream.of( stream.getStepname(), stream.getSubject() ) .filter( Objects::nonNull ) .findFirst().map( Object::toString ).orElse( null ); } public String getConditionXML() { try { conditionXML = condition.getXML(); } catch ( KettleValueException e ) { log.logError( e.getMessage() ); } return conditionXML; } @Injection( name = "CONDITION" ) public void setConditionXML( String conditionXML ) { try { this.condition = new Condition( conditionXML ); this.conditionXML = conditionXML; } catch ( KettleXMLException e ) { log.logError( e.getMessage() ); } } }