diff --git a/morf-excel/pom.xml b/morf-excel/pom.xml old mode 100755 new mode 100644 index 93e353315..96fd223a1 --- a/morf-excel/pom.xml +++ b/morf-excel/pom.xml @@ -40,8 +40,13 @@ morf-core - net.sourceforge.jexcelapi - jxl + org.apache.poi + poi + + + + org.apache.poi + poi-ooxml org.alfasoftware diff --git a/morf-excel/src/main/java/org/alfasoftware/morf/excel/AdditionalSchemaData.java b/morf-excel/src/main/java/org/alfasoftware/morf/excel/AdditionalSchemaData.java old mode 100755 new mode 100644 index a360264ad..eecef5296 --- a/morf-excel/src/main/java/org/alfasoftware/morf/excel/AdditionalSchemaData.java +++ b/morf-excel/src/main/java/org/alfasoftware/morf/excel/AdditionalSchemaData.java @@ -31,7 +31,7 @@ public interface AdditionalSchemaData { * @param columnName The column name. * @return The column documentation. */ - public String columnDocumentation(Table table, String columnName); + String columnDocumentation(Table table, String columnName); /** * Fetches the default value for a column. @@ -40,5 +40,5 @@ public interface AdditionalSchemaData { * @param columnName The column name. * @return The column's default value. */ - public String columnDefaultValue(Table table, String columnName); + String columnDefaultValue(Table table, String columnName); } diff --git a/morf-excel/src/main/java/org/alfasoftware/morf/excel/DefaultAdditionalSchemaDataImpl.java b/morf-excel/src/main/java/org/alfasoftware/morf/excel/DefaultAdditionalSchemaDataImpl.java old mode 100755 new mode 100644 index 577ed68e2..5d36be1d6 --- a/morf-excel/src/main/java/org/alfasoftware/morf/excel/DefaultAdditionalSchemaDataImpl.java +++ b/morf-excel/src/main/java/org/alfasoftware/morf/excel/DefaultAdditionalSchemaDataImpl.java @@ -15,9 +15,8 @@ package org.alfasoftware.morf.excel; -import org.apache.commons.lang3.StringUtils; - import org.alfasoftware.morf.metadata.Table; +import org.apache.commons.lang3.StringUtils; /** * A default implementation of {@link AdditionalSchemaData} which provides blank diff --git a/morf-excel/src/main/java/org/alfasoftware/morf/excel/SpreadsheetDataSetConsumer.java b/morf-excel/src/main/java/org/alfasoftware/morf/excel/SpreadsheetDataSetConsumer.java old mode 100755 new mode 100644 index 67f200297..bf7bf985e --- a/morf-excel/src/main/java/org/alfasoftware/morf/excel/SpreadsheetDataSetConsumer.java +++ b/morf-excel/src/main/java/org/alfasoftware/morf/excel/SpreadsheetDataSetConsumer.java @@ -28,17 +28,17 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - -import jxl.Workbook; -import jxl.format.Alignment; -import jxl.format.Colour; -import jxl.format.UnderlineStyle; -import jxl.write.Label; -import jxl.write.WritableCellFormat; -import jxl.write.WritableFont; -import jxl.write.WritableHyperlink; -import jxl.write.WritableSheet; -import jxl.write.WritableWorkbook; +import org.apache.poi.common.usermodel.HyperlinkType; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.WorkbookUtil; /** * Consumes data sets and outputs spreadsheets. @@ -49,214 +49,178 @@ public class SpreadsheetDataSetConsumer implements DataSetConsumer { private static final Log log = LogFactory.getLog(SpreadsheetDataSetConsumer.class); - /** - * - * The number of rows in the title. - */ private static final int NUMBER_OF_ROWS_IN_TITLE = 2; - - /** - * The number of rows to include in the sample section if not specified. - */ private static final int DEFAULT_SAMPLE_ROWS = 5; - /** - * Stream to which the spreadsheet will be output. - */ private final OutputStream documentOutputStream; - - /** - * The workbook currently under construction. - */ - private WritableWorkbook workbook; - - /** - * Number of rows to output for each table. - */ + private Workbook workbook; + private ConsumerStyles consumerStyles; private final Optional> rowsPerTable; - - - /** - * Outputter for putting tables into Excel. - */ private final TableOutputter tableOutputter; - - /** - * @param documentOutputStream Stream to which the spreadsheet file should be written. - */ public SpreadsheetDataSetConsumer(OutputStream documentOutputStream) { this(documentOutputStream, Optional.>empty()); } - - /** - * @param documentOutputStream Stream to which the spreadsheet file should be written. - * @param rowsPerTable stream The list of tables to export along with the number of rows per table to export. If not supplied, - * all tables are exported to the limit of rows in Excel. - */ - public SpreadsheetDataSetConsumer( - OutputStream documentOutputStream, - Optional> rowsPerTable) { + public SpreadsheetDataSetConsumer(OutputStream documentOutputStream, Optional> rowsPerTable) { this(documentOutputStream, rowsPerTable, new DefaultAdditionalSchemaDataImpl()); } - - /** - * @param documentOutputStream Stream to which the spreadsheet file should be written. - * @param rowsPerTable stream The list of tables to export along with the number of rows per table to export. If not supplied, - * all tables are exported to the limit of rows in Excel. - * @param additionalSchemaData the source of additional metadata not available from the database schema. - */ - public SpreadsheetDataSetConsumer( - OutputStream documentOutputStream, - Optional> rowsPerTable, + public SpreadsheetDataSetConsumer(OutputStream documentOutputStream, Optional> rowsPerTable, AdditionalSchemaData additionalSchemaData) { this(documentOutputStream, rowsPerTable, new TableOutputter(additionalSchemaData)); } - - /** - * Package private constructor, for testing purposes (isolates the {@link TableOutputter} - * dependency). - */ - SpreadsheetDataSetConsumer( - OutputStream documentOutputStream, - Optional> rowsPerTable, + SpreadsheetDataSetConsumer(OutputStream documentOutputStream, Optional> rowsPerTable, TableOutputter tableOutputter) { - super(); this.documentOutputStream = documentOutputStream; this.tableOutputter = tableOutputter; this.rowsPerTable = rowsPerTable; } - - /** - * @see org.alfasoftware.morf.dataset.DataSetConsumer#open() - */ @Override public void open() { - try { - workbook = Workbook.createWorkbook(documentOutputStream); - } catch (IOException e) { - throw new RuntimeException("Error creating writable workbook", e); - } + workbook = new HSSFWorkbook(); + consumerStyles = new ConsumerStyles(workbook); } - - /** - * Create the index worksheet. - * - *

This also creates links back to the index in each of the worksheets.

- */ public void createIndex() { - WritableSheet sheet = workbook.createSheet(spreadsheetifyName("Index"), 0); + Sheet sheet = workbook.createSheet(safeSheetName(spreadsheetifyName("Index"))); + workbook.setSheetOrder(sheet.getSheetName(), 0); createTitle(sheet, "Index"); try { - // Create links for each worksheet, apart from the first sheet which is the - // index we're currently creating - final String[] names = workbook.getSheetNames(); - for (int currentSheet = 1; currentSheet < names.length; currentSheet++) { - // Create the link from the index to the table's worksheet - WritableHyperlink link = new WritableHyperlink(0, currentSheet - 1 + NUMBER_OF_ROWS_IN_TITLE, names[currentSheet], workbook.getSheet(currentSheet), 0, 0); - sheet.addHyperlink(link); - - //Add the filename in column B (stored in cell B2 of each sheet) - String fileName = workbook.getSheet(currentSheet).getCell(1, 1).getContents(); - Label fileNameLabel = new Label(1, currentSheet - 1 + NUMBER_OF_ROWS_IN_TITLE, fileName); - WritableFont fileNameFont = new WritableFont(WritableFont.ARIAL,10,WritableFont.NO_BOLD,false,UnderlineStyle.NO_UNDERLINE,Colour.BLACK); - WritableCellFormat fileNameFormat = new WritableCellFormat(fileNameFont); - fileNameLabel.setCellFormat(fileNameFormat); - sheet.addCell(fileNameLabel); - - // Create the link back to the index - link = new WritableHyperlink(0, 1, "Back to index", sheet, 0, currentSheet + NUMBER_OF_ROWS_IN_TITLE - 1); - workbook.getSheet(currentSheet).addHyperlink(link); - //Set column A of each sheet to be wide enough to show "Back to index" - workbook.getSheet(currentSheet).setColumnView(0, 13); + for (int currentSheet = 1; currentSheet < workbook.getNumberOfSheets(); currentSheet++) { + Sheet tableSheet = workbook.getSheetAt(currentSheet); + int rowIndex = currentSheet - 1 + NUMBER_OF_ROWS_IN_TITLE; + + Row row = getOrCreateRow(sheet, rowIndex); + Cell linkCell = row.createCell(0); + linkCell.setCellValue(tableSheet.getSheetName()); + linkCell.setHyperlink(workbook.getCreationHelper().createHyperlink(HyperlinkType.DOCUMENT)); + linkCell.getHyperlink().setAddress("'" + tableSheet.getSheetName() + "'!A1"); + linkCell.setCellStyle(consumerStyles.hyperlinkStyle); + String fileName = getCellString(tableSheet, 1, 1); + Cell fileNameCell = row.createCell(1); + fileNameCell.setCellValue(fileName); + fileNameCell.setCellStyle(indexFileNameStyle()); + + Row backLinkRow = getOrCreateRow(tableSheet, 1); + Cell backLinkCell = backLinkRow.createCell(0); + backLinkCell.setCellValue("Back to index"); + backLinkCell.setHyperlink(workbook.getCreationHelper().createHyperlink(HyperlinkType.DOCUMENT)); + backLinkCell.getHyperlink().setAddress("'" + sheet.getSheetName() + "'!A" + (rowIndex + 1)); + backLinkCell.setCellStyle(consumerStyles.hyperlinkStyle); + tableSheet.setColumnWidth(0, 13 * 256); } - // Make Column A fairly wide to show tab names and hide column B - sheet.setColumnView(0, 35); - sheet.setColumnView(1, 0); - + sheet.setColumnWidth(0, 35 * 256); + sheet.setColumnWidth(1, 1); } catch (Exception e) { throw new RuntimeException(e); } } + private CellStyle indexFileNameStyle() { + return consumerStyles.indexFileNameStyle; + } - /** - * Converts camel capped names to something we can show in a spreadsheet. - * - * @param name Name to convert. - * @return A human readable version of the name wtih camel caps replaced by spaces. - */ private String spreadsheetifyName(String name) { return StringUtils.capitalize(name).replaceAll("([A-Z])", " $1").trim(); } - - /** - * Inserts a row at the top of the sheet with the given title - * @param sheet add the title to - * @param title to add - */ - protected void createTitle(WritableSheet sheet, String title) { + protected void createTitle(Sheet sheet, String title) { try { - Label cell = new Label(0, 0, title); - WritableFont headingFont = new WritableFont(WritableFont.ARIAL, 16, WritableFont.BOLD); - WritableCellFormat headingFormat = new WritableCellFormat(headingFont); - cell.setCellFormat(headingFormat); - sheet.addCell(cell); - - cell = new Label(12, 0, "Copyright " + new SimpleDateFormat("yyyy").format(new Date()) + " Alfa Financial Software Ltd."); - WritableCellFormat copyrightFormat = new WritableCellFormat(); - copyrightFormat.setAlignment(Alignment.RIGHT); - cell.setCellFormat(copyrightFormat); - sheet.addCell(cell); + Cell cell = getOrCreateRow(sheet, 0).createCell(0); + cell.setCellValue(title); + cell.setCellStyle(consumerStyles.headingStyle); + Cell copyrightCell = getOrCreateRow(sheet, 0).createCell(12); + copyrightCell.setCellValue("Copyright " + new SimpleDateFormat("yyyy").format(new Date()) + " Alfa Financial Software Ltd."); + copyrightCell.setCellStyle(consumerStyles.copyrightStyle); } catch (Exception e) { throw new RuntimeException(e); } } - - /** - * @see org.alfasoftware.morf.dataset.DataSetConsumer#close(CloseState) - */ @Override public void close(CloseState closeState) { try { - - // Create the index createIndex(); - - workbook.write(); + workbook.write(documentOutputStream); workbook.close(); - } catch (Exception e) { + } catch (IOException e) { throw new RuntimeException("Error closing writable workbook", e); } } - - /** - * @see org.alfasoftware.morf.dataset.DataSetConsumer#table(org.alfasoftware.morf.metadata.Table, java.lang.Iterable) - */ @Override public void table(Table table, Iterable records) { Integer maxSampleRows = DEFAULT_SAMPLE_ROWS; if (rowsPerTable.isPresent()) { maxSampleRows = rowsPerTable.get().get(table.getName().toUpperCase()); } + if (maxSampleRows == null) { - log.info("File [" + table.getName() + "] excluded in configuration." ); + log.info("File [" + table.getName() + "] excluded in configuration."); } else if (tableOutputter.tableHasUnsupportedColumns(table)) { - log.info("File [" + table.getName() + "] skipped - unsupported columns." ); + log.info("File [" + table.getName() + "] skipped - unsupported columns."); } else { - log.info("File [" + table.getName() + "] generating..." ); + log.info("File [" + table.getName() + "] generating..."); tableOutputter.table(maxSampleRows, workbook, table, records); } } + + private Row getOrCreateRow(Sheet sheet, int rowIndex) { + Row row = sheet.getRow(rowIndex); + return row == null ? sheet.createRow(rowIndex) : row; + } + + private String getCellString(Sheet sheet, int columnIndex, int rowIndex) { + Row row = sheet.getRow(rowIndex); + if (row == null) { + return ""; + } + Cell cell = row.getCell(columnIndex); + return cell == null ? "" : cell.toString(); + } + + private String safeSheetName(String name) { + return WorkbookUtil.createSafeSheetName(name); + } + + private static final class ConsumerStyles { + private final CellStyle indexFileNameStyle; + private final CellStyle headingStyle; + private final CellStyle copyrightStyle; + + private final CellStyle hyperlinkStyle; + + private ConsumerStyles(Workbook workbook) { + Font normalFont = workbook.createFont(); + normalFont.setColor(Font.COLOR_NORMAL); + + Font headingFont = workbook.createFont(); + headingFont.setBold(true); + headingFont.setFontHeightInPoints((short) 16); + + Font hyperlinkFont = workbook.createFont(); + hyperlinkFont.setUnderline(Font.U_SINGLE); + hyperlinkFont.setColor(IndexedColors.BLUE.getIndex()); + hyperlinkFont.setFontHeightInPoints((short) 8); + + + indexFileNameStyle = workbook.createCellStyle(); + indexFileNameStyle.setFont(normalFont); + + headingStyle = workbook.createCellStyle(); + headingStyle.setFont(headingFont); + + copyrightStyle = workbook.createCellStyle(); + copyrightStyle.setAlignment(HorizontalAlignment.RIGHT); + + hyperlinkStyle = workbook.createCellStyle(); + hyperlinkStyle.setFont(hyperlinkFont); + } + } } diff --git a/morf-excel/src/main/java/org/alfasoftware/morf/excel/SpreadsheetDataSetProducer.java b/morf-excel/src/main/java/org/alfasoftware/morf/excel/SpreadsheetDataSetProducer.java old mode 100755 new mode 100644 index 6e8cff643..dfd835cb5 --- a/morf-excel/src/main/java/org/alfasoftware/morf/excel/SpreadsheetDataSetProducer.java +++ b/morf-excel/src/main/java/org/alfasoftware/morf/excel/SpreadsheetDataSetProducer.java @@ -1,509 +1,285 @@ -/* Copyright 2017 Alfa Financial Software - * - * 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.alfasoftware.morf.excel; - -import java.io.InputStream; -import java.text.SimpleDateFormat; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.alfasoftware.morf.dataset.DataSetProducer; -import org.alfasoftware.morf.dataset.Record; -import org.alfasoftware.morf.metadata.DataSetUtils; -import org.alfasoftware.morf.metadata.Schema; -import org.alfasoftware.morf.metadata.Sequence; -import org.alfasoftware.morf.metadata.Table; -import org.alfasoftware.morf.metadata.DataSetUtils.RecordBuilder; -import org.alfasoftware.morf.metadata.View; -import org.apache.commons.lang3.StringUtils; - -import jxl.Cell; -import jxl.Hyperlink; -import jxl.Sheet; -import jxl.Workbook; -import jxl.WorkbookSettings; -import jxl.read.biff.HyperlinkRecord; - -/** - * Converts Excel spreadsheets into a dataset. - * - * @author Copyright (c) Alfa Financial Software 2010 - */ -public class SpreadsheetDataSetProducer implements DataSetProducer { - /** - * Pattern for extracting sheet names from hyperlinks of the form: - * 'Sheet name'!A1:A1 - */ - private static final Pattern sheetName = Pattern.compile("'([^']*)'.*"); - - /** - * Store of tables extracted from the given excel files and their records. - */ - private final Map> tables = new HashMap<>(); - - - /** - * List of translations that have been extracted from the set of Excel files. - */ - private final List translations = new LinkedList<>(); - - - /** - * Prepares the producer with a set of Excel files to produce data from. - * - * @param excelFiles the Excel files to harvest data from - */ - public SpreadsheetDataSetProducer(final InputStream... excelFiles) { - // Open each spreadsheet and parse it - for (InputStream stream : excelFiles) { - parseWorkbook(stream); - } - } - - - /** - * Creates the collection of translation records from a given set of - * translations. - * - * @param id ID of the translation record - * @param translation Translation string to create - * @return the record representing the translation - */ - private Record createTranslationRecord(final int id, final String translation) { - final RecordBuilder record = DataSetUtils.record(); - record.setString("translationText", translation); - final Date now = new Date(); - record.setString("changeDate", new SimpleDateFormat("yyyyMMdd").format(now)); - record.setString("changedTime", new SimpleDateFormat("hhmmss").format(now)); - record.setInteger("localeSequenceNumber", 1); // Assume locale 1 for translations on initial upload - record.setInteger("translationSequenceNumber", id); - record.setInteger("translationId", id); - record.setInteger("id", id); - return record; - } - - - /** - * Gets the hyperlink that starts at the given column/row in the given sheet. - * - * @param sheet sheet to look for a hyperlink in - * @param column column of the hyperlink - * @param row row of the hyperlink - * @return the hyperlink, if found. Otherwise, null - */ - private HyperlinkRecord getHyperlink(Sheet sheet, int column, int row) { - for (Hyperlink link : sheet.getHyperlinks()) { - if (link.getColumn() == column && link.getRow() == row) { - return (HyperlinkRecord)link; - } - } - - return null; - } - - - /** - * Parse the workbook from an {@link InputStream}. - * - * @param inputStream InputStream from the spreadsheet - */ - private void parseWorkbook(final InputStream inputStream) { - Workbook workbook = null; - try { - final WorkbookSettings settings = new WorkbookSettings(); - settings.setEncoding("CP1252"); - workbook = Workbook.getWorkbook(inputStream, settings); - - /* - * The first sheet in the workbook is the index sheet. It contains links - * to sheets containing data as well as the table name to use. - */ - final Sheet sheet = workbook.getSheet(0); - final int column = 1; - for (int row = 2; row < sheet.getRows(); row++) { - final Cell cell = sheet.getCell(column, row); - if (StringUtils.isEmpty(cell.getContents())) { - break; - } - - final HyperlinkRecord hyperlink = getHyperlink(sheet, cell.getColumn() - 1, cell.getRow()); - final String worksheetName = getDestinationWorksheet(hyperlink); - if (workbook.getSheet(worksheetName) == null) { - throw new IllegalStateException("Failed to find worksheet with name [" + worksheetName + "]"); - } - final List records = getRecords(workbook.getSheet(worksheetName)); - this.tables.put(cell.getContents(), records); - } - } catch (Exception e) { - throw new RuntimeException("Failed to parse spreadsheet", e); - } finally { - if (workbook != null) { - workbook.close(); - } - } - } - - - /** - * Gets the name of the destination worksheet for the given hyperlink. - * - * @param hyperlink Hyperlink to determine worksheet name for - * @return the name of the worksheet that the hyperlink points to - */ - private String getDestinationWorksheet(HyperlinkRecord hyperlink) { - /* - * Hyperlinks will be either to a specific cell or to a worksheet as a - * whole. If the regular expression for the sheet name part of a link - * doesn't match then the hyperlink must be to a worksheet as a whole. - */ - final Matcher matcher = sheetName.matcher(hyperlink.getLocation()); - if (matcher.matches()) { - return matcher.group(1); - } else { - return hyperlink.getLocation(); - } - } - - - /** - * Finds the heading row in a given worksheet. - * - *

This works by assuming that the table data starts in the row - * immediately following a row containing hyperlinks that is after - * a row that starts with "Parameters to Set Up". E.g.

- * - *
-   * Parameters to Setup |                  |
-   * ----------------------------------------
-   * Random comments     | More comments    |
-   * ----------------------------------------
-   * Column Heading 1    | Column Heading 2 | <-- These are hyperlinks
-   * 
- * - * @param sheet worksheet to search for data - * @return the index of the starting row for the data - */ - private int findHeaderRow(final Sheet sheet) { - int row = 0; - - // Find the start row - for (; row < sheet.getRows(); row++) { - if ("Parameters to Set Up".equalsIgnoreCase(sheet.getCell(0, row).getContents())) { - // Skip this row - row++; - break; - } - } - - // The heading row contains hyperlinks so continue scanning down until a - // hyperlink is found - for (; row < sheet.getRows(); row++) { - final HyperlinkRecord hyperlink = getHyperlink(sheet, 0, row); - if (hyperlink != null) { - return row; - } - } - - // Either the parameters to set up row wasn't found or no hyperlinks were found - throw new IllegalStateException("Could not find header row in worksheet [" + sheet.getName() + "]"); - } - - - /** - * Determines if a worksheet contains something that looks like translations. - * This is done by looking for a gap in the column headings followed by an - * actual heading, e.g. - * - *
-   * Heading 1 | Heading 2 |         | Translation
-   * -------------------------------------------------
-   * Value 1   | Value 2   |         | Bonjour
-   * 
- * - * @param sheet sheet to look for translations in - * @param headingRow the index of the heading row - * @return the index of the translation column, -1 otherwise - */ - private int getTranslationsColumnIndex(Sheet sheet, int headingRow) { - boolean hasBlank = false; - int i = 0; - for (; i < sheet.getRow(headingRow).length; i++) { - if (sheet.getCell(i, headingRow).getContents().length() == 0) { - hasBlank = true; - break; - } - } - if (!hasBlank) { - return -1; - } - for (; i < sheet.getRow(headingRow).length; i++) { - if (sheet.getCell(i, headingRow).getContents().length() > 0) { - return i; - } - } - return -1; - } - - - /** - * Counts the number of headings in the given sheet. This excludes any - * heading related to translations. - * - * @param sheet Worksheet to count headings in - * @param headingRowIndex Index of the heading row - * @return the number of headings - */ - private int countHeadings(final Sheet sheet, final int headingRowIndex) { - for (int i = 0; i < sheet.getRow(headingRowIndex).length; i++) { - // A blank heading is the start of additional headings such as the - // translation heading - if (sheet.getCell(i, headingRowIndex).getContents().length() == 0) { - return i; - } - } - return sheet.getRow(headingRowIndex).length; - } - - - /** - * Get all the records from the given Excel sheet. - * - * @param sheet worksheet to get records from - * @return the extracted records - */ - private List getRecords(Sheet sheet) { - try { - long id = 1; - int row = findHeaderRow(sheet); - - // Get the column headings - final Map columnHeadingsMap = new HashMap<>(); - for (int i = 0; i < countHeadings(sheet, row); i++) { - columnHeadingsMap.put(columnName(sheet.getCell(i, row).getContents()), i); - } - - // Does this sheet have translations or not? - final int translationColumn = getTranslationsColumnIndex(sheet, row); - - // -- Now get the data... - // - row++; // The data is always one row below the headings - List records = new LinkedList<>(); - for (; row < sheet.getRows(); row++) { - final Cell[] cells = sheet.getRow(row); - - // If all the cells are blank then this is the end of the table - if (allBlank(cells)) { - break; - } - - records.add(createRecord(id++, columnHeadingsMap, translationColumn, cells)); - } - - return records; - } catch (Exception e) { - throw new RuntimeException("Failed to parse worksheet [" + sheet.getName() + "]", e); - } - } - - - /** - * Determines if the given cells are all blank or not. - * @param cells to check if they are blank or not - * @return true if all the cells are blank, otherwise false. - */ - private boolean allBlank(final Cell... cells) { - for (Cell cell : cells) { - if (cell.getContents().length() != 0) { - return false; - } - } - return true; - } - - - /** - * Creates a record from a set of cells from a worksheet. - * - * @param id ID of the row - * @param columnHeadingsMap Map of column headings to their index - * @param translationColumn Column containing translations - * @param cells The cells to process - * @return the created record - */ - private Record createRecord(final long id, final Map columnHeadingsMap, final int translationColumn, final Cell... cells) { - final int translationId; - if (translationColumn != -1 && cells[translationColumn].getContents().length() > 0) { - translationId = translations.size() + 1; - translations.add(createTranslationRecord(translationId, cells[translationColumn].getContents())); - } else { - translationId = 0; - } - - final RecordBuilder record = DataSetUtils.record(); - for (Entry column : columnHeadingsMap.entrySet()) { - if (column.getValue() < cells.length) { - record.setString(column.getKey(), cells[column.getValue()].getContents()); - } else { - // If the cell is actually specified then assume it is default blank - record.setString(column.getKey(), ""); - } - } - record.setLong("id", id); - record.setInteger("translationId", translationId); - return record; - } - - - /** - * Converts the given long name in to a column name. This is the same as - * removing all the spaces and making the first character lowercase. - * - * @param longName the long name to convert - * @return the name of the column - */ - private String columnName(final String longName) { - final String noSpaces = longName.replaceAll(" ", ""); - return noSpaces.substring(0, 1).toLowerCase() + noSpaces.substring(1); - } - - - /** - * {@inheritDoc} - * - * @see org.alfasoftware.morf.dataset.DataSetProducer#getSchema() - */ - @Override - public Schema getSchema() { - return new Schema() { - - @Override - public Table getTable(String name) { - throw new UnsupportedOperationException("Cannot get the metadata of a table for a spreadsheet"); - } - - @Override - public boolean isEmptyDatabase() { - return tables.isEmpty(); - } - - @Override - public boolean tableExists(String name) { - return tables.containsKey(name); - } - - @Override - public Collection tableNames() { - return tables.keySet(); - } - - @Override - public Collection tables() { - throw new UnsupportedOperationException("Cannot get the metadata of a table for a spreadsheet"); - } - - @Override - public boolean viewExists(String name) { - return false; - } - - @Override - public View getView(String name) { - throw new IllegalArgumentException("Invalid view [" + name + "]. Views are not supported in spreadsheets"); - } - - @Override - public Collection viewNames() { - return Collections.emptySet(); - } - - @Override - public Collection views() { - return Collections.emptySet(); - } - - @Override - public boolean sequenceExists(String name) { - return false; - } - - @Override - public Sequence getSequence(String name) { - throw new IllegalArgumentException("Invalid sequence [" + name + "]. Sequences are not supported in spreadsheets"); - } - - @Override - public Collection sequenceNames() { - return Collections.emptySet(); - } - - @Override - public Collection sequences() { - return Collections.emptySet(); - } - }; - } - - - /** - * {@inheritDoc} - * - * @see org.alfasoftware.morf.dataset.DataSetProducer#open() - */ - @Override - public void open() { - // Nothing to do - } - - - /** - * {@inheritDoc} - * - * @see org.alfasoftware.morf.dataset.DataSetProducer#close() - */ - @Override - public void close() { - // Nothing to do - } - - - /** - * {@inheritDoc} - * - * @see org.alfasoftware.morf.dataset.DataSetProducer#records(java.lang.String) - */ - @Override - public Iterable records(String tableName) { - return tables.get(tableName); - } - - - /** - * @see org.alfasoftware.morf.dataset.DataSetProducer#isTableEmpty(java.lang.String) - */ - @Override - public boolean isTableEmpty(String tableName) { - return tables.get(tableName).isEmpty(); - } -} +/* Copyright 2017 Alfa Financial Software + * + * 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.alfasoftware.morf.excel; + +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.alfasoftware.morf.dataset.DataSetProducer; +import org.alfasoftware.morf.dataset.Record; +import org.alfasoftware.morf.metadata.DataSetUtils; +import org.alfasoftware.morf.metadata.DataSetUtils.RecordBuilder; +import org.alfasoftware.morf.metadata.Schema; +import org.alfasoftware.morf.metadata.Sequence; +import org.alfasoftware.morf.metadata.Table; +import org.alfasoftware.morf.metadata.View; +import org.apache.commons.lang3.StringUtils; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; + +/** + * Converts Excel spreadsheets into a dataset. + */ +public class SpreadsheetDataSetProducer implements DataSetProducer { + + private static final Pattern SHEET_NAME = Pattern.compile("'([^']*)'.*"); + + private final Map> tables = new HashMap<>(); + private final List translations = new LinkedList<>(); + private final DataFormatter dataFormatter = new DataFormatter(); + + public SpreadsheetDataSetProducer(final InputStream... excelFiles) { + for (InputStream stream : excelFiles) { + parseWorkbook(stream); + } + } + + private Record createTranslationRecord(final int id, final String translation) { + final RecordBuilder record = DataSetUtils.record(); + record.setString("translationText", translation); + final Date now = new Date(); + record.setString("changeDate", new SimpleDateFormat("yyyyMMdd").format(now)); + record.setString("changedTime", new SimpleDateFormat("hhmmss").format(now)); + record.setInteger("localeSequenceNumber", 1); + record.setInteger("translationSequenceNumber", id); + record.setInteger("translationId", id); + record.setInteger("id", id); + return record; + } + + private void parseWorkbook(final InputStream inputStream) { + if (inputStream == null) { + throw new IllegalArgumentException("Spreadsheet input stream was null"); + } + try (Workbook workbook = WorkbookFactory.create(inputStream)) { + final Sheet sheet = workbook.getSheetAt(0); + final int column = 1; + for (int row = 2; row <= sheet.getLastRowNum(); row++) { + String tableName = getCellContents(sheet, column, row); + if (StringUtils.isEmpty(tableName)) { + break; + } + + String worksheetName = getDestinationWorksheet(sheet, column - 1, row); + Sheet worksheet = workbook.getSheet(worksheetName); + if (worksheet == null) { + throw new IllegalStateException("Failed to find worksheet with name [" + worksheetName + "]"); + } + this.tables.put(tableName, getRecords(worksheet)); + } + } catch (Exception e) { + throw new RuntimeException("Failed to parse spreadsheet", e); + } + } + + private String getDestinationWorksheet(Sheet sheet, int column, int row) { + Cell cell = getCell(sheet, column, row); + if (cell == null || cell.getHyperlink() == null || cell.getHyperlink().getAddress() == null) { + throw new IllegalStateException("Failed to find hyperlink in index sheet at [" + column + "," + row + "]"); + } + + String address = cell.getHyperlink().getAddress(); + Matcher matcher = SHEET_NAME.matcher(address); + if (matcher.matches()) { + return matcher.group(1); + } + int separator = address.indexOf('!'); + return separator >= 0 ? address.substring(0, separator) : address; + } + + private int findHeaderRow(final Sheet sheet) { + int row = 0; + for (; row <= sheet.getLastRowNum(); row++) { + if ("Parameters to Set Up".equalsIgnoreCase(getCellContents(sheet, 0, row))) { + row++; + break; + } + } + + for (; row <= sheet.getLastRowNum(); row++) { + Cell cell = getCell(sheet, 0, row); + if (cell != null && cell.getHyperlink() != null) { + return row; + } + } + + throw new IllegalStateException("Could not find header row in worksheet [" + sheet.getSheetName() + "]"); + } + + private int getTranslationsColumnIndex(Sheet sheet, int headingRow) { + boolean hasBlank = false; + int width = getRowWidth(sheet, headingRow); + int i = 0; + for (; i < width; i++) { + if (getCellContents(sheet, i, headingRow).isEmpty()) { + hasBlank = true; + break; + } + } + if (!hasBlank) { + return -1; + } + for (; i < width; i++) { + if (!getCellContents(sheet, i, headingRow).isEmpty()) { + return i; + } + } + return -1; + } + + private int countHeadings(final Sheet sheet, final int headingRowIndex) { + int width = getRowWidth(sheet, headingRowIndex); + for (int i = 0; i < width; i++) { + if (getCellContents(sheet, i, headingRowIndex).isEmpty()) { + return i; + } + } + return width; + } + + private List getRecords(Sheet sheet) { + try { + long id = 1; + int row = findHeaderRow(sheet); + final Map columnHeadingsMap = new HashMap<>(); + for (int i = 0; i < countHeadings(sheet, row); i++) { + columnHeadingsMap.put(columnName(getCellContents(sheet, i, row)), i); + } + + final int translationColumn = getTranslationsColumnIndex(sheet, row); + row++; + List records = new LinkedList<>(); + for (; row <= sheet.getLastRowNum(); row++) { + if (allBlank(sheet, row)) { + break; + } + records.add(createRecord(id++, columnHeadingsMap, translationColumn, sheet, row)); + } + return records; + } catch (Exception e) { + throw new RuntimeException("Failed to parse worksheet [" + sheet.getSheetName() + "]", e); + } + } + + private boolean allBlank(final Sheet sheet, int rowIndex) { + Row row = sheet.getRow(rowIndex); + if (row == null) { + return true; + } + for (int i = row.getFirstCellNum() < 0 ? 0 : row.getFirstCellNum(); i < row.getLastCellNum(); i++) { + if (!getCellContents(sheet, i, rowIndex).isEmpty()) { + return false; + } + } + return true; + } + + private Record createRecord(final long id, final Map columnHeadingsMap, final int translationColumn, + final Sheet sheet, final int rowIndex) { + final int translationId; + String translationValue = translationColumn == -1 ? "" : getCellContents(sheet, translationColumn, rowIndex); + if (translationColumn != -1 && !translationValue.isEmpty()) { + translationId = translations.size() + 1; + translations.add(createTranslationRecord(translationId, translationValue)); + } else { + translationId = 0; + } + + final RecordBuilder record = DataSetUtils.record(); + for (Entry column : columnHeadingsMap.entrySet()) { + record.setString(column.getKey(), getCellContents(sheet, column.getValue(), rowIndex)); + } + record.setLong("id", id); + record.setInteger("translationId", translationId); + return record; + } + + private String columnName(final String longName) { + final String noSpaces = longName.replaceAll(" ", ""); + return noSpaces.substring(0, 1).toLowerCase() + noSpaces.substring(1); + } + + @Override + public Schema getSchema() { + return new Schema() { + @Override public Table getTable(String name) { throw new UnsupportedOperationException("Cannot get the metadata of a table for a spreadsheet"); } + @Override public boolean isEmptyDatabase() { return tables.isEmpty(); } + @Override public boolean tableExists(String name) { return tables.containsKey(name); } + @Override public Collection tableNames() { return tables.keySet(); } + @Override public Collection
tables() { throw new UnsupportedOperationException("Cannot get the metadata of a table for a spreadsheet"); } + @Override public boolean viewExists(String name) { return false; } + @Override public View getView(String name) { throw new IllegalArgumentException("Invalid view [" + name + "]. Views are not supported in spreadsheets"); } + @Override public Collection viewNames() { return Collections.emptySet(); } + @Override public Collection views() { return Collections.emptySet(); } + @Override public boolean sequenceExists(String name) { return false; } + @Override public Sequence getSequence(String name) { throw new IllegalArgumentException("Invalid sequence [" + name + "]. Sequences are not supported in spreadsheets"); } + @Override public Collection sequenceNames() { return Collections.emptySet(); } + @Override public Collection sequences() { return Collections.emptySet(); } + }; + } + + /** + * No-op. + * + *

This producer is eagerly initialised: the spreadsheet is fully parsed + * during construction. There are therefore no resources to open.

+ */ + @Override public void open() { + // Intentionally empty + } + + + /** + * No-op. + * + *

This producer does not hold any open resources after construction, + * so there is nothing to close.

+ */ + @Override public void close() { + // Intentionally empty + } + @Override public Iterable records(String tableName) { return tables.get(tableName); } + @Override public boolean isTableEmpty(String tableName) { return tables.get(tableName).isEmpty(); } + + private int getRowWidth(Sheet sheet, int rowIndex) { + Row row = sheet.getRow(rowIndex); + return row == null || row.getLastCellNum() < 0 ? 0 : row.getLastCellNum(); + } + + private Cell getCell(Sheet sheet, int columnIndex, int rowIndex) { + Row row = sheet.getRow(rowIndex); + return row == null ? null : row.getCell(columnIndex); + } + + private String getCellContents(Sheet sheet, int columnIndex, int rowIndex) { + Cell cell = getCell(sheet, columnIndex, rowIndex); + return cell == null ? "" : dataFormatter.formatCellValue(cell); + } +} diff --git a/morf-excel/src/main/java/org/alfasoftware/morf/excel/TableOutputter.java b/morf-excel/src/main/java/org/alfasoftware/morf/excel/TableOutputter.java old mode 100755 new mode 100644 index cd51588aa..8763be8e3 --- a/morf-excel/src/main/java/org/alfasoftware/morf/excel/TableOutputter.java +++ b/morf-excel/src/main/java/org/alfasoftware/morf/excel/TableOutputter.java @@ -1,591 +1,460 @@ -/* Copyright 2017 Alfa Financial Software - * - * 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.alfasoftware.morf.excel; - -import static org.alfasoftware.morf.metadata.DataType.BIG_INTEGER; -import static org.alfasoftware.morf.metadata.DataType.CLOB; -import static org.alfasoftware.morf.metadata.DataType.DECIMAL; -import static org.alfasoftware.morf.metadata.DataType.INTEGER; -import static org.alfasoftware.morf.metadata.DataType.STRING; - -import java.math.BigDecimal; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import org.alfasoftware.morf.dataset.Record; -import org.alfasoftware.morf.metadata.Column; -import org.alfasoftware.morf.metadata.DataType; -import org.alfasoftware.morf.metadata.Table; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import com.google.common.base.Predicate; -import com.google.common.collect.Iterables; -import com.google.common.collect.Sets; -import jxl.Cell; -import jxl.format.Alignment; -import jxl.format.Border; -import jxl.format.BorderLineStyle; -import jxl.format.Colour; -import jxl.format.UnderlineStyle; -import jxl.format.VerticalAlignment; -import jxl.write.Label; -import jxl.write.WritableCell; -import jxl.write.WritableCellFormat; -import jxl.write.WritableFont; -import jxl.write.WritableHyperlink; -import jxl.write.WritableSheet; -import jxl.write.WritableWorkbook; -import jxl.write.WriteException; - -/** - * Outputs tables to an excel spreadsheet. - * - * @author Copyright (c) Alfa Financial Software 2010 - */ -class TableOutputter { - - private static final Log log = LogFactory.getLog(TableOutputter.class); - - /** - * The number of rows in the title. - */ - private static final int NUMBER_OF_ROWS_IN_TITLE = 2; - - /** - * The maximum number of rows supported in an XLS. - */ - private static final int MAX_EXCEL_ROWS = 65536; - - /** - * The maximum number of rows supported in an XLS. - */ - private static final int MAX_EXCEL_COLUMNS = 256; - - /** - * The maximum number of characters supported in an XLS cell. - */ - private static final int MAX_CELL_CHARACTERS = 32767; - - /** - * The data types we can output to a spreadsheet. - */ - private static final Set supportedDataTypes = Sets.immutableEnumSet(STRING, DECIMAL, BIG_INTEGER, INTEGER, CLOB); - - /** - * A source of non-schema related data. - */ - private final AdditionalSchemaData additionalSchemaData; - - - /** - * Constructor. - * - * @param additionalSchemaData A source of non-schema related data. - */ - public TableOutputter(AdditionalSchemaData additionalSchemaData) { - this.additionalSchemaData = additionalSchemaData; - } - - - /** - * Output the given table to the given workbook. - * - * @param maxSampleRows the maximum number of rows to export in the "sample data" section - * (all rows are included in the "Parameters to set up" section). - * @param workbook to add the table to. - * @param table to add to the workbook. - * @param records of data to output. - */ - public void table(int maxSampleRows, final WritableWorkbook workbook, final Table table, final Iterable records) { - final WritableSheet workSheet = workbook.createSheet(spreadsheetifyName(table.getName()), workbook.getNumberOfSheets()); - - boolean columnsTruncated = table.columns().size() > MAX_EXCEL_COLUMNS; - if(columnsTruncated) { - log.warn("Output for table '" + table.getName() + "' exceeds the maximum number of columns (" + MAX_EXCEL_COLUMNS + ") in an Excel worksheet. It will be truncated."); - } - - boolean rowsTruncated = false; - - try { - - int currentRow = NUMBER_OF_ROWS_IN_TITLE + 1; - - try { - - final Map helpTextRowNumbers = new HashMap<>(); - - //Now output.... - //Help text - currentRow = outputHelp(workSheet, table, currentRow, helpTextRowNumbers); - - //"Example Data" - Label exampleLabel = new Label(0, currentRow, "Example Data"); - exampleLabel.setCellFormat(getBoldFormat()); - workSheet.addCell(exampleLabel); - currentRow++; - - //Headings for example data - currentRow = outputDataHeadings(workSheet, table, currentRow, helpTextRowNumbers); - - //Actual example data - currentRow = outputExampleData(maxSampleRows, workSheet, table, currentRow, records); - - //"Parameters to Set Up" - Label dataLabel = new Label(0, currentRow, "Parameters to Set Up"); - dataLabel.setCellFormat(getBoldFormat()); - workSheet.addCell(dataLabel); - currentRow++; - - //Headings for parameters to be uploaded - currentRow = outputDataHeadings(workSheet, table, currentRow, helpTextRowNumbers); - currentRow = outputExampleData(null, workSheet, table, currentRow, records); - } catch (RowLimitExceededException e) { - log.warn(e.getMessage()); - rowsTruncated = true; - } - } - catch (Exception e) { - throw new RuntimeException("Error outputting table '" + table.getName() + "'", e); - } - - /* - * Write the title for the worksheet - adding truncation information if appropriate - */ - if(columnsTruncated || rowsTruncated) { - StringBuilder truncatedSuffix = new StringBuilder(); - truncatedSuffix.append(" ["); - - if(columnsTruncated) { - truncatedSuffix.append("COLUMNS"); - } - - if(columnsTruncated && rowsTruncated) { - truncatedSuffix.append(" & "); - } - - if(rowsTruncated) { - truncatedSuffix.append("ROWS"); - } - - truncatedSuffix.append(" TRUNCATED]"); - - createTitle(workSheet, workSheet.getName() + truncatedSuffix.toString(), table.getName()); - } - else { - createTitle(workSheet, workSheet.getName(), table.getName()); - } - } - - - /** - * Converts camel capped names to something we can show in a spreadsheet. - * - * @param name Name to convert. - * @return A human readable version of the name wtih camel caps replaced by spaces. - */ - private String spreadsheetifyName(String name) { - return StringUtils.capitalize(name).replaceAll("([A-Z][a-z])", " $1").trim(); - } - - - /** - * Inserts a row at the top of the sheet with the given title - * @param sheet to add the title to - * @param title to add - * @param fileName of the ALFA file to which the sheet relates - */ - private void createTitle(WritableSheet sheet, String title, String fileName) { - try { - //Friendly file name in A1 - Label cell = new Label(0, 0, title); - WritableFont headingFont = new WritableFont(WritableFont.ARIAL, 16, WritableFont.BOLD); - WritableCellFormat headingFormat = new WritableCellFormat(headingFont); - cell.setCellFormat(headingFormat); - sheet.addCell(cell); - - //ALFA file name in B2 (hidden in white) - cell = new Label(1, 1, fileName); - WritableFont fileNameFont = new WritableFont(WritableFont.ARIAL,10,WritableFont.NO_BOLD,false,UnderlineStyle.NO_UNDERLINE,Colour.WHITE); - WritableCellFormat fileNameFormat = new WritableCellFormat(fileNameFont); - cell.setCellFormat(fileNameFormat); - sheet.addCell(cell); - - //Copyright notice in M1 - cell = new Label(12, 0, "Copyright " + new SimpleDateFormat("yyyy").format(new Date()) + " Alfa Financial Software Ltd."); - WritableCellFormat copyrightFormat = new WritableCellFormat(); - copyrightFormat.setAlignment(Alignment.RIGHT); - cell.setCellFormat(copyrightFormat); - sheet.addCell(cell); - - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - - /** - * @return the standard font to use - */ - private WritableFont getStandardFont() { - return new WritableFont(WritableFont.ARIAL, 8); - } - - - /** - * @return the format to use for normal cells - * @throws WriteException if the format could not be created - */ - private WritableCellFormat getStandardFormat() throws WriteException { - WritableCellFormat standardFormat = new WritableCellFormat(getStandardFont()); - standardFormat.setVerticalAlignment(VerticalAlignment.TOP); - return standardFormat; - } - - - /** - * @return the format to use for bold cells - * @throws WriteException if the format could not be created - */ - private WritableCellFormat getBoldFormat() throws WriteException { - WritableFont boldFont = new WritableFont(WritableFont.ARIAL, 8, WritableFont.BOLD); - WritableCellFormat boldHeading = new WritableCellFormat(boldFont); - boldHeading.setBorder(Border.BOTTOM, BorderLineStyle.MEDIUM); - boldHeading.setVerticalAlignment(VerticalAlignment.CENTRE); - boldHeading.setBackground(Colour.GRAY_25); - - - WritableCellFormat boldFormat = new WritableCellFormat(boldFont); - boldFormat.setVerticalAlignment(VerticalAlignment.TOP); - return boldFormat; - } - - - /** - * Outputs the example data rows. - * - * @param numberOfExamples to output - * @param workSheet to add the data rows to - * @param table to get metadata from - * @param startRow to start adding the example rows at - * @param records to add as examples - * @return the new row to carry on outputting at - * @throws WriteException if any of the writes to workSheet fail - */ - private int outputExampleData(final Integer numberOfExamples, WritableSheet workSheet, Table table, final int startRow, Iterable records) throws WriteException { - int currentRow = startRow; - - int rowsOutput = 0; - for (Record record : records) { - - if (currentRow >= MAX_EXCEL_ROWS) { - continue; - } - - if (numberOfExamples != null && rowsOutput >= numberOfExamples) { - // Need to continue the loop rather than break as we need to close - // the connection which happens at the end of iteration... - continue; - } - - record(currentRow, workSheet, table, record); - rowsOutput++; - currentRow++; - } - - if (currentRow >= MAX_EXCEL_ROWS) { - // This is a fix for WEB-56074. It will be removed if/when WEB-42351 is developed. - throw new RowLimitExceededException("Output for table '" + table.getName() + "' exceeds the maximum number of rows (" + MAX_EXCEL_ROWS + ") in an Excel worksheet. It will be truncated."); - } - - currentRow++; - return currentRow; - } - - - /** - * @param workSheet to add the help to - * @param table to fetch metadata from - * @param startRow to start adding rows at - * @param helpTextRowNumbers - map to insert row numbers for each help field into - * @return the index of the next row to use - * @throws WriteException if any of the writes to workSheet failed - */ - private int outputHelp(WritableSheet workSheet, Table table, final int startRow, final Map helpTextRowNumbers) throws WriteException { - int currentRow = startRow; - - // Title for the descriptions - Label dataLabel = new Label(0, currentRow, "Column Descriptions"); - dataLabel.setCellFormat(getBoldFormat()); - workSheet.addCell(dataLabel); - currentRow++; - - int currentColumn = 0; - - for (Column column : table.columns()) { - if (!column.getName().equals("id") && !column.getName().equals("version")) { - // Field name to go with the description - Label fieldName = new Label(0, currentRow, spreadsheetifyName(column.getName())); - fieldName.setCellFormat(getBoldFormat()); - workSheet.addCell(fieldName); - - // The type/width - String typeString = column.getType() + "(" + column.getWidth() + (column.getScale() == 0 ? "" : "," + column.getScale()) + ")"; - Label fieldType = new Label(1, currentRow, typeString); - fieldType.setCellFormat(getStandardFormat()); - workSheet.addCell(fieldType); - - // The default - String defaultValue = additionalSchemaData.columnDefaultValue(table, column.getName()); - Label fieldDefault = new Label(2, currentRow, defaultValue); - fieldDefault.setCellFormat(getStandardFormat()); - workSheet.addCell(fieldDefault); - - // The field documentation - workSheet.mergeCells(3, currentRow, 12, currentRow); - String documentation = additionalSchemaData.columnDocumentation(table, column.getName()); - Label documentationLabel = new Label(3, currentRow, documentation); - WritableCellFormat format = new WritableCellFormat(getStandardFormat()); - format.setWrap(true); - format.setVerticalAlignment(VerticalAlignment.TOP); - documentationLabel.setCellFormat(format); - workSheet.addCell(documentationLabel); - - //If we've exceed the maximum number of columns - then output truncated warnings - if(currentColumn >= MAX_EXCEL_COLUMNS) { - Label truncatedWarning = new Label(13, currentRow, "[TRUNCATED]"); - truncatedWarning.setCellFormat(getBoldFormat()); - workSheet.addCell(truncatedWarning); - } - - // We are aiming for 150px. 1px is 15 Excel "Units" - workSheet.setRowView(currentRow, 150 * 15); - - // Remember at what row we created the help text for this column - helpTextRowNumbers.put(column.getName(), currentRow); - - currentRow++; - currentColumn++; - } - - } - - // Group all the help rows together - workSheet.setRowGroup(startRow + 1, currentRow - 1, true); - - // Some extra blank space for neatness - currentRow++; - - return currentRow; - } - - - /** - * Outputs the data headings row. - * - * @param workSheet to add the row to - * @param table to fetch metadata from - * @param startRow to add the headings at - * @param helpTextRowNumbers - the map of column names to row index for each - * bit of help text - * @throws WriteException if any of the writes to workSheet failed - * @return the row to carry on inserting at - */ - private int outputDataHeadings(WritableSheet workSheet, Table table, final int startRow, final Map helpTextRowNumbers) throws WriteException { - int currentRow = startRow; - - int columnNumber = 0; - final WritableCellFormat columnHeadingFormat = getBoldFormat(); - - columnHeadingFormat.setBackground(Colour.VERY_LIGHT_YELLOW); - WritableFont font = new WritableFont(WritableFont.ARIAL, 8, WritableFont.BOLD); - font.setColour(Colour.BLUE); - font.setUnderlineStyle(UnderlineStyle.SINGLE); - columnHeadingFormat.setFont(font); - - for (Column column : table.columns()) { - - if(columnNumber < MAX_EXCEL_COLUMNS && !column.getName().equals("id") && !column.getName().equals("version")) { - // Data heading is a link back to the help text - WritableHyperlink linkToHelp = new WritableHyperlink( - columnNumber, currentRow, - spreadsheetifyName(column.getName()), - workSheet, 0, helpTextRowNumbers.get(column.getName())); - workSheet.addHyperlink(linkToHelp); - WritableCell label = workSheet.getWritableCell(columnNumber, currentRow); - label.setCellFormat(columnHeadingFormat); - - // Update the help text such that it is a link to the heading - Cell helpCell = workSheet.getCell(0, helpTextRowNumbers.get(column.getName())); - WritableHyperlink linkFromHelp = new WritableHyperlink( - 0, helpTextRowNumbers.get(column.getName()), - helpCell.getContents(), - workSheet, columnNumber, currentRow); - workSheet.addHyperlink(linkFromHelp); - - columnNumber++; - } - } - - currentRow++; - - return currentRow; - } - - - /** - * @param row to add the record at - * @param worksheet to add the record to - * @param table that the record comes from - * @param record Record to serialise. This method is part of the old Cryo API. - */ - private void record(final int row, final WritableSheet worksheet, final Table table, Record record) { - int columnNumber = 0; - WritableFont standardFont = new WritableFont(WritableFont.ARIAL, 8); - WritableCellFormat standardFormat = new WritableCellFormat(standardFont); - - WritableCellFormat exampleFormat = new WritableCellFormat(standardFont); - try { - exampleFormat.setBackground(Colour.ICE_BLUE); - } catch (WriteException e) { - throw new RuntimeException("Failed to set example background colour", e); - } - - for (Column column : table.columns()) { - if(columnNumber < MAX_EXCEL_COLUMNS && !column.getName().equals("id") && !column.getName().equals("version")) { - createCell(worksheet, column, columnNumber, row, record, standardFormat); - columnNumber++; - } - } - } - - /** - * Creates the cell at the given position. - * - * @param currentWorkSheet to add the cell to - * @param column The meta data for the column in the source table - * @param columnNumber The column number to insert at (0 based) - * @param rowIndex The row number to insert at (0 based) - * @param record The source record - * @param format The format to apply to the cell - */ - private void createCell(final WritableSheet currentWorkSheet, Column column, int columnNumber, int rowIndex, Record record, WritableCellFormat format) { - WritableCell writableCell; - - switch (column.getType()) { - case STRING: - writableCell = new Label(columnNumber, rowIndex, record.getString(column.getName())); - break; - - case DECIMAL: - BigDecimal decimalValue = record.getBigDecimal(column.getName()); - try { - writableCell = decimalValue == null ? createBlankWriteableCell(columnNumber, rowIndex) : new jxl.write.Number(columnNumber, rowIndex, decimalValue.doubleValue()); - } catch (Exception e) { - throw new UnsupportedOperationException("Cannot generate Excel cell (parseDouble) for data [" + decimalValue + "]" + unsupportedOperationExceptionMessageSuffix(column, currentWorkSheet), e); - } - break; - - case BIG_INTEGER: - case INTEGER: - Long longValue = record.getLong(column.getName()); - try { - writableCell = longValue == null ? createBlankWriteableCell(columnNumber, rowIndex) : new jxl.write.Number(columnNumber, rowIndex, longValue); - } catch (Exception e) { - throw new UnsupportedOperationException("Cannot generate Excel cell (parseInt) for data [" + longValue + "]" + unsupportedOperationExceptionMessageSuffix(column, currentWorkSheet), e); - } - break; - - case CLOB: - try { - String stringValue = record.getString(column.getName()); - writableCell = stringValue == null ? createBlankWriteableCell(columnNumber, rowIndex) : new Label(columnNumber, rowIndex, StringUtils.substring(stringValue, 0, MAX_CELL_CHARACTERS)); - } catch (Exception e) { - throw new UnsupportedOperationException("Cannot generate Excel cell for CLOB data" + unsupportedOperationExceptionMessageSuffix(column, currentWorkSheet), e); - } - break; - - default: - throw new UnsupportedOperationException("Cannot output data type [" + column.getType() + "] to a spreadsheet"); - } - writableCell.setCellFormat(format); - - try { - currentWorkSheet.addCell(writableCell); - } catch (Exception e) { - throw new RuntimeException("Error writing value to spreadsheet", e); - } - } - - - /** - * Indicates if the table has a column with a column type which we can't - * output to a spreadsheet. - * - * @param table The table metadata. - * @return - */ - boolean tableHasUnsupportedColumns(Table table) { - return Iterables.any(table.columns(), new Predicate() { - @Override - public boolean apply(Column column) { - return !supportedDataTypes.contains(column.getType()); - } - }); - } - - - /** - * Thrown if the excel representation of the table has more than {@link TableOutputter#MAX_EXCEL_ROWS} - * - * @author Copyright (c) Alfa Financial Software 2017 - */ - private class RowLimitExceededException extends RuntimeException { - - public RowLimitExceededException(String message) { - super(message); - } - } - - - /** - * Creates a blank {@link WritableCell} for a given column number and row index. - * - * @param columnNumber the column number - * @param rowIndex the row index - * @return a blank {@link WritableCell} - */ - private WritableCell createBlankWriteableCell(int columnNumber, int rowIndex) { - return new jxl.write.Blank(columnNumber, rowIndex); - } - - - /** - * Creates an {@link UnsupportedOperationException} message suffix for a given - * {@link Column} and {@link WritableSheet}. - * - * @param column the {@link Column} - * @param writableSheet the {@link WritableSheet} - * @return the {@link UnsupportedOperationException} message suffix - */ - private String unsupportedOperationExceptionMessageSuffix(Column column, WritableSheet writableSheet) { - return " in column [" + column.getName() + "] of table [" + writableSheet.getName() + "]"; - } -} +/* Copyright 2017 Alfa Financial Software + * + * 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.alfasoftware.morf.excel; + +import static org.alfasoftware.morf.metadata.DataType.BIG_INTEGER; +import static org.alfasoftware.morf.metadata.DataType.CLOB; +import static org.alfasoftware.morf.metadata.DataType.DECIMAL; +import static org.alfasoftware.morf.metadata.DataType.INTEGER; +import static org.alfasoftware.morf.metadata.DataType.STRING; + +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; + +import org.alfasoftware.morf.dataset.Record; +import org.alfasoftware.morf.metadata.Column; +import org.alfasoftware.morf.metadata.DataType; +import org.alfasoftware.morf.metadata.Table; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.poi.common.usermodel.HyperlinkType; +import org.apache.poi.hssf.util.HSSFColor.HSSFColorPredefined; +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.CellType; +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.Hyperlink; +import org.apache.poi.ss.usermodel.IndexedColors; +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.WorkbookUtil; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; + +/** + * Outputs tables to an excel spreadsheet. + */ +class TableOutputter { + + private static final Log log = LogFactory.getLog(TableOutputter.class); + private static final int NUMBER_OF_ROWS_IN_TITLE = 2; + private static final int MAX_EXCEL_ROWS = 65536; + private static final int MAX_EXCEL_COLUMNS = 256; + private static final int MAX_CELL_CHARACTERS = 32767; + private static final String ID = "id"; + private static final String VERSION = "version"; + private static final Set SUPPORTED_DATA_TYPES = Sets.immutableEnumSet(STRING, DECIMAL, BIG_INTEGER, INTEGER, CLOB); + + private final AdditionalSchemaData additionalSchemaData; + private final Map styleCache = new IdentityHashMap<>(); + + public TableOutputter(AdditionalSchemaData additionalSchemaData) { + this.additionalSchemaData = additionalSchemaData; + } + + public void table(int maxSampleRows, final Workbook workbook, final Table table, final Iterable records) { + final Sheet worksheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(spreadsheetifyName(table.getName()))); + + boolean columnsTruncated = table.columns().size() > MAX_EXCEL_COLUMNS; + if (columnsTruncated) { + log.warn("Output for table '" + table.getName() + "' exceeds the maximum number of columns (" + MAX_EXCEL_COLUMNS + ") in an Excel worksheet. It will be truncated."); + } + + boolean rowsTruncated = false; + try { + int currentRow = NUMBER_OF_ROWS_IN_TITLE + 1; + try { + final Map helpTextRowNumbers = new HashMap<>(); + currentRow = outputHelp(worksheet, workbook, table, currentRow, helpTextRowNumbers); + writeValue(worksheet, currentRow++, 0, "Example Data", getBoldFormat(workbook)); + currentRow = outputDataHeadings(worksheet, workbook, table, currentRow, helpTextRowNumbers); + currentRow = outputExampleData(maxSampleRows, worksheet, workbook, table, currentRow, records); + writeValue(worksheet, currentRow++, 0, "Parameters to Set Up", getBoldFormat(workbook)); + currentRow = outputDataHeadings(worksheet, workbook, table, currentRow, helpTextRowNumbers); + outputExampleData(null, worksheet, workbook, table, currentRow, records); + } catch (RowLimitExceededException e) { + log.warn(e.getMessage()); + rowsTruncated = true; + } + } catch (Exception e) { + throw new RuntimeException("Error outputting table '" + table.getName() + "'", e); + } + + if (columnsTruncated || rowsTruncated) { + StringBuilder suffix = new StringBuilder(" ["); + if (columnsTruncated) { + suffix.append("COLUMNS"); + } + if (columnsTruncated && rowsTruncated) { + suffix.append(" & "); + } + if (rowsTruncated) { + suffix.append("ROWS"); + } + suffix.append(" TRUNCATED]"); + createTitle(worksheet, workbook, worksheet.getSheetName() + suffix, table.getName()); + } else { + createTitle(worksheet, workbook, worksheet.getSheetName(), table.getName()); + } + } + + + + private int outputHelp(Sheet worksheet, Workbook workbook, Table table, int startRow, Map helpTextRowNumbers) { + int currentRow = startRow; + writeValue(worksheet, currentRow++, 0, "Column Descriptions", getBoldFormat(workbook)); + + int currentColumn = 0; + for (Column column : table.columns()) { + if (ID.equals(column.getName()) || VERSION.equals(column.getName())) { + continue; + } + + writeValue(worksheet, currentRow, 0, spreadsheetifyName(column.getName()), getBoldFormat(workbook)); + String typeString = column.getType() + "(" + column.getWidth() + (column.getScale() == 0 ? "" : "," + column.getScale()) + ")"; + writeValue(worksheet, currentRow, 1, typeString, getStandardFormat(workbook)); + writeValue(worksheet, currentRow, 2, additionalSchemaData.columnDefaultValue(table, column.getName()), getStandardFormat(workbook)); + writeValue(worksheet, currentRow, 3, additionalSchemaData.columnDocumentation(table, column.getName()), getWrappedFormat(workbook)); + if (currentColumn >= MAX_EXCEL_COLUMNS) { + writeValue(worksheet, currentRow, 13, "[TRUNCATED]", getBoldFormat(workbook)); + } + helpTextRowNumbers.put(column.getName(), currentRow); + currentRow++; + currentColumn++; + } + return currentRow + 1; + } + + private int outputDataHeadings(Sheet worksheet, Workbook workbook, Table table, int rowIndex, Map helpTextRowNumbers) { + int columnIndex = 0; + for (Column column : table.columns()) { + if (ID.equals(column.getName()) || VERSION.equals(column.getName())) { + continue; + } + if (columnIndex >= MAX_EXCEL_COLUMNS) { + break; + } + Cell cell = writeValue(worksheet, rowIndex, columnIndex, spreadsheetifyName(column.getName()), getBoldHeadingFormat(workbook)); + Integer helpRow = helpTextRowNumbers.get(column.getName()); + if (helpRow != null) { + Hyperlink hyperlink = workbook.getCreationHelper().createHyperlink(HyperlinkType.DOCUMENT); + hyperlink.setAddress("'" + worksheet.getSheetName() + "'!A" + (helpRow + 1)); + cell.setHyperlink(hyperlink); + cell.setCellStyle(getHyperlinkFormat(workbook)); + } + columnIndex++; + } + return rowIndex + 1; + } + + private int outputExampleData(Integer numberOfExamples, Sheet worksheet, Workbook workbook, Table table, int startRow, + Iterable records) { + int currentRow = startRow; + int written = 0; + + for (Record record : records) { + if (currentRow >= MAX_EXCEL_ROWS || hasWrittenRequestedExamples(numberOfExamples, written)) { + break; + } + + outputExampleRow(worksheet, workbook, table, currentRow, record); + currentRow++; + written++; + } + + if (currentRow >= MAX_EXCEL_ROWS) { + throw new RowLimitExceededException("Output for table '" + table.getName() + "' exceeds the maximum number of rows (" + MAX_EXCEL_ROWS + ") in an Excel worksheet. It will be truncated."); + } + return currentRow + 1; + } + + private boolean hasWrittenRequestedExamples(Integer numberOfExamples, int written) { + return numberOfExamples != null && written >= numberOfExamples; + } + + private void outputExampleRow(Sheet worksheet, Workbook workbook, Table table, int rowIndex, Record record) { + int columnIndex = 0; + + for (Column column : table.columns()) { + if (!isSystemColumn(column)) { + if (columnIndex >= MAX_EXCEL_COLUMNS) { + return; + } + writeColumnValue(worksheet, workbook, rowIndex, columnIndex, column, record); + columnIndex++; + } + } + } + + private boolean isSystemColumn(Column column) { + return ID.equals(column.getName()) || VERSION.equals(column.getName()); + } + + private void writeColumnValue(Sheet worksheet, Workbook workbook, int rowIndex, int columnIndex, Column column, Record record) { + CellStyle style = getStandardFormat(workbook); + + switch (column.getType()) { + case STRING: + writeStringCell(worksheet, rowIndex, columnIndex, record.getString(column.getName()), style); + return; + + case DECIMAL: + writeDecimalCell(worksheet, rowIndex, columnIndex, column, record, style); + return; + + case BIG_INTEGER: + case INTEGER: + writeIntegerCell(worksheet, rowIndex, columnIndex, column, record, style); + return; + + case CLOB: + writeClobCell(worksheet, rowIndex, columnIndex, column, record, style); + return; + + default: + throw new UnsupportedOperationException("Cannot output data type [" + column.getType() + "] to a spreadsheet"); + } + } + + private void writeDecimalCell(Sheet worksheet, int rowIndex, int columnIndex, Column column, Record record, CellStyle style) { + BigDecimal decimalValue = record.getBigDecimal(column.getName()); + try { + writeNullableNumericCell(worksheet, rowIndex, columnIndex, decimalValue, style); + } catch (Exception e) { + throw new UnsupportedOperationException( + "Cannot generate Excel cell (parseDouble) for data [" + decimalValue + "]" + + unsupportedOperationExceptionMessageSuffix(column, worksheet), + e); + } + } + + private void writeIntegerCell(Sheet worksheet, int rowIndex, int columnIndex, Column column, Record record, CellStyle style) { + Long longValue = record.getLong(column.getName()); + try { + writeNullableNumericCell(worksheet, rowIndex, columnIndex, longValue, style); + } catch (Exception e) { + throw new UnsupportedOperationException( + "Cannot generate Excel cell (parseInt) for data [" + longValue + "]" + + unsupportedOperationExceptionMessageSuffix(column, worksheet), + e); + } + } + + private void writeClobCell(Sheet worksheet, int rowIndex, int columnIndex, Column column, Record record, CellStyle style) { + try { + String stringValue = record.getString(column.getName()); + if (stringValue == null) { + createBlankCell(worksheet, rowIndex, columnIndex, style); + } else { + writeStringCell( + worksheet, + rowIndex, + columnIndex, + StringUtils.substring(stringValue, 0, MAX_CELL_CHARACTERS), + style); + } + } catch (Exception e) { + throw new UnsupportedOperationException( + "Cannot generate Excel cell for CLOB data" + unsupportedOperationExceptionMessageSuffix(column, worksheet), + e); + } + } + + private void writeNullableNumericCell(Sheet worksheet, int rowIndex, int columnIndex, Number value, CellStyle style) { + if (value == null) { + createBlankCell(worksheet, rowIndex, columnIndex, style); + } else { + writeNumericCell(worksheet, rowIndex, columnIndex, value.doubleValue(), style); + } + } + + private Cell writeValue(Sheet sheet, int rowIndex, int columnIndex, String value, CellStyle style) { + Row row = getOrCreateRow(sheet, rowIndex); + Cell cell = row.createCell(columnIndex); + cell.setCellValue(value); + cell.setCellStyle(style); + return cell; + } + + private void writeStringCell(Sheet sheet, int rowIndex, int columnIndex, String value, CellStyle style) { + if (value == null) { + createBlankCell(sheet, rowIndex, columnIndex, style); + } else { + writeValue(sheet, rowIndex, columnIndex, value, style); + } + } + + private void writeNumericCell(Sheet sheet, int rowIndex, int columnIndex, double value, CellStyle style) { + Row row = getOrCreateRow(sheet, rowIndex); + Cell cell = row.createCell(columnIndex, CellType.NUMERIC); + cell.setCellValue(value); + cell.setCellStyle(style); + } + + private void createBlankCell(Sheet sheet, int rowIndex, int columnIndex, CellStyle style) { + Row row = getOrCreateRow(sheet, rowIndex); + Cell cell = row.createCell(columnIndex, CellType.BLANK); + cell.setCellStyle(style); + } + + private void createTitle(Sheet sheet, Workbook workbook, String title, String fileName) { + Cell titleCell = writeValue(sheet, 0, 0, title, titleStyle(workbook)); + titleCell.setCellStyle(titleStyle(workbook)); + + Cell fileNameCell = writeValue(sheet, 1, 1, fileName, hiddenFileNameStyle(workbook)); + fileNameCell.setCellStyle(hiddenFileNameStyle(workbook)); + + Cell copyrightCell = writeValue(sheet, 0, 12, + "Copyright " + new SimpleDateFormat("yyyy").format(new Date()) + " Alfa Financial Software Ltd.", + copyrightStyle(workbook)); + copyrightCell.setCellStyle(copyrightStyle(workbook)); + } + + private CellStyle titleStyle(Workbook workbook) { + return getStyles(workbook).titleStyle; + } + + private CellStyle hiddenFileNameStyle(Workbook workbook) { + return getStyles(workbook).hiddenFileNameStyle; + } + + private CellStyle copyrightStyle(Workbook workbook) { + return getStyles(workbook).copyrightStyle; + } + + private String spreadsheetifyName(String name) { + return StringUtils.capitalize(name).replaceAll("([A-Z][a-z])", " $1").trim(); + } + + private CellStyle getStandardFormat(Workbook workbook) { + return getStyles(workbook).standardFormat; + } + + private CellStyle getWrappedFormat(Workbook workbook) { + return getStyles(workbook).wrappedFormat; + } + + private CellStyle getBoldFormat(Workbook workbook) { + return getStyles(workbook).boldFormat; + } + + private CellStyle getBoldHeadingFormat(Workbook workbook) { + return getStyles(workbook).boldHeadingFormat; + } + + private CellStyle getHyperlinkFormat(Workbook workbook){ + return getStyles(workbook).hyperlinkFormat; + } + + private CellStyles getStyles(Workbook workbook) { + return styleCache.computeIfAbsent(workbook, CellStyles::new); + } + + boolean tableHasUnsupportedColumns(Table table) { + return Iterables.any(table.columns(), column -> !SUPPORTED_DATA_TYPES.contains(column.getType())); + } + + private Row getOrCreateRow(Sheet sheet, int rowIndex) { + Row row = sheet.getRow(rowIndex); + return row == null ? sheet.createRow(rowIndex) : row; + } + + private String unsupportedOperationExceptionMessageSuffix(Column column, Sheet worksheet) { + return " in column [" + column.getName() + "] of table [" + worksheet.getSheetName() + "]"; + } + + private static final class CellStyles { + private final CellStyle titleStyle; + private final CellStyle hiddenFileNameStyle; + private final CellStyle copyrightStyle; + private final CellStyle standardFormat; + private final CellStyle wrappedFormat; + private final CellStyle boldFormat; + private final CellStyle boldHeadingFormat; + private final CellStyle hyperlinkFormat; + + private CellStyles(Workbook workbook) { + Font standardFont = workbook.createFont(); + standardFont.setFontHeightInPoints((short) 8); + + Font boldFont = workbook.createFont(); + boldFont.setBold(true); + boldFont.setFontHeightInPoints((short) 8); + + Font titleFont = workbook.createFont(); + titleFont.setBold(true); + titleFont.setFontHeightInPoints((short) 16); + + Font hyperlinkFont = workbook.createFont(); + hyperlinkFont.setUnderline(Font.U_SINGLE); + hyperlinkFont.setColor(IndexedColors.BLUE.getIndex()); + hyperlinkFont.setFontHeightInPoints((short) 8); + + Font hiddenFileNameFont = workbook.createFont(); + hiddenFileNameFont.setColor(HSSFColorPredefined.WHITE.getIndex()); + + titleStyle = workbook.createCellStyle(); + titleStyle.setFont(titleFont); + + hiddenFileNameStyle = workbook.createCellStyle(); + hiddenFileNameStyle.setFont(hiddenFileNameFont); + + copyrightStyle = workbook.createCellStyle(); + copyrightStyle.setAlignment(HorizontalAlignment.RIGHT); + + standardFormat = workbook.createCellStyle(); + standardFormat.setVerticalAlignment(VerticalAlignment.TOP); + standardFormat.setFont(standardFont); + + wrappedFormat = workbook.createCellStyle(); + wrappedFormat.cloneStyleFrom(standardFormat); + wrappedFormat.setWrapText(true); + + boldFormat = workbook.createCellStyle(); + boldFormat.setVerticalAlignment(VerticalAlignment.TOP); + boldFormat.setFont(boldFont); + + boldHeadingFormat = workbook.createCellStyle(); + boldHeadingFormat.cloneStyleFrom(boldFormat); + boldHeadingFormat.setBorderBottom(BorderStyle.MEDIUM); + boldHeadingFormat.setVerticalAlignment(VerticalAlignment.CENTER); + boldHeadingFormat.setFillForegroundColor(HSSFColorPredefined.GREY_25_PERCENT.getIndex()); + boldHeadingFormat.setFillPattern(FillPatternType.SOLID_FOREGROUND); + + hyperlinkFormat = workbook.createCellStyle(); + hyperlinkFormat.setVerticalAlignment(VerticalAlignment.TOP); + hyperlinkFormat.setFont(hyperlinkFont); + } + } + + private static class RowLimitExceededException extends RuntimeException { + RowLimitExceededException(String message) { + super(message); + } + } +} diff --git a/morf-excel/src/main/resources/org/alfasoftware/morf/excel/package.html b/morf-excel/src/main/resources/org/alfasoftware/morf/excel/package.html old mode 100755 new mode 100644 diff --git a/morf-excel/src/test/java/org/alfasoftware/morf/excel/TestSpreadsheetDataSetConsumer.java b/morf-excel/src/test/java/org/alfasoftware/morf/excel/TestSpreadsheetDataSetConsumer.java old mode 100755 new mode 100644 index 7db52ec74..097e44f49 --- a/morf-excel/src/test/java/org/alfasoftware/morf/excel/TestSpreadsheetDataSetConsumer.java +++ b/morf-excel/src/test/java/org/alfasoftware/morf/excel/TestSpreadsheetDataSetConsumer.java @@ -4,7 +4,7 @@ * 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 + * 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, @@ -12,61 +12,51 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.alfasoftware.morf.excel; import static org.alfasoftware.morf.metadata.SchemaUtils.table; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.mockito.ArgumentMatchers.nullable; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.OutputStream; -import java.util.Map; import java.util.Optional; +import org.alfasoftware.morf.dataset.DataSetConsumer.CloseState; import org.alfasoftware.morf.dataset.Record; import org.alfasoftware.morf.metadata.Table; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.WorkbookUtil; import org.junit.Test; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import jxl.write.WritableWorkbook; - - -/** - * Ensure that {@link SpreadsheetDataSetConsumer} works correctly. Particularly: - * - *
    - *
  • configuration must be picked up correctly
  • - *
- * - * @author Copyright (c) Alfa Financial Software 2010 - */ public class TestSpreadsheetDataSetConsumer { - private static final ImmutableList NO_RECORDS = ImmutableList.of(); + private static final ImmutableList NO_RECORDS = ImmutableList.of(); - /** - * Ensure that a table that is not configured is not output. - */ @Test public void testIgnoreTable() { final MockTableOutputter outputter = new MockTableOutputter(); - - final SpreadsheetDataSetConsumer consumer = - new SpreadsheetDataSetConsumer( - mock(OutputStream.class), - Optional.>of(ImmutableMap.of("COMPANY", 5)), - outputter - ); + final SpreadsheetDataSetConsumer consumer = new SpreadsheetDataSetConsumer( + mock(OutputStream.class), + Optional.of(ImmutableMap.of("COMPANY", 5)), + outputter); consumer.table(table("NotCompany"), NO_RECORDS); @@ -77,74 +67,143 @@ public void testIgnoreTable() { @Test public void testUnsupportedColumns() { TableOutputter outputter = mock(TableOutputter.class); - final SpreadsheetDataSetConsumer consumer = - new SpreadsheetDataSetConsumer( - mock(OutputStream.class), - Optional.>empty(), - outputter - ); + final SpreadsheetDataSetConsumer consumer = new SpreadsheetDataSetConsumer( + mock(OutputStream.class), + Optional.empty(), + outputter); Table one = table("one"); Table two = table("two"); + when(outputter.tableHasUnsupportedColumns(one)).thenReturn(true); when(outputter.tableHasUnsupportedColumns(two)).thenReturn(false); consumer.table(one, NO_RECORDS); consumer.table(two, NO_RECORDS); - verify(outputter).table(nullable(Integer.class), nullable(WritableWorkbook.class), eq(two), eq(NO_RECORDS)); - verify(outputter, times(0)).table(nullable(Integer.class), nullable(WritableWorkbook.class), eq(one), eq(NO_RECORDS)); + verify(outputter).table(nullable(Integer.class), nullable(Workbook.class), eq(two), eq(NO_RECORDS)); + verify(outputter, times(0)).table(nullable(Integer.class), nullable(Workbook.class), eq(one), eq(NO_RECORDS)); } - /** - * Mock for {@link TableOutputter} that tracks what table should have been - * output and with how many rows. - * - * @author Copyright (c) Alfa Financial Software 2010 - */ - private static class MockTableOutputter extends TableOutputter { + @Test + public void testIncludeTableWithSpecificRowCount() { + final MockTableOutputter outputter = new MockTableOutputter(); + SpreadsheetDataSetConsumer consumer = new SpreadsheetDataSetConsumer( + mock(OutputStream.class), + Optional.of(ImmutableMap.of("COMPANY", 5)), + outputter); - /** - * The name of the table passed in to {@link #table(TableEntry, WritableWorkbook, Table, Iterable)}. - */ - private String tableReceived; + consumer.table(table("Company"), NO_RECORDS); + + assertEquals("Table passed through for output", "Company", outputter.tableReceived); + assertEquals("Number of rows desired", 5, outputter.rowCountReceived); + } + + + @Test + public void testCloseCreatesIndexAndNavigationHyperlinks() throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + SpreadsheetDataSetConsumer consumer = new SpreadsheetDataSetConsumer( + outputStream, + Optional.empty(), + new WritingTableOutputter()); + + consumer.open(); + consumer.table(table("Company"), NO_RECORDS); + consumer.close(CloseState.COMPLETE); + + try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(outputStream.toByteArray()))) { + assertEquals("Index sheet should be first", "Index", workbook.getSheetAt(0).getSheetName()); + assertEquals("Workbook should contain index and table sheet", 2, workbook.getNumberOfSheets()); + + Row indexRow = workbook.getSheet("Index").getRow(2); + assertEquals("Table name should be written to index", "Company", indexRow.getCell(0).getStringCellValue()); + assertNotNull("Index entry should have a hyperlink", indexRow.getCell(0).getHyperlink()); + assertEquals("Index hyperlink should point to the table sheet", "'Company'!A1", indexRow.getCell(0).getHyperlink().getAddress()); + assertEquals("Filename should be copied from the table sheet", "Company file", indexRow.getCell(1).getStringCellValue()); + + Row backLinkRow = workbook.getSheet("Company").getRow(1); + assertEquals("Back-link text should be added to the table sheet", "Back to index", backLinkRow.getCell(0).getStringCellValue()); + assertNotNull("Back-link should have a hyperlink", backLinkRow.getCell(0).getHyperlink()); + assertEquals("Back-link should point back to the index row", "'Index'!A3", backLinkRow.getCell(0).getHyperlink().getAddress()); + } + } + + + @Test + public void testCloseWrapsIOException() { + SpreadsheetDataSetConsumer consumer = new SpreadsheetDataSetConsumer( + new OutputStream() { + @Override + public void write(int b) throws IOException { + throw new IOException("boom"); + } + }, + Optional.empty(), + new WritingTableOutputter()); + + consumer.open(); + + try { + consumer.close(CloseState.COMPLETE); + } catch (RuntimeException e) { + assertEquals("Error closing writable workbook", e.getMessage()); + assertNotNull("Cause should be preserved", e.getCause()); + assertEquals("boom", e.getCause().getMessage()); + return; + } - /** - * The number of rows that were requested in the output. - */ - private Number rowCountReceived; + throw new AssertionError("Exception should have been thrown"); + } - public MockTableOutputter() { + + @Test + public void testWritingTableOutputterUsesSafeSheetNames() throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + SpreadsheetDataSetConsumer consumer = new SpreadsheetDataSetConsumer( + outputStream, + Optional.empty(), + new WritingTableOutputter()); + + consumer.open(); + consumer.table(table("Bad:/Name*[]?"), NO_RECORDS); + consumer.close(CloseState.COMPLETE); + + try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(outputStream.toByteArray()))) { + String safeSheetName = WorkbookUtil.createSafeSheetName("Bad:/Name*[]?"); + assertNotNull("Unsafe table name should be converted to a valid sheet name", workbook.getSheet(safeSheetName)); + assertTrue("Original unsafe sheet name should not exist", workbook.getSheet("Bad:/Name*[]?") == null); + } + } + + private static class MockTableOutputter extends TableOutputter { + private String tableReceived; + private Integer rowCountReceived; + + MockTableOutputter() { super(new DefaultAdditionalSchemaDataImpl()); } - /** - * @see org.alfasoftware.morf.excel.TableOutputter#table(com.chpconsulting.cryo.excel.TableConfigurationProvider.TableEntry, jxl.write.WritableWorkbook, org.alfasoftware.morf.metadata.Table, java.lang.Iterable) - */ @Override - public void table(int maxRows, WritableWorkbook workbook, Table table, Iterable records) { + public void table(int maxRows, Workbook workbook, Table table, Iterable records) { tableReceived = table.getName(); rowCountReceived = maxRows; } } - /** - * Ensure that a table with a specific row count is included in the set of - * tables that are output. - */ - @Test - public void testIncludeTableWithSpecificRowCount() { - final MockTableOutputter outputter = new MockTableOutputter(); - SpreadsheetDataSetConsumer consumer = new SpreadsheetDataSetConsumer( - mock(OutputStream.class), - Optional.>of(ImmutableMap.of("COMPANY", 5)), - outputter - ); - consumer.table(table("Company"), NO_RECORDS); + private static final class WritingTableOutputter extends TableOutputter { - assertEquals("Table passed through for output", "Company", outputter.tableReceived); - assertEquals("Number of rows desired", Integer.valueOf(5), outputter.rowCountReceived); + WritingTableOutputter() { + super(new DefaultAdditionalSchemaDataImpl()); + } + + @Override + public void table(int maxRows, Workbook workbook, Table table, Iterable records) { + org.apache.poi.ss.usermodel.Sheet sheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(table.getName())); + sheet.createRow(0).createCell(0).setCellValue(table.getName()); + sheet.createRow(1).createCell(1).setCellValue(table.getName() + " file"); + } } } diff --git a/morf-excel/src/test/java/org/alfasoftware/morf/excel/TestSpreadsheetDataSetProducer.java b/morf-excel/src/test/java/org/alfasoftware/morf/excel/TestSpreadsheetDataSetProducer.java old mode 100755 new mode 100644 index fe2d6888e..7c85bac86 --- a/morf-excel/src/test/java/org/alfasoftware/morf/excel/TestSpreadsheetDataSetProducer.java +++ b/morf-excel/src/test/java/org/alfasoftware/morf/excel/TestSpreadsheetDataSetProducer.java @@ -1,116 +1,264 @@ -/* Copyright 2017 Alfa Financial Software - * - * 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.alfasoftware.morf.excel; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import java.io.InputStream; -import java.net.URISyntaxException; -import java.util.Collection; -import java.util.List; - -import org.alfasoftware.morf.dataset.Record; -import org.junit.Test; - -import com.google.common.collect.Lists; - -/** - * Ensure that the {@link SpreadsheetDataSetProducer} works correctly. - * - * @author Copyright (c) Alfa Financial Software 2010 - */ -public class TestSpreadsheetDataSetProducer { - - /** - * Ensure that the schema can be retrieved from a set of excel files - * - * @throws URISyntaxException ... - */ - @Test - public void testGetSchema() throws URISyntaxException { - final SpreadsheetDataSetProducer producer = produceTestSpreadsheet(); - - // Check that the table names were picked up correctly - Collection tableNames = producer.getSchema().tableNames(); - assertEquals("Number of tables found [" + tableNames + "]", 12, tableNames.size()); - assertTrue("Tables correctly populated [" + tableNames + "]", tableNames.contains("AssetType")); - assertTrue("Tables correctly populated [" + tableNames + "]", tableNames.contains("Allowance")); - } - - - /** - * Ensure that the rows can be retrieved from a set of excel files - * - * @throws URISyntaxException ... - */ - @Test - public void testGetRecords() throws URISyntaxException { - final SpreadsheetDataSetProducer producer = produceTestSpreadsheet(); - - // Check that the table names were picked up correctly - List records = Lists.newArrayList(producer.records("UsageMeterType")); - assertEquals("Number of rows [" + records + "]", 2, records.size()); - Record record = records.get(0); - assertEquals("Usage Meter", "KM", record.getString("usageMeter")); - assertEquals("Description", "Kilometers", record.getString("description")); - assertEquals("Usage Meter Type", "M", record.getString("usageMeterType")); - - record = records.get(1); - assertEquals("Usage Meter", "HOUR", record.getString("usageMeter")); - assertEquals("Description", "Hours", record.getString("description")); - assertEquals("Usage Meter Type", "", record.getString("usageMeterType")); - } - - - /** - * Ensure that the generated schema behaves as expected - * - * @throws URISyntaxException ... - */ - @Test - public void testGetSchemaMethods() throws URISyntaxException { - final SpreadsheetDataSetProducer producer = produceTestSpreadsheet(); - try { - // Getting the metadata of a table is an unsupported method of the - // minimal schema - producer.getSchema().getTable("Name"); - fail("Exception should have been thrown"); - } catch (UnsupportedOperationException e) { - // This is expected - } - - try { - // Getting all tables is an unsupported method of the minimal schema - producer.getSchema().tables(); - fail("Exception should have been thrown"); - } catch (UnsupportedOperationException e) { - // This is expected - } - - assertFalse("Schema is not empty", producer.getSchema().isEmptyDatabase()); - assertTrue("UsageMeterType table exists", producer.getSchema().tableExists("UsageMeterType")); - } - - - private SpreadsheetDataSetProducer produceTestSpreadsheet() { - InputStream testExcel = getClass().getResourceAsStream("TestSpreadsheetDataSetProducer.xls"); - final SpreadsheetDataSetProducer producer = new SpreadsheetDataSetProducer(testExcel); - return producer; - } -} +/* Copyright 2017 Alfa Financial Software + * + * 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.alfasoftware.morf.excel; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.List; + +import org.alfasoftware.morf.dataset.Record; +import org.apache.poi.common.usermodel.HyperlinkType; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Workbook; +import org.junit.Test; + +import com.google.common.collect.Lists; + + +/** + * Ensure that the {@link SpreadsheetDataSetProducer} works correctly. + * + * @author Copyright (c) Alfa Financial Software 2010 + */ +public class TestSpreadsheetDataSetProducer { + + /** + * Ensure that the schema can be retrieved from a set of excel files + */ + @Test + public void testGetSchema() { + final SpreadsheetDataSetProducer producer = produceTestSpreadsheet(); + + // Check that the table names were picked up correctly + Collection tableNames = producer.getSchema().tableNames(); + assertEquals("Number of tables found [" + tableNames + "]", 12, tableNames.size()); + assertTrue("Tables correctly populated [" + tableNames + "]", tableNames.contains("AssetType")); + assertTrue("Tables correctly populated [" + tableNames + "]", tableNames.contains("Allowance")); + } + + + /** + * Ensure that the rows can be retrieved from a set of excel files + */ + @Test + public void testGetRecords() { + final SpreadsheetDataSetProducer producer = produceTestSpreadsheet(); + + // Check that the table names were picked up correctly + List records = Lists.newArrayList(producer.records("UsageMeterType")); + assertEquals("Number of rows [" + records + "]", 2, records.size()); + + Record record = records.get(0); + assertEquals("Usage Meter", "KM", record.getString("usageMeter")); + assertEquals("Description", "Kilometers", record.getString("description")); + assertEquals("Usage Meter Type", "M", record.getString("usageMeterType")); + + record = records.get(1); + assertEquals("Usage Meter", "HOUR", record.getString("usageMeter")); + assertEquals("Description", "Hours", record.getString("description")); + assertEquals("Usage Meter Type", "", record.getString("usageMeterType")); + } + + + /** + * Ensure that the generated schema behaves as expected + */ + @Test + public void testGetSchemaMethods() { + final SpreadsheetDataSetProducer producer = produceTestSpreadsheet(); + + try { + // Getting the metadata of a table is an unsupported method of the + // minimal schema + producer.getSchema().getTable("Name"); + fail("Exception should have been thrown"); + } catch (UnsupportedOperationException e) { + // Expected + } + + try { + // Getting all tables is an unsupported method of the minimal schema + producer.getSchema().tables(); + fail("Exception should have been thrown"); + } catch (UnsupportedOperationException e) { + // Expected + } + + assertFalse("Schema is not empty", producer.getSchema().isEmptyDatabase()); + assertTrue("UsageMeterType table exists", producer.getSchema().tableExists("UsageMeterType")); + } + + + @Test + public void testNullInputStreamRejected() { + try { + new SpreadsheetDataSetProducer((InputStream) null); + fail("Exception should have been thrown"); + } catch (IllegalArgumentException e) { + assertEquals("Spreadsheet input stream was null", e.getMessage()); + } + } + + + @Test + public void testUnquotedHyperlinkAddressIsParsedAndBlankRowStopsRecords() throws Exception { + SpreadsheetDataSetProducer producer = new SpreadsheetDataSetProducer(new ByteArrayInputStream( + workbookBytes(workbookWithSingleTable("UsageMeterType", "UsageMeterType!A1", true, true, true, false, false)))); + + List records = Lists.newArrayList(producer.records("UsageMeterType")); + assertEquals("One populated data row should be parsed", 1, records.size()); + assertEquals("KM", records.get(0).getString("usageMeter")); + assertEquals("Kilometers", records.get(0).getString("description")); + assertEquals("1", records.get(0).getString("id")); + assertEquals("1", records.get(0).getString("translationId")); + } + + @Test + public void testMissingHyperlinkInIndexIsWrapped() throws Exception { + try { + new SpreadsheetDataSetProducer(new ByteArrayInputStream( + workbookBytes(workbookWithSingleTable("UsageMeterType", null, true, false, false, false, false)))); + fail("Exception should have been thrown"); + } catch (RuntimeException e) { + assertEquals("Failed to parse spreadsheet", e.getMessage()); + assertTrue("Root cause should mention the missing hyperlink", + findMessage(e, "Failed to find hyperlink in index sheet at [0,2]")); + } + } + + @Test + public void testMissingWorksheetIsWrapped() throws Exception { + try { + new SpreadsheetDataSetProducer(new ByteArrayInputStream( + workbookBytes(workbookWithSingleTable("UsageMeterType", "'MissingSheet'!A1", false, true, false, false, false)))); + fail("Exception should have been thrown"); + } catch (RuntimeException e) { + assertEquals("Failed to parse spreadsheet", e.getMessage()); + assertTrue("Root cause should mention the missing worksheet", + findMessage(e, "Failed to find worksheet with name [MissingSheet]")); + } + } + + @Test + public void testMissingHeaderRowIsWrapped() throws Exception { + try { + new SpreadsheetDataSetProducer(new ByteArrayInputStream( + workbookBytes(workbookWithSingleTable("UsageMeterType", "'UsageMeterType'!A1", true, true, false, true, false)))); + fail("Exception should have been thrown"); + } catch (RuntimeException e) { + assertEquals("Failed to parse spreadsheet", e.getMessage()); + assertTrue("Root cause should mention the missing header row", + findMessage(e, "Could not find header row in worksheet [UsageMeterType]")); + } + } + + @Test + public void testNoTranslationColumnStillProducesRecords() throws Exception { + SpreadsheetDataSetProducer producer = new SpreadsheetDataSetProducer(new ByteArrayInputStream( + workbookBytes(workbookWithSingleTable("UsageMeterType", "'UsageMeterType'!A1", true, true, false, false, true)))); + + List records = Lists.newArrayList(producer.records("UsageMeterType")); + assertEquals("One populated data row should be parsed", 1, records.size()); + assertEquals("KM", records.get(0).getString("usageMeter")); + assertEquals("Kilometers", records.get(0).getString("description")); + assertEquals("0", String.valueOf(records.get(0).getInteger("translationId"))); + } + + @Test + public void testQuotedHyperlinkAddressResolvesWorksheet() throws Exception { + SpreadsheetDataSetProducer producer = new SpreadsheetDataSetProducer(new ByteArrayInputStream( + workbookBytes(workbookWithSingleTable("UsageMeterType", "'UsageMeterType'!A1", true, true, false, false, true)))); + + List records = Lists.newArrayList(producer.records("UsageMeterType")); + assertEquals(1, records.size()); + assertNotNull(records.get(0)); + } + + private SpreadsheetDataSetProducer produceTestSpreadsheet() { + InputStream testExcel = getClass().getResourceAsStream("TestSpreadsheetDataSetProducer.xls"); + return new SpreadsheetDataSetProducer(testExcel); + } + + private Workbook workbookWithSingleTable(String tableName, String hyperlinkAddress, boolean createWorksheet, + boolean indexHasHyperlink, boolean useTranslationColumn, boolean omitHeaderHyperlink, boolean noTranslationColumn) { + Workbook workbook = new HSSFWorkbook(); + + org.apache.poi.ss.usermodel.Sheet index = workbook.createSheet("Index"); + Row indexRow = index.createRow(2); + if (indexHasHyperlink) { + indexRow.createCell(0).setCellValue(tableName); + indexRow.getCell(0).setHyperlink(workbook.getCreationHelper().createHyperlink(HyperlinkType.DOCUMENT)); + indexRow.getCell(0).getHyperlink().setAddress(hyperlinkAddress); + } + indexRow.createCell(1).setCellValue(tableName); + + if (createWorksheet) { + org.apache.poi.ss.usermodel.Sheet tableSheet = workbook.createSheet(tableName); + tableSheet.createRow(0).createCell(0).setCellValue(tableName); + tableSheet.createRow(1).createCell(0).setCellValue("Parameters to Set Up"); + + Row headerRow = tableSheet.createRow(2); + headerRow.createCell(0).setCellValue("Usage Meter"); + if (!omitHeaderHyperlink) { + headerRow.getCell(0).setHyperlink(workbook.getCreationHelper().createHyperlink(HyperlinkType.DOCUMENT)); + headerRow.getCell(0).getHyperlink().setAddress("'Index'!A1"); + } + headerRow.createCell(1).setCellValue("Description"); + if (useTranslationColumn && !noTranslationColumn) { + headerRow.createCell(2).setCellValue(""); + headerRow.createCell(3).setCellValue("Translation"); + } + + Row dataRow = tableSheet.createRow(3); + dataRow.createCell(0).setCellValue("KM"); + dataRow.createCell(1).setCellValue("Kilometers"); + if (useTranslationColumn && !noTranslationColumn) { + dataRow.createCell(3).setCellValue("M"); + } + + tableSheet.createRow(4); + } + + return workbook; + } + + private byte[] workbookBytes(Workbook workbook) throws IOException { + try (Workbook closeableWorkbook = workbook; ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + closeableWorkbook.write(outputStream); + return outputStream.toByteArray(); + } + } + + private boolean findMessage(Throwable throwable, String expectedMessage) { + Throwable current = throwable; + while (current != null) { + if (expectedMessage.equals(current.getMessage())) { + return true; + } + current = current.getCause(); + } + return false; + } +} diff --git a/morf-excel/src/test/java/org/alfasoftware/morf/excel/TestTableOutputter.java b/morf-excel/src/test/java/org/alfasoftware/morf/excel/TestTableOutputter.java index 34759b858..ce7d67f89 100644 --- a/morf-excel/src/test/java/org/alfasoftware/morf/excel/TestTableOutputter.java +++ b/morf-excel/src/test/java/org/alfasoftware/morf/excel/TestTableOutputter.java @@ -1,13 +1,10 @@ package org.alfasoftware.morf.excel; import static org.alfasoftware.morf.metadata.SchemaUtils.column; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.math.BigDecimal; @@ -18,33 +15,23 @@ import org.alfasoftware.morf.metadata.Column; import org.alfasoftware.morf.metadata.DataType; import org.alfasoftware.morf.metadata.Table; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; import org.junit.Before; import org.junit.Test; -import org.mockito.ArgumentCaptor; import com.google.common.collect.ImmutableList; -import jxl.Cell; -import jxl.write.WritableCell; -import jxl.write.WritableSheet; -import jxl.write.WritableWorkbook; -import jxl.write.WriteException; - -/** - * Ensure that {@link TableOutputter} works correctly. - * - * @author Copyright (c) Alfa Financial Software 2023 - */ + public class TestTableOutputter { private Table table; private TableOutputter tableOutputter; - private WritableSheet writableSheet; - private WritableWorkbook writableWorkbook; - + private Workbook workbook; + private Sheet sheet; + private final DataFormatter formatter = new DataFormatter(); - /** - * Set up the test. - */ @Before public void setUp() { table = mock(Table.class); @@ -54,247 +41,155 @@ public void setUp() { column("Col2", DataType.DECIMAL), column("Col3", DataType.BIG_INTEGER), column("Col4", DataType.INTEGER), - column("Col5", DataType.CLOB) - )); - - writableWorkbook = mock(WritableWorkbook.class); - writableSheet = mock(WritableSheet.class); - when(writableSheet.getName()).thenReturn("Sheet1"); - when(writableSheet.getWritableCell(anyInt(), anyInt())).thenReturn(mock(WritableCell.class)); - when(writableSheet.getCell(anyInt(), anyInt())).thenReturn(mock(Cell.class)); - when(writableWorkbook.createSheet(any(String.class), any(Integer.class))).thenReturn(writableSheet); + column("Col5", DataType.CLOB))); + workbook = new HSSFWorkbook(); AdditionalSchemaData additionalSchemaData = mock(AdditionalSchemaData.class); + when(additionalSchemaData.columnDocumentation(table, "Col1")).thenReturn(""); + when(additionalSchemaData.columnDocumentation(table, "Col2")).thenReturn(""); + when(additionalSchemaData.columnDocumentation(table, "Col3")).thenReturn(""); + when(additionalSchemaData.columnDocumentation(table, "Col4")).thenReturn(""); + when(additionalSchemaData.columnDocumentation(table, "Col5")).thenReturn(""); + when(additionalSchemaData.columnDefaultValue(table, "Col1")).thenReturn(""); + when(additionalSchemaData.columnDefaultValue(table, "Col2")).thenReturn(""); + when(additionalSchemaData.columnDefaultValue(table, "Col3")).thenReturn(""); + when(additionalSchemaData.columnDefaultValue(table, "Col4")).thenReturn(""); + when(additionalSchemaData.columnDefaultValue(table, "Col5")).thenReturn(""); tableOutputter = new TableOutputter(additionalSchemaData); } - - /** - * Tests that a table with fields containing different populated data types is written correctly to the worksheet. - * - * @throws {@link WriteException} if an error occurs - */ @Test - public void testTableOutputWithFieldsPopulated() throws WriteException { - // Given + public void testTableOutputWithFieldsPopulated() { Record record1 = mock(Record.class); when(record1.getString("Col1")).thenReturn("STRING VALUE"); when(record1.getBigDecimal("Col2")).thenReturn(BigDecimal.valueOf(12.34)); when(record1.getLong("Col3")).thenReturn(22L); when(record1.getLong("Col4")).thenReturn(33L); when(record1.getString("Col5")).thenReturn("CLOB VALUE"); - ImmutableList recordList = ImmutableList.of(record1); - - // When - tableOutputter.table(1, writableWorkbook, table, recordList); - - ArgumentCaptor writableCellCaptor = ArgumentCaptor.forClass(WritableCell.class); - verify(writableSheet, times(36)).addCell(writableCellCaptor.capture()); - List capturedWritableCellList = writableCellCaptor.getAllValues(); - // Example Data - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,12, 0, "STRING VALUE")); - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,12, 1, BigDecimal.valueOf(12.34).toString())); - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,12, 2, Long.valueOf(22).toString())); - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,12, 3, Long.valueOf(33).toString())); - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,12, 4, "CLOB VALUE")); - - // Parameters to Set Up - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,16, 0, "STRING VALUE")); - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,16, 1, BigDecimal.valueOf(12.34).toString())); - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,16, 2, Long.valueOf(22).toString())); - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,16, 3, Long.valueOf(33).toString())); - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,16, 4, "CLOB VALUE")); - } + tableOutputter.table(1, workbook, table, ImmutableList.of(record1)); + sheet = workbook.getSheetAt(0); + + assertEquals("STRING VALUE", value(12, 0)); + assertEquals("12.34", value(12, 1)); + assertEquals("22", value(12, 2)); + assertEquals("33", value(12, 3)); + assertEquals("CLOB VALUE", value(12, 4)); + + assertEquals("STRING VALUE", value(16, 0)); + assertEquals("12.34", value(16, 1)); + assertEquals("22", value(16, 2)); + assertEquals("33", value(16, 3)); + assertEquals("CLOB VALUE", value(16, 4)); + } - /** - * Tests that a table with fields containing different unpopulated data types is written correctly to the worksheet. - * - * @throws {@link WriteException} if an error occurs - */ @Test - public void testTableOutputWithFieldsUnpopulated() throws WriteException { - // Given + public void testTableOutputWithFieldsUnpopulated() { Record record1 = mock(Record.class); - ImmutableList recordList = ImmutableList.of(record1); - - // When - tableOutputter.table(1, writableWorkbook, table, recordList); - - ArgumentCaptor writableCellCaptor = ArgumentCaptor.forClass(WritableCell.class); - verify(writableSheet, times(36)).addCell(writableCellCaptor.capture()); - List capturedWritableCellList = writableCellCaptor.getAllValues(); - // Example Data - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,12, 0, "")); - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,12, 1, "")); - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,12, 2, "0")); - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,12, 3, "0")); - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,12, 4, "")); - - // Parameters to Set Up - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,16, 0, "")); - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,16, 1, "")); - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,16, 2, "0")); - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,16, 3, "0")); - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,16, 4, "")); - } + tableOutputter.table(1, workbook, table, ImmutableList.of(record1)); + sheet = workbook.getSheetAt(0); + + assertEquals("", value(12, 0)); + assertEquals("", value(12, 1)); + assertEquals("0", value(12, 2)); + assertEquals("0", value(12, 3)); + assertEquals("", value(12, 4)); + + assertEquals("", value(16, 0)); + assertEquals("", value(16, 1)); + assertEquals("0", value(16, 2)); + assertEquals("0", value(16, 3)); + assertEquals("", value(16, 4)); + } - /** - * Tests that columns are truncated correctly. - * - * @throws {@link WriteException} if an error occurs - */ @Test - public void testColumnTruncation() throws WriteException { - // Given + public void testColumnTruncation() { Record record1 = mock(Record.class); List columnList = new ArrayList<>(); - for (int c = 1; c <= 257; c++) { columnList.add(column("Col" + c, DataType.STRING)); - when(record1.getString("Col") + c).thenReturn("Value" + c); + when(record1.getString("Col" + c)).thenReturn("Value" + c); } - when(table.columns()).thenReturn(columnList); - ImmutableList recordList = ImmutableList.of(record1); - // When - tableOutputter.table(1, writableWorkbook, table, recordList); + tableOutputter.table(1, workbook, table, ImmutableList.of(record1)); + sheet = workbook.getSheetAt(0); - // Then - ArgumentCaptor writableCellCaptor = ArgumentCaptor.forClass(WritableCell.class); - verify(writableSheet, times(1547)).addCell(writableCellCaptor.capture()); - List capturedWritableCellList = writableCellCaptor.getAllValues(); - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,260, 13, "[TRUNCATED]")); + assertEquals("[TRUNCATED]", value(260, 13)); } - - - /** - * Tests that rows are truncated correctly. - * - * @throws {@link WriteException} if an error occurs - */ @Test - public void testRowTruncation() throws WriteException { - // Given - Record record1 = mock(Record.class); + public void testRowTruncation() { when(table.columns()).thenReturn(ImmutableList.of(column("Col1", DataType.STRING))); List recordList = new ArrayList<>(); - for (int r = 1; r < 65537; r++) { recordList.add(mock(Record.class)); } - // When - tableOutputter.table(1, writableWorkbook, table, recordList); + tableOutputter.table(1, workbook, table, recordList); + sheet = workbook.getSheetAt(0); - // Then - ArgumentCaptor writableCellCaptor = ArgumentCaptor.forClass(WritableCell.class); - verify(writableSheet, times(65535)).addCell(writableCellCaptor.capture()); - List capturedWritableCellList = writableCellCaptor.getAllValues(); - - for (int i = 0; i < capturedWritableCellList.size(); i++) { - if (capturedWritableCellList.get(i).getContents().contains("TRUNC")) { - System.out.println("x"); - } - } - - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,0, 0, "Sheet1 [ROWS TRUNCATED]")); + assertTrue(value(0, 0).contains("ROWS TRUNCATED")); } - - /** - * Tests that a clob value that exceeds the max length supported by Excel is truncated before being written to the worksheet. - * - * @throws {@link WriteException} if an error occurs - */ @Test - public void testClobTruncation() throws WriteException { - // Given - when(table.columns()).thenReturn(ImmutableList.of( - column("Col1", DataType.CLOB) - )); - + public void testClobTruncation() { + when(table.columns()).thenReturn(ImmutableList.of(column("Col1", DataType.CLOB))); Record record1 = mock(Record.class); + when(record1.getString("Col1")).thenReturn("X".repeat(35000)); - String clobValue = ""; - - for (int c = 0; c < 35000; c++) { - clobValue += "X"; - } - - when(record1.getString("Col1")).thenReturn(clobValue); - ImmutableList recordList = ImmutableList.of(record1); - int expectedMaxClobLength = 32767; - - // When - tableOutputter.table(1, writableWorkbook, table, recordList); + tableOutputter.table(1, workbook, table, ImmutableList.of(record1)); + sheet = workbook.getSheetAt(0); - ArgumentCaptor writableCellCaptor = ArgumentCaptor.forClass(WritableCell.class); - verify(writableSheet, times(12)).addCell(writableCellCaptor.capture()); - List capturedWritableCellList = writableCellCaptor.getAllValues(); - // Example Data - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,12, 0, clobValue.substring(0, expectedMaxClobLength))); - - // Parameters to Set Up - assertTrue(isCapturedWritableCellListContains(capturedWritableCellList,12, 0, clobValue.substring(0, expectedMaxClobLength))); + assertEquals(32767, value(8, 0).length()); + assertEquals(32767, value(12, 0).length()); } - - /** - * Tests that a table with fields containing invalid data results in a {@link UnsupportedOperationException}. - */ @Test public void testUnsupportedOperationExceptionHandling() { - // Given Record record1 = mock(Record.class); - // DECIMAL when(table.columns()).thenReturn(ImmutableList.of(column("Col1", DataType.DECIMAL))); BigDecimal bigDecimal = mock(BigDecimal.class); when(record1.getBigDecimal("Col1")).thenReturn(bigDecimal); when(bigDecimal.doubleValue()).thenThrow(new RuntimeException("BAD BIG DECIMAL")); try { - // When - tableOutputter.table(1, writableWorkbook, table, ImmutableList.of(record1)); + tableOutputter.table(1, workbook, table, ImmutableList.of(record1)); fail("UnsupportedOperationException should be thrown"); } catch (Exception e) { - // Then - assertTrue(e.getCause().getMessage().startsWith("Cannot generate Excel cell (parseDouble) for data [Mock for BigDecimal")); + Throwable t = e.getCause() != null ? e.getCause() : e; + assertTrue(t.getMessage().startsWith("Cannot generate Excel cell (parseDouble) for data")); } - // CLOB + workbook = new HSSFWorkbook(); + sheet = workbook.createSheet("dummy"); + when(table.columns()).thenReturn(ImmutableList.of(column("Col1", DataType.CLOB))); when(record1.getString("Col1")).thenThrow(new RuntimeException("BAD CLOB")); try { - // When - tableOutputter.table(1, writableWorkbook, table, ImmutableList.of(record1)); + tableOutputter.table(1, workbook, table, ImmutableList.of(record1)); fail("UnsupportedOperationException should be thrown"); } catch (Exception e) { - // Then - assertTrue(e.getCause().getMessage().startsWith("Cannot generate Excel cell for CLOB data")); + Throwable t = e; + boolean matched = false; + while (t != null) { + if (t.getMessage() != null && t.getMessage().startsWith("Cannot generate Excel cell for CLOB data")) { + matched = true; + break; + } + t = t.getCause(); + } + assertTrue(matched); } } - - /** - * Tests whether a given {@link WritableCell} {@link List} contains an entry with a given row, column and value. - * - * @param capturedWritableCellList the {@link WritableCell} {@link List} - * @param row the row - * @param col the column - * @param value the value - * @return true if capturedWritableCellList contains the entry, otherwise false - */ - private boolean isCapturedWritableCellListContains(List capturedWritableCellList, int row, int col, String value) { - return capturedWritableCellList.stream() - .filter(writableCell -> writableCell.getRow() == row && - writableCell.getColumn() == col && - writableCell.getContents().equals(value)) - .count() == 1; + private String value(int rowIndex, int colIndex) { + if (sheet.getRow(rowIndex) == null || sheet.getRow(rowIndex).getCell(colIndex) == null) { + return ""; + } + return formatter.formatCellValue(sheet.getRow(rowIndex).getCell(colIndex)); } } diff --git a/morf-excel/src/test/resources/commons-logging.properties b/morf-excel/src/test/resources/commons-logging.properties deleted file mode 100644 index 4e88c3c10..000000000 --- a/morf-excel/src/test/resources/commons-logging.properties +++ /dev/null @@ -1,3 +0,0 @@ -# This is here to override other files that might get picked up on the classpath from third party JARs. -org.apache.commons.logging.LogFactory=org.apache.commons.logging.impl.LogFactoryImpl -org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger \ No newline at end of file diff --git a/morf-excel/src/test/resources/log4j.properties b/morf-excel/src/test/resources/log4j.properties deleted file mode 100644 index 29d0a92e9..000000000 --- a/morf-excel/src/test/resources/log4j.properties +++ /dev/null @@ -1,9 +0,0 @@ -# Logging configuration - -log4j.rootLogger=INFO, CONSOLE - -log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender -log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout -log4j.appender.CONSOLE.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n - -log4j.logger.org.alfasoftware.morf.diagnostics.DatabaseAndSystemTimeConsistencyChecker=DEBUG diff --git a/morf-excel/src/test/resources/org/alfasoftware/morf/excel/TestSpreadsheetDataSetProducer.xls b/morf-excel/src/test/resources/org/alfasoftware/morf/excel/TestSpreadsheetDataSetProducer.xls old mode 100755 new mode 100644 diff --git a/pom.xml b/pom.xml index 079afb266..3b73410a7 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,7 @@ 2.1 4.13.1 1.0.4 - 2.6.12 + 5.5.1 1.2.16 5.4.0 2.6.9 @@ -251,7 +251,13 @@ - + + + log4j + log4j + test + + @@ -356,9 +362,43 @@ ${junitparams.version} - net.sourceforge.jexcelapi - jxl - ${jxl.version} + org.apache.poi + poi + ${apache-poi.version} + + + org.slf4j + jcl-over-slf4j + + + + + org.apache.poi + poi-ooxml + ${apache-poi.version} + + + org.apache.xmlgraphics + batik-all + + + de.rototor.pdfbox + graphics2d + + + org.bouncycastle + bcpkix-jdk15on + + + org.bouncycastle + bcprov-jdk15on + + + + + org.apache.poi + poi-ooxml-lite + ${apache-poi.version} log4j