001package org.hl7.fhir.utilities.xls;
002
003import java.io.File;
004import java.io.FileInputStream;
005import java.io.FileOutputStream;
006import java.io.IOException;
007import java.io.InputStream;
008
009import javax.xml.parsers.DocumentBuilder;
010import javax.xml.parsers.DocumentBuilderFactory;
011import javax.xml.parsers.ParserConfigurationException;
012import javax.xml.transform.Result;
013import javax.xml.transform.Source;
014import javax.xml.transform.Transformer;
015import javax.xml.transform.TransformerException;
016import javax.xml.transform.TransformerFactory;
017import javax.xml.transform.dom.DOMSource;
018import javax.xml.transform.stream.StreamResult;
019
020import org.hl7.fhir.exceptions.FHIRException;
021import org.hl7.fhir.utilities.TextFile;
022import org.hl7.fhir.utilities.Utilities;
023import org.hl7.fhir.utilities.xml.XMLUtil;
024import org.w3c.dom.Document;
025import org.w3c.dom.Element;
026import org.w3c.dom.Node;
027import org.xml.sax.SAXException;
028
029public class XLSXmlNormaliser {
030  
031  private static final String XLS_NS = "urn:schemas-microsoft-com:office:spreadsheet";
032
033  private Document xml;
034
035  private String source;
036  private String dest;
037  private boolean exceptionIfExcelNotNormalised;
038  
039  public XLSXmlNormaliser(String source, String dest, boolean exceptionIfExcelNotNormalised) {
040    super();
041    this.source = source;
042    this.dest = dest;
043    this.exceptionIfExcelNotNormalised = exceptionIfExcelNotNormalised;
044  }
045  
046  public XLSXmlNormaliser(String source, boolean exceptionIfExcelNotNormalised) {
047    super();
048    this.source = source;
049    this.dest = source;
050    this.exceptionIfExcelNotNormalised = exceptionIfExcelNotNormalised;
051  }
052  
053  public void go() throws FHIRException, TransformerException, ParserConfigurationException, SAXException, IOException {
054    File inp = new File(source);
055    long time = inp.lastModified();
056    xml = parseXml(new FileInputStream(inp));
057    
058    Element root = xml.getDocumentElement();
059
060    boolean hasComment = false;
061    Node n = root.getFirstChild();
062    while (n != null) {
063      if (n.getNodeType() == Node.COMMENT_NODE && "canonicalized".equals(n.getTextContent())) {
064        hasComment = true;
065        break;
066      }
067      n = n.getNextSibling();
068    }
069    if (hasComment)
070      return;
071    if (exceptionIfExcelNotNormalised)
072      throw new FHIRException("The spreadsheet "+dest+" was committed after editing in excel, but before the build could run *after Excel was closed*");
073    
074    System.out.println("normalise: "+source);
075    
076    XMLUtil.deleteByName(root, "ActiveSheet");
077    Element xw = XMLUtil.getNamedChild(root, "ExcelWorkbook");
078    XMLUtil.deleteByName(xw, "WindowHeight");
079    XMLUtil.deleteByName(xw, "WindowWidth");
080    XMLUtil.deleteByName(xw, "WindowTopX");
081    XMLUtil.deleteByName(xw, "WindowTopY");
082
083    for (Element wk : XMLUtil.getNamedChildren(root, "Worksheet"))
084      processWorksheet(wk);
085    
086    if (!hasComment)
087      root.appendChild(xml.createComment("canonicalized"));
088    try {
089      saveXml(new FileOutputStream(dest));
090      String s = TextFile.fileToString(dest);
091      s = s.replaceAll("\r\n","\n");
092      s = replaceSignificantEoln(s);
093      TextFile.stringToFile(s, dest, false);
094      new File(dest).setLastModified(time);
095    } catch (Exception e) {
096      System.out.println("The file "+dest+" is still open in Excel, and you will have to run the build after closing Excel before committing");
097    }
098  }
099
100  private String replaceSignificantEoln(String s) {
101    StringBuilder b = new StringBuilder();
102    boolean hasText = false;
103    for (char c : s.toCharArray()) {
104      if (c == '>' || c == '<' ) {
105        hasText = false;
106        b.append(c);
107      } else if (c == '\n') {
108        if (hasText) {
109          b.append("&#10;");
110        } else
111          b.append(c);
112        
113      } else if (!Character.isWhitespace(c)) {
114        b.append(c);
115        hasText = true;
116      } else 
117        b.append(c);
118    }
119    
120    return b.toString();
121  }
122
123  private void processWorksheet(Element wk) throws FHIRException  {
124    Element tbl = XMLUtil.getNamedChild(wk, "Table");
125    processTable(tbl);
126    for (Element row : XMLUtil.getNamedChildren(tbl, "Row"))
127      processRow(row);      
128    for (Element col : XMLUtil.getNamedChildren(tbl, "Column"))
129      processCol(col);      
130    for (Element wo : XMLUtil.getNamedChildren(wk, "WorksheetOptions"))
131      processOptions(wo);      
132  }
133  
134  private void processOptions(Element wo) {
135    XMLUtil.deleteByName(wo, "Unsynced");
136    XMLUtil.deleteByName(wo, "Panes");
137    for (Element panes : XMLUtil.getNamedChildren(wo, "Panes"))
138      processPanes(panes);      
139  }
140
141  private void processPanes(Element panes) {
142    for (Element pane : XMLUtil.getNamedChildren(panes, "Pane"))
143      processPane(pane);        
144  }
145
146  private void processPane(Element pane) {
147    XMLUtil.deleteByName(pane, "ActiveRow");
148    XMLUtil.deleteByName(pane, "ActiveCol");    
149  }
150
151//  private void setTextElement(Element e, String name, String text) {
152//    Element te = XMLUtil.getNamedChild(e, name);
153//    if (te != null)
154//      te.setTextContent(text);
155//  }
156
157  private void processTable(Element col) {
158    XMLUtil.deleteAttr(col, "urn:schemas-microsoft-com:office:spreadsheet", "DefaultColumnWidth");
159    XMLUtil.deleteAttr(col, "urn:schemas-microsoft-com:office:spreadsheet", "DefaultRowHeight");
160  }
161
162
163  private void processCol(Element col) {
164    String width = col.getAttributeNS("urn:schemas-microsoft-com:office:spreadsheet", "Width");
165    if (!Utilities.noString(width)) {
166      Double d = Double.valueOf(width);
167      width = Double.toString(Math.round(d*2)/2);
168      col.setAttributeNS("urn:schemas-microsoft-com:office:spreadsheet", "ss:Width", width);
169    }        
170  }
171
172  private void processRow(Element row) {
173    String height = row.getAttributeNS("urn:schemas-microsoft-com:office:spreadsheet", "Height");
174    if (!Utilities.noString(height) && height.contains(".")) {
175      Double d = Double.valueOf(height);
176      row.setAttributeNS("urn:schemas-microsoft-com:office:spreadsheet", "ss:Height", Long.toString(Math.round(d)));
177    }    
178  }
179
180  private void check(boolean test, String message) throws FHIRException  {
181    if (!test)
182      throw new FHIRException(message+" in "+getLocation());
183  }
184  
185
186  private Document parseXml(InputStream in) throws FHIRException, ParserConfigurationException, SAXException, IOException  {
187    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
188    factory.setNamespaceAware(true);
189    DocumentBuilder builder = factory.newDocumentBuilder();
190    return builder.parse(in);
191  }
192
193  private void saveXml(FileOutputStream stream) throws TransformerException, IOException {
194
195    TransformerFactory factory = TransformerFactory.newInstance();
196    Transformer transformer = factory.newTransformer();
197    Result result = new StreamResult(stream);
198    Source source = new DOMSource(xml);
199    transformer.transform(source, result);    
200    stream.flush();
201  }
202
203  private String getLocation() {
204    return source; //+", row "+rowIndex.toString();
205  }
206
207
208}