/*! ****************************************************************************** * * 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.core.row; import com.google.common.annotations.VisibleForTesting; import org.pentaho.di.compatibility.Row; import org.pentaho.di.compatibility.Value; import org.pentaho.di.core.Const; import org.pentaho.di.core.util.Utils; import org.pentaho.di.core.exception.KettleEOFException; import org.pentaho.di.core.exception.KettleException; import org.pentaho.di.core.exception.KettleFileException; import org.pentaho.di.core.exception.KettlePluginException; import org.pentaho.di.core.exception.KettleValueException; import org.pentaho.di.core.row.value.ValueMetaFactory; import org.pentaho.di.core.xml.XMLHandler; import org.w3c.dom.Node; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.IOException; import java.math.BigDecimal; import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantReadWriteLock; public class RowMeta implements RowMetaInterface { public static final String XML_META_TAG = "row-meta"; public static final String XML_DATA_TAG = "row-data"; private final ReentrantReadWriteLock lock; private final RowMetaCache cache; private List<ValueMetaInterface> valueMetaList; public RowMeta() { this( new ArrayList<ValueMetaInterface>(), new RowMetaCache() ); } /** * Copy constructor for clone * * @param rowMeta * @throws KettlePluginException */ private RowMeta( RowMeta rowMeta, Integer targetType ) throws KettlePluginException { this( new ArrayList<ValueMetaInterface>( rowMeta.valueMetaList.size() ), new RowMetaCache( rowMeta.cache ) ); for ( ValueMetaInterface valueMetaInterface : rowMeta.valueMetaList ) { valueMetaList.add( ValueMetaFactory .cloneValueMeta( valueMetaInterface, targetType == null ? valueMetaInterface.getType() : targetType ) ); } } private RowMeta( List<ValueMetaInterface> valueMetaList, RowMetaCache rowMetaCache ) { lock = new ReentrantReadWriteLock(); this.cache = rowMetaCache; this.valueMetaList = valueMetaList; } @Override public RowMeta clone() { lock.readLock().lock(); try { return new RowMeta( this, null ); } catch ( Exception e ) { throw new RuntimeException( e ); } finally { lock.readLock().unlock(); } } /** * This method copies the row metadata and sets all values to the specified type (usually String) * * @param targetType The target type * @return The cloned metadata * @throws if the target type could not be loaded from the plugin registry */ @Override public RowMetaInterface cloneToType( int targetType ) throws KettleValueException { lock.readLock().lock(); try { return new RowMeta( this, targetType ); } catch ( KettlePluginException e ) { throw new KettleValueException( e ); } finally { lock.readLock().unlock(); } } @Override public String toString() { StringBuilder buffer = new StringBuilder(); lock.readLock().lock(); try { boolean notFirst = false; for ( ValueMetaInterface valueMeta : valueMetaList ) { if ( notFirst ) { buffer.append( ", " ); } else { notFirst = true; } buffer.append( "[" ).append( valueMeta.toString() ).append( "]" ); } return buffer.toString(); } finally { lock.readLock().unlock(); } } /** * @return the list of value metadata */ @Override public List<ValueMetaInterface> getValueMetaList() { List<ValueMetaInterface> copy; lock.readLock().lock(); try { copy = new ArrayList<ValueMetaInterface>( valueMetaList ); } finally { lock.readLock().unlock(); } // kept for backward compatibility return Collections.unmodifiableList( copy ); } /** * @param valueMetaList the list of valueMeta to set */ @Override public void setValueMetaList( List<ValueMetaInterface> valueMetaList ) { lock.writeLock().lock(); try { this.valueMetaList = valueMetaList; this.cache.invalidate(); for ( int i = 0, len = valueMetaList.size(); i < len; i++ ) { ValueMetaInterface valueMeta = valueMetaList.get( i ); cache.storeMapping( valueMeta.getName(), i ); } } finally { lock.writeLock().unlock(); } } /** * @return the number of values in the row */ @Override public int size() { lock.readLock().lock(); try { return valueMetaList.size(); } finally { lock.readLock().unlock(); } } /** * @return true if there are no elements in the row metadata */ @Override public boolean isEmpty() { lock.readLock().lock(); try { return valueMetaList.isEmpty(); } finally { lock.readLock().unlock(); } } @Override public boolean exists( ValueMetaInterface meta ) { return ( meta != null ) && searchValueMeta( meta.getName() ) != null; } /** * Add a metadata value. If a value with the same name already exists, it gets renamed. * * @param meta The metadata value to add */ @Override public void addValueMeta( ValueMetaInterface meta ) { if ( meta != null ) { lock.writeLock().lock(); try { ValueMetaInterface newMeta; if ( !exists( meta ) ) { newMeta = meta; } else { newMeta = renameValueMetaIfInRow( meta, null ); } int index = valueMetaList.size(); valueMetaList.add( newMeta ); cache.storeMapping( newMeta.getName(), index ); } finally { lock.writeLock().unlock(); } } } /** * Add a metadata value on a certain location in the row. If a value with the same name already exists, it gets * renamed. Remember to change the data row according to this. * * @param index The index where the metadata value needs to be put in the row * @param meta The metadata value to add to the row */ @Override public void addValueMeta( int index, ValueMetaInterface meta ) { if ( meta != null ) { lock.writeLock().lock(); try { ValueMetaInterface newMeta; if ( !exists( meta ) ) { newMeta = meta; } else { newMeta = renameValueMetaIfInRow( meta, null ); } valueMetaList.add( index, newMeta ); cache.invalidate(); } finally { lock.writeLock().unlock(); } } } /** * Get the value metadata on the specified index. * * @param index The index to get the value metadata from * @return The value metadata specified by the index. */ @Override public ValueMetaInterface getValueMeta( int index ) { lock.readLock().lock(); try { if ( ( index >= 0 ) && ( index < valueMetaList.size() ) ) { return valueMetaList.get( index ); } else { return null; } } finally { lock.readLock().unlock(); } } /** * Replaces a value meta entry in the row metadata with another one * * @param index The index in the row to replace at * @param valueMeta the metadata to replace with */ @Override public void setValueMeta( int index, ValueMetaInterface valueMeta ) { if ( valueMeta != null ) { lock.writeLock().lock(); try { ValueMetaInterface old = valueMetaList.get( index ); ValueMetaInterface newMeta = valueMeta; // try to check if a ValueMeta with the same name already exists int existsIndex = indexOfValue( valueMeta.getName() ); // if it exists and it's not in the requested position // we need to take care of renaming if ( existsIndex >= 0 && existsIndex != index ) { newMeta = renameValueMetaIfInRow( valueMeta, null ); } valueMetaList.set( index, newMeta ); cache.replaceMapping( old.getName(), newMeta.getName(), index ); } finally { lock.writeLock().unlock(); } } } /** * Get a String value from a row of data. Convert data if this needed. * * @param dataRow the row of data * @param index the index * @return The string found on that position in the row * @throws KettleValueException in case there was a problem converting the data. */ @Override public String getString( Object[] dataRow, int index ) throws KettleValueException { if ( dataRow == null ) { return null; } ValueMetaInterface meta = getValueMeta( index ); return meta.getString( dataRow[ index ] ); } /** * Get an Integer value from a row of data. Convert data if this needed. * * @param dataRow the row of data * @param index the index * @return The integer found on that position in the row * @throws KettleValueException in case there was a problem converting the data. */ @Override public Long getInteger( Object[] dataRow, int index ) throws KettleValueException { if ( dataRow == null ) { return null; } ValueMetaInterface meta = getValueMeta( index ); return meta.getInteger( dataRow[ index ] ); } /** * Get a Number value from a row of data. Convert data if this needed. * * @param dataRow the row of data * @param index the index * @return The number found on that position in the row * @throws KettleValueException in case there was a problem converting the data. */ @Override public Double getNumber( Object[] dataRow, int index ) throws KettleValueException { if ( dataRow == null ) { return null; } ValueMetaInterface meta = getValueMeta( index ); return meta.getNumber( dataRow[ index ] ); } /** * Get a Date value from a row of data. Convert data if this needed. * * @param dataRow the row of data * @param index the index * @return The date found on that position in the row * @throws KettleValueException in case there was a problem converting the data. */ @Override public Date getDate( Object[] dataRow, int index ) throws KettleValueException { if ( dataRow == null ) { return null; } ValueMetaInterface meta = getValueMeta( index ); return meta.getDate( dataRow[ index ] ); } /** * Get a BigNumber value from a row of data. Convert data if this needed. * * @param dataRow the row of data * @param index the index * @return The bignumber found on that position in the row * @throws KettleValueException in case there was a problem converting the data. */ @Override public BigDecimal getBigNumber( Object[] dataRow, int index ) throws KettleValueException { if ( dataRow == null ) { return null; } ValueMetaInterface meta = getValueMeta( index ); return meta.getBigNumber( dataRow[ index ] ); } /** * Get a Boolean value from a row of data. Convert data if this needed. * * @param dataRow the row of data * @param index the index * @return The boolean found on that position in the row * @throws KettleValueException in case there was a problem converting the data. */ @Override public Boolean getBoolean( Object[] dataRow, int index ) throws KettleValueException { if ( dataRow == null ) { return null; } ValueMetaInterface meta = getValueMeta( index ); return meta.getBoolean( dataRow[ index ] ); } /** * Get a Binary value from a row of data. Convert data if this needed. * * @param dataRow the row of data * @param index the index * @return The binary found on that position in the row * @throws KettleValueException in case there was a problem converting the data. */ @Override public byte[] getBinary( Object[] dataRow, int index ) throws KettleValueException { if ( dataRow == null ) { return null; } ValueMetaInterface meta = getValueMeta( index ); return meta.getBinary( dataRow[ index ] ); } /** * Determines whether a value in a row is null. A value is null when the object is null or when it's an empty String * * @param dataRow The row of data * @param index the index to reference * @return true if the value on the index is null. * @throws KettleValueException in case there is a conversion error (only thrown in case of lazy conversion) */ @Override public boolean isNull( Object[] dataRow, int index ) throws KettleValueException { if ( dataRow == null ) { // I guess so... return true; } return getValueMeta( index ).isNull( dataRow[ index ] ); } /** * @return a cloned Object[] object. * @throws KettleValueException in case something is not quite right with the expected data */ @Override public Object[] cloneRow( Object[] objects ) throws KettleValueException { return cloneRow( objects, objects.clone() ); } /** * @return a cloned Object[] object. * @throws KettleValueException in case something is not quite right with the expected data */ @Override public Object[] cloneRow( Object[] objects, Object[] newObjects ) throws KettleValueException { lock.readLock().lock(); try { List<Integer> list = cache.getOrCreateValuesThatNeedRealClone( valueMetaList ); for ( Integer i : list ) { ValueMetaInterface valueMeta = valueMetaList.get( i ); newObjects[ i ] = valueMeta.cloneValueData( objects[ i ] ); } return newObjects; } finally { lock.readLock().unlock(); } } @Override public String getString( Object[] dataRow, String valueName, String defaultValue ) throws KettleValueException { int index = indexOfValue( valueName ); if ( index < 0 ) { return defaultValue; } return getString( dataRow, index ); } @Override public Long getInteger( Object[] dataRow, String valueName, Long defaultValue ) throws KettleValueException { int index = indexOfValue( valueName ); if ( index < 0 ) { return defaultValue; } return getInteger( dataRow, index ); } @Override public Date getDate( Object[] dataRow, String valueName, Date defaultValue ) throws KettleValueException { int index = indexOfValue( valueName ); if ( index < 0 ) { return defaultValue; } return getDate( dataRow, index ); } /** * Searches the index of a value meta with a given name * * @param valueName the name of the value metadata to look for * @return the index or -1 in case we didn't find the value */ @Override public int indexOfValue( String valueName ) { if ( valueName == null ) { return -1; } lock.readLock().lock(); try { Integer index = cache.findAndCompare( valueName, valueMetaList ); for ( int i = 0; ( index == null ) && ( i < valueMetaList.size() ); i++ ) { if ( valueName.equalsIgnoreCase( valueMetaList.get( i ).getName() ) ) { index = i; // it is possible, that several threads can call storing simultaneously // but it makes no harm as they will put the same value, // because valueMetaList is defended from modifications by read lock cache.storeMapping( valueName, index ); } } if ( index == null ) { return -1; } return index; } finally { lock.readLock().unlock(); } } /** * Searches for a value with a certain name in the value meta list * * @param valueName The value name to search for * @return The value metadata or null if nothing was found */ @Override public ValueMetaInterface searchValueMeta( String valueName ) { lock.readLock().lock(); try { Integer index = indexOfValue( valueName ); if ( index < 0 ) { return null; } return valueMetaList.get( index ); } finally { lock.readLock().unlock(); } } @Override public void addRowMeta( RowMetaInterface rowMeta ) { for ( int i = 0; i < rowMeta.size(); i++ ) { addValueMeta( rowMeta.getValueMeta( i ) ); } } /** * Merge the values of row r to this Row. The values that are not yet in the row are added unchanged. The values that * are in the row are renamed to name_2, name_3, etc. * * @param r The row to be merged with this row */ @Override public void mergeRowMeta( RowMetaInterface r ) { mergeRowMeta( r, null ); } /** * Merge the values of row r to this Row. The fields that are not yet in the row are added unchanged. The fields that * are in the row are renamed to name_2, name_3, etc. If the fields are renamed, the provided originStepName will be * assigned as the origin step for those fields. * * @param r The row to be merged with this row * @param originStepName The name to use as the origin step */ @Override public void mergeRowMeta( RowMetaInterface r, String originStepName ) { lock.writeLock().lock(); try { for ( int x = 0; x < r.size(); x++ ) { ValueMetaInterface field = r.getValueMeta( x ); if ( searchValueMeta( field.getName() ) == null ) { addValueMeta( field ); // Not in list yet: add } else { // We want to rename the field to Name[2], Name[3], ... // addValueMeta( renameValueMetaIfInRow( field, originStepName ) ); } } } finally { lock.writeLock().unlock(); } } private ValueMetaInterface renameValueMetaIfInRow( ValueMetaInterface valueMeta, String originStep ) { // We want to rename the field to Name[2], Name[3], ... // int index = 1; String name = valueMeta.getName() + "_" + index; while ( searchValueMeta( name ) != null ) { index++; name = valueMeta.getName() + "_" + index; } // Create a copy of the valueMeta object to make sure we don't rename any other value meta objects. // It's only being renamed because of the addition to THIS row metadata object, not another. // ValueMetaInterface copy = valueMeta.clone(); // OK, this is the new name and origin to pick // copy.setName( name ); if ( originStep != null ) { copy.setOrigin( originStep ); } return copy; } /** * Get an array of the names of all the Values in the Row. * * @return an array of Strings: the names of all the Values in the Row. */ @Override public String[] getFieldNames() { lock.readLock().lock(); try { String[] retval = new String[ size() ]; for ( int i = 0; i < size(); i++ ) { String valueName = getValueMeta( i ).getName(); retval[i] = valueName == null ? "" : valueName; } return retval; } finally { lock.readLock().unlock(); } } /** * Write ONLY the specified data to the outputStream * * @throws KettleFileException in case things go awry */ @Override public void writeData( DataOutputStream outputStream, Object[] data ) throws KettleFileException { lock.readLock().lock(); try { // Write all values in the row for ( int i = 0; i < size(); i++ ) { getValueMeta( i ).writeData( outputStream, data[ i ] ); } // If there are 0 values in the row, we write a marker flag to be able to detect an EOF on the other end (sockets // etc) // if ( size() == 0 ) { try { outputStream.writeBoolean( true ); } catch ( IOException e ) { throw new KettleFileException( "Error writing marker flag", e ); } } } finally { lock.readLock().unlock(); } } /** * Write ONLY the specified metadata to the outputStream * * @throws KettleFileException in case things go awry */ @Override public void writeMeta( DataOutputStream outputStream ) throws KettleFileException { lock.readLock().lock(); try { // First handle the number of fields in a row try { outputStream.writeInt( size() ); } catch ( IOException e ) { throw new KettleFileException( "Unable to write nr of metadata values", e ); } // Write all values in the row for ( int i = 0; i < size(); i++ ) { getValueMeta( i ).writeMeta( outputStream ); } } finally { lock.readLock().unlock(); } } public RowMeta( DataInputStream inputStream ) throws KettleFileException, SocketTimeoutException { this(); int nr; try { nr = inputStream.readInt(); } catch ( SocketTimeoutException e ) { throw e; } catch ( EOFException e ) { throw new KettleEOFException( "End of file while reading the number of metadata values in the row metadata", e ); } catch ( IOException e ) { throw new KettleFileException( "Unable to read nr of metadata values: " + e.toString(), e ); } for ( int i = 0; i < nr; i++ ) { try { int type = inputStream.readInt(); ValueMetaInterface valueMeta = ValueMetaFactory.createValueMeta( type ); valueMeta.readMetaData( inputStream ); addValueMeta( valueMeta ); } catch ( EOFException e ) { throw new KettleEOFException( e ); } catch ( Exception e ) { throw new KettleFileException( toString() + " : Unable to read row metadata from input stream", e ); } } } @Override public Object[] readData( DataInputStream inputStream ) throws KettleFileException, SocketTimeoutException { lock.readLock().lock(); try { Object[] data = new Object[ size() ]; for ( int i = 0; i < size(); i++ ) { data[ i ] = getValueMeta( i ).readData( inputStream ); } if ( size() == 0 ) { try { inputStream.readBoolean(); } catch ( EOFException e ) { throw new KettleEOFException( e ); } catch ( SocketTimeoutException e ) { throw e; } catch ( IOException e ) { throw new KettleFileException( toString() + " : Unable to read the marker flag data from input stream", e ); } } return data; } finally { lock.readLock().unlock(); } } @Override public void clear() { lock.writeLock().lock(); try { valueMetaList.clear(); cache.invalidate(); } finally { lock.writeLock().unlock(); } } @Override public void removeValueMeta( String valueName ) throws KettleValueException { lock.writeLock().lock(); try { int index = indexOfValue( valueName ); if ( index < 0 ) { throw new KettleValueException( "Unable to find value metadata with name '" + valueName + "', so I can't delete it." ); } removeValueMeta( index ); } finally { lock.writeLock().unlock(); } } @Override public void removeValueMeta( int index ) { lock.writeLock().lock(); try { valueMetaList.remove( index ); cache.invalidate(); } finally { lock.writeLock().unlock(); } } /** * @return a string with a description of all the metadata values of the complete row of metadata */ @Override public String toStringMeta() { StringBuilder buffer = new StringBuilder(); lock.readLock().lock(); try { boolean notFirst = false; for ( ValueMetaInterface valueMeta : valueMetaList ) { if ( notFirst ) { buffer.append( ", " ); } else { notFirst = true; } buffer.append( "[" ).append( valueMeta.toStringMeta() ).append( "]" ); } return buffer.toString(); } finally { lock.readLock().unlock(); } } /** * Get the string representation of the data in a row of data * * @param row the row of data to convert to string * @return the row of data in string form * @throws KettleValueException in case of a conversion error */ @Override public String getString( Object[] row ) throws KettleValueException { lock.readLock().lock(); try { StringBuilder buffer = new StringBuilder(); for ( int i = 0; i < size(); i++ ) { if ( i > 0 ) { buffer.append( ", " ); } buffer.append( "[" ); buffer.append( getString( row, i ) ); buffer.append( "]" ); } return buffer.toString(); } finally { lock.readLock().unlock(); } } /** * Get an array of strings showing the name of the values in the row padded to a maximum length, followed by the types * of the values. * * @param maxlen The length to which the name will be padded. * @return an array of strings: the names and the types of the fieldnames in the row. */ @Override public String[] getFieldNamesAndTypes( int maxlen ) { lock.readLock().lock(); try { final int size = size(); String[] retval = new String[ size ]; for ( int i = 0; i < size; i++ ) { ValueMetaInterface v = getValueMeta( i ); retval[ i ] = Const.rightPad( v.getName(), maxlen ) + " (" + v.getTypeDesc() + ")"; } return retval; } finally { lock.readLock().unlock(); } } /** * Compare 2 rows with each other using certain values in the rows and also considering the specified ascending * clauses of the value metadata. * * @param rowData1 The first row of data * @param rowData2 The second row of data * @param fieldnrs the fields to compare on (in that order) * @return 0 if the rows are considered equal, -1 is data1 is smaller, 1 if data2 is smaller. * @throws KettleValueException */ @Override public int compare( Object[] rowData1, Object[] rowData2, int[] fieldnrs ) throws KettleValueException { lock.readLock().lock(); try { for ( int fieldnr : fieldnrs ) { ValueMetaInterface valueMeta = getValueMeta( fieldnr ); int cmp = valueMeta.compare( rowData1[ fieldnr ], rowData2[ fieldnr ] ); if ( cmp != 0 ) { return cmp; } } return 0; } finally { lock.readLock().unlock(); } } /** * Compare 2 rows with each other for equality using certain values in the rows and also considering the case * sensitivity flag. * * @param rowData1 The first row of data * @param rowData2 The second row of data * @param fieldnrs the fields to compare on (in that order) * @return true if the rows are considered equal, false if they are not. * @throws KettleValueException */ @Override public boolean equals( Object[] rowData1, Object[] rowData2, int[] fieldnrs ) throws KettleValueException { lock.readLock().lock(); try { for ( int fieldnr : fieldnrs ) { ValueMetaInterface valueMeta = getValueMeta( fieldnr ); int cmp = valueMeta.compare( rowData1[ fieldnr ], rowData2[ fieldnr ] ); if ( cmp != 0 ) { return false; } } return true; } finally { lock.readLock().unlock(); } } /** * Compare 2 rows with each other using certain values in the rows and also considering the specified ascending * clauses of the value metadata. * * @param rowData1 The first row of data * @param rowData2 The second row of data * @param fieldnrs1 The indexes of the values to compare in the first row * @param fieldnrs2 The indexes of the values to compare with in the second row * @return 0 if the rows are considered equal, -1 is data1 is smaller, 1 if data2 is smaller. * @throws KettleValueException */ @Override public int compare( Object[] rowData1, Object[] rowData2, int[] fieldnrs1, int[] fieldnrs2 ) throws KettleValueException { int len = ( fieldnrs1.length < fieldnrs2.length ) ? fieldnrs1.length : fieldnrs2.length; lock.readLock().lock(); try { for ( int i = 0; i < len; i++ ) { ValueMetaInterface valueMeta = getValueMeta( fieldnrs1[ i ] ); int cmp = valueMeta.compare( rowData1[ fieldnrs1[ i ] ], rowData2[ fieldnrs2[ i ] ] ); if ( cmp != 0 ) { return cmp; } } return 0; } finally { lock.readLock().unlock(); } } /** * Compare 2 rows with each other using certain values in the rows and also considering the specified ascending * clauses of the value metadata. * * @param rowData1 The first row of data * @param rowMeta2 the metadata of the second row of data * @param rowData2 The second row of data * @param fieldnrs1 The indexes of the values to compare in the first row * @param fieldnrs2 The indexes of the values to compare with in the second row * @return 0 if the rows are considered equal, -1 is data1 is smaller, 1 if data2 is smaller. * @throws KettleValueException */ @Override public int compare( Object[] rowData1, RowMetaInterface rowMeta2, Object[] rowData2, int[] fieldnrs1, int[] fieldnrs2 ) throws KettleValueException { int len = ( fieldnrs1.length < fieldnrs2.length ) ? fieldnrs1.length : fieldnrs2.length; lock.readLock().lock(); try { for ( int i = 0; i < len; i++ ) { ValueMetaInterface valueMeta1 = getValueMeta( fieldnrs1[ i ] ); ValueMetaInterface valueMeta2 = rowMeta2.getValueMeta( fieldnrs2[ i ] ); int cmp = valueMeta1.compare( rowData1[ fieldnrs1[ i ] ], valueMeta2, rowData2[ fieldnrs2[ i ] ] ); if ( cmp != 0 ) { return cmp; } } return 0; } finally { lock.readLock().unlock(); } } /** * Compare 2 rows with each other using all values in the rows and also considering the specified ascending clauses of * the value metadata. * * @param rowData1 The first row of data * @param rowData2 The second row of data * @return 0 if the rows are considered equal, -1 is data1 is smaller, 1 if data2 is smaller. * @throws KettleValueException */ @Override public int compare( Object[] rowData1, Object[] rowData2 ) throws KettleValueException { lock.readLock().lock(); try { for ( int i = 0; i < size(); i++ ) { ValueMetaInterface valueMeta = getValueMeta( i ); int cmp = valueMeta.compare( rowData1[ i ], rowData2[ i ] ); if ( cmp != 0 ) { return cmp; } } return 0; } finally { lock.readLock().unlock(); } } /** * Calculate a hashCode of the content (not the index) of the data specified NOTE: This method uses a simple XOR of * the individual hashCodes which can result in a lot of collisions for similar types of data (e.g. [A,B] == [B,A] and * is not suitable for normal use. It is kept to provide backward compatibility with CombinationLookup.lookupValues() * * @param rowData The data to calculate a hashCode with * @return the calculated hashCode * @throws KettleValueException in case there is a data conversion error * @deprecated */ @Override @Deprecated public int oldXORHashCode( Object[] rowData ) throws KettleValueException { int hash = 0; lock.readLock().lock(); try { for ( int i = 0; i < size(); i++ ) { ValueMetaInterface valueMeta = getValueMeta( i ); hash ^= valueMeta.hashCode( rowData[ i ] ); } return hash; } finally { lock.readLock().unlock(); } } /** * Calculates a simple hashCode of all the native data objects in the supplied row. This method will return a better * distribution of values for rows of numbers or rows with the same values in different positions. NOTE: This method * performs against the native values, not the values returned by ValueMeta. This means that if you have two rows with * different primitive values ['2008-01-01:12:30'] and ['2008-01-01:00:00'] that use a format object to change the * value (as Date yyyy-MM-dd), the hashCodes will be different resulting in the two rows not being considered equal * via the hashCode even though compare() or equals() might consider them to be. * * @param rowData The data to calculate a hashCode with * @return the calculated hashCode * @throws KettleValueException in case there is a data conversion error */ @Override public int hashCode( Object[] rowData ) throws KettleValueException { return Arrays.hashCode( rowData ); } /** * Calculates a hashcode of the converted value of all objects in the supplied row. This method returns distinct * values for nulls of different data types and will return the same hashCode for different native values that have a * ValueMeta converting them into the same value (e.g. ['2008-01-01:12:30'] and ['2008-01-01:00:00'] as Date * yyyy-MM-dd) * * @param rowData The data to calculate a hashCode with * @return the calculated hashCode * @throws KettleValueException in case there is a data conversion error */ @Override public int convertedValuesHashCode( Object[] rowData ) throws KettleValueException { if ( rowData == null ) { return 0; } int result = 1; lock.readLock().lock(); try { for ( int i = 0; i < rowData.length; i++ ) { result = 31 * result + getValueMeta( i ).hashCode(); } return result; } finally { lock.readLock().unlock(); } } /** * Serialize a row of data to byte[] * * @param metadata the metadata to use * @param row the row of data * @return a serialized form of the data as a byte array */ public static byte[] extractData( RowMetaInterface metadata, Object[] row ) { try { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); DataOutputStream dataOutputStream = new DataOutputStream( byteArrayOutputStream ); metadata.writeData( dataOutputStream, row ); dataOutputStream.close(); byteArrayOutputStream.close(); return byteArrayOutputStream.toByteArray(); } catch ( Exception e ) { throw new RuntimeException( "Error serializing row to byte array", e ); } } /** * Create a row of data bases on a serialized format (byte[]) * * @param data the serialized data * @param metadata the metadata to use * @return a new row of data */ public static Object[] getRow( RowMetaInterface metadata, byte[] data ) { try { ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( data ); DataInputStream dataInputStream = new DataInputStream( byteArrayInputStream ); return metadata.readData( dataInputStream ); } catch ( Exception e ) { throw new RuntimeException( "Error de-serializing row of data from byte array", e ); } } public static Row createOriginalRow( RowMetaInterface rowMeta, Object[] rowData ) throws KettleValueException { Row row = new Row(); for ( int i = 0; i < rowMeta.size(); i++ ) { ValueMetaInterface valueMeta = rowMeta.getValueMeta( i ); Object valueData = rowData[ i ]; Value value = valueMeta.createOriginalValue( valueData ); row.addValue( value ); } return row; } /** * @return an XML representation of the row metadata * @throws IOException Thrown in case there is an (Base64/GZip) encoding problem */ @Override public String getMetaXML() throws IOException { StringBuilder xml = new StringBuilder(); xml.append( "<" ).append( XML_META_TAG ).append( ">" ); lock.readLock().lock(); try { for ( int i = 0; i < size(); i++ ) { xml.append( getValueMeta( i ).getMetaXML() ); } } finally { lock.readLock().unlock(); } xml.append( "</" ).append( XML_META_TAG ).append( ">" ); return xml.toString(); } /** * Create a new row metadata object from XML * * @param node the XML node to deserialize from * @throws IOException Thrown in case there is an (Base64/GZip) decoding problem */ public RowMeta( Node node ) throws KettleException { this(); int nrValues = XMLHandler.countNodes( node, ValueMeta.XML_META_TAG ); for ( int i = 0; i < nrValues; i++ ) { addValueMeta( new ValueMeta( XMLHandler.getSubNodeByNr( node, ValueMeta.XML_META_TAG, i ) ) ); } } /** * @param rowData the row of data to serialize as XML * @return an XML representation of the row data * @throws IOException Thrown in case there is an (Base64/GZip) encoding problem */ @Override public String getDataXML( Object[] rowData ) throws IOException { StringBuilder xml = new StringBuilder(); xml.append( "<" ).append( XML_DATA_TAG ).append( ">" ); lock.readLock().lock(); try { for ( int i = 0; i < size(); i++ ) { xml.append( getValueMeta( i ).getDataXML( rowData[ i ] ) ); } } finally { lock.readLock().unlock(); } xml.append( "</" ).append( XML_DATA_TAG ).append( ">" ); return xml.toString(); } /** * Convert an XML node into binary data using the row metadata supplied. * * @param node The data row node * @return a row of data, converted from XML * @throws IOException Thrown in case there is an (Base64/GZip) decoding problem */ @Override public Object[] getRow( Node node ) throws KettleException { lock.readLock().lock(); try { Object[] rowData = RowDataUtil.allocateRowData( size() ); for ( int i = 0; i < size(); i++ ) { Node valueDataNode = XMLHandler.getSubNodeByNr( node, ValueMeta.XML_DATA_TAG, i ); rowData[ i ] = getValueMeta( i ).getValue( valueDataNode ); } return rowData; } finally { lock.readLock().unlock(); } } @VisibleForTesting static class RowMetaCache { @VisibleForTesting final Map<String, Integer> mapping; @VisibleForTesting List<Integer> needRealClone; RowMetaCache() { this( new ConcurrentHashMap<String, Integer>(), null ); } /** * Copy constructor for clone * * @param rowMetaCache */ RowMetaCache( RowMetaCache rowMetaCache ) { this( new ConcurrentHashMap<>( rowMetaCache.mapping ), rowMetaCache.needRealClone == null ? null : new ArrayList<>( rowMetaCache.needRealClone ) ); } RowMetaCache( Map<String, Integer> mapping, List<Integer> needRealClone ) { this.mapping = mapping; this.needRealClone = needRealClone; } synchronized void invalidate() { mapping.clear(); needRealClone = null; } void storeMapping( String name, int index ) { if ( Utils.isEmpty( name ) ) { return; } synchronized ( this ) { mapping.put( name.toLowerCase(), index ); needRealClone = null; } } synchronized void replaceMapping( String old, String current, int index ) { if ( !Utils.isEmpty( old ) ) { mapping.remove( old.toLowerCase() ); } storeMapping( current, index ); } Integer findAndCompare( String name, List<? extends ValueMetaInterface> metas ) { if ( Utils.isEmpty( name ) ) { return null; } synchronized ( this ) { name = name.toLowerCase(); Integer index = mapping.get( name ); if ( index != null ) { ValueMetaInterface value = metas.get( index ); if ( !name.equalsIgnoreCase( value.getName() ) ) { mapping.remove( name ); index = null; } } return index; } } synchronized List<Integer> getOrCreateValuesThatNeedRealClone( List<ValueMetaInterface> values ) { if ( needRealClone == null ) { int len = values.size(); needRealClone = new ArrayList<Integer>( len ); for ( int i = 0; i < len; i++ ) { ValueMetaInterface valueMeta = values.get( i ); if ( valueMeta.requiresRealClone() ) { needRealClone.add( i ); } } } return needRealClone; } } }