package charts.builder.spreadsheet; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.Iterator; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import javax.xml.namespace.NamespaceContext; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathFactory; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.FormulaEvaluator; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.CellReference; import org.apache.poi.xssf.usermodel.XSSFFormulaEvaluator; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.xml.sax.InputSource; import charts.builder.spreadsheet.external.SimpleCellLink; import charts.builder.spreadsheet.external.UnresolvedRef; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.io.Files; public class XlsxDataSource extends SpreadsheetDataSource { private static final Pattern EXTERNAL_REF_FILE_PATH = Pattern.compile( "^xl/externalLinks/_rels/externalLink(\\d+).xml.rels$"); private static final Pattern EXTERNAL_REF_FORMULA = Pattern.compile( "^\\+?('?)\\[(\\d+)\\](.*?)'?!\\$?([A-Za-z]+)\\$?(\\d+)$"); private static final String REL_NS_URI = "http://schemas.openxmlformats.org/package/2006/relationships"; private static final String REL_PREFIX = "ref"; private Map<Integer, String> rmap = Maps.newHashMap(); public XlsxDataSource(InputStream in) throws IOException { initWorkbook(initExternalRefs(in)); } private InputStream initExternalRefs(InputStream in) throws IOException { final File tmpDir = Files.createTempDir(); File fSpreadsheet = new File(tmpDir, "spreadsheet.xlsx"); FileOutputStream out = new FileOutputStream(fSpreadsheet); IOUtils.copyLarge(in, out); try (ZipInputStream zip = new ZipInputStream(new FileInputStream(fSpreadsheet))) { ZipEntry entry; while((entry = zip.getNextEntry())!=null) { Matcher m = EXTERNAL_REF_FILE_PATH.matcher(entry.getName()); if(m.matches()) { ByteArrayOutputStream bufOut = new ByteArrayOutputStream(); IOUtils.copy(zip, bufOut); ByteArrayInputStream bufIn = new ByteArrayInputStream(bufOut.toByteArray()); setupExternalRef(new Integer(m.group(1)), bufIn); } } } catch(Exception e) {} return new FileInputStream(fSpreadsheet) { @Override public void close() throws IOException { super.close(); FileUtils.deleteQuietly(tmpDir); } }; } private void setupExternalRef(Integer refnr, InputStream in) throws Exception { XPath xp = XPathFactory.newInstance().newXPath(); xp.setNamespaceContext(new NamespaceContext() { @Override public String getNamespaceURI(String prefix) { if(StringUtils.equalsIgnoreCase(REL_PREFIX, prefix)) { return REL_NS_URI; } return ""; } @Override public String getPrefix(String namespaceURI) { if(StringUtils.equalsIgnoreCase(REL_NS_URI, namespaceURI)) { return REL_PREFIX; } return ""; } @Override public Iterator<?> getPrefixes(String namespaceURI) { return Lists.newArrayList(getPrefix(namespaceURI)).iterator(); }}); String target = xp.evaluate( "/ref:Relationships/ref:Relationship/@Target", new InputSource(in)); if(StringUtils.isNotBlank(target)) { rmap.put(refnr, target); } } private void initWorkbook(InputStream in) throws IOException { XSSFWorkbook workbook = new XSSFWorkbook(in); XSSFFormulaEvaluator evaluator = new XSSFFormulaEvaluator(workbook); evaluator.setIgnoreMissingWorkbooks(true); init(workbook, evaluator); } private XlsxDataSource(Workbook workbook, FormulaEvaluator evaluator, int defaultSheet, Map<Integer, String> rmap) { super(workbook, evaluator, defaultSheet); this.rmap = rmap; } @Override public SpreadsheetDataSource toSheet(int sheet) { return new XlsxDataSource(workbook(), evaluator(), sheet, rmap); } @Override UnresolvedRef externalReference(Cell cell) { UnresolvedRef uref = null; if((cell != null) && (cell.getCellType() == Cell.CELL_TYPE_FORMULA)) { Matcher m = EXTERNAL_REF_FORMULA.matcher(cell.getCellFormula()); if(m.matches()) { try { Integer refnr = new Integer(m.group(2)); String sheetname = m.group(3); String column = m.group(4); String row = m.group(5); if(StringUtils.isNotBlank(m.group(1))) { sheetname = unescapeSheetname(sheetname); } String nameOrId = rmap.get(refnr); if(nameOrId != null) { uref = uref(nameOrId, String.format("%s!%s%s", sheetname, column, row), String.format("%s!%s", cell.getSheet().getSheetName(), new CellReference(cell).formatAsString())); } } catch(Exception e) {} } } return uref; } @Override protected UnresolvedRef uref(String sIdOrName, final String sSelector, final String dSelector) { try { return new UnresolvedRef(URLDecoder.decode(sIdOrName, "UTF-8"), new SimpleCellLink(sSelector, dSelector)); } catch (UnsupportedEncodingException e) { // "UTF-8" is should never throw this exception throw new RuntimeException(e); } } private String unescapeSheetname(String name) { return StringUtils.replace(name, "''", "'"); } }