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

import java.io.File;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;

import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.context.ContextException;
import org.apache.avalon.framework.context.DefaultContext;
import org.apache.avalon.framework.service.ServiceException;
import org.apache.avalon.framework.service.ServiceManager;
import org.apache.cocoon.environment.Request;
import org.apache.excalibur.source.SourceException;
import org.apache.excalibur.source.impl.FileSource;
import org.xml.sax.SAXException;

import fr.gouv.culture.oai.AbstractOAIRepository;
import fr.gouv.culture.oai.OAIObject;
import fr.gouv.culture.oai.OAIRequest;
import fr.gouv.culture.oai.util.OAIUtilities;
import fr.gouv.culture.sdx.documentbase.DocumentBase;
import fr.gouv.culture.sdx.documentbase.IDGenerator;
import fr.gouv.culture.sdx.exception.SDXException;
import fr.gouv.culture.sdx.exception.SDXExceptionCode;
import fr.gouv.culture.sdx.framework.Framework;
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.database.Database;
import fr.gouv.culture.sdx.utils.database.DatabaseBacked;
import fr.gouv.culture.sdx.utils.database.DatabaseEntity;

/**Abstract oai repository for DocumentBases
 * providing common behaviors
 *<p>
 * TODORefactor: bring appropriate methods up from sub-classes
 *
 */
public abstract class AbstractDocumentBaseOAIRepository extends AbstractOAIRepository implements DocumentBaseOAIRepository {
    /** The framework's service manager. */
    protected ServiceManager manager = null;
    /** The framework's context. */
    protected DefaultContext context = null;
    /**The underlying document base*/
    protected DocumentBase documentBase = null;
    /**The database for this object*/
    protected Database _database = null;
    /**The document base's id*/
    protected String documentBaseId = null;
    /**The number of records for before issuing a resumption tokent*/
    protected int numRecordsPerResponse = OAIObject.NUMBER_RECORDS_PER_RESPONSE;//defaulted
    protected IDGenerator resumptionTokenIdGen = null;
    protected String externalIdPrefix = null;
    protected String id = null;
    protected boolean isDefault = false;

    public static final String PARAMETER_NAME_SDX_FIELD = "sdxFieldName";
    public static final String PARAMETER_NAME_SET_NAME = OAIObject.Node.Name.SET_NAME;
    public static final String PARAMETER_NAME_SET_SPEC = OAIObject.Node.Name.SET_SPEC;

    public void service(ServiceManager serviceManager) throws ServiceException {
        this.manager = serviceManager;
    }


    /**Basic configuration*/
    public void configure(Configuration configuration) throws ConfigurationException {
    	
    	String[] args = {this.documentBaseId};
    	OAIUtilities.logInfo(logger, SDXExceptionCode.OAI_REPOSITORY_CONFIGURING, args);
    	
        //should configure protocolVersion, granularity, deleteRecord, repositoryName, and admin email
        ConfigurationUtils.checkConfiguration(configuration);
        configureAdminEmails(configuration);
        //repository human readable name
        super.repositoryName = configuration.getAttribute(DocumentBaseOAIRepository.ConfigurationNode.NAME);
        this.numRecordsPerResponse = configuration.getAttributeAsInteger(DocumentBaseOAIRepository.ConfigurationNode.NO_PER_RESPONSE, 1000);
        configureBaseURL(configuration);
        configureId(configuration);
        configureDefault(configuration);
        configureExternalIdPrefix(configuration);
        configureDescription(configuration);
        configureDatabase(configuration);
        configureResumptionTokenIDGenerator(configuration);
        
        String[] args2 = {this.documentBaseId,this.getRepositoryId()};
        OAIUtilities.logInfo(logger, SDXExceptionCode.OAI_REPOSITORY_CONFIGURED, args2);

    }

    /**
     * Configures the id of this OAI repository
     * @param configuration
     * @throws ConfigurationException
     */
    protected void configureId(Configuration configuration) throws ConfigurationException {
   	 String str = configuration.getAttribute(DocumentBaseOAIRepository.ConfigurationNode.ID, null);
   	 ConfigurationUtils.checkConfAttributeValue(DocumentBaseOAIRepository.ConfigurationNode.ID, str, configuration.getLocation());
   	OAIUtilities.logDebug(this.logger, "Configures oai repository id: "+str);
   	 if( Utilities.checkString(str) ) {
   		 this.id = str;
   	 }
   }

    /**
     * Configures the default flag : <code>true</code> if this OAI repository
     * is the default one, <code>false</code> or <code>null</code> if not.
     * @param configuration
     * @throws ConfigurationException
     */
    protected void configureDefault(Configuration configuration) throws ConfigurationException {
    	this.isDefault = configuration.getAttributeAsBoolean(DocumentBaseOAIRepository.ConfigurationNode.DEFAULT_OAI_REPOSITORY, false);
    	if ( !this.isDefault && documentBase.getOAIRepositoriesSize()==0 ) {
    		// si on n'a pas defini cet entrepot oai comme entrepot oai par defaut, et qu'il n'existe qu'un unique entrepot oai, alors on definit celui-ci comme l'entrepot oai par defaut
    		setDefault();
    	}
    	OAIUtilities.logDebug(this.logger, "Configures oai repository default flag: "+this.isDefault);
    }
    
    public void setDefault() {
    	this.isDefault = true;
    }
    
    public void unsetDefault() {
    	this.isDefault = false;
    }

    /**
     * Returns the default flag value
     * @return <code>true</code> if this OAI repository is the default one;
     * <code>false</code> if not.
     */
    public boolean isDefault() {
    	return this.isDefault;
    }

    /**Configures (sets) the external OAI id prefix from a configuration object.
     * <p>The prefix can be passed throught the configuration attribute:
     * <pre>&lt;oai-repository oai-id-prefix="my.external.oai.id.prefix"</pre>
     * If the configuration element does not contain an <code>oai-id-prefix</code>
     * attribute, the prefix is based on the pattern: <code>sdx:{baseURL}</code>
     * <br/>For example: <code>sdx:www.sdx.org:</code> for the baseURL = http://www.sdx.org/</p>
     * */
    protected void configureExternalIdPrefix(Configuration configuration) throws ConfigurationException {
        String str = configuration.getAttribute(DocumentBaseOAIRepository.ConfigurationNode.EXT_OAI_ID_PREFIX, null);
        if ( Utilities.checkString(str) ) {
        	this.externalIdPrefix = str + ((str.endsWith(":"))?"":":");
        } else {
        	 String scheme = Framework.SDXNamespacePrefix, //sdx
             		beginSub = ( this.baseURL.indexOf("http://") != -1 ) ? this.baseURL.substring("http://".length()) : this.baseURL,
             		endSub = beginSub.substring(0, beginSub.indexOf("/")),
             		nsId = endSub;//server base
     		 this.externalIdPrefix = scheme + ((nsId.endsWith(":"))?"":":") + nsId + ((nsId.endsWith(":"))?"":":");
        }
        OAIUtilities.logDebug(this.logger, "Configures oai repository external id prefix: "+this.externalIdPrefix);
    }

    protected void configureResumptionTokenIDGenerator(Configuration configuration) throws ConfigurationException {
        this.resumptionTokenIdGen = ConfigurationUtils.configureIDGenerator(this.logger, configuration);
        this.resumptionTokenIdGen.setDatabase(this._database);
    }

    /**Configures (sets) the baseURL class-field from a configuration object*/
    protected void configureBaseURL(Configuration configuration) throws ConfigurationException {
        //the base url of the repo
        super.baseURL = configuration.getAttribute(DocumentBaseOAIRepository.ConfigurationNode.BASE_URL);
        OAIUtilities.logDebug(this.logger, "Configures oai repository base url: "+this.baseURL);
    }


    /**Configures (sets) the description class-field from a configuration object*/
    protected void configureDescription(Configuration configuration) throws ConfigurationException {
        //Getting the value of the description element
        Configuration descriptionConf = configuration.getChild(DocumentBaseOAIRepository.ConfigurationNode.DESCRIPTION, false);
        if (descriptionConf != null) {

            String src = descriptionConf.getAttribute(DocumentBaseOAIRepository.ConfigurationNode.SRC);
            //verifying we have a good attribute
            ConfigurationUtils.checkConfAttributeValue(DocumentBaseOAIRepository.ConfigurationNode.SRC, src, descriptionConf.getLocation());
            File srcFile = null;
            try {
                srcFile = Utilities.resolveFile(null, descriptionConf.getLocation(), getContext(), src, false);
                src = srcFile.toURI().toURL().toExternalForm();
            } catch (SDXException e) {
                throw new ConfigurationException(e.getMessage(), e);
            } catch (MalformedURLException e) {
                throw new ConfigurationException(e.getMessage(), e);
            }

            //setting the class field

            try {
				super.description = new FileSource(src);
                //parsing the file to see if it is well-formed
                getDescription(this);
            }catch (SourceException e) {
				throw new ConfigurationException(e.getMessage(), e);
			}catch (MalformedURLException e) {
				throw new ConfigurationException(e.getMessage(), e);
			}catch (SAXException e) {
                throw new ConfigurationException(e.getMessage(), e);
            }
        }
    }


    /**Configures (sets) the adminEmails class-field from a configuration object*/
    protected void configureAdminEmails(Configuration configuration) throws ConfigurationException {
        //configure the admin email
        ArrayList locAdminEmailsList = new ArrayList();
        String firstAdminEmail = configuration.getAttribute(DocumentBaseOAIRepository.ConfigurationNode.ADMIN_EMAIL, null);
        Configuration[] locAdminEmails = configuration.getChildren(DocumentBaseOAIRepository.ConfigurationNode.ADMIN_EMAIL);
        if (Utilities.checkString(firstAdminEmail))
            locAdminEmailsList.add(firstAdminEmail);
        for (int i = 0; i < locAdminEmails.length; i++) {
            Configuration locAdminEmail = locAdminEmails[i];
            if (locAdminEmail != null) {
                String value = locAdminEmail.getValue();
                if (Utilities.checkString(value))
                    locAdminEmailsList.add(value);
            }
        }
        if (locAdminEmailsList.size() <= 0)//no admin email throw an error TODOException:make this exception better
            ConfigurationUtils.checkConfAttributeValue(DocumentBaseOAIRepository.ConfigurationNode.ADMIN_EMAIL, "", configuration.getLocation());

        super.adminEmails = (String[]) locAdminEmailsList.toArray(new String[0]);
        //releasing resources
        locAdminEmailsList.clear();
        locAdminEmailsList = null;

    }

    /**Configures (sets) the database class-field from a configuration object*/
    protected void configureDatabase(Configuration configuration) throws ConfigurationException {
        DatabaseBacked internalDb = new DatabaseBacked();
        try {
            internalDb.enableLogging(this.logger);
            internalDb.contextualize(Utilities.createNewReadOnlyContext(getContext()));
            internalDb.service(this.manager);
            internalDb.setId(getRepositoryId());
            internalDb.configure(configuration);
            internalDb.init();
            this._database = internalDb.getDatabase();
        } catch (SDXException e) {
            throw new ConfigurationException(e.getMessage(), e);
        } catch (ServiceException e) {
            throw new ConfigurationException(e.getMessage(), e);
        } catch (ContextException e) {
            throw new ConfigurationException(e.getMessage(), e);
        }


    }

    /**
     * Returns the internal identifier for this OAI repository.
     * <p>Returns the internal identifier for this OAI repository. It's build
     * on the pattern : <code>sdx_oai_r_{document base id}_{OAI repo id}</code>.
     * <br/>It's used, among other, to configure the SQL database, ie the table name.
     * </p>
     * @return
     */
    protected String getRepositoryId() {
        return	Framework.SDXNamespacePrefix + "_" +
        		OAIObject.Node.Prefix.OAI + "_" + "r" + "_" +
        		this.documentBaseId + "_" + this.getId();
    }

    /**
     * Returns the identifier for this OAI repository as a String.
     * @return The identifier: <code>default</code> if it's the default OAI
     * repository, the id if not.
     * @see #configureId(Configuration) ID configuration
     */
    public String getId(){
    	if ( this.isDefault) {
    		return "default";
    	}
    	else return this.id;
    }

    /**Builds an SDX/OAI identifier for a document
     *
     *<p>This identifier is used to populate the identifier OAI element. For
     *example, the one wich is returned by the "ListIdentifiers" verb.</p>
     *
     *@param request The request from which the url will be used for id construction
     *@param docId The sdx document identifier
     *@see #deriveInternalSdxId Deconstruction of id's built with this method
     *
     *@deprecated Since 2.4.0 use {@link #buildExternalOaiId(String)} instead.
     */
    protected String buildExternalOaiId(OAIRequest request, String docId) {
//    	String scheme = Framework.SDXNamespacePrefix, //sdx
// 			   beginSub = request.substring("http://".length()),
// 			   endSub = beginSub.substring(0, beginSub.indexOf("/")),
// 			   nsId = endSub,//server base
// 			   prefix = scheme + ":" + nsId + ":",
// 			   encoding = Utilities.getStringFromContext(ContextKeys.SDX.ENCODING, getContext()),
//		       appId = Utilities.getStringFromContext(ContextKeys.SDX.Application.DIRECTORY_NAME, getContext()),//application path string
//		       suffix = Utilities.encodeURL(appId, encoding) + "/" + Utilities.encodeURL(documentBaseId, encoding) + "/" + Utilities.encodeURL(docId, encoding);//particular ids
//        return this.externalIdPrefix + suffix;
    	return buildExternalOaiId(docId);
    }

    /**Builds an SDX/OAI identifier for a document
     *
     *<p>This identifier is used to populate the identifier OAI element. For
     *example, the one wich is returned by the "ListIdentifiers" verb.</p>
     *
     *@param request The request from which the url will be used for id construction
     *@param docId The sdx document identifier
     *@see #deriveInternalSdxId Deconstruction of id's built with this method
     *@see #configureExternalIdPrefix The configuration of the prefix
     */
    protected String buildExternalOaiId(String docId) {
        String encoding = Utilities.getStringFromContext(ContextKeys.SDX.ENCODING, getContext());
       //application path string
        String appId = Utilities.getStringFromContext(ContextKeys.SDX.Application.DIRECTORY_NAME, getContext());
        //particular ids
        String suffix = Utilities.encodeURL(appId, encoding) + "/" + Utilities.encodeURL(documentBaseId, encoding) + "/" + Utilities.encodeURL(docId, encoding);
        OAIUtilities.logDebug(logger, "Builds external OAI id for "+docId+": "+this.externalIdPrefix + suffix);
        return (this.externalIdPrefix + suffix);
    }

    /**Retrieves an internal SDX identifier from a full OAI identifier
     *
     *@param request The request from which the url will be used for id construction
     *@param fullOaiId The OAI record identifier
     *@see #buildExternalOaiId Construction of id's passed as the <code>fullOaiId</code> argument
     *@deprecated Since SDX 2.4.0, use {@link #deriveInternalSdxId(String)} instead.
     */
     protected String deriveInternalSdxId(OAIRequest request, String fullOaiId) {
    	 return deriveInternalSdxId(fullOaiId);
     }
	/**Retrieves an internal SDX identifier from a full OAI identifier
    *
    *@param request The request from which the url will be used for id construction
    *@param fullOaiId The OAI record identifier
    *@see #buildExternalOaiId construction of id's passed as the <code>fullOaiId</code> argument
    */
    protected String deriveInternalSdxId(String fullOaiId) {

    	OAIUtilities.logDebug(logger, "Derives internal SDX id for "+fullOaiId);

        //if we get a bad id we send it back
        if (!Utilities.checkString(fullOaiId)) return null;

        String dummyId = "sdxDummyId";
        // String dummyExternalId = buildExternalOaiId(request, dummyId);
        String dummyExternalId = buildExternalOaiId(dummyId);

        if (!fullOaiId.startsWith(dummyExternalId.substring(0, dummyExternalId.indexOf(dummyId)))) return null;

        //getting the end portion of our AOI identifier for internal use
        String appId = Utilities.getStringFromContext(ContextKeys.SDX.Application.DIRECTORY_NAME, getContext());
        String substring = appId + "/" + documentBaseId + "/";
        String encoding = Utilities.getStringFromContext(ContextKeys.SDX.ENCODING, getContext());
        substring = Utilities.decodeURL(substring, encoding);
        int beginIdx = fullOaiId.indexOf(substring);
        if (beginIdx < 0)
            beginIdx = 0;
        else
            beginIdx = beginIdx + substring.length();

        int endIdx = fullOaiId.length();
        String internalId = fullOaiId.substring(beginIdx, endIdx);
        internalId = Utilities.decodeURL(internalId, encoding);
        OAIUtilities.logDebug(logger, "Returns "+internalId);
        return internalId;

    }

    /**Builds an URL locator for the document with the provided id
     *
     *@param request The request
     *@param docId  The id of the document for which to build the locator
     * TODORemove: this is tightly coupled to the external sdx/sdx/sitemap.xmap
     */
    protected String buildUrlLocator(OAIRequest request, String docId) {
        String requestUrl = request.getRequestURL();
        int endIdx = requestUrl.indexOf(OAIRequest.URL_CHARACTER_QUESTION_MARK);
        if (endIdx < 0) endIdx = requestUrl.length();
        String base = requestUrl.substring(0, endIdx);
        if (!base.endsWith("/")) base += "/";
        base += docId;
        OAIUtilities.logDebug(logger, "Builds URL locator for the doc "+docId+": "+base);
        return base;
    }

    /**Builds a resumption token for a request and the database entity to store needed informations.
    *
    * @param request   The request
    * @return newResumptionToken	The next resumptionToken
    * @throws fr.gouv.culture.sdx.exception.SDXException
    */
    protected String createResumptionToken(OAIRequest request) throws SDXException {
        String previousResumptionToken = request.getResumptionToken();
        DatabaseEntity rtEnt = _database.getEntity(previousResumptionToken);
        String newResumptionToken = this.resumptionTokenIdGen.generate();
        String resultsId = "";
        Date date = fr.gouv.culture.oai.util.OAIUtilities.Date.getUtcIso8601Date();
        String utcDate = fr.gouv.culture.oai.util.OAIUtilities.Date.formatUtcISO8601Date(date);
        while (_database.entityExists(newResumptionToken)) {
            newResumptionToken = this.resumptionTokenIdGen.generate();
        }
        int cursor;
        if (rtEnt == null) {
            rtEnt = new DatabaseEntity(newResumptionToken);
            cursor = 0;
            Request cocoonRequest = request.getRequest();
            if (cocoonRequest != null) {
                Enumeration paramNames = cocoonRequest.getParameterNames();
                if (paramNames != null) {
                    while (paramNames.hasMoreElements()) {
                        String paramName = (String) paramNames.nextElement();
                        String paramVal = cocoonRequest.getParameter(paramName);
                        paramName = OAIUtilities.normalizeHttpRequestParameterName(paramName);
                        if (Utilities.checkString(paramName) && Utilities.checkString(paramVal)) {
                            if (OAIRequest.URL_PARAM_NAME_IDENTIFIER.equals(paramName))
                                rtEnt.addProperty(paramName, paramVal);
                            else if (OAIRequest.URL_PARAM_NAME_METADATA_PREFIX.equals(paramName))
                                rtEnt.addProperty(paramName, paramVal);
                            else if (OAIRequest.URL_PARAM_NAME_SET.equals(paramName))
                                rtEnt.addProperty(paramName, paramVal);
                            else if (OAIRequest.URL_PARAM_NAME_FROM.equals(paramName))
                                rtEnt.addProperty(paramName, paramVal);
                            else if (OAIRequest.URL_PARAM_NAME_UNTIL.equals(paramName))
                                rtEnt.addProperty(paramName, paramVal);
                            else if (OAIRequest.URL_PARAM_NAME_RESUMPTION_TOKEN.equals(paramName))
                                rtEnt.addProperty(paramName, paramVal);
                            else if (OAIRequest.URL_PARAM_NAME_VERB.equals(paramName))
                                rtEnt.addProperty(paramName, paramVal);
                        }
                    }
                }
            }

            resultsId = newResumptionToken+"_results";

        } else {
            //TODO: here, we have to search for existing DBE with invalid resumptionToken and delete them ; resumptionToken may have a validity limited in time -mp 2005-03-09
            cursor = Integer.parseInt(rtEnt.getProperty("cursor")) + this.numRecordsPerResponse;
            rtEnt.setId(newResumptionToken);
            resultsId = rtEnt.getProperty("resultsId");
        }

        //deleting the old info.
        rtEnt.deleteProperty("cursor");
        rtEnt.deleteProperty("responseDate");
        //adding the latest info.
        rtEnt.addProperty("cursor", Integer.toString(cursor));
        rtEnt.addProperty("resultsId",resultsId);
        rtEnt.addProperty("responseDate",utcDate);
        //deleting the previous database entity
        if (Utilities.checkString(previousResumptionToken))
            this._database.delete(new DatabaseEntity(previousResumptionToken));//deleting old token
        //adding the new entity
        this._database.update(rtEnt);//adding new token
        this._database.optimize();
        return newResumptionToken;

    }

    /**Returns the cursor index of the provided resumption token
     *
     * @param	resumptionToken
     */
    public String getResumptionTokenCursor(String resumptionToken) {
        return getResumptionTokenProperty(resumptionToken, "cursor");

    }

    /**Returns a property value for provided resumption token
     *
     * @param resumptionToken The resumption token in question
     * @param propertyName  The property name for which a value is desired
     *
     */
    protected String getResumptionTokenProperty(String resumptionToken, String propertyName) {
        String propVal = null;
        try {
            DatabaseEntity ent = _database.getEntity(resumptionToken);
            if (ent != null)
                propVal = ent.getProperty(propertyName);
        } catch (SDXException e) {
        	OAIUtilities.logException(logger, e);
        }
        return propVal;
    }

}
