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

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.Date;
import java.util.Locale;

import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.Searchable;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

import fr.gouv.culture.sdx.document.IndexableDocument;
import fr.gouv.culture.sdx.documentbase.LuceneDocumentBase;
import fr.gouv.culture.sdx.documentbase.LuceneIndexParameters;
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.framework.FrameworkImpl;
import fr.gouv.culture.sdx.search.lucene.Field;
import fr.gouv.culture.sdx.search.lucene.FieldList;
import fr.gouv.culture.sdx.search.lucene.analysis.MetaAnalyzer;
import fr.gouv.culture.sdx.search.lucene.queryparser.QueryParser;
import fr.gouv.culture.sdx.utils.SdxObject;
import fr.gouv.culture.sdx.utils.Utilities;
import fr.gouv.culture.sdx.utils.constants.Node;
import fr.gouv.culture.sdx.utils.logging.LoggingUtils;
import fr.gouv.culture.sdx.utils.lucene.LuceneDataStore;
import fr.gouv.culture.sdx.utils.save.SaveParameters;

/**
 * Information and services related to a LuceneIndex.
 *
 * <p>
 * From here we can get an IndexReader, a Searcher, and know a little more
 * about defined fields.
 */
public class LuceneIndex extends LuceneDataStore implements SdxObject, Index {

	/** The type of index */
	private boolean _isSub = false;

    /**The application in which this index is used.*/
    private String appId = "";

    /**The document base in which this index is used.*/
    private String dbId = "";

    /**The container for available search fields. */
    private FieldList fields = null;

    /**The temporary RAM directory for a batch of documents*/
    private RAMDirectory tempBatch = null;
    /** The current directory name*/
    public String CURRENT_INDEX_DIR = "current";

    /**The MetaAnalyzer*/
    private MetaAnalyzer metaAnalyzer = null;

    /**The specified query parser*/
    private Class queryParserClass = null;

    /**Default query parser class name*/
    private static final String DEFAULT_QUERYPARSER = "fr.gouv.culture.sdx.search.lucene.queryparser.DefaultQueryParser";

    /**The RMI registry name for the remote index*/
    private String remoteIndexName = "";

    /**The remote host for the remote index*/
    private String rmiHost = "";

    /**The remote port for the remote index*/
    private int rmiPort;

    /**The creation date of this index*/
    protected Date creationTimestamp = null;

    /**The last modification timestamp of this index*/
    protected long lastModificationTimestamp = 0;

    /**The "last-modification-timestamp" file*/
    protected File lastModFile = null;

    /** int representation of indexing action for adding a document. */
    public final static int ACTION_ADD_DOCUMENT = 1;

    /**Defaults for IndexWriter parameters; these are based on lucene defaults*/
    public static final int DEFAULT_MAX_FIELD_LENGTH = 10000;
    /**Defaults for IndexWriter parameters; these are based on lucene defaults*/
    public static final int DEFAULT_MAX_MERGE_DOCS = Integer.MAX_VALUE;
    /**Defaults for IndexWriter parameters; these are based on lucene defaults*/
    public static final int DEFAULT_MERGE_FACTOR = 10;

    /**Lucene IndexWriter parameter*/
    private int maxFieldLength = DEFAULT_MAX_FIELD_LENGTH; //Defaulted
    /**Lucene IndexWriter parameter*/
    private int maxMergeDocs = DEFAULT_MAX_MERGE_DOCS; //Defaulted
    /**Lucene IndexWriter parameter*/
    private int mergeFactor = DEFAULT_MERGE_FACTOR; //Defaulted

    /**String representation of the element name "queryParser". */
    private static final String ELEMENT_NAME_QUERYPARSER = "queryParser";

    /**String representation of the attribute named "class". */
    private static final String ATTRIBUTE_CLASS = "class";

    /**String representation of the attribute name "maxFieldLength". */
    private final String ATTRIBUTE_MAX_FIELD_LENGTH = "maxFieldLength";
		/**int indicating the number of actual documents labeled with the sdxall:1 field*/
    private int _docCount = -1;


    /**
     * Builds an index with fields definition and a path to the index files.
     *
     * @param dir     A directory where the index is kept.
     * @param host
     * @param port
     * @param appId
     * @param dbId
     * @throws SDXException
     */
    public LuceneIndex(File dir, String host, Integer port, String appId, String dbId) throws SDXException {
        super(dir);

        this.appId = appId;
        this.dbId = dbId;


        if (Utilities.checkString(host))
            this.rmiHost = host;

        if (port != null)
            this.rmiPort = port.intValue();
        else
            this.rmiPort = FrameworkImpl.SDX_DEFAULT_RMI_PORT;

        if (Utilities.checkString(appId) && Utilities.checkString(dbId))
            this.remoteIndexName = Utilities.buildRmiName(this.rmiHost, this.rmiPort, appId, dbId);
    }

    /**
     * @param dir
     * @param host
     * @param port
     * @param appId
     * @param dbId
     * @param useCompoundFiles
     * @throws SDXException
     */
    public LuceneIndex(File dir, String host, Integer port, String appId, String dbId, boolean useCompoundFiles) throws SDXException {
        super(dir);

        this.appId = appId;
        this.dbId = dbId;
        this.useCompoundFiles = useCompoundFiles;

        if (Utilities.checkString(host))
            this.rmiHost = host;

        if (port != null)
            this.rmiPort = port.intValue();
        else
            this.rmiPort = FrameworkImpl.SDX_DEFAULT_RMI_PORT;

        if (Utilities.checkString(appId) && Utilities.checkString(dbId))
            this.remoteIndexName = Utilities.buildRmiName(this.rmiHost, this.rmiPort, appId, dbId);
    }


    /**
     * Sub index constructor
     * @param parentIndex
     * @param dir
     */
    public LuceneIndex(LuceneIndex parentIndex, File dir)
    {
    	super(dir);
    	initSubIndex(parentIndex);
    }

    /**
     * Sub index constructor
     * @param parentIndex
     */
    public LuceneIndex(LuceneIndex parentIndex)
    {
    	super(new File(parentIndex.fsdFile.getAbsolutePath() + File.separator + "current"));
    	initSubIndex(parentIndex);
    }

    /** Initializes sub index
     *
     * @param parentIndex
     */
    private void initSubIndex(LuceneIndex parentIndex) {
    	this.appId = parentIndex.appId;
    	this.dbId = parentIndex.dbId;
    	this.rmiHost = parentIndex.rmiHost;
    	this.rmiPort = parentIndex.rmiPort;
    	this.useCompoundFiles = parentIndex.useCompoundFiles;
    	this.remoteIndexName = Utilities.buildRmiName(this.rmiHost, this.rmiPort, this.appId, this.dbId);
        this.creationTimestamp = parentIndex.creationTimestamp;
        this.lastModificationTimestamp = parentIndex.getLastModificationTimestamp();
        this.lastModFile = parentIndex.lastModFile;
    }

    public void configure(Configuration configuration) throws ConfigurationException {

        configureQueryParser(configuration);

        //configuring the names for the remote objects
        boolean remoteConf = configuration.getAttributeAsBoolean(LuceneDocumentBase.DBELEM_ATTRIBUTE_REMOTE_ACCESS, false);
        /*TODO:FIXME-if we don't have a remote index configuration we nullify the name so that
        calling methods will not have access to a remote object name*/
        if (!remoteConf) this.remoteIndexName = null;

        if (remoteConf) {
            try {
                bindRemoteObjectToRegistry(this.remoteIndexName, new RemoteIndex(this));
            } catch (SDXException sdxE) {
                throw new ConfigurationException(sdxE.getMessage(), sdxE);
            } catch (RemoteException e) {
                throw new ConfigurationException(e.getMessage(), e);
            }
        }

        //configuring the maxFieldLength for indexation, default is 10000 based on lucene defaults
        this.maxFieldLength = configuration.getAttributeAsInteger(ATTRIBUTE_MAX_FIELD_LENGTH, DEFAULT_MAX_FIELD_LENGTH);

    }

    private void configureQueryParser(Configuration configuration) throws ConfigurationException {
        //configuring the queryparser
        Configuration qpConf = configuration.getChild(ELEMENT_NAME_QUERYPARSER, false);
        String qpClassName = DEFAULT_QUERYPARSER;
        if (qpConf != null)
            qpClassName = qpConf.getAttribute(ATTRIBUTE_CLASS, DEFAULT_QUERYPARSER);
        //TODO : add better checking here -pb
        try {
            this.queryParserClass = Class.forName(qpClassName);
        } catch (ClassNotFoundException e) {
            SDXException sdxE = new SDXException(super.getLog(), SDXExceptionCode.ERROR_CONFIGURE_QUERY_PARSER, null, e);
            throw new ConfigurationException(sdxE.getMessage(), sdxE);
        }

    }


    private void bindRemoteObjectToRegistry(String name, Remote obj) throws SDXException {

        try {
            Naming.rebind(name, obj);
        } catch (IOException e) {
            String[] args = new String[1];
            args[0] = name;
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_BIND_REMOTE_INDEX, args, e);
        }

    }

    /**
     * Initializes the Lucene database.
     *
     * <p>It the index exists, nothing is done here. If it is doesn't exist, it will be created.</p>
     * @throws SDXException
     */
    public void init() throws SDXException {
    	if ( !isSubIndex() ) {
    		// current index is not a subindex
    		super.init(false);
    		writeCreationTimestampFile(); // ensuring we have a creation timestamp file
    		indexModified(); // ensuring we have a last modification timestamp file for this index
    	}
    	else {
    		// current index is a subindex
    		super.init(false);
    	}

        // ensuring we have a metaAnalyzer for fields
        if (this.metaAnalyzer == null)
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_INDEX_INIT, null, null);
    }

    /** Creates the file that represents the creation timestamp of this Lucene
     * index if relevant
     */
    protected void writeCreationTimestampFile() {
        try {
            File createFile = new File( super.fsdFile.getParentFile(),
            							Index.CREATION_TIMESTAMP_FILENAME );
            if ( !createFile.exists() ) {
            	createFile.createNewFile(); // the file does not exist: creates it
            }
            this.creationTimestamp = new java.util.Date(createFile.lastModified());
        } catch (IOException e) {
            LoggingUtils.logError(super.getLog(), e.getMessage(), e);
        }

    }

    /**
     * @throws SDXException
     */
    public void indexModified()
    	throws SDXException
    {
        writeLastModificationTimestampFile();
        //setDocCount(search(new TermQuery(new Term(LuceneDataStore.ALL_FIELD, LuceneDataStore.ALL_VALUE))).length());
    }
    /**
     * @deprecated Use {@link #indexModified()} instead.
     */
    public void indexModified(boolean createLastModficationTimestampFile)
    	throws SDXException
    {
    	indexModified();
    }

    protected void setDocCount(int i){
        _docCount = i;
    }

    public int docCount(){
       return _docCount;
    }

    /** Updates the date of the last modification timestamp of this Lucene index
     *
     * <p>Updates the date of the last modification timestamp of this Lucene
     * index. If the file does not exist, creates this file.</p>
     */
    public void writeLastModificationTimestampFile() {
    	try {
    		java.util.Date now = new java.util.Date();
    		lastModFile = new File( super.fsdFile.getParentFile(),
    										Index.LAST_MODIFICATION_DATE_FILENAME );
    		if (!lastModFile.exists()) {
    			lastModFile.createNewFile(); // the file does not exist: creates it
    		}
    		this.lastModificationTimestamp = now.getTime();
    		lastModFile.setLastModified( this.lastModificationTimestamp );
    	} catch (IOException e) {
    		LoggingUtils.logError(super.getLog(), e.getMessage(), e);
    	}
    }

    /**
     * @deprecated Use {@link #writeCreationTimestampFile()} instead.
     */
    public void writeLastModificationTimestampFile(boolean create) {
    	try {
    		java.util.Date now = new java.util.Date();
    		lastModFile = new File( super.fsdFile.getParentFile(),
    										Index.LAST_MODIFICATION_DATE_FILENAME );
    		if (!lastModFile.exists()) {
    			lastModFile.createNewFile();
    		}
    		this.lastModificationTimestamp = now.getTime();
    		lastModFile.setLastModified( this.lastModificationTimestamp );
    	} catch (IOException e) {
    		LoggingUtils.logError(super.getLog(), e.getMessage(), e);
    	}
    }


    /** Returns the type of a field.
     *
     * @param name  The name of the field for which the type is desired.
     * @return      The int field code (see Field doc)
     */
    public int getFieldType(String name) {
        return fields.getFieldType(name);
    }

    /** Returns the locale for a field.
     *
     * @param name The name of the field for which the Locale is desired.
     */
    public Locale getLocale(String name) {
        return fields.getLocale(name);
    }

    /**
     * Returns the default field for this index.
     */
    public Field getDefaultField() {
        return fields.getDefaultField();
    }


    /** Returns a field given a name.
     *
     * @param name The name of the field for which the Field is desired.
     */
    public Field getField(String name) {
        return fields.getField(name);
    }

    /**Gets the MetaAnalyzer*/
    public MetaAnalyzer getMetaAnalyzer() {
        return metaAnalyzer;

    }

    /**Sets the MetaAnalyzer
     *
     * @param mAnalyzer A MetaAnalyzer object containing a FieldList object for this index.
     * @throws SDXException
     */
    public void setMetaAnalyzer(MetaAnalyzer mAnalyzer) throws SDXException {
        //setting the metaAnalyzer
        this.metaAnalyzer = mAnalyzer;
        //assigning the class field
        this.fields = this.metaAnalyzer.getFieldList();
        super.analyzer = this.metaAnalyzer;

    }

    /**
     * Stores a Lucene document within the database.
     *
     * @param   ldoc     The Lucene document to store.
     * @param   batchIndex     Indicates wheter a tempBatch index is taking place or not. Useful for efficiency of index optimizations
     */
    public synchronized void writeDocument(org.apache.lucene.document.Document ldoc, boolean batchIndex) throws SDXException {
    	writeDocument(ldoc, batchIndex, true);
    }

    /**
     * Stores a Lucene document within the database.
     *
     * @param   ldoc     The Lucene document to store.
     * @param   batchIndex     Indicates wheter a tempBatch index is taking place or not. Useful for efficiency of index optimizations
     * @param	autoOptimize	Is the DocumentBase autoOptimize ?
     * @throws SDXException
     */
    public synchronized void writeDocument(org.apache.lucene.document.Document ldoc, boolean batchIndex, boolean autoOptimize) throws SDXException {
        IndexWriter w = null;
        try {

        	//if the DocumentBase is not autoOptimize, or we have a single document, then indexes directly in the current index
        	if( !autoOptimize || !batchIndex ){
        		this.write(ldoc);
        	}
        	//the DocumentBase is autoOptimize, indexes in a RAMDirectory to increase performance
        	else{
        		//if we have batchIndex and don't already have one, we create the temporary directory for the tempBatch
        		if (  batchIndex && this.tempBatch == null ) {
                    //initializing the directory
                    this.tempBatch = new RAMDirectory();
                    IndexWriter initTempBatch = new IndexWriter(tempBatch, getMetaAnalyzer(), true);
                    initTempBatch.setUseCompoundFile(this.useCompoundFiles);
                    initTempBatch.close();
                }
        		//if we still have a tempBatch  we add to it
                if (this.tempBatch != null) {
                    //getting a writer for the document tempBatch directory
                    w = this.getWriter(this.tempBatch);
                    if (w != null) {
                        //adding the document
                        w.addDocument(ldoc);
                        w.close();
                        w = null;
                    }
                }
        	}

        } catch (IOException e) {
            String[] args = new String[2];
            args[0] = super.getIndexPath();
            args[1] = e.getMessage();
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_WRITE, args, e);
        } finally {
            try {
                if (w != null) w.close();
            } catch (IOException e) {
                String[] args = new String[2];
                args[0] = super.getIndexPath();
                args[1] = e.getMessage();
                throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_WRITE, args, e);
            }
        }

    }

    /** Recycles the Lucene searcher for this index
     *
     * @throws SDXException
     */
    public synchronized void recycleLuceneIndexSearcher()
	throws SDXException {
    	recycleSearcher();
    }

    /** Recycles the Lucene reader for this index
     *
     * @throws SDXException
     */
    public synchronized void recycleIndexReader() throws SDXException{
    	IndexWriter w;
    	try {
    		w = getWriter(fsd);
    		if(w==null) w = getWriter();
    		if(w==null) w = new IndexWriter(fsd, analyzer, false);
    		w.close();
    	} catch (IOException e) {
    		String[] args = new String[2];
    		args[0] = super.getIndexPath();
    		args[1] = e.getMessage();
    		throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_WRITE, args, e);
    	}
    }


    /**
     * Deletes a document from the index.
     *
     * @param	docId	The document's id.
     */
    public synchronized void deleteDocument(String docId) throws SDXException {
        super.delete(docId);
    }

    public QueryParser getQueryParser() throws IOException, SDXException {
        try {
            if (this.queryParserClass == null)
                return null;
            else
                return (QueryParser) queryParserClass.newInstance();
        } catch (InstantiationException e) {
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_GET_QUERY_PARSER, null, e);
        } catch (IllegalAccessException e) {
            throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_GET_QUERY_PARSER, null, e);
        }
    }

    public String getRemoteIndexName() {
        return remoteIndexName;
    }

    /**Free's the resources associated with this index
     * USE WITH CARE!
     *
     * @throws IOException  Lucene IOExceptions
     */
    public void freeResources() throws IOException {
        if (this.tempBatch != null) {
            this.tempBatch.close();
            this.tempBatch = null;
        }
        super.freeResources();
    }

    /**Sets the indexing parameters
     *
     *@param params The relevant parameters for this lucene index.
     */
    public void setParameters(LuceneIndexParameters params) {
        //TODO: should we reset these after indexation or should we leave them set until the server is restarted?
        this.maxMergeDocs = params.getMaxMergeDocs();
        this.mergeFactor = params.getMergeFactor();
    }

    /** Gets a writer for the Lucene index
     *
     */
    protected synchronized IndexWriter getWriter(Directory directory)
    	throws IOException {
        return getWriter(directory, false);
    }
    protected synchronized IndexWriter getWriter(Directory directory, boolean create)
    	throws IOException {
    	IndexWriter w = super.getWriter(directory, create);
      //setting parameters to the index writer if they differ from the defaults
      if (this.maxFieldLength != DEFAULT_MAX_FIELD_LENGTH)
      	// MAJ Lucene 2.1.0
          //w.maxFieldLength = this.maxFieldLength;
      	w.setMaxFieldLength( this.maxFieldLength );
      if (this.maxMergeDocs != DEFAULT_MAX_MERGE_DOCS)
      	// MAJ Lucene 2.1.0
          //w.maxMergeDocs = this.maxMergeDocs;
      	w.setMaxMergeDocs(this.maxMergeDocs);
      if (this.mergeFactor != DEFAULT_MERGE_FACTOR)
      	// MAJ Lucene 2.1.0
          //w.mergeFactor = this.mergeFactor;
      	w.setMergeFactor(this.mergeFactor);
      return w;
    }

    /** Gets a Lucene searcher
     *
     * <p>Ensures we have a correct Lucene searcher for this index. For that, we
     * ensure that this index has not been modified since the last initialization
     * of the current searcher. If so, we have to recycle the searcher.
     * </p>
     *
     * @return Searchable	A Lucene searcher.
     */
    public synchronized Searchable getSearcher() {
    	if ( hasBeenModified() ) {
    		this.recycleSearcherReader();
    	}
      return super.getSearcher();
    }
    public synchronized Searchable getSearcher(boolean automaticRecycle) {
    	if ( !automaticRecycle ) {
    		return super.getSearcher();
    	}
    	if ( hasBeenModified() ) {
    		this.recycleSearcherReader();
    	}
      return super.getSearcher();
    }

    /** Gets a Lucene reader
     *
     * <p>Ensures we have a correct Lucene reader for this index. For that, we
     * ensure that this index has not been modified since the last initialization
     * of the current reader. If so, we have to recycle the reader.
     * </p>
     *
     * @return IndexReader	A Lucene reader
     */
    public synchronized IndexReader getReader() throws SDXException {
    	if ( hasBeenModified() ) {
    		this.recycleSearcherReader();
    	}
      return super.getReader();
    }
    public synchronized IndexReader getReader(boolean automaticRecycle) throws SDXException {
    	if ( !automaticRecycle ) {
    		return super.getReader();
    	}
  		if ( hasBeenModified() ) {
				this.recycleSearcherReader();
  		}
  		return super.getReader();
    }

    /**Recycles Lucene Searcher and Reader*/
    public synchronized void recycleSearcherReader() {
    	try {
    		LoggingUtils.logDebug(this._logger, "Recycling searcher and reader for application "+this.appId+" document base "+this.dbId+" Lucene index "+this._id, null);
    		this.recycleSearcher();
    		this.recycleIndexReader();
    		// mise a jour du timestamp pour que la prochaine fois, on n'est pas besoin de recycler comme on vient de le faire
    		lastModificationTimestamp = lastModFile.lastModified();
    	} catch (SDXException e) {
    		// TODO (MP) : meilleure message d'erreur
    		new SDXException("Error while recycling searcher and reader for application "+this.appId+" document base "+this.dbId+" Lucene index "+this._id, e);
    	}
    }

    /** Gets the value of a field.
     *
     * @param doc		The Lucene document
     * @param fieldName	The name of the field
     * @return String	The value of the field as a String
     * @throws SDXException
     */
    public String getFieldValue(IndexableDocument doc, String fieldName) throws SDXException {
        Utilities.checkDocument(super.getLog(), doc);
        if (!Utilities.checkString(fieldName)) return null;
        Hits h = search(new TermQuery(new Term(ID_FIELD, doc.getId())));
        // The return it if it exists
        if (h.length() == 1) {
            try {
                org.apache.lucene.document.Field field = h.doc(0).getField(fieldName);
                if (field != null)
                    return field.stringValue();
                else
                    return null;
            } catch (IOException e) {
                String[] args = new String[2];
                args[0] = fsd.toString();
                args[1] = e.getMessage();
                throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_RETRIEVE_DOCUMENT, args, e);
            }
        } else
            return null;

    }

    /** Returns the creation date of this Lucene index.
     *
     * @return Date
     */
    public Date getCreationDate() {
        return creationTimestamp;
    }

    /** Returns the last modification date
     * <p> Returns the the last modification timestamp of this Lucene index as a
     *  Date.</p>
     *
     * @return Date
     */
    public Date getLastModificationDate() {
    	return new Date( lastModFile.lastModified() );
        // return lastModificationTimestamp;
    }

    /**
     * Returns the last modification timestamp
     * @return long
     */
    public long getLastModificationTimestamp(){
    	return this.lastModificationTimestamp;
    }

    public void toSAX(ContentHandler hdl) throws SAXException {
        String sdxNsUri = Framework.SDXNamespaceURI;
        String sdxNsPrefix = Framework.SDXNamespacePrefix;
        String localName = Node.Name.SEARCH_LOCATION;
        String qName = sdxNsPrefix + ":" + localName;
        AttributesImpl atts = new AttributesImpl();
        //TODO : use constant names-pb
        atts.addAttribute("", Node.Name.SEARCH_ENGINE, Node.Name.SEARCH_ENGINE, Node.Type.CDATA, "lucene");
        if (Utilities.checkString(this.appId)) atts.addAttribute("", Node.Name.APPID, Node.Name.APPID, Node.Type.CDATA, this.appId);
        if (Utilities.checkString(this.dbId)) atts.addAttribute("", Node.Name.DB_ID, Node.Name.DB_ID, Node.Type.CDATA, this.dbId);
        //TODO : add other infos ? Count, directory... -pb
        atts.addAttribute("", "creationDate", "creationDate", Node.Type.CDATA, fr.gouv.culture.sdx.utils.Date.formatUtcISO8601Date(getCreationDate()));
        atts.addAttribute("", "lastModificationDate", "lastModificationDate", Node.Type.CDATA, fr.gouv.culture.sdx.utils.Date.formatUtcISO8601Date(getLastModificationDate()));
        //Let's build the index representation
        hdl.startElement(sdxNsUri, localName, qName, atts);
        //All over
        hdl.endElement(sdxNsUri, localName, qName);
    }

    protected String getClassNameSuffix() {
        return Index.CLASS_NAME_SUFFIX;
    }

    /** Index file optimization
     * @throws SDXException
	 */
    public synchronized void optimize() throws SDXException {
    	IndexWriter w = null;
    	try {
    		w = new IndexWriter(fsdFile, getMetaAnalyzer(), false);
    		w.optimize();
    	} catch (IOException e) {
    		String[] args = new String[2];
    		args[0] = super.getIndexPath();
    		args[1] = e.getMessage();
    		throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_WRITE, args, e);
    	} finally {
    		if( w!=null ) {
    			try {
    				w.close();
    			} catch (IOException ioe) {
    				String[] args = new String[3];
        		args[0] = super.getIndexPath();
        		args[1] = ioe.getMessage();
        		args[2] = "Unable to close the IndexWriter.";
    				throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_WRITE, args, ioe);
    			} finally {
    				w = null;
    			}
    		}
    	}

    }
    
    
    /*
      Solution de Michael MacCandless :
      It's best to let IndexWriter manage the deletion of files (for exactly
			this reason).
			
			It turns out, it's perfectly fine to open an IndexWriter with
			"create=true" even when IndexReaders are reading that same index.
			Those open IndexReaders continue to search their point-in-time
			snapshot, and then when they reopen they'll switch to the "reset"
			index.
			
			So, assuming you have enough free disk space, you could leave your
			searcher open, create an IndexWriter with create=true, call
			addIndexesNoOptimize passing in your RAMDir (or, skip RAMDir and
			simply add your docs directly to this writer), then commit or close
			the writer and reopen the searcher.
			
			Because Lucene is transactional, and assuming you use autoCommit=false
			when opening the writer, you can make all of your index updates and
			the searcher, even if reopened, will see none of these changes, until
			you've called commit/close from the writer. 
     */
    /**
     * Merges any batch in memory
     */
    public synchronized void mergeCurrentBatch() throws SDXException {
    	mergeCurrentBatch(false);
    }
    /**
     * Merges any batch in memory and optimize it if necessary
     */
    public synchronized void mergeCurrentBatch(boolean optimize) throws SDXException {

    	if(!fsdFile.exists() || fsd == null) return;//check if the index directory exists

    	if(tempBatch == null) return;//nothing to merge
      if(!fsdFile.exists() || fsd == null) return;//check if the index directory exists

      // Fusion des index main et temp dans un troisieme index, temporaire
      Directory[] dirs = new Directory[1];
      IndexWriter w = null;
      
     	try {
          
          //adding the batch
          dirs[0] = this.tempBatch;
          //getting the writer for the new temp dir
          w = getWriter(this.fsd);
        	w.addIndexesNoOptimize(dirs);

          //freeing resources so we can rename the directories
        	this.freeResources();

          //and make it functionnal again
          init();
          
          if (optimize) {
          	w.optimize();
          }

     	} catch (IOException e) {
     		String[] args = new String[2];
     		args[0] = super.getIndexPath();
     		args[1] = e.getMessage();
     		throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_MERGE_INDECES, args, e);
     	} finally {
        //freeing resources after the merge
        if (w!=null) {
        	try {
        		w.close();
        	} catch (IOException ioe) {
        		/*FIXME (MP): Handled exception ?*/;
        	} finally {
        		w = null;
        		try {
	        		if (IndexReader.isLocked(fsd)) {
	        			IndexReader.unlock(fsd);
	        		}
        		} catch (IOException ioe) {
        			String[] args = new String[2];
           		args[0] = super.getIndexPath();
           		args[1] = ioe.getMessage();
           		throw new SDXException(super.getLog(), SDXExceptionCode.ERROR_LUCENE_UNLOCK, args, ioe);
        		}
        	}
        }
     	}

     	return;
		}

    /**
     * Tell if the index is a subIndex or not
     */
    public boolean isSubIndex()
    {
    	return _isSub;
    }

    /** Has Lucene index been modified
     *
     * <p>Returns true if this Lucene index has been modified since the last
     * modification timestamp.</p>
     *
     * @return boolean
     */
    public synchronized boolean hasBeenModified() {
    	if( lastModificationTimestamp<1 || lastModFile==null ) {
    		String[] args = {this._id,this.dbId,this.appId};
    		LoggingUtils.logWarn(this._logger, SDXExceptionCode.WARN_INVALID_LUCENE_INDEX_DIRECTORY,args);
    		return false;
    	}
    	long cmt = lastModFile.lastModified();
    	boolean ret = ( lastModificationTimestamp < cmt );
    	LoggingUtils.logDebug(this._logger,
    			"Is Lucene index modified since last modification date: "+ret+
    			"\nlastModificationTimeStamp:"+lastModificationTimestamp+
    			"\ncurrentModificationTimeStamp:"+cmt,
    			null);
    	return ret;
    }

	/**
	 * Filter for index content (all but directories)
	 */
    FilenameFilter indexContentFilter = new FilenameFilter() {
        public boolean accept(File dir, String name) {
        	return !Utilities.isNameMatchIndexFiles(name) && !name.startsWith("current");
        }
    };

	/** Save LuceneIndex files
	 * @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);

	}
	/** Restore LuceneIndex files
	 * @see fr.gouv.culture.sdx.utils.save.Saveable#restore(fr.gouv.culture.sdx.utils.save.SaveParameters)
	 */
	public void restore(SaveParameters save_config) throws SDXException{
		// TODO Auto-generated method stub

	}

}
