/**
 * **********************************************************************
 *
 * 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.pkg;

import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.util.ResourceUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.transform.Templates;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import org.odftoolkit.odfdom.pkg.rdfa.Util;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;

/**
 *
 * The package layer described by the ODF 1.2 Package specification is
 * independent of the above ODF XML layer described by the ODF 1.2 XML Schema
 * specification.
 *
 * Still the abstract concept of documents exist in the ODF Package layer.
 */
public class OdfPackageDocument implements Closeable {

	private static final String TWO_DOTS = "..";
	private static final String SLASH = "/";
	private static final String COLON = ":";
	private static final String EMPTY_STRING = "";
	/**
	 * The path of the root document
	 */
	protected static final String ROOT_DOCUMENT_PATH = EMPTY_STRING;
	private Resolver mResolver;
	/**
	 * The ODF package containing the document
	 */
	protected OdfPackage mPackage;
	/**
	 * The internal path to the document relative to the ODF package
	 */
	protected String mDocumentPathInPackage;
	/**
	 * The mediatype of the ODF package document. Note: Not necessarily an ODF
	 * XML mediatype as specified in ODF 1.2 part1
	 */
	protected String mDocumentMediaType;
	private static Templates mRdfFileExtractionTemplate;

	/**
	 * Creates a new OdfPackageDocument.
	 *
	 * @param pkg - the ODF Package that contains the document. A baseURL is
	 * being generated based on its location.
	 * @param internalPath - the directory path within the package from where
	 * the document should be loaded.
	 * @param mediaTypeString - media type of stream. If unknown null can be
	 * used.
	 */
	protected OdfPackageDocument(OdfPackage pkg, String internalPath, String mediaTypeString) {
		if (pkg != null) {
			mPackage = pkg;
			mDocumentPathInPackage = internalPath;
			this.setMediaTypeString(mediaTypeString);
			pkg.cacheDocument(this, internalPath);
		} else {
			throw new IllegalArgumentException("No Package provided for new document!");
		}
	}

	/**
	 * Loads an OdfPackageDocument from the provided path.
	 *
	 * <p>OdfPackageDocument 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 OdfPackageDocument loadDocument(String documentPath) throws Exception {
		OdfPackage pkg = OdfPackage.loadPackage(documentPath);
		return pkg.loadDocument(ROOT_DOCUMENT_PATH);
	}

	/**
	 * Returns an embedded OdfPackageDocument from the given package path.
	 *
	 * @param documentPath to the document within the package. The path is
	 * relative the current document path.
	 * @return an embedded OdfPackageDocument
	 */
	public OdfPackageDocument loadSubDocument(String documentPath) {
		String internalPath = this.getDocumentPath() + documentPath;
		internalPath = OdfPackage.normalizeDirectoryPath(internalPath);
		return mPackage.loadDocument(internalPath);
	}

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

	/**
	 * @param mediaTypeString for the mediatype of this document
	 */
	protected final void setMediaTypeString(String mediaTypeString) {
		mDocumentMediaType = mediaTypeString;
		if (isRootDocument()) {
			mPackage.setMediaTypeString(mediaTypeString);
		}
	}

	/**
	 * Sets the OdfPackage that contains this OdfPackageDocument.
	 *
	 * @param pkg the OdfPackage that contains this OdfPackageDocument
	 */
	void setPackage(OdfPackage pkg) {
		mPackage = pkg;
	}

	/**
	 * Retreives the OdfPackage for this OdfPackageDocument.
	 *
	 * @return the OdfPackage that contains this OdfPackageDocument.
	 */
	public OdfPackage getPackage() {
		return mPackage;
	}

	/**
	 * Set the relative path for an embedded ODF document.
	 *
	 * @param path to directory of the embedded ODF document (relative to ODF
	 * package root).
	 */
	String setDocumentPath(String path) {
		mDocumentPathInPackage = normalizeDocumentPath(path);
		return mDocumentPathInPackage;
	}

	/**
	 * Get the relative path for an embedded ODF document.
	 *
	 * @return path to the directory of the embedded ODF document (relative to
	 * ODF package root).
	 */
	public String getDocumentPath() {
		return mDocumentPathInPackage;
	}

	/**
	 * Removes an embedded ODF document from the ODF Package. All files within
	 * the embedded document directory will be removed.
	 *
	 * @param internDocumentPath path to the directory of the embedded ODF
	 * document (always relative to the package path of the current document).
	 */
	public void removeDocument(String internDocumentPath) {
		mPackage.removeDocument(mDocumentPathInPackage + internDocumentPath);
	}

	/**
	 * @return true if the document is at the root level of the package
	 */
	public boolean isRootDocument() {
		if (getDocumentPath().equals(ROOT_DOCUMENT_PATH)) {
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Checks if the given reference is a reference, which points outside the
	 * ODF package Only relative path are allowed with the exception of a single
	 * slash '/' representing the root document.
	 *
	 * @param ref the file reference to be checked
	 * @return true if the reference is an package external reference
	 */
	protected static boolean isExternalReference(String ref) {
		boolean isExternalReference = false;
		// if the reference is a external relative filePath..
		if (ref.startsWith(TWO_DOTS)
				|| // or absolute filePath AND not root document)
				ref.startsWith(SLASH) && !ref.equals(SLASH)
				|| // or absolute IRI
				ref.contains(COLON)) {
			isExternalReference = true;
		}
		return isExternalReference;
	}

	/**
	 * Ensure the document path for is valid and gurantee unique encoding by
	 * normalizing the path.
	 *
	 * @see OdfPackage#normalizeDirectoryPath(java.lang.String)
	 * @param documentPath the destination directory of the document. The path
	 * should end with a '/'.
	 * @return the documentPath after normalization.
	 */
	protected static String normalizeDocumentPath(String documentPath) {
		String dirPath = OdfPackage.normalizeDirectoryPath(documentPath);
		//package path should not start with '/'.
		if (dirPath.startsWith(SLASH) && !dirPath.equals(SLASH)) {
			dirPath = dirPath.substring(1);
		}
		return dirPath;
	}

	/**
	 * Save the document to given path.
	 *
	 * <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 given path to get
	 * the saved embedded document.
	 *
	 * @param documentPath - the path to the package document
	 * @throws java.lang.Exception if the document could not be saved
	 */
	public void save(String documentPath) throws Exception {
		File f = new File(documentPath);
		save(f);
	}

	/**
	 * 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
	 */
	public void save(File file) throws Exception {
		mPackage.save(file);
	}

	/**
	 * Flush the existing DOM to the document to get in advantage of the recent
	 * changes from the DOM
	 */
	protected void flushDoms() {
		mPackage.flushDoms(this);
	}

	/**
	 * Embed an OdfPackageDocument to the current OdfPackageDocument. All the
	 * file entries of child document will be embedded as well to the current
	 * document package.
	 *
	 * @param newDocument the OdfPackageDocument to be embedded.
	 * @param documentPath to the directory the ODF document should be inserted
	 * (relative to the root of this document).
	 */
	public void insertDocument(OdfPackageDocument newDocument, String documentPath) {
		newDocument.flushDoms();
		// if the existing document already has a subdirtory if had to be added to the absolute
		mPackage.insertDocument(newDocument, mDocumentPathInPackage + documentPath);
	}

	/**
	 * @param internalPath path to the XML file relative to package root
	 * @return the typed DOM of the given file
	 */
	public OdfFileDom getFileDom(String internalPath) throws Exception {
		String normalizeDocumentPath = getDocumentPath();
		if (!isRootDocument()) {
			normalizeDocumentPath = normalizeDocumentPath(normalizeDocumentPath);
		}
		return OdfFileDom.newFileDom(this, normalizeDocumentPath + internalPath);
	}

	/**
	 * Get EntityResolver to be used in XML Parsers which can resolve content
	 * inside the OdfPackage
	 */
	EntityResolver getEntityResolver() {
		if (mResolver == null) {
			mResolver = new Resolver(mPackage);
		}
		return mResolver;
	}

	/**
	 * Get URIResolver to be used in XSL Transformations which can resolve
	 * content inside the OdfPackage
	 */
	URIResolver getURIResolver() {
		if (mResolver == null) {
			mResolver = new Resolver(mPackage);
		}
		return mResolver;
	}

	/**
	 * Close the OdfPackageDocument, its 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.
	 */
	public void close() {
		mPackage.close();
		// set all member variables explicit to null
		mPackage = null;
	}

	/**
	 * Helper class to receive an ODF document template for new documents from
	 * the environment (ie. from the JAR via classloader)
	 */
	protected static class Resource {

		private String name;

		public Resource(String name) {
			this.name = name;
		}

		public InputStream createInputStream() {
			InputStream in = OdfPackageDocument.class.getResourceAsStream(this.name);
			if (in == null) {
				Logger.getLogger(OdfPackageDocument.class.getName()).log(Level.SEVERE, "Could not find resource: {0}", this.name);
			}
			return in;
		}
	}

	/**
	 * Extracts RDF Metadata triple from XML files
	 *
	 * @param internalPath path to the XML file relative to package root
	 * @return RDF Metadata through GRDDL XSLT of given XML file
	 */
	public Model getXMLFileMetadata(String internalPath) {
		Model rdfModel = null;
		try {
			ByteArrayOutputStream out = new ByteArrayOutputStream();
			OdfXMLHelper helper = new OdfXMLHelper();
			if (mRdfFileExtractionTemplate == null) {
				String filePath = "file:" + OdfPackageDocument.class.getClassLoader().getResource("grddl/odf2rdf.xsl").getPath();
				URI uri = new URI(filePath);
				InputSource inputSource = new InputSource(uri.toString());
				mRdfFileExtractionTemplate = TransformerFactory.newInstance().newTemplates(new SAXSource(inputSource));
			}
			helper.transform(this.getPackage(), internalPath, mRdfFileExtractionTemplate, new StreamResult(out));
			String RDFBaseUri = Util.getRDFBaseUri(this.getPackage().getBaseURI(), internalPath);
			rdfModel = ModelFactory.createDefaultModel();
			rdfModel.read(new InputStreamReader(new ByteArrayInputStream(out.toByteArray()), "utf-8"), RDFBaseUri);
			// remove the last SLASH at the end of the RDFBaseUri:
			// test_rdfmeta.odt/ --> test_rdfmeta.odt
			ResourceUtils.renameResource(rdfModel.getResource(RDFBaseUri), RDFBaseUri.substring(0, RDFBaseUri.length() - 1));
		} catch (Exception ex) {
			Logger.getLogger(OdfPackageDocument.class.getName()).log(Level.SEVERE, null, ex);
		}
		return rdfModel;
	}
}
