OdfDocument.java

/**
 * **********************************************************************
 *
 * <p>DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 *
 * <p>Copyright 2008, 2010 Oracle and/or its affiliates. All rights reserved.
 *
 * <p>Use is subject to license terms.
 *
 * <p>Licensed 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. You can also obtain a copy of the License at
 * http://odftoolkit.org/docs/license.txt
 *
 * <p>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.
 *
 * <p>See the License for the specific language governing permissions and limitations under the
 * License.
 *
 * <p>**********************************************************************
 */
package org.odftoolkit.odfdom.doc;

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import org.odftoolkit.odfdom.doc.table.OdfTable;
import org.odftoolkit.odfdom.dom.OdfContentDom;
import org.odftoolkit.odfdom.dom.OdfDocumentNamespace;
import org.odftoolkit.odfdom.dom.OdfMetaDom;
import org.odftoolkit.odfdom.dom.OdfSchemaConstraint;
import org.odftoolkit.odfdom.dom.OdfSchemaDocument;
import org.odftoolkit.odfdom.dom.attribute.text.TextAnchorTypeAttribute;
import org.odftoolkit.odfdom.dom.element.draw.DrawPageElement;
import org.odftoolkit.odfdom.dom.element.office.OfficeAnnotationElement;
import org.odftoolkit.odfdom.dom.element.office.OfficeBodyElement;
import org.odftoolkit.odfdom.dom.element.table.TableTableCellElement;
import org.odftoolkit.odfdom.dom.element.table.TableTableElement;
import org.odftoolkit.odfdom.dom.element.text.TextPElement;
import org.odftoolkit.odfdom.dom.style.OdfStyleFamily;
import org.odftoolkit.odfdom.dom.style.props.OdfStyleProperty;
import org.odftoolkit.odfdom.dom.style.props.OdfTextProperties;
import org.odftoolkit.odfdom.incubator.doc.draw.OdfDrawFrame;
import org.odftoolkit.odfdom.incubator.doc.draw.OdfDrawImage;
import org.odftoolkit.odfdom.incubator.doc.office.OdfOfficeStyles;
import org.odftoolkit.odfdom.incubator.doc.style.OdfDefaultStyle;
import org.odftoolkit.odfdom.incubator.meta.OdfOfficeMeta;
import org.odftoolkit.odfdom.pkg.MediaType;
import org.odftoolkit.odfdom.pkg.OdfElement;
import org.odftoolkit.odfdom.pkg.OdfName;
import org.odftoolkit.odfdom.pkg.OdfPackage;
import org.odftoolkit.odfdom.pkg.OdfValidationException;
import org.odftoolkit.odfdom.type.Duration;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;

/** This abstract class is representing one of the possible ODF documents. */
public abstract class OdfDocument extends OdfSchemaDocument {
  // Static parts of file references

  private static final String SLASH = "/";
  private OdfMediaType mMediaType;
  private OdfOfficeMeta mOfficeMeta;
  private long documentOpeningTime;
  private static final Pattern CONTROL_CHAR_PATTERN = Pattern.compile("\\p{Cntrl}");
  private static final String EMPTY_STRING = "";
  private Calendar mCreationDate;
  private static final String FORMER_OPEN_OFFICE_VERSION =
      "StarOffice/8$Win32 OpenOffice.org_project/680m18$Build-9161";
  protected Boolean mHasCollaboration = null;

  // Using static factory instead of constructor
  protected OdfDocument(OdfPackage pkg, String internalPath, OdfMediaType mediaType)
      throws SAXException {
    super(pkg, internalPath, mediaType.getMediaTypeString());
    mMediaType = mediaType;
    // set document opening time.
    documentOpeningTime = System.currentTimeMillis();
  }

  /** This enum contains all possible media types of OpenDocument documents. */
  public enum OdfMediaType implements MediaType {
    CHART("application/vnd.oasis.opendocument.chart", "odc"),
    CHART_TEMPLATE("application/vnd.oasis.opendocument.chart-template", "otc"),
    FORMULA("application/vnd.oasis.opendocument.formula", "odf"),
    FORMULA_TEMPLATE("application/vnd.oasis.opendocument.formula-template", "otf"),
    DATABASE_FRONT_END("application/vnd.oasis.opendocument.base", "odb"),
    GRAPHICS("application/vnd.oasis.opendocument.graphics", "odg"),
    GRAPHICS_TEMPLATE("application/vnd.oasis.opendocument.graphics-template", "otg"),
    IMAGE("application/vnd.oasis.opendocument.image", "odi"),
    IMAGE_TEMPLATE("application/vnd.oasis.opendocument.image-template", "oti"),
    PRESENTATION("application/vnd.oasis.opendocument.presentation", "odp"),
    PRESENTATION_TEMPLATE("application/vnd.oasis.opendocument.presentation-template", "otp"),
    SPREADSHEET("application/vnd.oasis.opendocument.spreadsheet", "ods"),
    SPREADSHEET_TEMPLATE("application/vnd.oasis.opendocument.spreadsheet-template", "ots"),
    TEXT("application/vnd.oasis.opendocument.text", "odt"),
    TEXT_MASTER("application/vnd.oasis.opendocument.text-master", "odm"),
    TEXT_TEMPLATE("application/vnd.oasis.opendocument.text-template", "ott"),
    TEXT_WEB("application/vnd.oasis.opendocument.text-web", "oth");
    private final String mMediaType;
    private final String mSuffix;

    OdfMediaType(String mediaType, String suffix) {
      this.mMediaType = mediaType;
      this.mSuffix = suffix;
    }

    /** @return the mediatype String of this document */
    @Override
    public String getMediaTypeString() {
      return mMediaType;
    }

    /** @return the ODF filesuffix of this document */
    @Override
    public String getSuffix() {
      return mSuffix;
    }

    /**
     * @param mediaType string defining an ODF document
     * @return the according OdfMediatype encapsulating the given string and the suffix
     */
    public static OdfMediaType getOdfMediaType(String mediaType) {
      OdfMediaType odfMediaType = null;
      if (mediaType != null) {

        String mediaTypeShort =
            mediaType.substring(mediaType.lastIndexOf(".") + 1, mediaType.length());
        mediaTypeShort = mediaTypeShort.replace('-', '_').toUpperCase();
        try {
          odfMediaType = OdfMediaType.valueOf(mediaTypeShort);

        } catch (IllegalArgumentException e) {
          throw new IllegalArgumentException(
              "Given mediaType '" + mediaType + "' is not an ODF mediatype!");
        }
      }
      return odfMediaType;
    }
  }

  /**
   * Loads the ODF root document from the given Resource.
   *
   * <p>NOTE: Initial meta data (like the document creation time) will be added in this method.
   *
   * @param res a resource containing a package with a root document
   * @param odfMediaType the media type of the root document
   * @return the OpenDocument document or NULL if the media type is not supported by ODFDOM.
   * @throws java.lang.Exception - if the document could not be created.
   */
  protected static OdfDocument loadTemplate(Resource res, OdfMediaType odfMediaType)
      throws Exception {
    InputStream in = res.createInputStream();
    OdfPackage pkg = null;
    try {
      pkg = OdfPackage.loadPackage(in);
    } finally {
      in.close();
    }
    OdfDocument newDocument = newDocument(pkg, ROOT_DOCUMENT_PATH, odfMediaType, Boolean.FALSE);
    // add creation time, the metadata have to be explicitly set
    newDocument.mCreationDate = Calendar.getInstance();
    return newDocument;
  }

  /**
   * Loads the ODF root document from the given Resource.
   *
   * <p>NOTE: Initial meta data (like the document creation time) will be added in this method.
   *
   * @param res a resource containing a package with a root document
   * @param odfMediaType the media type of the root document
   * @param enableCollaboration - user changes equivalent for creating this document are gathered
   * @return the OpenDocument document or NULL if the media type is not supported by ODFDOM.
   * @throws java.lang.Exception - if the document could not be created.
   */
  protected static OdfDocument loadTemplate(
      Resource res, OdfMediaType odfMediaType, Boolean enableCollaboration) throws Exception {
    InputStream in = res.createInputStream();
    OdfPackage pkg = null;
    try {
      pkg = OdfPackage.loadPackage(in);
    } finally {
      in.close();
    }
    OdfDocument newDocument =
        newDocument(pkg, ROOT_DOCUMENT_PATH, odfMediaType, enableCollaboration);
    // add creation time, the metadata have to be explicitly set
    newDocument.mCreationDate = Calendar.getInstance();
    return newDocument;
  }

  /**
   * Loads the ODF root document from the ODF package provided by its path.
   *
   * <p>OdfDocument relies on the file being available for read access over the whole life-cycle of
   * OdfDocument.
   *
   * @param documentPath - the path from where the document can be loaded
   * @return the OpenDocument from the given path or NULL if the media type is not supported by
   *     ODFDOM.
   * @throws java.lang.Exception - if the document could not be created.
   */
  public static OdfDocument loadDocument(String documentPath) throws Exception {
    return loadDocument(OdfPackage.loadPackage(documentPath));
  }

  /**
   * Loads the ODF root document from the ODF package provided by a Stream.
   *
   * <p>Since an InputStream does not provide the arbitrary (non sequential) read access needed by
   * OdfDocument, the InputStream is cached. This usually takes more time compared to the other
   * createInternalDocument methods. An advantage of caching is that there are no problems
   * overwriting an input file.
   *
   * @param inStream - the InputStream of the ODF document.
   * @param configuration - key/value pairs of user given run-time settings (configuration)
   * @return the document created from the given InputStream
   * @throws java.lang.Exception - if the document could not be created.
   */
  public static OdfDocument loadDocument(InputStream inStream, Map<String, Object> configuration)
      throws Exception {
    return loadDocument(OdfPackage.loadPackage(inStream, configuration));
  }

  /**
   * Loads the ODF root document from the ODF package provided by a Stream.
   *
   * <p>Since an InputStream does not provide the arbitrary (non sequential) read access needed by
   * OdfDocument, the InputStream is cached. This usually takes more time compared to the other
   * createInternalDocument methods. An advantage of caching is that there are no problems
   * overwriting an input file.
   *
   * @param inStream - the InputStream of the ODF document.
   * @param configuration - key/value pairs of user given run-time settings (configuration)
   * @param enableCollaboration - user changes equivalent for creating this document are gathered
   * @return the document created from the given InputStream
   * @throws java.lang.Exception - if the document could not be created.
   */
  public static OdfDocument loadDocument(
      InputStream inStream, Map<String, Object> configuration, Boolean enableCollaboration)
      throws Exception {
    return loadDocument(OdfPackage.loadPackage(inStream, configuration), enableCollaboration);
  }

  /**
   * Loads the ODF root document from the ODF package provided by a Stream.
   *
   * <p>Since an InputStream does not provide the arbitrary (non sequential) read access needed by
   * OdfDocument, the InputStream is cached. This usually takes more time compared to the other
   * createInternalDocument methods. An advantage of caching is that there are no problems
   * overwriting an input file.
   *
   * @param inStream - the InputStream of the ODF document.
   * @return the document created from the given InputStream
   * @throws java.lang.Exception - if the document could not be created.
   */
  public static OdfDocument loadDocument(InputStream inStream) throws Exception {
    return loadDocument(OdfPackage.loadPackage(inStream));
  }

  /**
   * Loads the ODF root document from the ODF package provided by a Stream.
   *
   * <p>Since an InputStream does not provide the arbitrary (non sequential) read access needed by
   * OdfDocument, the InputStream is cached. This usually takes more time compared to the other
   * createInternalDocument methods. An advantage of caching is that there are no problems
   * overwriting an input file.
   *
   * @param inStream - the InputStream of the ODF document.
   * @param enableCollaboration - user changes equivalent for creating this document are gathered
   * @return the document created from the given InputStream
   * @throws java.lang.Exception - if the document could not be created.
   */
  public static OdfDocument loadDocument(InputStream inStream, Boolean enableCollaboration)
      throws Exception {
    return loadDocument(OdfPackage.loadPackage(inStream), enableCollaboration);
  }

  /**
   * Loads the ODF root document from the ODF package provided as a File.
   *
   * @param file - a file representing the ODF document.
   * @return the document created from the given File
   * @throws java.lang.Exception - if the document could not be created.
   */
  public static OdfDocument loadDocument(File file) throws Exception {
    return loadDocument(OdfPackage.loadPackage(file));
  }

  /**
   * Loads the ODF root document from the ODF package.
   *
   * @param odfPackage - the ODF package containing the ODF document.
   * @return the root document of the given OdfPackage
   * @throws java.lang.Exception - if the ODF document could not be created.
   */
  public static OdfDocument loadDocument(OdfPackage odfPackage) throws Exception {
    return loadDocument(odfPackage, ROOT_DOCUMENT_PATH);
  }

  /**
   * Loads the ODF root document from the ODF package.
   *
   * @param odfPackage - the ODF package containing the ODF document.
   * @param enableCollaboration - user changes equivalent for creating this document are gathered
   * @return the root document of the given OdfPackage
   * @throws java.lang.Exception - if the ODF document could not be created.
   */
  public static OdfDocument loadDocument(OdfPackage odfPackage, Boolean enableCollaboration)
      throws Exception {
    return loadDocument(odfPackage, ROOT_DOCUMENT_PATH, enableCollaboration);
  }

  /**
   * Creates an OdfDocument from the OpenDocument provided by an ODF package.
   *
   * @param odfPackage - the ODF package containing the ODF document.
   * @param internalPath - the path to the ODF document relative to the package root, or an empty
   *     String for the root document.
   * @return the root document of the given OdfPackage
   * @throws java.lang.Exception - if the ODF document could not be created.
   */
  public static OdfDocument loadDocument(OdfPackage odfPackage, String internalPath)
      throws Exception {
    return loadDocument(odfPackage, internalPath, Boolean.FALSE);
  }

  /**
   * Creates an OdfDocument from the OpenDocument provided by an ODF package.
   *
   * @param odfPackage - the ODF package containing the ODF document.
   * @param internalPath - the path to the ODF document relative to the package root, or an empty
   *     String for the root document.
   * @param enableCollaboration - user changes equivalent for creating this document are gathered
   * @return the root document of the given OdfPackage
   * @throws java.lang.Exception - if the ODF document could not be created.
   */
  public static OdfDocument loadDocument(
      OdfPackage odfPackage, String internalPath, Boolean enableCollaboration) throws Exception {
    String documentMediaType = odfPackage.getMediaTypeString(internalPath);
    OdfMediaType odfMediaType = null;
    try {
      odfMediaType = OdfMediaType.getOdfMediaType(documentMediaType);
    } catch (IllegalArgumentException e) {
      // the returned NULL will be taking care of afterwards
    }
    if (odfMediaType == null) {
      ErrorHandler errorHandler = odfPackage.getErrorHandler();

      if (documentMediaType != null) {
        Matcher matcherCTRL = CONTROL_CHAR_PATTERN.matcher(documentMediaType);
        if (matcherCTRL.find()) {
          documentMediaType = matcherCTRL.replaceAll(EMPTY_STRING);
        }
      }
      OdfValidationException ve =
          new OdfValidationException(
              OdfSchemaConstraint.DOCUMENT_WITHOUT_ODF_MIMETYPE, internalPath, documentMediaType);
      if (errorHandler != null) {
        errorHandler.fatalError(ve);
      }
      throw ve;
    }
    return newDocument(odfPackage, internalPath, odfMediaType, enableCollaboration);
  }

  // return null if the media type can not be recognized.
  private static OdfDocument loadDocumentFromTemplate(OdfMediaType odfMediaType) throws Exception {

    final Resource documentTemplate;
    switch (odfMediaType) {
      case TEXT:
      case TEXT_TEMPLATE:
      case TEXT_MASTER:
      case TEXT_WEB:
        documentTemplate = OdfTextDocument.EMPTY_TEXT_DOCUMENT_RESOURCE;
        break;

      case SPREADSHEET:
      case SPREADSHEET_TEMPLATE:
        documentTemplate = OdfSpreadsheetDocument.EMPTY_SPREADSHEET_DOCUMENT_RESOURCE;
        break;

      case PRESENTATION:
      case PRESENTATION_TEMPLATE:
        documentTemplate = OdfPresentationDocument.EMPTY_PRESENTATION_DOCUMENT_RESOURCE;
        break;

      case GRAPHICS:
      case GRAPHICS_TEMPLATE:
        documentTemplate = OdfGraphicsDocument.EMPTY_GRAPHICS_DOCUMENT_RESOURCE;
        break;

      case CHART:
      case CHART_TEMPLATE:
        documentTemplate = OdfChartDocument.EMPTY_CHART_DOCUMENT_RESOURCE;
        break;

      case IMAGE:
      case IMAGE_TEMPLATE:
        documentTemplate = OdfImageDocument.EMPTY_IMAGE_DOCUMENT_RESOURCE;
        break;

      default:
        documentTemplate = null;
        throw new IllegalArgumentException(
            "Given mediaType '" + odfMediaType.mMediaType + "' is not yet supported!");
    }
    return loadTemplate(documentTemplate, odfMediaType);
  }

  /**
   * Creates one of the ODF documents based a given mediatype.
   *
   * @param odfMediaType The ODF Mediatype of the ODF document to be created.
   * @param enableCollaboration - user changes equivalent for creating this document are gathered
   * @return The ODF document, which mediatype dependends on the parameter or NULL if media type
   *     were not supported.
   */
  private static OdfDocument newDocument(
      OdfPackage pkg, String internalPath, OdfMediaType odfMediaType, Boolean enableCollaboration)
      throws SAXException {
    OdfDocument newDoc = null;
    switch (odfMediaType) {
      case TEXT:
        newDoc =
            new OdfTextDocument(
                pkg, internalPath, OdfTextDocument.OdfMediaType.TEXT, enableCollaboration);
        break;

      case TEXT_TEMPLATE:
        newDoc =
            new OdfTextDocument(
                pkg, internalPath, OdfTextDocument.OdfMediaType.TEXT_TEMPLATE, enableCollaboration);
        break;

      case TEXT_MASTER:
        newDoc =
            new OdfTextDocument(
                pkg, internalPath, OdfTextDocument.OdfMediaType.TEXT_MASTER, enableCollaboration);
        break;

      case TEXT_WEB:
        newDoc =
            new OdfTextDocument(
                pkg, internalPath, OdfTextDocument.OdfMediaType.TEXT_WEB, enableCollaboration);
        break;

      case SPREADSHEET:
        newDoc =
            new OdfSpreadsheetDocument(
                pkg, internalPath, OdfSpreadsheetDocument.OdfMediaType.SPREADSHEET);
        break;

      case SPREADSHEET_TEMPLATE:
        newDoc =
            new OdfSpreadsheetDocument(
                pkg, internalPath, OdfSpreadsheetDocument.OdfMediaType.SPREADSHEET_TEMPLATE);
        break;

      case PRESENTATION:
        newDoc =
            new OdfPresentationDocument(
                pkg, internalPath, OdfPresentationDocument.OdfMediaType.PRESENTATION);
        break;

      case PRESENTATION_TEMPLATE:
        newDoc =
            new OdfPresentationDocument(
                pkg, internalPath, OdfPresentationDocument.OdfMediaType.PRESENTATION_TEMPLATE);
        break;

      case GRAPHICS:
        newDoc =
            new OdfGraphicsDocument(pkg, internalPath, OdfGraphicsDocument.OdfMediaType.GRAPHICS);
        break;

      case GRAPHICS_TEMPLATE:
        newDoc =
            new OdfGraphicsDocument(
                pkg, internalPath, OdfGraphicsDocument.OdfMediaType.GRAPHICS_TEMPLATE);
        break;

      case CHART:
        newDoc = new OdfChartDocument(pkg, internalPath, OdfChartDocument.OdfMediaType.CHART);
        break;

      case CHART_TEMPLATE:
        newDoc =
            new OdfChartDocument(pkg, internalPath, OdfChartDocument.OdfMediaType.CHART_TEMPLATE);
        break;

      case IMAGE:
        newDoc = new OdfImageDocument(pkg, internalPath, OdfImageDocument.OdfMediaType.IMAGE);
        break;

      case IMAGE_TEMPLATE:
        newDoc =
            new OdfImageDocument(pkg, internalPath, OdfImageDocument.OdfMediaType.IMAGE_TEMPLATE);
        break;

      default:
        newDoc = null;
        throw new IllegalArgumentException(
            "Given mediaType '" + odfMediaType.mMediaType + "' is not yet supported!");
    }
    // returning null if MediaType is not supported
    return newDoc;
  }

  /**
   * Returns an embedded OdfPackageDocument from the given package path.
   *
   * @param documentPath to the ODF document within the package. The path is relative the current
   *     ODF document path.
   * @return an embedded OdfPackageDocument
   */
  @Override
  public OdfDocument loadSubDocument(String documentPath) {
    return (OdfDocument) super.loadSubDocument(documentPath);
  }

  /**
   * Method returns all embedded OdfPackageDocuments, which match a valid OdfMediaType, of the
   * current OdfPackageDocument. Note: The root document is not part of the returned collection.
   *
   * @return a map with all embedded documents and their paths of the current OdfPackageDocument
   */
  public Map<String, OdfDocument> loadSubDocuments() {
    return loadSubDocuments(null);
  }

  /**
   * Method returns all embedded OdfPackageDocuments of sthe current OdfPackageDocument matching the
   * according MediaType. This is done by matching the subfolder entries of the manifest file with
   * the given OdfMediaType.
   *
   * @param desiredMediaType media type of the documents to be returned (used as a filter).
   * @return embedded documents of the current OdfPackageDocument matching the given media type
   */
  public Map<String, OdfDocument> loadSubDocuments(OdfMediaType desiredMediaType) {
    String wantedMediaString = null;
    if (desiredMediaType != null) {
      wantedMediaString = desiredMediaType.getMediaTypeString();
    }
    Map<String, OdfDocument> embeddedObjectsMap = new HashMap<String, OdfDocument>();
    // check manifest for current embedded OdfPackageDocuments
    Set<String> manifestEntries = mPackage.getFilePaths();
    for (String path : manifestEntries) {
      // any directory that is not the root document "/"
      if (path.length() > 1 && path.endsWith(SLASH)) {
        String entryMediaType = mPackage.getFileEntry(path).getMediaTypeString();
        // if the entry is a document (directory has mediaType)
        if (entryMediaType != null) {
          // if a specific ODF mediatype was requested
          if (wantedMediaString != null) {
            // test if the desired mediatype matches the current
            if (entryMediaType.equals(wantedMediaString)) {
              path = normalizeDocumentPath(path);
              embeddedObjectsMap.put(path, (OdfDocument) mPackage.loadDocument(path));
            }
          } else {
            // test if any ODF mediatype matches the current
            for (OdfMediaType mediaType : OdfMediaType.values()) {
              if (entryMediaType.equals(mediaType.getMediaTypeString())) {
                embeddedObjectsMap.put(path, (OdfDocument) mPackage.loadDocument(path));
              }
            }
          }
        }
      }
    }
    return embeddedObjectsMap;
  }

  /**
   * Sets the media type of the OdfDocument
   *
   * @param odfMediaType media type to be set
   */
  protected void setOdfMediaType(OdfMediaType odfMediaType) {
    mMediaType = odfMediaType;
    super.setMediaTypeString(odfMediaType.getMediaTypeString());
  }

  /** Gets the media type of the OdfDocument */
  protected OdfMediaType getOdfMediaType() {
    return mMediaType;
  }

  /**
   * Get the meta data feature instance of the current document
   *
   * @return the meta data feature instance which represent <code>office:meta</code> in the meta.xml
   */
  public OdfOfficeMeta getOfficeMetadata() {
    if (mOfficeMeta == null) {
      try {
        OdfMetaDom metaDom = getMetaDom();
        mOfficeMeta = new OdfOfficeMeta(metaDom);
      } catch (Exception ex) {
        Logger.getLogger(OdfDocument.class.getName()).log(Level.SEVERE, null, ex);
      }
    }
    return mOfficeMeta;
  }

  /**
   * Save the document to an OutputStream. Delegate to the root document and save possible embedded
   * OdfDocuments.
   *
   * <p>If the input file has been cached (this is the case when loading from an InputStream), the
   * input file can be overwritten.
   *
   * <p>If not, the OutputStream may not point to the input file! Otherwise this will result in
   * unwanted behaviour and broken files.
   *
   * <p>When save the embedded document to a stand alone document, all the file entries of the
   * embedded document will be copied to a new document package. If the embedded document is outside
   * of the current document directory, you have to embed it to the sub directory and refresh the
   * link of the embedded document. you should reload it from the stream to get the saved embedded
   * document.
   *
   * @param out - the OutputStream to write the file to
   * @throws java.lang.Exception if the document could not be saved
   */
  public void save(OutputStream out) throws Exception {
    updateMetaData();
    if (!isRootDocument()) {
      OdfDocument newDoc = loadDocumentFromTemplate(getOdfMediaType());
      newDoc.insertDocument(this, ROOT_DOCUMENT_PATH);
      newDoc.updateMetaData();
      newDoc.mPackage.save(out);
      // ToDo: (Issue 219 - PackageRefactoring) - Return the document, when not closing!
      // Should we close the sources now? User will never receive the open package!
    } else {
      mPackage.save(out);
    }
  }

  /**
   * Save the document to a given file.
   *
   * <p>If the input file has been cached (this is the case when loading from an InputStream), the
   * input file can be overwritten.
   *
   * <p>Otherwise it's allowed to overwrite the input file as long as the same path name is used
   * that was used for loading (no symbolic link foo2.odt pointing to the loaded file foo1.odt, no
   * network path X:\foo.odt pointing to the loaded file D:\foo.odt).
   *
   * <p>When saving the embedded document to a stand alone document, all files of the embedded
   * document will be copied to a new document package. If the embedded document is outside of the
   * current document directory, you have to embed it to the sub directory and refresh the link of
   * the embedded document. You should reload it from the given file to get the saved embedded
   * document.
   *
   * @param file - the file to save the document
   * @throws java.lang.Exception if the document could not be saved
   */
  @Override
  public void save(File file) throws Exception {
    updateMetaData();
    if (!isRootDocument()) {
      OdfDocument newDoc = loadDocumentFromTemplate(getOdfMediaType());
      newDoc.insertDocument(this, ROOT_DOCUMENT_PATH);
      newDoc.updateMetaData();
      newDoc.mPackage.save(file);
      // ToDo: (Issue 219 - PackageRefactoring) - Return the document, when not closing!
      // Should we close the sources now? User will never receive the open package!
    } else {
      this.mPackage.save(file);
    }
  }

  /**
   * Close the document and release all temporary created data. After execution of this method, this
   * class is no longer usable. Do this as the last action to free resources. Closing an already
   * closed document has no effect.
   */
  @Override
  public void close() {
    // set all member variables explicit to null
    mMediaType = null;
    mOfficeMeta = null;
    super.close();
  }

  /**
   * Get the content root of a document.
   *
   * <p>You may prefer to use the getContentRoot methods of subclasses of OdfDocument. Their return
   * parameters are already casted to respective subclasses of OdfElement.
   *
   * @param the type of the content root, depend on the document type
   * @return the child element of office:body, e.g. office:text for text docs
   * @throws Exception if the file DOM could not be created.
   */
  @SuppressWarnings("unchecked")
  <T extends OdfElement> T getContentRoot(Class<T> clazz) throws Exception {
    OdfElement contentRoot = getContentDom().getRootElement();
    OfficeBodyElement contentBody =
        OdfElement.findFirstChildNode(OfficeBodyElement.class, contentRoot);
    NodeList childs = contentBody.getChildNodes();
    for (int i = 0; i < childs.getLength(); i++) {
      Node cur = childs.item(i);
      if ((cur != null) && clazz.isInstance(cur)) {
        return (T) cur;
      }
    }
    return null;
  }

  /**
   * Get the content root of a document.
   *
   * <p>You may prefer to use the getContentRoot methods of subclasses of OdfDocument.
   *
   * @return the child element of office:body, e.g. office:text for text docs
   * @throws Exception if the file DOM could not be created.
   */
  public OdfElement getContentRoot() throws Exception {
    return getContentRoot(OdfElement.class);
  }

  @Override
  public String toString() {
    return "\n"
        + getMediaTypeString()
        + " - ID: "
        + this.hashCode()
        + " "
        + getPackage().getBaseURI();
  }

  /**
   * Insert an Image from the specified uri to the end of the OdfDocument.
   *
   * @param imageUri The URI of the image that will be added to the document, add image stream to
   *     the package, in the 'Pictures/' graphic directory with the same image file name as in the
   *     URI. If the imageURI is relative first the user.dir is taken to make it absolute.
   * @return Returns the internal package path of the image, which was created based on the given
   *     URI.
   */
  public String newImage(URI imageUri) {
    try {
      OdfContentDom contentDom = this.getContentDom();
      OdfDrawFrame drawFrame = contentDom.newOdfElement(OdfDrawFrame.class);
      XPath xpath = contentDom.getXPath();
      if (this instanceof OdfSpreadsheetDocument) {
        TableTableCellElement lastCell =
            (TableTableCellElement)
                xpath.evaluate("//table:table-cell[last()]", contentDom, XPathConstants.NODE);
        lastCell.appendChild(drawFrame);
        drawFrame.removeAttribute("text:anchor-type");

      } else if (this instanceof OdfTextDocument) {
        TextPElement lastPara =
            (TextPElement) xpath.evaluate("//text:p[last()]", contentDom, XPathConstants.NODE);
        if (lastPara == null) {
          lastPara = ((OdfTextDocument) this).newParagraph();
        }
        lastPara.appendChild(drawFrame);
        drawFrame.setTextAnchorTypeAttribute(TextAnchorTypeAttribute.Value.PARAGRAPH.toString());
      } else if (this instanceof OdfPresentationDocument) {
        DrawPageElement lastPage =
            (DrawPageElement)
                xpath.evaluate("//draw:page[last()]", contentDom, XPathConstants.NODE);
        lastPage.appendChild(drawFrame);
      }
      OdfDrawImage image = (OdfDrawImage) drawFrame.newDrawImageElement();
      String imagePath = image.newImage(imageUri);
      return imagePath;
    } catch (Exception ex) {
      Logger.getLogger(OdfDocument.class.getName()).log(Level.SEVERE, null, ex);
    }
    return null;
  }

  /**
   * Return an instance of table feature with the specific table name.
   *
   * @param name of the table being searched for.
   * @return an instance of table feature with the specific table name.
   */
  public OdfTable getTableByName(String name) {
    try {
      OdfElement root = getContentDom().getRootElement();
      OfficeBodyElement officeBody = OdfElement.findFirstChildNode(OfficeBodyElement.class, root);
      OdfElement typedContent = OdfElement.findFirstChildNode(OdfElement.class, officeBody);

      NodeList childList = typedContent.getChildNodes();
      for (int i = 0; i < childList.getLength(); i++) {
        if (childList.item(i) instanceof TableTableElement) {
          TableTableElement table = (TableTableElement) childList.item(i);
          if (table
              .getOdfAttributeValue(OdfName.newName(OdfDocumentNamespace.TABLE, "name"))
              .equals(name)) {
            return OdfTable.getInstance(table);
          }
        }
      }
    } catch (Exception e) {
      Logger.getLogger(OdfDocument.class.getName()).log(Level.SEVERE, null, e);
    }
    return null;
  }

  /**
   * Return a list of table features in this document. For general ODF documents it searches for
   * them recursively through the document. For ODF documents, there is a getOdsTableList
   *
   * @see OdfSpreadsheetDocument:getSpreadsheetTables
   * @return a list of table features in this document.
   */
  @Deprecated(
      since =
          "It was not clear that this is searching recursively, especialy in OdfSpreadsheetDocuments")
  public List<OdfTable> getTableList() {
    return getTableList(false);
  }

  /**
   * Return a list of table features in this document. For general ODF documents it searches for
   * tables recursively through the document.
   *
   * @see OdfSpreadsheetDocument:getSpreadsheetTables
   * @param doRecursiveSearch In spreadsheet documents you do not need a recursive search.
   * @return a list of table features in this document.
   */
  public List<OdfTable> getTableList(boolean doRecursiveSearch) {
    List<OdfTable> tableList = null;
    try {
      List<TableTableElement> tableElementList = getTables(doRecursiveSearch);
      tableList = new ArrayList<OdfTable>(tableElementList.size());
      for (int i = 0; i < tableElementList.size(); i++) {
        tableList.add(OdfTable.getInstance(tableElementList.get(i)));
      }
    } catch (Exception e) {
      Logger.getLogger(OdfDocument.class.getName()).log(Level.SEVERE, null, e);
    }
    return tableList;
  }

  /**
   * Update document meta data in the ODF document. Following metadata data is being updated:
   *
   * <ul>
   *   <li>The name of the person who last modified this document will be the Java user.name System
   *       property
   *   <li>The date and time when the document was last modified using current data
   *   <li>The number of times this document has been edited is incremented by 1
   *   <li>The total time spent editing this document
   * </ul>
   *
   * TODO:This method will be moved to OdfMetadata class. see
   * http://odftoolkit.org/bugzilla/show_bug.cgi?id=204
   */
  public void updateMetaData() {
    if (getOfficeMetadata().hasAutomaticUpdate()) {
      OdfOfficeMeta metaData = getOfficeMetadata();

      // OpenOffice 3.4.1 needs this metadata to continue list numbering correctly
      metaData.setGenerator(FORMER_OPEN_OFFICE_VERSION);

      // set creation date
      if (mCreationDate != null) {
        getOfficeMetadata().setCreationDate(mCreationDate);
      }

      // update late modfied date
      Calendar calendar = Calendar.getInstance();
      metaData.setDate(calendar);

      // update editing-cycles
      Integer cycle = metaData.getEditingCycles();
      if (cycle != null) {
        metaData.setEditingCycles(++cycle);
      } else {
        metaData.setEditingCycles(1);
      }
      // update editing-duration
      long editingDuration = calendar.getTimeInMillis() - documentOpeningTime;
      editingDuration = (editingDuration < 1) ? 1 : editingDuration;
      try {
        DatatypeFactory aFactory = DatatypeFactory.newInstance();
        metaData.setEditingDuration(new Duration(aFactory.newDurationDayTime(editingDuration)));
      } catch (DatatypeConfigurationException e) {
        Logger.getLogger(OdfDocument.class.getName())
            .log(
                Level.SEVERE,
                "editing duration update fail as DatatypeFactory can not be instanced",
                e);
      }
    }
  }

  // /////////////////
  // Following is the implementation of locale settings
  // ////////////////
  /**
   * Unicode characters are in general divided by office applications into three different groups.
   *
   * <p>1) There is CJK: the Chinese, Japanese and Korean script (also old Vietnamese belong to this
   * group). See http://en.wikipedia.org/wiki/CJK_characters
   *
   * <p>2) There is CTL: Complex Text Layout, which uses BIDI algorithms and/or glyph modules for
   * instance Arabic, Hebrew, Indic and Thai. See http://en.wikipedia.org/wiki/Complex_Text_Layout
   *
   * <p>3) And there is all the rest, which was once called by MS Western.
   */
  public enum UnicodeGroup {

    /** Western language */
    WESTERN,
    /** Chinese, Japanese and Korean */
    CJK,
    /** Complex Text Layout language */
    CTL;
  }

  private static final HashSet<String> CJKLanguage = new HashSet<String>();
  private static final HashSet<String> CTLLanguage = new HashSet<String>();

  {
    CJKLanguage.add("zh"); // LANGUAGE_CHINES
    CJKLanguage.add("ja"); // LANGUAGE_JAPANESE
    CJKLanguage.add("ko"); // LANGUAGE_KOREANE

    CTLLanguage.add("am"); // LANGUAGE_AMHARIC_ETHIOPIA
    CTLLanguage.add("ar"); // LANGUAGE_ARABIC_SAUDI_ARABIA
    CTLLanguage.add("as"); // LANGUAGE_ASSAMESE
    CTLLanguage.add("bn"); // LANGUAGE_BENGALI
    CTLLanguage.add("bo"); // LANGUAGE_TIBETAN
    CTLLanguage.add("brx"); // LANGUAGE_USER_BODO_INDIA
    CTLLanguage.add("dgo"); // LANGUAGE_USER_DOGRI_INDIA
    CTLLanguage.add("dv"); // LANGUAGE_DHIVEHI
    CTLLanguage.add("dz"); // LANGUAGE_DZONGKHA
    CTLLanguage.add("fa"); // LANGUAGE_FARSI
    CTLLanguage.add("gu"); // LANGUAGE_GUJARATI
    CTLLanguage.add("he"); // LANGUAGE_HEBREW
    CTLLanguage.add("hi"); // LANGUAGE_HINDI
    CTLLanguage.add("km"); // LANGUAGE_KHMER
    CTLLanguage.add("kn"); // LANGUAGE_KANNADA
    CTLLanguage.add("ks"); // LANGUAGE_KASHMIRI
    CTLLanguage.add("ku"); // LANGUAGE_USER_KURDISH_IRAQ
    CTLLanguage.add("lo"); // LANGUAGE_LAO
    CTLLanguage.add("mai"); // LANGUAGE_USER_MAITHILI_INDIA
    CTLLanguage.add("ml"); // LANGUAGE_MALAYALAM
    CTLLanguage.add("mn"); // LANGUAGE_MONGOLIAN_MONGOLIAN
    CTLLanguage.add("mni"); // LANGUAGE_MANIPURI
    CTLLanguage.add("mr"); // LANGUAGE_MARATHI
    CTLLanguage.add("my"); // LANGUAGE_BURMESE
    CTLLanguage.add("ne"); // LANGUAGE_NEPALI
    CTLLanguage.add("or"); // LANGUAGE_ORIYA
    CTLLanguage.add("pa"); // LANGUAGE_PUNJABI
    CTLLanguage.add("sa"); // LANGUAGE_SANSKRIT
    CTLLanguage.add("sd"); // LANGUAGE_SINDHI
    CTLLanguage.add("si"); // LANGUAGE_SINHALESE_SRI_LANKA
    CTLLanguage.add("syr"); // LANGUAGE_SYRIAC
    CTLLanguage.add("ta"); // LANGUAGE_TAMIL
    CTLLanguage.add("te"); // LANGUAGE_TELUGU
    CTLLanguage.add("th"); // LANGUAGE_THAI
    CTLLanguage.add("ug"); // LANGUAGE_UIGHUR_CHINA
    CTLLanguage.add("ur"); // LANGUAGE_URDU
    CTLLanguage.add("yi"); // LANGUAGE_YIDDISH
  }

  /**
   * Set a locale information.
   *
   * <p>The locale information will affect the language and country setting of the document. Thus
   * the font settings, the spell checkings and etc will be affected.
   *
   * @param locale - an instance of Locale
   */
  public void setLocale(Locale locale) {
    setLocale(locale, getUnicodeGroup(locale));
  }

  /**
   * This method will return Locale, which presents the default language and country information
   * settings in this document.
   *
   * @return an instance of Locale that the default language and country is set to.
   */
  /**
   * Similar to OpenOffice.org, ODFDOM assumes that every Locale is related to one of the three
   * Unicodes Groups, either CJK, CTL or Western.
   *
   * @param locale the UnicodeGroup is requested for
   * @return the related UnicodeGroup
   */
  public static UnicodeGroup getUnicodeGroup(Locale locale) {
    String language = locale.getLanguage();
    if (CJKLanguage.contains(language)) {
      return UnicodeGroup.CJK;
    }
    if (CTLLanguage.contains(language)) {
      return UnicodeGroup.CTL;
    }
    return UnicodeGroup.WESTERN;
  }

  /**
   * Set a locale of a specific script type.
   *
   * <p>If the locale does not belong to the script type, it will not be set.
   *
   * @param locale - Locale information
   * @param unicodeGroup - The script type
   */
  private void setLocale(Locale locale, UnicodeGroup unicodeGroup) {
    try {
      switch (unicodeGroup) {
        case WESTERN:
          setDefaultWesternLanguage(locale);
          break;
        case CJK:
          setDefaultAsianLanguage(locale);
          break;
        case CTL:
          setDefaultComplexLanguage(locale);
          break;
      }
    } catch (Exception e) {
      Logger.getLogger(OdfDocument.class.getName()).log(Level.SEVERE, "Failed to set locale", e);
    }
  }

  /**
   * This method will return Locale, which presents the default language and country information
   * settings in this document
   *
   * <p>ODF allows to set a Locale for each of the three UnicodeGroups. Therefore there might be
   * three different Locale for the document.
   *
   * @param unicodeGroup - One of the three (CJK, CTL or Western).
   * @return the Locale for the given UnicodeGroup
   */
  public Locale getLocale(UnicodeGroup unicodeGroup) {
    try {
      switch (unicodeGroup) {
        case WESTERN:
          return getDefaultLanguageByProperty(
              OdfTextProperties.Country, OdfTextProperties.Language);
        case CJK:
          return getDefaultLanguageByProperty(
              OdfTextProperties.CountryAsian, OdfTextProperties.LanguageAsian);
        case CTL:
          return getDefaultLanguageByProperty(
              OdfTextProperties.CountryComplex, OdfTextProperties.LanguageComplex);
      }
    } catch (Exception e) {
      Logger.getLogger(OdfDocument.class.getName()).log(Level.SEVERE, "Failed to get locale", e);
    }
    return null;
  }

  /** Returns the current Locale for the OdfStyleProperty of the corresponding UnicodeGroup */
  private Locale getDefaultLanguageByProperty(
      OdfStyleProperty countryProp, OdfStyleProperty languageProp) throws Exception {
    String lang = null, ctry = null;

    OdfOfficeStyles styles = getStylesDom().getOfficeStyles();

    // get language and country setting from default style setting for
    // paragraph
    OdfDefaultStyle defaultStyle = styles.getDefaultStyle(OdfStyleFamily.Paragraph);
    if (defaultStyle != null) {
      if (defaultStyle.hasProperty(countryProp) && defaultStyle.hasProperty(languageProp)) {
        ctry = defaultStyle.getProperty(countryProp);
        lang = defaultStyle.getProperty(languageProp);
        return new Locale(lang, ctry);
      }
    }
    // if no default style setting for paragraph
    // get language and country setting from other default style settings
    Iterable<OdfDefaultStyle> defaultStyles = styles.getDefaultStyles();
    Iterator<OdfDefaultStyle> itera = defaultStyles.iterator();
    while (itera.hasNext()) {
      OdfDefaultStyle style = itera.next();
      if (style.hasProperty(countryProp) && style.hasProperty(languageProp)) {
        ctry = style.getProperty(countryProp);
        lang = style.getProperty(languageProp);
        return new Locale(lang, ctry);
      }
    }
    return null;
  }

  /**
   * This method will set the default language and country information of the document, based on the
   * parameter of the Locale information.
   *
   * @param locale - an instance of Locale that the default language and country will be set to.
   * @throws Exception
   */
  private void setDefaultWesternLanguage(Locale locale) throws Exception {
    OdfOfficeStyles styles = getStylesDom().getOfficeStyles();
    Iterable<OdfDefaultStyle> defaultStyles = styles.getDefaultStyles();
    if (defaultStyles != null) {
      Iterator<OdfDefaultStyle> itera = defaultStyles.iterator();
      while (itera.hasNext()) {
        OdfDefaultStyle style = itera.next();
        if (style.getFamily().getProperties().contains(OdfTextProperties.Language)) {
          style.setProperty(OdfTextProperties.Language, locale.getLanguage());
          style.setProperty(OdfTextProperties.Country, locale.getCountry());
        }
      }
    }
  }

  /**
   * This method will set the default Asian language and country information of the document, based
   * on the parameter of the Locale information. If the Locale instance is not set a Asian language
   * (Chinese, Traditional Chinese, Japanese and Korean, nothing will take effect.
   *
   * @param locale - an instance of Locale that the default Asian language and country will be set
   *     to.
   * @throws Exception
   */
  private void setDefaultAsianLanguage(Locale locale) throws Exception {
    OdfOfficeStyles styles = getStylesDom().getOfficeStyles();
    Iterable<OdfDefaultStyle> defaultStyles = styles.getDefaultStyles();
    if (defaultStyles != null) {
      Iterator<OdfDefaultStyle> itera = defaultStyles.iterator();
      while (itera.hasNext()) {
        OdfDefaultStyle style = itera.next();
        if (style.getFamily().getProperties().contains(OdfTextProperties.LanguageAsian)) {
          style.setProperty(OdfTextProperties.LanguageAsian, locale.getLanguage());
          style.setProperty(OdfTextProperties.CountryAsian, locale.getCountry());
        }
      }
    }
  }

  /**
   * This method will set the default complex language and country information of the document,
   * based on the parameter of the Locale information.
   *
   * @param locale - an instance of Locale that the default complex language and country will be set
   *     to.
   * @throws Exception
   */
  private void setDefaultComplexLanguage(Locale locale) throws Exception {
    OdfOfficeStyles styles = getStylesDom().getOfficeStyles();
    Iterable<OdfDefaultStyle> defaultStyles = styles.getDefaultStyles();
    if (defaultStyles != null) {
      Iterator<OdfDefaultStyle> itera = defaultStyles.iterator();
      while (itera.hasNext()) {
        OdfDefaultStyle style = itera.next();
        if (style.getFamily().getProperties().contains(OdfTextProperties.LanguageComplex)) {
          style.setProperty(OdfTextProperties.LanguageComplex, locale.getLanguage());
          style.setProperty(OdfTextProperties.CountryComplex, locale.getCountry());
        }
      }
    }
  }

  private Set<String> mFontNames = null;
  private Map<String, OfficeAnnotationElement> annotations = null;

  /**
   * This is a temporary solution to know about the fonts within the documents. Project client does
   * not provide font information, therefore the 16 fonts are hard coded
   */
  public Set<String> getFontNames() {
    if (mFontNames == null) {
      mFontNames = new HashSet<String>();
    }
    return mFontNames;
  }

  public void addAnnotation(String name, OfficeAnnotationElement element) {
    if (annotations == null) {
      annotations = new HashMap<String, OfficeAnnotationElement>();
    }
    annotations.put(name, element);
  }

  public OfficeAnnotationElement getAnnotation(String name) {
    if (annotations == null || !annotations.containsKey(name)) {
      return null;
    }
    return annotations.get(name);
  }

  public void removeAnnotation(String name) {
    if (annotations != null && annotations.containsKey(name)) {
      annotations.remove(name);
    }
  }

  public String getUniqueAnnotationName() {
    String prefix = "CmtId";
    int freeIndex = 0;
    if (annotations != null) {
      while (annotations.containsKey(prefix + freeIndex)) {
        ++freeIndex;
      }
    }
    return prefix + freeIndex;
  }

  protected void removeCachedView() {
    mPackage = getPackage();
    // removes the LO/AO view caching
    mPackage.remove("Thumbnails/thumbnail.png");
  }

  /**
   * @return TRUE if the document was created by CollabTextDocument and thereby supports user
   *     changes, otherwise FALSE
   */
  public Boolean hasCollaboration() {
    return mHasCollaboration != null && mHasCollaboration;
  }
}