/*
 * $Header: /home/cvspublic/jakarta-slide/src/stores/org/apache/slide/index/lucene/IndexConfiguration.java,v 1.9 2005/03/11 09:48:52 luetzkendorf Exp $
 * $Revision: 1.9 $
 * $Date: 2005/03/11 09:48:52 $
 *
 * ====================================================================
 *
 * Copyright 1999-2004 The Apache Software Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package org.apache.slide.index.lucene;

import java.io.InputStream;
import java.io.Reader;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.SimpleAnalyzer;
import org.apache.lucene.analysis.TokenStream;

import org.apache.slide.common.PropertyName;
import org.apache.slide.content.NodeRevisionNumber;
import org.apache.slide.search.IndexException;
import org.apache.slide.util.conf.Configuration;
import org.apache.slide.util.conf.ConfigurationElement;
import org.apache.slide.util.conf.ConfigurationException;
import org.apache.slide.util.conf.Populate;
import org.xml.sax.InputSource;

/**
 * Holds all configuration infos about indexing.
 */
public class IndexConfiguration
{
     
    protected Set stringProperties = new HashSet();
    /** maps field names (properies) to analyzers. */
    protected Map textProperties = new HashMap();
    protected Set dateProperties = new HashSet();
    protected Set intProperties = new HashSet();
    protected Set supportsIsdefinedProperties = new HashSet();
    protected Set indexedProperties = new HashSet();
    protected int optimizeThreshold = 100;
    protected AnalyzerImpl analyzer = new AnalyzerImpl();
    protected String indexPath = null;
    protected boolean indexAsynchron = false;
    protected String namespaceName = null;
    protected int priority = Thread.NORM_PRIORITY;
    protected Set knownResourceTypes = new HashSet();
    
    public void addStringProperty(String namespace, String name) {
        PropertyName key = PropertyName.getPropertyName(name, namespace);
        this.stringProperties.add(key);
        this.indexedProperties.add(key);
    }
    public boolean isStringProperty(PropertyName propertyName) {
        return this.stringProperties.contains(propertyName);
    }
    public boolean isStringProperty(String namespace, String name) {
        PropertyName key = PropertyName.getPropertyName(name, namespace);
        return this.stringProperties.contains(key);
    }
    
    public void addDateProperty(String namespace, String name) {
        PropertyName key = PropertyName.getPropertyName(name, namespace);
        this.dateProperties.add(key);
        this.indexedProperties.add(key);
    }
    public boolean isDateProperty(PropertyName propertyName) {
        return this.dateProperties.contains(propertyName);
    }
    public boolean isDateProperty(String namespace, String name) {
        PropertyName key = PropertyName.getPropertyName(name, namespace);
        return this.dateProperties.contains(key);
    }
    
    
    public void addIntProperty(String namespace, String name) {
        PropertyName key = PropertyName.getPropertyName(name, namespace);
        this.intProperties.add(key);
        this.indexedProperties.add(key);
    }
    public boolean isIntProperty(PropertyName propertyName) {
        return this.intProperties.contains(propertyName);
    }
    public boolean isIntProperty(String namespace, String name) {
        PropertyName key = PropertyName.getPropertyName(name, namespace);
        return this.intProperties.contains(key);
    }
    
    public void addSupportsIsdefinedProperty(String namespace, String name) {
        PropertyName key = PropertyName.getPropertyName(name, namespace);
        this.supportsIsdefinedProperties.add(key);
        this.indexedProperties.add(key);
    }
    
    public boolean supportsIsDefined(PropertyName propertyName) {
        return this.supportsIsdefinedProperties.contains(propertyName);
    }
    public boolean supportsIsDefined(String namespace, String name) {
        PropertyName key = PropertyName.getPropertyName(name, namespace);
        return this.supportsIsdefinedProperties.contains(key);
    }

    public void addTextProperty(String namespace, String name, Analyzer analyzer) {
        PropertyName key = PropertyName.getPropertyName(name, namespace);
        this.textProperties.put(key, analyzer);
        this.indexedProperties.add(key);
    }
    public boolean isTextProperty(PropertyName propertyName) {
        return this.textProperties.containsKey(propertyName);
    }
    public boolean isTextProperty(String namespace, String name) {
        PropertyName key = PropertyName.getPropertyName(name, namespace);
        return this.textProperties.containsKey(key);
    }

    public boolean isIndexedProperty(PropertyName name) {
        return this.indexedProperties.contains(name);
    }
    public boolean isIndexedProperty(String namespace, String name) {
        PropertyName key = PropertyName.getPropertyName(name, namespace);
        return this.indexedProperties.contains(key);
    }
    
    /**
     * Tests whether ops <code>eq</code>, <code>lt</code>, <code>ge</code>,etc
     * @param namespace
     * @param name
     * @return
     */
    public boolean isComparableProperty(String namespace, String name) {
        PropertyName key = PropertyName.getPropertyName(name, namespace);
        return this.stringProperties.contains(key) ||
               this.intProperties.contains(key) ||
               this.dateProperties.contains(key);
    }
    public boolean isComparableProperty(PropertyName name) {
        return this.stringProperties.contains(name) ||
               this.intProperties.contains(name) ||
               this.dateProperties.contains(name);
    }
    
    Iterator knownResourceTypes() {
        return knownResourceTypes.iterator();
    }
    
    public int getOptimizeThreshold() {
        return this.optimizeThreshold;
    }
    public void setOptimizeThreshold(int value) {
        this.optimizeThreshold = value;
    }
    public String getIndexPath()
    {
        return indexPath;
    }
    public void setIndexPath(String indexPath)
    {
        this.indexPath = indexPath;
    }
    public boolean isIndexAsynchron()
    {
        return indexAsynchron;
    }
    public void setIndexAsynchron(boolean indexAsynchron)
    {
        this.indexAsynchron = indexAsynchron;
    }
    public int getPriority() {
        return this.priority;
    }
    public void setPriority(int priority) {
        this.priority = priority;
    }
    public String getNamespaceName() {
        return this.namespaceName;
    }
    public void setNamespaceName(String name) {
        this.namespaceName = name;
    }

    public Analyzer getAnalyzer() {
        return this.analyzer;
    }
    
    public void setContentAnalyzer(Analyzer analyzer) {
        if (analyzer == null) throw new NullPointerException();
        this.analyzer.contentAnalyzer = analyzer;
    }
        
    void initDefaultConfiguration() throws IndexException {
        loadConfigurationFromResource(
                "org/apache/slide/index/lucene/defaultConfig.xml");
    }
    
    private void loadConfigurationFromResource(String resourceName) throws IndexException {
        InputStream is = this.getClass().getClassLoader().getResourceAsStream(
            resourceName);
        
        if (is != null) {
            try {
                SAXParserFactory factory = SAXParserFactory.newInstance();
                factory.setNamespaceAware(false);
                factory.setValidating(false);
                SAXParser parser = factory.newSAXParser();
                
                Populate pop = new Populate();
                Configuration conf = new ConfigurationElement(
                        pop.load(new InputSource(is), parser.getXMLReader()));
                
                try {
                    readPropertyConfiguration(conf.getConfiguration("indexed-properties"));
                } catch(ConfigurationException e) {
                    // ok none
                }
                
                try {
                    readResourceTypeConfiguration(conf.getConfiguration("resource-types"));
                } catch(ConfigurationException e) {
                    // ok none
                }
                
            } catch (Exception e) {
                throw new IndexException
                    ("error while loading configuration from " + resourceName, e);
            } 
        } else {
            throw new IndexException("Can't find index configuration at: " + resourceName);
        }
    }
    
    void readResourceTypeConfiguration(Configuration conf) {
        for(Enumeration e = conf.getConfigurations("resource-type");e.hasMoreElements();) {
            Configuration resourceType = (Configuration)e.nextElement();
            
            String n; //, ns;
            try {
                n = resourceType.getAttribute("name");
                //ns = resourceType.getAttribute("namespace");
            } catch (ConfigurationException ex) {
                continue;
            }
            
            this.knownResourceTypes.add(n); // TODO do we need the namespace
        }
    }
    
    void readPropertyConfiguration(Configuration conf)
        throws IndexException
    {
        for(Enumeration e = conf.getConfigurations("property");e.hasMoreElements();) {
            Configuration property = (Configuration)e.nextElement();

            String n, ns;
            try {
                n = property.getAttribute("name");
                ns = property.getAttribute("namespace");
            } catch (ConfigurationException ex) {
                continue;
            }
            
            Configuration child;
            try {
                child = property.getConfiguration("string");
                addStringProperty(ns, n);
                addSupportsIsdefinedProperty(ns, n);
            } 
            catch (ConfigurationException ex) {}

            try {
                child = property.getConfiguration("integer"); 
                addIntProperty(ns, n);
                addSupportsIsdefinedProperty(ns, n);
            } 
            catch (ConfigurationException ex) {}

            try {
                child = property.getConfiguration("date");
                addDateProperty(ns, n);
                addSupportsIsdefinedProperty(ns, n);
            }
            catch (ConfigurationException ex) {}
            
            try {
                child = property.getConfiguration("text");
                String clsName;
                try {
                    clsName = child.getAttribute("analyzer");
                } catch (ConfigurationException ex) {
                    clsName = "org.apache.lucene.analysis.SimpleAnalyzer";
                }
                
                Analyzer analyzer;
                try {
                    Class cls = Class.forName(clsName);
                    analyzer = (Analyzer)cls.newInstance();
                } catch (ClassNotFoundException ex) {
                    throw new IndexException("Analyzer class not found (" + ns + ", " + n + ")", ex);
                } catch (InstantiationException ex) {
                    throw new IndexException("Can't instanciate analyzer (" + ns + ", " + n + ")", ex);
                } catch (IllegalAccessException ex) {
                    throw new IndexException("Can't instanciate analyzer (" + ns + ", " + n + ")", ex);
                } catch (ClassCastException ex) {
                    throw new IndexException("Analyzer does not extend Analyzer (" + ns + ", " + n + ")", ex);
                }
                addTextProperty(ns, n, analyzer);
            }
            catch (ConfigurationException ex) {}
            
            try {
                child = property.getConfiguration("is-defined");
                addSupportsIsdefinedProperty(ns, n);
            }
            catch (ConfigurationException ex) {}
        }
    }
    
    // ------ data type helper -------------------------------------------------
    
    /**
     * Generates a field name for "normal fields".
     */
    public String generateFieldName(String namespaceUri, String name) {
        return namespaceUri + name;
    }
        
    public String generateKey(String uri, NodeRevisionNumber number) {
        return uri + "#" + number;
    }
    
    public String dateToIndexString(Date date) {
        synchronized (Index.DATE_INDEX_FORMAT) {
            return Index.DATE_INDEX_FORMAT.format(date);
        }
    }
    
    public String intToIndexString(long value) {
        synchronized (Index.INT_INDEX_FORMAT) {
            if (value >= 0) {
                return Index.INT_INDEX_FORMAT.format(value);
            } else {
                return Index.INT_INDEX_FORMAT.format(-(Long.MAX_VALUE + value));
            }
        }
    }
    
    
    private static final SimpleDateFormat formats[] = {
            new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US),
            new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US),
            new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US),
            new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US),
            new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
        };
    /**
     * Helper that converts property values to dates.
     * @param value 
     * @return a date of <code>null</code> if value can't convert
     */
    public static Date getDateValue(Object value) {
        if (value instanceof Date) {
            return (Date)value;
        } else {
            String valstr = value.toString();
            // Parsing the HTTP Date
            for (int i = 0; i < formats.length; i++) {
                try {
                    synchronized (formats[i]) {
                        return formats[i].parse(valstr);
                    }
                } catch (ParseException e) {
                }
            }
            return null;
        }
    }
    
    public String predecessor(String field, String value) {
        StringBuffer b = new StringBuffer(value);
        b.setCharAt(b.length()-1, (char)(b.charAt(b.length()-1)-1));
        return b.toString();
    }
    
    public String successor(String field, String value) {
        StringBuffer b = new StringBuffer(value);
        b.setCharAt(b.length()-1, (char)(b.charAt(b.length()-1)+1));
        return b.toString();
    }
    class AnalyzerImpl extends Analyzer {
        Analyzer defaultAnalyzer = new SimpleAnalyzer();
        Analyzer contentAnalyzer = null;
        
        public TokenStream tokenStream(String fieldName, Reader reader)
        {
            if (fieldName.equals(Index.CONTENT_FIELD_NAME)) {
                return contentAnalyzer.tokenStream(fieldName, reader);
            }
            
            Analyzer analyzer = (Analyzer)textProperties.get(fieldName);
            if (analyzer != null) {
                return analyzer.tokenStream(fieldName, reader);
            } else {
                // TODO should not happen, throw an exception?
                return this.defaultAnalyzer.tokenStream(fieldName, reader);
            }
        }
    }
    
    
}
