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 java.util.ArrayList;
|
---|
21 | import java.util.LinkedHashMap;
|
---|
22 | import java.util.List;
|
---|
23 | import java.util.Map;
|
---|
24 |
|
---|
25 | import org.apache.poi.hssf.record.BoundSheetRecord;
|
---|
26 | import org.apache.poi.hssf.record.DateWindow1904Record;
|
---|
27 | import org.apache.poi.hssf.record.EOFRecord;
|
---|
28 | import org.apache.poi.hssf.record.ExtSSTRecord;
|
---|
29 | import org.apache.poi.hssf.record.ExtendedFormatRecord;
|
---|
30 | import org.apache.poi.hssf.record.ExternSheetRecord;
|
---|
31 | import org.apache.poi.hssf.record.FormatRecord;
|
---|
32 | import org.apache.poi.hssf.record.HyperlinkRecord;
|
---|
33 | import org.apache.poi.hssf.record.NameCommentRecord;
|
---|
34 | import org.apache.poi.hssf.record.NameRecord;
|
---|
35 | import org.apache.poi.hssf.record.Record;
|
---|
36 | import org.apache.poi.hssf.record.SSTRecord;
|
---|
37 | import org.apache.poi.hssf.record.SupBookRecord;
|
---|
38 | import org.apache.poi.hssf.record.TabIdRecord;
|
---|
39 | import org.apache.poi.hssf.record.WindowOneRecord;
|
---|
40 | import org.apache.poi.hssf.record.common.UnicodeString;
|
---|
41 | import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheet;
|
---|
42 | import org.apache.poi.util.Internal;
|
---|
43 | import org.apache.poi.util.POILogFactory;
|
---|
44 | import org.apache.poi.util.POILogger;
|
---|
45 |
|
---|
46 | /**
|
---|
47 | * Low level model implementation of a Workbook. Provides creational methods
|
---|
48 | * for settings and objects contained in the workbook object.
|
---|
49 | * <P>
|
---|
50 | * This file contains the low level binary records starting at the workbook's BOF and
|
---|
51 | * ending with the workbook's EOF. Use HSSFWorkbook for a high level representation.
|
---|
52 | * <P>
|
---|
53 | * The structures of the highlevel API use references to this to perform most of their
|
---|
54 | * operations. Its probably unwise to use these low level structures directly unless you
|
---|
55 | * really know what you're doing. I recommend you read the Microsoft Excel 97 Developer's
|
---|
56 | * Kit (Microsoft Press) and the documentation at http://sc.openoffice.org/excelfileformat.pdf
|
---|
57 | * before even attempting to use this.
|
---|
58 | *
|
---|
59 | *
|
---|
60 | * @author Luc Girardin (luc dot girardin at macrofocus dot com)
|
---|
61 | * @author Sergei Kozello (sergeikozello at mail.ru)
|
---|
62 | * @author Shawn Laubach (slaubach at apache dot org) (Data Formats)
|
---|
63 | * @author Andrew C. Oliver (acoliver at apache dot org)
|
---|
64 | * @author Brian Sanders (bsanders at risklabs dot com) - custom palette
|
---|
65 | * @author Dan Sherman (dsherman at isisph.com)
|
---|
66 | * @author Glen Stampoultzis (glens at apache.org)
|
---|
67 | * @see org.apache.poi.hssf.usermodel.HSSFWorkbook
|
---|
68 | */
|
---|
69 | @Internal
|
---|
70 | public final class InternalWorkbook {
|
---|
71 |
|
---|
72 |
|
---|
73 | private static final POILogger log = POILogFactory.getLogger(InternalWorkbook.class);
|
---|
74 | private static final int DEBUG = POILogger.DEBUG;
|
---|
75 |
|
---|
76 | /**
|
---|
77 | * this contains the Worksheet record objects
|
---|
78 | */
|
---|
79 | private final WorkbookRecordList records;
|
---|
80 |
|
---|
81 | /**
|
---|
82 | * this contains a reference to the SSTRecord so that new stings can be added
|
---|
83 | * to it.
|
---|
84 | */
|
---|
85 | protected SSTRecord sst;
|
---|
86 |
|
---|
87 |
|
---|
88 | private LinkTable linkTable; // optionally occurs if there are references in the document. (4.10.3)
|
---|
89 |
|
---|
90 | /**
|
---|
91 | * holds the "boundsheet" records (aka bundlesheet) so that they can have their
|
---|
92 | * reference to their "BOF" marker
|
---|
93 | */
|
---|
94 | private final List<BoundSheetRecord> boundsheets;
|
---|
95 | private final List<FormatRecord> formats;
|
---|
96 | private final List<HyperlinkRecord> hyperlinks;
|
---|
97 |
|
---|
98 | /** the number of extended format records */
|
---|
99 | private int numxfs;
|
---|
100 | /** holds the max format id */
|
---|
101 | private int maxformatid;
|
---|
102 | /** whether 1904 date windowing is being used */
|
---|
103 | private boolean uses1904datewindowing;
|
---|
104 | private WindowOneRecord windowOne;
|
---|
105 |
|
---|
106 | /**
|
---|
107 | * Hold the {@link NameCommentRecord}s indexed by the name of the {@link NameRecord} to which they apply.
|
---|
108 | */
|
---|
109 | private final Map<String, NameCommentRecord> commentRecords;
|
---|
110 |
|
---|
111 | private InternalWorkbook() {
|
---|
112 | records = new WorkbookRecordList();
|
---|
113 |
|
---|
114 | boundsheets = new ArrayList<BoundSheetRecord>();
|
---|
115 | formats = new ArrayList<FormatRecord>();
|
---|
116 | hyperlinks = new ArrayList<HyperlinkRecord>();
|
---|
117 | numxfs = 0;
|
---|
118 | maxformatid = -1;
|
---|
119 | uses1904datewindowing = false;
|
---|
120 | commentRecords = new LinkedHashMap<String, NameCommentRecord>();
|
---|
121 | }
|
---|
122 |
|
---|
123 | /**
|
---|
124 | * read support for low level
|
---|
125 | * API. Pass in an array of Record objects, A Workbook
|
---|
126 | * object is constructed and passed back with all of its initialization set
|
---|
127 | * to the passed in records and references to those records held. Unlike Sheet
|
---|
128 | * workbook does not use an offset (its assumed to be 0) since its first in a file.
|
---|
129 | * If you need an offset then construct a new array with a 0 offset or write your
|
---|
130 | * own ;-p.
|
---|
131 | *
|
---|
132 | * @param recs an array of Record objects
|
---|
133 | * @return Workbook object
|
---|
134 | */
|
---|
135 | public static InternalWorkbook createWorkbook(List<Record> recs) {
|
---|
136 | if (log.check( POILogger.DEBUG ))
|
---|
137 | log.log(DEBUG, "Workbook (readfile) created with reclen=",
|
---|
138 | Integer.valueOf(recs.size()));
|
---|
139 | InternalWorkbook retval = new InternalWorkbook();
|
---|
140 | List<Record> records = new ArrayList<Record>(recs.size() / 3);
|
---|
141 | retval.records.setRecords(records);
|
---|
142 |
|
---|
143 | int k;
|
---|
144 | for (k = 0; k < recs.size(); k++) {
|
---|
145 | Record rec = recs.get(k);
|
---|
146 |
|
---|
147 | if (rec.getSid() == EOFRecord.sid) {
|
---|
148 | records.add(rec);
|
---|
149 | if (log.check( POILogger.DEBUG ))
|
---|
150 | log.log(DEBUG, "found workbook eof record at " + k);
|
---|
151 | break;
|
---|
152 | }
|
---|
153 | switch (rec.getSid()) {
|
---|
154 |
|
---|
155 | case BoundSheetRecord.sid :
|
---|
156 | if (log.check( POILogger.DEBUG ))
|
---|
157 | log.log(DEBUG, "found boundsheet record at " + k);
|
---|
158 | retval.boundsheets.add((BoundSheetRecord) rec);
|
---|
159 | retval.records.setBspos( k );
|
---|
160 | break;
|
---|
161 |
|
---|
162 | case SSTRecord.sid :
|
---|
163 | if (log.check( POILogger.DEBUG ))
|
---|
164 | log.log(DEBUG, "found sst record at " + k);
|
---|
165 | retval.sst = ( SSTRecord ) rec;
|
---|
166 | break;
|
---|
167 |
|
---|
168 | case ExtendedFormatRecord.sid :
|
---|
169 | if (log.check( POILogger.DEBUG ))
|
---|
170 | log.log(DEBUG, "found XF record at " + k);
|
---|
171 | retval.records.setXfpos( k );
|
---|
172 | retval.numxfs++;
|
---|
173 | break;
|
---|
174 |
|
---|
175 | case TabIdRecord.sid :
|
---|
176 | if (log.check( POILogger.DEBUG ))
|
---|
177 | log.log(DEBUG, "found tabid record at " + k);
|
---|
178 | retval.records.setTabpos( k );
|
---|
179 | break;
|
---|
180 |
|
---|
181 | case ExternSheetRecord.sid :
|
---|
182 | throw new RuntimeException("Extern sheet is part of LinkTable");
|
---|
183 | case NameRecord.sid :
|
---|
184 | case SupBookRecord.sid :
|
---|
185 | // LinkTable can start with either of these
|
---|
186 | if (log.check( POILogger.DEBUG ))
|
---|
187 | log.log(DEBUG, "found SupBook record at " + k);
|
---|
188 | retval.linkTable = new LinkTable(recs, k, retval.records, retval.commentRecords);
|
---|
189 | k+=retval.linkTable.getRecordCount() - 1;
|
---|
190 | continue;
|
---|
191 | case FormatRecord.sid :
|
---|
192 | if (log.check( POILogger.DEBUG ))
|
---|
193 | log.log(DEBUG, "found format record at " + k);
|
---|
194 | retval.formats.add((FormatRecord) rec);
|
---|
195 | retval.maxformatid = retval.maxformatid >= ((FormatRecord)rec).getIndexCode() ? retval.maxformatid : ((FormatRecord)rec).getIndexCode();
|
---|
196 | break;
|
---|
197 | case DateWindow1904Record.sid :
|
---|
198 | if (log.check( POILogger.DEBUG ))
|
---|
199 | log.log(DEBUG, "found datewindow1904 record at " + k);
|
---|
200 | retval.uses1904datewindowing = ((DateWindow1904Record)rec).getWindowing() == 1;
|
---|
201 | break;
|
---|
202 | case WindowOneRecord.sid:
|
---|
203 | if (log.check( POILogger.DEBUG ))
|
---|
204 | log.log(DEBUG, "found WindowOneRecord at " + k);
|
---|
205 | retval.windowOne = (WindowOneRecord) rec;
|
---|
206 | break;
|
---|
207 | case NameCommentRecord.sid:
|
---|
208 | final NameCommentRecord ncr = (NameCommentRecord) rec;
|
---|
209 | if (log.check( POILogger.DEBUG ))
|
---|
210 | log.log(DEBUG, "found NameComment at " + k);
|
---|
211 | retval.commentRecords.put(ncr.getNameText(), ncr);
|
---|
212 | default :
|
---|
213 | }
|
---|
214 | records.add(rec);
|
---|
215 | }
|
---|
216 | //What if we dont have any ranges and supbooks
|
---|
217 | // if (retval.records.supbookpos == 0) {
|
---|
218 | // retval.records.supbookpos = retval.records.bspos + 1;
|
---|
219 | // retval.records.namepos = retval.records.supbookpos + 1;
|
---|
220 | // }
|
---|
221 |
|
---|
222 | // Look for other interesting values that
|
---|
223 | // follow the EOFRecord
|
---|
224 | for ( ; k < recs.size(); k++) {
|
---|
225 | Record rec = recs.get(k);
|
---|
226 | switch (rec.getSid()) {
|
---|
227 | case HyperlinkRecord.sid:
|
---|
228 | retval.hyperlinks.add((HyperlinkRecord)rec);
|
---|
229 | break;
|
---|
230 | }
|
---|
231 | }
|
---|
232 |
|
---|
233 | if (retval.windowOne == null) {
|
---|
234 | retval.windowOne = createWindowOne();
|
---|
235 | }
|
---|
236 | if (log.check( POILogger.DEBUG ))
|
---|
237 | log.log(DEBUG, "exit create workbook from existing file function");
|
---|
238 | return retval;
|
---|
239 | }
|
---|
240 |
|
---|
241 | public int getNumRecords() {
|
---|
242 | return records.size();
|
---|
243 | }
|
---|
244 |
|
---|
245 |
|
---|
246 | private BoundSheetRecord getBoundSheetRec(int sheetIndex) {
|
---|
247 | return boundsheets.get(sheetIndex);
|
---|
248 | }
|
---|
249 |
|
---|
250 |
|
---|
251 | /**
|
---|
252 | * gets the name for a given sheet.
|
---|
253 | *
|
---|
254 | * @param sheetIndex the sheet number (0 based)
|
---|
255 | * @return sheetname the name for the sheet
|
---|
256 | */
|
---|
257 | public String getSheetName(int sheetIndex) {
|
---|
258 | return getBoundSheetRec(sheetIndex).getSheetname();
|
---|
259 | }
|
---|
260 |
|
---|
261 | /**
|
---|
262 | * gets the ExtendedFormatRecord at the given 0-based index
|
---|
263 | *
|
---|
264 | * @param index of the Extended format record (0-based)
|
---|
265 | * @return ExtendedFormatRecord at the given index
|
---|
266 | */
|
---|
267 |
|
---|
268 | public ExtendedFormatRecord getExFormatAt(int index) {
|
---|
269 | int xfptr = records.getXfpos() - (numxfs - 1);
|
---|
270 |
|
---|
271 | xfptr += index;
|
---|
272 | ExtendedFormatRecord retval =
|
---|
273 | ( ExtendedFormatRecord ) records.get(xfptr);
|
---|
274 |
|
---|
275 | return retval;
|
---|
276 | }
|
---|
277 |
|
---|
278 | /**
|
---|
279 | * Adds a string to the SST table and returns its index (if its a duplicate
|
---|
280 | * just returns its index and update the counts) ASSUMES compressed unicode
|
---|
281 | * (meaning 8bit)
|
---|
282 | *
|
---|
283 | * @param string the string to be added to the SSTRecord
|
---|
284 | *
|
---|
285 | * @return index of the string within the SSTRecord
|
---|
286 | */
|
---|
287 |
|
---|
288 | public int addSSTString(UnicodeString string) {
|
---|
289 | if (log.check( POILogger.DEBUG ))
|
---|
290 | log.log(DEBUG, "insert to sst string='", string);
|
---|
291 | if (sst == null) {
|
---|
292 | insertSST();
|
---|
293 | }
|
---|
294 | return sst.addString(string);
|
---|
295 | }
|
---|
296 |
|
---|
297 | /**
|
---|
298 | * given an index into the SST table, this function returns the corresponding String value
|
---|
299 | * @return String containing the SST String
|
---|
300 | */
|
---|
301 |
|
---|
302 | public UnicodeString getSSTString(int str) {
|
---|
303 | if (sst == null) {
|
---|
304 | insertSST();
|
---|
305 | }
|
---|
306 | UnicodeString retval = sst.getString(str);
|
---|
307 |
|
---|
308 | if (log.check( POILogger.DEBUG ))
|
---|
309 | log.log(DEBUG, "Returning SST for index=", Integer.valueOf(str),
|
---|
310 | " String= ", retval);
|
---|
311 | return retval;
|
---|
312 | }
|
---|
313 |
|
---|
314 | /**
|
---|
315 | * use this function to add a Shared String Table to an existing sheet (say
|
---|
316 | * generated by a different java api) without an sst....
|
---|
317 | * @see #createExtendedSST()
|
---|
318 | * @see org.apache.poi.hssf.record.SSTRecord
|
---|
319 | */
|
---|
320 |
|
---|
321 | public void insertSST() {
|
---|
322 | if (log.check( POILogger.DEBUG ))
|
---|
323 | log.log(DEBUG, "creating new SST via insertSST!");
|
---|
324 | sst = new SSTRecord();
|
---|
325 | records.add(records.size() - 1, createExtendedSST());
|
---|
326 | records.add(records.size() - 2, sst);
|
---|
327 | }
|
---|
328 |
|
---|
329 | /**
|
---|
330 | * Serializes all records int the worksheet section into a big byte array. Use
|
---|
331 | * this to write the Workbook out.
|
---|
332 | *
|
---|
333 | * @return byte array containing the HSSF-only portions of the POIFS file.
|
---|
334 | */
|
---|
335 | // GJS: Not used so why keep it.
|
---|
336 | // public byte [] serialize() {
|
---|
337 | // log.log(DEBUG, "Serializing Workbook!");
|
---|
338 | // byte[] retval = null;
|
---|
339 | //
|
---|
340 | //// ArrayList bytes = new ArrayList(records.size());
|
---|
341 | // int arraysize = getSize();
|
---|
342 | // int pos = 0;
|
---|
343 | //
|
---|
344 | // retval = new byte[ arraysize ];
|
---|
345 | // for (int k = 0; k < records.size(); k++) {
|
---|
346 | //
|
---|
347 | // Record record = records.get(k);
|
---|
348 | //// Let's skip RECALCID records, as they are only use for optimization
|
---|
349 | // if(record.getSid() != RecalcIdRecord.sid || ((RecalcIdRecord)record).isNeeded()) {
|
---|
350 | // pos += record.serialize(pos, retval); // rec.length;
|
---|
351 | // }
|
---|
352 | // }
|
---|
353 | // log.log(DEBUG, "Exiting serialize workbook");
|
---|
354 | // return retval;
|
---|
355 | // }
|
---|
356 |
|
---|
357 | /**
|
---|
358 | * creates the WindowOne record with the following magic values: <P>
|
---|
359 | * horizontal hold - 0x168 <P>
|
---|
360 | * vertical hold - 0x10e <P>
|
---|
361 | * width - 0x3a5c <P>
|
---|
362 | * height - 0x23be <P>
|
---|
363 | * options - 0x38 <P>
|
---|
364 | * selected tab - 0 <P>
|
---|
365 | * displayed tab - 0 <P>
|
---|
366 | * num selected tab- 0 <P>
|
---|
367 | * tab width ratio - 0x258 <P>
|
---|
368 | */
|
---|
369 | private static WindowOneRecord createWindowOne() {
|
---|
370 | WindowOneRecord retval = new WindowOneRecord();
|
---|
371 |
|
---|
372 | retval.setHorizontalHold(( short ) 0x168);
|
---|
373 | retval.setVerticalHold(( short ) 0x10e);
|
---|
374 | retval.setWidth(( short ) 0x3a5c);
|
---|
375 | retval.setHeight(( short ) 0x23be);
|
---|
376 | retval.setOptions(( short ) 0x38);
|
---|
377 | retval.setActiveSheetIndex( 0x0);
|
---|
378 | retval.setFirstVisibleTab(0x0);
|
---|
379 | retval.setNumSelectedTabs(( short ) 1);
|
---|
380 | retval.setTabWidthRatio(( short ) 0x258);
|
---|
381 | return retval;
|
---|
382 | }
|
---|
383 |
|
---|
384 | /**
|
---|
385 | * Creates the ExtendedSST record with numstrings per bucket set to 0x8. HSSF
|
---|
386 | * doesn't yet know what to do with this thing, but we create it with nothing in
|
---|
387 | * it hardly just to make Excel happy and our sheets look like Excel's
|
---|
388 | */
|
---|
389 | private static ExtSSTRecord createExtendedSST() {
|
---|
390 | ExtSSTRecord retval = new ExtSSTRecord();
|
---|
391 | retval.setNumStringsPerBucket(( short ) 0x8);
|
---|
392 | return retval;
|
---|
393 | }
|
---|
394 |
|
---|
395 | /** finds the sheet name by his extern sheet index
|
---|
396 | * @param externSheetIndex extern sheet index
|
---|
397 | * @return sheet name.
|
---|
398 | */
|
---|
399 | public String findSheetNameFromExternSheet(int externSheetIndex){
|
---|
400 |
|
---|
401 | int indexToSheet = linkTable.getIndexToInternalSheet(externSheetIndex);
|
---|
402 | if (indexToSheet < 0) {
|
---|
403 | // TODO - what does '-1' mean here?
|
---|
404 | //error check, bail out gracefully!
|
---|
405 | return "";
|
---|
406 | }
|
---|
407 | if (indexToSheet >= boundsheets.size()) {
|
---|
408 | // Not sure if this can ever happen (See bug 45798)
|
---|
409 | return ""; // Seems to be what excel would do in this case
|
---|
410 | }
|
---|
411 | return getSheetName(indexToSheet);
|
---|
412 | }
|
---|
413 | public ExternalSheet getExternalSheet(int externSheetIndex) {
|
---|
414 | String[] extNames = linkTable.getExternalBookAndSheetName(externSheetIndex);
|
---|
415 | if (extNames == null) {
|
---|
416 | return null;
|
---|
417 | }
|
---|
418 | return new ExternalSheet(extNames[0], extNames[1]);
|
---|
419 | }
|
---|
420 |
|
---|
421 | /** gets the total number of names
|
---|
422 | * @return number of names
|
---|
423 | */
|
---|
424 | public int getNumNames(){
|
---|
425 | if(linkTable == null) {
|
---|
426 | return 0;
|
---|
427 | }
|
---|
428 | return linkTable.getNumNames();
|
---|
429 | }
|
---|
430 |
|
---|
431 | /** gets the name record
|
---|
432 | * @param index name index
|
---|
433 | * @return name record
|
---|
434 | */
|
---|
435 | public NameRecord getNameRecord(int index){
|
---|
436 | return linkTable.getNameRecord(index);
|
---|
437 | }
|
---|
438 |
|
---|
439 |
|
---|
440 |
|
---|
441 | /**
|
---|
442 | * Returns the list of FormatRecords in the workbook.
|
---|
443 | * @return ArrayList of FormatRecords in the notebook
|
---|
444 | */
|
---|
445 | public List<FormatRecord> getFormats() {
|
---|
446 | return formats;
|
---|
447 | }
|
---|
448 |
|
---|
449 | /**
|
---|
450 | * Whether date windowing is based on 1/2/1904 or 1/1/1900.
|
---|
451 | * Some versions of Excel (Mac) can save workbooks using 1904 date windowing.
|
---|
452 | *
|
---|
453 | * @return true if using 1904 date windowing
|
---|
454 | */
|
---|
455 | public boolean isUsing1904DateWindowing() {
|
---|
456 | return uses1904datewindowing;
|
---|
457 | }
|
---|
458 |
|
---|
459 | /**
|
---|
460 | * @param refIndex Index to REF entry in EXTERNSHEET record in the Link Table
|
---|
461 | * @param definedNameIndex zero-based to DEFINEDNAME or EXTERNALNAME record
|
---|
462 | * @return the string representation of the defined or external name
|
---|
463 | */
|
---|
464 | public String resolveNameXText(int refIndex, int definedNameIndex) {
|
---|
465 | return linkTable.resolveNameXText(refIndex, definedNameIndex);
|
---|
466 | }
|
---|
467 | }
|
---|