From: Yegor Kozlov Date: Thu, 16 Jul 2009 05:46:14 +0000 (+0000) Subject: support for custom XML mappings in XSSF, see Bugzilla 47520 X-Git-Tag: REL_3_5-FINAL~82 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=35906116edf297fbe512765cc4728acfca3b20eb;p=poi.git support for custom XML mappings in XSSF, see Bugzilla 47520 git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@794539 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index 79993d965f..415f5390ec 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -33,6 +33,7 @@ + 47520 - Initial support for custom XML mappings in XSSF 47460 - Fixed NPE when retrieving core properties from a newly created workbook 47498 - Fixed HyperlinkRecord to properly handle URL monikers 47504 - Fixed XSSFWorkbook to read files with hyperlinks to document locations diff --git a/src/examples/src/org/apache/poi/xssf/usermodel/examples/CustomXMLMapping.java b/src/examples/src/org/apache/poi/xssf/usermodel/examples/CustomXMLMapping.java new file mode 100755 index 0000000000..3e69df286c --- /dev/null +++ b/src/examples/src/org/apache/poi/xssf/usermodel/examples/CustomXMLMapping.java @@ -0,0 +1,42 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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.apache.poi.xssf.usermodel.examples; + +import org.apache.poi.xssf.extractor.XSSFExportToXml; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.apache.poi.xssf.usermodel.XSSFMap; + +import java.io.ByteArrayOutputStream; + +/** + * Print all custom XML mappings registered in the given workbook + */ +public class CustomXMLMapping { + + public static void main(String[] args) throws Exception { + XSSFWorkbook wb = new XSSFWorkbook(args[0]); + + for (XSSFMap map : wb.getCustomXMLMappings()) { + XSSFExportToXml exporter = new XSSFExportToXml(map); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + exporter.exportToXML(os, true); + String xml = os.toString("UTF-8"); + System.out.println(xml); + } + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFExportToXml.java b/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFExportToXml.java new file mode 100755 index 0000000000..01d96832b2 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFExportToXml.java @@ -0,0 +1,544 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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.apache.poi.xssf.extractor; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Vector; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; + +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.xssf.model.Table; +import org.apache.poi.xssf.usermodel.XSSFCell; +import org.apache.poi.xssf.usermodel.XSSFMap; +import org.apache.poi.xssf.usermodel.XSSFRow; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.helpers.XSSFSingleXmlCell; +import org.apache.poi.xssf.usermodel.helpers.XSSFXmlColumnPr; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.STXmlDataType; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * + * Maps an XLSX to an XML according to one of the mapping defined. + * + * + * The output XML Schema must respect this limitations: + * + *
    + *
  • all mandatory elements and attributes must be mapped
  • + * + *
  • no <any> in complex type/element declaration
  • + *
  • no <anyAttribute> attributes declaration
  • + *
  • no recursive structures: recursive structures can't be nested more than one level
  • + *
  • no abstract elements: abstract complex types can be declared but must not be used in elements.
  • + *
  • no mixed content: an element can't contain simple text and child element(s) together
  • + *
  • no <substitutionGroup> in complex type/element declaration
  • + *
+ * + * @author Roberto Manicardi + * + * + * + */ + + +public class XSSFExportToXml implements Comparator{ + + private XSSFMap map; + + /** + * Creates a new exporter and sets the mapping to be used when generating the XML output document + * + * @param map the mapping rule to be used + */ + public XSSFExportToXml(XSSFMap map){ + this.map = map; + } + + /** + * + * Exports the data in an XML stream + * + * @param os OutputStream in which will contain the output XML + * @param validate if true, validates the XML againts the XML Schema + * @throws SAXException + */ + public void exportToXML(OutputStream os, boolean validate) throws SAXException{ + exportToXML(os,"UTF-8", validate); + } + + private Document getEmptyDocument() throws ParserConfigurationException{ + + + DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = dbfac.newDocumentBuilder(); + Document doc = docBuilder.newDocument(); + + + + return doc; + } + + /** + * Exports the data in an XML stream + * + * @param os OutputStream in which will contain the output XML + * @param encoding the output charset encoding + * @param validate if true, validates the XML againts the XML Schema + * @throws SAXException + * @throws InvalidFormatException + */ + + public void exportToXML(OutputStream os, String encoding, boolean validate) throws SAXException{ + List singleXMLCells = map.getRelatedSingleXMLCell(); + List tables = map.getRelatedTables(); + + String rootElement = map.getCtMap().getRootElement(); + + try{ + + Document doc = getEmptyDocument(); + + Element root = null; + + if(isNamespaceDeclared()){ + root=doc.createElementNS(getNamespace(),rootElement); + }else{ + root=doc.createElement(rootElement); + } + doc.appendChild(root); + + + List xpaths = new Vector(); + Map singleXmlCellsMappings = new HashMap(); + Map tableMappings = new HashMap(); + + for(XSSFSingleXmlCell simpleXmlCell : singleXMLCells){ + xpaths.add(simpleXmlCell.getXpath()); + singleXmlCellsMappings.put(simpleXmlCell.getXpath(), simpleXmlCell); + } + for(Table table : tables){ + String commonXPath = table.getCommonXpath(); + xpaths.add(commonXPath); + tableMappings.put(commonXPath, table); + } + + + Collections.sort(xpaths,this); + + for(String xpath : xpaths){ + + XSSFSingleXmlCell simpleXmlCell = singleXmlCellsMappings.get(xpath); + Table table = tableMappings.get(xpath); + + if(!xpath.matches(".*\\[.*")){ + + // Exports elements and attributes mapped with simpleXmlCell + if(simpleXmlCell!=null){ + XSSFCell cell = simpleXmlCell.getReferencedCell(); + if(cell!=null){ + Node currentNode = getNodeByXPath(xpath,doc.getFirstChild(),doc,false); + STXmlDataType.Enum dataType = simpleXmlCell.getXmlDataType(); + mapCellOnNode(cell,currentNode,dataType); + } + } + + // Exports elements and attributes mapped with tables + if(table!=null){ + + List tableColumns = table.getXmlColumnPrs(); + + XSSFSheet sheet = table.getXSSFSheet(); + + int startRow = table.getStartCellReference().getRow(); + // In mappings created with Microsoft Excel the first row contains the table header and must be skipped + startRow +=1; + + int endRow = table.getEndCellReference().getRow(); + + for(int i = startRow; i<= endRow; i++){ + XSSFRow row = sheet.getRow(i); + + Node tableRootNode = getNodeByXPath(table.getCommonXpath(),doc.getFirstChild(),doc,true); + + short startColumnIndex = table.getStartCellReference().getCol(); + for(int j = startColumnIndex; j<= table.getEndCellReference().getCol();j++){ + XSSFCell cell = row.getCell(j); + if(cell!=null){ + XSSFXmlColumnPr pointer = tableColumns.get(j-startColumnIndex); + String localXPath = pointer.getLocalXPath(); + Node currentNode = getNodeByXPath(localXPath,tableRootNode,doc,false); + STXmlDataType.Enum dataType = pointer.getXmlDataType(); + + + mapCellOnNode(cell,currentNode,dataType); + } + + } + + } + + + + } + }else{ + // TODO: implement filtering management in xpath + } + } + + boolean isValid = true; + if(validate){ + isValid =isValid(doc); + } + + + + if(isValid){ + + ///////////////// + //Output the XML + + //set up a transformer + TransformerFactory transfac = TransformerFactory.newInstance(); + Transformer trans = transfac.newTransformer(); + trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + trans.setOutputProperty(OutputKeys.INDENT, "yes"); + trans.setOutputProperty(OutputKeys.ENCODING, encoding); + + //create string from xml tree + + StreamResult result = new StreamResult(os); + DOMSource source = new DOMSource(doc); + trans.transform(source, result); + + } + }catch(ParserConfigurationException e){ + e.printStackTrace(); + }catch(TransformerException e){ + e.printStackTrace(); + } + + + } + + /** + * Validate the generated XML against the XML Schema associated with the XSSFMap + * + * @param xml the XML to validate + * @return + */ + + private boolean isValid(Document xml) throws SAXException{ + boolean isValid = false; + try{ + String language = XMLConstants.W3C_XML_SCHEMA_NS_URI; + SchemaFactory factory = SchemaFactory.newInstance(language); + + Source source = new DOMSource(map.getSchema()); + Schema schema = factory.newSchema(source); + Validator validator = schema.newValidator(); + validator.validate(new DOMSource(xml)); + //if no exceptions where raised, the document is valid + isValid=true; + + + }catch(IOException e){ + e.printStackTrace(); + } + return isValid; + } + + + private void mapCellOnNode(XSSFCell cell, Node node, STXmlDataType.Enum outputDataType){ + + + String value =""; + switch (cell.getCellType()){ + + case XSSFCell.CELL_TYPE_STRING: value = cell.getStringCellValue(); break; + case XSSFCell.CELL_TYPE_BOOLEAN: value += cell.getBooleanCellValue(); break; + case XSSFCell.CELL_TYPE_ERROR: value = cell.getErrorCellString(); + case XSSFCell.CELL_TYPE_FORMULA: value = cell.getStringCellValue(); break; + case XSSFCell.CELL_TYPE_NUMERIC: value += cell.getRawValue(); break; + default: ; + + } + if(node instanceof Element){ + Element currentElement = (Element) node; + currentElement.setTextContent(value); + }else{ + node.setNodeValue(value); + } + + + } + + private String removeNamespace(String elementName){ + return elementName.matches(".*:.*")?elementName.split(":")[1]:elementName; + } + + + + private Node getNodeByXPath(String xpath,Node rootNode,Document doc,boolean createMultipleInstances){ + String[] xpathTokens = xpath.split("/"); + + + Node currentNode =rootNode; + // The first token is empty, the second is the root node + for(int i =2; i rightIndex){ + result = 1; + } + }else{ + // NOTE: the xpath doesn't match correctly in the schema + } + + + } + + } + + + + + return result; + } + + private int indexOfElementInComplexType(String elementName,Node complexType){ + + NodeList list = complexType.getChildNodes(); + int indexOf = -1; + + for(int i=0; i< list.getLength();i++){ + Node node = list.item(i); + if(node instanceof Element){ + if(node.getLocalName().equals("element")){ + Node nameAttribute = node.getAttributes().getNamedItem("name"); + if(nameAttribute.getNodeValue().equals(removeNamespace(elementName))){ + indexOf = i; + break; + } + + } + } + } + + return indexOf; + + } + + private Node getComplexTypeForElement(String elementName,Node xmlSchema,Node localComplexTypeRootNode){ + Node complexTypeNode = null; + + String elementNameWithoutNamespace = removeNamespace(elementName); + + + NodeList list = localComplexTypeRootNode.getChildNodes(); + String complexTypeName = ""; + + + + for(int i=0; i< list.getLength();i++){ + Node node = list.item(i); + if( node instanceof Element){ + if(node.getLocalName().equals("element")){ + Node nameAttribute = node.getAttributes().getNamedItem("name"); + if(nameAttribute.getNodeValue().equals(elementNameWithoutNamespace)){ + Node complexTypeAttribute = node.getAttributes().getNamedItem("type"); + if(complexTypeAttribute!=null){ + complexTypeName = complexTypeAttribute.getNodeValue(); + break; + } + } + } + } + } + // Note: we expect that all the complex types are defined at root level + if(!complexTypeName.equals("")){ + NodeList complexTypeList = xmlSchema.getChildNodes(); + for(int i=0; i< complexTypeList.getLength();i++){ + Node node = list.item(i); + if( node instanceof Element){ + if(node.getLocalName().equals("complexType")){ + Node nameAttribute = node.getAttributes().getNamedItem("name"); + if(nameAttribute.getNodeValue().equals(complexTypeName)){ + + NodeList complexTypeChildList =node.getChildNodes(); + for(int j=0; j maps ; + + public MapInfo() { + super(); + mapInfo = CTMapInfo.Factory.newInstance(); + + } + + public MapInfo(PackagePart part, PackageRelationship rel) + throws IOException { + super(part, rel); + readFrom(part.getInputStream()); + } + + public void readFrom(InputStream is) throws IOException { + try { + MapInfoDocument doc = MapInfoDocument.Factory.parse(is); + mapInfo = doc.getMapInfo(); + + maps= new HashMap(); + for(CTMap map :mapInfo.getMapArray()){ + maps.put((int)map.getID(), new XSSFMap(map,this)); + } + + } catch (XmlException e) { + throw new IOException(e.getLocalizedMessage()); + } + } + + /** + * Returns the parent XSSFWorkbook + * + * @return the parent XSSFWorkbook + */ + public XSSFWorkbook getWorkbook() { + return (XSSFWorkbook)getParent(); + } + + /** + * + * @return the internal data object + */ + public CTMapInfo getCTMapInfo(){ + return mapInfo; + + } + + /** + * Gets the CTSchema buy it's ID + * @param schemaId the schema ID + * @return + */ + public CTSchema getCTSchemaById(String schemaId){ + CTSchema xmlSchema = null; + + CTSchema[] schemas = mapInfo.getSchemaArray(); + for(CTSchema schema: schemas){ + if(schema.getID().equals(schemaId)){ + xmlSchema = schema; + break; + } + } + return xmlSchema; + } + + + public XSSFMap getXSSFMapById(int id){ + return maps.get(id); + } + + /** + * + * @return all the mappings configured in this document + */ + public Collection getAllXSSFMaps(){ + return maps.values(); + } + + protected void writeTo(OutputStream out) throws IOException { + MapInfoDocument doc = MapInfoDocument.Factory.newInstance(); + doc.setMapInfo(mapInfo); + doc.save(out, DEFAULT_XML_OPTIONS); + } + + @Override + protected void commit() throws IOException { + PackagePart part = getPackagePart(); + OutputStream out = part.getOutputStream(); + writeTo(out); + out.close(); + } + +} diff --git a/src/ooxml/java/org/apache/poi/xssf/model/SingleXmlCells.java b/src/ooxml/java/org/apache/poi/xssf/model/SingleXmlCells.java new file mode 100755 index 0000000000..9b118bfdf8 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/model/SingleXmlCells.java @@ -0,0 +1,106 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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.apache.poi.xssf.model; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.Vector; + +import org.apache.poi.POIXMLDocumentPart; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.openxml4j.opc.PackageRelationship; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.helpers.XSSFSingleXmlCell; +import org.apache.xmlbeans.XmlException; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTSingleXmlCell; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTSingleXmlCells; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.SingleXmlCellsDocument; + + +/** + * + * This class implements the Single Cell Tables Part (Open Office XML Part 4: + * chapter 3.5.2) + * + * + * @author Roberto Manicardi + */ +public class SingleXmlCells extends POIXMLDocumentPart { + + + private CTSingleXmlCells singleXMLCells; + + public SingleXmlCells() { + super(); + singleXMLCells = CTSingleXmlCells.Factory.newInstance(); + + } + + public SingleXmlCells(PackagePart part, PackageRelationship rel) + throws IOException { + super(part, rel); + readFrom(part.getInputStream()); + } + + public void readFrom(InputStream is) throws IOException { + try { + SingleXmlCellsDocument doc = SingleXmlCellsDocument.Factory.parse(is); + singleXMLCells = doc.getSingleXmlCells(); + } catch (XmlException e) { + throw new IOException(e.getLocalizedMessage()); + } + } + + public XSSFSheet getXSSFSheet(){ + return (XSSFSheet) getParent(); + } + + protected void writeTo(OutputStream out) throws IOException { + SingleXmlCellsDocument doc = SingleXmlCellsDocument.Factory.newInstance(); + doc.setSingleXmlCells(singleXMLCells); + doc.save(out, DEFAULT_XML_OPTIONS); + } + + @Override + protected void commit() throws IOException { + PackagePart part = getPackagePart(); + OutputStream out = part.getOutputStream(); + writeTo(out); + out.close(); + } + + public CTSingleXmlCells getCTSingleXMLCells(){ + return singleXMLCells; + } + + /** + * + * @return all the SimpleXmlCell contained in this SingleXmlCells element + */ + public List getAllSimpleXmlCell(){ + List list = new Vector(); + CTSingleXmlCell[] singleXMLCellArray = singleXMLCells.getSingleXmlCellArray(); + + for(CTSingleXmlCell singleXmlCell: singleXMLCellArray){ + list.add(new XSSFSingleXmlCell(singleXmlCell,this)); + } + return list; + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/model/Table.java b/src/ooxml/java/org/apache/poi/xssf/model/Table.java new file mode 100755 index 0000000000..9ab24ac57a --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/model/Table.java @@ -0,0 +1,248 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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.apache.poi.xssf.model; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Vector; + +import org.apache.poi.POIXMLDocumentPart; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.openxml4j.opc.PackageRelationship; +import org.apache.poi.ss.util.CellReference; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.helpers.XSSFXmlColumnPr; +import org.apache.xmlbeans.XmlException; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTable; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTableColumn; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.TableDocument; + +/** + * + * This class implements the Table Part (Open Office XML Part 4: + * chapter 3.5.1) + * + * This implementation works under the assumption that a table contains mappings to a subtree of an XML. + * The root element of this subtree an occur multiple times (one for each row of the table). The child nodes + * of the root element can be only attributes or element with maxOccurs=1 property set + * + * + * @author Roberto Manicardi + */ +public class Table extends POIXMLDocumentPart { + + private CTTable ctTable; + private List xmlColumnPr; + private CellReference startCellReference; + private CellReference endCellReference; + private String commonXPath; + + + public Table() { + super(); + ctTable = CTTable.Factory.newInstance(); + + } + + public Table(PackagePart part, PackageRelationship rel) + throws IOException { + super(part, rel); + readFrom(part.getInputStream()); + } + + public void readFrom(InputStream is) throws IOException { + try { + TableDocument doc = TableDocument.Factory.parse(is); + ctTable = doc.getTable(); + } catch (XmlException e) { + throw new IOException(e.getLocalizedMessage()); + } + } + + public XSSFSheet getXSSFSheet(){ + return (XSSFSheet) getParent(); + } + + public void writeTo(OutputStream out) throws IOException { + TableDocument doc = TableDocument.Factory.newInstance(); + doc.setTable(ctTable); + doc.save(out, DEFAULT_XML_OPTIONS); + } + + @Override + protected void commit() throws IOException { + PackagePart part = getPackagePart(); + OutputStream out = part.getOutputStream(); + writeTo(out); + out.close(); + } + + public CTTable getCTTable(){ + return ctTable; + } + + /** + * Checks if this Table element contains even a single mapping to the map identified by id + * @param id the XSSFMap ID + * @return true if the Table element contain mappings + */ + public boolean mapsTo(long id){ + boolean maps =false; + + List pointers = getXmlColumnPrs(); + + for(XSSFXmlColumnPr pointer: pointers){ + if(pointer.getMapId()==id){ + maps=true; + break; + } + } + + return maps; + } + + + /** + * + * Calculates the xpath of the root element for the table. This will be the common part + * of all the mapping's xpaths + * + * @return the xpath of the table's root element + */ + public String getCommonXpath() { + + if(commonXPath == null){ + + String[] commonTokens ={}; + + for(CTTableColumn column :ctTable.getTableColumns().getTableColumnArray()){ + if(column.getXmlColumnPr()!=null){ + String xpath = column.getXmlColumnPr().getXpath(); + String[] tokens = xpath.split("/"); + if(commonTokens.length==0){ + commonTokens = tokens; + + }else{ + int maxLenght = commonTokens.length>tokens.length? tokens.length:commonTokens.length; + for(int i =0; i subCommonTokens = Arrays.asList(commonTokens).subList(0, i); + + String[] container = {}; + + commonTokens = subCommonTokens.toArray(container); + break; + + + } + } + } + + } + } + + + commonXPath =""; + + for(int i = 1 ; i< commonTokens.length;i++){ + commonXPath +="/"+commonTokens[i]; + + } + } + + return commonXPath; + } + + + public List getXmlColumnPrs() { + + if(xmlColumnPr==null){ + xmlColumnPr = new Vector(); + for(CTTableColumn column:ctTable.getTableColumns().getTableColumnArray()){ + if(column.getXmlColumnPr()!=null){ + XSSFXmlColumnPr columnPr = new XSSFXmlColumnPr(this,column,column.getXmlColumnPr()); + xmlColumnPr.add(columnPr); + } + } + } + return xmlColumnPr; + } + + /** + * the number of mapped table columns (see Open Office XML Part 4: chapter 3.5.1.4) + * @return + */ + public long getNumerOfMappedColumns(){ + return ctTable.getTableColumns().getCount(); + } + + + /** + * The reference for the cell in the top-left part of the table + * (see Open Office XML Part 4: chapter 3.5.1.2, attribute ref) + * @return + */ + public CellReference getStartCellReference() { + + if(startCellReference==null){ + String ref = ctTable.getRef(); + String[] boundaries = ref.split(":"); + String from = boundaries[0]; + startCellReference = new CellReference(from); + } + return startCellReference; + } + + /** + * The reference for the cell in the bottom-right part of the table + * (see Open Office XML Part 4: chapter 3.5.1.2, attribute ref) + * @return + */ + public CellReference getEndCellReference() { + + if(endCellReference==null){ + + String ref = ctTable.getRef(); + String[] boundaries = ref.split(":"); + String from = boundaries[1]; + endCellReference = new CellReference(from); + } + return endCellReference; + } + + + /** + * Gets the total number of rows in the selection. (Note: in this version autofiltering is ignored) + * @return + */ + public int getRowCount(){ + + + CellReference from = getStartCellReference(); + CellReference to = getEndCellReference(); + + int rowCount = -1; + if (from!=null && to!=null){ + rowCount = to.getRow()-from.getRow(); + } + return rowCount; + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFMap.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFMap.java new file mode 100755 index 0000000000..33c742f6e7 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFMap.java @@ -0,0 +1,120 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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.apache.poi.xssf.usermodel; + +import java.util.List; +import java.util.Vector; + +import org.apache.poi.POIXMLDocumentPart; +import org.apache.poi.xssf.model.MapInfo; +import org.apache.poi.xssf.model.SingleXmlCells; +import org.apache.poi.xssf.model.Table; +import org.apache.poi.xssf.usermodel.helpers.XSSFSingleXmlCell; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTMap; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTSchema; +import org.w3c.dom.Node; + +/** + * This class implements the Map element (Open Office XML Part 4: + * chapter 3.16.2) + *

+ * This element contains all of the properties related to the XML map, + * and the behaviors expected during data refresh operations. + * + * @author Roberto Manicardi + */ + + +public class XSSFMap { + + private CTMap ctMap; + + private MapInfo mapInfo; + + + public XSSFMap(CTMap ctMap, MapInfo mapInfo) { + this.ctMap = ctMap; + this.mapInfo = mapInfo; + } + + + public CTMap getCtMap() { + return ctMap; + } + + + public CTSchema getCTSchema() { + String schemaId = ctMap.getSchemaID(); + return mapInfo.getCTSchemaById(schemaId); + } + + public Node getSchema() { + Node xmlSchema = null; + + CTSchema schema = getCTSchema(); + xmlSchema = schema.getDomNode().getFirstChild(); + + return xmlSchema; + } + + /** + * @return the list of Single Xml Cells that provide a map rule to this mapping. + */ + public List getRelatedSingleXMLCell() { + List relatedSimpleXmlCells = new Vector(); + + int sheetNumber = mapInfo.getWorkbook().getNumberOfSheets(); + for (int i = 0; i < sheetNumber; i++) { + XSSFSheet sheet = mapInfo.getWorkbook().getSheetAt(i); + for (POIXMLDocumentPart p : sheet.getRelations()) { + if (p instanceof SingleXmlCells) { + SingleXmlCells singleXMLCells = (SingleXmlCells) p; + for (XSSFSingleXmlCell cell : singleXMLCells.getAllSimpleXmlCell()) { + if (cell.getMapId() == ctMap.getID()) { + relatedSimpleXmlCells.add(cell); + } + } + } + } + } + return relatedSimpleXmlCells; + } + + /** + * @return the list of all Tables that provide a map rule to this mapping + */ + public List

getRelatedTables() { + + List
tables = new Vector
(); + int sheetNumber = mapInfo.getWorkbook().getNumberOfSheets(); + + for (int i = 0; i < sheetNumber; i++) { + XSSFSheet sheet = mapInfo.getWorkbook().getSheetAt(i); + for (POIXMLDocumentPart p : sheet.getRelations()) { + if (p.getPackageRelationship().getRelationshipType().equals(XSSFRelation.TABLE.getRelation())) { + Table table = (Table) p; + if (table.mapsTo(ctMap.getID())) { + tables.add(table); + } + } + } + } + + return tables; + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFRelation.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFRelation.java index 73fe263a26..c3b97c6c75 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFRelation.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFRelation.java @@ -25,10 +25,13 @@ import java.util.HashMap; import org.apache.poi.POIXMLDocument; import org.apache.poi.POIXMLRelation; import org.apache.poi.POIXMLDocumentPart; +import org.apache.poi.xssf.model.MapInfo; +import org.apache.poi.xssf.model.SingleXmlCells; import org.apache.poi.xssf.model.StylesTable; import org.apache.poi.xssf.model.SharedStringsTable; import org.apache.poi.xssf.model.CommentsTable; import org.apache.poi.xssf.model.CalculationChain; +import org.apache.poi.xssf.model.Table; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; @@ -93,6 +96,28 @@ public final class XSSFRelation extends POIXMLRelation { "/xl/drawings/vmlDrawing#.vml", null ); + + public static final XSSFRelation CUSTOM_XML_MAPPINGS = new XSSFRelation( + "application/xml", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/xmlMaps", + "/xl/xmlMaps.xml", + MapInfo.class + ); + + public static final XSSFRelation SINGLE_XML_CELLS = new XSSFRelation( + "application/vnd.openxmlformats-officedocument.spreadsheetml.tableSingleCells+xml", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableSingleCells", + "/tables/tableSingleCells#.xml", + SingleXmlCells.class + ); + + public static final XSSFRelation TABLE = new XSSFRelation( + "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table", + "/tables/table#.xml", + Table.class + ); + public static final XSSFRelation IMAGES = new XSSFRelation( null, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFWorkbook.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFWorkbook.java index 5d509260e3..5453b33b14 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFWorkbook.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFWorkbook.java @@ -22,12 +22,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.regex.Pattern; import javax.xml.namespace.QName; @@ -56,6 +51,8 @@ import org.apache.poi.util.PackageHelper; import org.apache.poi.xssf.model.CalculationChain; import org.apache.poi.xssf.model.SharedStringsTable; import org.apache.poi.xssf.model.StylesTable; +import org.apache.poi.xssf.model.MapInfo; +import org.apache.poi.xssf.extractor.XSSFExportToXml; import org.apache.xmlbeans.XmlException; import org.apache.xmlbeans.XmlObject; import org.apache.xmlbeans.XmlOptions; @@ -106,6 +103,11 @@ public class XSSFWorkbook extends POIXMLDocument implements Workbook, Iterable getCustomXMLMappings(){ + return mapInfo == null ? new ArrayList() : mapInfo.getAllXSSFMaps(); + } } diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFSingleXmlCell.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFSingleXmlCell.java new file mode 100755 index 0000000000..055f2b8145 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFSingleXmlCell.java @@ -0,0 +1,84 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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.apache.poi.xssf.usermodel.helpers; + +import org.apache.poi.ss.util.CellReference; +import org.apache.poi.xssf.model.SingleXmlCells; +import org.apache.poi.xssf.usermodel.XSSFCell; +import org.apache.poi.xssf.usermodel.XSSFRow; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTSingleXmlCell; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTXmlCellPr; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTXmlPr; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.STXmlDataType.Enum; + +/** + * + * This class is a wrapper around the CTSingleXmlCell (Open Office XML Part 4: + * chapter 3.5.2.1) + * + + * + * @author Roberto Manicardi + * + */ +public class XSSFSingleXmlCell { + + private CTSingleXmlCell singleXmlCell; + private SingleXmlCells parent; + + + public XSSFSingleXmlCell(CTSingleXmlCell singleXmlCell, SingleXmlCells parent){ + this.singleXmlCell = singleXmlCell; + this.parent = parent; + } + + /** + * Gets the XSSFCell referenced by the R attribute + * @return the referenced XSSFCell, null if the cell reference is invalid + */ + public XSSFCell getReferencedCell(){ + XSSFCell cell = null; + + + CellReference cellReference = new CellReference(singleXmlCell.getR()); + + XSSFRow row = parent.getXSSFSheet().getRow(cellReference.getRow()); + cell = row.getCell(cellReference.getCol()); + + + return cell; + } + + public String getXpath(){ + CTXmlCellPr xmlCellPr = singleXmlCell.getXmlCellPr(); + CTXmlPr xmlPr = xmlCellPr.getXmlPr(); + String xpath = xmlPr.getXpath(); + return xpath; + } + + public long getMapId(){ + return singleXmlCell.getXmlCellPr().getXmlPr().getMapId(); + } + + public Enum getXmlDataType() { + CTXmlCellPr xmlCellPr = singleXmlCell.getXmlCellPr(); + CTXmlPr xmlPr = xmlCellPr.getXmlPr(); + return xmlPr.getXmlDataType(); + } + +} diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFXmlColumnPr.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFXmlColumnPr.java new file mode 100755 index 0000000000..c03b56e250 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFXmlColumnPr.java @@ -0,0 +1,65 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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.apache.poi.xssf.usermodel.helpers; + +import org.apache.poi.xssf.model.Table; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTableColumn; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTXmlColumnPr; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.STXmlDataType.Enum; + + +/** + * + * This class is a wrapper around the CTXmlColumnPr (Open Office XML Part 4: + * chapter 3.5.1.7) + * + * + * @author Roberto Manicardi + */ +public class XSSFXmlColumnPr { + + private Table table; + private CTTableColumn ctTableColumn; + private CTXmlColumnPr ctXmlColumnPr; + + public XSSFXmlColumnPr(Table table ,CTTableColumn ctTableColum,CTXmlColumnPr ctXmlColumnPr){ + this.table = table; + this.ctTableColumn = ctTableColum; + this.ctXmlColumnPr = ctXmlColumnPr; + } + + public long getMapId(){ + return ctXmlColumnPr.getMapId(); + } + + public String getLocalXPath(){ + String localXPath = ""; + int numberOfCommonXPathAxis = table.getCommonXpath().split("/").length-1; + + String[] xPathTokens = ctXmlColumnPr.getXpath().split("/"); + for(int i=numberOfCommonXPathAxis; i")[1].split("")[0].trim(); + String nome = xml.split("")[1].split("")[0].trim(); + String tutor = xml.split("")[1].split("")[0].trim(); + String cdl = xml.split("")[1].split("")[0].trim(); + String durata = xml.split("")[1].split("")[0].trim(); + String argomento = xml.split("")[1].split("")[0].trim(); + String progetto = xml.split("")[1].split("")[0].trim(); + String crediti = xml.split("")[1].split("")[0].trim(); + + assertEquals("ro",docente); + assertEquals("ro",nome); + assertEquals("ds",tutor); + assertEquals("gs",cdl); + assertEquals("g",durata); + assertEquals("gvvv",argomento); + assertEquals("aaaa",progetto); + assertEquals("aa",crediti); + + } + + } + + } + + + public void testExportToXMLInverseOrder() throws Exception{ + + + + XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("CustomXmlMappings-inverse-order.xlsx"); + + MapInfo mapInfo = null; + + for(POIXMLDocumentPart p : wb.getRelations()){ + + + if(p instanceof MapInfo){ + mapInfo = (MapInfo) p; + + + XSSFMap map = mapInfo.getXSSFMapById(1); + XSSFExportToXml exporter = new XSSFExportToXml(map); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + exporter.exportToXML(os,true); + String xml = os.toString("UTF-8"); + + assertNotNull(xml); + assertTrue(!xml.equals("")); + + String docente = xml.split("")[1].split("")[0].trim(); + String nome = xml.split("")[1].split("")[0].trim(); + String tutor = xml.split("")[1].split("")[0].trim(); + String cdl = xml.split("")[1].split("")[0].trim(); + String durata = xml.split("")[1].split("")[0].trim(); + String argomento = xml.split("")[1].split("")[0].trim(); + String progetto = xml.split("")[1].split("")[0].trim(); + String crediti = xml.split("")[1].split("")[0].trim(); + + assertEquals("aa",nome); + assertEquals("aaaa",docente); + assertEquals("gvvv",tutor); + assertEquals("g",cdl); + assertEquals("gs",durata); + assertEquals("ds",argomento); + assertEquals("ro",progetto); + assertEquals("ro",crediti); + + } + + } + + + + + + } + + public void testXPathOrdering() throws Exception{ + + XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("CustomXmlMappings-inverse-order.xlsx"); + + MapInfo mapInfo = null; + + for(POIXMLDocumentPart p : wb.getRelations()){ + + + if(p instanceof MapInfo){ + mapInfo = (MapInfo) p; + + XSSFMap map = mapInfo.getXSSFMapById(1); + XSSFExportToXml exporter = new XSSFExportToXml(map); + + assertEquals(1,exporter.compare("/CORSO/DOCENTE", "/CORSO/NOME")); + + assertEquals(-1,exporter.compare("/CORSO/NOME", "/CORSO/DOCENTE")); + + + } + } + } + + + + + public void testMultiTable() throws Exception{ + + + XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("CustomXMLMappings-complex-type.xlsx"); + + for(POIXMLDocumentPart p : wb.getRelations()){ + + + if(p instanceof MapInfo){ + MapInfo mapInfo = (MapInfo) p; + + XSSFMap map = mapInfo.getXSSFMapById(2); + + assertNotNull(map); + + XSSFExportToXml exporter = new XSSFExportToXml(map); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + exporter.exportToXML(os,true); + String xml = os.toString("UTF-8"); + + assertNotNull(xml); + + String[] regexConditions = { "", + "", + "", + "DataBinding", + "Map Append=\"false\" AutoFit=\"false\" ID=\"1\"", + "Map Append=\"false\" AutoFit=\"false\" ID=\"5\"" + + }; + + for(String condition : regexConditions){ + Pattern pattern = Pattern.compile(condition); + Matcher matcher = pattern.matcher(xml); + assertTrue(matcher.find()); + } + + + } + } + + + } + + + +} diff --git a/src/ooxml/testcases/org/apache/poi/xssf/model/TestMapInfo.java b/src/ooxml/testcases/org/apache/poi/xssf/model/TestMapInfo.java new file mode 100755 index 0000000000..35c80f06d7 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xssf/model/TestMapInfo.java @@ -0,0 +1,84 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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.apache.poi.xssf.model; + +import org.apache.poi.POIXMLDocumentPart; +import org.apache.poi.xssf.XSSFTestDataSamples; +import org.apache.poi.xssf.usermodel.XSSFMap; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTSchema; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTMapInfo; +import org.w3c.dom.Node; + +import junit.framework.TestCase; + +/** + * @author Roberto Manicardi + */ +public class TestMapInfo extends TestCase { + + + public void testMapInfoExists() throws Exception { + + + XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("CustomXmlMappings.xlsx"); + + MapInfo mapInfo = null; + SingleXmlCells singleXMLCells = null; + + for (POIXMLDocumentPart p : wb.getRelations()) { + + + if (p instanceof MapInfo) { + mapInfo = (MapInfo) p; + + + CTMapInfo ctMapInfo = mapInfo.getCTMapInfo(); + + assertNotNull(ctMapInfo); + + CTSchema[] schemas = ctMapInfo.getSchemaArray(); + assertEquals(1, schemas.length); + + + for (XSSFMap map : mapInfo) { + Node xmlSchema = map.getSchema(); + assertNotNull(xmlSchema); + } + } + } + + XSSFSheet sheet1 = wb.getSheetAt(0); + + for (POIXMLDocumentPart p : sheet1.getRelations()) { + + if (p instanceof SingleXmlCells) { + singleXMLCells = (SingleXmlCells) p; + } + + } + + + assertNotNull(mapInfo); + assertNotNull(singleXMLCells); + + + } + + +} diff --git a/src/testcases/org/apache/poi/hssf/data/CustomXMLMappings-complex-type.xlsx b/src/testcases/org/apache/poi/hssf/data/CustomXMLMappings-complex-type.xlsx new file mode 100755 index 0000000000..0d6d8d0099 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/CustomXMLMappings-complex-type.xlsx differ diff --git a/src/testcases/org/apache/poi/hssf/data/CustomXMLMappings.xlsx b/src/testcases/org/apache/poi/hssf/data/CustomXMLMappings.xlsx new file mode 100755 index 0000000000..32a5573c29 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/CustomXMLMappings.xlsx differ diff --git a/src/testcases/org/apache/poi/hssf/data/CustomXmlMappings-inverse-order.xlsx b/src/testcases/org/apache/poi/hssf/data/CustomXmlMappings-inverse-order.xlsx new file mode 100755 index 0000000000..f3acef3359 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/CustomXmlMappings-inverse-order.xlsx differ