| /* ==================================================================== |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You under the Apache License, Version 2.0 |
| (the "License"); you may not use this file except in compliance with |
| the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| ==================================================================== */ |
| |
| package org.apache.poi.hssf.model; |
| |
| import java.util.List; |
| |
| import org.apache.poi.hssf.record.ArrayRecord; |
| import org.apache.poi.hssf.record.BOFRecord; |
| import org.apache.poi.hssf.record.BlankRecord; |
| import org.apache.poi.hssf.record.BoolErrRecord; |
| import org.apache.poi.hssf.record.CalcCountRecord; |
| import org.apache.poi.hssf.record.CalcModeRecord; |
| import org.apache.poi.hssf.record.DVALRecord; |
| import org.apache.poi.hssf.record.DateWindow1904Record; |
| import org.apache.poi.hssf.record.DefaultRowHeightRecord; |
| import org.apache.poi.hssf.record.DeltaRecord; |
| import org.apache.poi.hssf.record.DimensionsRecord; |
| import org.apache.poi.hssf.record.DrawingRecord; |
| import org.apache.poi.hssf.record.DrawingSelectionRecord; |
| import org.apache.poi.hssf.record.EOFRecord; |
| import org.apache.poi.hssf.record.FormulaRecord; |
| import org.apache.poi.hssf.record.GridsetRecord; |
| import org.apache.poi.hssf.record.GutsRecord; |
| import org.apache.poi.hssf.record.HyperlinkRecord; |
| import org.apache.poi.hssf.record.IndexRecord; |
| import org.apache.poi.hssf.record.IterationRecord; |
| import org.apache.poi.hssf.record.LabelRecord; |
| import org.apache.poi.hssf.record.LabelSSTRecord; |
| import org.apache.poi.hssf.record.NumberRecord; |
| import org.apache.poi.hssf.record.ObjRecord; |
| import org.apache.poi.hssf.record.PaneRecord; |
| import org.apache.poi.hssf.record.PrecisionRecord; |
| import org.apache.poi.hssf.record.PrintGridlinesRecord; |
| import org.apache.poi.hssf.record.PrintHeadersRecord; |
| import org.apache.poi.hssf.record.RKRecord; |
| import org.apache.poi.hssf.record.Record; |
| import org.apache.poi.hssf.record.RecordBase; |
| import org.apache.poi.hssf.record.RefModeRecord; |
| import org.apache.poi.hssf.record.RowRecord; |
| import org.apache.poi.hssf.record.SCLRecord; |
| import org.apache.poi.hssf.record.SaveRecalcRecord; |
| import org.apache.poi.hssf.record.SelectionRecord; |
| import org.apache.poi.hssf.record.SharedFormulaRecord; |
| import org.apache.poi.hssf.record.TableRecord; |
| import org.apache.poi.hssf.record.TextObjectRecord; |
| import org.apache.poi.hssf.record.UncalcedRecord; |
| import org.apache.poi.hssf.record.UnknownRecord; |
| import org.apache.poi.hssf.record.WindowOneRecord; |
| import org.apache.poi.hssf.record.WindowTwoRecord; |
| import org.apache.poi.hssf.record.aggregates.ConditionalFormattingTable; |
| import org.apache.poi.hssf.record.aggregates.DataValidityTable; |
| import org.apache.poi.hssf.record.aggregates.MergedCellsTable; |
| import org.apache.poi.hssf.record.aggregates.PageSettingsBlock; |
| |
| /** |
| * Finds correct insert positions for records in workbook streams<p/> |
| * |
| * See OOO excelfileformat.pdf sec. 4.2.5 'Record Order in a BIFF8 Workbook Stream' |
| * |
| * @author Josh Micich |
| */ |
| final class RecordOrderer { |
| |
| // TODO - simplify logic using a generalised record ordering |
| |
| private RecordOrderer() { |
| // no instances of this class |
| } |
| /** |
| * Adds the specified new record in the correct place in sheet records list |
| * |
| */ |
| public static void addNewSheetRecord(List sheetRecords, RecordBase newRecord) { |
| int index = findSheetInsertPos(sheetRecords, newRecord.getClass()); |
| sheetRecords.add(index, newRecord); |
| } |
| |
| private static int findSheetInsertPos(List records, Class recClass) { |
| if (recClass == DataValidityTable.class) { |
| return findDataValidationTableInsertPos(records); |
| } |
| if (recClass == MergedCellsTable.class) { |
| return findInsertPosForNewMergedRecordTable(records); |
| } |
| if (recClass == ConditionalFormattingTable.class) { |
| return findInsertPosForNewCondFormatTable(records); |
| } |
| if (recClass == GutsRecord.class) { |
| return getGutsRecordInsertPos(records); |
| } |
| if (recClass == PageSettingsBlock.class) { |
| return getPageBreakRecordInsertPos(records); |
| } |
| throw new RuntimeException("Unexpected record class (" + recClass.getName() + ")"); |
| } |
| |
| private static int getPageBreakRecordInsertPos(List records) { |
| int dimensionsIndex = getDimensionsIndex(records); |
| int i = dimensionsIndex-1; |
| while (i > 0) { |
| i--; |
| Object rb = records.get(i); |
| if (isPageBreakPriorRecord(rb)) { |
| return i+1; |
| } |
| } |
| throw new RuntimeException("Did not find insert point for GUTS"); |
| } |
| private static boolean isPageBreakPriorRecord(Object rb) { |
| if (rb instanceof Record) { |
| Record record = (Record) rb; |
| switch (record.getSid()) { |
| case BOFRecord.sid: |
| case IndexRecord.sid: |
| // calc settings block |
| case UncalcedRecord.sid: |
| case CalcCountRecord.sid: |
| case CalcModeRecord.sid: |
| case PrecisionRecord.sid: |
| case RefModeRecord.sid: |
| case DeltaRecord.sid: |
| case IterationRecord.sid: |
| case DateWindow1904Record.sid: |
| case SaveRecalcRecord.sid: |
| // end calc settings |
| case PrintHeadersRecord.sid: |
| case PrintGridlinesRecord.sid: |
| case GridsetRecord.sid: |
| case DefaultRowHeightRecord.sid: |
| case UnknownRecord.SHEETPR_0081: |
| return true; |
| // next is the 'Worksheet Protection Block' |
| } |
| } |
| return false; |
| } |
| /** |
| * Find correct position to add new CFHeader record |
| */ |
| private static int findInsertPosForNewCondFormatTable(List records) { |
| |
| for (int i = records.size() - 2; i >= 0; i--) { // -2 to skip EOF record |
| Object rb = records.get(i); |
| if (rb instanceof MergedCellsTable) { |
| return i + 1; |
| } |
| Record rec = (Record) rb; |
| switch (rec.getSid()) { |
| case WindowTwoRecord.sid: |
| case SCLRecord.sid: |
| case PaneRecord.sid: |
| case SelectionRecord.sid: |
| case UnknownRecord.STANDARDWIDTH_0099: |
| // MergedCellsTable usually here |
| case UnknownRecord.LABELRANGES_015F: |
| case UnknownRecord.PHONETICPR_00EF: |
| return i + 1; |
| } |
| } |
| throw new RuntimeException("Did not find Window2 record"); |
| } |
| |
| private static int findInsertPosForNewMergedRecordTable(List records) { |
| for (int i = records.size() - 2; i >= 0; i--) { // -2 to skip EOF record |
| Object rb = records.get(i); |
| if (!(rb instanceof Record)) { |
| // DataValidityTable, ConditionalFormattingTable, |
| // even PageSettingsBlock (which doesn't normally appear after 'View Settings') |
| continue; |
| } |
| Record rec = (Record) rb; |
| switch (rec.getSid()) { |
| // 'View Settings' (4 records) |
| case WindowTwoRecord.sid: |
| case SCLRecord.sid: |
| case PaneRecord.sid: |
| case SelectionRecord.sid: |
| |
| case UnknownRecord.STANDARDWIDTH_0099: |
| return i + 1; |
| } |
| } |
| throw new RuntimeException("Did not find Window2 record"); |
| } |
| |
| |
| /** |
| * Finds the index where the sheet validations header record should be inserted |
| * @param records the records for this sheet |
| * |
| * + WINDOW2 |
| * o SCL |
| * o PANE |
| * oo SELECTION |
| * o STANDARDWIDTH |
| * oo MERGEDCELLS |
| * o LABELRANGES |
| * o PHONETICPR |
| * o Conditional Formatting Table |
| * o Hyperlink Table |
| * o Data Validity Table |
| * o SHEETLAYOUT |
| * o SHEETPROTECTION |
| * o RANGEPROTECTION |
| * + EOF |
| */ |
| private static int findDataValidationTableInsertPos(List records) { |
| int i = records.size() - 1; |
| if (!(records.get(i) instanceof EOFRecord)) { |
| throw new IllegalStateException("Last sheet record should be EOFRecord"); |
| } |
| while (i > 0) { |
| i--; |
| Object rb = records.get(i); |
| if (isDVTPriorRecord(rb)) { |
| Record nextRec = (Record) records.get(i + 1); |
| if (!isDVTSubsequentRecord(nextRec.getSid())) { |
| throw new IllegalStateException("Unexpected (" + nextRec.getClass().getName() |
| + ") found after (" + rb.getClass().getName() + ")"); |
| } |
| return i+1; |
| } |
| Record rec = (Record) rb; |
| if (!isDVTSubsequentRecord(rec.getSid())) { |
| throw new IllegalStateException("Unexpected (" + rec.getClass().getName() |
| + ") while looking for DV Table insert pos"); |
| } |
| } |
| return 0; |
| } |
| |
| |
| private static boolean isDVTPriorRecord(Object rb) { |
| if (rb instanceof MergedCellsTable || rb instanceof ConditionalFormattingTable) { |
| return true; |
| } |
| short sid = ((Record)rb).getSid(); |
| switch(sid) { |
| case WindowTwoRecord.sid: |
| case UnknownRecord.SCL_00A0: |
| case PaneRecord.sid: |
| case SelectionRecord.sid: |
| case UnknownRecord.STANDARDWIDTH_0099: |
| // MergedCellsTable |
| case UnknownRecord.LABELRANGES_015F: |
| case UnknownRecord.PHONETICPR_00EF: |
| // ConditionalFormattingTable |
| case HyperlinkRecord.sid: |
| case UnknownRecord.QUICKTIP_0800: |
| return true; |
| } |
| return false; |
| } |
| |
| private static boolean isDVTSubsequentRecord(short sid) { |
| switch(sid) { |
| case UnknownRecord.SHEETEXT_0862: |
| case UnknownRecord.SHEETPROTECTION_0867: |
| case UnknownRecord.RANGEPROTECTION_0868: |
| case EOFRecord.sid: |
| return true; |
| } |
| return false; |
| } |
| /** |
| * DIMENSIONS record is always present |
| */ |
| private static int getDimensionsIndex(List records) { |
| int nRecs = records.size(); |
| for(int i=0; i<nRecs; i++) { |
| if(records.get(i) instanceof DimensionsRecord) { |
| return i; |
| } |
| } |
| // worksheet stream is seriously broken |
| throw new RuntimeException("DimensionsRecord not found"); |
| } |
| |
| private static int getGutsRecordInsertPos(List records) { |
| int dimensionsIndex = getDimensionsIndex(records); |
| int i = dimensionsIndex-1; |
| while (i > 0) { |
| i--; |
| Object rb = records.get(i); |
| if (isGutsPriorRecord(rb)) { |
| return i+1; |
| } |
| } |
| throw new RuntimeException("Did not find insert point for GUTS"); |
| } |
| |
| private static boolean isGutsPriorRecord(Object rb) { |
| if (rb instanceof Record) { |
| Record record = (Record) rb; |
| switch (record.getSid()) { |
| case BOFRecord.sid: |
| case IndexRecord.sid: |
| // calc settings block |
| case UncalcedRecord.sid: |
| case CalcCountRecord.sid: |
| case CalcModeRecord.sid: |
| case PrecisionRecord.sid: |
| case RefModeRecord.sid: |
| case DeltaRecord.sid: |
| case IterationRecord.sid: |
| case DateWindow1904Record.sid: |
| case SaveRecalcRecord.sid: |
| // end calc settings |
| case PrintHeadersRecord.sid: |
| case PrintGridlinesRecord.sid: |
| case GridsetRecord.sid: |
| return true; |
| // DefaultRowHeightRecord.sid is next |
| } |
| } |
| return false; |
| } |
| /** |
| * @return <code>true</code> if the specified record ID terminates a sequence of Row block records |
| * It is assumed that at least one row or cell value record has been found prior to the current |
| * record |
| */ |
| public static boolean isEndOfRowBlock(int sid) { |
| switch(sid) { |
| case DrawingRecord.sid: |
| case DrawingSelectionRecord.sid: |
| case ObjRecord.sid: |
| case TextObjectRecord.sid: |
| |
| case WindowOneRecord.sid: |
| // should really be part of workbook stream, but some apps seem to put this before WINDOW2 |
| case WindowTwoRecord.sid: |
| return true; |
| |
| case DVALRecord.sid: |
| return true; |
| case EOFRecord.sid: |
| // WINDOW2 should always be present, so shouldn't have got this far |
| throw new RuntimeException("Found EOFRecord before WindowTwoRecord was encountered"); |
| } |
| return PageSettingsBlock.isComponentRecord(sid); |
| } |
| |
| /** |
| * @return <code>true</code> if the specified record id normally appears in the row blocks section |
| * of the sheet records |
| */ |
| public static boolean isRowBlockRecord(int sid) { |
| switch (sid) { |
| case RowRecord.sid: |
| |
| case BlankRecord.sid: |
| case BoolErrRecord.sid: |
| case FormulaRecord.sid: |
| case LabelRecord.sid: |
| case LabelSSTRecord.sid: |
| case NumberRecord.sid: |
| case RKRecord.sid: |
| |
| case ArrayRecord.sid: |
| case SharedFormulaRecord.sid: |
| case TableRecord.sid: |
| return true; |
| |
| } |
| return false; |
| } |
| } |