You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SharedValueManager.java 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. /* ====================================================================
  2. Licensed to the Apache Software Foundation (ASF) under one or more
  3. contributor license agreements. See the NOTICE file distributed with
  4. this work for additional information regarding copyright ownership.
  5. The ASF licenses this file to You under the Apache License, Version 2.0
  6. (the "License"); you may not use this file except in compliance with
  7. the License. You may obtain a copy of the License at
  8. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. ==================================================================== */
  15. package org.apache.poi.hssf.record.aggregates;
  16. import java.util.ArrayList;
  17. import java.util.HashMap;
  18. import java.util.List;
  19. import java.util.Map;
  20. import org.apache.poi.hssf.record.ArrayRecord;
  21. import org.apache.poi.hssf.record.FormulaRecord;
  22. import org.apache.poi.hssf.record.SharedFormulaRecord;
  23. import org.apache.poi.hssf.record.SharedValueRecordBase;
  24. import org.apache.poi.hssf.record.TableRecord;
  25. import org.apache.poi.ss.formula.ptg.ExpPtg;
  26. import org.apache.poi.hssf.util.CellRangeAddress8Bit;
  27. import org.apache.poi.ss.util.CellReference;
  28. /**
  29. * Manages various auxiliary records while constructing a
  30. * {@link RowRecordsAggregate}:
  31. * <ul>
  32. * <li>{@link SharedFormulaRecord}s</li>
  33. * <li>{@link ArrayRecord}s</li>
  34. * <li>{@link TableRecord}s</li>
  35. * </ul>
  36. *
  37. * @author Josh Micich
  38. * @author Vladimirs Abramovs(Vladimirs.Abramovs at exigenservices.com) - handling of ArrayRecords
  39. */
  40. public final class SharedValueManager {
  41. private static final class SharedFormulaGroup {
  42. private final SharedFormulaRecord _sfr;
  43. private final FormulaRecordAggregate[] _frAggs;
  44. private int _numberOfFormulas;
  45. /**
  46. * Coordinates of the first cell having a formula that uses this shared formula.
  47. * This is often <i>but not always</i> the top left cell in the range covered by
  48. * {@link #_sfr}
  49. */
  50. private final CellReference _firstCell;
  51. public SharedFormulaGroup(SharedFormulaRecord sfr, CellReference firstCell) {
  52. if (!sfr.isInRange(firstCell.getRow(), firstCell.getCol())) {
  53. throw new IllegalArgumentException("First formula cell " + firstCell.formatAsString()
  54. + " is not shared formula range " + sfr.getRange().toString() + ".");
  55. }
  56. _sfr = sfr;
  57. _firstCell = firstCell;
  58. int width = sfr.getLastColumn() - sfr.getFirstColumn() + 1;
  59. int height = sfr.getLastRow() - sfr.getFirstRow() + 1;
  60. _frAggs = new FormulaRecordAggregate[width * height];
  61. _numberOfFormulas = 0;
  62. }
  63. public void add(FormulaRecordAggregate agg) {
  64. if (_numberOfFormulas == 0) {
  65. if (_firstCell.getRow() != agg.getRow() || _firstCell.getCol() != agg.getColumn()) {
  66. throw new IllegalStateException("shared formula coding error: "+_firstCell.getCol()+'/'+_firstCell.getRow()+" != "+agg.getColumn()+'/'+agg.getRow());
  67. }
  68. }
  69. if (_numberOfFormulas >= _frAggs.length) {
  70. throw new RuntimeException("Too many formula records for shared formula group");
  71. }
  72. _frAggs[_numberOfFormulas++] = agg;
  73. }
  74. public void unlinkSharedFormulas() {
  75. for (int i = 0; i < _numberOfFormulas; i++) {
  76. _frAggs[i].unlinkSharedFormula();
  77. }
  78. }
  79. public SharedFormulaRecord getSFR() {
  80. return _sfr;
  81. }
  82. public final String toString() {
  83. StringBuffer sb = new StringBuffer(64);
  84. sb.append(getClass().getName()).append(" [");
  85. sb.append(_sfr.getRange().toString());
  86. sb.append("]");
  87. return sb.toString();
  88. }
  89. }
  90. /**
  91. * @return a new empty {@link SharedValueManager}.
  92. */
  93. public static SharedValueManager createEmpty() {
  94. // Note - must create distinct instances because they are assumed to be mutable.
  95. return new SharedValueManager(
  96. new SharedFormulaRecord[0], new CellReference[0], new ArrayRecord[0], new TableRecord[0]);
  97. }
  98. private final List<ArrayRecord> _arrayRecords;
  99. private final TableRecord[] _tableRecords;
  100. private final Map<SharedFormulaRecord, SharedFormulaGroup> _groupsBySharedFormulaRecord;
  101. /** cached for optimization purposes */
  102. private Map<Integer,SharedFormulaGroup> _groupsCache;
  103. private SharedValueManager(SharedFormulaRecord[] sharedFormulaRecords,
  104. CellReference[] firstCells, ArrayRecord[] arrayRecords, TableRecord[] tableRecords) {
  105. int nShF = sharedFormulaRecords.length;
  106. if (nShF != firstCells.length) {
  107. throw new IllegalArgumentException("array sizes don't match: " + nShF + "!=" + firstCells.length + ".");
  108. }
  109. _arrayRecords = toList(arrayRecords);
  110. _tableRecords = tableRecords;
  111. Map<SharedFormulaRecord, SharedFormulaGroup> m = new HashMap<SharedFormulaRecord, SharedFormulaGroup>(nShF * 3 / 2);
  112. for (int i = 0; i < nShF; i++) {
  113. SharedFormulaRecord sfr = sharedFormulaRecords[i];
  114. m.put(sfr, new SharedFormulaGroup(sfr, firstCells[i]));
  115. }
  116. _groupsBySharedFormulaRecord = m;
  117. }
  118. /**
  119. * @return a modifiable list, independent of the supplied array
  120. */
  121. private static <Z> List<Z> toList(Z[] zz) {
  122. List<Z> result = new ArrayList<Z>(zz.length);
  123. for (int i = 0; i < zz.length; i++) {
  124. result.add(zz[i]);
  125. }
  126. return result;
  127. }
  128. /**
  129. */
  130. public static SharedValueManager create(SharedFormulaRecord[] sharedFormulaRecords,
  131. CellReference[] firstCells, ArrayRecord[] arrayRecords, TableRecord[] tableRecords) {
  132. if (sharedFormulaRecords.length + firstCells.length + arrayRecords.length + tableRecords.length < 1) {
  133. return createEmpty();
  134. }
  135. return new SharedValueManager(sharedFormulaRecords, firstCells, arrayRecords, tableRecords);
  136. }
  137. /**
  138. * @param firstCell as extracted from the {@link ExpPtg} from the cell's formula.
  139. * @return never <code>null</code>
  140. */
  141. public SharedFormulaRecord linkSharedFormulaRecord(CellReference firstCell, FormulaRecordAggregate agg) {
  142. SharedFormulaGroup result = findFormulaGroupForCell(firstCell);
  143. if(null == result) {
  144. throw new RuntimeException("Failed to find a matching shared formula record");
  145. }
  146. result.add(agg);
  147. return result.getSFR();
  148. }
  149. private SharedFormulaGroup findFormulaGroupForCell(final CellReference cellRef) {
  150. if(null == _groupsCache) {
  151. _groupsCache = new HashMap<Integer,SharedFormulaGroup>(_groupsBySharedFormulaRecord.size());
  152. for(SharedFormulaGroup group: _groupsBySharedFormulaRecord.values()) {
  153. _groupsCache.put(getKeyForCache(group._firstCell),group);
  154. }
  155. }
  156. SharedFormulaGroup sfg = _groupsCache.get(getKeyForCache(cellRef));
  157. return sfg;
  158. }
  159. private Integer getKeyForCache(final CellReference cellRef) {
  160. // The HSSF has a max of 2^16 rows and 2^8 cols
  161. return new Integer((cellRef.getCol()+1)<<16 | cellRef.getRow());
  162. }
  163. /**
  164. * Gets the {@link SharedValueRecordBase} record if it should be encoded immediately after the
  165. * formula record contained in the specified {@link FormulaRecordAggregate} agg. Note - the
  166. * shared value record always appears after the first formula record in the group. For arrays
  167. * and tables the first formula is always the in the top left cell. However, since shared
  168. * formula groups can be sparse and/or overlap, the first formula may not actually be in the
  169. * top left cell.
  170. *
  171. * @return the SHRFMLA, TABLE or ARRAY record for the formula cell, if it is the first cell of
  172. * a table or array region. <code>null</code> if the formula cell is not shared/array/table,
  173. * or if the specified formula is not the the first in the group.
  174. */
  175. public SharedValueRecordBase getRecordForFirstCell(FormulaRecordAggregate agg) {
  176. CellReference firstCell = agg.getFormulaRecord().getFormula().getExpReference();
  177. // perhaps this could be optimised by consulting the (somewhat unreliable) isShared flag
  178. // and/or distinguishing between tExp and tTbl.
  179. if (firstCell == null) {
  180. // not a shared/array/table formula
  181. return null;
  182. }
  183. int row = firstCell.getRow();
  184. int column = firstCell.getCol();
  185. if (agg.getRow() != row || agg.getColumn() != column) {
  186. // not the first formula cell in the group
  187. return null;
  188. }
  189. if(!_groupsBySharedFormulaRecord.isEmpty()) {
  190. SharedFormulaGroup sfg = findFormulaGroupForCell(firstCell);
  191. if(null != sfg) {
  192. return sfg.getSFR();
  193. }
  194. }
  195. // Since arrays and tables cannot be sparse (all cells in range participate)
  196. // The first cell will be the top left in the range. So we can match the
  197. // ARRAY/TABLE record directly.
  198. for (TableRecord tr : _tableRecords) {
  199. if (tr.isFirstCell(row, column)) {
  200. return tr;
  201. }
  202. }
  203. for (ArrayRecord ar : _arrayRecords) {
  204. if (ar.isFirstCell(row, column)) {
  205. return ar;
  206. }
  207. }
  208. return null;
  209. }
  210. /**
  211. * Converts all {@link FormulaRecord}s handled by <tt>sharedFormulaRecord</tt>
  212. * to plain unshared formulas
  213. */
  214. public void unlink(SharedFormulaRecord sharedFormulaRecord) {
  215. SharedFormulaGroup svg = _groupsBySharedFormulaRecord.remove(sharedFormulaRecord);
  216. if (svg == null) {
  217. throw new IllegalStateException("Failed to find formulas for shared formula");
  218. }
  219. _groupsCache = null; // be sure to reset cached value
  220. svg.unlinkSharedFormulas();
  221. }
  222. /**
  223. * Add specified Array Record.
  224. */
  225. public void addArrayRecord(ArrayRecord ar) {
  226. // could do a check here to make sure none of the ranges overlap
  227. _arrayRecords.add(ar);
  228. }
  229. /**
  230. * Removes the {@link ArrayRecord} for the cell group containing the specified cell.
  231. * The caller should clear (set blank) all cells in the returned range.
  232. * @return the range of the array formula which was just removed. Never <code>null</code>.
  233. */
  234. public CellRangeAddress8Bit removeArrayFormula(int rowIndex, int columnIndex) {
  235. for (ArrayRecord ar : _arrayRecords) {
  236. if (ar.isInRange(rowIndex, columnIndex)) {
  237. _arrayRecords.remove(ar);
  238. return ar.getRange();
  239. }
  240. }
  241. String ref = new CellReference(rowIndex, columnIndex, false, false).formatAsString();
  242. throw new IllegalArgumentException("Specified cell " + ref
  243. + " is not part of an array formula.");
  244. }
  245. /**
  246. * @return the shared ArrayRecord identified by (firstRow, firstColumn). never <code>null</code>.
  247. */
  248. public ArrayRecord getArrayRecord(int firstRow, int firstColumn) {
  249. for(ArrayRecord ar : _arrayRecords) {
  250. if(ar.isFirstCell(firstRow, firstColumn)) {
  251. return ar;
  252. }
  253. }
  254. return null;
  255. }
  256. }