Package org.openxml4j.opc.internal

Source Code of org.openxml4j.opc.internal.ContentTypeManager

/*
* 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.openxml4j.opc.internal;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Iterator;
import java.util.List;
import java.util.TreeMap;
import java.util.Map.Entry;
import java.util.zip.ZipOutputStream;

import org.apache.log4j.Logger;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Namespace;
import org.dom4j.QName;
import org.dom4j.io.SAXReader;
import org.openxml4j.exceptions.InvalidFormatException;
import org.openxml4j.exceptions.InvalidOperationException;
import org.openxml4j.exceptions.OpenXML4JRuntimeException;
import org.openxml4j.opc.Package;
import org.openxml4j.opc.PackagePart;
import org.openxml4j.opc.PackagePartName;
import org.openxml4j.opc.PackagingURIHelper;

/**
* Manage package content types ([Content_Types].xml part).
*
* @author Julien Chable
* @version 1.0
*/
public abstract class ContentTypeManager {

  protected static Logger logger = Logger.getLogger("org.openxml4j");

  /**
   * Reference to the package using this content type manager.
   */
  protected Package container;

  /**
   * 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 = "http://schemas.openxmlformats.org/package/2006/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";

  /**
   * 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 archive
   *            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, Package pkg)
      throws InvalidFormatException {
    this.container = pkg;
    this.defaultContentType = new TreeMap<String, String>();
    if (in != null) {
      try {
        parseContentTypesFile(in);
      } catch (InvalidFormatException e) {
        throw new InvalidFormatException(
            "Can't read content types part !");
      }
    }
  }

  /**
   * Build association extention-> content type (will be stored in
   * [Content_Types].xml) for example ContentType="image/png" Extension="png"
   *
   * [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:
   *
   * 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.
   *
   * 2. If a part name has no extension, a corresponding Override element
   * shall be added to the Content Types stream.
   *
   * 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.
   *
   * 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.
   *
   * a. If the content types match, no further action is required.
   *
   * b. If the content types do not match, a new Override element shall be
   * added to the Content Types stream. .
   *
   * 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.
   *
   *
   * @param partUri
   *            the uri that will be stored
   * @return <b>false</b> if an error occured.
   */
  public void addContentType(PackagePartName partName, String contentType) {
    boolean defaultCTExists = false;
    String extension = partName.getExtension().toLowerCase();
    if ((extension.length() == 0)
        || (this.defaultContentType.containsKey(extension) && !(defaultCTExists = this.defaultContentType
            .containsValue(contentType))))
      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<PackagePartName, String>();
    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) {
    defaultContentType.put(extension.toLowerCase(), contentType
        .toLowerCase());
  }

  /**
   * 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.
   *
   * 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.
   *
   * @param partUri
   *            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.
   *
   * Rule [M2.9]: To get the content type of a part, the package implementer
   * shall perform the steps described in��9.1.2.4:
   *
   * 1. Compare the part name with the values specified for the PartName
   * attribute of the Override elements. The comparison shall be
   * case-insensitive ASCII.
   *
   * 2. If there is an Override element with a matching PartName attribute,
   * return the value of its ContentType attribute. No further action is
   * required.
   *
   * 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.
   *
   * 4. If there is a Default element with a matching Extension attribute,
   * return the value of its ContentType attribute. No further action is
   * required.
   *
   * 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.
   *
   * @param partUri
   *            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();
    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 : this error should NEVER happen, if so please send a mail to the developers team, thanks !");
    } else {
      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 {
      SAXReader xmlReader = new SAXReader();
      Document xmlContentTypetDoc = xmlReader.read(in);

      // Default content types
      List defaultTypes = xmlContentTypetDoc.getRootElement().elements(
          DEFAULT_TAG_NAME);
      Iterator elementIteratorDefault = defaultTypes.iterator();
      while (elementIteratorDefault.hasNext()) {
        Element element = (Element) elementIteratorDefault.next();
        String extension = element.attribute(EXTENSION_ATTRIBUTE_NAME)
            .getValue();
        String contentType = element.attribute(
            CONTENT_TYPE_ATTRIBUTE_NAME).getValue();
        addDefaultContentType(extension, contentType);
      }

      // Overriden content types
      List overrideTypes = xmlContentTypetDoc.getRootElement().elements(
          OVERRIDE_TAG_NAME);
      Iterator elementIteratorOverride = overrideTypes.iterator();
      while (elementIteratorOverride.hasNext()) {
        Element element = (Element) elementIteratorOverride.next();
        URI uri = new URI(element.attribute(PART_NAME_ATTRIBUTE_NAME)
            .getValue());
        PackagePartName partName = PackagingURIHelper
            .createPartName(uri);
        String contentType = element.attribute(
            CONTENT_TYPE_ATTRIBUTE_NAME).getValue();
        addOverrideContentType(partName, contentType);
      }
    } catch (URISyntaxException urie) {
      throw new InvalidFormatException(urie.getMessage());
    } catch (DocumentException 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
    Namespace dfNs = Namespace.get("", TYPES_NAMESPACE_URI);
    Element typesElem = xmlOutDoc
        .addElement(new QName(TYPES_TAG_NAME, dfNs));

    // 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(ZipOutputStream)
   */
  private void appendSpecificTypes(Element root,
      Entry<PackagePartName, String> entry) {
    root.addElement(OVERRIDE_TAG_NAME).addAttribute(
        PART_NAME_ATTRIBUTE_NAME,
        ((PackagePartName) entry.getKey()).getName()).addAttribute(
        CONTENT_TYPE_ATTRIBUTE_NAME, (String) entry.getValue());
  }

  /**
   * Use to append default types XML elements, use by the save() metid.
   *
   * @param root
   *            XML parent element use to append this default type element.
   * @param entry
   *            The values to append.
   * @see #save(ZipOutputStream)
   */
  private void appendDefaultType(Element root, Entry<String, String> entry) {
    root.addElement(DEFAULT_TAG_NAME).addAttribute(
        EXTENSION_ATTRIBUTE_NAME, (String) entry.getKey())
        .addAttribute(CONTENT_TYPE_ATTRIBUTE_NAME,
            (String) entry.getValue());

  }

  /**
   * 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);
}
TOP

Related Classes of org.openxml4j.opc.internal.ContentTypeManager

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.