/************************************************************************
 *
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 *
 * Copyright 2008, 2010 Oracle and/or its affiliates. All rights reserved.
 *
 * Use is subject to license terms.
 *
 * 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
 *
 * 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.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.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;

	// 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
		 */
		public String getMediaTypeString() {
			return mMediaType;
		}

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

		/**
		 *
		 * @param mediaType string defining an ODF document
		 * @return the according OdfMediatype encapuslating 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.
	 *
	 * 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);
		//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 lifecycle of OdfDocument.</p>
	 *
	 * @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 sequentiell)
	 * 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.</p>
	 *
	 * @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 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);
	}

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

	//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.
	 * @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) throws SAXException {
		OdfDocument newDoc = null;
		switch (odfMediaType) {
			case TEXT:
				newDoc = new OdfTextDocument(pkg, internalPath, OdfTextDocument.OdfMediaType.TEXT);
				break;

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

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

			case TEXT_WEB:
				newDoc = new OdfTextDocument(pkg, internalPath, OdfTextDocument.OdfMediaType.TEXT_WEB);
				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();
				if (metaDom == null) {
					metaDom = new OdfMetaDom(this, OdfSchemaDocument.OdfXMLFile.META.getFileName());
				}
				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>
	 *
	 * <p>If not, the OutputStream may not point to the input file! Otherwise
	 * this will result in unwanted behaviour and broken files.</p>
	 *
	 * <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>
	 *
	 * <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>
	 *
	 * <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 OdfPackage and release all temporary created data.
	 * Acter 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.
	 *
	 * 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.
	 *
	 * 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 beeing 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.
	 * @return a list of table features in this document.
	 */
	public List<OdfTable> getTableList() {
		List<OdfTable> tableList = null;
		try {
			List<TableTableElement> tableElementList = getTables();
			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>
	 * <li>The date and time when the document was last modified using current data</li>
	 * <li>The number of times this document has been edited is incremented by 1</li>
	 * <li>The total time spent editing this document</li>
	 * </ul>
	 *
	 * TODO:This method will be moved to OdfMetadata class.
	 *      see http://odftoolkit.org/bugzilla/show_bug.cgi?id=204
	 * @throws Exception
	 * @throws IllegalArgumentException
	 */
	private void updateMetaData() throws IllegalArgumentException, Exception {
		if (getOfficeMetadata().hasAutomaticUpdate()) {
			OdfOfficeMeta metaData = getOfficeMetadata();

			// 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
	// ////////////////
	/**
	 * <p>
	 * 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 final static HashSet<String> CJKLanguage = new HashSet<String>();
	private final static 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
	}

	/**
	 * <p>
	 * 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;

	}

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