| /* ==================================================================== |
| 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.ddf; |
| |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.function.Supplier; |
| |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| import org.apache.poi.util.GenericRecordUtil; |
| import org.apache.poi.util.HexDump; |
| import org.apache.poi.util.LittleEndian; |
| |
| import static org.apache.logging.log4j.util.Unbox.box; |
| |
| /** |
| * Escher container records store other escher records as children. |
| * The container records themselves never store any information beyond |
| * the standard header used by all escher records. This one record is |
| * used to represent many different types of records. |
| */ |
| public final class EscherContainerRecord extends EscherRecord implements Iterable<EscherRecord> { |
| public static final short DGG_CONTAINER = EscherRecordTypes.DGG_CONTAINER.typeID; |
| public static final short BSTORE_CONTAINER = EscherRecordTypes.BSTORE_CONTAINER.typeID; |
| public static final short DG_CONTAINER = EscherRecordTypes.DG_CONTAINER.typeID; |
| public static final short SPGR_CONTAINER = EscherRecordTypes.SPGR_CONTAINER.typeID; |
| public static final short SP_CONTAINER = EscherRecordTypes.SP_CONTAINER.typeID; |
| public static final short SOLVER_CONTAINER = EscherRecordTypes.SOLVER_CONTAINER.typeID; |
| |
| private static final Logger LOGGER = LogManager.getLogger(EscherContainerRecord.class); |
| |
| /** |
| * in case if document contains any charts we have such document structure: |
| * BOF |
| * ... |
| * DrawingRecord |
| * ... |
| * ObjRecord|TxtObjRecord |
| * ... |
| * EOF |
| * ... |
| * BOF(Chart begin) |
| * ... |
| * DrawingRecord |
| * ... |
| * ObjRecord|TxtObjRecord |
| * ... |
| * EOF |
| * So, when we call EscherAggregate.createAggregate() we have not all needed data. |
| * When we got warning "WARNING: " + bytesRemaining + " bytes remaining but no space left" |
| * we should save value of bytesRemaining |
| * and add it to container size when we serialize it |
| */ |
| private int _remainingLength; |
| |
| private final List<EscherRecord> _childRecords = new ArrayList<>(); |
| |
| public EscherContainerRecord() {} |
| |
| public EscherContainerRecord(EscherContainerRecord other) { |
| super(other); |
| _remainingLength = other._remainingLength; |
| other._childRecords.stream().map(EscherRecord::copy).forEach(_childRecords::add); |
| } |
| |
| @Override |
| public int fillFields(byte[] data, int pOffset, EscherRecordFactory recordFactory) { |
| int bytesRemaining = readHeader(data, pOffset); |
| int bytesWritten = 8; |
| int offset = pOffset + 8; |
| while (bytesRemaining > 0 && offset < data.length) { |
| EscherRecord child = recordFactory.createRecord(data, offset); |
| int childBytesWritten = child.fillFields(data, offset, recordFactory); |
| bytesWritten += childBytesWritten; |
| offset += childBytesWritten; |
| bytesRemaining -= childBytesWritten; |
| addChildRecord(child); |
| if (offset >= data.length && bytesRemaining > 0) { |
| _remainingLength = bytesRemaining; |
| LOGGER.atWarn().log("Not enough Escher data: {} bytes remaining but no space left", box(bytesRemaining)); |
| } |
| } |
| return bytesWritten; |
| } |
| |
| @Override |
| public int serialize( int offset, byte[] data, EscherSerializationListener listener ) |
| { |
| listener.beforeRecordSerialize( offset, getRecordId(), this ); |
| |
| LittleEndian.putShort(data, offset, getOptions()); |
| LittleEndian.putShort(data, offset+2, getRecordId()); |
| int remainingBytes = 0; |
| for (EscherRecord r : this) { |
| remainingBytes += r.getRecordSize(); |
| } |
| remainingBytes += _remainingLength; |
| LittleEndian.putInt(data, offset+4, remainingBytes); |
| int pos = offset+8; |
| for (EscherRecord r : this) { |
| pos += r.serialize(pos, data, listener ); |
| } |
| |
| listener.afterRecordSerialize( pos, getRecordId(), pos - offset, this ); |
| return pos - offset; |
| } |
| |
| @Override |
| public int getRecordSize() { |
| int childRecordsSize = 0; |
| for (EscherRecord r : this) { |
| childRecordsSize += r.getRecordSize(); |
| } |
| return 8 + childRecordsSize; |
| } |
| |
| /** |
| * Do any of our (top level) children have the given recordId? |
| * |
| * @param recordId the recordId of the child |
| * |
| * @return true, if any child has the given recordId |
| */ |
| public boolean hasChildOfType(short recordId) { |
| return _childRecords.stream().anyMatch(r -> r.getRecordId() == recordId); |
| } |
| |
| @Override |
| public EscherRecord getChild( int index ) { |
| return _childRecords.get(index); |
| } |
| |
| /** |
| * @return a copy of the list of all the child records of the container. |
| */ |
| @Override |
| public List<EscherRecord> getChildRecords() { |
| return new ArrayList<>(_childRecords); |
| } |
| |
| /** |
| * @return an iterator over the child records |
| */ |
| @Override |
| public Iterator<EscherRecord> iterator() { |
| return Collections.unmodifiableList(_childRecords).iterator(); |
| } |
| |
| |
| /** |
| * replaces the internal child list with the contents of the supplied <tt>childRecords</tt> |
| */ |
| @Override |
| public void setChildRecords(List<EscherRecord> childRecords) { |
| if (childRecords == _childRecords) { |
| throw new IllegalStateException("Child records private data member has escaped"); |
| } |
| _childRecords.clear(); |
| _childRecords.addAll(childRecords); |
| } |
| |
| /** |
| * Removes the given escher record from the child list |
| * |
| * @param toBeRemoved the escher record to be removed |
| * @return true, if the record was found and removed |
| */ |
| public boolean removeChildRecord(EscherRecord toBeRemoved) { |
| return _childRecords.remove(toBeRemoved); |
| } |
| |
| |
| |
| /** |
| * Returns all of our children which are also |
| * EscherContainers (may be 0, 1, or vary rarely 2 or 3) |
| * |
| * @return EscherContainer children |
| */ |
| public List<EscherContainerRecord> getChildContainers() { |
| List<EscherContainerRecord> containers = new ArrayList<>(); |
| for (EscherRecord r : this) { |
| if(r instanceof EscherContainerRecord) { |
| containers.add((EscherContainerRecord) r); |
| } |
| } |
| return containers; |
| } |
| |
| @Override |
| public String getRecordName() { |
| final short id = getRecordId(); |
| EscherRecordTypes t = EscherRecordTypes.forTypeID(id); |
| return (t != EscherRecordTypes.UNKNOWN) ? t.recordName : "Container 0x" + HexDump.toHex(id); |
| } |
| |
| @Override |
| public void display(PrintWriter w, int indent) { |
| super.display(w, indent); |
| for (EscherRecord escherRecord : this) { |
| escherRecord.display(w, indent + 1); |
| } |
| } |
| |
| /** |
| * Append a child record |
| * |
| * @param record the record to be added |
| */ |
| public void addChildRecord(EscherRecord record) { |
| _childRecords.add(record); |
| } |
| |
| /** |
| * Add a child record before the record with given recordId |
| * |
| * @param record the record to be added |
| * @param insertBeforeRecordId the recordId of the next sibling |
| */ |
| public void addChildBefore(EscherRecord record, int insertBeforeRecordId) { |
| int idx = 0; |
| for (EscherRecord rec : this) { |
| if(rec.getRecordId() == (short)insertBeforeRecordId) { |
| break; |
| } |
| // TODO - keep looping? Do we expect multiple matches? |
| idx++; |
| } |
| _childRecords.add(idx, record); |
| } |
| |
| public <T extends EscherRecord> T getChildById( short recordId ) { |
| for ( EscherRecord childRecord : this ) { |
| if ( childRecord.getRecordId() == recordId ) { |
| @SuppressWarnings( "unchecked" ) |
| final T result = (T) childRecord; |
| return result; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Recursively find records with the specified record ID |
| * |
| * @param recordId the recordId to be searched for |
| * @param out - list to store found records |
| */ |
| public void getRecordsById(short recordId, List<EscherRecord> out){ |
| for (EscherRecord r : this) { |
| if(r instanceof EscherContainerRecord) { |
| EscherContainerRecord c = (EscherContainerRecord)r; |
| c.getRecordsById(recordId, out ); |
| } else if (r.getRecordId() == recordId){ |
| out.add(r); |
| } |
| } |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return GenericRecordUtil.getGenericProperties( |
| "base", super::getGenericProperties, |
| "isContainer", this::isContainerRecord |
| ); |
| } |
| |
| @Override |
| public Enum getGenericRecordType() { |
| return EscherRecordTypes.forTypeID(getRecordId()); |
| } |
| |
| @Override |
| public EscherContainerRecord copy() { |
| return new EscherContainerRecord(this); |
| } |
| } |