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.

RecordOrderer.java 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  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.model;
  16. import java.util.List;
  17. import org.apache.poi.hssf.record.ArrayRecord;
  18. import org.apache.poi.hssf.record.BOFRecord;
  19. import org.apache.poi.hssf.record.BlankRecord;
  20. import org.apache.poi.hssf.record.BoolErrRecord;
  21. import org.apache.poi.hssf.record.CalcCountRecord;
  22. import org.apache.poi.hssf.record.CalcModeRecord;
  23. import org.apache.poi.hssf.record.ColumnInfoRecord;
  24. import org.apache.poi.hssf.record.DVALRecord;
  25. import org.apache.poi.hssf.record.DateWindow1904Record;
  26. import org.apache.poi.hssf.record.DefaultColWidthRecord;
  27. import org.apache.poi.hssf.record.DefaultRowHeightRecord;
  28. import org.apache.poi.hssf.record.DeltaRecord;
  29. import org.apache.poi.hssf.record.DimensionsRecord;
  30. import org.apache.poi.hssf.record.DrawingRecord;
  31. import org.apache.poi.hssf.record.DrawingSelectionRecord;
  32. import org.apache.poi.hssf.record.EOFRecord;
  33. import org.apache.poi.hssf.record.FeatRecord;
  34. import org.apache.poi.hssf.record.FormulaRecord;
  35. import org.apache.poi.hssf.record.GridsetRecord;
  36. import org.apache.poi.hssf.record.GutsRecord;
  37. import org.apache.poi.hssf.record.HyperlinkRecord;
  38. import org.apache.poi.hssf.record.IndexRecord;
  39. import org.apache.poi.hssf.record.IterationRecord;
  40. import org.apache.poi.hssf.record.LabelRecord;
  41. import org.apache.poi.hssf.record.LabelSSTRecord;
  42. import org.apache.poi.hssf.record.NumberRecord;
  43. import org.apache.poi.hssf.record.ObjRecord;
  44. import org.apache.poi.hssf.record.PaneRecord;
  45. import org.apache.poi.hssf.record.PrecisionRecord;
  46. import org.apache.poi.hssf.record.PrintGridlinesRecord;
  47. import org.apache.poi.hssf.record.PrintHeadersRecord;
  48. import org.apache.poi.hssf.record.RKRecord;
  49. import org.apache.poi.hssf.record.Record;
  50. import org.apache.poi.hssf.record.RecordBase;
  51. import org.apache.poi.hssf.record.RefModeRecord;
  52. import org.apache.poi.hssf.record.RowRecord;
  53. import org.apache.poi.hssf.record.SCLRecord;
  54. import org.apache.poi.hssf.record.SaveRecalcRecord;
  55. import org.apache.poi.hssf.record.SelectionRecord;
  56. import org.apache.poi.hssf.record.SharedFormulaRecord;
  57. import org.apache.poi.hssf.record.TableRecord;
  58. import org.apache.poi.hssf.record.TextObjectRecord;
  59. import org.apache.poi.hssf.record.UncalcedRecord;
  60. import org.apache.poi.hssf.record.UnknownRecord;
  61. import org.apache.poi.hssf.record.WindowOneRecord;
  62. import org.apache.poi.hssf.record.WindowTwoRecord;
  63. import org.apache.poi.hssf.record.aggregates.ColumnInfoRecordsAggregate;
  64. import org.apache.poi.hssf.record.aggregates.ConditionalFormattingTable;
  65. import org.apache.poi.hssf.record.aggregates.DataValidityTable;
  66. import org.apache.poi.hssf.record.aggregates.MergedCellsTable;
  67. import org.apache.poi.hssf.record.aggregates.PageSettingsBlock;
  68. import org.apache.poi.hssf.record.aggregates.WorksheetProtectionBlock;
  69. import org.apache.poi.hssf.record.pivottable.ViewDefinitionRecord;
  70. /**
  71. * Finds correct insert positions for records in workbook streams<p/>
  72. *
  73. * See OOO excelfileformat.pdf sec. 4.2.5 'Record Order in a BIFF8 Workbook Stream'
  74. */
  75. final class RecordOrderer {
  76. // TODO - simplify logic using a generalised record ordering
  77. private RecordOrderer() {
  78. // no instances of this class
  79. }
  80. /**
  81. * Adds the specified new record in the correct place in sheet records list
  82. */
  83. public static void addNewSheetRecord(List<RecordBase> sheetRecords, RecordBase newRecord) {
  84. int index = findSheetInsertPos(sheetRecords, newRecord.getClass());
  85. sheetRecords.add(index, newRecord);
  86. }
  87. private static int findSheetInsertPos(List<RecordBase> records, Class<? extends RecordBase> recClass) {
  88. if (recClass == DataValidityTable.class) {
  89. return findDataValidationTableInsertPos(records);
  90. }
  91. if (recClass == MergedCellsTable.class) {
  92. return findInsertPosForNewMergedRecordTable(records);
  93. }
  94. if (recClass == ConditionalFormattingTable.class) {
  95. return findInsertPosForNewCondFormatTable(records);
  96. }
  97. if (recClass == GutsRecord.class) {
  98. return getGutsRecordInsertPos(records);
  99. }
  100. if (recClass == PageSettingsBlock.class) {
  101. return getPageBreakRecordInsertPos(records);
  102. }
  103. if (recClass == WorksheetProtectionBlock.class) {
  104. return getWorksheetProtectionBlockInsertPos(records);
  105. }
  106. throw new RuntimeException("Unexpected record class (" + recClass.getName() + ")");
  107. }
  108. /**
  109. * Finds the index where the protection block should be inserted
  110. * @param records the records for this sheet
  111. * <pre>
  112. * + BOF
  113. * o INDEX
  114. * o Calculation Settings Block
  115. * o PRINTHEADERS
  116. * o PRINTGRIDLINES
  117. * o GRIDSET
  118. * o GUTS
  119. * o DEFAULTROWHEIGHT
  120. * o SHEETPR
  121. * o Page Settings Block
  122. * o Worksheet Protection Block
  123. * o DEFCOLWIDTH
  124. * oo COLINFO
  125. * o SORT
  126. * + DIMENSION
  127. * </pre>
  128. */
  129. private static int getWorksheetProtectionBlockInsertPos(List<RecordBase> records) {
  130. int i = getDimensionsIndex(records);
  131. while (i > 0) {
  132. i--;
  133. Object rb = records.get(i);
  134. if (!isProtectionSubsequentRecord(rb)) {
  135. return i+1;
  136. }
  137. }
  138. throw new IllegalStateException("did not find insert pos for protection block");
  139. }
  140. /**
  141. * These records may occur between the 'Worksheet Protection Block' and DIMENSION:
  142. * <pre>
  143. * o DEFCOLWIDTH
  144. * oo COLINFO
  145. * o SORT
  146. * </pre>
  147. */
  148. private static boolean isProtectionSubsequentRecord(Object rb) {
  149. if (rb instanceof ColumnInfoRecordsAggregate) {
  150. return true; // oo COLINFO
  151. }
  152. if (rb instanceof Record) {
  153. Record record = (Record) rb;
  154. switch (record.getSid()) {
  155. case DefaultColWidthRecord.sid:
  156. case UnknownRecord.SORT_0090:
  157. return true;
  158. }
  159. }
  160. return false;
  161. }
  162. private static int getPageBreakRecordInsertPos(List<RecordBase> records) {
  163. int dimensionsIndex = getDimensionsIndex(records);
  164. int i = dimensionsIndex-1;
  165. while (i > 0) {
  166. i--;
  167. Object rb = records.get(i);
  168. if (isPageBreakPriorRecord(rb)) {
  169. return i+1;
  170. }
  171. }
  172. throw new RuntimeException("Did not find insert point for GUTS");
  173. }
  174. private static boolean isPageBreakPriorRecord(Object rb) {
  175. if (rb instanceof Record) {
  176. Record record = (Record) rb;
  177. switch (record.getSid()) {
  178. case BOFRecord.sid:
  179. case IndexRecord.sid:
  180. // calc settings block
  181. case UncalcedRecord.sid:
  182. case CalcCountRecord.sid:
  183. case CalcModeRecord.sid:
  184. case PrecisionRecord.sid:
  185. case RefModeRecord.sid:
  186. case DeltaRecord.sid:
  187. case IterationRecord.sid:
  188. case DateWindow1904Record.sid:
  189. case SaveRecalcRecord.sid:
  190. // end calc settings
  191. case PrintHeadersRecord.sid:
  192. case PrintGridlinesRecord.sid:
  193. case GridsetRecord.sid:
  194. case DefaultRowHeightRecord.sid:
  195. case UnknownRecord.SHEETPR_0081:
  196. return true;
  197. // next is the 'Worksheet Protection Block'
  198. }
  199. }
  200. return false;
  201. }
  202. /**
  203. * Find correct position to add new CFHeader record
  204. */
  205. private static int findInsertPosForNewCondFormatTable(List<RecordBase> records) {
  206. for (int i = records.size() - 2; i >= 0; i--) { // -2 to skip EOF record
  207. Object rb = records.get(i);
  208. if (rb instanceof MergedCellsTable) {
  209. return i + 1;
  210. }
  211. if (rb instanceof DataValidityTable) {
  212. continue;
  213. }
  214. Record rec = (Record) rb;
  215. switch (rec.getSid()) {
  216. case WindowTwoRecord.sid:
  217. case SCLRecord.sid:
  218. case PaneRecord.sid:
  219. case SelectionRecord.sid:
  220. case UnknownRecord.STANDARDWIDTH_0099:
  221. // MergedCellsTable usually here
  222. case UnknownRecord.LABELRANGES_015F:
  223. case UnknownRecord.PHONETICPR_00EF:
  224. // ConditionalFormattingTable goes here
  225. return i + 1;
  226. // HyperlinkTable (not aggregated by POI yet)
  227. // DataValidityTable
  228. }
  229. }
  230. throw new RuntimeException("Did not find Window2 record");
  231. }
  232. private static int findInsertPosForNewMergedRecordTable(List<RecordBase> records) {
  233. for (int i = records.size() - 2; i >= 0; i--) { // -2 to skip EOF record
  234. Object rb = records.get(i);
  235. if (!(rb instanceof Record)) {
  236. // DataValidityTable, ConditionalFormattingTable,
  237. // even PageSettingsBlock (which doesn't normally appear after 'View Settings')
  238. continue;
  239. }
  240. Record rec = (Record) rb;
  241. switch (rec.getSid()) {
  242. // 'View Settings' (4 records)
  243. case WindowTwoRecord.sid:
  244. case SCLRecord.sid:
  245. case PaneRecord.sid:
  246. case SelectionRecord.sid:
  247. case UnknownRecord.STANDARDWIDTH_0099:
  248. return i + 1;
  249. }
  250. }
  251. throw new RuntimeException("Did not find Window2 record");
  252. }
  253. /**
  254. * Finds the index where the sheet validations header record should be inserted
  255. * @param records the records for this sheet
  256. *
  257. * + WINDOW2
  258. * o SCL
  259. * o PANE
  260. * oo SELECTION
  261. * o STANDARDWIDTH
  262. * oo MERGEDCELLS
  263. * o LABELRANGES
  264. * o PHONETICPR
  265. * o Conditional Formatting Table
  266. * o Hyperlink Table
  267. * o Data Validity Table
  268. * o SHEETLAYOUT
  269. * o SHEETPROTECTION
  270. * o RANGEPROTECTION
  271. * + EOF
  272. */
  273. private static int findDataValidationTableInsertPos(List<RecordBase> records) {
  274. int i = records.size() - 1;
  275. if (!(records.get(i) instanceof EOFRecord)) {
  276. throw new IllegalStateException("Last sheet record should be EOFRecord");
  277. }
  278. while (i > 0) {
  279. i--;
  280. RecordBase rb = records.get(i);
  281. if (isDVTPriorRecord(rb)) {
  282. Record nextRec = (Record) records.get(i + 1);
  283. if (!isDVTSubsequentRecord(nextRec.getSid())) {
  284. throw new IllegalStateException("Unexpected (" + nextRec.getClass().getName()
  285. + ") found after (" + rb.getClass().getName() + ")");
  286. }
  287. return i+1;
  288. }
  289. Record rec = (Record) rb;
  290. if (!isDVTSubsequentRecord(rec.getSid())) {
  291. throw new IllegalStateException("Unexpected (" + rec.getClass().getName()
  292. + ") while looking for DV Table insert pos");
  293. }
  294. }
  295. return 0;
  296. }
  297. private static boolean isDVTPriorRecord(RecordBase rb) {
  298. if (rb instanceof MergedCellsTable || rb instanceof ConditionalFormattingTable) {
  299. return true;
  300. }
  301. short sid = ((Record)rb).getSid();
  302. switch(sid) {
  303. case WindowTwoRecord.sid:
  304. case UnknownRecord.SCL_00A0:
  305. case PaneRecord.sid:
  306. case SelectionRecord.sid:
  307. case UnknownRecord.STANDARDWIDTH_0099:
  308. // MergedCellsTable
  309. case UnknownRecord.LABELRANGES_015F:
  310. case UnknownRecord.PHONETICPR_00EF:
  311. // ConditionalFormattingTable
  312. case HyperlinkRecord.sid:
  313. case UnknownRecord.QUICKTIP_0800:
  314. // name of a VBA module
  315. case UnknownRecord.CODENAME_1BA:
  316. return true;
  317. }
  318. return false;
  319. }
  320. private static boolean isDVTSubsequentRecord(short sid) {
  321. switch(sid) {
  322. case UnknownRecord.SHEETEXT_0862:
  323. case UnknownRecord.SHEETPROTECTION_0867:
  324. case UnknownRecord.PLV_MAC:
  325. case FeatRecord.sid:
  326. case EOFRecord.sid:
  327. return true;
  328. }
  329. return false;
  330. }
  331. /**
  332. * DIMENSIONS record is always present
  333. */
  334. private static int getDimensionsIndex(List<RecordBase> records) {
  335. int nRecs = records.size();
  336. for(int i=0; i<nRecs; i++) {
  337. if(records.get(i) instanceof DimensionsRecord) {
  338. return i;
  339. }
  340. }
  341. // worksheet stream is seriously broken
  342. throw new RuntimeException("DimensionsRecord not found");
  343. }
  344. private static int getGutsRecordInsertPos(List<RecordBase> records) {
  345. int dimensionsIndex = getDimensionsIndex(records);
  346. int i = dimensionsIndex-1;
  347. while (i > 0) {
  348. i--;
  349. RecordBase rb = records.get(i);
  350. if (isGutsPriorRecord(rb)) {
  351. return i+1;
  352. }
  353. }
  354. throw new RuntimeException("Did not find insert point for GUTS");
  355. }
  356. private static boolean isGutsPriorRecord(RecordBase rb) {
  357. if (rb instanceof Record) {
  358. Record record = (Record) rb;
  359. switch (record.getSid()) {
  360. case BOFRecord.sid:
  361. case IndexRecord.sid:
  362. // calc settings block
  363. case UncalcedRecord.sid:
  364. case CalcCountRecord.sid:
  365. case CalcModeRecord.sid:
  366. case PrecisionRecord.sid:
  367. case RefModeRecord.sid:
  368. case DeltaRecord.sid:
  369. case IterationRecord.sid:
  370. case DateWindow1904Record.sid:
  371. case SaveRecalcRecord.sid:
  372. // end calc settings
  373. case PrintHeadersRecord.sid:
  374. case PrintGridlinesRecord.sid:
  375. case GridsetRecord.sid:
  376. return true;
  377. // DefaultRowHeightRecord.sid is next
  378. }
  379. }
  380. return false;
  381. }
  382. /**
  383. * @return <code>true</code> if the specified record ID terminates a sequence of Row block records
  384. * It is assumed that at least one row or cell value record has been found prior to the current
  385. * record
  386. */
  387. public static boolean isEndOfRowBlock(int sid) {
  388. switch(sid) {
  389. case ViewDefinitionRecord.sid:
  390. // should have been prefixed with DrawingRecord (0x00EC), but bug 46280 seems to allow this
  391. case DrawingRecord.sid:
  392. case DrawingSelectionRecord.sid:
  393. case ObjRecord.sid:
  394. case TextObjectRecord.sid:
  395. case ColumnInfoRecord.sid: // See Bugzilla 53984
  396. case GutsRecord.sid: // see Bugzilla 50426
  397. case WindowOneRecord.sid:
  398. // should really be part of workbook stream, but some apps seem to put this before WINDOW2
  399. case WindowTwoRecord.sid:
  400. return true;
  401. case DVALRecord.sid:
  402. return true;
  403. case EOFRecord.sid:
  404. // WINDOW2 should always be present, so shouldn't have got this far
  405. throw new RuntimeException("Found EOFRecord before WindowTwoRecord was encountered");
  406. }
  407. return PageSettingsBlock.isComponentRecord(sid);
  408. }
  409. /**
  410. * @return <code>true</code> if the specified record id normally appears in the row blocks section
  411. * of the sheet records
  412. */
  413. public static boolean isRowBlockRecord(int sid) {
  414. switch (sid) {
  415. case RowRecord.sid:
  416. case BlankRecord.sid:
  417. case BoolErrRecord.sid:
  418. case FormulaRecord.sid:
  419. case LabelRecord.sid:
  420. case LabelSSTRecord.sid:
  421. case NumberRecord.sid:
  422. case RKRecord.sid:
  423. case ArrayRecord.sid:
  424. case SharedFormulaRecord.sid:
  425. case TableRecord.sid:
  426. return true;
  427. }
  428. return false;
  429. }
  430. }