Cell.java

  1. /*
  2.  * Copyright 2012 The Apache Software Foundation.
  3.  *
  4.  * Licensed under the Apache License, Version 2.0 (the "License");
  5.  * you may not use this file except in compliance with the License.
  6.  * You may obtain a copy of the License at
  7.  *
  8.  *      http://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  * Unless required by applicable law or agreed to in writing, software
  11.  * distributed under the License is distributed on an "AS IS" BASIS,
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  * See the License for the specific language governing permissions and
  14.  * limitations under the License.
  15.  */
  16. package org.odftoolkit.odfdom.changes;

  17. import static org.odftoolkit.odfdom.changes.JsonOperationConsumer.addParagraph;
  18. import static org.odftoolkit.odfdom.changes.JsonOperationConsumer.addStyle;
  19. import static org.odftoolkit.odfdom.changes.JsonOperationConsumer.addText;
  20. import static org.odftoolkit.odfdom.changes.OperationConstants.OPK_STYLE_ID;

  21. import java.util.logging.Level;
  22. import java.util.logging.Logger;
  23. import org.json.JSONException;
  24. import org.json.JSONObject;
  25. import org.odftoolkit.odfdom.dom.OdfDocumentNamespace;
  26. import org.odftoolkit.odfdom.dom.element.number.DataStyleElement;
  27. import org.odftoolkit.odfdom.dom.element.number.NumberBooleanStyleElement;
  28. import org.odftoolkit.odfdom.dom.element.number.NumberTextStyleElement;
  29. import org.odftoolkit.odfdom.dom.element.table.TableTableCellElement;
  30. import org.odftoolkit.odfdom.dom.element.text.TextAElement;
  31. import org.odftoolkit.odfdom.dom.element.text.TextPElement;
  32. import org.odftoolkit.odfdom.dom.element.text.TextParagraphElementBase;
  33. import org.odftoolkit.odfdom.dom.style.props.OdfStylePropertiesSet;
  34. import org.odftoolkit.odfdom.incubator.doc.number.OdfNumberCurrencyStyle;
  35. import org.odftoolkit.odfdom.incubator.doc.number.OdfNumberDateStyle;
  36. import org.odftoolkit.odfdom.incubator.doc.number.OdfNumberPercentageStyle;
  37. import org.odftoolkit.odfdom.incubator.doc.number.OdfNumberStyle;
  38. import org.odftoolkit.odfdom.incubator.doc.number.OdfNumberTimeStyle;
  39. import org.odftoolkit.odfdom.incubator.doc.style.OdfStyle;
  40. import org.odftoolkit.odfdom.pkg.OdfElement;
  41. import org.odftoolkit.odfdom.pkg.OdfFileDom;
  42. import org.w3c.dom.Node;
  43. import org.w3c.dom.NodeList;

  44. /**
  45.  * A MultiCoomponent uses a single XML element to represent multiple components. This container can
  46.  * be used for spreadsheet row and cell components using repeated elements via an attribute.
  47.  *
  48.  * @author svante.schubertATgmail.com
  49.  * @param <T>
  50.  */
  51. class Cell<T> extends Component {

  52.   private static final String FORMULA_PREFIX = "of:";

  53.   public Cell(OdfElement componentElement, Component parent) {
  54.     super(componentElement, parent);
  55.   }

  56.   /**
  57.    * A multiple components can be represented by a single XML element
  58.    *
  59.    * @return the number of components the elements represents
  60.    */
  61.   @Override
  62.   public int repetition() {
  63.     return mRootElement.getRepetition();
  64.   }

  65.   // CELL ONLY
  66.   //    Map<String, Object> mInnerCellStyle = null;
  67.   //
  68.   //    /** The inner style of a cell will be temporary saved at the cell.
  69.   //     Whenever the cell content is deleted, the style is being merged/applied to the cell style */
  70.   //    public Map<String, Object> getInternalCellStyle(){
  71.   //        return mInnerCellStyle;
  72.   //    }
  73.   //
  74.   //
  75.   //    /** The inner style of a cell will be temporary saved at the cell.
  76.   //     Whenever the cell content is deleted, the style is being merged/applied to the cell style */
  77.   //    public void setInternalCellStyle(Map<String, Object> newStyles){
  78.   //        mInnerCellStyle = newStyles;
  79.   //    }
  80.   //
  81.   /** Adds the given component to the root element */
  82.   @Override
  83.   public void addChild(int index, Component c) {
  84.     mRootElement.insert(c.getRootElement(), index);
  85.     // 2DO: Svante: ARE THE ABOVE AND THE BELOW EQUIVALENT?
  86.     //      OdfElement rootElement = c.getRootElement();
  87.     //      if (index >= 0) {
  88.     //          mRootElement.insertBefore(rootElement, ((OdfElement) mRootElement).receiveNode(index));
  89.     //      } else {
  90.     //          mRootElement.appendChild(rootElement);
  91.     //      }
  92.   }

  93.   /** @return either a text node of size 1 or an element being the root element of a component */
  94.   @Override
  95.   public Node getChildNode(int index) {
  96.     return mRootElement.receiveNode(index);
  97.   }

  98.   /**
  99.    * Removes a component from the text element container. Removes either an element representing a
  100.    * component or text node of size 1
  101.    */
  102.   @Override
  103.   public Node remove(int index) {
  104.     Node removedNode = null;
  105.     Node node = this.getChildNode(index);
  106.     if (node != null) {
  107.       removedNode = mRootElement.removeChild(node);
  108.     }
  109.     return removedNode;
  110.   }

  111.   /**
  112.    * All children of the root element will be traversed. If it is a text node the size is added, if
  113.    * it is an element and a component a size of one is added, if it is a marker, for known text
  114.    * marker elements (text:span, text:bookmark) the children are recursive checked
  115.    *
  116.    * @return the number of child components
  117.    */
  118.   @Override
  119.   public int size() {
  120.     return mRootElement.componentSize();
  121.   }

  122.   private static final String FLOAT = "float";
  123.   private static final String STRING = "string";
  124.   private static final String CURRENCY = "currency";
  125.   private static final String DATE = "date";
  126.   private static final String TIME = "time";
  127.   private static final String PERCENTAGE = "percentage";
  128.   private static final String BOOLEAN = "boolean";

  129.   /**
  130.    * Adding cell content: either as formula or paragraph and text content, latter with default
  131.    * styles
  132.    */
  133.   public TableTableCellElement addCellStyleAndContent(
  134.       Component rootComponent, Object value, JSONObject attrs) {
  135.     // see if the cell is repeated
  136.     TableTableCellElement cell = (TableTableCellElement) this.getRootElement();
  137.     OdfFileDom ownerDoc = (OdfFileDom) rootComponent.getOwnerDocument();
  138.     // save the URL as everyting else will be deleted
  139.     String url = reuseCellHyperlink(cell, attrs);
  140.     boolean setValueType = true;
  141.     boolean isNumberValue = true;
  142.     // exchanges the content if requested
  143.     if (value != null) {
  144.       cell.removeContent();
  145.       // if there is new content..
  146.       if (!value.equals(JSONObject.NULL)) {
  147.         String valueString = value.toString();
  148.         if (valueString.startsWith(Constants.EQUATION)) {
  149.           // How am I able to set the other values? What is the OOXML solution for this?
  150.           cell.setAttributeNS(OdfDocumentNamespace.OFFICE.getUri(), "office:value-type", FLOAT);
  151.           cell.setAttributeNS(
  152.               OdfDocumentNamespace.TABLE.getUri(),
  153.               "table:formula",
  154.               FORMULA_PREFIX.concat(valueString));
  155.         } else {
  156.           // insert a paragraph to store the text within..
  157.           TextParagraphElementBase newParagraph = addParagraph(this, 0, attrs);
  158.           if (url != null) {
  159.             attrs = addUrlToCharacterProps(attrs, url);
  160.           }
  161.           // if the formula was masked
  162.           if (value instanceof String
  163.               && valueString.startsWith(Constants.APOSTROPHE_AND_EQUATION)) {
  164.             // cut the first apostrophe
  165.             valueString = valueString.substring(1);
  166.           }
  167.           // addChild the text & removes existing values
  168.           addText(newParagraph, 0, attrs, valueString);
  169.           if (value instanceof Integer || value instanceof Double || value instanceof Float) {
  170.             isNumberValue = true;
  171.             cell.setAttributeNS(OdfDocumentNamespace.OFFICE.getUri(), "office:value", valueString);
  172.           } else if (value instanceof String) {
  173.             cell.setAttributeNS(OdfDocumentNamespace.OFFICE.getUri(), "office:value-type", STRING);
  174.             setValueType = false;
  175.           }
  176.         }
  177.       }
  178.     }
  179.     if (attrs != null) {
  180.       // Format: Adding Styles to the element
  181.       addStyle(attrs, cell, ownerDoc);
  182.       if (cell.hasChildNodes()) {
  183.         NodeList children = cell.getChildNodes();
  184.         for (int i = 0; i < children.getLength(); i++) {
  185.           Node child = children.item(i);
  186.           if (child instanceof OdfElement) {
  187.             ((OdfElement) child).markText(0, Integer.MAX_VALUE - 1, attrs);
  188.           }
  189.         }
  190.       } else {
  191.         if (url != null) {
  192.           TextPElement containerElement1 = new TextPElement(ownerDoc);
  193.           TextAElement containerElement2 = new TextAElement(ownerDoc);
  194.           containerElement2.setXlinkHrefAttribute(url);
  195.           cell.appendChild(containerElement1);
  196.           containerElement1.appendChild(containerElement2);
  197.         }
  198.       }
  199.     }
  200.     // value-type has to be set if
  201.     // - a number is set replacing a previous string-type
  202.     // - if a number is changed to a string (already done above ) or vice-versa
  203.     // - if attributes have been set (that contain a number format or a new style)
  204.     //
  205.     if (setValueType) {
  206.       String currentValueType = cell.getOfficeValueTypeAttribute();
  207.       boolean isStringType = currentValueType != null && currentValueType.equals("String");
  208.       boolean changedToNumber = isNumberValue && isStringType;
  209.       boolean numberFormatChanged = false;
  210.       if (attrs != null) {
  211.         JSONObject cellAttrs = attrs.optJSONObject("cell");
  212.         if (cellAttrs != null) {
  213.           numberFormatChanged = cellAttrs.has("formatCode");
  214.         }
  215.         if (!numberFormatChanged) {
  216.           String styleId = attrs.optString(OPK_STYLE_ID);
  217.           numberFormatChanged = styleId != null;
  218.         }
  219.       }
  220.       if (currentValueType == null || changedToNumber || numberFormatChanged) {
  221.         DataStyleElement dataStyle = cell.getOwnDataStyle();
  222.         if (dataStyle != null) {
  223.           String valueType = "";
  224.           String currencySymbol = "";
  225.           if (dataStyle instanceof OdfNumberStyle) {
  226.             valueType = FLOAT;
  227.           } else if (dataStyle instanceof OdfNumberCurrencyStyle) {
  228.             currencySymbol =
  229.                 ((OdfNumberCurrencyStyle) dataStyle).getCurrencySymbolElement().getTextContent();
  230.             valueType = CURRENCY;
  231.           } else if (dataStyle instanceof NumberTextStyleElement) {
  232.             valueType = STRING;
  233.           } else if (dataStyle instanceof OdfNumberDateStyle) {
  234.             valueType = DATE;
  235.           } else if (dataStyle instanceof OdfNumberTimeStyle) {
  236.             valueType = TIME;
  237.           } else if (dataStyle instanceof OdfNumberPercentageStyle) {
  238.             valueType = PERCENTAGE;
  239.           } else if (dataStyle instanceof NumberBooleanStyleElement) {
  240.             valueType = BOOLEAN;
  241.           }
  242.           if (!valueType.isEmpty()) {
  243.             cell.setOfficeValueTypeAttribute(valueType);
  244.             cell.setCalcextValueTypeAttribute(valueType);
  245.             cell.setOfficeCurrencyAttribute(currencySymbol);
  246.             // make sure that an appropriate value is available:
  247.             if (value == null && cell.getOfficeValueAttribute() == null) {
  248.               String oldDateValue = cell.getOfficeDateValueAttribute();
  249.               String oldTimeValue = cell.getOfficeTimeValueAttribute();
  250.               Boolean oldBooleanValue = cell.getOfficeBooleanValueAttribute();
  251.               Double newValue = null;
  252.               if (oldDateValue != null) {
  253.                 newValue = new Double(MapHelper.dateToDouble(oldDateValue));
  254.               } else if (oldTimeValue != null) {
  255.                 newValue = new Double(MapHelper.timeToDouble(oldTimeValue));
  256.               } else if (oldBooleanValue != null) {
  257.                 newValue = new Double(oldBooleanValue.booleanValue() ? 1 : 0);
  258.               }

  259.               if (newValue != null) {
  260.                 cell.setOfficeValueAttribute(newValue);
  261.               }
  262.             }
  263.           }
  264.         }
  265.       }
  266.     }
  267.     return cell;
  268.   }

  269.   /**
  270.    * To be able to reuse existing style on the full table, new cell hyperlinks will be stored in the
  271.    * cell text style properties as @xlink:href attribute and taken back when nothing new is set.
  272.    */
  273.   private static String reuseCellHyperlink(TableTableCellElement cell, JSONObject attrs) {
  274.     String cellURL = null;
  275.     if (attrs != null) { // apply style changes to the cell
  276.       // apply new styles to the cell (modifying not overwriting)
  277.       if (attrs.has("character")) {
  278.         JSONObject charProps = attrs.optJSONObject("character");
  279.         if (charProps != null) {
  280.           if (charProps.has("url") && !charProps.get("url").equals(JSONObject.NULL)) {
  281.             cellURL = charProps.optString("url");
  282.           } else if (charProps.has("url")) {
  283.             // removeAnchors();
  284.           }
  285.         }
  286.       }
  287.     }
  288.     // if there is no new hyperlink given, check for an existing cached (in the properties)
  289.     if (cellURL == null || cellURL.isEmpty()) {
  290.       // check if there is still one given at the cell
  291.       OdfStyle autoStyle = cell.getAutomaticStyle();
  292.       if (autoStyle != null) {
  293.         OdfElement textProps = autoStyle.getPropertiesElement(OdfStylePropertiesSet.TextProperties);
  294.         if (textProps != null
  295.             && textProps.hasAttributeNS(OdfDocumentNamespace.XLINK.getUri(), "href")) {
  296.           cellURL = textProps.getAttributeNS(OdfDocumentNamespace.XLINK.getUri(), "href");
  297.         }
  298.       }
  299.     }
  300.     return cellURL;
  301.   }

  302.   private static JSONObject addUrlToCharacterProps(JSONObject attrs, String cellURL) {
  303.     JSONObject charProps = null;
  304.     if (cellURL != null && !cellURL.isEmpty()) {
  305.       if (attrs == null) {
  306.         attrs = new JSONObject();
  307.       }
  308.       if (!attrs.has("character")) {
  309.         charProps = new JSONObject();
  310.         try {
  311.           attrs.put("character", charProps);
  312.         } catch (JSONException ex) {
  313.           Logger.getLogger(JsonOperationConsumer.class.getName()).log(Level.SEVERE, null, ex);
  314.         }
  315.       } else {
  316.         charProps = attrs.optJSONObject("character");
  317.       }
  318.       try {
  319.         charProps.put("url", cellURL);
  320.       } catch (JSONException ex) {
  321.         Logger.getLogger(JsonOperationConsumer.class.getName()).log(Level.SEVERE, null, ex);
  322.       }
  323.     }
  324.     return attrs;
  325.   }
  326. }