/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2008 jOpenDocument, by ILM Informatique. All rights reserved. * * The contents of this file are subject to the terms of the GNU * General Public License Version 3 only ("GPL"). * You may not use this file except in compliance with the License. * You can obtain a copy of the License at http://www.gnu.org/licenses/gpl-3.0.html * See the License for the specific language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each file. * */ package org.jopendocument.dom.spreadsheet; import org.jopendocument.dom.ContentType; import org.jopendocument.dom.ContentTypeVersioned; import org.jopendocument.dom.ODDocument; import org.jopendocument.dom.ODPackage; import org.jopendocument.dom.OOUtils; import org.jopendocument.dom.XMLVersion; import org.jopendocument.dom.spreadsheet.SheetTableModel.MutableTableModel; import java.awt.Point; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.table.TableModel; import org.jdom.DocType; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.xpath.XPath; /** * A calc document. * * @author Sylvain */ public class SpreadSheet implements ODDocument { public static SpreadSheet createFromFile(File f) throws IOException { return create(new ODPackage(f)); } public static SpreadSheet create(final ODPackage fd) { return new SpreadSheet(fd.getDocument("content.xml"), fd.getDocument("styles.xml"), fd); } public static SpreadSheet createEmpty(TableModel t) throws IOException { return createEmpty(t, XMLVersion.getOD()); } public static SpreadSheet createEmpty(TableModel t, XMLVersion ns) throws IOException { final Document doc = new Document(new Element("document", ns.getOFFICE())); // OpenDocument use relaxNG if (ns == XMLVersion.OOo) doc.setDocType(new DocType("office:document", "-//OpenOffice.org//DTD OfficeDocument 1.0//EN", "office.dtd")); final ContentTypeVersioned ct = ContentTypeVersioned.fromType(ContentType.SPREADSHEET, ns); if (ct.getVersion().equals(XMLVersion.OOo)) { doc.getRootElement().setAttribute("class", ct.getShortName(), ns.getOFFICE()); } else if (ct.getVersion().equals(XMLVersion.OD)) { doc.getRootElement().setAttribute("mimetype", ct.getMimeType(), ns.getOFFICE()); } // don't forget that, otherwise OO crash doc.getRootElement().addContent(new Element("automatic-styles", ns.getOFFICE())); final Element topBody = new Element("body", ns.getOFFICE()); final Element body; if (ct.getVersion().equals(XMLVersion.OD)) { body = new Element(ct.getShortName(), ns.getOFFICE()); topBody.addContent(body); } else body = topBody; doc.getRootElement().addContent(topBody); final Element sheetElem = Sheet.createEmpty(ns); body.addContent(sheetElem); final SpreadSheet spreadSheet = new SpreadSheet(doc, null); spreadSheet.getSheet(0).merge(t, 0, 0, true); return spreadSheet; } /** * Export the passed data to file. * * @param t the data to export. * @param f where to export, if the extension is missing (or wrong) the correct one will be * added, eg "dir/data". * @param ns the version of XML. * @return the saved file, eg "dir/data.ods". * @throws IOException if the file can't be saved. */ public static File export(TableModel t, File f, XMLVersion ns) throws IOException { return SpreadSheet.createEmpty(t, ns).saveAs(f); } private final ODPackage originalFile; private final Map<Element, Sheet> sheets; public SpreadSheet(Document doc, Document styles) { this(doc, styles, null); } private SpreadSheet(final Document doc, final Document styles, final ODPackage orig) { if (orig != null) { // ATTN OK because this is our private instance (see createFromFile()) this.originalFile = orig; } else { this.originalFile = new ODPackage(); } this.originalFile.putFile("content.xml", doc); if (styles != null) this.originalFile.putFile("styles.xml", styles); // map Sheet by XML elements so has not to depend on ordering or name this.sheets = new HashMap<Element, Sheet>(); } final Document getContent() { return this.getPackage().getContent().getDocument(); } @Override public final XMLVersion getVersion() { return this.getPackage().getVersion(); } private Element getBody() { final Element body = this.getContent().getRootElement().getChild("body", this.getVersion().getOFFICE()); if (this.getVersion().equals(XMLVersion.OOo)) return body; else return body.getChild("spreadsheet", this.getVersion().getOFFICE()); } // ** from 8.3.1 Referencing Table Cells (just double the backslash for . and escape the $) private static final String minCell = "\\$?([A-Z]+)\\$?([0-9]+)"; // added parens to capture cell address // \1 is sheet name, \4 cell address static final Pattern cellPattern = Pattern.compile("(\\$?([^\\. ']+|'([^']|'')+'))?\\.(" + minCell + ")"); static final Pattern minCellPattern = Pattern.compile(minCell); // added parens to capture cell addresses // \1 is sheet name, \4 cell address, \6 second sheet name, \9 second cell address private static final Pattern cellRangePattern = java.util.regex.Pattern .compile("(\\$?([^\\. ']+|'([^']|'')+'))?\\.(\\$?[A-Z]+\\$?[0-9]+)(:(\\$?([^\\. ']+|'([^']|'')+'))?\\.(\\$?[A-Z]+\\$?[0-9]+))?"); protected final String parseSheetName(final String n) { if (n == null) return null; // ToDo handle '' (but OpenOffice doesn't) return n.charAt(0) == '$' ? n.substring(1) : n; } /** * Return a view of the passed range. * * @param name a named range. * @return the matching TableModel, <code>null</code> if it doesn't exist. */ public final MutableTableModel<SpreadSheet> getTableModel(String name) { final Element range; try { final XPath path = this.getXPath("./table:named-expressions/table:named-range[@table:name='" + name + "']"); range = (Element) path.selectSingleNode(this.getBody()); } catch (JDOMException e) { throw new IllegalStateException(e); } if (range == null) return null; // OpenOffice only supports absolute addresses, so need to use base-cell-address final String baseCell = range.getAttributeValue("cell-range-address", getVersion().getTABLE()); final Matcher m = cellRangePattern.matcher(baseCell); if (!m.matches()) throw new IllegalStateException(baseCell + " is not a valid range address"); final String sheet1 = parseSheetName(m.group(1)); final String sheet2 = parseSheetName(m.group(6)); if (sheet2 != null && !sheet2.equals(sheet1)) throw new UnsupportedOperationException("different sheet names: " + sheet1 + " != " + sheet2); final Sheet sheet = this.getSheet(sheet1, true); final Point start = Table.resolve(m.group(4)); final Point end = Table.resolve(m.group(9)); return sheet.getMutableTableModel(start, end); } /** * Return the cell at the passed address. * * @param ref the full address, eg "$sheet.A12". * @return the cell at the passed address. */ public final Cell<SpreadSheet> getCellAt(String ref) { final Matcher m = cellPattern.matcher(ref); if (!m.matches()) throw new IllegalArgumentException(ref + " is not a valid cell address: " + m.pattern().pattern()); final String sheetName = parseSheetName(m.group(1)); if (sheetName == null) throw new IllegalArgumentException("no sheet specified: " + ref); return this.getSheet(sheetName, true).getCellAt(Sheet.resolve(m.group(5), m.group(6))); } public XPath getXPath(String p) throws JDOMException { return OOUtils.getXPath(p, this.getVersion()); } // query directly the DOM, that way don't need to listen to it (eg for name, size or order // change) @SuppressWarnings("unchecked") private final List<Element> getTables() { return this.getBody().getChildren("table", this.getVersion().getTABLE()); } public int getSheetCount() { return this.getTables().size(); } public Sheet getSheet(int i) { return this.getSheet(getTables().get(i)); } public Sheet getSheet(String name) { return this.getSheet(name, false); } /** * Return the first sheet with the passed name. * * @param name the name of a sheet. * @param mustExist what to do when no match is found : <code>true</code> to throw an exception, * <code>false</code> to return null. * @return the first matching sheet, <code>null</code> if <code>mustExist</code> is * <code>false</code> and no match is found. * @throws NoSuchElementException if <code>mustExist</code> is <code>true</code> and no match is * found. */ public Sheet getSheet(String name, final boolean mustExist) throws NoSuchElementException { for (final Element table : getTables()) { if (name.equals(Table.getName(table))) return getSheet(table); } if (mustExist) throw new NoSuchElementException("no such sheet: " + name); else return null; } private final Sheet getSheet(Element table) { Sheet res = this.sheets.get(table); if (res == null) { res = new Sheet(this, table); this.sheets.put(table, res); } return res; } void invalidate(Element element) { this.sheets.remove(element); } /** * Adds an empty sheet. * * @param index where to add the new sheet. * @param name the name of the new sheet. * @return the newly created sheet. */ public final Sheet addSheet(final int index, String name) { if (name == null) throw new NullPointerException("null name"); final Element newElem = Table.createEmpty(getVersion()); return this.addSheet(index, newElem, name); } final Sheet addSheet(final int index, final Element newElem, final String name) { if (index < 0) throw new IndexOutOfBoundsException("Negative index: " + index); if (index > getSheetCount()) throw new IndexOutOfBoundsException("index (" + index + ") > count (" + getSheetCount() + ")"); // the following statement fails when adding after the last table:table : // this.getTables().add(index, newElem); // it add at the end of its parent element (e.g. after table:named-expressions). // so use the fact that there's always at least one sheet and all sheets are grouped : final Element sheet1 = this.getSheet(0).getElement(); final int firstIndex = sheet1.getParentElement().indexOf(sheet1); sheet1.getParentElement().addContent(firstIndex + index, newElem); final Sheet res = this.getSheet(newElem); if (name != null) res.setName(name); assert res.getName() != null; return res; } public final Sheet addSheet(String name) { return this.addSheet(getSheetCount(), name); } void move(Sheet sheet, int toIndex) { final Element parentElement = sheet.getElement().getParentElement(); sheet.getElement().detach(); parentElement.addContent(toIndex, sheet.getElement()); // no need to update this.sheets since it doesn't depend on order } // *** Files public File saveAs(File file) throws FileNotFoundException, IOException { this.getPackage().setFile(file); return this.getPackage().save(); } @Override public final ODPackage getPackage() { return this.originalFile; } }