/* ====================================================================
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 2000-2003 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Apache" and "Apache Software Foundation" and
 *     "Apache Jetspeed" must not be used to endorse or promote products
 *    derived from this software without prior written permission. For
 *    written permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache" or
 *    "Apache Jetspeed", nor may "Apache" appear in their name, without
 *    prior written permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */

package org.apache.jetspeed.services.search.lucene;

// Java imports
import java.io.File;
import java.io.IOException;
import java.net.URL;
import javax.servlet.ServletConfig;
import java.util.Collection;
import java.util.ArrayList;
import java.util.Iterator;

// Jetspeed imports
import org.apache.jetspeed.services.logging.JetspeedLogFactoryService;
import org.apache.jetspeed.services.logging.JetspeedLogger;
import org.apache.jetspeed.services.search.HandlerFactory;
import org.apache.jetspeed.services.search.ObjectHandler;
import org.apache.jetspeed.services.search.ParsedObject;
import org.apache.jetspeed.services.search.BaseParsedObject;
import org.apache.jetspeed.services.search.SearchResults;
import org.apache.jetspeed.services.search.SearchService;

// Turbine imports
import org.apache.turbine.services.InitializationException;
import org.apache.turbine.services.resources.ResourceService;
import org.apache.turbine.services.servlet.TurbineServlet;
import org.apache.turbine.services.TurbineBaseService;
import org.apache.turbine.services.TurbineServices;

// Lucene imports
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Searcher;

/**
 * Lucene implementation of search service.
 *
 * @author <a href="mailto:taylor@apache.org">David Sean taylor</a>
 * @author <a href="mailto:caius1440@hotmail.com">Jeremy Ford</a>
 * @author <a href="mailto:morciuch@apache.org">Mark Orciuch</a> 
 * @version $Id: LuceneSearchService.java,v 1.5 2003/07/23 19:50:23 morciuch Exp $
 */
public class LuceneSearchService extends TurbineBaseService implements SearchService
{
    /**
     * Static initialization of the logger for this class
     */    
    private static final JetspeedLogger logger = JetspeedLogFactoryService.getLogger(LuceneSearchService.class.getName());
    
    private static final String CONFIG_DIRECTORY = "directory";
    private File rootDir = null;
    private String indexRoot = null;

    /**
     * This is the early initialization method called by the
     * Turbine <code>Service</code> framework
     * @param conf The <code>ServletConfig</code>
     * @exception throws a <code>InitializationException</code> if the service
     * fails to initialize
     */
    public synchronized void init(ServletConfig conf) throws InitializationException
    {

        // already initialized
        if (getInit())
        {
            return;
        }

        initConfiguration(conf);

        // initialization done
        setInit(true);

    }

    /**
     * This is the lateinitialization method called by the
     * Turbine <code>Service</code> framework
     *
     * @exception throws a <code>InitializationException</code> if the service
     * fails to initialize
     */
    public void init() throws InitializationException
    {
        logger.info("Late init for " + SearchService.SERVICE_NAME + " called");
        while (!getInit())
        {
            //Not yet...
            try
            {
                Thread.sleep(100);
                logger.info("Waiting for init of " + SearchService.SERVICE_NAME + "...");
            }
            catch (InterruptedException ie)
            {
                logger.error("Exception", ie);
            }
        }
    }

    /**
     * This is the shutdown method called by the
     * Turbine <code>Service</code> framework
     */
    public void shutdown()
    {
    }

    /**
     * Loads the configuration parameters for this service from the
     * JetspeedResources.properties file.
     *
     * @exception throws a <code>InitializationException</code> if the service
     * fails to initialize
     */
    private void initConfiguration(ServletConfig conf) throws InitializationException
    {
        if (getInit())
        {
            return;
        }

        // get configuration parameters from Jetspeed Resources
        ResourceService serviceConf = ((TurbineServices) TurbineServices.getInstance())
                                      .getResources(SearchService.SERVICE_NAME);

        // Get config properties
        indexRoot = serviceConf.getString(CONFIG_DIRECTORY);
        //
        // The following section opens or creates the search index
        //
        //
        rootDir = new File(indexRoot);

        //If the rootDir does not exist, treat it as context relative
        if (!rootDir.exists())
        {
            if (indexRoot != null)
            {
                String rootDirPath = TurbineServlet.getRealPath("") + indexRoot;
                rootDir = new File(rootDirPath);
                if (!rootDir.exists())
                {
                    rootDir.mkdir();
                    logger.info("Created index directory '" + rootDir.getPath() + "'");
                }
            }
        }

        try
        {
            Searcher searcher = null;
            searcher = new IndexSearcher(rootDir.getPath());
            searcher.close();
        }
        catch (Exception e)
        {
            try
            {
                IndexWriter indexWriter = new IndexWriter(rootDir, new StandardAnalyzer(), true);
                indexWriter.close();
                indexWriter = null;
                logger.info("Created Lucene Index in " + rootDir.getPath());
            }
            catch (Exception e1)
            {
                logger.error(this.getClass().getName() + ".initConfiguration - Getting or creating IndexSearcher", e);
                throw new InitializationException("Getting or creating Index Searcher");
            }
        }

        //Mark that we are done
        setInit(true);
    }

    /**
     * Search
     * 
     * @task Parse content into title and description fields
     * @param searchString
     *               is the what is being searched for
     * @return Hits, if no hits then null.
     */
    public SearchResults search(String searchString)
    {
        Searcher searcher = null;
        Hits hits = null;
        try
        {
            searcher = new IndexSearcher(rootDir.getPath());
            Analyzer analyzer = new StandardAnalyzer();
            Query query = QueryParser.parse(searchString, ParsedObject.FIELDNAME_CONTENT, analyzer);
            hits = searcher.search(query);
        }
        catch (Exception e)
        {
            logger.error("Exception", e);
        }

        // Copy hits to the result list
        int hitCount = hits.length();
        Document doc = null;
        SearchResults results = new SearchResults(hitCount);
        for (int counter = 0; counter < hitCount; counter++)
        {            
            ParsedObject result = new BaseParsedObject();
            try
            {
                doc = hits.doc(counter);
                result.setScore(hits.score(counter));
                result.setType(doc.getField(ParsedObject.FIELDNAME_TYPE).stringValue());
                result.setKey(doc.getField(ParsedObject.FIELDNAME_KEY).stringValue());
                result.setDescription(doc.getField(ParsedObject.FIELDNAME_DESCRIPTION).stringValue());
                result.setTitle(doc.getField(ParsedObject.FIELDNAME_TITLE).stringValue());
                Field url = doc.getField(ParsedObject.FIELDNAME_URL);
                if (url != null)
                {
                    result.setURL(new URL(url.stringValue()));
                }                
                results.add(counter, result);
            }
            catch (Exception ioe)
            {
                logger.error("Exception", ioe);
            }
            result = null;
        }

        if (searcher != null)
        {
            try
            {
                searcher.close();
            }
            catch (IOException ioe)
            {
                logger.error("Closing Searcher", ioe);
            }
        }
        return results;
    }

    /**
     * 
     * @return 
     */
    public String[] getSearchSets()
    {
        return null;
    }

    /**
     * 
     * @see org.apache.jetspeed.services.search.SearchService#add(java.lang.Object)
     * @param o
     * @return 
     */
    public boolean add(Object o)
    {
        Collection c = new ArrayList();
        c.add(o);

        return add(c);
    }

    /**
     * 
     * @see org.apache.jetspeed.services.search.SearchService#add(java.lang.Collection)
     * @param c
     * @return 
     */
    public boolean add(Collection c)
    {
        boolean result = false;

        try 
        {
            IndexWriter indexWriter = new IndexWriter(rootDir, new StandardAnalyzer(), false);

            Iterator it = c.iterator();
            while (it.hasNext()) 
            {
                Object o = it.next();
                // Look up appropriate handler
                ObjectHandler handler = HandlerFactory.getHandler(o);

                // Parse the object
                ParsedObject parsedObject = handler.parseObject(o);

                // Create document
                Document doc = new Document();

                // Populate document from the parsed object
                if (parsedObject.getKey() != null)
                {
                    doc.add(Field.Keyword(ParsedObject.FIELDNAME_KEY, parsedObject.getKey()));
                }
                if (parsedObject.getType() != null)
                {
                    doc.add(Field.Text(ParsedObject.FIELDNAME_TYPE, parsedObject.getType()));
                }
                if (parsedObject.getTitle() != null)
                {
                    doc.add(Field.Text(ParsedObject.FIELDNAME_TITLE, parsedObject.getTitle()));
                }
                if (parsedObject.getDescription() != null)
                {
                    doc.add(Field.Text(ParsedObject.FIELDNAME_DESCRIPTION, parsedObject.getDescription()));
                }
                if (parsedObject.getContent() != null)
                {
                    doc.add(Field.Text(ParsedObject.FIELDNAME_CONTENT, parsedObject.getContent()));
                }
                if (parsedObject.getLanguage() != null)
                {
                    doc.add(Field.Text(ParsedObject.FIELDNAME_LANGUAGE, parsedObject.getLanguage()));   
                }
                if (parsedObject.getURL() != null)
                {
                    doc.add(Field.Text(ParsedObject.FIELDNAME_URL, parsedObject.getURL().toString()));
                }

                // TODO: How to handle keywords and fields
                /*String[] keywords = parsedObject.getKeywords();
                if (keywords != null)
                {
                    for (int i = 0; i < keywords.length; i++)
                    {
                        doc.add(Field.Keyword();
                    }
                } */

                // Add the document to search index
                indexWriter.addDocument(doc);
                logger.debug("Index Document Count = " + indexWriter.docCount());
                logger.info("Added '" + parsedObject.getTitle() + "' to index");
                result = true;
            }

            indexWriter.optimize();
            indexWriter.close();

        }
        catch (Exception e)
        {
            logger.error("Exception", e);
            result = false;
        }

        return result;
    }

    /**
     * 
     * @see org.apache.jetspeed.services.search.SearchService#remove(java.lang.Object)
     * @param o
     * @return 
     */
    public boolean remove(Object o)
    {
        Collection c = new ArrayList();
        c.add(o);

        return remove(c);
    }

    /**
     * 
     * @see org.apache.jetspeed.services.search.SearchService#remove(java.lang.Collection)
     * @param c
     * @return 
     */
    public boolean remove(Collection c)
    {
        boolean result = false;

        try 
        {
            IndexReader indexReader = IndexReader.open(this.rootDir);

            Iterator it = c.iterator();
            while (it.hasNext()) 
            {
                Object o = it.next();
                // Look up appropriate handler
                ObjectHandler handler = HandlerFactory.getHandler(o);

                // Parse the object
                ParsedObject parsedObject = handler.parseObject(o);

                // Create term
                Term term = null;

                if (parsedObject.getKey() != null)
                {
                    term = new Term(ParsedObject.FIELDNAME_KEY, parsedObject.getKey());
                    // Remove the document from search index
                    int rc = indexReader.delete(term);
                    logger.info("Attempted to delete '" + term.toString() + "' from index, documents deleted = " + rc);
                    //System.out.println("Attempted to delete '" + term.toString() + "' from index, documents deleted = " + rc);
                    result = rc > 0;
                }
            }

            indexReader.close();

            IndexWriter indexWriter = new IndexWriter(rootDir, new StandardAnalyzer(), false);
            indexWriter.optimize();
            indexWriter.close();

        }
        catch (Exception e)
        {
            logger.error("Exception", e);
            result = false;
        }

        return result;
    }

    /**
     * 
     * @see org.apache.jetspeed.services.search.SearchService#update(java.lang.Object)
     * @param o
     * @return 
     */
    public boolean update(Object o)
    {
        Collection c = new ArrayList();
        c.add(o);

        return update(c);
    }
    /**
     * Updates an index entry. For now, it's a remove and add.
     * 
     * @param c
     * @return 
     * @see org.apache.jetspeed.services.search.SearchService#update(java.lang.Collection)
     */
    public boolean update(Collection c)
    {
        boolean result = false;

        try
        {
            // Delete entries from index
            remove(c);
            result = true;
        }
        catch (Throwable e)
        {
            logger.error("Exception",  e);
        }

        try
        {
            // Add entries to index
            add(c);
            result = true;
        }
        catch (Throwable e)
        {
            logger.error("Exception",  e);
        }

        return false;
    }

}
