blob: cb4434bfd0d3d30e0731eb45c4a793497a43b614 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2020 Original authors and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Original authors and others - initial API and implementation
* Dirk Fauth <dirk.fauth@googlemail.com> - Bug 457304
******************************************************************************/
package org.eclipse.nebula.widgets.nattable.extension.poi;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.ClientAnchor;
import org.apache.poi.ss.usermodel.CreationHelper;
import org.apache.poi.ss.usermodel.Drawing;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.Picture;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.util.IOUtils;
import org.eclipse.nebula.widgets.nattable.config.CellConfigAttributes;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.export.ExportConfigAttributes;
import org.eclipse.nebula.widgets.nattable.export.ILayerExporter;
import org.eclipse.nebula.widgets.nattable.export.IOutputStreamProvider;
import org.eclipse.nebula.widgets.nattable.formula.FormulaParser;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.painter.cell.AbstractTextPainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.CellPainterWrapper;
import org.eclipse.nebula.widgets.nattable.painter.cell.ICellPainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.VerticalTextPainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.decorator.CellPainterDecorator;
import org.eclipse.nebula.widgets.nattable.style.CellStyleAttributes;
import org.eclipse.nebula.widgets.nattable.style.CellStyleProxy;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
import org.eclipse.nebula.widgets.nattable.style.HorizontalAlignmentEnum;
import org.eclipse.nebula.widgets.nattable.style.VerticalAlignmentEnum;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.widgets.Shell;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class PoiExcelExporter implements ILayerExporter {
private static final Logger LOG = LoggerFactory.getLogger(PoiExcelExporter.class);
private final IOutputStreamProvider outputStreamProvider;
private Map<ExcelCellStyleAttributes, CellStyle> xlCellStyles;
protected Workbook xlWorkbook;
protected int sheetNumber;
protected Sheet xlSheet;
protected Row xlRow;
private boolean applyBackgroundColor = true;
private boolean applyVerticalTextConfiguration = false;
private boolean applyTextWrapping = false;
private boolean applyColumnWidths = false;
private boolean applyRowHeights = false;
private boolean applyCellBorders = false;
private String sheetname;
protected FormulaParser formulaParser;
protected NumberFormat nf = NumberFormat.getInstance();
protected CreationHelper helper;
protected Drawing<?> drawing;
protected boolean exportOnSameSheet = false;
protected int currentRow = 0;
private Set<Integer> hiddenColumnPositions = new HashSet<>();
public PoiExcelExporter(IOutputStreamProvider outputStreamProvider) {
this.outputStreamProvider = outputStreamProvider;
}
@Override
public OutputStream getOutputStream(Shell shell) {
return this.outputStreamProvider.getOutputStream(shell);
}
@Override
public void exportBegin(OutputStream outputStream) throws IOException {
this.xlCellStyles = new HashMap<>();
this.xlWorkbook = createWorkbook();
// the hidden column positions are determined by inspection so
// it needs to be cleared at the beginning
this.hiddenColumnPositions.clear();
}
@Override
public void exportEnd(OutputStream outputStream) throws IOException {
this.xlWorkbook.write(outputStream);
this.xlCellStyles = null;
this.xlWorkbook = null;
this.sheetNumber = 0;
this.xlSheet = null;
this.xlRow = null;
this.drawing = null;
this.currentRow = 0;
}
@Override
public void exportLayerBegin(OutputStream outputStream, String layerName) throws IOException {
this.sheetNumber++;
if (layerName == null || layerName.length() == 0) {
layerName = this.sheetname;
if (layerName == null || layerName.length() == 0) {
layerName = "Sheet" + this.sheetNumber; //$NON-NLS-1$
}
}
this.xlSheet = this.xlWorkbook.createSheet(layerName);
}
@Override
public void exportLayerEnd(OutputStream outputStream, String layerName) throws IOException {
}
@Override
public void exportRowBegin(OutputStream outputStream, int rowPosition) throws IOException {
int pos = this.exportOnSameSheet ? this.currentRow : rowPosition;
// check if row was already created because of spanning
this.xlRow = this.xlSheet.getRow(pos);
if (this.xlRow == null) {
this.xlRow = this.xlSheet.createRow(pos);
}
this.currentRow++;
}
@Override
public void exportRowEnd(OutputStream outputStream, int rowPosition) throws IOException {
}
@Override
public void exportCell(
OutputStream outputStream,
Object exportDisplayValue,
ILayerCell cell,
IConfigRegistry configRegistry) throws IOException {
// if the width is 0 we do not export the cell as it is not visible to
// the user
if (cell.getLayer().getColumnWidthByPosition(cell.getColumnPosition()) == 0) {
this.hiddenColumnPositions.add(cell.getColumnPosition());
return;
}
int columnPosition = cell.getColumnPosition();
if (!this.hiddenColumnPositions.isEmpty()) {
for (int hidden : this.hiddenColumnPositions) {
if (hidden < cell.getColumnPosition()) {
columnPosition--;
}
}
}
int rowPosition = cell.getRowPosition();
if (this.applyColumnWidths && cell.getColumnSpan() == 1) {
this.xlSheet.setColumnWidth(columnPosition, getPoiColumnWidth(cell.getBounds().width) + getPoiColumnWidth(5));
}
if (this.applyRowHeights && cell.getRowSpan() == 1) {
this.xlRow.setHeight(getPoiRowHeight(cell.getBounds().height));
}
// check if cell was already created because of spanning
Cell xlCell = this.xlRow.getCell(columnPosition);
if (xlCell == null) {
xlCell = this.xlRow.createCell(columnPosition);
} else {
// if the cell was created before because of spanning, there is no
// need to perform further tasks
return;
}
CellStyleProxy cellStyle = new CellStyleProxy(
configRegistry,
DisplayMode.NORMAL,
cell.getConfigLabels());
Color fg = cellStyle.getAttributeValue(CellStyleAttributes.FOREGROUND_COLOR);
Color bg = cellStyle.getAttributeValue(CellStyleAttributes.BACKGROUND_COLOR);
org.eclipse.swt.graphics.Font font = cellStyle.getAttributeValue(CellStyleAttributes.FONT);
FontData fontData = font.getFontData()[0];
String dataFormat = null;
if (exportDisplayValue instanceof Calendar
|| exportDisplayValue instanceof Date) {
dataFormat = getDataFormatString(cell, configRegistry);
}
int hAlign = HorizontalAlignmentEnum.getSWTStyle(cellStyle);
int vAlign = VerticalAlignmentEnum.getSWTStyle(cellStyle);
ICellPainter cellPainter = configRegistry.getConfigAttribute(
CellConfigAttributes.CELL_PAINTER,
DisplayMode.NORMAL,
cell.getConfigLabels());
boolean vertical = this.applyVerticalTextConfiguration && isVertical(cellPainter);
boolean wrap = this.applyTextWrapping && wrapText(cellPainter);
CellStyle xlCellStyle = getExcelCellStyle(fg, bg, fontData, dataFormat, hAlign, vAlign, vertical, wrap, this.applyCellBorders);
xlCell.setCellStyle(xlCellStyle);
int columnSpan = cell.getColumnSpan();
if (columnSpan > 1) {
// check if a column width in the spanned region is 0
// as we do not export columns with a width of 0 we need to adjust
// the spanning
for (int col = cell.getOriginColumnPosition(); col < (cell.getOriginColumnPosition() + cell.getColumnSpan()); col++) {
if (cell.getLayer().getColumnWidthByPosition(col) == 0) {
columnSpan--;
}
}
}
int rowSpan = cell.getRowSpan();
if (columnSpan > 1 || rowSpan > 1) {
int lastRow = rowPosition + rowSpan - 1;
int lastColumn = columnPosition + columnSpan - 1;
this.xlSheet.addMergedRegion(new CellRangeAddress(rowPosition, lastRow, columnPosition, lastColumn));
// apply style to all cells that get merged
Row tempRow = null;
Cell tempCell = null;
for (int i = rowPosition; i <= lastRow; i++) {
tempRow = this.xlSheet.getRow(i);
if (tempRow == null) {
tempRow = this.xlSheet.createRow(i);
}
for (int j = columnPosition; j <= lastColumn; j++) {
tempCell = tempRow.getCell(j);
if (tempCell == null) {
tempCell = tempRow.createCell(j);
}
tempCell.setCellStyle(xlCellStyle);
}
}
}
if (exportDisplayValue == null)
exportDisplayValue = ""; //$NON-NLS-1$
if (exportDisplayValue instanceof Boolean) {
xlCell.setCellValue((Boolean) exportDisplayValue);
} else if (exportDisplayValue instanceof Calendar) {
xlCell.setCellValue((Calendar) exportDisplayValue);
} else if (exportDisplayValue instanceof Date) {
xlCell.setCellValue((Date) exportDisplayValue);
} else if (exportDisplayValue instanceof Number) {
xlCell.setCellValue(((Number) exportDisplayValue).doubleValue());
} else if (exportDisplayValue instanceof InputStream) {
exportImage((InputStream) exportDisplayValue, xlCell);
} else if (this.formulaParser != null) {
// formula export is enabled, so we perform checks on the cell
// values
String cellValue = exportDisplayValue.toString();
if (this.formulaParser.isFunction(cellValue)) {
String functionString = this.formulaParser.getFunctionOnly(cellValue);
// POI expects the formula parameters to be separated by ,
// instead of ;
// also localized decimal separators need to be modified for
// export
functionString = functionString.replace(',', '.');
functionString = functionString.replace(';', ',');
xlCell.setCellFormula(functionString);
} else if (this.formulaParser.isNumber(cellValue)) {
try {
xlCell.setCellValue(this.nf.parse(cellValue).doubleValue());
} catch (ParseException e) {
throw new IOException("Error on parsing number value: " + cellValue, e); //$NON-NLS-1$
}
} else {
xlCell.setCellValue(exportDisplayValue.toString());
}
} else {
xlCell.setCellValue(exportDisplayValue.toString());
}
}
private boolean isVertical(ICellPainter cellPainter) {
if (cellPainter instanceof VerticalTextPainter) {
return true;
} else if (cellPainter instanceof CellPainterWrapper) {
return isVertical(((CellPainterWrapper) cellPainter).getWrappedPainter());
} else if (cellPainter instanceof CellPainterDecorator) {
return (isVertical(((CellPainterDecorator) cellPainter).getBaseCellPainter())
|| isVertical(((CellPainterDecorator) cellPainter).getDecoratorCellPainter()));
}
return false;
}
private boolean wrapText(ICellPainter cellPainter) {
if (cellPainter instanceof AbstractTextPainter) {
return ((AbstractTextPainter) cellPainter).isWordWrapping()
|| ((AbstractTextPainter) cellPainter).isWrapText();
} else if (cellPainter instanceof CellPainterWrapper) {
return wrapText(((CellPainterWrapper) cellPainter).getWrappedPainter());
} else if (cellPainter instanceof CellPainterDecorator) {
return (wrapText(((CellPainterDecorator) cellPainter).getBaseCellPainter())
|| wrapText(((CellPainterDecorator) cellPainter).getDecoratorCellPainter()));
}
return false;
}
private CellStyle getExcelCellStyle(
Color fg, Color bg, FontData fontData,
String dataFormat, int hAlign, int vAlign,
boolean vertical, boolean wrap, boolean border) {
CellStyle xlCellStyle = this.xlCellStyles.get(
new ExcelCellStyleAttributes(fg, bg, fontData, dataFormat, hAlign, vAlign, vertical, wrap, border));
if (xlCellStyle == null) {
xlCellStyle = this.xlWorkbook.createCellStyle();
if (this.applyBackgroundColor) {
// Note: xl fill foreground = background
setFillForegroundColor(xlCellStyle, bg);
xlCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
}
Font xlFont = this.xlWorkbook.createFont();
setFontColor(xlFont, fg);
xlFont.setFontName(fontData.getName());
xlFont.setFontHeightInPoints((short) fontData.getHeight());
xlCellStyle.setFont(xlFont);
if (vertical)
xlCellStyle.setRotation((short) 90);
if (wrap)
xlCellStyle.setWrapText(wrap);
if (border) {
xlCellStyle.setBorderTop(BorderStyle.THIN);
xlCellStyle.setBorderRight(BorderStyle.THIN);
xlCellStyle.setBorderBottom(BorderStyle.THIN);
xlCellStyle.setBorderLeft(BorderStyle.THIN);
}
switch (hAlign) {
case SWT.CENTER:
xlCellStyle.setAlignment(HorizontalAlignment.CENTER);
break;
case SWT.LEFT:
xlCellStyle.setAlignment(HorizontalAlignment.LEFT);
break;
case SWT.RIGHT:
xlCellStyle.setAlignment(HorizontalAlignment.RIGHT);
break;
}
switch (vAlign) {
case SWT.TOP:
xlCellStyle.setVerticalAlignment(VerticalAlignment.TOP);
break;
case SWT.CENTER:
xlCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
break;
case SWT.BOTTOM:
xlCellStyle.setVerticalAlignment(VerticalAlignment.BOTTOM);
break;
}
if (dataFormat != null) {
CreationHelper createHelper = this.xlWorkbook.getCreationHelper();
xlCellStyle.setDataFormat(createHelper.createDataFormat().getFormat(dataFormat));
}
this.xlCellStyles.put(
new ExcelCellStyleAttributes(fg, bg, fontData, dataFormat, hAlign, vAlign, vertical, wrap, border), xlCellStyle);
}
return xlCellStyle;
}
/**
*
* @param cell
* The cell for which the date format needs to be determined.
* @param configRegistry
* The ConfigRegistry needed to retrieve the configuration.
* @return The date format that should be used to format Date or Calendar
* values in the export.
*/
protected String getDataFormatString(ILayerCell cell, IConfigRegistry configRegistry) {
String dataFormat = configRegistry.getConfigAttribute(
ExportConfigAttributes.DATE_FORMAT,
DisplayMode.NORMAL,
cell.getConfigLabels());
if (dataFormat == null) {
dataFormat = "m/d/yy h:mm"; //$NON-NLS-1$
}
return dataFormat;
}
/**
*
* @param applyBackgroundColor
* <code>true</code> to apply the background color set in the
* NatTable to the exported Excel. This also includes white
* background and header background color. <code>false</code> if
* the background color should not be set on export.
*/
public void setApplyBackgroundColor(boolean applyBackgroundColor) {
this.applyBackgroundColor = applyBackgroundColor;
}
/**
* Configure this exporter whether it should check for vertical text
* configuration in NatTable and apply the corresponding rotation style
* attribute in the export, or not.
* <p>
* Note: As showing text vertically in NatTable is not a style information
* but a configured via painter implementation, the check whether text is
* showed vertically needs to be done via reflection. Therefore setting this
* value to <code>true</code> could cause performance issues. As vertical
* text is not the default case and the effect on performance might be
* negative, the default value for this configuration is <code>false</code>.
* If vertical text (e.g. column headers) should also be exported
* vertically, you need to set this value to <code>true</code>.
*
* @param inspectVertical
* <code>true</code> to configure this exporter to check for
* vertical text configuration and apply the rotation style for
* the export, <code>false</code> to always use the regular text
* direction, regardless of vertical rendered text in NatTable.
*/
public void setApplyVerticalTextConfiguration(boolean inspectVertical) {
this.applyVerticalTextConfiguration = inspectVertical;
}
/**
* Configure this exporter whether it should check for text wrapping
* configuration in NatTable and apply the corresponding style attribute in
* the export, or not.
* <p>
* Note: As showing text wrapping in NatTable is not a style information but
* a configured via painter implementation, the check whether text is
* wrapped needs to be done via reflection. Therefore setting this value to
* <code>true</code> could cause performance issues. As wrapped text is not
* the default case and the effect on performance might be negative, the
* default value for this configuration is <code>false</code>. If wrapped
* text (e.g. column headers) should also be exported wrapped, you need to
* set this value to <code>true</code>.
*
* @param inspectTextWrap
* <code>true</code> to configure this exporter to check for text
* wrapping configuration, <code>false</code> to never apply text
* wrapping to the export.
* @since 1.5
*/
public void setApplyTextWrapping(boolean inspectTextWrap) {
this.applyTextWrapping = inspectTextWrap;
}
/**
* Configure this exporter whether it should apply the cell dimensions to
* the same size configuration the NatTable shows.
*
* @param apply
* <code>true</code> to configure this exporter to apply the cell
* dimensions based on the NatTable cell dimensions,
* <code>false</code> if the Excel default cell dimensions should
* be used.
* @since 1.5
*/
public void setApplyCellDimensions(boolean apply) {
this.applyColumnWidths = apply;
this.applyRowHeights = apply;
}
/**
* Configure this exporter whether it should apply the column widths to the
* same size configuration the NatTable shows.
*
* @param apply
* <code>true</code> to configure this exporter to apply the
* column widths based on the NatTable cell dimensions,
* <code>false</code> if the Excel default cell dimensions should
* be used.
* @since 1.5
*/
public void setApplyColumnWidths(boolean apply) {
this.applyColumnWidths = apply;
}
/**
* Configure this exporter whether it should apply the row heights to the
* same size configuration the NatTable shows.
*
* @param apply
* <code>true</code> to configure this exporter to apply the row
* heights based on the NatTable cell dimensions,
* <code>false</code> if the Excel default cell dimensions should
* be used.
* @since 1.5
*/
public void setApplyRowHeights(boolean apply) {
this.applyRowHeights = apply;
}
/**
* Configure this exporter whether it should render cell borders on all
* cells. This should typically be enabled if background colors should be
* applied to make the cell borders visible.
*
* @param apply
* <code>true</code> to configure this exporter to render cell
* borders, <code>false</code> if not.
* @since 1.5
*/
public void setApplyCellBorders(boolean apply) {
this.applyCellBorders = apply;
}
protected abstract Workbook createWorkbook();
protected abstract void setFillForegroundColor(CellStyle xlCellStyle, Color swtColor);
protected abstract void setFontColor(Font xlFont, Color swtColor);
@Override
public Object getResult() {
return this.outputStreamProvider.getResult();
}
/**
*
* @param sheetname
* The name that should be set as sheet name in the resulting
* Excel file. Setting this value to <code>null</code> will
* result in a sheet name following the pattern <i>Sheet +
* &lt;sheet number&gt;</i>
*/
public void setSheetname(String sheetname) {
this.sheetname = sheetname;
}
/**
* Configure the {@link FormulaParser} that should be used to determine
* whether formulas should be exported or not. If <code>null</code> is set,
* formulas and cell values of type string will be simply exported as
* string. If a valid {@link FormulaParser} is set, cell values will get
* inspected so that number values are converted to numbers and formulas
* will be exported as formulas.
*
* @param formulaParser
* The {@link FormulaParser} that should be used to determine
* whether cell values should be interpreted as formulas or
* <code>null</code> to disable formula export handling.
*
* @since 1.4
*/
public void setFormulaParser(FormulaParser formulaParser) {
this.formulaParser = formulaParser;
}
/**
*
* @param nf
* The {@link NumberFormat} that should be used to format numeric
* values.
*/
public void setNumberFormat(NumberFormat nf) {
this.nf = nf;
}
/**
* Adds a picture to the workbook at the given cell position.
*
* @param is
* The {@link InputStream} to access the picture. This will be
* automatically closed after reading.
* @param xlCell
* The {@link Cell} to position the image to.
* @since 1.5
*/
protected void exportImage(InputStream is, Cell xlCell) {
try {
byte[] bytes = IOUtils.toByteArray(is);
int pictureIdx = this.xlWorkbook.addPicture(bytes, Workbook.PICTURE_TYPE_PNG);
if (this.helper == null) {
this.helper = this.xlWorkbook.getCreationHelper();
}
// Create the drawing patriarch.
// This is the top level container for all shapes.
if (this.drawing == null) {
this.drawing = this.xlSheet.createDrawingPatriarch();
}
// add a picture shape
ClientAnchor anchor = this.helper.createClientAnchor();
// set top-left corner of the picture,
// subsequent call of Picture#resize() will operate relative to it
anchor.setCol1(xlCell.getColumnIndex());
anchor.setRow1(xlCell.getRowIndex());
Picture pict = this.drawing.createPicture(anchor, pictureIdx);
// auto-size picture relative to its top-left corner
pict.resize();
} catch (IOException e) {
LOG.error("Error on transforming the image input stream to byte array", e); //$NON-NLS-1$
} finally {
try {
is.close();
} catch (IOException e) {
LOG.error("Error on closing the image input stream", e); //$NON-NLS-1$
}
}
}
/**
*
* @param sameSheet
* <code>true</code> if multiple NatTable instances should be
* exported on the same sheet, <code>false</code> if every
* instance should be exported on separate sheets. Default is
* <code>false</code>.
*
* @since 1.5
*/
@Override
public void setExportOnSameSheet(boolean sameSheet) {
this.exportOnSameSheet = sameSheet;
}
private int getPoiColumnWidth(int pixel) {
return (256 * (pixel / 7)) + 128;
}
private short getPoiRowHeight(int pixel) {
double points = pixel;
points *= 72;
points /= 96;
points *= 21;
return (short) (points + 32);
}
}