| /* ==================================================================== |
| 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.openxml4j.opc.internal; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.util.Locale; |
| import java.util.Map.Entry; |
| import java.util.TreeMap; |
| |
| import org.apache.poi.ooxml.util.DocumentHelper; |
| import org.apache.poi.openxml4j.exceptions.InvalidFormatException; |
| import org.apache.poi.openxml4j.exceptions.InvalidOperationException; |
| import org.apache.poi.openxml4j.exceptions.OpenXML4JRuntimeException; |
| import org.apache.poi.openxml4j.opc.OPCPackage; |
| import org.apache.poi.openxml4j.opc.PackageNamespaces; |
| import org.apache.poi.openxml4j.opc.PackagePart; |
| import org.apache.poi.openxml4j.opc.PackagePartName; |
| import org.apache.poi.openxml4j.opc.PackagingURIHelper; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.NodeList; |
| import org.xml.sax.SAXException; |
| |
| /** |
| * Manage package content types ([Content_Types].xml part). |
| * |
| * @author Julien Chable |
| */ |
| public abstract class ContentTypeManager { |
| |
| /** |
| * Content type part name. |
| */ |
| public static final String CONTENT_TYPES_PART_NAME = "[Content_Types].xml"; |
| |
| /** |
| * Content type namespace |
| */ |
| public static final String TYPES_NAMESPACE_URI = PackageNamespaces.CONTENT_TYPES; |
| |
| /* Xml elements in content type part */ |
| |
| private static final String TYPES_TAG_NAME = "Types"; |
| |
| private static final String DEFAULT_TAG_NAME = "Default"; |
| |
| private static final String EXTENSION_ATTRIBUTE_NAME = "Extension"; |
| |
| private static final String CONTENT_TYPE_ATTRIBUTE_NAME = "ContentType"; |
| |
| private static final String OVERRIDE_TAG_NAME = "Override"; |
| |
| private static final String PART_NAME_ATTRIBUTE_NAME = "PartName"; |
| |
| /** |
| * Reference to the package using this content type manager. |
| */ |
| protected OPCPackage container; |
| |
| /** |
| * Default content type tree. <Extension, ContentType> |
| */ |
| private TreeMap<String, String> defaultContentType; |
| |
| /** |
| * Override content type tree. |
| */ |
| private TreeMap<PackagePartName, String> overrideContentType; |
| |
| /** |
| * Constructor. Parses the content of the specified input stream. |
| * |
| * @param in |
| * If different of <i>null</i> then the content types part is |
| * retrieve and parse. |
| * @throws InvalidFormatException |
| * If the content types part content is not valid. |
| */ |
| public ContentTypeManager(InputStream in, OPCPackage pkg) |
| throws InvalidFormatException { |
| this.container = pkg; |
| this.defaultContentType = new TreeMap<>(); |
| if (in != null) { |
| try { |
| parseContentTypesFile(in); |
| } catch (InvalidFormatException e) { |
| InvalidFormatException ex = new InvalidFormatException("Can't read content types part !"); |
| |
| // here it is useful to add the cause to not loose the original stack-trace |
| ex.initCause(e); |
| |
| throw ex; |
| } |
| } |
| } |
| |
| /** |
| * Build association extention-> content type (will be stored in |
| * [Content_Types].xml) for example ContentType="image/png" Extension="png" |
| * <p> |
| * [M2.8]: When adding a new part to a package, the package implementer |
| * shall ensure that a content type for that part is specified in the |
| * Content Types stream; the package implementer shall perform the steps |
| * described in §9.1.2.3: |
| * </p><p> |
| * 1. Get the extension from the part name by taking the substring to the |
| * right of the rightmost occurrence of the dot character (.) from the |
| * rightmost segment. |
| * </p><p> |
| * 2. If a part name has no extension, a corresponding Override element |
| * shall be added to the Content Types stream. |
| * </p><p> |
| * 3. Compare the resulting extension with the values specified for the |
| * Extension attributes of the Default elements in the Content Types stream. |
| * The comparison shall be case-insensitive ASCII. |
| * </p><p> |
| * 4. If there is a Default element with a matching Extension attribute, |
| * then the content type of the new part shall be compared with the value of |
| * the ContentType attribute. The comparison might be case-sensitive and |
| * include every character regardless of the role it plays in the |
| * content-type grammar of RFC 2616, or it might follow the grammar of RFC |
| * 2616. |
| * </p><p> |
| * a. If the content types match, no further action is required. |
| * </p><p> |
| * b. If the content types do not match, a new Override element shall be |
| * added to the Content Types stream. . |
| * </p><p> |
| * 5. If there is no Default element with a matching Extension attribute, a |
| * new Default element or Override element shall be added to the Content |
| * Types stream. |
| * </p> |
| */ |
| public void addContentType(PackagePartName partName, String contentType) { |
| boolean defaultCTExists = this.defaultContentType.containsValue(contentType); |
| String extension = partName.getExtension().toLowerCase(Locale.ROOT); |
| if ((extension.length() == 0) || |
| // check if content-type and extension do match in both directions |
| // some applications create broken files, e.g. extension "jpg" instead of "jpeg" |
| (this.defaultContentType.containsKey(extension) && !defaultCTExists) || |
| (!this.defaultContentType.containsKey(extension) && defaultCTExists)) { |
| this.addOverrideContentType(partName, contentType); |
| } else if (!defaultCTExists) { |
| this.addDefaultContentType(extension, contentType); |
| } |
| } |
| |
| /** |
| * Add an override content type for a specific part. |
| * |
| * @param partName |
| * Name of the part. |
| * @param contentType |
| * Content type of the part. |
| */ |
| private void addOverrideContentType(PackagePartName partName, |
| String contentType) { |
| if (overrideContentType == null) { |
| overrideContentType = new TreeMap<>(); |
| } |
| overrideContentType.put(partName, contentType); |
| } |
| |
| /** |
| * Add a content type associated with the specified extension. |
| * |
| * @param extension |
| * The part name extension to bind to a content type. |
| * @param contentType |
| * The content type associated with the specified extension. |
| */ |
| private void addDefaultContentType(String extension, String contentType) { |
| // Remark : Originally the latest parameter was : |
| // contentType.toLowerCase(). Change due to a request ID 1996748. |
| defaultContentType.put(extension.toLowerCase(Locale.ROOT), contentType); |
| } |
| |
| /** |
| * <p> |
| * Delete a content type based on the specified part name. If the specified |
| * part name is register with an override content type, then this content |
| * type is remove, else the content type is remove in the default content |
| * type list if it exists and if no part is associated with it yet. |
| * </p><p> |
| * Check rule M2.4: The package implementer shall require that the Content |
| * Types stream contain one of the following for every part in the package: |
| * One matching Default element One matching Override element Both a |
| * matching Default element and a matching Override element, in which case |
| * the Override element takes precedence. |
| * </p> |
| * @param partName |
| * The part URI associated with the override content type to |
| * delete. |
| * @exception InvalidOperationException |
| * Throws if |
| */ |
| public void removeContentType(PackagePartName partName) |
| throws InvalidOperationException { |
| if (partName == null) { |
| throw new IllegalArgumentException("partName"); |
| } |
| |
| /* Override content type */ |
| if (this.overrideContentType != null |
| && (this.overrideContentType.get(partName) != null)) { |
| // Remove the override definition for the specified part. |
| this.overrideContentType.remove(partName); |
| return; |
| } |
| |
| /* Default content type */ |
| String extensionToDelete = partName.getExtension(); |
| boolean deleteDefaultContentTypeFlag = true; |
| if (this.container != null) { |
| try { |
| for (PackagePart part : this.container.getParts()) { |
| if (!part.getPartName().equals(partName) |
| && part.getPartName().getExtension() |
| .equalsIgnoreCase(extensionToDelete)) { |
| deleteDefaultContentTypeFlag = false; |
| break; |
| } |
| } |
| } catch (InvalidFormatException e) { |
| throw new InvalidOperationException(e.getMessage()); |
| } |
| } |
| |
| // Remove the default content type, no other part use this content type. |
| if (deleteDefaultContentTypeFlag) { |
| this.defaultContentType.remove(extensionToDelete); |
| } |
| |
| /* |
| * Check rule 2.4: The package implementer shall require that the |
| * Content Types stream contain one of the following for every part in |
| * the package: One matching Default element One matching Override |
| * element Both a matching Default element and a matching Override |
| * element, in which case the Override element takes precedence. |
| */ |
| if (this.container != null) { |
| try { |
| for (PackagePart part : this.container.getParts()) { |
| if (!part.getPartName().equals(partName) |
| && this.getContentType(part.getPartName()) == null) { |
| throw new InvalidOperationException( |
| "Rule M2.4 is not respected: Nor a default element or override element is associated with the part: " |
| + part.getPartName().getName()); |
| } |
| } |
| } catch (InvalidFormatException e) { |
| throw new InvalidOperationException(e.getMessage()); |
| } |
| } |
| } |
| |
| /** |
| * Check if the specified content type is already register. |
| * |
| * @param contentType |
| * The content type to check. |
| * @return <code>true</code> if the specified content type is already |
| * register, then <code>false</code>. |
| */ |
| public boolean isContentTypeRegister(String contentType) { |
| if (contentType == null) { |
| throw new IllegalArgumentException("contentType"); |
| } |
| |
| return (this.defaultContentType.containsValue(contentType) || |
| (this.overrideContentType != null && this.overrideContentType.containsValue(contentType))); |
| } |
| |
| /** |
| * Get the content type for the specified part, if any. |
| * <p> |
| * Rule [M2.9]: To get the content type of a part, the package implementer |
| * shall perform the steps described in §9.1.2.4: |
| * </p><p> |
| * 1. Compare the part name with the values specified for the PartName |
| * attribute of the Override elements. The comparison shall be |
| * case-insensitive ASCII. |
| * </p><p> |
| * 2. If there is an Override element with a matching PartName attribute, |
| * return the value of its ContentType attribute. No further action is |
| * required. |
| * </p><p> |
| * 3. If there is no Override element with a matching PartName attribute, |
| * then a. Get the extension from the part name by taking the substring to |
| * the right of the rightmost occurrence of the dot character (.) from the |
| * rightmost segment. b. Check the Default elements of the Content Types |
| * stream, comparing the extension with the value of the Extension |
| * attribute. The comparison shall be case-insensitive ASCII. |
| * </p><p> |
| * 4. If there is a Default element with a matching Extension attribute, |
| * return the value of its ContentType attribute. No further action is |
| * required. |
| * </p><p> |
| * 5. If neither Override nor Default elements with matching attributes are |
| * found for the specified part name, the implementation shall not map this |
| * part name to a part. |
| * </p> |
| * @param partName |
| * The URI part to check. |
| * @return The content type associated with the URI (in case of an override |
| * content type) or the extension (in case of default content type), |
| * else <code>null</code>. |
| * |
| * @exception OpenXML4JRuntimeException |
| * Throws if the content type manager is not able to find the |
| * content from an existing part. |
| */ |
| public String getContentType(PackagePartName partName) { |
| if (partName == null) { |
| throw new IllegalArgumentException("partName"); |
| } |
| |
| if ((this.overrideContentType != null) |
| && this.overrideContentType.containsKey(partName)) { |
| return this.overrideContentType.get(partName); |
| } |
| |
| String extension = partName.getExtension().toLowerCase(Locale.ROOT); |
| if (this.defaultContentType.containsKey(extension)) { |
| return this.defaultContentType.get(extension); |
| } |
| |
| /* |
| * [M2.4] : The package implementer shall require that the Content Types |
| * stream contain one of the following for every part in the package: |
| * One matching Default element, One matching Override element, Both a |
| * matching Default element and a matching Override element, in which |
| * case the Override element takes precedence. |
| */ |
| if (this.container != null && this.container.getPart(partName) != null) { |
| throw new OpenXML4JRuntimeException( |
| "Rule M2.4 exception : Part \'" |
| + partName |
| + "\' not found - this error should NEVER happen!\n" |
| + "Check that your code is closing the open resources in the correct order prior to filing a bug report.\n" |
| + "If you can provide the triggering file, then please raise a bug at https://bz.apache.org/bugzilla/enter_bug.cgi?product=POI and attach the file that triggers it, thanks!"); |
| } |
| return null; |
| } |
| |
| /** |
| * Clear all content types. |
| */ |
| public void clearAll() { |
| this.defaultContentType.clear(); |
| if (this.overrideContentType != null) { |
| this.overrideContentType.clear(); |
| } |
| } |
| |
| /** |
| * Clear all override content types. |
| * |
| */ |
| public void clearOverrideContentTypes() { |
| if (this.overrideContentType != null) { |
| this.overrideContentType.clear(); |
| } |
| } |
| |
| /** |
| * Parse the content types part. |
| * |
| * @throws InvalidFormatException |
| * Throws if the content type doesn't exist or the XML format is |
| * invalid. |
| */ |
| private void parseContentTypesFile(InputStream in) |
| throws InvalidFormatException { |
| try { |
| Document xmlContentTypetDoc = DocumentHelper.readDocument(in); |
| |
| // Default content types |
| NodeList defaultTypes = xmlContentTypetDoc.getDocumentElement().getElementsByTagNameNS(TYPES_NAMESPACE_URI, DEFAULT_TAG_NAME); |
| int defaultTypeCount = defaultTypes.getLength(); |
| for (int i = 0; i < defaultTypeCount; i++) { |
| Element element = (Element) defaultTypes.item(i); |
| String extension = element.getAttribute(EXTENSION_ATTRIBUTE_NAME); |
| String contentType = element.getAttribute(CONTENT_TYPE_ATTRIBUTE_NAME); |
| addDefaultContentType(extension, contentType); |
| } |
| |
| // Overriden content types |
| NodeList overrideTypes = xmlContentTypetDoc.getDocumentElement().getElementsByTagNameNS(TYPES_NAMESPACE_URI, OVERRIDE_TAG_NAME); |
| int overrideTypeCount = overrideTypes.getLength(); |
| for (int i = 0; i < overrideTypeCount; i++) { |
| Element element = (Element) overrideTypes.item(i); |
| URI uri = new URI(element.getAttribute(PART_NAME_ATTRIBUTE_NAME)); |
| PackagePartName partName = PackagingURIHelper.createPartName(uri); |
| String contentType = element.getAttribute(CONTENT_TYPE_ATTRIBUTE_NAME); |
| addOverrideContentType(partName, contentType); |
| } |
| } catch (URISyntaxException | IOException | SAXException e) { |
| throw new InvalidFormatException(e.getMessage()); |
| } |
| } |
| |
| /** |
| * Save the contents type part. |
| * |
| * @param outStream |
| * The output stream use to save the XML content of the content |
| * types part. |
| * @return <b>true</b> if the operation success, else <b>false</b>. |
| */ |
| public boolean save(OutputStream outStream) { |
| Document xmlOutDoc = DocumentHelper.createDocument(); |
| |
| // Building namespace |
| Element typesElem = xmlOutDoc.createElementNS(TYPES_NAMESPACE_URI, TYPES_TAG_NAME); |
| xmlOutDoc.appendChild(typesElem); |
| |
| // Adding default types |
| for (Entry<String, String> entry : defaultContentType.entrySet()) { |
| appendDefaultType(typesElem, entry); |
| } |
| |
| // Adding specific types if any exist |
| if (overrideContentType != null) { |
| for (Entry<PackagePartName, String> entry : overrideContentType |
| .entrySet()) { |
| appendSpecificTypes(typesElem, entry); |
| } |
| } |
| xmlOutDoc.normalize(); |
| |
| // Save content in the specified output stream |
| return this.saveImpl(xmlOutDoc, outStream); |
| } |
| |
| /** |
| * Use to append specific type XML elements, use by the save() method. |
| * |
| * @param root |
| * XML parent element use to append this override type element. |
| * @param entry |
| * The values to append. |
| * @see #save(java.io.OutputStream) |
| */ |
| private void appendSpecificTypes(Element root, |
| Entry<PackagePartName, String> entry) { |
| Element specificType = root.getOwnerDocument().createElementNS(TYPES_NAMESPACE_URI, OVERRIDE_TAG_NAME); |
| specificType.setAttribute(PART_NAME_ATTRIBUTE_NAME, entry.getKey().getName()); |
| specificType.setAttribute(CONTENT_TYPE_ATTRIBUTE_NAME, entry.getValue()); |
| root.appendChild(specificType); |
| } |
| |
| /** |
| * Use to append default types XML elements, use by the save() method. |
| * |
| * @param root |
| * XML parent element use to append this default type element. |
| * @param entry |
| * The values to append. |
| * @see #save(java.io.OutputStream) |
| */ |
| private void appendDefaultType(Element root, Entry<String, String> entry) { |
| Element defaultType = root.getOwnerDocument().createElementNS(TYPES_NAMESPACE_URI, DEFAULT_TAG_NAME); |
| defaultType.setAttribute(EXTENSION_ATTRIBUTE_NAME, entry.getKey()); |
| defaultType.setAttribute(CONTENT_TYPE_ATTRIBUTE_NAME, entry.getValue()); |
| root.appendChild(defaultType); |
| } |
| |
| /** |
| * Specific implementation of the save method. Call by the save() method, |
| * call before exiting. |
| * |
| * @param out |
| * The output stream use to write the content type XML. |
| */ |
| public abstract boolean saveImpl(Document content, OutputStream out); |
| } |