/*
Copyright (C) 2000-2010  Ministere de la culture et de la communication (France), AJLSM
See LICENCE file
*/
package fr.gouv.culture.sdx.repository;

import fr.gouv.culture.sdx.document.Document;
import fr.gouv.culture.sdx.document.ParsableDocument;
import fr.gouv.culture.sdx.exception.SDXException;
import fr.gouv.culture.sdx.exception.SDXExceptionCode;
import fr.gouv.culture.sdx.utils.Utilities;
import fr.gouv.culture.sdx.utils.configuration.ConfigurationUtils;
import fr.gouv.culture.sdx.utils.constants.ContextKeys;
import fr.gouv.culture.sdx.utils.constants.Node;
import fr.gouv.culture.sdx.utils.database.DatabaseEntity;
import fr.gouv.culture.sdx.utils.logging.LoggingUtils;
import fr.gouv.culture.sdx.utils.save.SaveParameters;
import fr.gouv.culture.sdx.utils.save.Saveable;

/*MAJ_COCOON_2.1.10
import org.apache.avalon.excalibur.io.IOUtil;*/
import org.apache.commons.io.IOUtils;
import org.apache.excalibur.xml.sax.SAXParser;
import org.apache.avalon.framework.service.ServiceException;
import org.apache.avalon.framework.service.ServiceManager;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.context.Context;
import org.apache.avalon.framework.context.ContextException;
import org.apache.cocoon.xml.XMLConsumer;
import org.xml.sax.ContentHandler;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;

/**
 * Implements a repository where url's are referenced.
 *
 * <p>
 * There are two main concerns with this repository : (1) how to retrieve efficiently
 * a document from it's id and (2) how to access the referenced urls.
 * <p>
 * For the first concern, we use a simplified SDX database to keep track of
 * ids and their references.
 * <p>
 * For the second concern, one can give some parameters to help
 * reference the urls. These parameters are :
 * <ol>
 * <li><code>base</code>, http url, local file url, relative path
 * to the directory which contains this file, it must already exist
 * </ol>
 *
 */
public class URLRepository extends AbstractDatabaseBackedRepository {

    /** If true, we must check documents when asked to retrieve it. */
    private boolean checkOnGet = false;

    /** If true, we use a cache. */
    private boolean useCache = false;

    /** The base URL for this repository, may be null. */
    private URL baseURL = null;

    /** The repository used for caching, may be null. */
    private Repository cacheRepository = null;

    /** The property used for sroting the URL. */
    private static String URL_PROPERTY = "url";

    /** The property used for storing the modification time. */
    private static String TIME_PROPERTY = "time";

    /** The property used for storing a flag indicating the url's path format. */
    private static String IS_RELATIVE_PROPERTY = "relative";

    /**String representation of an attribute name*/
    private final String ATTRIBUTE_BASE = "base";

    /**String representation of and element name in configuration file*/
    private final String ELEMENT_CACHE = "cache";

    /**String representation of and element name in configuration file*/
    private final String ELEMENT_REPOSITORY = "repository";

    /**String representation of an attribute name*/
    private final String ATTRIBUTE_CHECK = "check";

    /**String representation of an attribute name*/
    private String ATTRIBUTE_VALUE_ON_GET = "onGet";

    /**Indicates whether a file's path we store is relative, will return true (for relative paths based on a baseURL), if not the object is null*/
    private Boolean pathIsRelative;

    /**
     * Creates a repository.
     *
     * <p>
     * A super.getLog() must be set and then this repository must be configured and initialized.
     *
     * @see #enableLogging
     * @see #configure
     * @see #init
     */
    public URLRepository() {
    }

    /** Releases a previously opened connection.
     *
     *	@param	c	The connection to release.
     */
    public void releaseConnection(RepositoryConnection c) throws SDXException {
        super.releaseConnection(c);
        if (useCache) cacheRepository.releaseConnection(c);
    }

    /** Gets a connection for manipulating store's content.
     *
     */
    public RepositoryConnection getConnection() throws SDXException {
        if (useCache)
            return cacheRepository.getConnection();
        else {
            URLRepositoryConnection conn = new URLRepositoryConnection();
            conn.enableLogging(super.getLog());
            //TODO: determine if this is necessary
            //conn.setUp(this._database.getIndexPath());
            return conn;
        }
    }

    /** Configures this repository.
     *
     * <p>
     *  In addition to the parameters needed in the base configuration handled by the parent class,
     *  the following parameters are allowed : a base (optional) useful if documents exist in one directory;
     *  links to the documents will be made relative to this base, this facilitates the portability of a repository
     *  to other machines.
     *
     * @param   configuration   The configuration for this repository (based on a xml file).
     *
     *<p> Sample configuration entry:
     *<p>&lt;sdx:repository sdx:type = "URL" sdx:id = "myRepoId" base = "http url, local file url, relative path to the directory which contains this file"/>
     *@see #documented_application.xconf we should link to this in the future when we have better documentation capabilities
     *
     */
    public void configure(Configuration configuration) throws ConfigurationException {
        /*

            A URL repository can be configured easily, there is only one thing to
            take care or not : caching.

            If we decide to cache, it means that we keep a copy of the document
            locally. We have two options : either this document is checked for changes
            at every acceptRequest for it, or it is checked only at indexing.

            A cache will be implemented as a repository. Of course this second repository
            should not be a URLRepository, but probably a FS or JDBC repository. file: URL
            should never be cached.

            So it could be something like :

                <repository type="URL" base="file:///some/path">
                    <cache check="onIndex|onGet">
                        <repository type="FS">
                            ...
                        </repository>
                    </cache>
                </repository>

            The base attribute could be used to store resources relative to a
            base directory, this directory needs to end with a File.separator (system specific). So if we move an application and the documents, only
            the base URL could be changed, and all the resources would still
            be available.

            A URL repository will need to store some information within a database. The key will
            be the document's id. Other fields needed will be :

                - Absolute or relative URL of the document
                - Date and time of the last modification of the original document

        */

        try {
            // Let the superclass handle basic configurations of the "id" and "isDefault" fields
            loadBaseConfiguration(configuration);

            //getting the value of the url base attribute
            String base = configuration.getAttribute(ATTRIBUTE_BASE, null);
            //not verifying attribute, because if non-existent, we default to absolute paths, absolute paths
            //TODOLogging:maybe log a info message detailing what type of  storage and retrieval we are doing, relative or absolute
            if (Utilities.checkString(base)) {

                //TODOImplement: add support for ftp and other protocols as needed
                if (base.startsWith("http://")) {
                    //ensuring our base url ends with at path separator so we can accurately build relative urls
                    if (!base.endsWith("/")) base = base + "/";

                    //assigning a value to the baseURL field, if it is an absolute http url
                    baseURL = new URL(base);
                } else {
                    //assigning a value to the baseURL field, it is a file path so we will build a file and then take it's url
                    File baseFile = Utilities.resolveFile(null, configuration.getLocation(), super.getContext(), base, false);
//                    parentURL = new URL(baseFile.toString().substring(0, baseFile.toString().indexOf(base)));
                    baseURL = baseFile.toURI().toURL();
                }
            }

            //getting the cached configuration element if it exists, if not it will be null (based on the false param)
            Configuration cacheConf = configuration.getChild(ELEMENT_CACHE, false);

            //if we have the element we configure the cache repository, if not the value will be null
            if (cacheConf != null) {
                //setting class boolean to indicate caching is desired
                useCache = true;
                //getting the attribute value, if doesn't exist we use the default check="onGet"
                //is using this default value ok?
                String check = cacheConf.getAttribute(ATTRIBUTE_CHECK, ATTRIBUTE_VALUE_ON_GET);
                //comparing the value against the values and setting the boolean
                if (check.equalsIgnoreCase(ATTRIBUTE_VALUE_ON_GET)) checkOnGet = true;
                //TODOImplement?:checkOnIndex scenario?
                //getting the configuration element for the caching repository
                Configuration cacheRepoConf = cacheConf.getChild(ELEMENT_REPOSITORY, false);

                if (cacheRepoConf == null) {
                    String[] args = new String[1];
                    args[0] = cacheConf.getLocation();
                    SDXException sdxE = new SDXException(super.getLog(), SDXExceptionCode.ERROR_NO_CACHE_REPO_CONFIG, args, null);
                    throw new ConfigurationException(sdxE.getMessage(), sdxE);
                }

                /*check for the ref attribute, if it exists, get the repository object and add it to the local hashtable
                *if the attribute doesn't exist create the repo like below, we also need to handle DEFAULTS with refs*/
                String ref = cacheRepoConf.getAttribute(Repository.ConfigurationNode.REF, null);
                if (Utilities.checkString(ref)) {
                    Context appRepos = (Context) super.getContext().get(ContextKeys.SDX.Application.REPOSITORIES);
                    if (appRepos != null)
                        cacheRepository = (Repository) appRepos.get(ref);
                    if (cacheRepository == null) {
                        String[] args = new String[1];
                        args[0] = ref;
                        throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LOAD_REFERENCED_REPO, args, null);
                    }
                } else
                //creating the desired repository and assiging a value to the the class field
                    cacheRepository = ConfigurationUtils.createRepository(super.getLog(), super.getContext(), super.getServiceManager(), cacheRepoConf);

            }

            //creating a path for the location of this repository
            //TODORefactor this line exists in both FSRepository and URLRepository
            /*
            String databaseDirPath = Utilities.getStringFromHashtable(Application.REPOSITORIES_DIR_PATH, _context) + id + File.separator + DATABASE_DIR_NAME + File.separator;
            //create a file for the directory of our indexing information for this repository
            //testing the directory for the path, to ensure it is available and we have access
            Utilities.checkDirectory(databaseDirPath, super.getLog());
            //adding the directory path to the _context table
            _context.put(Database.DATABASE_DIR_PATH, databaseDirPath);
            */

        } catch (ConfigurationException e) {
            LoggingUtils.logException(super.getLog(), e);
            throw e;
        } catch (MalformedURLException e) {
            LoggingUtils.logException(super.getLog(), e);
        } catch (SDXException e) {
            throw new ConfigurationException(e.getMessage(), e.fillInStackTrace());
        } catch (ContextException e) {
            throw new ConfigurationException(e.getMessage(), e);
        }

    }

    /** Creates the store (creates DB) if not already done. */
    public void init() throws SDXException {
        //initializing the super class
        super.init();
    }

    /** Returns the number of documents within the repository (all kind of documents). */
    public long size() throws SDXException {
        return _database.size();
    }

    /** Returns the number of documents of specified type within the store.
     *
     *	@param	type	The type of document, muse be defined in fr.gouv.culture.sdx.documents.Document constants.
     */
    public long size(int type) throws SDXException {
        return _database.size();
    }

    /**
     * Lists the repository content as SAX events.
     *
     * <p>
     * The exact structure is still to be defined, but it should be very
     * simple, with only one element per document and a few properties
     * as attributes.
     *
     *	@param	hdl		The SAX content handler to feed with events.
     */
    public void lists(ContentHandler hdl) throws SDXException {
    }

    /**
     * Adds a document to the repository.
     *
     *	@param	doc		The document to add.
     *	@param	c		A connection to the repository.
     */
    public synchronized void add(Document doc, RepositoryConnection c) throws SDXException {
        //ensuring we have valid objects
        super.add(doc, c);
        String docId = doc.getId();
        String docUrl = getURL(doc);
        long now = (new Date()).getTime();
        //TODOException?:should we throw an exception if we cant get a valid url, i think getURL should be throwing an exception?-rbp
        if (Utilities.checkString(docUrl)) {
            // With a cache, we need to store the document in this cache.
            if (useCache) cacheRepository.add(doc, c);
            // We then store the information needed
            DatabaseEntity de = new DatabaseEntity(docId);
            de.addProperty(URL_PROPERTY, docUrl);
            de.addProperty(TIME_PROPERTY, (new Long(now)).toString());
            de.addProperty(IS_RELATIVE_PROPERTY, pathIsRelative.toString());
            if(_database!=null) _database.update(de);
        }
    }

    /**
     * Deletes all documents from the repository.
     */
    public synchronized void empty() throws SDXException {
        if (useCache) cacheRepository.empty();
        if(_database!=null) _database.empty();
    }

    /**
     * Deletes a document.
     *
     *	@param	doc		The document to delete.
     *  @param c        The connection to the repository.
     */
    public synchronized void delete(Document doc, RepositoryConnection c) throws SDXException {
        //ensuring we have valid objects
        super.delete(doc, c);

        if (useCache) cacheRepository.delete(doc, c);

        // We just need to remove the reference to the document in the database.
        if(_database!=null) _database.delete(new DatabaseEntity(doc.getId()));
    }

    /**
     * Retrieves a SDX document as SAX events.
     *
     *	@param	doc		    A ParsableDocument, ie XMLDocument or HTMLDocument.
     * @param	consumer	A SAX content handler to feed with events.
     * <p>The wrapped contentHandler for including events within an XSP page contentHandler should be created using
     IncludeXMLConsumer stripper = new IncludeXMLConsumer(xspContentHandler);</p>
     *	@param	c		A connection to the repository.
     */
    public void toSAX(ParsableDocument doc, XMLConsumer consumer, RepositoryConnection c) throws SDXException {
        /*TODORefactor?:can we refactor these methods into abstract repository given the changes i made there with the
        *necessity to make specific calls to the openStream methods of the subclasses?-rbp
        */
        //ensuring we have valid objects
        super.toSAX(doc, consumer, c);
        SAXParser parser = null;
        ServiceManager l_manager = super.getServiceManager();
        try {
            doc.setContent(this.openStream(doc, null, c));
            parser = (SAXParser) l_manager.lookup(SAXParser.ROLE);
            doc.parse(parser, consumer);
        } catch (ServiceException e) {
            String[] args = new String[1];
            args[0] = e.getMessage();
            //null super.getLog() passed to prevent double logging
            SDXException sdxE = new SDXException(null, SDXExceptionCode.ERROR_ACQUIRE_PARSER, args, e);

            String[] args2 = new String[2];
            args2[0] = this.getId();
            args2[1] = sdxE.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_GET_DOC, args2, sdxE);
        }/* catch (SDXException e) {
            //is this catch necessary, i think the finally will be exectued either way, but for safety we'll catch it now?-rbp
            throw e;
        }*/ finally {
            if (parser != null) l_manager.release(parser);
        }
    }

    /**
     * Opens a stream to read a document.
     *
     *	@param	doc		The document to read.
     *	@param	encoding		The encoding to use for serialization of XML content (may be <code> null</code> ).
     * <p>If <code> null</code> or invalid we use a default.
     * <p>TODOImplement: use of encoding currently not implemented , will do soon.
     *	@param	c		A connection to the repository.
     *
     *	@return		An input stream from which the serialized content of the document can be read.
     */
    public InputStream openStream(Document doc, String encoding, RepositoryConnection c) throws SDXException {
        //ensuring we have valid objects
        super.openStream(doc, encoding, c);
        return openStream(doc);
    }

    /**
     * Feeds a stream with a document.
     *
     *	@param	doc		The document to read.
     *	@param	os		The output stream where to write.
     *	@param	c		A connection to the repository.
     */
    public void get(Document doc, OutputStream os, RepositoryConnection c) throws SDXException {
        //ensuring we have valid objects
        super.get(doc, os, c);
        // We will first get an inputstream and then copy it into the output stream.
        try {
            InputStream is = openStream(doc);
            IOUtils.copy(is, os);
        } catch (IOException e) {
            String[] args = new String[3];
            args[0] = doc.getId();
            args[1] = this.getId();
            args[2] = e.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_GET_DOC, args, e);
        }
    }
    // TODOImplement : implement a cache...

    /**
     * Returns an URL given a URL property for a document.
     *
     * <p>
     * The URL is either the property itself if it's absolute or
     * if no base URL is used, or a combination of the relative URL
     * given and the base URL.
     */
    private URL getURL(Boolean isRelative, String u) throws MalformedURLException, IOException {
        if (isRelative.booleanValue())
            return new URL(baseURL, u);

        else
            return new URL(u);
    }

    /**
     * Opens a stream to a document managed with this repository.
     */
    private InputStream openStream(Document doc) throws SDXException {
    	//the document should be verified before passing it to this method, use Utilities.checkDocument(doc) in the calling method
    	// First get the database entity for this document.
    	DatabaseEntity de = null;
    	if(_database!=null)
    		de = this._database.getEntity(doc.getId());
    	if (de == null) {
    		// throw an exception for document not found.
    		//we need a doc or the doc needs an id
    		String[] args = new String[2];
    		args[0] = doc.getId();
    		args[1] = this.getId();
    		throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_NO_DOC_EXISTS_REPO, args, null);
    	} else {
    		if (useCache) {
    			//shouldn't we be checking the resource first to see if it update or even exists, before going to the cached copy?
    			// TODOImplement: add date checking if necessary
    			RepositoryConnection conn = cacheRepository.getConnection();
    			InputStream is = cacheRepository.openStream(doc, null, conn);
    			cacheRepository.releaseConnection(conn);    // Will this work if we close before using the stream?
    			return is;
    		} else {
    			// If we don't cache, we simply send the URL's content.
    			try {
    				URL docURL = getURL(Boolean.valueOf(de.getProperty(URLRepository.IS_RELATIVE_PROPERTY)), de.getProperty(URLRepository.URL_PROPERTY));
    				return docURL.openStream();
    			} catch (MalformedURLException e) {
    				// an application error here : the URL is not well formed.
    				String[] args = new String[3];
    				args[0] = doc.getId();
    				args[1] = this.getId();
    				args[2] = e.getMessage();
    				throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_GET_DOC, args, e);
    			} catch (IOException e) {
    				// a system error, resource not available
    				String[] args = new String[3];
    				args[0] = doc.getId();
    				args[1] = this.getId();
    				args[2] = e.getMessage();
    				throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_GET_DOC, args, e);
    			}
    		}
    	}
    }

    /**
     * Returns a string representation of the URL of a document, may be relative to the base URL.
     */
    private String getURL(Document doc) throws SDXException {
        URL docURL = doc.getURL();
        if (docURL != null) {
            if (baseURL == null) {
                pathIsRelative = new Boolean(false);
                return docURL.toExternalForm();
            } else
                return compareURL(docURL);
        } else
            return null;
    }

    /**Compares a URL against the base URL
     *
     * @param docURL The full URL for the file
     * @return
     */
    private String compareURL(URL docURL) {
        String rootFilePath = docURL.toExternalForm().substring(0, baseURL.toExternalForm().length());
        if (rootFilePath.equals(baseURL.toExternalForm())) {
            pathIsRelative = new Boolean(true);
            return docURL.toExternalForm().substring(baseURL.toExternalForm().length());
        } else
            pathIsRelative = new Boolean(false);
        return docURL.toExternalForm();
    }

    public void optimize() throws SDXException {
        super.optimize();
        if (useCache) cacheRepository.optimize();
    }

    protected boolean initToSax(){
    	if(!super.initToSax())
    		return false;
    	else{
    		if(this.baseURL!=null){
    			this._xmlizable_objects.put("Base_URL",this.baseURL.toString());
    		}else{
    			this._xmlizable_objects.put("Base_URL","none");
    		}
    		this._xmlizable_objects.put("Check_On_Get",String.valueOf(this.checkOnGet));
    		this._xmlizable_objects.put("Use_cache",String.valueOf(this.useCache));
    		this._xmlizable_objects.put("Cache_Repository",this.cacheRepository);
    		if(this.pathIsRelative!=null)
    			this._xmlizable_objects.put("Path_Is_Relative",this.pathIsRelative.toString());
    		return true;
    	}
    }

    /**Init the LinkedHashMap _xmlizable_volatile_objects with the objects in order to describ them in XML
	 * Some objects need to be refresh each time a toSAX is called*/
	protected void initVolatileObjectsToSax() {
		super.initVolatileObjectsToSax();
		if(this.baseURL!=null){
			this._xmlizable_objects.put("Base_URL",this.baseURL.toString());
		}else{
			this._xmlizable_objects.put("Base_URL","null");
		}
		this._xmlizable_objects.put("Use_cache",String.valueOf(this.useCache));
		this._xmlizable_objects.put("Check_On_Get",String.valueOf(this.checkOnGet));
		if(this.pathIsRelative!=null)
			this._xmlizable_objects.put("Path_Is_Relative",this.pathIsRelative.toString());
	}

	/** Save the repository
	 * @see fr.gouv.culture.sdx.utils.save.Saveable#backup(fr.gouv.culture.sdx.utils.save.SaveParameters)
	 */
	public void backup(SaveParameters save_config) throws SDXException {
		super.backup(save_config);
		if(save_config != null)
			if(save_config.getAttributeAsBoolean(Saveable.ALL_SAVE_ATTRIB,false))
			{
				save_config.setAttribute(Node.Name.TYPE,"URL");

			}
	}
	/** Restore the repository
	 * @see fr.gouv.culture.sdx.utils.save.Saveable#restore(fr.gouv.culture.sdx.utils.save.SaveParameters)
	 */
	public void restore(SaveParameters save_config) throws SDXException {

	}

}
