/* ====================================================================
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;
import java.io.
IOException;
import java.net.
URI;
import java.util.
ArrayList;
import java.util.
Collections;
import java.util.
LinkedHashMap;
import java.util.
List;
import java.util.
Map;
import java.util.
Set;
import org.apache.poi.openxml4j.exceptions.
InvalidFormatException;
import org.apache.poi.openxml4j.exceptions.
OpenXML4JException;
import org.apache.poi.openxml4j.exceptions.
PartAlreadyExistsException;
import org.apache.poi.openxml4j.opc.
OPCPackage;
import org.apache.poi.openxml4j.opc.
PackagePart;
import org.apache.poi.openxml4j.opc.
PackagePartName;
import org.apache.poi.openxml4j.opc.
PackageRelationship;
import org.apache.poi.openxml4j.opc.
PackageRelationshipCollection;
import org.apache.poi.openxml4j.opc.
PackageRelationshipTypes;
import org.apache.poi.openxml4j.opc.
PackagingURIHelper;
import org.apache.poi.openxml4j.opc.
TargetMode;
import org.apache.poi.util.
Internal;
import org.apache.poi.util.
POILogFactory;
import org.apache.poi.util.
POILogger;
import org.apache.poi.xssf.usermodel.
XSSFRelation;
/**
* Represents an entry of a OOXML package.
*
* <p>
* Each POIXMLDocumentPart keeps a reference to the underlying a {@link org.apache.poi.openxml4j.opc.PackagePart}.
* </p>
*/
public class
POIXMLDocumentPart {
private static final
POILogger logger =
POILogFactory.
getLogger(
POIXMLDocumentPart.class);
private
String coreDocumentRel =
PackageRelationshipTypes.
CORE_DOCUMENT;
private
PackagePart packagePart;
private
POIXMLDocumentPart parent;
private
Map<
String,
RelationPart>
relations = new
LinkedHashMap<
String,
RelationPart>();
/**
* The RelationPart is a cached relationship between the document, which contains the RelationPart,
* and one of its referenced child document parts.
* The child document parts may only belong to one parent, but it's often referenced by other
* parents too, having varying {@link PackageRelationship#getId() relationship ids} pointing to it.
*/
public static class
RelationPart {
private final
PackageRelationship relationship;
private final
POIXMLDocumentPart documentPart;
RelationPart(
PackageRelationship relationship,
POIXMLDocumentPart documentPart) {
this.
relationship =
relationship;
this.
documentPart =
documentPart;
}
/**
* @return the cached relationship, which uniquely identifies this child document part within the parent
*/
public
PackageRelationship getRelationship() {
return
relationship;
}
/**
* @param <T> the cast of the caller to a document sub class
*
* @return the child document part
*/
@
SuppressWarnings("unchecked")
public <T extends
POIXMLDocumentPart> T
getDocumentPart() {
return (T)
documentPart;
}
}
/**
* Counter that provides the amount of incoming relations from other parts
* to this part.
*/
private int
relationCounter = 0;
int
incrementRelationCounter() {
relationCounter++;
return
relationCounter;
}
int
decrementRelationCounter() {
relationCounter--;
return
relationCounter;
}
int
getRelationCounter() {
return
relationCounter;
}
/**
* Construct POIXMLDocumentPart representing a "core document" package part.
*
* @param pkg the OPCPackage containing this document
*/
public
POIXMLDocumentPart(
OPCPackage pkg) {
this(
pkg,
PackageRelationshipTypes.
CORE_DOCUMENT);
}
/**
* Construct POIXMLDocumentPart representing a custom "core document" package part.
*
* @param pkg the OPCPackage containing this document
* @param coreDocumentRel the relation type of this document
*/
public
POIXMLDocumentPart(
OPCPackage pkg,
String coreDocumentRel) {
this(
getPartFromOPCPackage(
pkg,
coreDocumentRel));
this.
coreDocumentRel =
coreDocumentRel;
}
/**
* Creates new POIXMLDocumentPart - called by client code to create new parts from scratch.
*
* @see #createRelationship(POIXMLRelation, POIXMLFactory, int, boolean)
*/
public
POIXMLDocumentPart() {
}
/**
* Creates an POIXMLDocumentPart representing the given package part and relationship.
* Called by {@link #read(POIXMLFactory, java.util.Map)} when reading in an existing file.
*
* @param part - The package part that holds xml data representing this sheet.
* @see #read(POIXMLFactory, java.util.Map)
*
* @since POI 3.14-Beta1
*/
public
POIXMLDocumentPart(
PackagePart part) {
this(null,
part);
}
/**
* Creates an POIXMLDocumentPart representing the given package part, relationship and parent
* Called by {@link #read(POIXMLFactory, java.util.Map)} when reading in an existing file.
*
* @param parent - Parent part
* @param part - The package part that holds xml data representing this sheet.
* @see #read(POIXMLFactory, java.util.Map)
*
* @since POI 3.14-Beta1
*/
public
POIXMLDocumentPart(
POIXMLDocumentPart parent,
PackagePart part) {
this.
packagePart =
part;
this.
parent =
parent;
}
/**
* When you open something like a theme, call this to
* re-base the XML Document onto the core child of the
* current core document
*
* @param pkg the package to be rebased
*
* @throws InvalidFormatException if there was an error in the core document relation
* @throws IllegalStateException if there are more than one core document relations
*/
protected final void
rebase(
OPCPackage pkg) throws
InvalidFormatException {
PackageRelationshipCollection cores =
packagePart.
getRelationshipsByType(
coreDocumentRel);
if(
cores.
size() != 1) {
throw new
IllegalStateException(
"Tried to rebase using " +
coreDocumentRel +
" but found " +
cores.
size() + " parts of the right type"
);
}
packagePart =
packagePart.
getRelatedPart(
cores.
getRelationship(0));
}
/**
* Provides access to the underlying PackagePart
*
* @return the underlying PackagePart
*/
public final
PackagePart getPackagePart(){
return
packagePart;
}
/**
* Returns the list of child relations for this POIXMLDocumentPart
*
* @return child relations
*/
public final
List<
POIXMLDocumentPart>
getRelations(){
List<
POIXMLDocumentPart>
l = new
ArrayList<
POIXMLDocumentPart>();
for (
RelationPart rp :
relations.
values()) {
l.
add(
rp.
getDocumentPart());
}
return
Collections.
unmodifiableList(
l);
}
/**
* Returns the list of child relations for this POIXMLDocumentPart
*
* @return child relations
*/
public final
List<
RelationPart>
getRelationParts() {
List<
RelationPart>
l = new
ArrayList<
RelationPart>(
relations.
values());
return
Collections.
unmodifiableList(
l);
}
/**
* Returns the target {@link POIXMLDocumentPart}, where a
* {@link PackageRelationship} is set from the {@link PackagePart} of this
* {@link POIXMLDocumentPart} to the {@link PackagePart} of the target
* {@link POIXMLDocumentPart} with a {@link PackageRelationship#getId()}
* matching the given parameter value.
*
* @param id
* The relation id to look for
* @return the target part of the relation, or null, if none exists
*/
public final
POIXMLDocumentPart getRelationById(
String id) {
RelationPart rp =
relations.
get(
id);
return (
rp == null) ? null :
rp.
getDocumentPart();
}
/**
* Returns the {@link PackageRelationship#getId()} of the
* {@link PackageRelationship}, that sources from the {@link PackagePart} of
* this {@link POIXMLDocumentPart} to the {@link PackagePart} of the given
* parameter value.
*
* @param part
* The {@link POIXMLDocumentPart} for which the according
* relation-id shall be found.
* @return The value of the {@link PackageRelationship#getId()} or null, if
* parts are not related.
*/
public final
String getRelationId(
POIXMLDocumentPart part) {
for (
RelationPart rp :
relations.
values()) {
if (
rp.
getDocumentPart() ==
part) {
return
rp.
getRelationship().
getId();
}
}
return null;
}
/**
* Add a new child POIXMLDocumentPart
*
* @param relId the preferred relation id, when null the next free relation id will be used
* @param relationshipType the package relationship type
* @param part the child to add
*
* @return the new RelationPart
*
* @since 3.14-Beta1
*/
public final
RelationPart addRelation(
String relId,
POIXMLRelation relationshipType,
POIXMLDocumentPart part) {
PackageRelationship pr = this.
packagePart.
findExistingRelation(
part.
getPackagePart());
if (
pr == null) {
PackagePartName ppn =
part.
getPackagePart().
getPartName();
String relType =
relationshipType.
getRelation();
pr =
packagePart.
addRelationship(
ppn,
TargetMode.
INTERNAL,
relType,
relId);
}
addRelation(
pr,
part);
return new
RelationPart(
pr,
part);
}
/**
* Add a new child POIXMLDocumentPart
*
* @param pr the relationship of the child
* @param part the child to add
*/
private void
addRelation(
PackageRelationship pr,
POIXMLDocumentPart part) {
relations.
put(
pr.
getId(), new
RelationPart(
pr,
part));
part.
incrementRelationCounter();
}
/**
* Remove the relation to the specified part in this package and remove the
* part, if it is no longer needed.
*
* @param part the part which relation is to be removed from this document
*/
protected final void
removeRelation(
POIXMLDocumentPart part){
removeRelation(
part,true);
}
/**
* Remove the relation to the specified part in this package and remove the
* part, if it is no longer needed and flag is set to true.
*
* @param part
* The related part, to which the relation shall be removed.
* @param removeUnusedParts
* true, if the part shall be removed from the package if not
* needed any longer.
* @return true, if the relation was removed
*/
protected final boolean
removeRelation(
POIXMLDocumentPart part, boolean
removeUnusedParts){
String id =
getRelationId(
part);
if (
id == null) {
// part is not related with this POIXMLDocumentPart
return false;
}
/* decrement usage counter */
part.
decrementRelationCounter();
/* remove packagepart relationship */
getPackagePart().
removeRelationship(
id);
/* remove POIXMLDocument from relations */
relations.
remove(
id);
if (
removeUnusedParts) {
/* if last relation to target part was removed, delete according target part */
if (
part.
getRelationCounter() == 0) {
try {
part.
onDocumentRemove();
} catch (
IOException e) {
throw new
POIXMLException(
e);
}
getPackagePart().
getPackage().
removePart(
part.
getPackagePart());
}
}
return true;
}
/**
* Returns the parent POIXMLDocumentPart. All parts except root have not-null parent.
*
* @return the parent POIXMLDocumentPart or <code>null</code> for the root element.
*/
public final
POIXMLDocumentPart getParent(){
return
parent;
}
@
Override
public
String toString(){
return
packagePart == null ? "" :
packagePart.
toString();
}
/**
* Save the content in the underlying package part.
* Default implementation is empty meaning that the package part is left unmodified.
*
* Sub-classes should override and add logic to marshal the "model" into Ooxml4J.
*
* For example, the code saving a generic XML entry may look as follows:
* <pre>
* protected void commit() throws IOException {
* PackagePart part = getPackagePart();
* OutputStream out = part.getOutputStream();
* XmlObject bean = getXmlBean(); //the "model" which holds changes in memory
* bean.save(out, DEFAULT_XML_OPTIONS);
* out.close();
* }
* </pre>
*
* @throws IOException a subclass may throw an IOException if the changes can't be committed
*/
protected void
commit() throws
IOException {
}
/**
* Save changes in the underlying OOXML package.
* Recursively fires {@link #commit()} for each package part
*
* @param alreadySaved context set containing already visited nodes
*
* @throws IOException a related part may throw an IOException if the changes can't be saved
*/
protected final void
onSave(
Set<
PackagePart>
alreadySaved) throws
IOException{
// this usually clears out previous content in the part...
prepareForCommit();
commit();
alreadySaved.
add(this.
getPackagePart());
for(
RelationPart rp :
relations.
values()){
POIXMLDocumentPart p =
rp.
getDocumentPart();
if (!
alreadySaved.
contains(
p.
getPackagePart())) {
p.
onSave(
alreadySaved);
}
}
}
/**
* Ensure that a memory based package part does not have lingering data from previous
* commit() calls.
*
* Note: This is overwritten for some objects, as *PictureData seem to store the actual content
* in the part directly without keeping a copy like all others therefore we need to handle them differently.
*/
protected void
prepareForCommit() {
PackagePart part = this.
getPackagePart();
if(
part != null) {
part.
clear();
}
}
/**
* Create a new child POIXMLDocumentPart
*
* @param descriptor the part descriptor
* @param factory the factory that will create an instance of the requested relation
* @return the created child POIXMLDocumentPart
* @throws PartAlreadyExistsException
* If rule M1.12 is not verified : Packages shall not contain
* equivalent part names and package implementers shall neither
* create nor recognize packages with equivalent part names.
*/
public final
POIXMLDocumentPart createRelationship(
POIXMLRelation descriptor,
POIXMLFactory factory){
return
createRelationship(
descriptor,
factory, -1, false).
getDocumentPart();
}
/**
* Create a new child POIXMLDocumentPart
*
* @param descriptor the part descriptor
* @param factory the factory that will create an instance of the requested relation
* @param idx part number
* @return the created child POIXMLDocumentPart
* @throws PartAlreadyExistsException
* If rule M1.12 is not verified : Packages shall not contain
* equivalent part names and package implementers shall neither
* create nor recognize packages with equivalent part names.
*/
public final
POIXMLDocumentPart createRelationship(
POIXMLRelation descriptor,
POIXMLFactory factory, int
idx){
return
createRelationship(
descriptor,
factory,
idx, false).
getDocumentPart();
}
/**
* Identifies the next available part number for a part of the given type,
* if possible, otherwise -1 if none are available.
* The found (valid) index can then be safely given to
* {@link #createRelationship(POIXMLRelation, POIXMLFactory, int)} or
* {@link #createRelationship(POIXMLRelation, POIXMLFactory, int, boolean)}
* without naming clashes.
* If parts with other types are already claiming a name for this relationship
* type (eg a {@link XSSFRelation#CHART} using the drawing part namespace
* normally used by {@link XSSFRelation#DRAWINGS}), those will be considered
* when finding the next spare number.
*
* @param descriptor The relationship type to find the part number for
* @param minIdx The minimum free index to assign, use -1 for any
* @return The next free part number, or -1 if none available
*/
protected final int
getNextPartNumber(
POIXMLRelation descriptor, int
minIdx) {
OPCPackage pkg =
packagePart.
getPackage();
try {
String name =
descriptor.
getDefaultFileName();
if (
name.
equals(
descriptor.
getFileName(9999))) {
// Non-index based, check if default is free
PackagePartName ppName =
PackagingURIHelper.
createPartName(
name);
if (
pkg.
containPart(
ppName)) {
// Default name already taken, not index based, nothing free
return -1;
} else {
// Default name free
return 0;
}
}
// Default to searching from 1, unless they asked for 0+
int
idx = (
minIdx < 0) ? 1 :
minIdx;
int
maxIdx =
minIdx +
pkg.
getParts().
size();
while (
idx <=
maxIdx) {
name =
descriptor.
getFileName(
idx);
PackagePartName ppName =
PackagingURIHelper.
createPartName(
name);
if (!
pkg.
containPart(
ppName)) {
return
idx;
}
idx++;
}
} catch (
InvalidFormatException e) {
// Give a general wrapped exception for the problem
throw new
POIXMLException(
e);
}
return -1;
}
/**
* Create a new child POIXMLDocumentPart
*
* @param descriptor the part descriptor
* @param factory the factory that will create an instance of the requested relation
* @param idx part number
* @param noRelation if true, then no relationship is added.
* @return the created child POIXMLDocumentPart
* @throws PartAlreadyExistsException
* If rule M1.12 is not verified : Packages shall not contain
* equivalent part names and package implementers shall neither
* create nor recognize packages with equivalent part names.
*/
protected final
RelationPart createRelationship(
POIXMLRelation descriptor,
POIXMLFactory factory, int
idx, boolean
noRelation){
try {
PackagePartName ppName =
PackagingURIHelper.
createPartName(
descriptor.
getFileName(
idx));
PackageRelationship rel = null;
PackagePart part =
packagePart.
getPackage().
createPart(
ppName,
descriptor.
getContentType());
if(!
noRelation) {
/* only add to relations, if according relationship is being created. */
rel =
packagePart.
addRelationship(
ppName,
TargetMode.
INTERNAL,
descriptor.
getRelation());
}
POIXMLDocumentPart doc =
factory.
newDocumentPart(
descriptor);
doc.
packagePart =
part;
doc.
parent = this;
if (!
noRelation) {
/* only add to relations, if according relationship is being created. */
addRelation(
rel,
doc);
}
return new
RelationPart(
rel,
doc);
} catch (
PartAlreadyExistsException pae) {
// Return the specific exception so the user knows
// that the name is already taken
throw
pae;
} catch (
Exception e){
// Give a general wrapped exception for the problem
throw new
POIXMLException(
e);
}
}
/**
* Iterate through the underlying PackagePart and create child POIXMLFactory instances
* using the specified factory
*
* @param factory the factory object that creates POIXMLFactory instances
* @param context context map containing already visited noted keyed by targetURI
*
* @throws OpenXML4JException thrown when a related part can't be read
*/
protected void
read(
POIXMLFactory factory,
Map<
PackagePart,
POIXMLDocumentPart>
context) throws
OpenXML4JException {
PackagePart pp =
getPackagePart();
// add mapping a second time, in case of initial caller hasn't done so
POIXMLDocumentPart otherChild =
context.
put(
pp, this);
if (
otherChild != null &&
otherChild != this) {
throw new
POIXMLException("Unique PackagePart-POIXMLDocumentPart relation broken!");
}
if (!
pp.
hasRelationships()) return;
PackageRelationshipCollection rels =
packagePart.
getRelationships();
List<
POIXMLDocumentPart>
readLater = new
ArrayList<
POIXMLDocumentPart>();
// scan breadth-first, so parent-relations are hopefully the shallowest element
for (
PackageRelationship rel :
rels) {
if(
rel.
getTargetMode() ==
TargetMode.
INTERNAL){
URI uri =
rel.
getTargetURI();
// check for internal references (e.g. '#Sheet1!A1')
PackagePartName relName;
if(
uri.
getRawFragment() != null) {
relName =
PackagingURIHelper.
createPartName(
uri.
getPath());
} else {
relName =
PackagingURIHelper.
createPartName(
uri);
}
final
PackagePart p =
packagePart.
getPackage().
getPart(
relName);
if (
p == null) {
logger.
log(
POILogger.
ERROR, "Skipped invalid entry " +
rel.
getTargetURI());
continue;
}
POIXMLDocumentPart childPart =
context.
get(
p);
if (
childPart == null) {
childPart =
factory.
createDocumentPart(this,
p);
childPart.
parent = this;
// already add child to context, so other children can reference it
context.
put(
p,
childPart);
readLater.
add(
childPart);
}
addRelation(
rel,
childPart);
}
}
for (
POIXMLDocumentPart childPart :
readLater) {
childPart.
read(
factory,
context);
}
}
/**
* Get the PackagePart that is the target of a relationship from this Part.
*
* @param rel The relationship
* @return The target part
* @throws InvalidFormatException thrown if the related part has is erroneous
*/
protected
PackagePart getTargetPart(
PackageRelationship rel) throws
InvalidFormatException {
return
getPackagePart().
getRelatedPart(
rel);
}
/**
* Fired when a new package part is created
*
* @throws IOException a subclass may throw an IOException on document creation
*/
protected void
onDocumentCreate() throws
IOException {
}
/**
* Fired when a package part is read
*
* @throws IOException a subclass may throw an IOException when a document is read
*/
protected void
onDocumentRead() throws
IOException {
}
/**
* Fired when a package part is about to be removed from the package
*
* @throws IOException a subclass may throw an IOException when a document is removed
*/
protected void
onDocumentRemove() throws
IOException {
}
/**
* Internal method, do not use!
* <p>
* This method only exists to allow access to protected {@link POIXMLDocumentPart#onDocumentRead()}
* from {@link org.apache.poi.xwpf.usermodel.XWPFDocument} without reflection. It should be removed.
*
* @param part the part which is to be read
*
* @throws IOException if the part can't be read
*/
@
Internal @
Deprecated
public static void
_invokeOnDocumentRead(
POIXMLDocumentPart part) throws
IOException {
part.
onDocumentRead();
}
/**
* Retrieves the core document part
*
* @since POI 3.14-Beta1
*/
private static
PackagePart getPartFromOPCPackage(
OPCPackage pkg,
String coreDocumentRel) {
PackageRelationship coreRel =
pkg.
getRelationshipsByType(
coreDocumentRel).
getRelationship(0);
if (
coreRel != null) {
PackagePart pp =
pkg.
getPart(
coreRel);
if (
pp == null) {
throw new
POIXMLException("OOXML file structure broken/invalid - core document '"+
coreRel.
getTargetURI()+"' not found.");
}
return
pp;
}
coreRel =
pkg.
getRelationshipsByType(
PackageRelationshipTypes.
STRICT_CORE_DOCUMENT).
getRelationship(0);
if (
coreRel != null) {
throw new
POIXMLException("Strict OOXML isn't currently supported, please see bug #57699");
}
throw new
POIXMLException("OOXML file structure broken/invalid - no core document found!");
}
}