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
9 http://www.apache.org/licenses/LICENSE-2.0
10
11 Unless required by applicable law or agreed to in writing, software
12 distributed under the License is distributed on an "AS IS" BASIS,
13 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 See the License for the specific language governing permissions and
15 limitations under the License.
16 ==================================================================== */
17
18 package org.apache.poi.hssf.model;
19
20 import org.apache.poi.hssf.record;
21 import org.apache.poi.hssf.record.aggregates.ColumnInfoRecordsAggregate;
22 import org.apache.poi.hssf.record.aggregates.FormulaRecordAggregate;
23 import org.apache.poi.hssf.record.aggregates.RowRecordsAggregate;
24 import org.apache.poi.hssf.record.aggregates.ValueRecordsAggregate;
25 import org.apache.poi.hssf.record.aggregates.CFRecordsAggregate;
26 import org.apache.poi.hssf.record.formula.Ptg;
27 import org.apache.poi.hssf.util.PaneInformation;
28
29 import org.apache.poi.util.POILogFactory;
30 import org.apache.poi.util.POILogger;
31
32 import java.util.ArrayList;
33 import java.util.Iterator;
34 import java.util.List; // normally I don't do this, buy we literally mean ALL
35
36 /**
37 * Low level model implementation of a Sheet (one workbook contains many sheets)
38 * This file contains the low level binary records starting at the sheets BOF and
39 * ending with the sheets EOF. Use HSSFSheet for a high level representation.
40 * <P>
41 * The structures of the highlevel API use references to this to perform most of their
42 * operations. Its probably unwise to use these low level structures directly unless you
43 * really know what you're doing. I recommend you read the Microsoft Excel 97 Developer's
44 * Kit (Microsoft Press) and the documentation at http://sc.openoffice.org/excelfileformat.pdf
45 * before even attempting to use this.
46 * <P>
47 * @author Andrew C. Oliver (acoliver at apache dot org)
48 * @author Glen Stampoultzis (glens at apache.org)
49 * @author Shawn Laubach (slaubach at apache dot org) Gridlines, Headers, Footers, PrintSetup, and Setting Default Column Styles
50 * @author Jason Height (jheight at chariot dot net dot au) Clone support. DBCell & Index Record writing support
51 * @author Brian Sanders (kestrel at burdell dot org) Active Cell support
52 * @author Jean-Pierre Paris (jean-pierre.paris at m4x dot org) (Just a little)
53 *
54 * @see org.apache.poi.hssf.model.Workbook
55 * @see org.apache.poi.hssf.usermodel.HSSFSheet
56 * @version 1.0-pre
57 */
58 public final class Sheet implements Model {
59 public static final short LeftMargin = 0;
60 public static final short RightMargin = 1;
61 public static final short TopMargin = 2;
62 public static final short BottomMargin = 3;
63
64 private static POILogger log = POILogFactory.getLogger(Sheet.class);
65
66 protected ArrayList records = null;
67 int preoffset = 0; // offset of the sheet in a new file
68 int loc = 0;
69 protected int dimsloc = -1; // TODO - is it legal for dims record to be missing?
70 protected DimensionsRecord dims;
71 protected DefaultColWidthRecord defaultcolwidth = null;
72 protected DefaultRowHeightRecord defaultrowheight = null;
73 protected GridsetRecord gridset = null;
74 protected PrintSetupRecord printSetup = null;
75 protected HeaderRecord header = null;
76 protected FooterRecord footer = null;
77 protected PrintGridlinesRecord printGridlines = null;
78 protected WindowTwoRecord windowTwo = null;
79 protected MergeCellsRecord merged = null;
80 protected Margin[] margins = null;
81 protected List mergedRecords = new ArrayList();
82 protected int numMergedRegions = 0;
83 protected SelectionRecord selection = null;
84 protected ColumnInfoRecordsAggregate columns = null;
85 protected ValueRecordsAggregate cells = null;
86 protected RowRecordsAggregate rows = null;
87 private Iterator valueRecIterator = null;
88 private Iterator rowRecIterator = null;
89 protected int eofLoc = 0;
90 protected ProtectRecord protect = null;
91 protected PageBreakRecord rowBreaks = null;
92 protected PageBreakRecord colBreaks = null;
93 protected ObjectProtectRecord objprotect = null;
94 protected ScenarioProtectRecord scenprotect = null;
95 protected PasswordRecord password = null;
96 protected List condFormatting = new ArrayList();
97
98 /** Add an UncalcedRecord if not true indicating formulas have not been calculated */
99 protected boolean _isUncalced = false;
100
101 public static final byte PANE_LOWER_RIGHT = (byte)0;
102 public static final byte PANE_UPPER_RIGHT = (byte)1;
103 public static final byte PANE_LOWER_LEFT = (byte)2;
104 public static final byte PANE_UPPER_LEFT = (byte)3;
105
106 /**
107 * Creates new Sheet with no initialization --useless at this point
108 * @see #createSheet(List,int,int)
109 */
110 public Sheet()
111 {
112 }
113
114 /**
115 * read support (offset used as starting point for search) for low level
116 * API. Pass in an array of Record objects, the sheet number (0 based) and
117 * a record offset (should be the location of the sheets BOF record). A Sheet
118 * object is constructed and passed back with all of its initialization set
119 * to the passed in records and references to those records held. This function
120 * is normally called via Workbook.
121 *
122 * @param recs array containing those records in the sheet in sequence (normally obtained from RecordFactory)
123 * @param sheetnum integer specifying the sheet's number (0,1 or 2 in this release)
124 * @param offset of the sheet's BOF record
125 *
126 * @return Sheet object with all values set to those read from the file
127 *
128 * @see org.apache.poi.hssf.model.Workbook
129 * @see org.apache.poi.hssf.record.Record
130 */
131 public static Sheet createSheet(List recs, int sheetnum, int offset)
132 {
133 if (log.check( POILogger.DEBUG ))
134 log.logFormatted(POILogger.DEBUG,
135 "Sheet createSheet (existing file) with %",
136 new Integer(recs.size()));
137 Sheet retval = new Sheet();
138 ArrayList records = new ArrayList(recs.size() / 5);
139 boolean isfirstcell = true;
140 boolean isfirstrow = true;
141 int bofEofNestingLevel = 0;
142
143 for (int k = offset; k < recs.size(); k++)
144 {
145 Record rec = ( Record ) recs.get(k);
146
147 if (rec.getSid() == BOFRecord.sid)
148 {
149 bofEofNestingLevel++;
150 if (log.check( POILogger.DEBUG ))
151 log.log(POILogger.DEBUG, "Hit BOF record. Nesting increased to " + bofEofNestingLevel);
152 }
153 else if (rec.getSid() == EOFRecord.sid)
154 {
155 --bofEofNestingLevel;
156 if (log.check( POILogger.DEBUG ))
157 log.log(POILogger.DEBUG, "Hit EOF record. Nesting decreased to " + bofEofNestingLevel);
158 if (bofEofNestingLevel == 0) {
159 records.add(rec);
160 retval.eofLoc = k;
161 break;
162 }
163 }
164 else if (rec.getSid() == UncalcedRecord.sid) {
165 retval._isUncalced = true;
166 }
167 else if (rec.getSid() == DimensionsRecord.sid)
168 {
169 // Make a columns aggregate if one hasn't ready been created.
170 if (retval.columns == null)
171 {
172 retval.columns = new ColumnInfoRecordsAggregate();
173 records.add(retval.columns);
174 }
175
176 retval.dims = ( DimensionsRecord ) rec;
177 retval.dimsloc = records.size();
178 }
179 else if (rec.getSid() == MergeCellsRecord.sid)
180 {
181 retval.mergedRecords.add(rec);
182 retval.merged = ( MergeCellsRecord ) rec;
183 retval.numMergedRegions += retval.merged.getNumAreas();
184 }
185 else if ( rec.getSid() == CFHeaderRecord.sid )
186 {
187 CFRecordsAggregate cfAgg = CFRecordsAggregate.createCFAggregate(recs, k);
188 retval.condFormatting.add(cfAgg);
189 rec = cfAgg;
190 }
191 else if ( rec.getSid() == CFRuleRecord.sid )
192 {
193 // Skip it since it is processed by CFRecordsAggregate
194 rec = null;
195 }
196 else if (rec.getSid() == ColumnInfoRecord.sid)
197 {
198 ColumnInfoRecord col = (ColumnInfoRecord)rec;
199 if (retval.columns != null)
200 {
201 rec = null; //only add the aggregate once
202 }
203 else
204 {
205 rec = retval.columns = new ColumnInfoRecordsAggregate();
206 }
207 retval.columns.insertColumn(col);
208 }
209 else if (rec.getSid() == DefaultColWidthRecord.sid)
210 {
211 retval.defaultcolwidth = ( DefaultColWidthRecord ) rec;
212 }
213 else if (rec.getSid() == DefaultRowHeightRecord.sid)
214 {
215 retval.defaultrowheight = ( DefaultRowHeightRecord ) rec;
216 }
217 else if ( rec.isValue() && bofEofNestingLevel == 1 )
218 {
219 if ( isfirstcell )
220 {
221 retval.cells = new ValueRecordsAggregate();
222 rec = retval.cells;
223 retval.cells.construct( k, recs );
224 isfirstcell = false;
225 }
226 else
227 {
228 rec = null;
229 }
230 }
231 else if ( rec.getSid() == StringRecord.sid )
232 {
233 rec = null;
234 }
235 else if ( rec.getSid() == RowRecord.sid )
236 {
237 RowRecord row = (RowRecord)rec;
238 if (!isfirstrow) rec = null; //only add the aggregate once
239
240 if ( isfirstrow )
241 {
242 retval.rows = new RowRecordsAggregate();
243 rec = retval.rows;
244 isfirstrow = false;
245 }
246 retval.rows.insertRow(row);
247 }
248 else if ( rec.getSid() == PrintGridlinesRecord.sid )
249 {
250 retval.printGridlines = (PrintGridlinesRecord) rec;
251 }
252 else if ( rec.getSid() == GridsetRecord.sid )
253 {
254 retval.gridset = (GridsetRecord) rec;
255 }
256 else if ( rec.getSid() == HeaderRecord.sid && bofEofNestingLevel == 1)
257 {
258 retval.header = (HeaderRecord) rec;
259 }
260 else if ( rec.getSid() == FooterRecord.sid && bofEofNestingLevel == 1)
261 {
262 retval.footer = (FooterRecord) rec;
263 }
264 else if ( rec.getSid() == PrintSetupRecord.sid && bofEofNestingLevel == 1)
265 {
266 retval.printSetup = (PrintSetupRecord) rec;
267 }
268 else if ( rec.getSid() == LeftMarginRecord.sid)
269 {
270 retval.getMargins()[LeftMargin] = (LeftMarginRecord) rec;
271 }
272 else if ( rec.getSid() == RightMarginRecord.sid)
273 {
274 retval.getMargins()[RightMargin] = (RightMarginRecord) rec;
275 }
276 else if ( rec.getSid() == TopMarginRecord.sid)
277 {
278 retval.getMargins()[TopMargin] = (TopMarginRecord) rec;
279 }
280 else if ( rec.getSid() == BottomMarginRecord.sid)
281 {
282 retval.getMargins()[BottomMargin] = (BottomMarginRecord) rec;
283 }
284 else if ( rec.getSid() == SelectionRecord.sid )
285 {
286 retval.selection = (SelectionRecord) rec;
287 }
288 else if ( rec.getSid() == WindowTwoRecord.sid )
289 {
290 retval.windowTwo = (WindowTwoRecord) rec;
291 }
292 else if ( rec.getSid() == DBCellRecord.sid )
293 {
294 rec = null;
295 }
296 else if ( rec.getSid() == IndexRecord.sid )
297 {
298 // ignore INDEX record because it is only needed by Excel,
299 // and POI always re-calculates its contents
300 rec = null;
301 }
302
303 else if ( rec.getSid() == ProtectRecord.sid )
304 {
305 retval.protect = (ProtectRecord) rec;
306 }
307 else if ( rec.getSid() == ObjectProtectRecord.sid )
308 {
309 retval.objprotect = (ObjectProtectRecord) rec;
310 }
311 else if ( rec.getSid() == ScenarioProtectRecord.sid )
312 {
313 retval.scenprotect = (ScenarioProtectRecord) rec;
314 }
315 else if ( rec.getSid() == PasswordRecord.sid )
316 {
317 retval.password = (PasswordRecord) rec;
318 }
319 else if (rec.getSid() == PageBreakRecord.HORIZONTAL_SID)
320 {
321 retval.rowBreaks = (PageBreakRecord)rec;
322 }
323 else if (rec.getSid() == PageBreakRecord.VERTICAL_SID)
324 {
325 retval.colBreaks = (PageBreakRecord)rec;
326 }
327
328 if (rec != null)
329 {
330 records.add(rec);
331 }
332 }
333 retval.records = records;
334 retval.checkRows();
335 retval.checkCells();
336 if (log.check( POILogger.DEBUG ))
337 log.log(POILogger.DEBUG, "sheet createSheet (existing file) exited");
338 return retval;
339 }
340
341 /**
342 * Clones the low level records of this sheet and returns the new sheet instance.
343 * This method is implemented by adding methods for deep cloning to all records that
344 * can be added to a sheet. The <b>Record</b> object does not implement cloneable.
345 * When adding a new record, implement a public clone method if and only if the record
346 * belongs to a sheet.
347 */
348 public Sheet cloneSheet()
349 {
350 ArrayList clonedRecords = new ArrayList(this.records.size());
351 for (int i=0; i<this.records.size();i++) {
352 Record rec = (Record)((Record)this.records.get(i)).clone();
353 //Need to pull out the Row record and the Value records from their
354 //Aggregates.
355 //This is probably the best way to do it since we probably dont want the createSheet
356 //To cater for these artificial Record types
357 if (rec instanceof RowRecordsAggregate) {
358 RowRecordsAggregate rrAgg = (RowRecordsAggregate)rec;
359 for (Iterator rowIter = rrAgg.getIterator();rowIter.hasNext();) {
360 Record rowRec = (Record)rowIter.next();
361 clonedRecords.add(rowRec);
362 }
363 } else if (rec instanceof ValueRecordsAggregate) {
364 ValueRecordsAggregate vrAgg = (ValueRecordsAggregate)rec;
365 for (Iterator cellIter = vrAgg.getIterator();cellIter.hasNext();) {
366 Record valRec = (Record)cellIter.next();
367
368 if (valRec instanceof FormulaRecordAggregate) {
369 FormulaRecordAggregate fmAgg = (FormulaRecordAggregate)valRec;
370 Record fmAggRec = fmAgg.getFormulaRecord();
371 if (fmAggRec != null)
372 clonedRecords.add(fmAggRec);
373 fmAggRec = fmAgg.getStringRecord();
374 if (fmAggRec != null)
375 clonedRecords.add(fmAggRec);
376 } else {
377 clonedRecords.add(valRec);
378 }
379 }
380 } else if (rec instanceof FormulaRecordAggregate) { //Is this required now??
381 FormulaRecordAggregate fmAgg = (FormulaRecordAggregate)rec;
382 Record fmAggRec = fmAgg.getFormulaRecord();
383 if (fmAggRec != null)
384 clonedRecords.add(fmAggRec);
385 fmAggRec = fmAgg.getStringRecord();
386 if (fmAggRec != null)
387 clonedRecords.add(fmAggRec);
388 } else {
389 clonedRecords.add(rec);
390 }
391 }
392 return createSheet(clonedRecords, 0, 0);
393 }
394
395
396 /**
397 * read support (offset = 0) Same as createSheet(Record[] recs, int, int)
398 * only the record offset is assumed to be 0.
399 *
400 * @param records array containing those records in the sheet in sequence (normally obtained from RecordFactory)
401 * @param sheetnum integer specifying the sheet's number (0,1 or 2 in this release)
402 * @return Sheet object
403 */
404
405 public static Sheet createSheet(List records, int sheetnum)
406 {
407 if (log.check( POILogger.DEBUG ))
408 log.log(POILogger.DEBUG,
409 "Sheet createSheet (exisiting file) assumed offset 0");
410 return createSheet(records, sheetnum, 0);
411 }
412
413 /**
414 * Creates a sheet with all the usual records minus values and the "index"
415 * record (not required). Sets the location pointer to where the first value
416 * records should go. Use this to create a sheet from "scratch".
417 *
418 * @return Sheet object with all values set to defaults
419 */
420
421 public static Sheet createSheet()
422 {
423 if (log.check( POILogger.DEBUG ))
424 log.log(POILogger.DEBUG, "Sheet createsheet from scratch called");
425 Sheet retval = new Sheet();
426 ArrayList records = new ArrayList(30);
427
428 records.add(retval.createBOF());
429
430 // records.add(retval.createIndex());
431 records.add(retval.createCalcMode());
432 records.add(retval.createCalcCount() );
433 records.add( retval.createRefMode() );
434 records.add( retval.createIteration() );
435 records.add( retval.createDelta() );
436 records.add( retval.createSaveRecalc() );
437 records.add( retval.createPrintHeaders() );
438 retval.printGridlines = (PrintGridlinesRecord) retval.createPrintGridlines();
439 records.add( retval.printGridlines );
440 retval.gridset = (GridsetRecord) retval.createGridset();
441 records.add( retval.gridset );
442 records.add( retval.createGuts() );
443 retval.defaultrowheight =
444 (DefaultRowHeightRecord) retval.createDefaultRowHeight();
445 records.add( retval.defaultrowheight );
446 records.add( retval.createWSBool() );
447
448 retval.rowBreaks = new PageBreakRecord(PageBreakRecord.HORIZONTAL_SID);
449 records.add(retval.rowBreaks);
450 retval.colBreaks = new PageBreakRecord(PageBreakRecord.VERTICAL_SID);
451 records.add(retval.colBreaks);
452
453 retval.header = (HeaderRecord) retval.createHeader();
454 records.add( retval.header );
455 retval.footer = (FooterRecord) retval.createFooter();
456 records.add( retval.footer );
457 records.add( retval.createHCenter() );
458 records.add( retval.createVCenter() );
459 retval.printSetup = (PrintSetupRecord) retval.createPrintSetup();
460 records.add( retval.printSetup );
461 retval.defaultcolwidth =
462 (DefaultColWidthRecord) retval.createDefaultColWidth();
463 records.add( retval.defaultcolwidth);
464 ColumnInfoRecordsAggregate columns = new ColumnInfoRecordsAggregate();
465 records.add( columns );
466 retval.columns = columns;
467 retval.dims = ( DimensionsRecord ) retval.createDimensions();
468 records.add(retval.dims);
469 retval.dimsloc = records.size()-1;
470 records.add(retval.windowTwo = retval.createWindowTwo());
471 retval.setLoc(records.size() - 1);
472 retval.selection =
473 (SelectionRecord) retval.createSelection();
474 records.add(retval.selection);
475 retval.protect = (ProtectRecord) retval.createProtect();
476 records.add(retval.protect);
477 records.add(retval.createEOF());
478
479
480 retval.records = records;
481 if (log.check( POILogger.DEBUG ))
482 log.log(POILogger.DEBUG, "Sheet createsheet from scratch exit");
483 return retval;
484 }
485
486 private void checkCells()
487 {
488 if (cells == null)
489 {
490 cells = new ValueRecordsAggregate();
491 // In the worksheet stream, the row records always occur before the cell (value)
492 // records. Therefore POI's aggregates (RowRecordsAggregate, ValueRecordsAggregate)
493 // should follow suit. Some methods in this class tolerate either order, while
494 // others have been found to fail (see bug 45145).
495 int rraIndex = getDimsLoc() + 1;
496 if (records.get(rraIndex).getClass() != RowRecordsAggregate.class) {
497 throw new IllegalStateException("Cannot create value records before row records exist");
498 }
499 records.add(rraIndex+1, cells);
500 }
501 }
502
503 private void checkRows()
504 {
505 if (rows == null)
506 {
507 rows = new RowRecordsAggregate();
508 records.add(getDimsLoc() + 1, rows);
509 }
510 }
511
512 public int addMergedRegion(int rowFrom, short colFrom, int rowTo, short colTo) {
513 // Validate input
514 if (rowTo < rowFrom) {
515 throw new IllegalArgumentException("The 'to' row (" + rowTo
516 + ") must not be less than the 'from' row (" + rowFrom + ")");
517 }
518 if (colTo < colFrom) {
519 throw new IllegalArgumentException("The 'to' col (" + colTo
520 + ") must not be less than the 'from' col (" + colFrom + ")");
521 }
522
523 if (merged == null || merged.getNumAreas() == 1027)
524 {
525 merged = ( MergeCellsRecord ) createMergedCells();
526 mergedRecords.add(merged);
527 records.add(records.size() - 1, merged);
528 }
529 merged.addArea(rowFrom, colFrom, rowTo, colTo);
530 return numMergedRegions++;
531 }
532
533 public void removeMergedRegion(int index)
534 {
535 //safety checks
536 if (index >= numMergedRegions || mergedRecords.size() == 0)
537 return;
538
539 int pos = 0;
540 int startNumRegions = 0;
541
542 //optimisation for current record
543 if (numMergedRegions - index < merged.getNumAreas())
544 {
545 pos = mergedRecords.size() - 1;
546 startNumRegions = numMergedRegions - merged.getNumAreas();
547 }
548 else
549 {
550 for (int n = 0; n < mergedRecords.size(); n++)
551 {
552 MergeCellsRecord record = (MergeCellsRecord) mergedRecords.get(n);
553 if (startNumRegions + record.getNumAreas() > index)
554 {
555 pos = n;
556 break;
557 }
558 startNumRegions += record.getNumAreas();
559 }
560 }
561
562 MergeCellsRecord rec = (MergeCellsRecord) mergedRecords.get(pos);
563 rec.removeAreaAt(index - startNumRegions);
564 numMergedRegions--;
565 if (rec.getNumAreas() == 0)
566 {
567 mergedRecords.remove(pos);
568 //get rid of the record from the sheet
569 records.remove(merged);
570 if (merged == rec) {
571 //pull up the LAST record for operations when we finally
572 //support continue records for mergedRegions
573 if (mergedRecords.size() > 0) {
574 merged = (MergeCellsRecord) mergedRecords.get(mergedRecords.size() - 1);
575 } else {
576 merged = null;
577 }
578 }
579 }
580 }
581
582 public MergeCellsRecord.MergedRegion getMergedRegionAt(int index)
583 {
584 //safety checks
585 if (index >= numMergedRegions || mergedRecords.size() == 0)
586 return null;
587
588 int pos = 0;
589 int startNumRegions = 0;
590
591 //optimisation for current record
592 if (numMergedRegions - index < merged.getNumAreas())
593 {
594 pos = mergedRecords.size() - 1;
595 startNumRegions = numMergedRegions - merged.getNumAreas();
596 }
597 else
598 {
599 for (int n = 0; n < mergedRecords.size(); n++)
600 {
601 MergeCellsRecord record = (MergeCellsRecord) mergedRecords.get(n);
602 if (startNumRegions + record.getNumAreas() > index)
603 {
604 pos = n;
605 break;
606 }
607 startNumRegions += record.getNumAreas();
608 }
609 }
610 return ((MergeCellsRecord) mergedRecords.get(pos)).getAreaAt(index - startNumRegions);
611 }
612
613 public int getNumMergedRegions()
614 {
615 return numMergedRegions;
616 }
617 // Find correct position to add new CF record
618 private int findConditionalFormattingPosition()
619 {
620 // This is default.
621 // If the algorithm does not find the right position,
622 // this one will be used (this is a position before EOF record)
623 int index = records.size()-2;
624
625 for( int i=index; i>=0; i-- )
626 {
627 Record rec = (Record)records.get(i);
628 short sid = rec.getSid();
629
630 // CFRecordsAggregate records already exist, just add to the end
631 if (rec instanceof CFRecordsAggregate) { return i+1; }
632
633 if( sid == (short)0x00ef ) { return i+1; }// PHONETICPR
634 if( sid == (short)0x015f ) { return i+1; }// LABELRANGES
635 if( sid == MergeCellsRecord.sid ) { return i+1; }
636 if( sid == (short)0x0099 ) { return i+1; }// STANDARDWIDTH
637 if( sid == SelectionRecord.sid ) { return i+1; }
638 if( sid == PaneRecord.sid ) { return i+1; }
639 if( sid == SCLRecord.sid ) { return i+1; }
640 if( sid == WindowTwoRecord.sid ) { return i+1; }
641 }
642
643 return index;
644 }
645
646 public int addConditionalFormatting(CFRecordsAggregate cfAggregate)
647 {
648 int index = findConditionalFormattingPosition();
649 records.add(index, cfAggregate);
650 condFormatting.add(cfAggregate);
651 return condFormatting.size()-1;
652 }
653
654 public void removeConditionalFormatting(int index)
655 {
656 if (index >= 0 && index <= condFormatting.size()-1 )
657 {
658 CFRecordsAggregate cfAggregate = getCFRecordsAggregateAt(index);
659 records.remove(cfAggregate);
660 condFormatting.remove(index);
661 }
662 }
663
664 public CFRecordsAggregate getCFRecordsAggregateAt(int index)
665 {
666 if (index >= 0 && index <= condFormatting.size()-1 )
667 {
668 return (CFRecordsAggregate) condFormatting.get(index);
669 }
670 return null;
671 }
672
673 public int getNumConditionalFormattings()
674 {
675 return condFormatting.size();
676 }
677
678 /**
679 * Returns the number of low level binary records in this sheet. This adjusts things for the so called
680 * AgregateRecords.
681 *
682 * @see org.apache.poi.hssf.record.Record
683 */
684
685 public int getNumRecords()
686 {
687 checkCells();
688 checkRows();
689 if (log.check( POILogger.DEBUG ))
690 {
691 log.log(POILogger.DEBUG, "Sheet.getNumRecords");
692 log.logFormatted(POILogger.DEBUG, "returning % + % + % - 2 = %", new int[]
693 {
694 records.size(), cells.getPhysicalNumberOfCells(),
695 rows.getPhysicalNumberOfRows(),
696 records.size() + cells.getPhysicalNumberOfCells()
697 + rows.getPhysicalNumberOfRows() - 2
698 });
699 }
700 return records.size() + cells.getPhysicalNumberOfCells()
701 + rows.getPhysicalNumberOfRows() - 2;
702 }
703
704 /**
705 * Per an earlier reported bug in working with Andy Khan's excel read library. This
706 * sets the values in the sheet's DimensionsRecord object to be correct. Excel doesn't
707 * really care, but we want to play nice with other libraries.
708 *
709 * @see org.apache.poi.hssf.record.DimensionsRecord
710 */
711 public void setDimensions(int firstrow, short firstcol, int lastrow,
712 short lastcol)
713 {
714 if (log.check( POILogger.DEBUG ))
715 {
716 log.log(POILogger.DEBUG, "Sheet.setDimensions");
717 log.log(POILogger.DEBUG,
718 (new StringBuffer("firstrow")).append(firstrow)
719 .append("firstcol").append(firstcol).append("lastrow")
720 .append(lastrow).append("lastcol").append(lastcol)
721 .toString());
722 }
723 dims.setFirstCol(firstcol);
724 dims.setFirstRow(firstrow);
725 dims.setLastCol(lastcol);
726 dims.setLastRow(lastrow);
727 if (log.check( POILogger.DEBUG ))
728 log.log(POILogger.DEBUG, "Sheet.setDimensions exiting");
729 }
730
731 /**
732 * set the locator for where we should look for the next value record. The
733 * algorithm will actually start here and find the correct location so you
734 * can set this to 0 and watch performance go down the tubes but it will work.
735 * After a value is set this is automatically advanced. Its also set by the
736 * create method. So you probably shouldn't mess with this unless you have
737 * a compelling reason why or the help for the method you're calling says so.
738 * Check the other methods for whether they care about
739 * the loc pointer. Many of the "modify" and "remove" methods re-initialize this
740 * to "dimsloc" which is the location of the Dimensions Record and presumably the
741 * start of the value section (at or around 19 dec).
742 *
743 * @param loc the record number to start at
744 *
745 */
746
747 public void setLoc(int loc)
748 {
749 valueRecIterator = null;
750 if (log.check( POILogger.DEBUG ))
751 log.log(POILogger.DEBUG, "sheet.setLoc(): " + loc);
752 this.loc = loc;
753 }
754
755 /**
756 * Returns the location pointer to the first record to look for when adding rows/values
757 *
758 */
759
760 public int getLoc()
761 {
762 if (log.check( POILogger.DEBUG ))
763 log.log(POILogger.DEBUG, "sheet.getLoc():" + loc);
764 return loc;
765 }
766
767 /**
768 * Set the preoffset when using DBCELL records (currently unused) - this is
769 * the position of this sheet within the whole file.
770 *
771 * @param offset the offset of the sheet's BOF within the file.
772 */
773
774 public void setPreOffset(int offset)
775 {
776 this.preoffset = offset;
777 }
778
779 /**
780 * get the preoffset when using DBCELL records (currently unused) - this is
781 * the position of this sheet within the whole file.
782 *
783 * @return offset the offset of the sheet's BOF within the file.
784 */
785
786 public int getPreOffset()
787 {
788 return preoffset;
789 }
790
791 /**
792 * Serializes all records in the sheet into one big byte array. Use this to write
793 * the sheet out.
794 *
795 * @param offset to begin write at
796 * @param data array containing the binary representation of the records in this sheet
797 *
798 */
799
800 public int serialize(int offset, byte [] data)
801 {
802 if (log.check( POILogger.DEBUG ))
803 log.log(POILogger.DEBUG, "Sheet.serialize using offsets");
804
805 int pos = offset;
806 boolean haveSerializedIndex = false;
807
808 for (int k = 0; k < records.size(); k++)
809 {
810 Record record = (( Record ) records.get(k));
811
812 // Don't write out UncalcedRecord entries, as
813 // we handle those specially just below
814 if (record instanceof UncalcedRecord) {
815 continue;
816 }
817
818 // Once the rows have been found in the list of records, start
819 // writing out the blocked row information. This includes the DBCell references
820 if (record instanceof RowRecordsAggregate) {
821 pos += ((RowRecordsAggregate)record).serialize(pos, data, cells);
822 } else if (record instanceof ValueRecordsAggregate) {
823 //Do nothing here. The records were serialized during the RowRecordAggregate block serialization
824 } else {
825 pos += record.serialize(pos, data );
826 }
827
828 // If the BOF record was just serialized then add the IndexRecord
829 if (record.getSid() == BOFRecord.sid) {
830 // Add an optional UncalcedRecord
831 if (_isUncalced) {
832 UncalcedRecord rec = new UncalcedRecord();
833 pos += rec.serialize(pos, data);
834 }
835 //Can there be more than one BOF for a sheet? If not then we can
836 //remove this guard. So be safe it is left here.
837 if (rows != null && !haveSerializedIndex) {
838 haveSerializedIndex = true;
839 pos += serializeIndexRecord(k, pos, data);
840 }
841 }
842 }
843 if (log.check( POILogger.DEBUG )) {
844 log.log(POILogger.DEBUG, "Sheet.serialize returning ");
845 }
846 return pos-offset;
847 }
848
849 /**
850 * @param indexRecordOffset also happens to be the end of the BOF record
851 * @return the size of the serialized INDEX record
852 */
853 private int serializeIndexRecord(final int bofRecordIndex, final int indexRecordOffset,
854 byte[] data) {
855 IndexRecord index = new IndexRecord();
856 index.setFirstRow(rows.getFirstRowNum());
857 index.setLastRowAdd1(rows.getLastRowNum() + 1);
858 // Calculate the size of the records from the end of the BOF
859 // and up to the RowRecordsAggregate...
860
861 // 'initial sheet records' are between INDEX and first ROW record.
862 int sizeOfInitialSheetRecords = 0;
863 // start just after BOF record (INDEX is not present in this list)
864 for (int j = bofRecordIndex + 1; j < records.size(); j++) {
865 Record tmpRec = ((Record) records.get(j));
866 if (tmpRec instanceof UncalcedRecord) {
867 continue;
868 }
869 if (tmpRec instanceof RowRecordsAggregate) {
870 break;
871 }
872 sizeOfInitialSheetRecords += tmpRec.getRecordSize();
873 }
874 if (_isUncalced) {
875 sizeOfInitialSheetRecords += UncalcedRecord.getStaticRecordSize();
876 }
877
878 // Add the references to the DBCells in the IndexRecord (one for each block)
879 // Note: The offsets are relative to the Workbook BOF. Assume that this is
880 // 0 for now.....
881
882 int blockCount = rows.getRowBlockCount();
883 // Calculate the size of this IndexRecord
884 int indexRecSize = IndexRecord.getRecordSizeForBlockCount(blockCount);
885
886 int currentOffset = indexRecordOffset + indexRecSize + sizeOfInitialSheetRecords;
887
888 for (int block = 0; block < blockCount; block++) {
889 // each row-block has a DBCELL record.
890 // The offset of each DBCELL record needs to be updated in the INDEX record
891
892 // account for row records in this row-block
893 currentOffset += rows.getRowBlockSize(block);
894 // account for cell value records after those
895 currentOffset += null == cells ? 0 : cells.getRowCellBlockSize(rows
896 .getStartRowNumberForBlock(block), rows.getEndRowNumberForBlock(block));
897
898 // currentOffset is now the location of the DBCELL record for this row-block
899 index.addDbcell(currentOffset);
900 // Add space required to write the DBCELL record (whose reference was just added).
901 currentOffset += (8 + (rows.getRowCountForBlock(block) * 2));
902 }
903 return index.serialize(indexRecordOffset, data);
904 }
905
906
907 /**
908 * Create a row record. (does not add it to the records contained in this sheet)
909 *
910 * @param row number
911 * @return RowRecord created for the passed in row number
912 * @see org.apache.poi.hssf.record.RowRecord
913 */
914
915 public RowRecord createRow(int row)
916 {
917 return RowRecordsAggregate.createRow( row );
918 }
919
920 /**
921 * Create a LABELSST Record (does not add it to the records contained in this sheet)
922 *
923 * @param row the row the LabelSST is a member of
924 * @param col the column the LabelSST defines
925 * @param index the index of the string within the SST (use workbook addSSTString method)
926 * @return LabelSSTRecord newly created containing your SST Index, row,col.
927 * @see org.apache.poi.hssf.record.SSTRecord
928 */
929 public LabelSSTRecord createLabelSST(int row, short col, int index)
930 {
931 log.logFormatted(POILogger.DEBUG, "create labelsst row,col,index %,%,%",
932 new int[]
933 {
934 row, col, index
935 });
936 LabelSSTRecord rec = new LabelSSTRecord();
937
938 rec.setRow(row);
939 rec.setColumn(col);
940 rec.setSSTIndex(index);
941 rec.setXFIndex(( short ) 0x0f);
942 return rec;
943 }
944
945 /**
946 * Create a NUMBER Record (does not add it to the records contained in this sheet)
947 *
948 * @param row the row the NumberRecord is a member of
949 * @param col the column the NumberRecord defines
950 * @param value for the number record
951 *
952 * @return NumberRecord for that row, col containing that value as added to the sheet
953 */
954 public NumberRecord createNumber(int row, short col, double value)
955 {
956 log.logFormatted(POILogger.DEBUG, "create number row,col,value %,%,%",
957 new double[]
958 {
959 row, col, value
960 });
961 NumberRecord rec = new NumberRecord();
962
963 rec.setRow(row);
964 rec.setColumn(col);
965 rec.setValue(value);
966 rec.setXFIndex(( short ) 0x0f);
967 return rec;
968 }
969
970 /**
971 * create a BLANK record (does not add it to the records contained in this sheet)
972 *
973 * @param row - the row the BlankRecord is a member of
974 * @param col - the column the BlankRecord is a member of
975 */
976 public BlankRecord createBlank(int row, short col)
977 {
978 log.logFormatted(POILogger.DEBUG, "create blank row,col %,%", new int[]
979 {
980 row, col
981 });
982 BlankRecord rec = new BlankRecord();
983
984 rec.setRow(row);
985 rec.setColumn(col);
986 rec.setXFIndex(( short ) 0x0f);
987 return rec;
988 }
989
990 /**
991 * Attempts to parse the formula into PTGs and create a formula record
992 * DOES NOT WORK YET
993 *
994 * @param row - the row for the formula record
995 * @param col - the column of the formula record
996 * @param formula - a String representing the formula. To be parsed to PTGs
997 * @return bogus/useless formula record
998 */
999 public FormulaRecord createFormula(int row, short col, String formula)
1000 {
1001 log.logFormatted(POILogger.DEBUG, "create formula row,col,formula %,%,%",
1002 new int[]
1003 {
1004 row, col
1005 }, formula);
1006 FormulaRecord rec = new FormulaRecord();
1007
1008 rec.setRow(row);
1009 rec.setColumn(col);
1010 rec.setOptions(( short ) 2);
1011 rec.setValue(0);
1012 rec.setXFIndex(( short ) 0x0f);
1013 FormulaParser fp = new FormulaParser(formula,null); //fix - do we need this method?
1014 fp.parse();
1015 Ptg[] ptg = fp.getRPNPtg();
1016 int size = 0;
1017
1018 for (int k = 0; k < ptg.length; k++)
1019 {
1020 size += ptg[ k ].getSize();
1021 rec.pushExpressionToken(ptg[ k ]);
1022 }
1023 rec.setExpressionLength(( short ) size);
1024 return rec;
1025 }
1026
1027 /**
1028 * Adds a value record to the sheet's contained binary records
1029 * (i.e. LabelSSTRecord or NumberRecord).
1030 * <P>
1031 * This method is "loc" sensitive. Meaning you need to set LOC to where you
1032 * want it to start searching. If you don't know do this: setLoc(getDimsLoc).
1033 * When adding several rows you can just start at the last one by leaving loc
1034 * at what this sets it to.
1035 *
1036 * @param row the row to add the cell value to
1037 * @param col the cell value record itself.
1038 */
1039 public void addValueRecord(int row, CellValueRecordInterface col)
1040 {
1041 checkCells();
1042 if(log.check(POILogger.DEBUG))
1043 {
1044 log.logFormatted(POILogger.DEBUG, "add value record row,loc %,%", new int[]
1045 {
1046 row, loc
1047 });
1048 }
1049 DimensionsRecord d = ( DimensionsRecord ) records.get(getDimsLoc());
1050
1051 if (col.getColumn() > d.getLastCol())
1052 {
1053 d.setLastCol(( short ) (col.getColumn() + 1));
1054 }
1055 if (col.getColumn() < d.getFirstCol())
1056 {
1057 d.setFirstCol(col.getColumn());
1058 }
1059 cells.insertCell(col);
1060 }
1061
1062 /**
1063 * remove a value record from the records array.
1064 *
1065 * This method is not loc sensitive, it resets loc to = dimsloc so no worries.
1066 *
1067 * @param row - the row of the value record you wish to remove
1068 * @param col - a record supporting the CellValueRecordInterface.
1069 * @see org.apache.poi.hssf.record.CellValueRecordInterface
1070 */
1071 public void removeValueRecord(int row, CellValueRecordInterface col)
1072 {
1073 checkCells();
1074 log.logFormatted(POILogger.DEBUG, "remove value record row,dimsloc %,%",
1075 new int[]{row, dimsloc} );
1076 loc = dimsloc;
1077 cells.removeCell(col);
1078 }
1079
1080 /**
1081 * replace a value record from the records array.
1082 *
1083 * This method is not loc sensitive, it resets loc to = dimsloc so no worries.
1084 *
1085 * @param newval - a record supporting the CellValueRecordInterface. this will replace
1086 * the cell value with the same row and column. If there isn't one, one will
1087 * be added.
1088 */
1089
1090 public void replaceValueRecord(CellValueRecordInterface newval)
1091 {
1092 checkCells();
1093 setLoc(dimsloc);
1094 if (log.check( POILogger.DEBUG ))
1095 log.log(POILogger.DEBUG, "replaceValueRecord ");
1096 //The ValueRecordsAggregate use a tree map underneath.
1097 //The tree Map uses the CellValueRecordInterface as both the
1098 //key and the value, if we dont do a remove, then
1099 //the previous instance of the key is retained, effectively using
1100 //double the memory
1101 cells.removeCell(newval);
1102 cells.insertCell(newval);
1103 }
1104
1105 /**
1106 * Adds a row record to the sheet
1107 *
1108 * <P>
1109 * This method is "loc" sensitive. Meaning you need to set LOC to where you
1110 * want it to start searching. If you don't know do this: setLoc(getDimsLoc).
1111 * When adding several rows you can just start at the last one by leaving loc
1112 * at what this sets it to.
1113 *
1114 * @param row the row record to be added
1115 * @see #setLoc(int)
1116 */
1117
1118 public void addRow(RowRecord row)
1119 {
1120 checkRows();
1121 if (log.check( POILogger.DEBUG ))
1122 log.log(POILogger.DEBUG, "addRow ");
1123 DimensionsRecord d = ( DimensionsRecord ) records.get(getDimsLoc());
1124
1125 if (row.getRowNumber() >= d.getLastRow())
1126 {
1127 d.setLastRow(row.getRowNumber() + 1);
1128 }
1129 if (row.getRowNumber() < d.getFirstRow())
1130 {
1131 d.setFirstRow(row.getRowNumber());
1132 }
1133 //IndexRecord index = null;
1134 //If the row exists remove it, so that any cells attached to the row are removed
1135 RowRecord existingRow = rows.getRow(row.getRowNumber());
1136 if (existingRow != null)
1137 rows.removeRow(existingRow);
1138
1139 rows.insertRow(row);
1140
1141 if (log.check( POILogger.DEBUG ))
1142 log.log(POILogger.DEBUG, "exit addRow");
1143 }
1144
1145 /**
1146 * Removes a row record
1147 *
1148 * This method is not loc sensitive, it resets loc to = dimsloc so no worries.
1149 *
1150 * @param row the row record to remove
1151 */
1152
1153 public void removeRow(RowRecord row)
1154 {
1155 checkRows();
1156
1157 setLoc(getDimsLoc());
1158 rows.removeRow(row);
1159 }
1160
1161 /**
1162 * get the NEXT value record (from LOC). The first record that is a value record
1163 * (starting at LOC) will be returned.
1164 *
1165 * <P>
1166 * This method is "loc" sensitive. Meaning you need to set LOC to where you
1167 * want it to start searching. If you don't know do this: setLoc(getDimsLoc).
1168 * When adding several rows you can just start at the last one by leaving loc
1169 * at what this sets it to. For this method, set loc to dimsloc to start with,
1170 * subsequent calls will return values in (physical) sequence or NULL when you get to the end.
1171 *
1172 * @return CellValueRecordInterface representing the next value record or NULL if there are no more
1173 * @see #setLoc(int)
1174 */
1175
1176 public CellValueRecordInterface getNextValueRecord()
1177 {
1178 if (log.check( POILogger.DEBUG ))
1179 log.log(POILogger.DEBUG, "getNextValue loc= " + loc);
1180 if (valueRecIterator == null)
1181 {
1182 valueRecIterator = cells.getIterator();
1183 }
1184 if (!valueRecIterator.hasNext())
1185 {
1186 return null;
1187 }
1188 return ( CellValueRecordInterface ) valueRecIterator.next();
1189 }
1190
1191 /**
1192 * get the NEXT RowRecord (from LOC). The first record that is a Row record
1193 * (starting at LOC) will be returned.
1194 * <P>
1195 * This method is "loc" sensitive. Meaning you need to set LOC to where you
1196 * want it to start searching. If you don't know do this: setLoc(getDimsLoc).
1197 * When adding several rows you can just start at the last one by leaving loc
1198 * at what this sets it to. For this method, set loc to dimsloc to start with.
1199 * subsequent calls will return rows in (physical) sequence or NULL when you get to the end.
1200 *
1201 * @return RowRecord representing the next row record or NULL if there are no more
1202 * @see #setLoc(int)
1203 *
1204 */
1205
1206 public RowRecord getNextRow()
1207 {
1208 if (log.check( POILogger.DEBUG ))
1209 log.log(POILogger.DEBUG, "getNextRow loc= " + loc);
1210 if (rowRecIterator == null)
1211 {
1212 rowRecIterator = rows.getIterator();
1213 }
1214 if (!rowRecIterator.hasNext())
1215 {
1216 return null;
1217 }
1218 return ( RowRecord ) rowRecIterator.next();
1219 }
1220
1221 /**
1222 * get the NEXT (from LOC) RowRecord where rownumber matches the given rownum.
1223 * The first record that is a Row record (starting at LOC) that has the
1224 * same rownum as the given rownum will be returned.
1225 * <P>
1226 * This method is "loc" sensitive. Meaning you need to set LOC to where you
1227 * want it to start searching. If you don't know do this: setLoc(getDimsLoc).
1228 * When adding several rows you can just start at the last one by leaving loc
1229 * at what this sets it to. For this method, set loc to dimsloc to start with.
1230 * subsequent calls will return rows in (physical) sequence or NULL when you get to the end.
1231 *
1232 * @param rownum which row to return (careful with LOC)
1233 * @return RowRecord representing the next row record or NULL if there are no more
1234 * @see #setLoc(int)
1235 *
1236 */
1237 public RowRecord getRow(int rownum) {
1238 if (log.check( POILogger.DEBUG ))
1239 log.log(POILogger.DEBUG, "getNextRow loc= " + loc);
1240 return rows.getRow(rownum);
1241 }
1242
1243 /**
1244 * creates the BOF record
1245 * @see org.apache.poi.hssf.record.BOFRecord
1246 * @see org.apache.poi.hssf.record.Record
1247 * @return record containing a BOFRecord
1248 */
1249
1250 protected Record createBOF()
1251 {
1252 BOFRecord retval = new BOFRecord();
1253
1254 retval.setVersion(( short ) 0x600);
1255 retval.setType(( short ) 0x010);
1256
1257 retval.setBuild(( short ) 0x0dbb);
1258 retval.setBuildYear(( short ) 1996);
1259 retval.setHistoryBitMask(0xc1);
1260 retval.setRequiredVersion(0x6);
1261 return retval;
1262 }
1263
1264 /**
1265 * creates the Index record - not currently used
1266 * @see org.apache.poi.hssf.record.IndexRecord
1267 * @see org.apache.poi.hssf.record.Record
1268 * @return record containing a IndexRecord
1269 */
1270
1271 protected Record createIndex()
1272 {
1273 IndexRecord retval = new IndexRecord();
1274
1275 retval.setFirstRow(0); // must be set explicitly
1276 retval.setLastRowAdd1(0);
1277 return retval;
1278 }
1279
1280 /**
1281 * creates the CalcMode record and sets it to 1 (automatic formula caculation)
1282 * @see org.apache.poi.hssf.record.CalcModeRecord
1283 * @see org.apache.poi.hssf.record.Record
1284 * @return record containing a CalcModeRecord
1285 */
1286
1287 protected Record createCalcMode()
1288 {
1289 CalcModeRecord retval = new CalcModeRecord();
1290
1291 retval.setCalcMode(( short ) 1);
1292 return retval;
1293 }
1294
1295 /**
1296 * creates the CalcCount record and sets it to 0x64 (default number of iterations)
1297 * @see org.apache.poi.hssf.record.CalcCountRecord
1298 * @see org.apache.poi.hssf.record.Record
1299 * @return record containing a CalcCountRecord
1300 */
1301
1302 protected Record createCalcCount()
1303 {
1304 CalcCountRecord retval = new CalcCountRecord();
1305
1306 retval.setIterations(( short ) 0x64); // default 64 iterations
1307 return retval;
1308 }
1309
1310 /**
1311 * creates the RefMode record and sets it to A1 Mode (default reference mode)
1312 * @see org.apache.poi.hssf.record.RefModeRecord
1313 * @see org.apache.poi.hssf.record.Record
1314 * @return record containing a RefModeRecord
1315 */
1316
1317 protected Record createRefMode()
1318 {
1319 RefModeRecord retval = new RefModeRecord();
1320
1321 retval.setMode(RefModeRecord.USE_A1_MODE);
1322 return retval;
1323 }
1324
1325 /**
1326 * creates the Iteration record and sets it to false (don't iteratively calculate formulas)
1327 * @see org.apache.poi.hssf.record.IterationRecord
1328 * @see org.apache.poi.hssf.record.Record
1329 * @return record containing a IterationRecord
1330 */
1331
1332 protected Record createIteration()
1333 {
1334 IterationRecord retval = new IterationRecord();
1335
1336 retval.setIteration(false);
1337 return retval;
1338 }
1339
1340 /**
1341 * creates the Delta record and sets it to 0.0010 (default accuracy)
1342 * @see org.apache.poi.hssf.record.DeltaRecord
1343 * @see org.apache.poi.hssf.record.Record
1344 * @return record containing a DeltaRecord
1345 */
1346
1347 protected Record createDelta()
1348 {
1349 DeltaRecord retval = new DeltaRecord();
1350
1351 retval.setMaxChange(0.0010);
1352 return retval;
1353 }
1354
1355 /**
1356 * creates the SaveRecalc record and sets it to true (recalculate before saving)
1357 * @see org.apache.poi.hssf.record.SaveRecalcRecord
1358 * @see org.apache.poi.hssf.record.Record
1359 * @return record containing a SaveRecalcRecord
1360 */
1361
1362 protected Record createSaveRecalc()
1363 {
1364 SaveRecalcRecord retval = new SaveRecalcRecord();
1365
1366 retval.setRecalc(true);
1367 return retval;
1368 }
1369
1370 /**
1371 * creates the PrintHeaders record and sets it to false (we don't create headers yet so why print them)
1372 * @see org.apache.poi.hssf.record.PrintHeadersRecord
1373 * @see org.apache.poi.hssf.record.Record
1374 * @return record containing a PrintHeadersRecord
1375 */
1376
1377 protected Record createPrintHeaders()
1378 {
1379 PrintHeadersRecord retval = new PrintHeadersRecord();
1380
1381 retval.setPrintHeaders(false);
1382 return retval;
1383 }
1384
1385 /**
1386 * creates the PrintGridlines record and sets it to false (that makes for ugly sheets). As far as I can
1387 * tell this does the same thing as the GridsetRecord
1388 *
1389 * @see org.apache.poi.hssf.record.PrintGridlinesRecord
1390 * @see org.apache.poi.hssf.record.Record
1391 * @return record containing a PrintGridlinesRecord
1392 */
1393
1394 protected Record createPrintGridlines()
1395 {
1396 PrintGridlinesRecord retval = new PrintGridlinesRecord();
1397
1398 retval.setPrintGridlines(false);
1399 return retval;
1400 }
1401
1402 /**
1403 * creates the Gridset record and sets it to true (user has mucked with the gridlines)
1404 * @see org.apache.poi.hssf.record.GridsetRecord
1405 * @see org.apache.poi.hssf.record.Record
1406 * @return record containing a GridsetRecord
1407 */
1408
1409 protected Record createGridset()
1410 {
1411 GridsetRecord retval = new GridsetRecord();
1412
1413 retval.setGridset(true);
1414 return retval;
1415 }
1416
1417 /**
1418 * creates the Guts record and sets leftrow/topcol guttter and rowlevelmax/collevelmax to 0
1419 * @see org.apache.poi.hssf.record.GutsRecord
1420 * @see org.apache.poi.hssf.record.Record
1421 * @return record containing a GutsRecordRecord
1422 */
1423
1424 protected Record createGuts()
1425 {
1426 GutsRecord retval = new GutsRecord();
1427
1428 retval.setLeftRowGutter(( short ) 0);
1429 retval.setTopColGutter(( short ) 0);
1430 retval.setRowLevelMax(( short ) 0);
1431 retval.setColLevelMax(( short ) 0);
1432 return retval;
1433 }
1434
1435 /**
1436 * creates the DefaultRowHeight Record and sets its options to 0 and rowheight to 0xff
1437 * @see org.apache.poi.hssf.record.DefaultRowHeightRecord
1438 * @see org.apache.poi.hssf.record.Record
1439 * @return record containing a DefaultRowHeightRecord
1440 */
1441
1442 protected Record createDefaultRowHeight()
1443 {
1444 DefaultRowHeightRecord retval = new DefaultRowHeightRecord();
1445
1446 retval.setOptionFlags(( short ) 0);
1447 retval.setRowHeight(( short ) 0xff);
1448 return retval;
1449 }
1450
1451 /**
1452 * creates the WSBoolRecord and sets its values to defaults
1453 * @see org.apache.poi.hssf.record.WSBoolRecord
1454 * @see org.apache.poi.hssf.record.Record
1455 * @return record containing a WSBoolRecord
1456 */
1457
1458 protected Record createWSBool()
1459 {
1460 WSBoolRecord retval = new WSBoolRecord();
1461
1462 retval.setWSBool1(( byte ) 0x4);
1463 retval.setWSBool2(( byte ) 0xffffffc1);
1464 return retval;
1465 }
1466
1467 /**
1468 * creates the Header Record and sets it to nothing/0 length
1469 * @see org.apache.poi.hssf.record.HeaderRecord
1470 * @see org.apache.poi.hssf.record.Record
1471 * @return record containing a HeaderRecord
1472 */
1473
1474 protected Record createHeader()
1475 {
1476 HeaderRecord retval = new HeaderRecord();
1477
1478 retval.setHeaderLength(( byte ) 0);
1479 retval.setHeader(null);
1480 return retval;
1481 }
1482
1483 /**
1484 * creates the Footer Record and sets it to nothing/0 length
1485 * @see org.apache.poi.hssf.record.FooterRecord
1486 * @see org.apache.poi.hssf.record.Record
1487 * @return record containing a FooterRecord
1488 */
1489
1490 protected Record createFooter()
1491 {
1492 FooterRecord retval = new FooterRecord();
1493
1494 retval.setFooterLength(( byte ) 0);
1495 retval.setFooter(null);
1496 return retval;
1497 }
1498
1499 /**
1500 * creates the HCenter Record and sets it to false (don't horizontally center)
1501 * @see org.apache.poi.hssf.record.HCenterRecord
1502 * @see org.apache.poi.hssf.record.Record
1503 * @return record containing a HCenterRecord
1504 */
1505
1506 protected Record createHCenter()
1507 {
1508 HCenterRecord retval = new HCenterRecord();
1509
1510 retval.setHCenter(false);
1511 return retval;
1512 }
1513
1514 /**
1515 * creates the VCenter Record and sets it to false (don't horizontally center)
1516 * @see org.apache.poi.hssf.record.VCenterRecord
1517 * @see org.apache.poi.hssf.record.Record
1518 * @return record containing a VCenterRecord
1519 */
1520
1521 protected Record createVCenter()
1522 {
1523 VCenterRecord retval = new VCenterRecord();
1524
1525 retval.setVCenter(false);
1526 return retval;
1527 }
1528
1529 /**
1530 * creates the PrintSetup Record and sets it to defaults and marks it invalid
1531 * @see org.apache.poi.hssf.record.PrintSetupRecord
1532 * @see org.apache.poi.hssf.record.Record
1533 * @return record containing a PrintSetupRecord
1534 */
1535
1536 protected Record createPrintSetup()
1537 {
1538 PrintSetupRecord retval = new PrintSetupRecord();
1539
1540 retval.setPaperSize(( short ) 1);
1541 retval.setScale(( short ) 100);
1542 retval.setPageStart(( short ) 1);
1543 retval.setFitWidth(( short ) 1);
1544 retval.setFitHeight(( short ) 1);
1545 retval.setOptions(( short ) 2);
1546 retval.setHResolution(( short ) 300);
1547 retval.setVResolution(( short ) 300);
1548 retval.setHeaderMargin( 0.5);
1549 retval.setFooterMargin( 0.5);
1550 retval.setCopies(( short ) 0);
1551 return retval;
1552 }
1553
1554 /**
1555 * creates the DefaultColWidth Record and sets it to 8
1556 * @see org.apache.poi.hssf.record.DefaultColWidthRecord
1557 * @see org.apache.poi.hssf.record.Record
1558 * @return record containing a DefaultColWidthRecord
1559 */
1560
1561 protected Record createDefaultColWidth()
1562 {
1563 DefaultColWidthRecord retval = new DefaultColWidthRecord();
1564
1565 retval.setColWidth(( short ) 8);
1566 return retval;
1567 }
1568
1569 /**
1570 * creates the ColumnInfo Record and sets it to a default column/width
1571 * @see org.apache.poi.hssf.record.ColumnInfoRecord
1572 * @return record containing a ColumnInfoRecord
1573 */
1574 // TODO change return type to ColumnInfoRecord
1575 protected Record createColInfo()
1576 {
1577 return ColumnInfoRecordsAggregate.createColInfo();
1578 }
1579
1580 /**
1581 * get the default column width for the sheet (if the columns do not define their own width)
1582 * @return default column width
1583 */
1584
1585 public short getDefaultColumnWidth()
1586 {
1587 return defaultcolwidth.getColWidth();
1588 }
1589
1590 /**
1591 * get whether gridlines are printed.
1592 * @return true if printed
1593 */
1594
1595 public boolean isGridsPrinted()
1596 {
1597 if (gridset == null) {
1598 gridset = (GridsetRecord)createGridset();
1599 //Insert the newlycreated Gridset record at the end of the record (just before the EOF)
1600 int loc = findFirstRecordLocBySid(EOFRecord.sid);
1601 records.add(loc, gridset);
1602 }
1603 return !gridset.getGridset();
1604 }
1605
1606 /**
1607 * set whether gridlines printed or not.
1608 * @param value True if gridlines printed.
1609 */
1610
1611 public void setGridsPrinted(boolean value)
1612 {
1613 gridset.setGridset(!value);
1614 }
1615
1616 /**
1617 * set the default column width for the sheet (if the columns do not define their own width)
1618 * @param dcw default column width
1619 */
1620
1621 public void setDefaultColumnWidth(short dcw)
1622 {
1623 defaultcolwidth.setColWidth(dcw);
1624 }
1625
1626 /**
1627 * set the default row height for the sheet (if the rows do not define their own height)
1628 */
1629
1630 public void setDefaultRowHeight(short dch)
1631 {
1632 defaultrowheight.setRowHeight(dch);
1633 }
1634
1635 /**
1636 * get the default row height for the sheet (if the rows do not define their own height)
1637 * @return default row height
1638 */
1639
1640 public short getDefaultRowHeight()
1641 {
1642 return defaultrowheight.getRowHeight();
1643 }
1644
1645 /**
1646 * get the width of a given column in units of 1/256th of a character width
1647 * @param column index
1648 * @see org.apache.poi.hssf.record.DefaultColWidthRecord
1649 * @see org.apache.poi.hssf.record.ColumnInfoRecord
1650 * @see #setColumnWidth(short,short)
1651 * @return column width in units of 1/256th of a character width
1652 */
1653
1654 public short getColumnWidth(short column)
1655 {
1656 short retval = 0;
1657 ColumnInfoRecord ci = null;
1658
1659 if (columns != null)
1660 {
1661 int count=columns.getNumColumns();
1662 for ( int k=0;k<count;k++ )
1663 {
1664 ci = columns.getColInfo(k);
1665 if ((ci.getFirstColumn() <= column)
1666 && (column <= ci.getLastColumn()))
1667 {
1668 break;
1669 }
1670 ci = null;
1671 }
1672 }
1673 if (ci != null)
1674 {
1675 retval = ci.getColumnWidth();
1676 }
1677 else
1678 {
1679 //default column width is measured in characters
1680 //multiply
1681 retval = (short)(256*defaultcolwidth.getColWidth());
1682 }
1683 return retval;
1684 }
1685
1686 /**
1687 * get the index to the ExtendedFormatRecord "associated" with
1688 * the column at specified 0-based index. (In this case, an
1689 * ExtendedFormatRecord index is actually associated with a
1690 * ColumnInfoRecord which spans 1 or more columns)
1691 * <br/>
1692 * Returns the index to the default ExtendedFormatRecord (0xF)
1693 * if no ColumnInfoRecord exists that includes the column
1694 * index specified.
1695 * @param column
1696 * @return index of ExtendedFormatRecord associated with
1697 * ColumnInfoRecord that includes the column index or the
1698 * index of the default ExtendedFormatRecord (0xF)
1699 */
1700 public short getXFIndexForColAt(short column) {
1701 short retval = 0;
1702 ColumnInfoRecord ci = null;
1703 if (columns != null) {
1704 int count=columns.getNumColumns();
1705 for ( int k=0;k<count;k++ )
1706 {
1707 ci = columns.getColInfo(k);
1708 if ((ci.getFirstColumn() <= column)
1709 && (column <= ci.getLastColumn())) {
1710 break;
1711 }
1712 ci = null;
1713 }
1714 }
1715 retval = (ci != null) ? ci.getXFIndex() : 0xF;
1716 return retval;
1717 }
1718
1719 /**
1720 * set the width for a given column in 1/256th of a character width units
1721 * @param column - the column number
1722 * @param width (in units of 1/256th of a character width)
1723 */
1724 public void setColumnWidth(short column, short width)
1725 {
1726 setColumn( column, new Short(width), null, null, null);
1727 }
1728
1729 /**
1730 * Get the hidden property for a given column.
1731 * @param column index
1732 * @see org.apache.poi.hssf.record.DefaultColWidthRecord
1733 * @see org.apache.poi.hssf.record.ColumnInfoRecord
1734 * @see #setColumnHidden(short,boolean)
1735 * @return whether the column is hidden or not.
1736 */
1737
1738 public boolean isColumnHidden(short column)
1739 {
1740 boolean retval = false;
1741 ColumnInfoRecord ci = null;
1742
1743 if (columns != null)
1744 {
1745 for ( Iterator iterator = columns.getIterator(); iterator.hasNext(); )
1746 {
1747 ci = ( ColumnInfoRecord ) iterator.next();
1748 if ((ci.getFirstColumn() <= column)
1749 && (column <= ci.getLastColumn()))
1750 {
1751 break;
1752 }
1753 ci = null;
1754 }
1755 }
1756 if (ci != null)
1757 {
1758 retval = ci.getHidden();
1759 }
1760 return retval;
1761 }
1762
1763 /**
1764 * Get the hidden property for a given column.
1765 * @param column - the column number
1766 * @param hidden - whether the column is hidden or not
1767 */
1768 public void setColumnHidden(short column, boolean hidden)
1769 {
1770 setColumn( column, null, null, new Boolean(hidden), null);
1771 }
1772
1773 public void setColumn(short column, Short width, Integer level, Boolean hidden, Boolean collapsed)
1774 {
1775 if (columns == null)
1776 columns = new ColumnInfoRecordsAggregate();
1777
1778 columns.setColumn( column, null, width, level, hidden, collapsed );
1779 }
1780
1781 public void setColumn(short column, Short xfStyle, Short width, Integer level, Boolean hidden, Boolean collapsed)
1782 {
1783 if (columns == null)
1784 columns = new ColumnInfoRecordsAggregate();
1785
1786 columns.setColumn( column, xfStyle, width, level, hidden, collapsed );
1787 }
1788
1789
1790 /**
1791 * Creates an outline group for the specified columns.
1792 * @param fromColumn group from this column (inclusive)
1793 * @param toColumn group to this column (inclusive)
1794 * @param indent if true the group will be indented by one level,
1795 * if false indenting will be removed by one level.
1796 */
1797 public void groupColumnRange(short fromColumn, short toColumn, boolean indent)
1798 {
1799
1800 // Set the level for each column
1801 columns.groupColumnRange( fromColumn, toColumn, indent);
1802
1803 // Determine the maximum overall level
1804 int maxLevel = 0;
1805 int count=columns.getNumColumns();
1806 for ( int k=0;k<count;k++ )
1807 {
1808 ColumnInfoRecord columnInfoRecord = columns.getColInfo(k);
1809 maxLevel = Math.max(columnInfoRecord.getOutlineLevel(), maxLevel);
1810 }
1811
1812 GutsRecord guts = (GutsRecord) findFirstRecordBySid( GutsRecord.sid );
1813 guts.setColLevelMax( (short) ( maxLevel+1 ) );
1814 if (maxLevel == 0)
1815 guts.setTopColGutter( (short)0 );
1816 else
1817 guts.setTopColGutter( (short) ( 29 + (12 * (maxLevel-1)) ) );
1818 }
1819
1820 /**
1821 * creates the Dimensions Record and sets it to bogus values (you should set this yourself
1822 * or let the high level API do it for you)
1823 * @see org.apache.poi.hssf.record.DimensionsRecord
1824 * @see org.apache.poi.hssf.record.Record
1825 * @return record containing a DimensionsRecord
1826 */
1827
1828 protected Record createDimensions()
1829 {
1830 DimensionsRecord retval = new DimensionsRecord();
1831
1832 retval.setFirstCol(( short ) 0);
1833 retval.setLastRow(1); // one more than it is
1834 retval.setFirstRow(0);
1835 retval.setLastCol(( short ) 1); // one more than it is
1836 return retval;
1837 }
1838
1839 /**
1840 * creates the WindowTwo Record and sets it to: <P>
1841 * options = 0x6b6 <P>
1842 * toprow = 0 <P>
1843 * leftcol = 0 <P>
1844 * headercolor = 0x40 <P>
1845 * pagebreakzoom = 0x0 <P>
1846 * normalzoom = 0x0 <p>
1847 * @see org.apache.poi.hssf.record.WindowTwoRecord
1848 * @see org.apache.poi.hssf.record.Record
1849 * @return record containing a WindowTwoRecord
1850 */
1851
1852 protected WindowTwoRecord createWindowTwo()
1853 {
1854 WindowTwoRecord retval = new WindowTwoRecord();
1855
1856 retval.setOptions(( short ) 0x6b6);
1857 retval.setTopRow(( short ) 0);
1858 retval.setLeftCol(( short ) 0);
1859 retval.setHeaderColor(0x40);
1860 retval.setPageBreakZoom(( short ) 0);
1861 retval.setNormalZoom(( short ) 0);
1862 return retval;
1863 }
1864
1865 /**
1866 * Creates the Selection record and sets it to nothing selected
1867 *
1868 * @see org.apache.poi.hssf.record.SelectionRecord
1869 * @see org.apache.poi.hssf.record.Record
1870 * @return record containing a SelectionRecord
1871 */
1872
1873 protected Record createSelection()
1874 {
1875 SelectionRecord retval = new SelectionRecord();
1876
1877 retval.setPane(( byte ) 0x3);
1878 retval.setActiveCellCol(( short ) 0x0);
1879 retval.setActiveCellRow(( short ) 0x0);
1880 retval.setNumRefs(( short ) 0x0);
1881 return retval;
1882 }
1883
1884 public short getTopRow()
1885 {
1886 return (windowTwo==null) ? (short) 0 : windowTwo.getTopRow();
1887 }
1888
1889 public void setTopRow(short topRow)
1890 {
1891 if (windowTwo!=null)
1892 {
1893 windowTwo.setTopRow(topRow);
1894 }
1895 }
1896
1897 /**
1898 * Sets the left column to show in desktop window pane.
1899 * @param leftCol the left column to show in desktop window pane
1900 */
1901 public void setLeftCol(short leftCol){
1902 if (windowTwo!=null)
1903 {
1904 windowTwo.setLeftCol(leftCol);
1905 }
1906 }
1907
1908 public short getLeftCol()
1909 {
1910 return (windowTwo==null) ? (short) 0 : windowTwo.getLeftCol();
1911 }
1912
1913
1914
1915 /**
1916 * Returns the active row
1917 *
1918 * @see org.apache.poi.hssf.record.SelectionRecord
1919 * @return row the active row index
1920 */
1921 public int getActiveCellRow()
1922 {
1923 if (selection == null)
1924 {
1925 return 0;
1926 }
1927 return selection.getActiveCellRow();
1928 }
1929
1930 /**
1931 * Sets the active row
1932 *
1933 * @param row the row index
1934 * @see org.apache.poi.hssf.record.SelectionRecord
1935 */
1936 public void setActiveCellRow(int row)
1937 {
1938 //shouldn't have a sheet w/o a SelectionRecord, but best to guard anyway
1939 if (selection != null)
1940 {
1941 selection.setActiveCellRow(row);
1942 }
1943 }
1944
1945 /**
1946 * Returns the active column
1947 *
1948 * @see org.apache.poi.hssf.record.SelectionRecord
1949 * @return row the active column index
1950 */
1951 public short getActiveCellCol()
1952 {
1953 if (selection == null)
1954 {
1955 return (short) 0;
1956 }
1957 return selection.getActiveCellCol();
1958 }
1959
1960 /**
1961 * Sets the active column
1962 *
1963 * @param col the column index
1964 * @see org.apache.poi.hssf.record.SelectionRecord
1965 */
1966 public void setActiveCellCol(short col)
1967 {
1968 //shouldn't have a sheet w/o a SelectionRecord, but best to guard anyway
1969 if (selection != null)
1970 {
1971 selection.setActiveCellCol(col);
1972 }
1973 }
1974
1975 protected Record createMergedCells()
1976 {
1977 MergeCellsRecord retval = new MergeCellsRecord();
1978 retval.setNumAreas(( short ) 0);
1979 return retval;
1980 }
1981
1982 /**
1983 * creates the EOF record
1984 * @see org.apache.poi.hssf.record.EOFRecord
1985 * @see org.apache.poi.hssf.record.Record
1986 * @return record containing a EOFRecord
1987 */
1988
1989 protected Record createEOF()
1990 {
1991 return new EOFRecord();
1992 }
1993
1994 /**
1995 * get the location of the DimensionsRecord (which is the last record before the value section)
1996 * @return location in the array of records of the DimensionsRecord
1997 */
1998
1999 public int getDimsLoc()
2000 {
2001 if (log.check( POILogger.DEBUG ))
2002 log.log(POILogger.DEBUG, "getDimsLoc dimsloc= " + dimsloc);
2003 return dimsloc;
2004 }
2005
2006 /**
2007 * in the event the record is a dimensions record, resets both the loc index and dimsloc index
2008 */
2009
2010 public void checkDimsLoc(Record rec, int recloc)
2011 {
2012 if (rec.getSid() == DimensionsRecord.sid)
2013 {
2014 loc = recloc;
2015 dimsloc = recloc;
2016 }
2017 }
2018
2019 public int getSize()
2020 {
2021 int retval = 0;
2022
2023 for ( int k = 0; k < records.size(); k++) {
2024 Record record = (Record) records.get(k);
2025 if (record instanceof UncalcedRecord) {
2026 // skip the UncalcedRecord if present, it's only encoded if the isUncalced flag is set
2027 continue;
2028 }
2029 retval += record.getRecordSize();
2030 }
2031 if (rows != null) {
2032 // Add space for the IndexRecord and DBCell records
2033 final int nBlocks = rows.getRowBlockCount();
2034 int nRows = 0;
2035 if (cells != null) {
2036 for (Iterator itr = rows.getIterator(); itr.hasNext();) {
2037 RowRecord row = (RowRecord)itr.next();
2038 if (cells.rowHasCells(row.getRowNumber())) {
2039 nRows++;
2040 }
2041 }
2042 }
2043 retval += IndexRecord.getRecordSizeForBlockCount(nBlocks);
2044 retval += DBCellRecord.calculateSizeOfRecords(nBlocks, nRows);
2045 }
2046 // Add space for UncalcedRecord
2047 if (_isUncalced) {
2048 retval += UncalcedRecord.getStaticRecordSize();
2049 }
2050 return retval;
2051 }
2052
2053 public List getRecords()
2054 {
2055 return records;
2056 }
2057
2058 /**
2059 * Gets the gridset record for this sheet.
2060 */
2061
2062 public GridsetRecord getGridsetRecord()
2063 {
2064 return gridset;
2065 }
2066
2067 /**
2068 * Returns the first occurance of a record matching a particular sid.
2069 */
2070
2071 public Record findFirstRecordBySid(short sid)
2072 {
2073 for (Iterator iterator = records.iterator(); iterator.hasNext(); )
2074 {
2075 Record record = ( Record ) iterator.next();
2076
2077 if (record.getSid() == sid)
2078 {
2079 return record;
2080 }
2081 }
2082 return null;
2083 }
2084
2085 /**
2086 * Sets the SCL record or creates it in the correct place if it does not
2087 * already exist.
2088 *
2089 * @param sclRecord The record to set.
2090 */
2091 public void setSCLRecord(SCLRecord sclRecord)
2092 {
2093 int oldRecordLoc = findFirstRecordLocBySid(SCLRecord.sid);
2094 if (oldRecordLoc == -1)
2095 {
2096 // Insert it after the window record
2097 int windowRecordLoc = findFirstRecordLocBySid(WindowTwoRecord.sid);
2098 records.add(windowRecordLoc+1, sclRecord);
2099 }
2100 else
2101 {
2102 records.set(oldRecordLoc, sclRecord);
2103 }
2104
2105 }
2106
2107 /**
2108 * Finds the first occurance of a record matching a particular sid and
2109 * returns it's position.
2110 * @param sid the sid to search for
2111 * @return the record position of the matching record or -1 if no match
2112 * is made.
2113 */
2114 public int findFirstRecordLocBySid( short sid )
2115 {
2116 int index = 0;
2117 for (Iterator iterator = records.iterator(); iterator.hasNext(); )
2118 {
2119 Record record = ( Record ) iterator.next();
2120
2121 if (record.getSid() == sid)
2122 {
2123 return index;
2124 }
2125 index++;
2126 }
2127 return -1;
2128 }
2129
2130 /**
2131 * Returns the HeaderRecord.
2132 * @return HeaderRecord for the sheet.
2133 */
2134 public HeaderRecord getHeader ()
2135 {
2136 return header;
2137 }
2138
2139 public WindowTwoRecord getWindowTwo() {
2140 return windowTwo;
2141 }
2142 /**
2143 * Sets the HeaderRecord.
2144 * @param newHeader The new HeaderRecord for the sheet.
2145 */
2146 public void setHeader (HeaderRecord newHeader)
2147 {
2148 header = newHeader;
2149 }
2150
2151 /**
2152 * Returns the FooterRecord.
2153 * @return FooterRecord for the sheet.
2154 */
2155 public FooterRecord getFooter ()
2156 {
2157 return footer;
2158 }
2159
2160 /**
2161 * Sets the FooterRecord.
2162 * @param newFooter The new FooterRecord for the sheet.
2163 */
2164 public void setFooter (FooterRecord newFooter)
2165 {
2166 footer = newFooter;
2167 }
2168
2169 /**
2170 * Returns the PrintSetupRecord.
2171 * @return PrintSetupRecord for the sheet.
2172 */
2173 public PrintSetupRecord getPrintSetup ()
2174 {
2175 return printSetup;
2176 }
2177
2178 /**
2179 * Sets the PrintSetupRecord.
2180 * @param newPrintSetup The new PrintSetupRecord for the sheet.
2181 */
2182 public void setPrintSetup (PrintSetupRecord newPrintSetup)
2183 {
2184 printSetup = newPrintSetup;
2185 }
2186
2187 /**
2188 * Returns the PrintGridlinesRecord.
2189 * @return PrintGridlinesRecord for the sheet.
2190 */
2191 public PrintGridlinesRecord getPrintGridlines ()
2192 {
2193 return printGridlines;
2194 }
2195
2196 /**
2197 * Sets the PrintGridlinesRecord.
2198 * @param newPrintGridlines The new PrintGridlinesRecord for the sheet.
2199 */
2200 public void setPrintGridlines (PrintGridlinesRecord newPrintGridlines)
2201 {
2202 printGridlines = newPrintGridlines;
2203 }
2204
2205 /**
2206 * Sets whether the sheet is selected
2207 * @param sel True to select the sheet, false otherwise.
2208 */
2209 public void setSelected(boolean sel) {
2210 windowTwo.setSelected(sel);
2211 }
2212
2213 /**
2214 * Gets the size of the margin in inches.
2215 * @param margin which margin to get
2216 * @return the size of the margin
2217 */
2218 public double getMargin(short margin) {
2219 if (getMargins()[margin] != null)
2220 return margins[margin].getMargin();
2221 else {
2222 switch ( margin )
2223 {
2224 case LeftMargin:
2225 return .75;
2226 case RightMargin:
2227 return .75;
2228 case TopMargin:
2229 return 1.0;
2230 case BottomMargin:
2231 return 1.0;
2232 default :
2233 throw new RuntimeException( "Unknown margin constant: " + margin );
2234 }
2235 }
2236 }
2237
2238 /**
2239 * Sets the size of the margin in inches.
2240 * @param margin which margin to get
2241 * @param size the size of the margin
2242 */
2243 public void setMargin(short margin, double size) {
2244 Margin m = getMargins()[margin];
2245 if (m == null) {
2246 switch ( margin )
2247 {
2248 case LeftMargin:
2249 m = new LeftMarginRecord();
2250 records.add( getDimsLoc() + 1, m );
2251 break;
2252 case RightMargin:
2253 m = new RightMarginRecord();
2254 records.add( getDimsLoc() + 1, m );
2255 break;
2256 case TopMargin:
2257 m = new TopMarginRecord();
2258 records.add( getDimsLoc() + 1, m );
2259 break;
2260 case BottomMargin:
2261 m = new BottomMarginRecord();
2262 records.add( getDimsLoc() + 1, m );
2263 break;
2264 default :
2265 throw new RuntimeException( "Unknown margin constant: " + margin );
2266 }
2267 margins[margin] = m;
2268 }
2269 m.setMargin( size );
2270 }
2271
2272 public int getEofLoc()
2273 {
2274 return eofLoc;
2275 }
2276
2277 /**
2278 * Creates a split (freezepane). Any existing freezepane or split pane is overwritten.
2279 * @param colSplit Horizonatal position of split.
2280 * @param rowSplit Vertical position of split.
2281 * @param topRow Top row visible in bottom pane
2282 * @param leftmostColumn Left column visible in right pane.
2283 */
2284 public void createFreezePane(int colSplit, int rowSplit, int topRow, int leftmostColumn )
2285 {
2286 int paneLoc = findFirstRecordLocBySid(PaneRecord.sid);
2287 if (paneLoc != -1)
2288 records.remove(paneLoc);
2289
2290 int loc = findFirstRecordLocBySid(WindowTwoRecord.sid);
2291 PaneRecord pane = new PaneRecord();
2292 pane.setX((short)colSplit);
2293 pane.setY((short)rowSplit);
2294 pane.setTopRow((short) topRow);
2295 pane.setLeftColumn((short) leftmostColumn);
2296 if (rowSplit == 0)
2297 {
2298 pane.setTopRow((short)0);
2299 pane.setActivePane((short)1);
2300 }
2301 else if (colSplit == 0)
2302 {
2303 pane.setLeftColumn((short)64);
2304 pane.setActivePane((short)2);
2305 }
2306 else
2307 {
2308 pane.setActivePane((short)0);
2309 }
2310 records.add(loc+1, pane);
2311
2312 windowTwo.setFreezePanes(true);
2313 windowTwo.setFreezePanesNoSplit(true);
2314
2315 SelectionRecord sel = (SelectionRecord) findFirstRecordBySid(SelectionRecord.sid);
2316 sel.setPane((byte)pane.getActivePane());
2317
2318 }
2319
2320 /**
2321 * Creates a split pane. Any existing freezepane or split pane is overwritten.
2322 * @param xSplitPos Horizonatal position of split (in 1/20th of a point).
2323 * @param ySplitPos Vertical position of split (in 1/20th of a point).
2324 * @param topRow Top row visible in bottom pane
2325 * @param leftmostColumn Left column visible in right pane.
2326 * @param activePane Active pane. One of: PANE_LOWER_RIGHT,
2327 * PANE_UPPER_RIGHT, PANE_LOWER_LEFT, PANE_UPPER_LEFT
2328 * @see #PANE_LOWER_LEFT
2329 * @see #PANE_LOWER_RIGHT
2330 * @see #PANE_UPPER_LEFT
2331 * @see #PANE_UPPER_RIGHT
2332 */
2333 public void createSplitPane(int xSplitPos, int ySplitPos, int topRow, int leftmostColumn, int activePane )
2334 {
2335 int paneLoc = findFirstRecordLocBySid(PaneRecord.sid);
2336 if (paneLoc != -1)
2337 records.remove(paneLoc);
2338
2339 int loc = findFirstRecordLocBySid(WindowTwoRecord.sid);
2340 PaneRecord r = new PaneRecord();
2341 r.setX((short)xSplitPos);
2342 r.setY((short)ySplitPos);
2343 r.setTopRow((short) topRow);
2344 r.setLeftColumn((short) leftmostColumn);
2345 r.setActivePane((short) activePane);
2346 records.add(loc+1, r);
2347
2348 windowTwo.setFreezePanes(false);
2349 windowTwo.setFreezePanesNoSplit(false);
2350
2351 SelectionRecord sel = (SelectionRecord) findFirstRecordBySid(SelectionRecord.sid);
2352 sel.setPane(PANE_LOWER_RIGHT);
2353
2354 }
2355
2356 /**
2357 * Returns the information regarding the currently configured pane (split or freeze).
2358 * @return null if no pane configured, or the pane information.
2359 */
2360 public PaneInformation getPaneInformation() {
2361 PaneRecord rec = (PaneRecord)findFirstRecordBySid(PaneRecord.sid);
2362 if (rec == null)
2363 return null;
2364
2365 return new PaneInformation(rec.getX(), rec.getY(), rec.getTopRow(),
2366 rec.getLeftColumn(), (byte)rec.getActivePane(), windowTwo.getFreezePanes());
2367 }
2368
2369 public SelectionRecord getSelection()
2370 {
2371 return selection;
2372 }
2373
2374 public void setSelection( SelectionRecord selection )
2375 {
2376 this.selection = selection;
2377 }
2378
2379 /**
2380 * creates a Protect record with protect set to false.
2381 * @see org.apache.poi.hssf.record.ProtectRecord
2382 * @see org.apache.poi.hssf.record.Record
2383 * @return a ProtectRecord
2384 */
2385 protected Record createProtect()
2386 {
2387 if (log.check( POILogger.DEBUG ))
2388 log.log(POILogger.DEBUG, "create protect record with protection disabled");
2389 ProtectRecord retval = new ProtectRecord();
2390
2391 retval.setProtect(false);
2392 return retval;
2393 }
2394
2395 /**
2396 * creates an ObjectProtect record with protect set to false.
2397 * @see org.apache.poi.hssf.record.ObjectProtectRecord
2398 * @see org.apache.poi.hssf.record.Record
2399 * @return an ObjectProtectRecord
2400 */
2401 protected ObjectProtectRecord createObjectProtect()
2402 {
2403 if (log.check( POILogger.DEBUG ))
2404 log.log(POILogger.DEBUG, "create protect record with protection disabled");
2405 ObjectProtectRecord retval = new ObjectProtectRecord();
2406
2407 retval.setProtect(false);
2408 return retval;
2409 }
2410
2411 /**
2412 * creates a ScenarioProtect record with protect set to false.
2413 * @see org.apache.poi.hssf.record.ScenarioProtectRecord
2414 * @see org.apache.poi.hssf.record.Record
2415 * @return a ScenarioProtectRecord
2416 */
2417 protected ScenarioProtectRecord createScenarioProtect()
2418 {
2419 if (log.check( POILogger.DEBUG ))
2420 log.log(POILogger.DEBUG, "create protect record with protection disabled");
2421 ScenarioProtectRecord retval = new ScenarioProtectRecord();
2422
2423 retval.setProtect(false);
2424 return retval;
2425 }
2426
2427 /** Returns the ProtectRecord.
2428 * If one is not contained in the sheet, then one is created.
2429 */
2430 public ProtectRecord getProtect()
2431 {
2432 if (protect == null) {
2433 protect = (ProtectRecord)createProtect();
2434 //Insert the newlycreated protect record at the end of the record (just before the EOF)
2435 int loc = findFirstRecordLocBySid(EOFRecord.sid);
2436 records.add(loc, protect);
2437 }
2438 return protect;
2439 }
2440
2441 /** Returns the PasswordRecord.
2442 * If one is not contained in the sheet, then one is created.
2443 */
2444 public PasswordRecord getPassword()
2445 {
2446 if (password == null) {
2447 password = createPassword();
2448 //Insert the newly created password record at the end of the record (just before the EOF)
2449 int loc = findFirstRecordLocBySid(EOFRecord.sid);
2450 records.add(loc, password);
2451 }
2452 return password;
2453 }
2454
2455 /**
2456 * creates a Password record with password set to 00.
2457 * @see org.apache.poi.hssf.record.PasswordRecord
2458 * @see org.apache.poi.hssf.record.Record
2459 * @return a PasswordRecord
2460 */
2461 protected PasswordRecord createPassword()
2462 {
2463 if (log.check( POILogger.DEBUG ))
2464 log.log(POILogger.DEBUG, "create password record with 00 password");
2465 PasswordRecord retval = new PasswordRecord();
2466
2467 retval.setPassword((short)00);
2468 return retval;
2469 }
2470
2471 /**
2472
2473 /**
2474 * Sets whether the gridlines are shown in a viewer.
2475 * @param show whether to show gridlines or not
2476 */
2477 public void setDisplayGridlines(boolean show) {
2478 windowTwo.setDisplayGridlines(show);
2479 }
2480
2481 /**
2482 * Returns if gridlines are displayed.
2483 * @return whether gridlines are displayed
2484 */
2485 public boolean isDisplayGridlines() {
2486 return windowTwo.getDisplayGridlines();
2487 }
2488
2489 /**
2490 * Sets whether the formulas are shown in a viewer.
2491 * @param show whether to show formulas or not
2492 */
2493 public void setDisplayFormulas(boolean show) {
2494 windowTwo.setDisplayFormulas(show);
2495 }
2496
2497 /**
2498 * Returns if formulas are displayed.
2499 * @return whether formulas are displayed
2500 */
2501 public boolean isDisplayFormulas() {
2502 return windowTwo.getDisplayFormulas();
2503 }
2504
2505 /**
2506 * Sets whether the RowColHeadings are shown in a viewer.
2507 * @param show whether to show RowColHeadings or not
2508 */
2509 public void setDisplayRowColHeadings(boolean show) {
2510 windowTwo.setDisplayRowColHeadings(show);
2511 }
2512
2513 /**
2514 * Returns if RowColHeadings are displayed.
2515 * @return whether RowColHeadings are displayed
2516 */
2517 public boolean isDisplayRowColHeadings() {
2518 return windowTwo.getDisplayRowColHeadings();
2519 }
2520
2521
2522 /**
2523 * @return whether an uncalced record must be inserted or not at generation
2524 */
2525 public boolean getUncalced() {
2526 return _isUncalced;
2527 }
2528 /**
2529 * @param uncalced whether an uncalced record must be inserted or not at generation
2530 */
2531 public void setUncalced(boolean uncalced) {
2532 this._isUncalced = uncalced;
2533 }
2534
2535 /**
2536 * Returns the array of margins. If not created, will create.
2537 *
2538 * @return the array of marings.
2539 */
2540 protected Margin[] getMargins() {
2541 if (margins == null)
2542 margins = new Margin[4];
2543 return margins;
2544 }
2545
2546 /**
2547 * Finds the DrawingRecord for our sheet, and
2548 * attaches it to the DrawingManager (which knows about
2549 * the overall DrawingGroup for our workbook).
2550 * If requested, will create a new DrawRecord
2551 * if none currently exist
2552 * @param drawingManager The DrawingManager2 for our workbook
2553 * @param createIfMissing Should one be created if missing?
2554 */
2555 public int aggregateDrawingRecords(DrawingManager2 drawingManager, boolean createIfMissing)
2556 {
2557 int loc = findFirstRecordLocBySid(DrawingRecord.sid);
2558 boolean noDrawingRecordsFound = (loc == -1);
2559 if (noDrawingRecordsFound)
2560 {
2561 if(!createIfMissing) {
2562 // None found, and not allowed to add in
2563 return -1;
2564 }
2565
2566 EscherAggregate aggregate = new EscherAggregate( drawingManager );
2567 loc = findFirstRecordLocBySid(EscherAggregate.sid);
2568 if (loc == -1)
2569 {
2570 loc = findFirstRecordLocBySid( WindowTwoRecord.sid );
2571 }
2572 else
2573 {
2574 getRecords().remove(loc);
2575 }
2576 getRecords().add( loc, aggregate );
2577 return loc;
2578 }
2579 else
2580 {
2581 List records = getRecords();
2582 EscherAggregate r = EscherAggregate.createAggregate( records, loc, drawingManager );
2583 int startloc = loc;
2584 while ( loc + 1 < records.size()
2585 && records.get( loc ) instanceof DrawingRecord
2586 && records.get( loc + 1 ) instanceof ObjRecord )
2587 {
2588 loc += 2;
2589 }
2590 int endloc = loc-1;
2591 for(int i = 0; i < (endloc - startloc + 1); i++)
2592 records.remove(startloc);
2593 records.add(startloc, r);
2594
2595 return startloc;
2596 }
2597 }
2598
2599 /**
2600 * Perform any work necessary before the sheet is about to be serialized.
2601 * For instance the escher aggregates size needs to be calculated before
2602 * serialization so that the dgg record (which occurs first) can be written.
2603 */
2604 public void preSerialize()
2605 {
2606 for ( Iterator iterator = getRecords().iterator(); iterator.hasNext(); )
2607 {
2608 Record r = (Record) iterator.next();
2609 if (r instanceof EscherAggregate)
2610 r.getRecordSize(); // Trigger flatterning of user model and corresponding update of dgg record.
2611 }
2612 }
2613
2614 /**
2615 * Shifts all the page breaks in the range "count" number of rows/columns
2616 * @param breaks The page record to be shifted
2617 * @param start Starting "main" value to shift breaks
2618 * @param stop Ending "main" value to shift breaks
2619 * @param count number of units (rows/columns) to shift by
2620 */
2621 public void shiftBreaks(PageBreakRecord breaks, short start, short stop, int count) {
2622
2623 if(rowBreaks == null)
2624 return;
2625 Iterator iterator = breaks.getBreaksIterator();
2626 List shiftedBreak = new ArrayList();
2627 while(iterator.hasNext())
2628 {
2629 PageBreakRecord.Break breakItem = (PageBreakRecord.Break)iterator.next();
2630 short breakLocation = breakItem.main;
2631 boolean inStart = (breakLocation >= start);
2632 boolean inEnd = (breakLocation <= stop);
2633 if(inStart && inEnd)
2634 shiftedBreak.add(breakItem);
2635 }
2636
2637 iterator = shiftedBreak.iterator();
2638 while (iterator.hasNext()) {
2639 PageBreakRecord.Break breakItem = (PageBreakRecord.Break)iterator.next();
2640 breaks.removeBreak(breakItem.main);
2641 breaks.addBreak((short)(breakItem.main+count), breakItem.subFrom, breakItem.subTo);
2642 }
2643 }
2644
2645 /**
2646 * Sets a page break at the indicated row
2647 * @param row
2648 */
2649 public void setRowBreak(int row, short fromCol, short toCol) {
2650 if (rowBreaks == null) {
2651 int loc = findFirstRecordLocBySid(WindowTwoRecord.sid);
2652 rowBreaks = new PageBreakRecord(PageBreakRecord.HORIZONTAL_SID);
2653 records.add(loc, rowBreaks);
2654 }
2655 rowBreaks.addBreak((short)row, fromCol, toCol);
2656 }
2657
2658 /**
2659 * Removes a page break at the indicated row
2660 * @param row
2661 */
2662 public void removeRowBreak(int row) {
2663 if (rowBreaks == null)
2664 throw new IllegalArgumentException("Sheet does not define any row breaks");
2665 rowBreaks.removeBreak((short)row);
2666 }
2667
2668 /**
2669 * Queries if the specified row has a page break
2670 * @param row
2671 * @return true if the specified row has a page break
2672 */
2673 public boolean isRowBroken(int row) {
2674 return (rowBreaks == null) ? false : rowBreaks.getBreak((short)row) != null;
2675 }
2676
2677 /**
2678 * Sets a page break at the indicated column
2679 *
2680 */
2681 public void setColumnBreak(short column, short fromRow, short toRow) {
2682 if (colBreaks == null) {
2683 int loc = findFirstRecordLocBySid(WindowTwoRecord.sid);
2684 colBreaks = new PageBreakRecord(PageBreakRecord.VERTICAL_SID);
2685 records.add(loc, colBreaks);
2686 }
2687 colBreaks.addBreak(column, fromRow, toRow);
2688 }
2689
2690 /**
2691 * Removes a page break at the indicated column
2692 *
2693 */
2694 public void removeColumnBreak(short column) {
2695 if (colBreaks == null)
2696 throw new IllegalArgumentException("Sheet does not define any column breaks");
2697
2698 colBreaks.removeBreak(column);
2699 }
2700
2701 /**
2702 * Queries if the specified column has a page break
2703 *
2704 * @return true if the specified column has a page break
2705 */
2706 public boolean isColumnBroken(short column) {
2707 return (colBreaks == null) ? false : colBreaks.getBreak(column) != null;
2708 }
2709
2710 /**
2711 * Shifts the horizontal page breaks for the indicated count
2712 * @param startingRow
2713 * @param endingRow
2714 * @param count
2715 */
2716 public void shiftRowBreaks(int startingRow, int endingRow, int count) {
2717 shiftBreaks(rowBreaks, (short)startingRow, (short)endingRow, (short)count);
2718 }
2719
2720 /**
2721 * Shifts the vertical page breaks for the indicated count
2722 * @param startingCol
2723 * @param endingCol
2724 * @param count
2725 */
2726 public void shiftColumnBreaks(short startingCol, short endingCol, short count) {
2727 shiftBreaks(colBreaks, startingCol, endingCol, count);
2728 }
2729
2730 /**
2731 * Returns all the row page breaks
2732 * @return all the row page breaks
2733 */
2734 public Iterator getRowBreaks() {
2735 return rowBreaks.getBreaksIterator();
2736 }
2737
2738 /**
2739 * Returns the number of row page breaks
2740 * @return the number of row page breaks
2741 */
2742 public int getNumRowBreaks(){
2743 return (rowBreaks == null) ? 0 : (int)rowBreaks.getNumBreaks();
2744 }
2745
2746 /**
2747 * Returns all the column page breaks
2748 * @return all the column page breaks
2749 */
2750 public Iterator getColumnBreaks(){
2751 return colBreaks.getBreaksIterator();
2752 }
2753
2754 /**
2755 * Returns the number of column page breaks
2756 * @return the number of column page breaks
2757 */
2758 public int getNumColumnBreaks(){
2759 return (colBreaks == null) ? 0 : (int)colBreaks.getNumBreaks();
2760 }
2761
2762 public void setColumnGroupCollapsed( short columnNumber, boolean collapsed )
2763 {
2764 if (collapsed)
2765 {
2766 columns.collapseColumn( columnNumber );
2767 }
2768 else
2769 {
2770 columns.expandColumn( columnNumber );
2771 }
2772 }
2773
2774 /**
2775 * protect a spreadsheet with a password (not encypted, just sets protect
2776 * flags and the password.
2777 * @param password to set
2778 * @param objects are protected
2779 * @param scenarios are protected
2780 */
2781 public void protectSheet( String password, boolean objects, boolean scenarios ) {
2782 int protIdx = -1;
2783 ProtectRecord prec = getProtect();
2784 PasswordRecord pass = getPassword();
2785 prec.setProtect(true);
2786 pass.setPassword(PasswordRecord.hashPassword(password));
2787 if((objprotect == null && objects) || (scenprotect != null && scenarios)) {
2788 protIdx = records.indexOf( protect );
2789 }
2790 if(objprotect == null && objects) {
2791 ObjectProtectRecord rec = createObjectProtect();
2792 rec.setProtect(true);
2793 records.add(protIdx+1,rec);
2794 objprotect = rec;
2795 }
2796 if(scenprotect == null && scenarios) {
2797 ScenarioProtectRecord srec = createScenarioProtect();
2798 srec.setProtect(true);
2799 records.add(protIdx+2,srec);
2800 scenprotect = srec;
2801 }
2802 }
2803
2804 /**
2805 * unprotect objects in the sheet (will not protect them, but any set to false are
2806 * unprotected.
2807 * @param sheet is unprotected (false = unprotect)
2808 * @param objects are unprotected (false = unprotect)
2809 * @param scenarios are unprotected (false = unprotect)
2810 */
2811 public void unprotectSheet( boolean sheet, boolean objects, boolean scenarios ) {
2812
2813 if (!sheet) {
2814 ProtectRecord prec = getProtect();
2815 prec.setProtect(sheet);
2816 PasswordRecord pass = getPassword();
2817 pass.setPassword((short)00);
2818 }
2819 if(objprotect != null && !objects) {
2820 objprotect.setProtect(false);
2821 }
2822 if(scenprotect != null && !scenarios) {
2823 scenprotect.setProtect(false);
2824 }
2825 }
2826
2827 /**
2828 * @return {sheet is protected, objects are proteced, scenarios are protected}
2829 */
2830 public boolean[] isProtected() {
2831 return new boolean[] { (protect != null && protect.getProtect()),
2832 (objprotect != null && objprotect.getProtect()),
2833 (scenprotect != null && scenprotect.getProtect())};
2834 }
2835
2836
2837 public void groupRowRange(int fromRow, int toRow, boolean indent)
2838 {
2839 checkRows();
2840 for (int rowNum = fromRow; rowNum <= toRow; rowNum++)
2841 {
2842 RowRecord row = getRow( rowNum );
2843 if (row == null)
2844 {
2845 row = createRow( rowNum );
2846 addRow( row );
2847 }
2848 int level = row.getOutlineLevel();
2849 if (indent) level++; else level--;
2850 level = Math.max(0, level);
2851 level = Math.min(7, level);
2852 row.setOutlineLevel((short) ( level ));
2853 }
2854
2855 recalcRowGutter();
2856 }
2857
2858 private void recalcRowGutter()
2859 {
2860 int maxLevel = 0;
2861 Iterator iterator = rows.getIterator();
2862 while ( iterator.hasNext() )
2863 {
2864 RowRecord rowRecord = (RowRecord) iterator.next();
2865 maxLevel = Math.max(rowRecord.getOutlineLevel(), maxLevel);
2866 }
2867
2868 // Grab the guts record, adding if needed
2869 GutsRecord guts = (GutsRecord) findFirstRecordBySid( GutsRecord.sid );
2870 if(guts == null) {
2871 guts = new GutsRecord();
2872 records.add(guts);
2873 }
2874 // Set the levels onto it
2875 guts.setRowLevelMax( (short) ( maxLevel + 1 ) );
2876 guts.setLeftRowGutter( (short) ( 29 + (12 * (maxLevel)) ) );
2877 }
2878
2879 public void setRowGroupCollapsed( int row, boolean collapse )
2880 {
2881 if (collapse)
2882 {
2883 rows.collapseRow( row );
2884 }
2885 else
2886 {
2887 rows.expandRow( row );
2888 }
2889 }
2890 }