501 lines
18 KiB
Java
501 lines
18 KiB
Java
/* ====================================================================
|
|
Licensed to the Apache Software Foundation (ASF) under one or more
|
|
contributor license agreements. See the NOTICE file distributed with
|
|
this work for additional information regarding copyright ownership.
|
|
The ASF licenses this file to You under the Apache License, Version 2.0
|
|
(the "License"); you may not use this file except in compliance with
|
|
the License. You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
==================================================================== */
|
|
|
|
package org.apache.poi.hssf.record;
|
|
|
|
import java.io.InputStream;
|
|
import java.lang.reflect.Constructor;
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.lang.reflect.Method;
|
|
import java.lang.reflect.Modifier;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
|
|
import org.apache.poi.EncryptedDocumentException;
|
|
import org.apache.poi.hssf.record.chart.BeginRecord;
|
|
import org.apache.poi.hssf.record.chart.CatLabRecord;
|
|
import org.apache.poi.hssf.record.chart.ChartEndBlockRecord;
|
|
import org.apache.poi.hssf.record.chart.ChartEndObjectRecord;
|
|
import org.apache.poi.hssf.record.chart.ChartFRTInfoRecord;
|
|
import org.apache.poi.hssf.record.chart.ChartRecord;
|
|
import org.apache.poi.hssf.record.chart.ChartStartBlockRecord;
|
|
import org.apache.poi.hssf.record.chart.ChartStartObjectRecord;
|
|
import org.apache.poi.hssf.record.chart.ChartTitleFormatRecord;
|
|
import org.apache.poi.hssf.record.chart.DataFormatRecord;
|
|
import org.apache.poi.hssf.record.chart.EndRecord;
|
|
import org.apache.poi.hssf.record.chart.LegendRecord;
|
|
import org.apache.poi.hssf.record.chart.LinkedDataRecord;
|
|
import org.apache.poi.hssf.record.chart.SeriesRecord;
|
|
import org.apache.poi.hssf.record.chart.SeriesTextRecord;
|
|
import org.apache.poi.hssf.record.chart.SeriesToChartGroupRecord;
|
|
import org.apache.poi.hssf.record.chart.ValueRangeRecord;
|
|
import org.apache.poi.hssf.record.pivottable.DataItemRecord;
|
|
import org.apache.poi.hssf.record.pivottable.ExtendedPivotTableViewFieldsRecord;
|
|
import org.apache.poi.hssf.record.pivottable.PageItemRecord;
|
|
import org.apache.poi.hssf.record.pivottable.StreamIDRecord;
|
|
import org.apache.poi.hssf.record.pivottable.ViewDefinitionRecord;
|
|
import org.apache.poi.hssf.record.pivottable.ViewFieldsRecord;
|
|
import org.apache.poi.hssf.record.pivottable.ViewSourceRecord;
|
|
|
|
/**
|
|
* Title: Record Factory<p>
|
|
* Description: Takes a stream and outputs an array of Record objects.
|
|
*
|
|
* @see org.apache.poi.hssf.eventmodel.EventRecordFactory
|
|
*/
|
|
public final class RecordFactory {
|
|
private static final int NUM_RECORDS = 512;
|
|
|
|
private interface I_RecordCreator {
|
|
Record create(RecordInputStream in);
|
|
|
|
Class<? extends Record> getRecordClass();
|
|
}
|
|
private static final class ReflectionConstructorRecordCreator implements I_RecordCreator {
|
|
|
|
private final Constructor<? extends Record> _c;
|
|
public ReflectionConstructorRecordCreator(Constructor<? extends Record> c) {
|
|
_c = c;
|
|
}
|
|
@Override
|
|
public Record create(RecordInputStream in) {
|
|
Object[] args = { in, };
|
|
try {
|
|
return _c.newInstance(args);
|
|
} catch (IllegalArgumentException e) {
|
|
throw new RuntimeException(e);
|
|
} catch (InstantiationException e) {
|
|
throw new RuntimeException(e);
|
|
} catch (IllegalAccessException e) {
|
|
throw new RuntimeException(e);
|
|
} catch (InvocationTargetException e) {
|
|
Throwable t = e.getTargetException();
|
|
if (t instanceof org.apache.poi.util.RecordFormatException) {
|
|
throw (org.apache.poi.util.RecordFormatException)t;
|
|
} else if (t instanceof EncryptedDocumentException) {
|
|
throw (EncryptedDocumentException)t;
|
|
} else {
|
|
throw new org.apache.poi.util.RecordFormatException("Unable to construct record instance" , t);
|
|
}
|
|
}
|
|
}
|
|
@Override
|
|
public Class<? extends Record> getRecordClass() {
|
|
return _c.getDeclaringClass();
|
|
}
|
|
}
|
|
/**
|
|
* A "create" method is used instead of the usual constructor if the created record might
|
|
* be of a different class to the declaring class.
|
|
*/
|
|
private static final class ReflectionMethodRecordCreator implements I_RecordCreator {
|
|
private final Method _m;
|
|
public ReflectionMethodRecordCreator(Method m) {
|
|
_m = m;
|
|
}
|
|
@Override
|
|
public Record create(RecordInputStream in) {
|
|
Object[] args = { in, };
|
|
try {
|
|
return (Record) _m.invoke(null, args);
|
|
} catch (IllegalArgumentException e) {
|
|
throw new RuntimeException(e);
|
|
} catch (IllegalAccessException e) {
|
|
throw new RuntimeException(e);
|
|
} catch (InvocationTargetException e) {
|
|
throw new org.apache.poi.util.RecordFormatException("Unable to construct record instance" , e.getTargetException());
|
|
}
|
|
}
|
|
@Override
|
|
@SuppressWarnings("unchecked")
|
|
public Class<? extends Record> getRecordClass() {
|
|
return (Class<? extends Record>) _m.getDeclaringClass();
|
|
}
|
|
}
|
|
|
|
private static final Class<?>[] CONSTRUCTOR_ARGS = { RecordInputStream.class, };
|
|
|
|
/**
|
|
* contains the classes for all the records we want to parse.<br/>
|
|
* Note - this most but not *every* subclass of Record.
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
private static final Class<? extends Record>[] recordClasses = new Class[] {
|
|
ArrayRecord.class,
|
|
AutoFilterInfoRecord.class,
|
|
BackupRecord.class,
|
|
BlankRecord.class,
|
|
BOFRecord.class,
|
|
BookBoolRecord.class,
|
|
BoolErrRecord.class,
|
|
BottomMarginRecord.class,
|
|
BoundSheetRecord.class,
|
|
CalcCountRecord.class,
|
|
CalcModeRecord.class,
|
|
CFHeaderRecord.class,
|
|
CFHeader12Record.class,
|
|
CFRuleRecord.class,
|
|
CFRule12Record.class,
|
|
ChartRecord.class,
|
|
ChartTitleFormatRecord.class,
|
|
CodepageRecord.class,
|
|
ColumnInfoRecord.class,
|
|
ContinueRecord.class,
|
|
CountryRecord.class,
|
|
CRNCountRecord.class,
|
|
CRNRecord.class,
|
|
DateWindow1904Record.class,
|
|
DBCellRecord.class,
|
|
DConRefRecord.class,
|
|
DefaultColWidthRecord.class,
|
|
DefaultRowHeightRecord.class,
|
|
DeltaRecord.class,
|
|
DimensionsRecord.class,
|
|
DrawingGroupRecord.class,
|
|
DrawingRecord.class,
|
|
DrawingSelectionRecord.class,
|
|
DSFRecord.class,
|
|
DVALRecord.class,
|
|
DVRecord.class,
|
|
EOFRecord.class,
|
|
ExtendedFormatRecord.class,
|
|
ExternalNameRecord.class,
|
|
ExternSheetRecord.class,
|
|
ExtSSTRecord.class,
|
|
FeatRecord.class,
|
|
FeatHdrRecord.class,
|
|
FilePassRecord.class,
|
|
FileSharingRecord.class,
|
|
FnGroupCountRecord.class,
|
|
FontRecord.class,
|
|
FooterRecord.class,
|
|
FormatRecord.class,
|
|
FormulaRecord.class,
|
|
GridsetRecord.class,
|
|
GutsRecord.class,
|
|
HCenterRecord.class,
|
|
HeaderRecord.class,
|
|
HeaderFooterRecord.class,
|
|
HideObjRecord.class,
|
|
HorizontalPageBreakRecord.class,
|
|
HyperlinkRecord.class,
|
|
IndexRecord.class,
|
|
InterfaceEndRecord.class,
|
|
InterfaceHdrRecord.class,
|
|
IterationRecord.class,
|
|
LabelRecord.class,
|
|
LabelSSTRecord.class,
|
|
LeftMarginRecord.class,
|
|
LegendRecord.class,
|
|
MergeCellsRecord.class,
|
|
MMSRecord.class,
|
|
MulBlankRecord.class,
|
|
MulRKRecord.class,
|
|
NameRecord.class,
|
|
NameCommentRecord.class,
|
|
NoteRecord.class,
|
|
NumberRecord.class,
|
|
ObjectProtectRecord.class,
|
|
ObjRecord.class,
|
|
PaletteRecord.class,
|
|
PaneRecord.class,
|
|
PasswordRecord.class,
|
|
PasswordRev4Record.class,
|
|
PrecisionRecord.class,
|
|
PrintGridlinesRecord.class,
|
|
PrintHeadersRecord.class,
|
|
PrintSetupRecord.class,
|
|
ProtectionRev4Record.class,
|
|
ProtectRecord.class,
|
|
RecalcIdRecord.class,
|
|
RefModeRecord.class,
|
|
RefreshAllRecord.class,
|
|
RightMarginRecord.class,
|
|
RKRecord.class,
|
|
RowRecord.class,
|
|
SaveRecalcRecord.class,
|
|
ScenarioProtectRecord.class,
|
|
SelectionRecord.class,
|
|
SeriesRecord.class,
|
|
SeriesTextRecord.class,
|
|
SharedFormulaRecord.class,
|
|
SSTRecord.class,
|
|
StringRecord.class,
|
|
StyleRecord.class,
|
|
SupBookRecord.class,
|
|
TabIdRecord.class,
|
|
TableRecord.class,
|
|
TableStylesRecord.class,
|
|
TextObjectRecord.class,
|
|
TopMarginRecord.class,
|
|
UncalcedRecord.class,
|
|
UseSelFSRecord.class,
|
|
UserSViewBegin.class,
|
|
UserSViewEnd.class,
|
|
ValueRangeRecord.class,
|
|
VCenterRecord.class,
|
|
VerticalPageBreakRecord.class,
|
|
WindowOneRecord.class,
|
|
WindowProtectRecord.class,
|
|
WindowTwoRecord.class,
|
|
WriteAccessRecord.class,
|
|
WriteProtectRecord.class,
|
|
WSBoolRecord.class,
|
|
|
|
// chart records
|
|
BeginRecord.class,
|
|
ChartFRTInfoRecord.class,
|
|
ChartStartBlockRecord.class,
|
|
ChartEndBlockRecord.class,
|
|
// TODO ChartFormatRecord.class,
|
|
ChartStartObjectRecord.class,
|
|
ChartEndObjectRecord.class,
|
|
CatLabRecord.class,
|
|
DataFormatRecord.class,
|
|
EndRecord.class,
|
|
LinkedDataRecord.class,
|
|
SeriesToChartGroupRecord.class,
|
|
|
|
// pivot table records
|
|
DataItemRecord.class,
|
|
ExtendedPivotTableViewFieldsRecord.class,
|
|
PageItemRecord.class,
|
|
StreamIDRecord.class,
|
|
ViewDefinitionRecord.class,
|
|
ViewFieldsRecord.class,
|
|
ViewSourceRecord.class,
|
|
};
|
|
|
|
/**
|
|
* cache of the recordsToMap();
|
|
*/
|
|
private static final Map<Integer, I_RecordCreator> _recordCreatorsById = recordsToMap(recordClasses);
|
|
|
|
private static short[] _allKnownRecordSIDs;
|
|
|
|
/**
|
|
* Debug / diagnosis method<p>
|
|
*
|
|
* Gets the POI implementation class for a given {@code sid}. Only a subset of the BIFF
|
|
* records are actually interpreted by POI. A few others are known but not interpreted
|
|
* (see {@link UnknownRecord#getBiffName(int)}).
|
|
*
|
|
* @param sid the record sid
|
|
*
|
|
* @return the POI implementation class for the specified record {@code sid}.
|
|
* {@code null} if the specified record is not interpreted by POI.
|
|
*/
|
|
public static Class<? extends Record> getRecordClass(int sid) {
|
|
I_RecordCreator rc = _recordCreatorsById.get(Integer.valueOf(sid));
|
|
if (rc == null) {
|
|
return null;
|
|
}
|
|
return rc.getRecordClass();
|
|
}
|
|
|
|
/**
|
|
* create a record, if there are MUL records than multiple records
|
|
* are returned digested into the non-mul form.
|
|
*
|
|
* @param in the RecordInputStream to read from
|
|
* @return the extracted records
|
|
*/
|
|
public static Record [] createRecord(RecordInputStream in) {
|
|
Record record = createSingleRecord(in);
|
|
if (record instanceof DBCellRecord) {
|
|
// Not needed by POI. Regenerated from scratch by POI when spreadsheet is written
|
|
return new Record[] { null, };
|
|
}
|
|
if (record instanceof RKRecord) {
|
|
return new Record[] { convertToNumberRecord((RKRecord) record), };
|
|
}
|
|
if (record instanceof MulRKRecord) {
|
|
return convertRKRecords((MulRKRecord)record);
|
|
}
|
|
return new Record[] { record, };
|
|
}
|
|
|
|
public static Record createSingleRecord(RecordInputStream in) {
|
|
I_RecordCreator constructor = _recordCreatorsById.get(Integer.valueOf(in.getSid()));
|
|
|
|
if (constructor == null) {
|
|
return new UnknownRecord(in);
|
|
}
|
|
|
|
return constructor.create(in);
|
|
}
|
|
|
|
/**
|
|
* RK record is a slightly smaller alternative to NumberRecord
|
|
* POI likes NumberRecord better
|
|
*
|
|
* @param rk the RK record to convert
|
|
* @return the NumberRecord
|
|
*/
|
|
public static NumberRecord convertToNumberRecord(RKRecord rk) {
|
|
NumberRecord num = new NumberRecord();
|
|
|
|
num.setColumn(rk.getColumn());
|
|
num.setRow(rk.getRow());
|
|
num.setXFIndex(rk.getXFIndex());
|
|
num.setValue(rk.getRKNumber());
|
|
return num;
|
|
}
|
|
|
|
/**
|
|
* Converts a {@link MulRKRecord} into an equivalent array of {@link NumberRecord NumberRecords}
|
|
*
|
|
* @param mrk the MulRKRecord to convert
|
|
* @return the equivalent array of {@link NumberRecord NumberRecords}
|
|
*/
|
|
public static NumberRecord[] convertRKRecords(MulRKRecord mrk) {
|
|
NumberRecord[] mulRecs = new NumberRecord[mrk.getNumColumns()];
|
|
for (int k = 0; k < mrk.getNumColumns(); k++) {
|
|
NumberRecord nr = new NumberRecord();
|
|
|
|
nr.setColumn((short) (k + mrk.getFirstColumn()));
|
|
nr.setRow(mrk.getRow());
|
|
nr.setXFIndex(mrk.getXFAt(k));
|
|
nr.setValue(mrk.getRKNumberAt(k));
|
|
mulRecs[k] = nr;
|
|
}
|
|
return mulRecs;
|
|
}
|
|
|
|
/**
|
|
* Converts a {@link MulBlankRecord} into an equivalent array of {@link BlankRecord BlankRecords}
|
|
*
|
|
* @param mbk the MulBlankRecord to convert
|
|
* @return the equivalent array of {@link BlankRecord BlankRecords}
|
|
*/
|
|
public static BlankRecord[] convertBlankRecords(MulBlankRecord mbk) {
|
|
BlankRecord[] mulRecs = new BlankRecord[mbk.getNumColumns()];
|
|
for (int k = 0; k < mbk.getNumColumns(); k++) {
|
|
BlankRecord br = new BlankRecord();
|
|
|
|
br.setColumn((short) (k + mbk.getFirstColumn()));
|
|
br.setRow(mbk.getRow());
|
|
br.setXFIndex(mbk.getXFAt(k));
|
|
mulRecs[k] = br;
|
|
}
|
|
return mulRecs;
|
|
}
|
|
|
|
/**
|
|
* @return an array of all the SIDS for all known records
|
|
*/
|
|
public static short[] getAllKnownRecordSIDs() {
|
|
if (_allKnownRecordSIDs == null) {
|
|
short[] results = new short[ _recordCreatorsById.size() ];
|
|
int i = 0;
|
|
|
|
for (Integer sid : _recordCreatorsById.keySet()) {
|
|
results[i++] = sid.shortValue();
|
|
}
|
|
Arrays.sort(results);
|
|
_allKnownRecordSIDs = results;
|
|
}
|
|
|
|
return _allKnownRecordSIDs.clone();
|
|
}
|
|
|
|
/**
|
|
* gets the record constructors and sticks them in the map by SID
|
|
* @return map of SIDs to short,short,byte[] constructors for Record classes
|
|
* most of org.apache.poi.hssf.record.*
|
|
*/
|
|
private static Map<Integer, I_RecordCreator> recordsToMap(Class<? extends Record> [] records) {
|
|
Map<Integer, I_RecordCreator> result = new HashMap<Integer, I_RecordCreator>();
|
|
Set<Class<?>> uniqueRecClasses = new HashSet<Class<?>>(records.length * 3 / 2);
|
|
|
|
for (Class<? extends Record> recClass : records) {
|
|
if(!Record.class.isAssignableFrom(recClass)) {
|
|
throw new RuntimeException("Invalid record sub-class (" + recClass.getName() + ")");
|
|
}
|
|
if(Modifier.isAbstract(recClass.getModifiers())) {
|
|
throw new RuntimeException("Invalid record class (" + recClass.getName() + ") - must not be abstract");
|
|
}
|
|
if(!uniqueRecClasses.add(recClass)) {
|
|
throw new RuntimeException("duplicate record class (" + recClass.getName() + ")");
|
|
}
|
|
|
|
int sid;
|
|
try {
|
|
sid = recClass.getField("sid").getShort(null);
|
|
} catch (Exception illegalArgumentException) {
|
|
throw new org.apache.poi.util.RecordFormatException(
|
|
"Unable to determine record types");
|
|
}
|
|
Integer key = Integer.valueOf(sid);
|
|
if (result.containsKey(key)) {
|
|
Class<?> prevClass = result.get(key).getRecordClass();
|
|
throw new RuntimeException("duplicate record sid 0x" +
|
|
Integer.toHexString(sid).toUpperCase(Locale.ROOT)
|
|
+ " for classes (" + recClass.getName() + ") and ("
|
|
+ prevClass.getName() + ")");
|
|
}
|
|
result.put(key, getRecordCreator(recClass));
|
|
}
|
|
// result.put(Integer.valueOf(0x0406), result.get(Integer.valueOf(0x06)));
|
|
return result;
|
|
}
|
|
|
|
private static I_RecordCreator getRecordCreator(Class<? extends Record> recClass) {
|
|
try {
|
|
Constructor<? extends Record> constructor;
|
|
constructor = recClass.getConstructor(CONSTRUCTOR_ARGS);
|
|
return new ReflectionConstructorRecordCreator(constructor);
|
|
} catch (NoSuchMethodException e) {
|
|
// fall through and look for other construction methods
|
|
}
|
|
try {
|
|
Method m = recClass.getDeclaredMethod("create", CONSTRUCTOR_ARGS);
|
|
return new ReflectionMethodRecordCreator(m);
|
|
} catch (NoSuchMethodException e) {
|
|
throw new RuntimeException("Failed to find constructor or create method for (" + recClass.getName() + ").");
|
|
}
|
|
}
|
|
/**
|
|
* Create an array of records from an input stream
|
|
*
|
|
* @param in the InputStream from which the records will be obtained
|
|
*
|
|
* @return an array of Records created from the InputStream
|
|
*
|
|
* @exception org.apache.poi.util.RecordFormatException on error processing the InputStream
|
|
*/
|
|
public static List<Record> createRecords(InputStream in) throws org.apache.poi.util.RecordFormatException {
|
|
|
|
List<Record> records = new ArrayList<Record>(NUM_RECORDS);
|
|
|
|
RecordFactoryInputStream recStream = new RecordFactoryInputStream(in, true);
|
|
|
|
Record record;
|
|
while ((record = recStream.nextRecord())!=null) {
|
|
records.add(record);
|
|
}
|
|
|
|
return records;
|
|
}
|
|
}
|