/* * Licensed Materials - Property of Perforce Software, Inc. * © Copyright Perforce Software, Inc. 2014, 2021 * © Copyright IBM Corp. 2009, 2014 * © Copyright ILOG 1996, 2009 * All Rights Reserved. * * Note to U.S. Government Users Restricted Rights: * The Software and Documentation were developed at private expense and * are "Commercial Items" as that term is defined at 48 CFR 2.101, * consisting of "Commercial Computer Software" and * "Commercial Computer Software Documentation", as such terms are * used in 48 CFR 12.212 or 48 CFR 227.7202-1 through 227.7202-4, * as applicable. */ package excelxml; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import javax.swing.table.DefaultTableModel; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.FactoryConfigurationError; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import ilog.views.chart.datax.IlvColumnUtilities; import ilog.views.chart.datax.IlvDefaultDataColumnInfo; import ilog.views.chart.datax.adapter.IlvTableToFlatTableModel; import ilog.views.chart.datax.flat.table.IlvFlatTableModel; import ilog.views.util.convert.IlvConvert; import ilog.views.util.convert.IlvConvertException; /** * This class reads an XML file saved by Microsoft Excel, and presents the * sheets in the form of <code>TableModel</code> instances. * * Note: This class cannot handle the native Microsoft Excel (.xls) format. */ public class Workbook { // Format description: // // Character set: UTF-8. // Structure: // <?xml version="1.0"?> // <Workbook ...> // ... // <Worksheet ...> // <Table ss:ExpandedColumnCount="NN" ss:ExpandedRowCount="NN" ...> // <Column .../> // ... // <Row [ss:Index="NN"]> // <Cell> // <Data ...>VALUE</Data> // </Cell> // ... // </Row> // ... // </Table> // ... // </WorkSheet> // ... // </Workbook> // The entire contents of the XML file. private Document document; /** * Creates a Workbook from the contents of an XML file, given as file name. */ public Workbook(String filename) throws IOException { this(new File(filename).toURI().toURL()); } /** * Creates a Workbook from the contents of an XML file, given as an URL. */ public Workbook(URL file) throws IOException { this(openStream(file)); } /** * Creates a Workbook from the contents of an XML file, given as an open * stream. The stream is closed by this constructor. */ public Workbook(InputStream stream) throws IOException { // Use a buffered stream, for speed. BufferedInputStream bstream = new BufferedInputStream(stream); InputStreamReader cstream = new InputStreamReader(bstream, "UTF-8"); // Read the XML contents into memory. InputSource xmlsource = new InputSource(cstream); DocumentBuilderFactory docBuilderFactory; try { docBuilderFactory = DocumentBuilderFactory.newInstance(); } catch (FactoryConfigurationError e) { e.printStackTrace(); return; } DocumentBuilder docBuilder; try { docBuilder = docBuilderFactory.newDocumentBuilder(); } catch (ParserConfigurationException e) { e.printStackTrace(); return; } Document doc; try { doc = docBuilder.parse(xmlsource); } catch (SAXException e) { e.printStackTrace(); return; } cstream.close(); bstream.close(); document = doc; } /** * Opens an URL for read-only access. Like url.openStream(), except that it * also works for Windows UNC pathnames. See Sun BR #4671171. */ private static InputStream openStream(URL url) throws IOException { if ("file".equals(url.getProtocol()) && !"".equals(url.getHost())) // Do it directly, avoiding a "Connection refused: connect" error. return new BufferedInputStream(new FileInputStream("//" + url.getHost() + url.getPath())); else { // But note that URLs of the form "jar:file://HOST/...!..." work // and don't need special treatment. return url.openStream(); } } /** * Returns the worksheet at the given index. Returns null if there are less * than index+1 worksheets. * * @param columnNames * A list of column names to use, or <code>null</code>. * @param columnTypes * A list of column types to use, or <code>null</code>. */ public IlvFlatTableModel getWorksheet(int index, String[] columnNames, Class<?>[] columnTypes) { return getWorksheet(index, columnNames, columnTypes, 0); } /** * Returns the worksheet at the given index, omitting the first rowsToSkip * rows. Returns null if there are less than index+1 worksheets. * * @param columnNames * A list of column names to use, or <code>null</code>. * @param columnTypes * A list of column types to use, or <code>null</code>. * @param rowsToSkip * The number of initial rows to skip. */ public IlvFlatTableModel getWorksheet(int index, String[] columnNames, Class<?>[] columnTypes, int rowsToSkip) { return getWorksheet(index, columnNames, columnTypes, rowsToSkip, Integer.MAX_VALUE); } /** * Returns the worksheet at the given index, limited to rows in the range * minRow..maxRow. Returns null if there are less than index+1 worksheets. * * @param columnNames * A list of column names to use, or <code>null</code>. * @param columnTypes * A list of column types to use, or <code>null</code>. * @param minRow * The first row to consider. * @param maxRow * The last row to consider. */ public IlvFlatTableModel getWorksheet(int index, String[] columnNames, Class<?>[] columnTypes, int minRow, int maxRow) { Element root = document.getDocumentElement(); if ("Workbook".equals(root.getTagName())) { NodeList workbookChildren = root.getChildNodes(); for (int i = 0; i < workbookChildren.getLength(); i++) if (workbookChildren.item(i) instanceof Element) { Element level1 = (Element) workbookChildren.item(i); if ("Worksheet".equals(level1.getTagName())) { if (index > 0) index--; else if (index == 0) { DefaultTableModel table = getWorksheetContents(level1, minRow, maxRow); if (table != null) { table = trimRows(table); table = trimColumns(table); return addColumnInfos(table, columnNames, columnTypes); } return null; } } } } return null; } // Extract the contents of a <Worksheet...> element. private DefaultTableModel getWorksheetContents(Element level1, int minRow, int maxRow) { // String worksheetName = level1.getAttribute("ss:Name"); DefaultTableModel table = null; NodeList worksheetChildren = level1.getChildNodes(); for (int j = 0; j < worksheetChildren.getLength(); j++) if (worksheetChildren.item(j) instanceof Element) { Element level2 = (Element) worksheetChildren.item(j); if ("Table".equals(level2.getTagName())) { String expandedColumnCount = level2.getAttribute("ss:ExpandedColumnCount"); String expandedRowCount = level2.getAttribute("ss:ExpandedRowCount"); int columnCount = Integer.parseInt(expandedColumnCount); int rowCount = Integer.parseInt(expandedRowCount); table = new DefaultTableModel(rowCount, columnCount); int rowIndex = 0; NodeList tableChildren = level2.getChildNodes(); for (int k = 0; k < tableChildren.getLength(); k++) if (tableChildren.item(k) instanceof Element) { Element level3 = (Element) tableChildren.item(k); if ("Row".equals(level3.getTagName())) { String index = level3.getAttribute("ss:Index"); if (index.length() > 0) rowIndex = Integer.parseInt(index) - 1; if (rowIndex >= minRow && rowIndex <= maxRow) { getRowContents(level3, table, rowIndex - minRow); } rowIndex++; } } // Care only about the first <Table...> element. break; } } return table; } // Extract the contents of a <Row...> element. private void getRowContents(Element level3, DefaultTableModel table, int rowIndex) { int columnIndex = 0; NodeList rowChildren = level3.getChildNodes(); for (int l = 0; l < rowChildren.getLength(); l++) if (rowChildren.item(l) instanceof Element) { Element level4 = (Element) rowChildren.item(l); if ("Cell".equals(level4.getTagName())) { NodeList cellChildren = level4.getChildNodes(); for (int m = 0; m < cellChildren.getLength(); m++) if (cellChildren.item(m) instanceof Element) { Element level5 = (Element) cellChildren.item(m); if ("Data".equals(level5.getTagName())) { Object value = getCellDataValue(level5, columnIndex); table.setValueAt(value, rowIndex, columnIndex); // Care only about the first <Data...> element. break; } } columnIndex++; } } } private static DateFormat XMLDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss.SSS", Locale.US); // Extract the value of a <Data...> element. private Object getCellDataValue(Element dataElement, int columnIndex) { StringBuffer valueBuffer = new StringBuffer(); NodeList dataChilds = dataElement.getChildNodes(); for (int n = 0; n < dataChilds.getLength(); n++) switch (dataChilds.item(n).getNodeType()) { case Node.TEXT_NODE: case Node.CDATA_SECTION_NODE: valueBuffer.append(dataChilds.item(n).getNodeValue()); break; } String string = valueBuffer.toString(); String type = dataElement.getAttribute("ss:Type"); if (type.equals("Number")) return convertToNumber(string, columnIndex); if (type.equals("Boolean")) return convertToBoolean(string, columnIndex); if (type.equals("DateTime")) return convertToDate(string, columnIndex); // Other possible types: Error, String, ... return string; } /** * Converts a cell entry of type "Number" to a Java object, ideally of type * Double. */ protected Object convertToNumber(String string, int columnIndex) { try { return Double.valueOf(Double.parseDouble(string)); } catch (Exception e) { // Complain later. return string; } } /** * Converts a cell entry of type "Boolean" to a Java object, ideally of type * Boolean. */ protected Object convertToBoolean(String string, int columnIndex) { if (string.equals("0")) return Boolean.FALSE; if (string.equals("1")) return Boolean.TRUE; return string; } /** * Converts a cell entry of type "DateTime" to a Java object, ideally of type * Date. */ protected Object convertToDate(String string, int columnIndex) { try { // FIXME: What about the time zone? return XMLDateFormat.parseObject(string); } catch (ParseException e) { // Complain later. return string; } } /** * Removes empty lines from the beginning and from the end of the given table, * and returns the modified table, */ private DefaultTableModel trimRows(DefaultTableModel table) { // Remove empty lines from the end. while (table.getRowCount() > 0) { int n = table.getRowCount(); int rowIndex = n - 1; boolean blank = true; for (int col = 0; col < table.getColumnCount(); col++) if (table.getValueAt(rowIndex, col) != null) { blank = false; break; } if (blank) table.removeRow(rowIndex); else break; } // Remove empty lines from the start. while (table.getRowCount() > 0) { int rowIndex = 0; boolean blank = true; for (int col = 0; col < table.getColumnCount(); col++) if (table.getValueAt(rowIndex, col) != null) { blank = false; break; } if (blank) table.removeRow(rowIndex); else break; } return table; } /** * Removes empty lines from the right border of the given table, and returns * the modified table, */ private DefaultTableModel trimColumns(DefaultTableModel table) { // Determine the number of columns as the maximum number of fields // in any line. int columnCount = 0; for (int i = 0; i < table.getRowCount(); i++) for (int j = columnCount; j < table.getColumnCount(); j++) if (table.getValueAt(i, j) != null) columnCount = j + 1; table.setColumnCount(columnCount); return table; } /** * Adds column metainformation to the given table, and converts it to an * instance of <code>IlvFlatTableModel</code>. * * @param columnNames * A list of column names to use, or <code>null</code>. * @param columnTypes * A list of column types to use, or <code>null</code>. */ private IlvFlatTableModel addColumnInfos(DefaultTableModel table, String[] columnNames, Class<?>[] columnTypes) { int columnCount = table.getColumnCount(); IlvDefaultDataColumnInfo[] columns = new IlvDefaultDataColumnInfo[columnCount]; for (int col = 0; col < columnCount; col++) { Class<?> type = null; if (columnTypes != null && col < columnTypes.length) type = columnTypes[col]; if (type != null) { // Enforce the given type. for (int i = 0; i < table.getRowCount(); i++) { Object value = table.getValueAt(i, col); if (value != null && !type.isInstance(value)) table.setValueAt(convertTo(value, type, col), i, col); } } else { // Guess the type. boolean allDouble = true; boolean allBoolean = true; boolean allDate = true; for (int i = 0; i < table.getRowCount(); i++) { Object value = table.getValueAt(i, col); if (value != null) { if (!(value instanceof Double)) allDouble = false; if (!(value instanceof Boolean)) allBoolean = false; if (!(value instanceof Date)) allDate = false; } } type = (allDouble && !allBoolean && !allDate ? Double.class : allBoolean && !allDouble && !allDate ? Boolean.class : allDate && !allDouble && !allBoolean ? Date.class : Object.class); } columns[col] = new IlvDefaultDataColumnInfo(columnNames != null && col < columnNames.length ? columnNames[col] : IlvColumnUtilities.constructColumnName(col), type); if (type == Boolean.class) columns[col].setEnumerated(true); } return new IlvTableToFlatTableModel(table, columns); } /** * Converts a cell entry to the desired type. */ protected Object convertTo(Object object, Class<?> targetType, int columnIndex) { try { Object attempt = IlvConvert.convert(object, targetType); if (targetType.isInstance(attempt)) return attempt; } catch (IlvConvertException e) { } return null; } }