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

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