/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright (c) 2001 - 2013 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved.
*/
package org.pentaho.reporting.engine.classic.core.states.datarow;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot;
import org.pentaho.reporting.engine.classic.core.DataRow;
import org.pentaho.reporting.engine.classic.core.util.IntegerCache;
import org.pentaho.reporting.libraries.base.util.ObjectUtilities;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
public final class FastGlobalView implements DataRow, MasterDataRowChangeHandler {
private static final Log logger = LogFactory.getLog( FastGlobalView.class );
private HashSet<String> duplicateColumns;
private HashSet<String> invalidColumns;
private boolean modifiableNameCache;
private HashMap<String, Integer> nameCache;
private String[] columnNames;
private Boolean[] columnChanged;
private Object[] columnValue;
private Object[] columnOldValue;
private int[] columnPrev;
private int length;
private boolean warnInvalidColumns;
private MasterDataRowChangeEvent reusableEvent;
public FastGlobalView( final FastGlobalView parent ) {
if ( parent.modifiableNameCache ) {
this.duplicateColumns = (HashSet<String>) parent.duplicateColumns.clone();
this.nameCache = (HashMap<String, Integer>) parent.nameCache.clone();
this.modifiableNameCache = false;
this.columnNames = parent.columnNames.clone();
} else {
this.duplicateColumns = parent.duplicateColumns;
this.nameCache = parent.nameCache;
this.columnNames = parent.columnNames;
this.modifiableNameCache = false;
}
this.reusableEvent = parent.reusableEvent;
this.columnChanged = parent.columnChanged.clone();
this.columnValue = parent.columnValue.clone();
this.columnOldValue = parent.columnOldValue.clone();
this.columnPrev = parent.columnPrev.clone();
this.length = parent.length;
this.warnInvalidColumns = parent.warnInvalidColumns;
this.invalidColumns = parent.invalidColumns;
}
public FastGlobalView() {
this.warnInvalidColumns =
"true".equals( ClassicEngineBoot.getInstance().getGlobalConfig().getConfigProperty(
"org.pentaho.reporting.engine.classic.core.WarnInvalidColumns" ) );
if ( warnInvalidColumns ) {
this.invalidColumns = new HashSet<String>();
}
this.reusableEvent = new MasterDataRowChangeEvent();
this.duplicateColumns = new HashSet<String>();
this.nameCache = new HashMap<String, Integer>();
this.modifiableNameCache = true;
this.columnNames = new String[20];
this.columnChanged = new Boolean[20];
this.columnValue = new Object[20];
this.columnOldValue = new Object[20];
this.columnPrev = new int[20];
}
public MasterDataRowChangeEvent getReusableEvent() {
return reusableEvent;
}
public void dataRowChanged( final MasterDataRowChangeEvent chEvent ) {
// rebuild the global view and tracks changes ..
final int type = chEvent.getType();
if ( type == MasterDataRowChangeEvent.COLUMN_ADDED ) {
putField( chEvent.getColumnName(), chEvent.getColumnValue(), false, false );
} else if ( type == MasterDataRowChangeEvent.COLUMN_UPDATED ) {
putField( chEvent.getColumnName(), chEvent.getColumnValue(), true, chEvent.isOptional() );
} else if ( type == MasterDataRowChangeEvent.COLUMN_REMOVED ) {
removeColumn( chEvent.getColumnName() );
}
}
public String[] getColumnNames() {
final String[] columnNames = new String[length];
System.arraycopy( this.columnNames, 0, columnNames, 0, length );
return columnNames;
}
public Object get( final String col ) throws IllegalStateException {
final int idx = findColumn( col );
if ( idx < 0 ) {
if ( warnInvalidColumns ) {
if ( invalidColumns.add( col ) ) {
logger.warn( "Warning: Data-Set does not contain a column with name '" + col + '\'' );
}
}
return null;
}
if ( columnChanged[idx] != null ) {
return columnValue[idx];
}
final String columnName = columnNames[idx];
if ( duplicateColumns.contains( columnName ) ) {
for ( int i = idx - 1; i >= 0; i-- ) {
if ( columnNames[i].equals( columnName ) && columnChanged[i] != null ) {
return columnValue[i];
}
}
}
return columnValue[idx];
}
private int findColumn( final String name ) {
final Integer o = nameCache.get( name );
if ( o == null ) {
return -1;
}
return o.intValue();
}
public boolean isChanged( final String name ) {
final int idx = findColumn( name );
if ( idx < 0 ) {
if ( warnInvalidColumns ) {
if ( invalidColumns.add( name ) ) {
logger.warn( "Warning: Data-Set does not contain a column with name '" + name + '\'' );
}
}
return false;
}
return isChanged( idx );
}
private boolean isChanged( final int col ) {
if ( col < 0 || col >= length ) {
throw new IndexOutOfBoundsException( "Column-Index " + col + " is invalid." );
}
final Boolean val = columnChanged[col];
if ( val != null ) {
return val.booleanValue();
}
final String columnName = columnNames[col];
if ( duplicateColumns.contains( columnName ) ) {
for ( int i = col - 1; i >= 0; i-- ) {
if ( columnNames[col].equals( columnName ) && columnChanged[i] != null ) {
return columnChanged[i].booleanValue();
}
}
}
// the 'isChanged' method may be called during the expression-evaluation before the expression that
// is checked is actually filled in. In that case, the result would be non-deterministic.
//
// When called from a formula, the formula now catches the exception and returns 'error' instead.
// When called from a expression, this has to be caught or the expression evaluates to a error state.
throw new IllegalStateException( "Checking the 'isChanged' flag before all data for this row is known. "
+ "This is a error condition that must be checked by the caller." );
}
public FastGlobalView derive() {
return new FastGlobalView( this );
}
public FastGlobalView advance() {
final FastGlobalView advanced = new FastGlobalView( this );
System.arraycopy( advanced.columnValue, 0, advanced.columnOldValue, 0, length );
Arrays.fill( advanced.columnChanged, null );
return advanced;
}
private void removeColumn( final String name ) {
final boolean needToRebuildCache;
int idx = -1;
if ( duplicateColumns.contains( name ) ) {
needToRebuildCache = true;
// linear index search from the end ..
for ( int i = columnNames.length - 1; i >= 0; i -= 1 ) {
if ( ObjectUtilities.equal( name, columnNames[i] ) ) {
idx = i;
break;
}
}
if ( idx < 0 ) {
return;
}
} else {
needToRebuildCache = false;
final Integer o = nameCache.get( name );
if ( o == null ) {
return;
}
idx = o.intValue();
}
if ( logger.isTraceEnabled() ) {
logger.trace( "Removing column " + name + " (Length: " + length + " NameCache: " + nameCache.size() + ", Idx: "
+ idx );
}
if ( modifiableNameCache == false ) {
this.duplicateColumns = (HashSet<String>) duplicateColumns.clone();
this.columnNames = columnNames.clone();
this.nameCache = (HashMap<String, Integer>) nameCache.clone();
this.modifiableNameCache = true;
}
if ( idx == ( length - 1 ) ) {
columnChanged[idx] = null;
columnNames[idx] = null;
columnValue[idx] = null;
if ( columnPrev[idx] == -1 ) {
nameCache.remove( name );
} else {
nameCache.put( name, IntegerCache.getInteger( columnPrev[idx] ) );
}
// thats the easy case ..
length -= 1;
if ( needToRebuildCache ) {
if ( columnPrev[idx] == -1 ) {
logger.warn( "Column marked as duplicate but no duplicate index recorded: " + name );
} else {
if ( columnPrev[columnPrev[idx]] == -1 ) {
duplicateColumns.remove( name );
}
}
}
return;
}
if ( logger.isTraceEnabled() ) {
logger.warn( "Out of order removeal of a column: " + name );
}
if ( columnPrev[idx] == -1 ) {
nameCache.remove( name );
} else {
nameCache.put( name, IntegerCache.getInteger( columnPrev[idx] ) );
}
final int moveStartIdx = idx + 1;
final int moveLength = length - moveStartIdx;
System.arraycopy( columnNames, moveStartIdx, columnNames, idx, moveLength );
System.arraycopy( columnChanged, moveStartIdx, columnChanged, idx, moveLength );
System.arraycopy( columnOldValue, moveStartIdx, columnOldValue, idx, moveLength );
System.arraycopy( columnValue, moveStartIdx, columnValue, idx, moveLength );
System.arraycopy( columnPrev, moveStartIdx, columnPrev, idx, moveLength );
columnNames[length - 1] = null;
columnChanged[length - 1] = null;
columnOldValue[length - 1] = null;
columnPrev[length - 1] = 0;
// Now it gets expensive: Rebuild the namecache ..
final int newLength = moveLength + idx;
nameCache.clear();
duplicateColumns.clear();
for ( int i = 0; i < newLength; i++ ) {
final String columnName = columnNames[i];
final Integer oldVal = nameCache.get( columnName );
if ( nameCache.containsKey( columnName ) ) {
duplicateColumns.add( columnName );
}
nameCache.put( columnName, IntegerCache.getInteger( i ) );
if ( oldVal != null ) {
columnPrev[i] = oldVal.intValue();
} else {
columnPrev[i] = -1;
}
}
length -= 1;
if ( logger.isTraceEnabled() ) {
logger.trace( "New Namecache: " + nameCache );
}
}
private void putField( final String name, final Object value, final boolean update, final boolean optional ) {
if ( logger.isTraceEnabled() ) {
if ( update ) {
logger.debug( " + : " + name );
} else {
logger.debug( "Adding: " + name );
}
}
if ( update == false ) {
addColumn( name, value );
} else {
// Updating an existing column ...
updateColumn( name, value, optional );
}
}
private void updateColumn( final String name, final Object value, final boolean optional ) {
final Integer o = nameCache.get( name );
if ( o == null ) {
if ( optional ) {
return;
}
throw new IllegalStateException( "Update to a non-existing column: " + name );
}
final String[] columnNames = this.columnNames;
final Boolean[] columnChanged = this.columnChanged;
final Object[] columnValue = this.columnValue;
int idx = -1;
if ( duplicateColumns.isEmpty() == false && duplicateColumns.contains( name ) ) {
final int length = this.length;
for ( int i = 0; i < length; i += 1 ) {
if ( columnChanged[i] == null && ObjectUtilities.equal( name, columnNames[i] ) ) {
idx = i;
break;
}
}
if ( idx < 0 ) {
idx = o.intValue();
}
} else {
idx = o.intValue();
}
columnNames[idx] = name;
final Object oldValue = columnValue[idx];
columnValue[idx] = value;
if ( columnChanged[idx] == null ) {
if ( ObjectUtilities.equal( oldValue, value ) ) {
columnChanged[idx] = Boolean.FALSE;
} else {
columnChanged[idx] = Boolean.TRUE;
}
}
}
private void addColumn( final String name, final Object value ) {
if ( modifiableNameCache == false ) {
this.columnNames = columnNames.clone();
this.nameCache = (HashMap<String, Integer>) nameCache.clone();
this.modifiableNameCache = true;
}
// A new column ...
ensureCapacity( length + 1 );
columnNames[length] = name;
columnValue[length] = value;
final Integer o = nameCache.get( name );
if ( o == null ) {
columnPrev[length] = -1;
} else {
columnPrev[length] = o.intValue();
duplicateColumns.add( name );
}
columnOldValue[length] = null;
columnChanged[length] = Boolean.TRUE;
nameCache.put( name, IntegerCache.getInteger( length ) );
length += 1;
}
private void ensureCapacity( final int requestedSize ) {
final int capacity = this.columnNames.length;
if ( capacity > requestedSize ) {
return;
}
final int newSize = Math.max( capacity << 1, requestedSize + 10 );
final String[] newColumnNames = new String[newSize];
System.arraycopy( columnNames, 0, newColumnNames, 0, length );
this.columnNames = newColumnNames;
final Boolean[] newColumnChanged = new Boolean[newSize];
System.arraycopy( columnChanged, 0, newColumnChanged, 0, length );
this.columnChanged = newColumnChanged;
final int[] newColumnPrev = new int[newSize];
System.arraycopy( columnPrev, 0, newColumnPrev, 0, length );
this.columnPrev = newColumnPrev;
final Object[] newColumnValue = new Object[newSize];
System.arraycopy( columnValue, 0, newColumnValue, 0, length );
this.columnValue = newColumnValue;
final Object[] newOldColumnValue = new Object[newSize];
System.arraycopy( columnOldValue, 0, newOldColumnValue, 0, length );
this.columnOldValue = newOldColumnValue;
}
public void validateChangedFlags() {
for ( int i = 0; i < length; i++ ) {
final Boolean b = columnChanged[i];
if ( b == null ) {
throw new IllegalStateException( "Validate failed: " + columnNames[i] );
}
}
}
}