/*
* Copyright 2012 Red Hat, Inc. and/or its affiliates.
*
* 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.kie.workbench.common.widgets.decoratedgrid.client.widget.data;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import org.kie.workbench.common.widgets.decoratedgrid.client.widget.CellValue;
import org.kie.workbench.common.widgets.decoratedgrid.client.widget.SortConfiguration;
/**
* A simple container for rows of data.
*/
public class DynamicData
implements
Iterable<DynamicDataRow> {
private boolean isMerged = false;
private List<Boolean> visibleColumns = new ArrayList<Boolean>();
private static final long serialVersionUID = 5061393855340039472L;
private List<DynamicDataRow> data = new ArrayList<DynamicDataRow>();
/**
* Add column to data
* @param index
* @param columnData
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public void addColumn( int index,
List<CellValue<? extends Comparable<?>>> columnData,
boolean isVisible ) {
//Check the column contains the same number of rows as the existing table
int numberOfRows = 0;
if ( this.data.size() > 0 ) {
for ( DynamicDataRow row : this.data ) {
numberOfRows++;
if ( row instanceof GroupedDynamicDataRow ) {
numberOfRows = numberOfRows + ( (GroupedDynamicDataRow) row ).getChildRows().size() - 1;
}
}
}
if ( numberOfRows != columnData.size() ) {
throw new IllegalArgumentException( "columnData contains a different number of rows to that defined." );
}
//Add the column data to the table
int iRowIndex = 0;
for ( int iRow = 0; iRow < data.size(); iRow++ ) {
DynamicDataRow row = data.get( iRow );
CellValue<? extends Comparable<?>> cell = columnData.get( iRowIndex );
if ( row instanceof GroupedDynamicDataRow ) {
GroupedDynamicDataRow groupedRow = (GroupedDynamicDataRow) row;
//Setting value on a GroupedCellValue causes all children to assume the same value
CellValue.GroupedCellValue gcv = cell.convertToGroupedCell();
groupedRow.add( index,
gcv );
//So set the children's values accordingly
for ( int iGroupedRow = 0; iGroupedRow < groupedRow.getChildRows().size(); iGroupedRow++ ) {
cell = columnData.get( iRowIndex );
gcv.addCellToGroup( cell );
groupedRow.getChildRows().get( iGroupedRow ).set( index,
cell );
iRowIndex++;
}
} else {
row.add( index,
cell );
iRowIndex++;
}
}
visibleColumns.add( index,
isVisible );
assertModelMerging();
}
/**
* Move a column
* @param sourceColumnIndex
* @param targetColumnIndex
*/
public void moveColumn( int sourceColumnIndex,
int targetColumnIndex ) {
for ( int iRow = 0; iRow < data.size(); iRow++ ) {
DynamicDataRow row = data.get( iRow );
row.add( targetColumnIndex,
row.remove( sourceColumnIndex ) );
}
visibleColumns.add( targetColumnIndex,
visibleColumns.remove( sourceColumnIndex ) );
assertModelMerging();
}
/**
* Add an empty row of data to the end of the table
* @return DynamicDataRow The newly created row
*/
public DynamicDataRow addRow() {
DynamicDataRow row = new DynamicDataRow();
data.add( row );
assertModelMerging();
return row;
}
/**
* Add a row of data at the specified index
* @param index
* @param rowData
*/
public void addRow( int index,
DynamicDataRow rowData ) {
data.add( index,
rowData );
assertModelMerging();
}
/**
* Add a row of data at the end of the table
* @param rowData
*/
public void addRow( DynamicDataRow rowData ) {
addRow( data.size(),
rowData );
}
/**
* Apply grouping by collapsing applicable rows
* @param startCell
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public void applyModelGrouping( CellValue<?> startCell ) {
int startRowIndex = startCell.getCoordinate().getRow();
int endRowIndex = findMergedCellExtent( startCell.getCoordinate() ).getRow();
int colIndex = startCell.getCoordinate().getCol();
//Delete grouped rows replacing with a single "grouped" row
CellValue.GroupedCellValue groupedCell;
DynamicDataRow row = data.get( startRowIndex );
GroupedDynamicDataRow groupedRow = new GroupedDynamicDataRow();
for ( int iCol = 0; iCol < row.size(); iCol++ ) {
groupedCell = row.get( iCol ).convertToGroupedCell();
if ( iCol == colIndex ) {
groupedCell.addState( CellValue.CellState.GROUPED );
} else {
groupedCell.removeState( CellValue.CellState.GROUPED );
}
groupedRow.add( groupedCell );
}
//Add individual cells to "grouped" row
for ( int iRow = startRowIndex; iRow <= endRowIndex; iRow++ ) {
DynamicDataRow childRow = data.get( startRowIndex );
groupedRow.addChildRow( childRow );
data.remove( childRow );
}
data.remove( row );
data.add( startRowIndex,
groupedRow );
assertModelMerging();
}
public void clear() {
data.clear();
}
/**
* Delete column data
* @param index
*/
public void deleteColumn( int index ) {
for ( int iRow = 0; iRow < data.size(); iRow++ ) {
DynamicDataRow row = data.get( iRow );
row.remove( index );
}
visibleColumns.remove( index );
assertModelMerging();
}
public DynamicDataRow deleteRow( int index ) {
DynamicDataRow row = data.remove( index );
assertModelMerging();
return row;
}
/**
* Get the CellValue at the given coordinate
* @param c
* @return
*/
public CellValue<? extends Comparable<?>> get( Coordinate c ) {
return data.get( c.getRow() ).get( c.getCol() );
}
public DynamicDataRow get( int index ) {
return data.get( index );
}
/**
* Return grid's data. Grouping in the data will be expanded and can
* therefore can be used prior to populate the underlying data structures
* prior to persisting.
* @return data
*/
public DynamicData getFlattenedData() {
DynamicData dataClone = new DynamicData();
for ( int iRow = 0; iRow < data.size(); iRow++ ) {
DynamicDataRow row = data.get( iRow );
if ( row instanceof GroupedDynamicDataRow ) {
List<DynamicDataRow> expandedRow = expandGroupedRow( row,
true );
dataClone.data.addAll( expandedRow );
} else {
dataClone.data.add( row );
}
}
return dataClone;
}
public int indexOf( DynamicDataRow row ) {
return data.indexOf( row );
}
/**
* Return the state of merging
* @return
*/
public boolean isMerged() {
return isMerged;
}
/**
* Return the state of grouping
* @return true if any rows are grouped
*/
public boolean isGrouped() {
for ( DynamicDataRow row : this.data ) {
if ( row instanceof GroupedDynamicDataRow ) {
return true;
}
}
return false;
}
public Iterator<DynamicDataRow> iterator() {
return data.iterator();
}
/**
* Remove grouping by expanding applicable rows
* @param startCell
* @return
*/
@SuppressWarnings("rawtypes")
public List<DynamicDataRow> removeModelGrouping( CellValue<?> startCell ) {
int startRowIndex = startCell.getCoordinate().getRow();
startCell.removeState( CellValue.CellState.GROUPED );
//Check if rows need to be recursively expanded
boolean bRecursive = true;
DynamicDataRow row = data.get( startRowIndex );
for ( int iCol = 0; iCol < row.size(); iCol++ ) {
CellValue<?> cv = row.get( iCol );
if ( cv instanceof CellValue.GroupedCellValue ) {
bRecursive = !( bRecursive ^ ( (CellValue.GroupedCellValue) cv ).hasMultipleValues() );
}
}
//Delete "grouped" row and replace with individual rows
List<DynamicDataRow> expandedRow = expandGroupedRow( row,
bRecursive );
deleteRow( startRowIndex );
data.addAll( startRowIndex,
expandedRow );
assertModelMerging();
//If the row is replaced with another grouped row ensure the row can be expanded
row = data.get( startRowIndex );
boolean hasCellToExpand = false;
for ( CellValue<?> cell : row ) {
if ( cell instanceof CellValue.GroupedCellValue ) {
if ( cell.isGrouped() && cell.getRowSpan() > 0 ) {
hasCellToExpand = true;
break;
}
}
}
if ( !hasCellToExpand ) {
for ( CellValue<?> cell : row ) {
if ( cell instanceof CellValue.GroupedCellValue && cell.getRowSpan() == 1 ) {
cell.addState( CellValue.CellState.GROUPED );
}
}
}
return expandedRow;
}
/**
* Set the value at the specified coordinate
* @param c
* @param value
*/
public void set( Coordinate c,
Object value ) {
if ( c == null ) {
throw new IllegalArgumentException( "c cannot be null" );
}
data.get( c.getRow() ).get( c.getCol() ).setValue( value );
assertModelMerging();
}
/**
* Set whether a columns is Visible
* @param index index of column
* @param isVisible True if the column is visible
*/
public void setColumnVisibility( int index,
boolean isVisible ) {
this.visibleColumns.set( index,
isVisible );
assertModelIndexes();
}
/**
* Set whether the grid's data is merged or not. Clearing merging within the
* data also clears grouping
* @param isMerged
*/
public void setMerged( boolean isMerged ) {
this.isMerged = isMerged;
if ( isMerged ) {
assertModelMerging();
} else {
removeModelGrouping();
removeModelMerging();
}
}
public int size() {
return data.size();
}
public void sort( final List<SortConfiguration> sortConfig ) {
if ( sortConfig.size() == 0 ) {
//No sort configuration, restore original creation sequence
Collections.sort( data,
new Comparator<DynamicDataRow>() {
public int compare( DynamicDataRow leftRow,
DynamicDataRow rightRow ) {
int comparison = 0;
long li = leftRow.getCreationIndex();
long ri = rightRow.getCreationIndex();
if ( li < ri ) {
comparison = -1;
} else if ( li > ri ) {
comparison = 1;
}
return comparison;
}
} );
} else {
//Sort Configuration needs to be sorted itself by sortOrder first
Collections.sort( sortConfig,
new Comparator<SortConfiguration>() {
public int compare( SortConfiguration o1,
SortConfiguration o2 ) {
Integer si1 = o1.getSortIndex();
Integer si2 = o2.getSortIndex();
return si1.compareTo( si2 );
}
} );
//Sort data
Collections.sort( data,
new Comparator<DynamicDataRow>() {
@SuppressWarnings({ "rawtypes", "unchecked" })
public int compare( DynamicDataRow leftRow,
DynamicDataRow rightRow ) {
int comparison = 0;
for ( int index = 0; index < sortConfig.size(); index++ ) {
SortConfiguration sc = sortConfig.get( index );
Comparable leftColumnValue = leftRow.get( sc.getColumnIndex() );
Comparable rightColumnValue = rightRow.get( sc.getColumnIndex() );
comparison =
( leftColumnValue == rightColumnValue ) ? 0
: ( leftColumnValue == null ) ? -1
: ( rightColumnValue == null ) ? 1
: leftColumnValue.compareTo( rightColumnValue );
if ( comparison != 0 ) {
switch ( sc.getSortDirection() ) {
case ASCENDING:
break;
case DESCENDING:
comparison = -comparison;
break;
default:
throw new IllegalStateException(
"Sorting can only be enabled for ASCENDING or"
+ " DESCENDING, not sortDirection ("
+ sc.getSortDirection()
+ ") ." );
}
return comparison;
}
}
return comparison;
}
} );
}
assertModelMerging();
}
// Here lays a can of worms! Each cell in the Decision Table has three
// coordinates: (1) The physical coordinate, (2) The coordinate relating to
// the HTML table element and (3) The coordinate mapping a HTML table
// element back to the physical coordinate. For example a cell could have
// the (1) physical coordinate (0,0) which equates to (2) HTML element (0,1)
// in which case the cell at physical coordinate (0,1) would have a (3)
// mapping back to (0,0).
private void assertModelIndexes() {
if ( data.size() == 0 ) {
return;
}
for ( int iRow = 0; iRow < data.size(); iRow++ ) {
DynamicDataRow row = data.get( iRow );
int colCount = 0;
for ( int iCol = 0; iCol < row.size(); iCol++ ) {
int newRow = iRow;
int newCol = colCount;
CellValue<? extends Comparable<?>> indexCell = row.get( iCol );
indexCell.setCoordinate( new Coordinate( iRow,
iCol ) );
// Don't index hidden columns; indexing is used to
// map between HTML elements and the data behind
if ( visibleColumns.get( iCol ) ) {
if ( indexCell.getRowSpan() != 0 ) {
newRow = iRow;
newCol = colCount++;
CellValue<? extends Comparable<?>> cell = data.get( newRow ).get( newCol );
cell.setPhysicalCoordinate( new Coordinate( iRow,
iCol ) );
} else {
DynamicDataRow priorRow = data.get( iRow - 1 );
CellValue<? extends Comparable<?>> priorCell = priorRow.get( iCol );
Coordinate priorHtmlCoordinate = priorCell.getHtmlCoordinate();
newRow = priorHtmlCoordinate.getRow();
newCol = priorHtmlCoordinate.getCol();
}
} else {
final int priorColIndex = ( iCol > 0 ? iCol - 1 : 0 );
CellValue<? extends Comparable<?>> priorCell = row.get( priorColIndex );
Coordinate priorHtmlCoordinate = priorCell.getHtmlCoordinate();
newRow = priorHtmlCoordinate.getRow();
newCol = priorHtmlCoordinate.getCol();
}
indexCell.setHtmlCoordinate( new Coordinate( newRow,
newCol ) );
}
}
}
/**
* Ensure merging and indexing is reflected in the entire model. This should
* be called whenever any changes are made to the underlying data externally
* to the add/remove methods provided publicly herein, such as bulk move
* operations.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public void assertModelMerging() {
if ( data.size() == 0 ) {
return;
}
//Remove merging first as it initialises all coordinates
removeModelMerging();
final int COLUMNS = data.get( 0 ).size();
//Only apply merging if merged
if ( isMerged ) {
int minRowIndex = 0;
int maxRowIndex = data.size();
//Add an empty row to the end of the data to simplify detection of merged cells that run to the end of the table
DynamicDataRow blankRow = new DynamicDataRow();
for ( int iCol = 0; iCol < COLUMNS; iCol++ ) {
CellValue cv = new CellValue( null );
Coordinate c = new Coordinate( data.size(),
iCol );
cv.setCoordinate( c );
cv.setHtmlCoordinate( c );
cv.setPhysicalCoordinate( c );
blankRow.add( cv );
}
data.add( blankRow );
maxRowIndex++;
//Look in columns for cells with identical values
for ( int iCol = 0; iCol < COLUMNS; iCol++ ) {
CellValue<?> cell1 = data.get( minRowIndex ).get( iCol );
CellValue<?> cell2 = null;
for ( int iRow = minRowIndex + 1; iRow < maxRowIndex; iRow++ ) {
cell1.setRowSpan( 1 );
cell2 = data.get( iRow ).get( iCol );
//Merge if both cells contain the same value and neither is grouped
boolean bSplit = true;
if ( !cell1.isEmpty() && !cell2.isEmpty() ) {
if ( cell1.getValue().equals( cell2.getValue() ) ) {
bSplit = false;
if ( cell1 instanceof CellValue.GroupedCellValue ) {
bSplit = true;
}
if ( cell2 instanceof CellValue.GroupedCellValue ) {
bSplit = true;
}
}
} else if ( cell1.isOtherwise() && cell2.isOtherwise() ) {
bSplit = false;
if ( cell1 instanceof CellValue.GroupedCellValue ) {
CellValue.GroupedCellValue gcv = (CellValue.GroupedCellValue) cell1;
if ( gcv.hasMultipleValues() ) {
bSplit = true;
}
}
if ( cell2 instanceof CellValue.GroupedCellValue ) {
CellValue.GroupedCellValue gcv = (CellValue.GroupedCellValue) cell2;
if ( gcv.hasMultipleValues() ) {
bSplit = true;
}
}
}
if ( bSplit ) {
mergeCells( cell1,
cell2 );
cell1 = cell2;
}
}
}
//Remove dummy blank row
data.remove( blankRow );
}
// Set indexes after merging has been corrected
assertModelIndexes();
}
//Expand a grouped row and return a list of expanded rows
private List<DynamicDataRow> expandGroupedRow( DynamicDataRow row,
boolean bRecursive ) {
List<DynamicDataRow> ungroupedRows = new ArrayList<DynamicDataRow>();
if ( row instanceof GroupedDynamicDataRow ) {
GroupedDynamicDataRow groupedRow = (GroupedDynamicDataRow) row;
for ( int iChildRow = 0; iChildRow < groupedRow.getChildRows().size(); iChildRow++ ) {
DynamicDataRow childRow = groupedRow.getChildRows().get( iChildRow );
if ( bRecursive ) {
if ( childRow instanceof GroupedDynamicDataRow ) {
List<DynamicDataRow> expandedRow = expandGroupedRow( childRow,
bRecursive );
ungroupedRows.addAll( expandedRow );
} else {
ungroupCells( childRow );
ungroupedRows.add( childRow );
}
} else {
ungroupedRows.add( childRow );
}
}
} else {
ungroupCells( row );
ungroupedRows.add( row );
}
return ungroupedRows;
}
//Find the bottom coordinate of a merged cell
private Coordinate findMergedCellExtent( Coordinate c ) {
if ( c.getRow() == data.size() - 1 ) {
return c;
}
Coordinate nc = new Coordinate( c.getRow() + 1,
c.getCol() );
CellValue<?> newCell = get( nc );
while ( newCell.getRowSpan() == 0 && nc.getRow() < data.size() - 1 ) {
nc = new Coordinate( nc.getRow() + 1,
nc.getCol() );
newCell = get( nc );
}
if ( newCell.getRowSpan() != 0 ) {
nc = new Coordinate( nc.getRow() - 1,
nc.getCol() );
newCell = get( nc );
}
return nc;
}
//Merge between the two provided cells
private void mergeCells( CellValue<?> cell1,
CellValue<?> cell2 ) {
int iStartRowIndex = cell1.getCoordinate().getRow();
int iEndRowIndex = cell2.getCoordinate().getRow();
int iColIndex = cell1.getCoordinate().getCol();
//Any rows that are grouped need row span of zero
for ( int iRow = iStartRowIndex; iRow < iEndRowIndex; iRow++ ) {
DynamicDataRow row = data.get( iRow );
row.get( iColIndex ).setRowSpan( 0 );
}
cell1.setRowSpan( iEndRowIndex - iStartRowIndex );
}
//Initialise cell parameters when ungrouped
private void ungroupCells( DynamicDataRow row ) {
for ( int iCol = 0; iCol < row.size(); iCol++ ) {
CellValue<?> cell = row.get( iCol );
cell.removeState( CellValue.CellState.GROUPED );
}
}
/**
* Remove all grouping throughout the model
*/
void removeModelGrouping() {
for ( int iRow = 0; iRow < data.size(); iRow++ ) {
DynamicDataRow row = data.get( iRow );
if ( row instanceof GroupedDynamicDataRow ) {
List<DynamicDataRow> expandedRow = expandGroupedRow( row,
true );
deleteRow( iRow );
data.addAll( iRow,
expandedRow );
iRow = iRow + expandedRow.size() - 1;
}
}
}
/**
* Remove merging from model
*/
void removeModelMerging() {
for ( int iRow = 0; iRow < data.size(); iRow++ ) {
DynamicDataRow row = data.get( iRow );
for ( int iCol = 0; iCol < row.size(); iCol++ ) {
CellValue<?> cell = row.get( iCol );
Coordinate c = new Coordinate( iRow,
iCol );
cell.setCoordinate( c );
cell.setHtmlCoordinate( c );
cell.setPhysicalCoordinate( c );
cell.setRowSpan( 1 );
}
}
// Set indexes after merging has been corrected
assertModelIndexes();
}
}