/******************************************************************************* * * 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.server.rest.html.sheet.impl; import java.security.AccessControlException; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import org.netxilia.api.INetxiliaSystem; import org.netxilia.api.display.IStyleService; import org.netxilia.api.exception.AlreadyExistsException; import org.netxilia.api.exception.NetxiliaBusinessException; import org.netxilia.api.exception.NotFoundException; import org.netxilia.api.exception.StorageException; import org.netxilia.api.formula.Formula; import org.netxilia.api.model.Alias; import org.netxilia.api.model.CellData; import org.netxilia.api.model.ColumnData; import org.netxilia.api.model.ISheet; 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.SpanTable; import org.netxilia.api.operation.ISheetOperations; import org.netxilia.api.reference.AreaReference; import org.netxilia.api.reference.CellReference; import org.netxilia.api.reference.Range; import org.netxilia.api.storage.IJsonSerializer; import org.netxilia.api.user.IAclService; import org.netxilia.api.user.Permission; import org.netxilia.api.utils.CollectionUtils; import org.netxilia.api.utils.Matrix; import org.netxilia.api.value.GenericValueType; import org.netxilia.api.value.RichValue; import org.netxilia.server.rest.html.sheet.CellModel; import org.netxilia.server.rest.html.sheet.ColumnModel; import org.netxilia.server.rest.html.sheet.ISheetModelService; import org.netxilia.server.rest.html.sheet.RowModel; import org.netxilia.server.rest.html.sheet.SheetModel; import org.netxilia.server.util.ILazyElementResolver; import org.netxilia.server.util.LazyArrayList; import org.springframework.beans.factory.annotation.Autowired; public class SheetModelServiceImpl implements ISheetModelService { // private static org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(SheetModelServiceImpl.class); @Autowired private INetxiliaSystem workbookProcessor; @Autowired private IStyleService styleService; @Autowired private IJsonSerializer jsonSerializer; @Autowired private ISheetOperations sheetOperations; @Autowired private IAclService aclService; // @Autowired // private IStyleService styleService; private int mainExtraRows = 50; private int mainExtraCols = 20; private int privateExtraRows = 15; private int privateExtraCols = 6; private int summaryExtraRows = 15; private int summaryExtraCols = 20; private int defaultColumnWidth = 80; private int defaultRowHeight = 20; private int pageSize = 120; private String emptyCellContent = "<td></td>"; private String skipCellContent = ""; private int getExtraRows(SheetType type) { switch (type) { case normal: return mainExtraRows; case summary: return summaryExtraRows; case user: return privateExtraRows; } return 0; } private int getExtraCols(SheetType type) { switch (type) { case normal: return mainExtraCols; case summary: return summaryExtraCols; case user: return privateExtraCols; } return 0; } public int getPageSize() { return pageSize; } public void setPageSize(int pageSize) { this.pageSize = pageSize; } public int getMainExtraRows() { return mainExtraRows; } public void setMainExtraRows(int mainExtraRows) { this.mainExtraRows = mainExtraRows; } public int getMainExtraCols() { return mainExtraCols; } public void setMainExtraCols(int mainExtraCols) { this.mainExtraCols = mainExtraCols; } public int getPrivateExtraRows() { return privateExtraRows; } public void setPrivateExtraRows(int privateExtraRows) { this.privateExtraRows = privateExtraRows; } public int getPrivateExtraCols() { return privateExtraCols; } public void setPrivateExtraCols(int privateExtraCols) { this.privateExtraCols = privateExtraCols; } public int getSummaryExtraRows() { return summaryExtraRows; } public void setSummaryExtraRows(int summaryExtraRows) { this.summaryExtraRows = summaryExtraRows; } public int getSummaryExtraCols() { return summaryExtraCols; } public void setSummaryExtraCols(int summaryExtraCols) { this.summaryExtraCols = summaryExtraCols; } public int getDefaultColumnWidth() { return defaultColumnWidth; } public void setDefaultColumnWidth(int defaultColumnWidth) { this.defaultColumnWidth = defaultColumnWidth; } public int getDefaultRowHeight() { return defaultRowHeight; } public void setDefaultRowHeight(int defaultRowHeight) { this.defaultRowHeight = defaultRowHeight; } public IJsonSerializer getJsonSerializer() { return jsonSerializer; } public void setJsonSerializer(IJsonSerializer jsonSerializer) { this.jsonSerializer = jsonSerializer; } private Alias findAliasByReference(SheetData sheet, AreaReference ref) { for (Map.Entry<Alias, AreaReference> entry : sheet.getAliases().entrySet()) { if (ref.equals(entry.getValue())) { return entry.getKey(); } } return null; } @Override public SheetModel buildModel(SheetFullName sheet, int start, Formula filter, boolean createSheetIfMissing) throws StorageException, NetxiliaBusinessException { return buildModel(sheet, false, 0, start, filter, createSheetIfMissing); } @Override public SheetModel buildOverviewModel(SheetFullName sheet) throws StorageException, NetxiliaBusinessException { return buildModel(sheet, true, 0, 0, null, false); } @Override public SheetModel buildSummaryModel(SheetFullName summarySheetName, int mainSheetColumnCount, boolean createSheetIfMissing) throws StorageException, NetxiliaBusinessException { return buildModel(summarySheetName, true, mainSheetColumnCount, 0, null, createSheetIfMissing); } private SheetModel buildModel(SheetFullName sheetName, boolean overviewMode, int mainSheetColumnCount, int startRow, Formula filter, boolean createSheetIfMissing) throws StorageException, NetxiliaBusinessException { ISheet sheet = null; try { SheetType type = sheetName.getType(); try { sheet = workbookProcessor.getWorkbook(sheetName.getWorkbookId()).getSheet(sheetName.getSheetName()); } catch (NotFoundException e) { if (createSheetIfMissing) { try { sheet = workbookProcessor.getWorkbook(sheetName.getWorkbookId()).addNewSheet( sheetName.getSheetName(), type); } catch (AlreadyExistsException e1) { // if the sheet was created in the mean time - nothing to do } } else { throw new NotFoundException("Cannot find sheet:" + sheetName); } } boolean readOnly = true; try { aclService.checkPermission(sheet.getFullName(), Permission.write); readOnly = false; } catch (AccessControlException e) { // readonly } SheetDimensions dim = sheet.getDimensions().getNonBlocking(); int totalColumns = dim.getColumnCount() + getExtraCols(type); // synchronize the number of columns from summary and main sheet if (type == SheetType.summary) { totalColumns = mainSheetColumnCount; } SheetData sheetData = sheet.receiveSheet().getNonBlocking(); List<ColumnModel> columns = new LazyArrayList<ColumnModel>(totalColumns, new LazyColumnResolver()); List<ColumnData> columnData = sheet.receiveColumns(Range.ALL).getNonBlocking(); SpanTable spans = new SpanTable(sheetData.getSpans()); for (int c = 0; c < columnData.size(); ++c) { AreaReference fullColumnRef = new AreaReference(new CellReference(null, CellReference.MAX_ROW_INDEX, c)); Alias alias = findAliasByReference(sheetData, fullColumnRef); columns.add(new ColumnModel(columnData.get(c), alias, defaultColumnWidth)); } List<RowModel> rows = Collections.emptyList(); // int totalRows = 0; int firstRow = startRow; int lastRow = Math.min(startRow + dim.getRowCount() + getExtraRows(type), pageSize + startRow); int rowCount = lastRow - firstRow; List<Integer> rowIds = null; if (filter != null) { rowIds = sheetOperations.filter(sheet, filter).getNonBlocking(); if (rowIds.size() > 0) { firstRow = rowIds.get(0); lastRow = rowIds.get(rowIds.size() - 1) + 1; } else { lastRow = firstRow; } rowCount = rowIds.size(); } rows = new LazyArrayList<RowModel>(rowCount, new LazyRowResolver(firstRow, totalColumns)); List<RowData> rowData = sheet.receiveRows(Range.range(firstRow, lastRow)).getNonBlocking(); Matrix<CellData> cellData = sheet.receiveCells( new AreaReference(new CellReference(sheetName.getSheetName(), firstRow, 0), new CellReference( sheetName.getSheetName(), lastRow, totalColumns))).getNonBlocking(); for (Integer rowIndex : rowIds != null ? getRowIdIterable(rowIds) : getRowIdIterable(firstRow, lastRow)) { int relativeRowIndex = rowIndex - firstRow; if (relativeRowIndex >= rowData.size()) { continue; } RowData row = rowData.get(relativeRowIndex); if (row == null) { continue; } List<CellModel> cells = new LazyArrayList<CellModel>(totalColumns, new LazyCellResolver()); for (CellData cell : cellData.getRow(relativeRowIndex)) { CellModel cellModel; int colSpan = cell != null ? spans.getColSpan(cell.getReference()) : 1; int rowSpan = cell != null ? spans.getRowSpan(cell.getReference()) : 1; if (cell == null) { cellModel = new CellModel(emptyCellContent); } else if (colSpan < 0 || rowSpan < 0) { // the cell is part of a merged area cellModel = new CellModel(skipCellContent); } else { ColumnData columnDataForCell = cell.getReference().getColumnIndex() < columnData.size() ? columnData .get(cell.getReference().getColumnIndex()) : null; RichValue formattedValue = styleService.formatCell(sheetName.getWorkbookId(), cell, row, columnDataForCell); StringBuilder cellContent = new StringBuilder(); cellContent.append("<td"); if (cell.getFormula() != null) { cellContent.append(" title='").append(cell.getFormula()).append("'"); } else if (cell.getValue() != null) { // XXX assumes Strings are not formatted which may be wrong. other types may also be // skipped (i.e. BINARY) if (cell.getValue().getValueType() != GenericValueType.STRING) { cellContent.append(" title='").append(cell.getValue().getStringValue()).append("'"); } } if (formattedValue.getStyles() != null && formattedValue.getStyles().toString().length() > 0) { cellContent.append(" class='").append(formattedValue.getStyles()).append("'"); } if (colSpan > 1) { cellContent.append(" colspan='").append(colSpan).append("'"); } if (rowSpan > 1) { cellContent.append(" rowspan='").append(rowSpan).append("'"); } cellContent.append(">"); if (colSpan > 1) { cellContent.append("<div class='merge'>"); cellContent.append(formattedValue.getDisplay()); cellContent.append("</div>"); } else { cellContent.append(formattedValue.getDisplay()); } cellContent.append("</td>"); cellModel = new CellModel(cellContent.toString()); } cells.add(cellModel); } rows.add(new RowModel(row, defaultRowHeight, cells)); } SheetModel model = new SheetModel(jsonSerializer, overviewMode, sheetName, rows, columns, sheetData.getAliases(), sheetData.getCharts(), sheetData.getSpans(), rowCount, pageSize, readOnly); return model; } catch (RuntimeException ex) { ex.printStackTrace(); throw ex; } } /** * Columns resolvers * * @author <a href='mailto:ax.craciun@gmail.com'>Alexandru Craciun</a> * */ private class LazyColumnResolver implements ILazyElementResolver<ColumnModel> { private LazyColumnModel columnModel = new LazyColumnModel(); public ColumnModel get(int index) { columnModel.setIndex(index); return columnModel; } } private class LazyColumnModel extends ColumnModel { private int index; public LazyColumnModel() { super(null, null, defaultColumnWidth); } @Override public int getWidth() { return defaultColumnWidth; } @Override public String getLabel() { return CellReference.columnLabel(index); } public void setIndex(int index) { this.index = index; } } /** * Row resolver. For inexistent rows will return its index shifted with the start row and empty cells * * @author <a href='mailto:ax.craciun@gmail.com'>Alexandru Craciun</a> * */ private class LazyRowResolver implements ILazyElementResolver<RowModel> { private LazyRowModel rowModel; private int startRow; public LazyRowResolver(int startRow, int columnCount) { rowModel = new LazyRowModel(columnCount); this.startRow = startRow; } public RowModel get(int index) { rowModel.setIndex(startRow + index); return rowModel; } } private class LazyRowModel extends RowModel { private int index; public LazyRowModel(int columnCount) { super(null, defaultRowHeight, new LazyArrayList<CellModel>(columnCount, new LazyCellResolver())); } @Override public int getHeight() { return defaultRowHeight; } @Override public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } } /** * CellResolver * * @author <a href='mailto:ax.craciun@gmail.com'>Alexandru Craciun</a> * */ private class LazyCellResolver implements ILazyElementResolver<CellModel> { private CellModel cellModel = new CellModel(emptyCellContent); public CellModel get(int index) { return cellModel; } } private Iterable<Integer> getRowIdIterable(List<Integer> rowIds) { return rowIds; } private Iterable<Integer> getRowIdIterable(final int firstRow, final int lastRow) { return CollectionUtils.iterable(new Iterator<Integer>() { private int i = firstRow; @Override public boolean hasNext() { return i < lastRow; } @Override public Integer next() { if (i >= lastRow) { throw new IllegalStateException(); } return i++; } @Override public void remove() { throw new UnsupportedOperationException(); } }); } }