/*
* Copyright (c) 2006, Wygwam
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
* - Neither the name of Wygwam nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.docx4j.openpackaging.parts;
import java.net.URI;
import java.net.URISyntaxException;
import org.docx4j.openpackaging.Base;
import org.docx4j.openpackaging.URIHelper;
import org.docx4j.openpackaging.exceptions.Docx4JRuntimeException;
import org.docx4j.openpackaging.exceptions.InvalidFormatException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An immutable Open Packaging Convention compliant part name.
*
* [Docx4J comment: Note that in docx4J, part names should be resolved,
* before being set, so that they are absolute
* (ie start with '/'). In contrast, this class enforces the
* OPC specification, which says that a part name can't be
* absolute. For this reason, you'll see the leading '/'
* being added and removed in various places :( ]
*
*
* @author Julien Chable
* @version 0.1
*/
public final class PartName implements Comparable<PartName> {
private static Logger log = LoggerFactory.getLogger(PartName.class);
/**
* Part name stored as an URI.
*/
private URI partNameURI;
/*
* URI Characters definition (RFC 3986)
*/
/**
* Reserved characters for sub delimitations.
*/
private static String[] RFC3986_PCHAR_SUB_DELIMS = { "!", "$", "&", "'",
"(", ")", "*", "+", ",", ";", "=" };
/**
* Unreserved character (+ ALPHA & DIGIT).
*/
private static String[] RFC3986_PCHAR_UNRESERVED_SUP = { "-", ".", "_", "~" };
/**
* Authorized reserved characters for pChar.
*/
private static String[] RFC3986_PCHAR_AUTHORIZED_SUP = { ":", "@" };
/**
* Flag to know if this part name is from a relationship part name.
*/
private boolean isRelationship;
/**
* Constructor. Makes a ValidPartName object from a java.net.URI
*
* @param uri
* The URI to validate and to transform into ValidPartName.
* @param checkConformance
* Flag to specify if the contructor have to validate the OPC
* conformance. Must be always <code>true</code> except for
* special URI like '/' which is needed for internal use by
* OpenXML4J but is not valid.
* @throws InvalidFormatException
* Throw if the specified part name is not conform to Open
* Packaging Convention specifications.
* @see java.net.URI
*/
public PartName(URI uri, boolean checkConformance)
throws InvalidFormatException {
if (checkConformance) {
throwExceptionIfInvalidPartUri(uri);
} else {
if (!URIHelper.PACKAGE_ROOT_URI.equals(uri)) {
throw new Docx4JRuntimeException(
"OCP conformance must be check for ALL part name except special cases : ['/']");
}
}
this.partNameURI = uri;
this.isRelationship = isRelationshipPartURI(this.partNameURI);
//log.debug( getName() + " part name created.");
}
/**
* Constructor. Makes a ValidPartName object from a String part name,
* provided it validates against OPC conformance.
*
* @param partName
* Part name to valid and to create.
* @throws InvalidFormatException
* Throw if the specified part name is not conform to Open
* Packaging Convention specifications.
*/
public PartName(String partName)
throws InvalidFormatException {
this(partName, true);
}
/**
* Constructor. Makes a ValidPartName object from a String part name.
*
* @param partName
* Part name to valid and to create.
* @param checkConformance
* Flag to specify if the contructor have to validate the OPC
* conformance. Must be always <code>true</code> except for
* special URI like '/' which is needed for internal use by
* OpenXML4J but is not valid.
* @throws InvalidFormatException
* Throw if the specified part name is not conform to Open
* Packaging Convention specifications.
*/
public PartName(String partName, boolean checkConformance)
throws InvalidFormatException {
// log.debug( "Trying to create part name " + partName);
URI partURI;
try {
partURI = new URI(partName);
} catch (URISyntaxException e) {
log.error( e.getMessage() );
throw new IllegalArgumentException(
"partName argmument is not a valid OPC part name !");
}
if (checkConformance) {
throwExceptionIfInvalidPartUri(partURI);
} else {
if (!URIHelper.PACKAGE_ROOT_URI.equals(partURI)) {
throw new Docx4JRuntimeException(
"OCP conformance must be check for ALL part name except special cases : ['/']");
}
}
this.partNameURI = partURI;
this.isRelationship = isRelationshipPartURI(this.partNameURI);
//log.debug( getName() + " part name created.");
}
/**
* Check if the specified part name is a relationship part name.
*
* @param partUri
* The URI to check.
* @return <code>true</code> if this part name respect the relationship
* part naming convention else <code>false</code>.
*/
private boolean isRelationshipPartURI(URI partUri) {
if (partUri == null)
throw new IllegalArgumentException("partUri");
return partUri.getPath().matches(
".*" + URIHelper.RELATIONSHIP_PART_SEGMENT_NAME + ".*"
+ URIHelper.RELATIONSHIP_PART_EXTENSION_NAME
+ "$");
}
/**
* To know if this part name is a relationship part name.
*
* @return <code>true</code> if this part name respect the relationship
* part naming convention else <code>false</code>.
*/
public boolean isRelationshipPartURI() {
return this.isRelationship;
}
/**
* Throws an exception (of any kind) if the specified part name does not
* follow the Open Packaging Convention specifications naming rules.
*
* @param partUri
* The part name to check.
* @throws Exception
* Throws if the part name is invalid.
*/
private static void throwExceptionIfInvalidPartUri(URI partUri)
throws InvalidFormatException {
if (partUri == null)
throw new IllegalArgumentException("partUri");
// Check if the part name URI is empty [M1.1]
throwExceptionIfEmptyURI(partUri);
// Check if the part name URI is absolute
throwExceptionIfAbsoluteUri(partUri);
// Check if the part name URI doesn't start with a forward slash [M1.4]
throwExceptionIfPartNameNotStartsWithForwardSlashChar(partUri);
// Check if the part name URI ends with a forward slash [M1.5]
throwExceptionIfPartNameEndsWithForwardSlashChar(partUri);
// Check if the part name does not have empty segments. [M1.3]
// Check if a segment ends with a dot ('.') character. [M1.9]
throwExceptionIfPartNameHaveInvalidSegments(partUri);
}
/**
* Throws an exception if the specified URI is empty. [M1.1]
*
* @param partURI
* Part URI to check.
* @throws InvalidFormatException
* If the specified URI is empty.
*/
private static void throwExceptionIfEmptyURI(URI partURI)
throws InvalidFormatException {
if (partURI == null) {
throw new IllegalArgumentException("partURI");
}
String uriPath = partURI.getPath();
if (uriPath.length() == 0
|| ((uriPath.length() == 1) && (uriPath.charAt(0) == URIHelper.FORWARD_SLASH_CHAR))) {
throw new InvalidFormatException(
"A part name shall not be empty [M1.1]: "
+ partURI.getPath());
}
}
/**
* Throws an exception if the part name has empty segments. [M1.3]
*
* Throws an exception if a segment any characters other than pchar
* characters. [M1.6]
*
* Throws an exception if a segment contain percent-encoded forward slash
* ('/'), or backward slash ('\') characters. [M1.7]
*
* Throws an exception if a segment contain percent-encoded unreserved
* characters. [M1.8]
*
* Throws an exception if the specified part name's segments end with a dot
* ('.') character. [M1.9]
*
* Throws an exception if a segment doesn't include at least one non-dot
* character. [M1.10]
*
* @param partUri
* The part name to check.
* @throws InvalidFormatException
* if the specified URI contain an empty segments or if one the
* segments contained in the part name, ends with a dot ('.')
* character.
*/
private static void throwExceptionIfPartNameHaveInvalidSegments(URI partUri)
throws InvalidFormatException {
if (partUri == null || "".equals(partUri)) {
throw new IllegalArgumentException("partUri");
}
// Split the URI into several part and analyze each
String[] segments = partUri.toASCIIString().split("/");
if (segments.length <= 1 || !segments[0].equals("")) {
log.error( "" );
throw new InvalidFormatException(
"A part name shall not have empty segments [M1.3]: "
+ partUri.getPath());
}
for (int i = 1; i < segments.length; ++i) {
String seg = segments[i];
if (seg == null || "".equals(seg)) {
log.error( "" );
throw new InvalidFormatException(
"A part name shall not have empty segments [M1.3]: "
+ partUri.getPath());
}
if (seg.endsWith(".")) {
log.error( "" );
throw new InvalidFormatException(
"A segment shall not end with a dot ('.') character [M1.9]: "
+ partUri.getPath());
}
if ("".equals(seg.replaceAll("\\\\.", ""))) {
// Normally will never been invoked with the previous
// implementation rule [M1.9]
log.error( "" );
throw new InvalidFormatException(
"A segment shall include at least one non-dot character. [M1.10]: "
+ partUri.getPath());
}
/*
* Check for rule M1.6, M1.7, M1.8
*/
checkPCharCompliance(seg);
}
}
/**
* Throws an exception if a segment any characters other than pchar
* characters. [M1.6]
*
* Throws an exception if a segment contain percent-encoded forward slash
* ('/'), or backward slash ('\') characters. [M1.7]
*
* Throws an exception if a segment contain percent-encoded unreserved
* characters. [M1.8]
*
* @param segment
* The segment to check
*/
private static void checkPCharCompliance(String segment)
throws InvalidFormatException {
boolean errorFlag;
for (int i = 0; i < segment.length(); ++i) {
char c = segment.charAt(i);
errorFlag = true;
/* Check rule M1.6 */
// Check for digit or letter
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
|| (c >= '0' && c <= '9')) {
errorFlag = false;
} else {
// Check "-", ".", "_", "~"
for (int j = 0; j < RFC3986_PCHAR_UNRESERVED_SUP.length; ++j) {
if (c == RFC3986_PCHAR_UNRESERVED_SUP[j].charAt(0)) {
errorFlag = false;
break;
}
}
// Check ":", "@"
for (int j = 0; errorFlag
&& j < RFC3986_PCHAR_AUTHORIZED_SUP.length; ++j) {
if (c == RFC3986_PCHAR_AUTHORIZED_SUP[j].charAt(0)) {
errorFlag = false;
}
}
// Check "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="
for (int j = 0; errorFlag
&& j < RFC3986_PCHAR_SUB_DELIMS.length; ++j) {
if (c == RFC3986_PCHAR_SUB_DELIMS[j].charAt(0)) {
errorFlag = false;
}
}
}
if (errorFlag && c == '%') {
// We certainly found an encoded character, check for length
// now ( '%' HEXDIGIT HEXDIGIT)
if (((segment.length() - i) < 2)) {
log.error( "" );
throw new InvalidFormatException("The segment " + segment
+ " contain invalid encoded character !");
}
// If not percent encoded character error occur then reset the
// flag -> the character is valid
errorFlag = false;
// Decode the encoded character
char decodedChar = (char) Integer.parseInt(segment.substring(
i + 1, i + 3), 16);
i += 2;
/* Check rule M1.7 */
if (decodedChar == '/' || decodedChar == '\\') {
log.error( "" );
throw new InvalidFormatException(
"A segment shall not contain percent-encoded forward slash ('/'), or backward slash ('\') characters. [M1.7]");
}
/* Check rule M1.8 */
// Check for unreserved character like define in RFC3986
if ((decodedChar >= 'A' && decodedChar <= 'Z')
|| (decodedChar >= 'a' && decodedChar <= 'z')
|| (decodedChar >= '0' && decodedChar <= '9'))
errorFlag = true;
// Check for unreserved character "-", ".", "_", "~"
for (int j = 0; !errorFlag
&& j < RFC3986_PCHAR_UNRESERVED_SUP.length; ++j) {
if (c == RFC3986_PCHAR_UNRESERVED_SUP[j].charAt(0)) {
errorFlag = true;
break;
}
}
if (errorFlag) {
log.error( "" );
throw new InvalidFormatException(
"A segment shall not contain percent-encoded unreserved characters. [M1.8]");
}
}
if (errorFlag)
{
log.error( "" );
throw new InvalidFormatException(
"A segment shall not hold any characters other than pchar characters. [M1.6]");
}
}
}
/**
* Throws an exception if the specified part name doesn't start with a
* forward slash character '/'. [M1.4]
*
* @param partUri
* The part name to check.
* @throws InvalidFormatException
* If the specified part name doesn't start with a forward slash
* character '/'.
*/
private static void throwExceptionIfPartNameNotStartsWithForwardSlashChar(
URI partUri) throws InvalidFormatException {
String uriPath = partUri.getPath();
if (uriPath.length() > 0
&& uriPath.charAt(0) != URIHelper.FORWARD_SLASH_CHAR)
{
log.error( "" );
throw new InvalidFormatException(
"A part name shall start with a forward slash ('/') character [M1.4]: "
+ partUri.getPath());
}
}
/**
* Throws an exception if the specified part name ends with a forward slash
* character '/'. [M1.5]
*
* @param partUri
* The part name to check.
* @throws InvalidFormatException
* If the specified part name ends with a forwar slash character
* '/'.
*/
private static void throwExceptionIfPartNameEndsWithForwardSlashChar(
URI partUri) throws InvalidFormatException {
String uriPath = partUri.getPath();
if (uriPath.length() > 0
&& uriPath.charAt(uriPath.length() - 1) == URIHelper.FORWARD_SLASH_CHAR) {
log.error( "" );
throw new InvalidFormatException(
"A part name shall not have a forward slash as the last character [M1.5]: "
+ partUri.getPath());
}
}
/**
* Throws an exception if the specified URI is absolute.
*
* @param partUri
* The URI to check.
* @throws InvalidFormatException
* Throws if the specified URI is absolute.
*/
private static void throwExceptionIfAbsoluteUri(URI partUri)
throws InvalidFormatException {
if (partUri.isAbsolute()) {
log.error( "" );
throw new InvalidFormatException("Absolute URI forbidden: "
+ partUri);
}
}
/**
* Compare two part name following the rule M1.12 :
*
* Part name equivalence is determined by comparing part names as
* case-insensitive ASCII strings. Packages shall not contain equivalent
* part names and package implementers shall neither create nor recognize
* packages with equivalent part names. [M1.12]
*/
public int compareTo(PartName otherPartName) {
if (otherPartName == null)
return -1;
return this.partNameURI.toASCIIString().toLowerCase().compareTo(
otherPartName.partNameURI.toASCIIString().toLowerCase());
}
/**
* Retrieves the extension of the part name if any. If there is no extension
* returns an empty String. Example : '/document/content.xml' => 'xml'
*
* @return The extension of the part name.
*/
public String getExtension() {
String fragment = this.partNameURI.getPath();
if (fragment.length() > 0) {
int i = fragment.lastIndexOf(".");
if (i > -1)
return fragment.substring(i + 1);
}
return "";
}
/**
* Get this part name.
*
* @return The name of this part name.
*/
public String getName() {
return this.partNameURI.toASCIIString();
}
/**
* Part name equivalence is determined by comparing part names as
* case-insensitive ASCII strings. Packages shall not contain equivalent
* part names and package implementers shall neither create nor recognize
* packages with equivalent part names. [M1.12]
*/
@Override
public boolean equals(Object otherPartName) {
if (otherPartName == null
|| !(otherPartName instanceof PartName))
return false;
return this.partNameURI.toASCIIString().toLowerCase().equals(
((PartName) otherPartName).partNameURI.toASCIIString()
.toLowerCase());
}
@Override
public int hashCode() {
return this.partNameURI.toASCIIString().toLowerCase().hashCode();
}
@Override
public String toString() {
return getName();
}
/* Getters and setters */
/**
* Part name property getter.
*
* @return This part name URI.
*/
public URI getURI() {
return this.partNameURI;
}
public static String generateUniqueName(Base sourcePart, String proposedRelId,
String directoryPrefix, String after_, String ext) {
// In order to ensure unique part name,
// idea is to use the relId, which ought to be unique
// Also need partName, since images for different parts are stored in a common dir
String sourcepartName = sourcePart.getPartName().getName();
int beginIndex = sourcepartName.lastIndexOf("/")+1;
int endIndex = sourcepartName.lastIndexOf(".");
String partPrefix = sourcepartName.substring(beginIndex, endIndex);
return directoryPrefix + partPrefix + "_" + after_ + "_" + proposedRelId + "." + ext;
}
/** Return the path of this PartName
* ie up to and including its last '/',
* but excluding the filename segment. */
// static public String base(String partName) {
// // word/document.xml --->
// // word/
// String relationshipsPartName = null;
//
// log.info("Splitting " + partName );
//
// // Split partName at its last "/"
// int pos = partName.lastIndexOf("/");
//
// return partName.substring(0, pos) + "/";
//
// }
/**
* @see URIHelper.getRelationshipPartName for Wygwam's
* implementation which I only found after writing this
*/
static public String getRelationshipsPartName(String partName) {
// word/document.xml --->
// word/_rels/document.xml.rels
String relationshipsPartName = null;
// log.info("Splitting " + partName );
String rightBit = partName;
// Split partName at its last "/"
int pos = partName.lastIndexOf("/");
if (pos>0) {
String leftBit = partName.substring(0, pos);
rightBit = partName.substring(pos);
// log.info("**" + leftBit + "/_rels" + rightBit + ".rels" );
return leftBit + "/_rels" + rightBit + ".rels" ;
} else {
// eg partname: foo.ext (ie in root)
if (!rightBit.startsWith("/"))
rightBit="/" + rightBit;
// log.info("**" + "/_rels" + rightBit + ".rels" );
return "/_rels" + rightBit + ".rels" ;
}
}
/*
* OpenXML4J's perhaps more robust alternative implementation
* of above.
*
* Build a part name where the relationship should be stored ((ex
* /word/document.xml -> /word/_rels/document.xml.rels)
*
* @param partUri
* Source part URI
* @return the full path (as URI) of the relation file
* @throws InvalidOperationException
* Throws if the specified URI is a relationshp part.
* @see PartName.getRelationshipsPartName which is the
* implementation I use in the IO package.
public static PartName getRelationshipPartName(
PartName partName) {
if (partName == null)
throw new IllegalArgumentException("partName");
if (URIHelper.PACKAGE_ROOT_URI.getPath() == partName.getURI()
.getPath())
return URIHelper.PACKAGE_RELATIONSHIPS_ROOT_PART_NAME;
if (partName.isRelationshipPartURI())
throw new InvalidOperationException("Can't be a relationship part");
String fullPath = partName.getURI().getPath();
String filename = getFilename(partName.getURI());
fullPath = fullPath.substring(0, fullPath.length() - filename.length());
fullPath = combine(fullPath,
URIHelper.RELATIONSHIP_PART_SEGMENT_NAME);
fullPath = combine(fullPath, filename);
fullPath = fullPath
+ URIHelper.RELATIONSHIP_PART_EXTENSION_NAME;
PartName retPartName;
try {
retPartName = createPartName(fullPath);
} catch (InvalidFormatException e) {
// Should never happen in production as all data are fixed but in
// case of return null:
return null;
}
return retPartName;
} */
}