/******************************************************************************* * * 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.spi.impl.storage.cache; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import org.apache.log4j.Logger; import org.netxilia.api.exception.NotFoundException; import org.netxilia.api.exception.StorageException; 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.RowData; import org.netxilia.api.model.SheetData; import org.netxilia.api.model.SheetData.Property; import org.netxilia.api.model.SheetFullName; import org.netxilia.api.model.SheetType; import org.netxilia.api.reference.AreaReference; import org.netxilia.api.reference.CellReference; import org.netxilia.api.reference.Range; import org.netxilia.api.utils.Matrix; import org.netxilia.api.utils.MatrixBuilder; import org.netxilia.spi.impl.storage.db.CellsMapper; import org.netxilia.spi.impl.storage.db.ColumnsMapper; import org.netxilia.spi.impl.storage.db.DbColumnStorageInfo; import org.netxilia.spi.impl.storage.db.DbRowStorageInfo; import org.netxilia.spi.impl.storage.db.DbSheetStorageInfo; import org.netxilia.spi.impl.storage.db.DbSheetStorageServiceImpl; import org.netxilia.spi.impl.storage.db.DbWorkbookStorageServiceImpl; import org.netxilia.spi.impl.storage.db.RowsMapper; import org.netxilia.spi.impl.storage.db.SheetDbSession; import org.netxilia.spi.impl.storage.db.SheetsMapper; import org.netxilia.spi.impl.storage.db.SparseMatrixCollection; import org.netxilia.spi.impl.storage.db.SparseMatrixMapper; /** * This is the entry for a sheet in the storage service cache. * * @author <a href='mailto:ax.craciun@gmail.com'>Alexandru Craciun</a> * */ public class CachedDbSheetStorageServiceImpl extends DbSheetStorageServiceImpl { private final static Logger log = Logger.getLogger(CachedDbSheetStorageServiceImpl.class); /** * this a collection of sparse matrices one for each property of a cell (other than value) */ private SparseMatrixCollection cellsStorage = null; private volatile Integer maxRowId = null; private volatile SheetData sheetData; private volatile DbSheetStorageInfo sheetStorage; private List<ColumnData> columns; private List<DbColumnStorageInfo> storageColumns; private List<RowData> rows; private List<DbRowStorageInfo> storageRows; private List<List<CellData>> cells; public CachedDbSheetStorageServiceImpl(DbWorkbookStorageServiceImpl workbookService, SheetFullName sheeetName, SheetsMapper sheetsMapper, RowsMapper rowsMapper, ColumnsMapper columnsMapper, CellsMapper cellsMapper, SparseMatrixMapper matrixMapper) { super(workbookService, sheeetName, sheetsMapper, rowsMapper, columnsMapper, cellsMapper, matrixMapper); } public void remove() { } @Override public void deleteSheet() throws StorageException, NotFoundException { super.deleteSheet(); } @Override public SheetData loadSheet() throws StorageException, NotFoundException { if (sheetData == null) { sheetData = super.loadSheet(); } return sheetData; } @Override public void saveSheet(SheetData sheet, Collection<Property> properties) throws StorageException, NotFoundException { super.saveSheet(sheet, properties); sheetData = sheet; } private void loadAllColumns(SheetDbSession data) throws StorageException, NotFoundException { if (columns == null || columns.size() != getColumnCount(data)) { columns = new ArrayList<ColumnData>(super.loadColumns(Range.ALL)); } } private void loadAllStorageColumns(SheetDbSession data) throws StorageException, NotFoundException { if (storageColumns == null) { storageColumns = new ArrayList<DbColumnStorageInfo>(super.loadColumnsStorageInfo(data, Range.ALL)); } } @Override public void saveColumn(ColumnData column, Collection<ColumnData.Property> properties) throws StorageException, NotFoundException { super.saveColumn(column, properties); if (columns != null) { if (column.getIndex() < columns.size()) { columns.set(column.getIndex(), column); } } } private void resetColumnIndexes(int startColumn) { // decrement next columns for (int i = startColumn; i < columns.size(); ++i) { ColumnData data = columns.get(i); columns.set(i, new ColumnData(i, data.getWidth(), data.getStyles())); } } @Override public void insertColumn(ColumnData column, Collection<ColumnData.Property> properties) throws NotFoundException { super.insertColumn(column, properties); if (columns != null) { if (column.getIndex() < columns.size()) { columns.add(column.getIndex(), column); resetColumnIndexes(column.getIndex() + 1); } } // increment next columns shiftCellsColumns(column.getIndex(), 1); } @Override public void deleteColumn(int column) throws StorageException, NotFoundException { super.deleteColumn(column); if (columns != null) { if (column < columns.size()) { columns.remove(column); resetColumnIndexes(column); } } // decrement next columns shiftCellsColumns(column, -1); } @Override public List<ColumnData> loadColumns(Range columnsRange) throws StorageException, NotFoundException { SheetDbSession session = newDbSession(); try { loadAllColumns(session); } finally { session.close(); } if (Range.ALL.equals(columnsRange)) { return columns; } Range realRange = columnsRange.bind(0, columns.size()); return columns.subList(realRange.getMin(), realRange.getMax()); } @Override public int getColumnCount(SheetDbSession data) throws NotFoundException { loadAllStorageColumns(data); return storageColumns.size(); } @Override public DbSheetStorageInfo getSheetStorage(SheetDbSession data) throws NotFoundException { if (sheetStorage == null) { sheetStorage = super.getSheetStorage(data); } return sheetStorage; } @Override public DbSheetStorageInfo getOrCreateSheetStorage(SheetDbSession data, SheetType type) { if (sheetStorage == null) { sheetStorage = super.getOrCreateSheetStorage(data, type); } return sheetStorage; } @Override public DbColumnStorageInfo insertColumnStorage(SheetDbSession data, int column) throws NotFoundException { DbColumnStorageInfo s = super.insertColumnStorage(data, column); if (storageColumns != null) { storageColumns.add(column, s); } return s; } @Override public void deleteColumnStorage(SheetDbSession data, int column) throws NotFoundException { super.deleteColumnStorage(data, column); if (storageColumns != null) { storageColumns.remove(column); } } @Override public List<DbColumnStorageInfo> loadColumnsStorageInfo(SheetDbSession data, Range columnsRange) throws NotFoundException { loadAllStorageColumns(data); if (Range.ALL.equals(columnsRange)) { return storageColumns; } Range realRange = columnsRange.bind(0, storageColumns.size()); return storageColumns.subList(realRange.getMin(), realRange.getMax()); } @Override public DbColumnStorageInfo getColumnStorage(SheetDbSession data, int column) throws NotFoundException { if (storageColumns != null) { return storageColumns.get(column); } return super.getColumnStorage(data, column); } @Override public List<DbColumnStorageInfo> createColumnStorage(SheetDbSession data, int maxColumn) throws NotFoundException { loadAllStorageColumns(data); if (maxColumn >= storageColumns.size()) { super.createColumnStorage(data, maxColumn); storageColumns = null; loadAllStorageColumns(data); } return storageColumns; } /*************************** * ROWS * * @param session * * @param b * * @throws NotFoundException * @throws StorageException *********************/ private List<RowData> loadCachedRows(SheetDbSession session, Range rowsRange) throws StorageException, NotFoundException { // for the moment load all if (rows == null) { rows = new ArrayList<RowData>(super.loadRows(Range.ALL)); } else if (rows.size() != getRowCount(session)) { // rows added when saving cells rows.addAll(super.loadRows(Range.range(rows.size(), getRowCount(session)))); } if (Range.ALL.equals(rowsRange)) { return rows; } if (rowsRange.getMin() > rows.size()) { throw new NotFoundException("No row index " + rowsRange + " for sheet " + getSheetName()); } Range realRowsRange = rowsRange.bind(0, rows.size()); return rows.subList(realRowsRange.getMin(), realRowsRange.getMax()); } private List<DbRowStorageInfo> loadCachedStorageRows(SheetDbSession session, Range rowsRange) throws StorageException, NotFoundException { // for the moment load all if (storageRows == null) { storageRows = new ArrayList<DbRowStorageInfo>(super.loadRowsStorageInfo(session, Range.ALL)); } if (Range.ALL.equals(rowsRange)) { return storageRows; } if (rowsRange.getMin() > storageRows.size()) { throw new NotFoundException("No row index " + rowsRange + " for sheet " + getSheetName()); } Range realRowsRange = rowsRange.bind(0, storageRows.size()); return storageRows.subList(realRowsRange.getMin(), realRowsRange.getMax()); } @Override public List<RowData> loadRows(Range rowsRange) throws StorageException, NotFoundException { SheetDbSession session = newDbSession(); try { return loadCachedRows(session, rowsRange); } finally { session.close(); } } @Override public void saveRow(RowData row, Collection<RowData.Property> properties) throws StorageException, NotFoundException { super.saveRow(row, properties); if (rows != null) { if (row.getIndex() < rows.size()) { rows.set(row.getIndex(), row); } // the rows will be completely loaded if the array size changed } } private void resetRowIndexes(int startRow) { for (int i = startRow; i < rows.size(); ++i) { RowData data = rows.get(i); rows.set(i, new RowData(i, data.getHeight(), data.getStyles())); } } @Override public void deleteRow(int row) throws StorageException, NotFoundException { if (rows != null) { rows.remove(row); resetRowIndexes(row); } shiftCellsRows(row, -1); super.deleteRow(row); } @Override public void insertRow(RowData row, Collection<RowData.Property> properties) throws StorageException, NotFoundException { if (rows != null) { rows.add(row.getIndex(), row); resetRowIndexes(row.getIndex() + 1); } shiftCellsRows(row.getIndex(), 1); super.insertRow(row, properties); } @Override public DbRowStorageInfo getRowStorage(SheetDbSession data, int row) throws NotFoundException { List<DbRowStorageInfo> cachedRows = loadCachedStorageRows(data, Range.range(row)); return cachedRows.get(0); } @Override protected DbRowStorageInfo createRowStorage(SheetDbSession session, int row) throws NotFoundException { return super.createRowStorage(session, row); } @Override public DbRowStorageInfo getOrCreateRowStorage(SheetDbSession data, int row) throws NotFoundException { try { List<DbRowStorageInfo> cachedRows = loadCachedStorageRows(data, Range.range(row)); if (cachedRows.size() > 0) { return cachedRows.get(0); } } catch (NotFoundException ex) { // rows need to be created } DbRowStorageInfo storage = createRowStorage(data, row); if (storageRows != null) { if (row > storageRows.size()) { storageRows.addAll(super.loadRowsStorageInfo(data, Range.range(storageRows.size(), row))); } if (row == storageRows.size()) { storageRows.add(storage); } else { storageRows.set(row, storage); } } maxRowId = storage.getId(); return storage; } @Override public DbRowStorageInfo insertRowStorage(SheetDbSession data, int row) throws NotFoundException { DbRowStorageInfo storage = super.insertRowStorage(data, row); if (storageRows != null) { storageRows.add(row, storage); } maxRowId = storage.getId(); return storage; } @Override public void deleteRowStorage(SheetDbSession data, int row) throws NotFoundException { super.deleteRowStorage(data, row); if (storageRows != null) { storageRows.remove(row); } } @Override public List<DbRowStorageInfo> loadRowsStorageInfo(SheetDbSession data, Range rowsRange) throws NotFoundException { return loadCachedStorageRows(data, rowsRange); } @Override public Integer getMaxRowId(SheetDbSession data) throws NotFoundException { if (maxRowId == null) { maxRowId = super.getMaxRowId(data); return maxRowId; } return maxRowId; } @Override public int getRowCount(SheetDbSession data) throws NotFoundException { // TODO: temporary loadCachedStorageRows(data, Range.ALL); return storageRows.size(); } /************** CELLS ***************/ @Override public SparseMatrixCollection getCellsStorage(SheetDbSession data) throws NotFoundException { if (cellsStorage == null) { cellsStorage = super.getCellsStorage(data); } return cellsStorage; } private List<List<CellData>> loadCachedCells(Range rowsRange) throws StorageException, NotFoundException { // TODO for the moment load all, but load only needed cells if (cells == null) { Matrix<CellData> allCells = super.loadCells(AreaReference.ALL); cells = new ArrayList<List<CellData>>(allCells.getRowCount()); for (List<CellData> row : allCells.getRows()) { cells.add(new ArrayList<CellData>(row)); } } if (Range.ALL.equals(rowsRange)) { return cells; } if (rowsRange.getMin() >= cells.size()) { return Collections.emptyList(); } // range should already by bound to the size return cells.subList(rowsRange.getMin(), rowsRange.getMax()); } private void resetReferences(int startRow, int startColumn) { for (int r = startRow; r < cells.size(); ++r) { List<CellData> rowCells = cells.get(r); for (int c = startColumn; c < rowCells.size(); ++c) { CellData crt = rowCells.get(c); rowCells.set(c, new CellData(new CellReference(getSheetName().getSheetName(), r, c), crt.getValue(), crt.getFormula(), crt.getStyles())); } } } private void shiftCellsColumns(int startColumn, int diff) { if (cells != null) { if (diff > 0 && startColumn >= cells.size()) { setCellsMinSize(cells.size(), startColumn + 1); } for (int r = 0; r < cells.size(); ++r) { List<CellData> rowCells = cells.get(r); // add / remove column if (diff < 0) { rowCells.remove(startColumn); } else if (startColumn < rowCells.size()) { rowCells.add(startColumn, emptyCell(r, startColumn)); } } resetReferences(0, startColumn); } } private void shiftCellsRows(int startRow, int diff) { if (cells != null) { int crtRowCount = cells.size(); int crtColumnCount = crtRowCount > 0 ? cells.get(0).size() : 0; // add / remove row if (diff < 0) { cells.remove(startRow); } else if (startRow < cells.size()) { cells.add(startRow, emptyRow(startRow, crtColumnCount)); } else { setCellsMinSize(startRow + 1, crtColumnCount); } resetReferences(startRow, 0); } } private CellData emptyCell(int row, int column) { return new CellData(new CellReference(getSheetName().getSheetName(), row, column)); } private List<CellData> emptyRow(int rowIndex, int columnCount) { List<CellData> row = new ArrayList<CellData>(columnCount); for (int c = 0; c < columnCount; ++c) { row.add(emptyCell(rowIndex, c)); } return row; } private void setCellsMinSize(int minRows, int minCols) { int crtRowCount = cells.size(); int crtColumnCount = crtRowCount > 0 ? cells.get(0).size() : 0; if (minCols > crtColumnCount) { // add the columns for (int r = 0; r < cells.size(); ++r) { for (int c = crtColumnCount; c < minCols; ++c) { cells.get(r).add(emptyCell(r, c)); } } } // add the rows for (int r = cells.size(); r < minRows; ++r) { cells.add(emptyRow(r, minCols)); } } @Override public void saveCells(Collection<CellDataWithProperties> saveCells) throws StorageException, NotFoundException { super.saveCells(saveCells); if (cells != null) { SheetDbSession session = newDbSession(); try { int newRows = cells.size(); int newCols = newRows > 0 ? cells.get(0).size() : 0; for (CellDataWithProperties saveCell : saveCells) { CellData cell = saveCell.getCellData(); newRows = Math.max(cell.getReference().getRowIndex() + 1, newRows); newCols = Math.max(cell.getReference().getColumnIndex() + 1, newCols); } setCellsMinSize(newRows, newCols); for (CellDataWithProperties saveCell : saveCells) { CellData cell = saveCell.getCellData(); List<CellData> row = cells.get(cell.getReference().getRowIndex()); row.set(cell.getReference().getColumnIndex(), cell); } } finally { session.close(); } } } @Override public Matrix<CellData> loadCells(AreaReference fullArea) throws StorageException, NotFoundException { SheetDbSession session = newDbSession(); try { int rowCount = getRowCount(session); int colCount = getColumnCount(session); if (fullArea.getFirstRowIndex() >= rowCount || fullArea.getFirstColumnIndex() >= colCount) { return new Matrix<CellData>(); } AreaReference area = fullArea.bind(rowCount, colCount); List<List<CellData>> cachedRows = loadCachedCells(area.getRows()); if (cachedRows.size() == 0) { return new Matrix<CellData>(); } MatrixBuilder<CellData> builder = new MatrixBuilder<CellData>(new CellCreator( getSheetName().getSheetName(), area.getFirstRowIndex(), area.getFirstColumnIndex())); for (int r = 0; r < cachedRows.size(); ++r) { for (int c = area.getFirstColumnIndex(); c <= area.getLastColumnIndex() && c < cachedRows.get(r).size(); ++c) { builder.set(r, c - area.getFirstColumnIndex(), cachedRows.get(r).get(c)); } } return builder.build(); } catch (Exception ex) { log.error("Exception on " + super.getSheetName() + " ! " + fullArea + ": " + ex + " dims:" + getSheetDimensions(), ex); throw new StorageException(ex); } finally { session.close(); } } }