/******************************************************************************* * * Copyright 2010 Alexandru Craciun, and individual contributors as indicated * by the @authors tag. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 3 of * the License, or (at your option) any later version. * * This software 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. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. ******************************************************************************/ package org.netxilia.api.impl.model; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.EnumSet; import java.util.List; import java.util.concurrent.Future; import org.netxilia.api.command.BlockCellCommandBuilder; import org.netxilia.api.command.CellCommands; import org.netxilia.api.command.ICellCommand; import org.netxilia.api.command.IColumnCommand; import org.netxilia.api.command.IMoreCellCommands; import org.netxilia.api.command.IRowCommand; import org.netxilia.api.command.ISheetCommand; import org.netxilia.api.command.RowCommands; import org.netxilia.api.concurrent.IFutureListener; import org.netxilia.api.concurrent.IListenableFuture; import org.netxilia.api.event.CellEvent; import org.netxilia.api.event.CellEventType; import org.netxilia.api.event.ColumnEvent; import org.netxilia.api.event.ColumnEventType; import org.netxilia.api.event.ISheetEventListener; import org.netxilia.api.event.RowEvent; import org.netxilia.api.event.RowEventType; import org.netxilia.api.event.SheetEvent; import org.netxilia.api.event.SheetEventType; import org.netxilia.api.exception.AbandonedCalculationException; import org.netxilia.api.exception.NetxiliaBusinessException; import org.netxilia.api.exception.NotFoundException; import org.netxilia.api.exception.StorageException; import org.netxilia.api.formula.CyclicDependenciesException; import org.netxilia.api.formula.Formula; import org.netxilia.api.formula.FormulaParsingException; import org.netxilia.api.formula.IPreloadedFormulaContext; import org.netxilia.api.formula.IPreloadedFormulaContextFactory; import org.netxilia.api.impl.IExecutorServiceFactory; import org.netxilia.api.impl.concurrent.MutableFutureWithCounter; import org.netxilia.api.impl.user.ISpringUserService; import org.netxilia.api.model.CellCreator; import org.netxilia.api.model.CellData; import org.netxilia.api.model.CellDataWithProperties; import org.netxilia.api.model.ColumnData; import org.netxilia.api.model.ISheet; import org.netxilia.api.model.IWorkbook; import org.netxilia.api.model.RowData; import org.netxilia.api.model.SheetData; import org.netxilia.api.model.SheetDimensions; import org.netxilia.api.model.SheetFullName; import org.netxilia.api.model.SheetType; import org.netxilia.api.model.SortSpecifier; import org.netxilia.api.model.SortSpecifier.SortColumn; import org.netxilia.api.reference.AreaReference; import org.netxilia.api.reference.CellReference; import org.netxilia.api.reference.IReferenceTransformer; import org.netxilia.api.reference.Range; import org.netxilia.api.reference.ReferenceTransformers; import org.netxilia.api.utils.Matrix; import org.netxilia.api.utils.MatrixBuilder; import org.netxilia.api.utils.Pair; import org.netxilia.api.value.IGenericValue; import org.netxilia.spi.formula.IFormulaCalculator; import org.netxilia.spi.formula.IFormulaCalculatorFactory; import org.netxilia.spi.formula.IFormulaParser; import org.netxilia.spi.storage.ISheetStorageService; import org.springframework.util.Assert; public class SheetActor { private static org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(SheetActor.class); enum SheetStatus { notInitiliazed, initializing, ready } private ISheet myReference; private final IFormulaCalculatorFactory formulaCalculatorFactory; private final IFormulaParser formulaParser; private final ISheetStorageService storageService; private final SheetEventSupport eventSupport; private final Workbook workbook; private final SheetFullName name; private final SheetType type; private boolean refreshEnabled = true; private SheetStatus status = SheetStatus.notInitiliazed; private final ISpringUserService userService; private final IMoreCellCommands moreCellCommands; private final IPreloadedFormulaContextFactory preloadedContextFactory; public SheetActor(Workbook workbook, SheetData sheetData, IFormulaCalculatorFactory formulaCalculatorFactory, IFormulaParser formulaParser, ISheetStorageService storageService, IExecutorServiceFactory executorServiceFactory, ISpringUserService userService, IMoreCellCommands moreCellCommands, IPreloadedFormulaContextFactory preloadedContextFactory) { Assert.notNull(workbook); Assert.notNull(sheetData); Assert.notNull(formulaCalculatorFactory); Assert.notNull(formulaParser); Assert.notNull(storageService); Assert.notNull(executorServiceFactory); Assert.notNull(userService); Assert.notNull(moreCellCommands); Assert.notNull(preloadedContextFactory); this.formulaCalculatorFactory = formulaCalculatorFactory; this.formulaParser = formulaParser; this.name = sheetData.getFullName(); this.type = sheetData.getType(); this.workbook = workbook; this.storageService = storageService; this.userService = userService; this.moreCellCommands = moreCellCommands; this.preloadedContextFactory = preloadedContextFactory; eventSupport = new SheetEventSupport(this.name, executorServiceFactory); } public ISheet getReference() { return myReference; } public SheetType getType() { return type; } public IWorkbook getWorkbook() { return workbook; } private void init() { if (status == SheetStatus.ready || status == SheetStatus.initializing) { return; } Assert.notNull(myReference); status = SheetStatus.initializing; if (workbook.isInitializationEnabled()) { new SheetInitializationProcess(this, formulaParser, workbook.getDependencyManager(), workbook.getAliasDependencyManager(), moreCellCommands); } status = SheetStatus.ready; } public SheetData getSheet() { try { return storageService.loadSheet(); } catch (StorageException e) { throw e; } catch (NotFoundException e) { throw new StorageException(e); } } public SheetDimensions getDimensions() { try { return storageService.getSheetDimensions(); } catch (StorageException e) { throw e; } catch (NotFoundException e) { throw new StorageException(e); } } public void setActorRef(ISheet proxy) { this.myReference = proxy; } public Matrix<CellData> getCells(AreaReference ref) { Assert.notNull(ref); Assert.isTrue(ref.getSheetName() == null || ref.getSheetName().equals(name.getSheetName())); init(); try { return storageService.loadCells(ref); } catch (StorageException e) { throw e; } catch (NotFoundException e) { // deleted externally !? throw new StorageException(e); } } public CellData getCell(CellReference ref) { Assert.notNull(ref); Assert.isTrue(ref.getSheetName() == null || ref.getSheetName().equals(name.getSheetName())); init(); Matrix<CellData> data; try { data = storageService.loadCells(new AreaReference(ref)); } catch (StorageException e) { throw e; } catch (NotFoundException e) { // deleted externally !? throw new StorageException(e); } if (data.getColumnCount() == 0 || data.getRowCount() == 0) { return null; } return data.get(0, 0); } public SheetFullName getFullName() { return name; } public String getName() { return name.getSheetName(); } private List<CellData> moveRow(List<CellData> cells, int fromRow, int toRow) throws FormulaParsingException { List<CellData> newCells = new ArrayList<CellData>(cells.size()); for (int c = 0; c < cells.size(); ++c) { CellData cell = cells.get(c); if (cell.getFormula() == null) { newCells.add(cell); } else { IReferenceTransformer transformer = ReferenceTransformers.shiftCell( new CellReference(name.getSheetName(), fromRow, c), new CellReference(name.getSheetName(), toRow, c)); newCells.add(cell.withFormula(formulaParser.transformFormula(cell.getFormula(), transformer))); } } return newCells; } /** * Sort the rows of this sheet using the sort specifier. It will modify the position of the rows within the sheet. * It generates an update modification type for each row, to only update the rows order. The cells should also * update their row indices. * * TODO should have formula's stored in a relative way. Thus no modification is needed to be done to formulas when * sorting. * * @param sortSpecifier * @throws NetxiliaBusinessException * @throws CyclicDependenciesException */ public int sort(SortSpecifier sortSpecifier) throws CyclicDependenciesException, NetxiliaBusinessException { init(); Matrix<CellData> cells = getCells(AreaReference.ALL); List<RowDataHolder> sortedRows = new ArrayList<RowDataHolder>(cells.getRowCount()); for (int i = 0; i < cells.getRowCount(); ++i) { sortedRows.add(new RowDataHolder(cells, i)); } List<RowData> rowData = getRows(Range.ALL); Collections.sort(sortedRows, new RowDataHolderComparator(sortSpecifier)); int changes = 0; for (int i = 0; i < sortedRows.size(); ++i) { RowDataHolder row = sortedRows.get(i); if (i == row.getIndex()) { // nothing changed continue; } // replace the content of the new row RowData prevRowData = rowData.get(row.getIndex()); sendCommand(RowCommands.row(Range.range(i), prevRowData.getHeight(), prevRowData.getStyles())); sendCommand(CellCommands.row(new AreaReference(name.getSheetName(), i, 0, i, cells.getColumnCount() - 1), moveRow(cells.getRow(row.getIndex()), row.getIndex(), i))); changes++; } return changes; } /** * reorder the sheet rows using the given list of changes. In each pair the first is the index of the row to be * moved and the second is the position where the row should arrive * * @param rowOrders */ public void reorder(Collection<Pair<Integer, Integer>> rowOrders) { Assert.notNull(rowOrders); } public void addListener(ISheetEventListener listener) { eventSupport.removeListener(listener); eventSupport.addListener(listener); } public void removeListener(ISheetEventListener listener) { eventSupport.removeListener(listener); } public ColumnData getColumn(int colIndex) { init(); List<ColumnData> result = getColumns(Range.range(colIndex)); return result.size() > 0 ? result.get(0) : null; } public List<ColumnData> getColumns(Range range) { init(); try { return storageService.loadColumns(range); } catch (StorageException e) { throw e; } catch (NotFoundException e) { throw new StorageException(e); } } public RowData getRow(int rowIndex) { init(); List<RowData> result = getRows(Range.range(rowIndex)); return result.size() > 0 ? result.get(0) : null; } public List<RowData> getRows(Range range) { init(); try { return storageService.loadRows(range); } catch (StorageException e) { throw e; } catch (NotFoundException e) { throw new StorageException(e); } } public void setValue(CellReference ref, IGenericValue value) throws NetxiliaBusinessException, CyclicDependenciesException { Assert.notNull(ref); init(); sendCommand(CellCommands.value(new AreaReference(ref), value)); } public void setFormula(CellReference ref, Formula formula) throws NetxiliaBusinessException, CyclicDependenciesException { Assert.notNull(ref); init(); sendCommand(CellCommands.formula(new AreaReference(ref), formula)); } private void saveCell(CellDataWithProperties dataWithProperties, ICellCommand command) throws StorageException, NotFoundException { if (dataWithProperties.getProperties().isEmpty()) { return; } saveCells(Collections.singletonList(dataWithProperties), command); } private void saveCells(List<CellDataWithProperties> saveCells, ICellCommand command) throws StorageException, NotFoundException { storageService.saveCells(saveCells); for (CellDataWithProperties saveCell : saveCells) { sendEvents(saveCell, command); command.done(myReference, saveCell); } } /** * saves the cells and send the event * * @param newCell * @param properties * @param eventToSend * @throws StorageException * @throws NotFoundException */ private void sendEvents(CellDataWithProperties dataWithProperties, ICellCommand command) throws StorageException, NotFoundException { if (!command.isStopPropagation() && refreshEnabled) { workbook.getDependencyManager().getManagerForSheet(name.getSheetName()) .propagateCellValue(dataWithProperties.getCellData(), dataWithProperties.getProperties()); } if (eventSupport.hasListeners()) { eventSupport.fireEvent(new CellEvent(CellEventType.modified, getFullName(), dataWithProperties .getCellData(), dataWithProperties.getProperties())); } } /** * This method will launch a formula calculator for the given cell * * @param oldCell * @param newCellWithProps * @param command * @throws FormulaParsingException * @throws CyclicDependenciesException * @throws StorageException * @throws NotFoundException */ private void calculateFormula(final MutableFutureWithCounter<ICellCommand> result, final CellData oldCell, final CellData newCell, final ICellCommand command) throws FormulaParsingException, CyclicDependenciesException, StorageException, NotFoundException { // check formula and set dependencies before final IPreloadedFormulaContext context = preloadedContextFactory.newPreloadAliasesContext(myReference, newCell.getReference(), newCell.getFormula(), myReference.getExecutor()); context.load(new Runnable() { @Override public void run() { try { workbook.getDependencyManager().setDependencies(newCell.getFormula(), context); workbook.getAliasDependencyManager().setAliasDependencies(newCell.getFormula(), context); // start actor IFormulaCalculator calculator = formulaCalculatorFactory.getCalculator(myReference, newCell); calculator.sendCalculate().addListener( new FutureListenerWithUser<CellData>(userService, new IFutureListener<CellData>() { @Override public void ready(Future<CellData> future) { CellData data; CellDataWithProperties newCellWithProperties = null; try { data = future.get(); Collection<CellData.Property> properties = CellData.diff(oldCell, data); newCellWithProperties = new CellDataWithProperties(data, properties); saveCell(newCellWithProperties, command); result.decrement(); } catch (Exception e) { if (e.getCause() instanceof AbandonedCalculationException) { log.info("Abandoned:" + oldCell.getReference()); } else { result.setException(e); } } finally { formulaCalculatorFactory.removeCalculator(myReference, newCell.getReference()); } } }), myReference.getExecutor()); } catch (Exception e) { result.setException(e); return; } } }); } /** * Send the given command to storage and listeners * * @param command * @return * @throws NetxiliaBusinessException * @throws CyclicDependenciesException */ public IListenableFuture<ICellCommand> sendCommandNoUndo(ICellCommand command) throws CyclicDependenciesException, NetxiliaBusinessException { Assert.notNull(command); return sendCommand(command, false); } public IListenableFuture<ICellCommand> sendCommand(ICellCommand command) throws NetxiliaBusinessException, CyclicDependenciesException { return sendCommand(command, true); } private IListenableFuture<ICellCommand> sendCommand(ICellCommand command, boolean withUndo) throws NetxiliaBusinessException, CyclicDependenciesException { Assert.notNull(command); init(); Matrix<CellData> cells = null; // special construction to add a new row if (command.getTarget().getFirstRowIndex() == CellReference.LAST_ROW_INDEX) { SheetDimensions dims = storageService.getSheetDimensions(); cells = new MatrixBuilder<CellData>(new CellCreator(name.getSheetName(), dims.getRowCount(), command .getTarget().getFirstColumnIndex())).setSize(1, command.getTarget().getColumnCount()).build(); } else { cells = getCells(command.getTarget()); // build a matrix of the needed size if the returned matrix is smaller if (cells.getRowCount() < command.getTarget().getRowCount() || cells.getColumnCount() < command.getTarget().getColumnCount()) { cells = new MatrixBuilder<CellData>(cells, new CellCreator(name.getSheetName(), command.getTarget() .getFirstRowIndex(), command.getTarget().getFirstColumnIndex())).setSize( command.getTarget().getRowCount(), command.getTarget().getColumnCount()).build(); } } BlockCellCommandBuilder commandBuilder = withUndo ? new BlockCellCommandBuilder() : null; List<CellDataWithProperties> saveCells = new ArrayList<CellDataWithProperties>(); MutableFutureWithCounter<ICellCommand> result = new MutableFutureWithCounter<ICellCommand>(cells.size()); for (CellData cell : cells) { CellDataWithProperties newCellWithProps = command.apply(cell); if (newCellWithProps.getProperties().size() == 0) { // nothing changed result.decrement(); continue; } if (newCellWithProps.getProperties().contains(CellData.Property.value) && !newCellWithProps.getProperties().contains(CellData.Property.formula) && newCellWithProps.getCellData().getFormula() != null) { // set formula to null when a value is set EnumSet<CellData.Property> newProps = EnumSet.copyOf(newCellWithProps.getProperties()); newProps.add(CellData.Property.formula); newCellWithProps = new CellDataWithProperties(newCellWithProps.getCellData().withFormula(null), newProps); } if (commandBuilder != null) { commandBuilder.command(CellCommands.properties(new AreaReference(cell.getReference()), cell.getValue(), cell.getStyles(), cell.getFormula(), newCellWithProps.getProperties())); } if (newCellWithProps.getProperties().contains(CellData.Property.formula) && refreshEnabled) { // the formula changed. this needs the formula evaluation before setting the value calculateFormula(result, cell, newCellWithProps.getCellData(), command); continue; } // TODO: the storage may not be able to store one of the cells - should break all for one cell !? saveCells.add(newCellWithProps); result.decrement(); } saveCells(saveCells, command); if (commandBuilder == null || commandBuilder.isEmpty()) { result.set(CellCommands.doNothing(command.getTarget())); } else { result.set(commandBuilder.build()); } return result; } private List<RowData> fillRows(List<RowData> rows, int startIndex, int count) { List<RowData> newRows = new ArrayList<RowData>(rows); for (int i = 0; i < count; ++i) { newRows.add(new RowData(startIndex + i, 0, null)); } return newRows; } private void rowEvent(RowEventType type, RowData row, Collection<RowData.Property> properties) { if (eventSupport.hasListeners()) { eventSupport.fireEvent(new RowEvent(type, getFullName(), row, properties)); } } /** * Send the given command to storage and listeners * * @param command * @return * @throws StorageException * @throws NetxiliaBusinessException */ public IRowCommand sendCommand(IRowCommand command) throws StorageException, NetxiliaBusinessException { Assert.notNull(command); init(); if (command.toInsert()) { // these are inserted rows List<RowData> rows = fillRows(Collections.<RowData> emptyList(), command.getTarget().getMin(), command .getTarget().count()); for (RowData row : rows) { RowData newRow = command.apply(row); Collection<RowData.Property> properties = RowData.diff(row, newRow); if (newRow != null) { storageService.insertRow(newRow, properties); workbook.getDependencyManager().getManagerForSheet(name.getSheetName()) .insertRow(newRow.getIndex()); workbook.getAliasDependencyManager().getManagerForSheet(name.getSheetName()) .insertRow(newRow.getIndex()); rowEvent(RowEventType.inserted, newRow, properties); } else {// the command decided not to insert the new row continue; } } return null; } // modified or deleted rows List<RowData> rows = getRows(command.getTarget()); if (!command.getTarget().equals(Range.ALL) && command.getTarget().count() != rows.size()) { // add needed rows int startInsertedRow = command.getTarget().getMin() + rows.size(); rows = fillRows(rows, startInsertedRow, command.getTarget().count() - rows.size()); } for (RowData row : rows) { RowData newRow = command.apply(row); Collection<RowData.Property> properties = RowData.diff(row, newRow); if (properties.size() == 0) { // nothing changed continue; } if (newRow == null) { storageService.deleteRow(row.getIndex()); workbook.getDependencyManager().getManagerForSheet(name.getSheetName()).deleteRow(row.getIndex()); workbook.getAliasDependencyManager().getManagerForSheet(name.getSheetName()).deleteRow(row.getIndex()); rowEvent(RowEventType.deleted, row, properties); } else { storageService.saveRow(newRow, properties); rowEvent(RowEventType.modified, newRow, properties); } } return null; } private List<ColumnData> fillColumns(List<ColumnData> columns, int startIndex, int count) { List<ColumnData> newColumns = new ArrayList<ColumnData>(columns); for (int i = 0; i < count; ++i) { newColumns.add(new ColumnData(startIndex + i, 0, null)); } return newColumns; } private void columnEvent(ColumnEventType type, ColumnData column, Collection<ColumnData.Property> properties) { if (eventSupport.hasListeners()) { eventSupport.fireEvent(new ColumnEvent(type, getFullName(), column, properties)); } } /** * Send the given command to storage and listeners * * @param command * @return * @throws StorageException * @throws NetxiliaBusinessException */ public IColumnCommand sendCommand(IColumnCommand command) throws StorageException, NetxiliaBusinessException { Assert.notNull(command); init(); if (command.toInsert()) { // these are inserted rows List<ColumnData> columns = fillColumns(Collections.<ColumnData> emptyList(), command.getTarget().getMin(), command.getTarget().count()); for (ColumnData col : columns) { ColumnData newCol = command.apply(col); Collection<ColumnData.Property> properties = ColumnData.diff(col, newCol); if (newCol != null) { storageService.insertColumn(newCol, properties); workbook.getDependencyManager().getManagerForSheet(name.getSheetName()) .insertColumn(newCol.getIndex()); workbook.getAliasDependencyManager().getManagerForSheet(name.getSheetName()) .insertColumn(newCol.getIndex()); columnEvent(ColumnEventType.inserted, newCol, properties); } else {// the command decided not to insert the new row continue; } } return null; } List<ColumnData> columns = getColumns(command.getTarget()); if (!command.getTarget().equals(Range.ALL) && command.getTarget().count() != columns.size()) { // add needed columns int startInsertedColumn = command.getTarget().getMin() + columns.size(); columns = fillColumns(columns, startInsertedColumn, command.getTarget().count() - columns.size()); } for (ColumnData col : columns) { ColumnData newCol = command.apply(col); Collection<ColumnData.Property> properties = ColumnData.diff(col, newCol); if (properties.size() == 0) { // nothing changed continue; } if (newCol == null) { storageService.deleteColumn(col.getIndex()); workbook.getDependencyManager().getManagerForSheet(name.getSheetName()).deleteColumn(col.getIndex()); workbook.getAliasDependencyManager().getManagerForSheet(name.getSheetName()) .deleteColumn(col.getIndex()); columnEvent(ColumnEventType.deleted, col, properties); } else { storageService.saveColumn(newCol, properties); columnEvent(ColumnEventType.modified, newCol, properties); } } return null; } public ISheetCommand sendCommand(ISheetCommand command) throws StorageException, NotFoundException { Assert.notNull(command); init(); SheetData sheet = getSheet(); SheetData newSheet = command.apply(sheet); Collection<SheetData.Property> properties = SheetData.diff(sheet, newSheet); if (properties.size() == 0) { // nothing changed return null; } storageService.saveSheet(newSheet, properties); workbook.getAliasDependencyManager().getManagerForSheet(name.getSheetName()).saveSheet(newSheet, properties); eventSupport.fireEvent(new SheetEvent(SheetEventType.modified, newSheet, properties)); return null; } /** * this class holds the row index while the cells are sorted. * * @author <a href='mailto:ax.craciun@gmail.com'>Alexandru Craciun</a> * */ private class RowDataHolder { private final Matrix<CellData> cells; private final int index; public RowDataHolder(Matrix<CellData> cells, int index) { this.cells = cells; this.index = index; } public Matrix<CellData> getCells() { return cells; } public int getIndex() { return index; } } /** * compares rows according to the value in a given column * * @author <a href='mailto:ax.craciun@gmail.com'>Alexandru Craciun</a> * */ public class RowDataHolderComparator implements Comparator<RowDataHolder> { private final SortSpecifier sortSpecifier; public RowDataHolderComparator(SortSpecifier sortSpecifier) { this.sortSpecifier = sortSpecifier; } public SortSpecifier getSortSpecifier() { return sortSpecifier; } private int checkNulls(Object obj1, Object obj2) { if (obj1 == null && obj2 == null) { return 0; } if (obj1 == null) { return -1; } if (obj2 == null) { return 1; } return 2; } @Override public int compare(RowDataHolder row1, RowDataHolder row2) { int result = 0; for (SortColumn sortColumn : sortSpecifier.getColumns()) { int columnIndex = CellReference.columnIndex(sortColumn.getName()); IGenericValue cell1 = row1.getCells().get(row1.getIndex(), columnIndex).getValue(); IGenericValue cell2 = row2.getCells().get(row2.getIndex(), columnIndex).getValue(); result = checkNulls(cell1, cell2); if (result != 2) { return sortColumn.getOrder() == SortSpecifier.SortOrder.ascending ? result : -result; } result = checkNulls(cell1, cell2); if (result != 2) { return sortColumn.getOrder() == SortSpecifier.SortOrder.ascending ? result : -result; } result = cell1.compareTo(cell2); if (result != 0) { return sortColumn.getOrder() == SortSpecifier.SortOrder.ascending ? result : -result; } } return 0; } } public void setRefreshEnabled(boolean enabled) { refreshEnabled = enabled; } }