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}