001package com.hfg.xml.msofficexml.xlsx.spreadsheetml;
002
003import java.time.LocalDate;
004import java.time.ZoneId;
005import java.time.temporal.ChronoUnit;
006import java.util.Calendar;
007import java.util.Date;
008import java.util.GregorianCalendar;
009import java.util.List;
010
011import com.hfg.datetime.DateUtil;
012import com.hfg.util.BooleanUtil;
013import com.hfg.util.StringUtil;
014import com.hfg.util.collection.CollectionUtil;
015import com.hfg.xml.XMLAttribute;
016import com.hfg.xml.XMLTag;
017import com.hfg.xml.XMLUtil;
018import com.hfg.xml.msofficexml.xlsx.CellRange;
019import com.hfg.xml.msofficexml.xlsx.CellRef;
020import com.hfg.xml.msofficexml.xlsx.spreadsheetml.style.SsmlCellFormat;
021
022
023//------------------------------------------------------------------------------
024/**
025 Represents an Office Open XML worksheet data cell (<ssml:c>) tag.
026
027 @author J. Alex Taylor, hairyfatguy.com
028 */
029//------------------------------------------------------------------------------
030// com.hfg XML/HTML Coding Library
031//
032// This library is free software; you can redistribute it and/or
033// modify it under the terms of the GNU Lesser General Public
034// License as published by the Free Software Foundation; either
035// version 2.1 of the License, or (at your option) any later version.
036//
037// This library is distributed in the hope that it will be useful,
038// but WITHOUT ANY WARRANTY; without even the implied warranty of
039// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
040// Lesser General Public License for more details.
041//
042// You should have received a copy of the GNU Lesser General Public
043// License along with this library; if not, write to the Free Software
044// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
045//
046// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
047// jataylor@hairyfatguy.com
048//------------------------------------------------------------------------------
049
050public class SsmlCell extends SsmlXMLTag
051{
052   private SsmlWorksheet  mParentWorksheet;
053   private SsmlCellFormat mCellFormat;
054
055   private CellRange      mMergedCellRange;
056
057   private Object  mValue;
058   private String  mStringValue;
059   private XMLTag  mValueTag;
060   private XMLTag  mFormulaTag;
061
062
063
064   private static final LocalDate REFERENCE_DATE;
065
066   static
067   {
068      Calendar cal = new GregorianCalendar();
069      cal.set(1900, Calendar.JANUARY, 1);
070      REFERENCE_DATE = cal.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
071   }
072
073   //###########################################################################
074   // CONSTRUCTORS
075   //###########################################################################
076
077   //---------------------------------------------------------------------------
078   public SsmlCell(SsmlWorksheet inParentWorksheet)
079   {
080      super(SsmlXML.CELL, inParentWorksheet.getParentDoc());
081      mParentWorksheet = inParentWorksheet;
082   }
083
084   //---------------------------------------------------------------------------
085   public SsmlCell(SsmlWorksheet inParentWorksheet, XMLTag inXMLTag)
086   {
087      this(inParentWorksheet);
088      inXMLTag.verifyTagName(SsmlXML.CELL);
089
090      if (CollectionUtil.hasValues(inXMLTag.getAttributes()))
091      {
092         for (XMLAttribute attr : inXMLTag.getAttributes())
093         {
094            setAttribute(attr);
095         }
096      }
097
098      // Copy subtags over to this tag
099      if (CollectionUtil.hasValues(inXMLTag.getSubtags()))
100      {
101         for (XMLTag subtag : (List<XMLTag>) (Object) inXMLTag.getSubtags())
102         {
103            addSubtag(subtag);
104         }
105      }
106   }
107
108   //###########################################################################
109   // PUBLIC METHODS
110   //###########################################################################
111
112   //---------------------------------------------------------------------------
113   public String toString()
114   {
115      Object value = getValue();
116      return (value != null ? value.toString() : null);
117   }
118
119   //---------------------------------------------------------------------------
120   public SsmlCellFormat getFormat()
121   {
122      if (null == mCellFormat)
123      {
124         // Check if it has been added via setAttribute()...
125         if (hasAttribute(SsmlXML.STYLE_IDX_ATT))
126         {
127            int cellFormatId = Integer.parseInt(getAttributeValue(SsmlXML.STYLE_IDX_ATT));
128            mCellFormat = getParentDoc().getStylesPart().getCellFormats().get(cellFormatId);
129         }
130         else
131         {
132            setFormat(new SsmlCellFormat(getParentDoc()));
133         }
134      }
135
136      return mCellFormat;
137   }
138
139   //---------------------------------------------------------------------------
140   public SsmlCell setFormat(SsmlCellFormat inValue)
141   {
142      mCellFormat = inValue;
143
144      if (inValue != null)
145      {
146         setAttribute(SsmlXML.STYLE_IDX_ATT, inValue.getIndex());
147      }
148      else
149      {
150         removeAttribute(SsmlXML.STYLE_IDX_ATT.getLocalName());
151      }
152
153      return this;
154   }
155
156   //---------------------------------------------------------------------------
157   public SsmlCell setRef(CellRef inValue)
158   {
159      if (inValue != null)
160      {
161         setAttribute(SsmlXML.REF_ATT, inValue);
162      }
163      else
164      {
165         removeAttribute(SsmlXML.REF_ATT.getLocalName());
166      }
167
168      return this;
169   }
170
171   //---------------------------------------------------------------------------
172   public CellRef getRef()
173   {
174      CellRef value = null;
175
176      String stringValue = getAttributeValue(SsmlXML.REF_ATT);
177      if (StringUtil.isSet(stringValue))
178      {
179         value = new CellRef(stringValue);
180      }
181
182      return value;
183   }
184
185   //---------------------------------------------------------------------------
186   public SsmlCell mergeDown(int inValue)
187   {
188      CellRef cellRef = getRef();
189      if (null == cellRef)
190      {
191         throw new RuntimeException("A CellRef must be specified for the cell before it can be merged!");
192      }
193
194      int endColIndex = cellRef.getColIndex();
195
196      if (mMergedCellRange != null)
197      {
198         mParentWorksheet.removeMergeCell(mMergedCellRange);
199         endColIndex = mMergedCellRange.getEndCell().getColIndex();
200      }
201
202      CellRef refForEndingCell = new CellRef().setColIndex(endColIndex).setRowIndex(cellRef.getRowIndex() + inValue);
203
204      mMergedCellRange = new CellRange(cellRef, refForEndingCell);
205      mParentWorksheet.mergeCells(mMergedCellRange);
206
207      // Ensure that the rest of the cells being merge have been created
208      ensureMergedCellsExist(mMergedCellRange);
209
210      return this;
211   }
212
213   //---------------------------------------------------------------------------
214   public SsmlCell mergeRight(int inValue)
215   {
216      CellRef cellRef = getRef();
217      if (null == cellRef)
218      {
219         throw new RuntimeException("A CellRef must be specified for the cell before it can be merged!");
220      }
221
222      int endRowIndex = cellRef.getRowIndex();
223
224      if (mMergedCellRange != null)
225      {
226         mParentWorksheet.removeMergeCell(mMergedCellRange);
227         endRowIndex = mMergedCellRange.getEndCell().getRowIndex();
228      }
229
230      CellRef refForEndingCell = new CellRef().setColIndex(cellRef.getColIndex() + inValue).setRowIndex(endRowIndex);
231
232      mMergedCellRange = new CellRange(cellRef, refForEndingCell);
233      mParentWorksheet.mergeCells(mMergedCellRange);
234
235      // Ensure that the rest of the cells being merge have been created
236      ensureMergedCellsExist(mMergedCellRange);
237
238      return this;
239   }
240
241   //---------------------------------------------------------------------------
242   public void addComment(XlsxComment inComment)
243   {
244      XlsxComment comment = inComment.clone().setCellRef(getRef());
245      mParentWorksheet.getCommentsPart().addComment(comment);
246   }
247
248   //---------------------------------------------------------------------------
249   @Override
250   public String getContent()
251   {
252      return mStringValue;
253   }
254
255   //---------------------------------------------------------------------------
256   @Override
257   public SsmlCell setContent(CharSequence inContent)
258   {
259      setValue(inContent);
260
261      return this;
262   }
263
264   //---------------------------------------------------------------------------
265   @Override
266   public SsmlCell addContent(CharSequence inContent)
267   {
268      return setValue((mStringValue != null ? mStringValue : "") + inContent.toString());
269   }
270
271   //---------------------------------------------------------------------------
272   public SsmlCellType getCellType()
273   {
274      String attrValueString = getAttributeValue(SsmlXML.CELL_DATA_TYPE_ATT);
275
276      return StringUtil.isSet(attrValueString) ? SsmlCellType.valueOf(attrValueString) : null;
277   }
278/*
279   //---------------------------------------------------------------------------
280   public SsmlCell setValue(String inValue)
281   {
282      setAttribute(SsmlXML.CELL_DATA_TYPE_ATT, DataType.inlineStr);
283
284      XMLTag inlineStrTag = new XMLTag(SsmlXML.INLINE_STRING);
285      addSubtag(inlineStrTag);
286
287      XMLTag textTag = new XMLTag(SsmlXML.TEXT);
288      textTag.setContent(inValue);
289      inlineStrTag.addSubtag(textTag);
290
291      return this;
292   }
293*/
294
295   //---------------------------------------------------------------------------
296   public SsmlCell setValue(Object inValue)
297   {
298      if (inValue != null)
299      {
300         if (inValue instanceof Integer)
301         {
302            setValue(((Integer)inValue).intValue());
303         }
304         else if (inValue instanceof Long)
305         {
306            setValue(((Long)inValue).longValue());
307         }
308         else if (inValue instanceof Float)
309         {
310            setValue(((Float)inValue).floatValue());
311         }
312         else if (inValue instanceof Double)
313         {
314            setValue(((Double)inValue).doubleValue());
315         }
316         else if (inValue instanceof Date)
317         {
318            // Store dates w/o a type and with a number value that is the number of days after Jan 1, 1900
319            LocalDate inputDate = ((Date)inValue).toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
320
321            long days = ChronoUnit.DAYS.between(REFERENCE_DATE, inputDate);
322
323            // For some obscure reason, Excel files built on a mac use 1904 as the reference date...
324            if (getParentDoc().getWorkbook().getProperties().get1904BasedDates())
325            {
326               days -= (365 * 4);
327            }
328            else
329            {
330               // Not exactly sure why this is necessary but it is
331               days += 2;
332            }
333            
334            getValueTag().setContent(days + "");
335
336            // Remember the string value
337            mStringValue = DateUtil.getISO_8601_Date((Date)inValue);
338
339            // Make sure that the cell is formatted as a date
340            if (null == mCellFormat)
341            {
342               //<xf numFmtId="14" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>
343               setFormat(getParentDoc().getStylesPart().getDefaultDateCellFormat());
344            }
345         }
346         else
347         {
348            setValue(inValue.toString());
349         }
350      }
351
352      mValue = inValue;
353
354      return this;
355   }
356
357   //---------------------------------------------------------------------------
358   public SsmlCell setValue(SsmlTextRun inValue)
359   {
360      setAttribute(SsmlXML.CELL_DATA_TYPE_ATT, SsmlCellType.s);
361
362      int index = getParentDoc().getSharedStringsPart().defineString(inValue);
363
364      getValueTag().setContent(index + "");
365
366      // Remember the string value
367      mStringValue = inValue.toXML();
368      mValue = mStringValue;
369
370      return this;
371   }
372
373   //---------------------------------------------------------------------------
374   public SsmlCell setTextRuns(List<SsmlTextRun> inValue)
375   {
376      setAttribute(SsmlXML.CELL_DATA_TYPE_ATT, SsmlCellType.s);
377
378      int index = getParentDoc().getSharedStringsPart().defineString(inValue);
379
380      getValueTag().setContent(index + "");
381
382      // Remember the string value
383      StringBuilder xml = new StringBuilder();
384      for (SsmlTextRun run : inValue)
385      {
386         xml.append(run.toXML());
387      }
388
389      mStringValue = xml.toString();
390
391      return this;
392   }
393
394   //---------------------------------------------------------------------------
395   public SsmlCell setValue(String inValue)
396   {
397      if (inValue != null)
398      {
399         setAttribute(SsmlXML.CELL_DATA_TYPE_ATT, SsmlCellType.s);
400
401         int index = getParentDoc().getSharedStringsPart().defineString(inValue);
402
403         getValueTag().setContent(index + "");
404
405         // Remember the string value
406         mStringValue = inValue;
407         mValue = inValue;
408      }
409      else
410      {
411         removeAttribute(SsmlXML.CELL_DATA_TYPE_ATT);
412         if (mValueTag != null)
413         {
414            removeSubtag(mValueTag);
415            mValueTag = null;
416            mValue = null;
417            mStringValue = null;
418         }
419      }
420
421      return this;
422   }
423
424   //---------------------------------------------------------------------------
425   public SsmlCell setFormula(String inValue)
426   {
427      // Clear any value
428      removeAttribute(SsmlXML.CELL_DATA_TYPE_ATT);
429      if (mValueTag != null)
430      {
431         removeSubtag(mValueTag);
432         mValueTag = null;
433         mValue = null;
434         mStringValue = null;
435      }
436
437      // Set the formula
438      getFormulaTag().setContent(inValue);
439
440      return this;
441   }
442
443   //---------------------------------------------------------------------------
444   public SsmlCell setValue(int inValue)
445   {
446      return setPrimitiveNumberValue(inValue + "");
447   }
448
449   //---------------------------------------------------------------------------
450   public SsmlCell setValue(long inValue)
451   {
452      return setPrimitiveNumberValue(inValue + "");
453   }
454
455   //---------------------------------------------------------------------------
456   public SsmlCell setValue(float inValue)
457   {
458      return setPrimitiveNumberValue(inValue + "");
459   }
460
461   //---------------------------------------------------------------------------
462   public SsmlCell setValue(double inValue)
463   {
464      return setPrimitiveNumberValue(inValue + "");
465   }
466
467
468   //---------------------------------------------------------------------------
469   public Object getValue()
470   {
471      if (null == mValue)
472      {
473         XMLTag valueTag = getValueTag();
474
475         Object value = null;
476
477         if (StringUtil.isSet(valueTag.getContent()))
478         {
479            SsmlCellType cellType = getCellType();
480            if (null == cellType)
481            {
482               cellType = SsmlCellType.n;
483            }
484
485
486            switch (cellType)
487            {
488               case s: // shared string
489                  value = XMLUtil.unescapeContent(getParentDoc().getSharedStringsPart().getString(Integer.parseInt(valueTag.getContent())));
490                  break;
491               case str: // string stored in the value tag
492                  value = valueTag.getUnescapedContent();
493                  break;
494               case n: // number
495                  try
496                  {
497                     Double doubleValue = Double.parseDouble(valueTag.getContent());
498                     if (doubleValue.intValue() == doubleValue)
499                     {
500                        value = doubleValue.intValue();
501                     }
502                     else
503                     {
504                        value = doubleValue;
505                     }
506                  }
507                  catch (Exception e)
508                  {
509                     value = valueTag.getUnescapedContent();
510                  }
511
512                  // Is it a date in disguise?
513                  if (getFormat() != null
514                      && value instanceof Integer
515                      && getFormat().getNumberFormat() != null
516                      && StringUtil.isSet(getFormat().getNumberFormat().getFormatCode())
517                      && getFormat().getNumberFormat().getFormatCode().toLowerCase().indexOf("y") >= 0)
518                  {
519                     // Dates are stored as a number value that is the number of days after Jan 1, 1900
520                     value = getDateFromDays((Integer) value);
521                  }
522
523                  break;
524               case b:
525                  value = BooleanUtil.valueOf(valueTag.getUnescapedContent());
526                  break;
527               default:
528                  System.err.println("Support for cell type " + cellType + " not yet added! Returning null.");
529                  // TODO
530            }
531
532            mValue = value;
533         }
534      }
535
536      return mValue;
537   }
538
539   //---------------------------------------------------------------------------
540   private Date getDateFromDays(int inDays)
541   {
542      Date date = null;
543      try
544      {
545         // For some obscure reason, Excel files built on a mac use 1904 as the reference date...
546         if (getParentDoc().getWorkbook().getProperties().get1904BasedDates())
547         {
548            inDays += (365 * 4);
549         }
550         else
551         {
552            // Not exactly sure why this is necessary but it is
553            inDays -= 2;
554         }
555
556         LocalDate localDate = REFERENCE_DATE.plus(inDays, ChronoUnit.DAYS);
557
558         date = Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
559      }
560      catch (Exception e)
561      {
562         // Igore
563      }
564
565      return date;
566   }
567
568   //---------------------------------------------------------------------------
569   private XMLTag getValueTag()
570   {
571      if (null == mValueTag)
572      {
573         // Check if it has been added via addSubtag()...
574         mValueTag = getOptionalSubtagByName(SsmlXML.VALUE);
575         if (null == mValueTag)
576         {
577            mValueTag = new XMLTag(SsmlXML.VALUE);
578            addSubtag(mValueTag);
579         }
580      }
581
582      return mValueTag;
583   }
584
585   //---------------------------------------------------------------------------
586   private XMLTag getFormulaTag()
587   {
588      if (null == mFormulaTag)
589      {
590         // Check if it has been added via addSubtag()...
591         mFormulaTag = getOptionalSubtagByName(SsmlXML.CELL_FORMULA);
592         if (null == mFormulaTag)
593         {
594            mFormulaTag = new XMLTag(SsmlXML.CELL_FORMULA);
595            addSubtag(mFormulaTag);
596         }
597      }
598
599      return mFormulaTag;
600   }
601
602   //---------------------------------------------------------------------------
603   private SsmlCell setPrimitiveNumberValue(String inStringValue)
604   {
605      setAttribute(SsmlXML.CELL_DATA_TYPE_ATT, SsmlCellType.n);
606
607      getValueTag().setContent(inStringValue);
608
609      // Remember the string value
610      mStringValue = inStringValue;
611
612      return this;
613   }
614
615   //---------------------------------------------------------------------------
616   private void ensureMergedCellsExist(CellRange inCellRange)
617   {
618      SsmlSheetData sheetData = (SsmlSheetData) getParentNode().getParentNode();
619
620      int startingRowIndex = inCellRange.getBeginCell().getRowIndex();
621      int endingRowIndex = inCellRange.getEndCell().getRowIndex();
622
623      for (int i = startingRowIndex; i <= endingRowIndex; i++)
624      {
625         SsmlRow row = sheetData.getRowByRowIndex(i);
626         if (null == row)
627         {
628            row = sheetData.addRow(i);
629         }
630
631         for (int colIndx = inCellRange.getBeginCell().getColIndex(); colIndx <= inCellRange.getEndCell().getColIndex(); colIndx++)
632         {
633            SsmlCell cell = row.getCellByColIndex(colIndx);
634            if (null == cell)
635            {
636               row.addCellByColIndex(colIndx);
637            }
638         }
639      }
640   }
641
642}