123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496 |
- /* ====================================================================
- 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.values().contains(contentType) || (this.overrideContentType != null && this.overrideContentType
- .values().contains(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);
- }
|