/*
 * $Id: Search.java,v 1.135 2007/12/05 08:19:55 marbobo Exp $
 *
 * Copyright (c) Siemens AG 2008 
 *           (c) Universität Paderborn 2008 
 * Licensed under EUPL 1.1. 
 *
 * $Log: Search.java,v $
 * Revision 1.135  2007/12/05 08:19:55  marbobo
 * + Bugfix
 *
 * Revision 1.134  2007/11/28 09:37:38  marbobo
 * + Bugfix
 *
 * Revision 1.133  2007/11/27 12:12:55  marbobo
 * + Bugfix
 *
 * Revision 1.132  2006/12/22 10:36:17  lawern
 * startSearch wird jetzt später aufgerufen.
 *
 * Revision 1.131  2006/12/21 14:42:07  lawern
 * Debug-Ausgabe vergessen.
 *
 * Revision 1.130  2006/12/21 13:02:54  lawern
 * WhatsRelated auch noch umgestellt.
 *
 * Revision 1.129  2006/12/21 12:08:42  lawern
 * Benachrichtigung zu registrierten Suchanfragen überarbeitet.
 *
 * Revision 1.128  2006/12/18 15:50:23  lawern
 * Korowas Änderungen überarbeitet.
 *
 * Revision 1.127  2006/12/11 14:40:40  korowa
 * Ein Multithreadproblem gelöst, bei dem während der Suche der Searcher-Objekt bei Indexoptimierung gelöscht wurde, was zu Problemen führte. Dafür sind 2 Methoden geschrieben worden:
 * +wantUseSearcher()- Sperrt den Searcher-Objekt
 * +usedSearcher()- gibt den Searcher wieder frei
 * searcherInUse zeigt die Anzahl der Suchprozesse, die auf Searcher gerade zugreifen
 * Suchfunktion abgeändert, da die Hits(aus dem Suchvorgang) vor dem Freigeben des Searcher ausgewertet werden sollen
 *
 * Revision 1.126  2006/12/07 15:36:35  lawern
 * Reihenfolge geändert: Erst Searcher schließen und dann Dokument löschen.
 *
 * Revision 1.125  2006/10/29 17:24:59  marbobo
 * + Methode zur Überprüfung der Syntax für Suchanfragen
 *
 * Revision 1.124  2006/10/25 10:33:30  lawern
 * Exception abgefangen
 *
 * Revision 1.123  2006/10/09 12:53:27  lawern
 * *** empty log message ***
 *
 * Revision 1.122  2006/10/09 12:19:27  marbobo
 * + Beim erhöhen der Zugriffsanzahl wird nicht mehr saveDocument aufgerufen
 *
 * Revision 1.121  2006/10/05 20:51:02  marbobo
 * + Synchronisierung der Classifizierungsmethode
 *
 * Revision 1.120  2006/10/04 11:34:54  lawern
 * *** empty log message ***
 *
 * Revision 1.119  2006/10/04 11:31:49  lawern
 * Keine Indexoptimierung während der Optimierung der Klassifikationsparameter.
 *
 * Revision 1.118  2006/09/29 21:10:07  marbobo
 * + Sucheoptimiert
 *
 * Revision 1.117  2006/09/13 14:40:49  marbobo
 * + Änderung vom 31.082006 (wegen cast) rückgängig gemacht
 *
 * Revision 1.116  2006/09/12 14:03:55  korowa
 * Fehler im Kommentar korriegiert
 *
 * Revision 1.115  2006/08/31 19:59:09  marbobo
 * + Refactoring
 *
 * Revision 1.114  2006/08/02 19:14:44  marbobo
 * + SearchAnalyzer (=WhiteSpaceAnalyzer) statt StandardAnalyzer zum indexieren aller Felder ausser "contents" verwenden
 *
 * Revision 1.113  2006/07/21 10:33:12  lawern
 * Noch 'ne Fehlermeldung rausgenommen.
 *
 * Revision 1.112  2006/07/21 10:12:25  lawern
 * Fehlermeldung "Error by retrieving document vector for documentId ..." nur noch wenn debug=true.
 *
 * Revision 1.111  2006/07/19 15:19:37  lawern
 * Bugfix
 *
 * Revision 1.110  2006/07/19 13:42:45  marbobo
 * + Bugfix
 *
 * Revision 1.109  2006/07/18 11:42:12  lawern
 * Stoppwörter aus den Testvektoren für die Optimierung rausgeschmissen.
 *
 * Revision 1.108  2006/07/17 14:16:09  lawern
 * Umstellung der Schlagwort- und Summary-Extraktion auf TFIDF-Gewichtung.
 *
 * Revision 1.107  2006/07/17 13:04:01  lawern
 * Neuer typografiegewichteter Index und k-NN-Klassifikation aus meiner Diss.
 *
 * Revision 1.106  2006/05/27 18:34:47  marbobo
 * + Neueste Lucene-Version eingebaut
 *
 * Revision 1.105  2006/05/24 17:58:27  marbobo
 * + Toolbar des Html-Editors kann optional angezeigt oder verborgen werden
 *
 * Revision 1.104  2006/05/14 17:27:37  marbobo
 * + Indexierung des Beschreibungsfeld korrigiert
 *
 * Revision 1.103  2006/05/10 21:46:21  marbobo
 * + Html-Editor zum Bearbeiten von Beschreibungsfeldern (Dokumente und Kategorien) hinzugefügt
 *
 * Revision 1.102  2006/03/23 11:40:56  marbobo
 * + Mögliche NPE abgefangen
 *
 * Revision 1.101  2005/09/04 23:21:21  marbobo
 * +Suche nach Metadaten-Attribute (Erstellungsdatum) für Dokumente implementiert
 *
 * Revision 1.100  2005/07/05 11:59:52  lawern
 * MAX_FIELD_LENGTH jetzt bei allen IndeWritern auf 40000 gesetzt.
 *
 * Revision 1.99  2005/06/14 10:37:49  marbobo
 * + MAX_FIELD_LENGTH auf maximum gesetzt
 * + refactoring
 *
 * Revision 1.98  2005/04/25 18:05:39  marbobo
 * + Verwendung von Standardanalyser
 *
 * Revision 1.97  2005/04/11 14:57:41  jonyp
 * + Einige Exception abgefangen
 *
 * Revision 1.96  2005/04/05 22:14:42  marbobo
 * + "Suchindex neu aufbauen" korrigiert
 * + StopAnalyzer durch StandardAnalyzer ersetzt (so funktioniert Volltextsuche auch für UTF-8-Wörter)
 *
 * Revision 1.95  2005/03/24 12:57:05  lawern
 * Umstellung des TDF-Formats auf Version 1.1 in UTF-8-Kodierung.
 *
 * Revision 1.94  2005/03/14 13:42:41  marbobo
 * + Vermeidung von NPE bei der Indizierung
 *
 * Revision 1.93  2005/02/14 01:31:23  marbobo
 * + Vollständige Unicode-Indizierung und Suche
 *
 * Revision 1.92  2005/02/10 01:06:54  marbobo
 * + Suche nach Unicode-Texten
 *
 * Revision 1.91  2005/01/31 10:59:20  bigmac
 * Fix für TooManyClauses-Exception auf leonardo
 *
 * Revision 1.90  2004/11/24 09:25:59  lawern
 * Hibernate ist VKC-Geschichte!
 *
 * Revision 1.89  2004/11/10 15:02:53  koppy
 * Projektweite Überarbeitung von Error-Logs, Stacktraces,..
 *
 * Revision 1.88  2004/11/09 20:14:58  marbobo
 * + Suche nach Herausgeber
 *
 * Revision 1.87  2004/11/09 14:35:41  marbobo
 * + Umstellung von authorId auf publisherId
 * + Hinzufügen von AuthorName für Dokumente
 * + Nötwendige Änderungen betreffend AuthorName und PublisherId
 *
 * Revision 1.86  2004/10/14 18:17:28  marbobo
 * + Bugfix: Anzeigen von globalen Dokument-Attributen
 *
 * Revision 1.1  2003/04/01 13:08:49  lawern
 * initiale Version
 *
 */
package vkc.search;

import cern.colt.map.OpenIntDoubleHashMap;
import cern.colt.map.OpenIntIntHashMap;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.de.GermanStemmer;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.document.DateTools.Resolution;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermFreqVector;
import org.apache.lucene.queryParser.MultiFieldQueryParser;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.FSDirectory;

import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleTrigger;

import vkc.analyse.Analyser;
import vkc.analyse.Analyser_de;
import vkc.analyse.Analyser_es;
import vkc.analyse.Analyser_gb;
import vkc.analyse.PorterStemmer;
import vkc.analyse.SearchAnalyzer;
import vkc.analyse.TdfAnalyzer;

import vkc.converter.DocConverter;

import vkc.db.entities.VKCAddAttribute;
import vkc.db.entities.VKCAdditionalDocumentFormat;
import vkc.db.entities.VKCDocument;
import vkc.db.entities.VKCDynaAttribute;
import vkc.db.entities.VKCUser;

import vkc.db.serializer.VKCAddAttributeSerializer;
import vkc.db.serializer.VKCDocumentSerializer;
import vkc.db.serializer.VKCSysParameterSerializer;

import vkc.exception.DBException;

import vkc.misc.VKCLogManager;

import vkc.util.CategoryUtils;
import vkc.util.DateUtils;
import vkc.util.DocumentUtils;
import vkc.util.StringIntHashMap;
import vkc.util.StringUtils;
import vkc.util.VKCStringUtils;

import vkc.view.WebShare;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * Klasse für den Aufbau und Pflege des Suchindex von Lucene
 *
 * @author lawern
 */
public final class Search implements Job {
    public static final Integer TDF_FORMAT = new Integer(10);
    private static boolean debug = false;
    private static boolean closeSearcher = false;
    private static Integer searcherInUse = new Integer(0);
    private static final long MAX_INDEX_AGE_ALLOWED = 1000L * 60L * 5L;

    // Verzeichnis, in dem der Suchindex liegt
    static File indexDir = null;

    /** Lucene-Objekt zur Suche über alle Dokumente */
    private static IndexSearcher searcher = null;
    private static Date searcherCreated = null;
    static IndexSearcher classificationSearcher = null;
    static Date classificationSearcherCreated = null;
    private static Date indexModified = null;

    // schützt den Index vor nebenläufigen Lese\-Schreiboperationen
    static Object semaphor = new Object();

    // Lucene-Parameter
    public static int MERGE_FACTOR = 5;
    public static int MAX_FIELD_LENGTH = 45000;

    // Stemmer
    private final static PorterStemmer porterStemmer = new PorterStemmer();
    private static final NNVKCGermanStemmer germanStemmer = new NNVKCGermanStemmer();

    static {
        try {
            // Quickfix für Suchanfrage a* auf leonardo: Standardwert hierfür
            // ist 1024, d.h bei mehr als 1024 gefundenen Termen für eine
            // Suchanfrage fliegt die TooManyClauses-Exception
            BooleanQuery.setMaxClauseCount(Integer.MAX_VALUE);
        } catch (Throwable e) {
        }
    }

    // Analyzer anlegen, die für Lucene die Dokumente parsen
    private static Analyzer globalAnalyzer = new TdfAnalyzer();
    private static Analyzer searchAnalyzer = new SearchAnalyzer();

    //KeyBase zur Verwaltung von Term-IDs und deren globalen Term-Gewichten
    static boolean keyStemming = true;
    private static StringIntHashMap keyIdBase = new StringIntHashMap(10000, 0.3d, 0);
    private static boolean keyBaseModified = false;
    public static OpenIntDoubleHashMap idFreqBase = new OpenIntDoubleHashMap(10000);
    static int keyBaseSize = 0;
    private static int optimizationPeriod;

    // Angabe der Felder, die mit der search-Methode durchsucht werden
    private static final String[] searchFields = { 
        "name", "keywords", "title", "description", "authorname", 
        "contents", "documentId", "dynaattributenames", 
        "dynaattributevalues", "docClassId", "docStatusId", "creationDate" };

    /**
     * Konstruktor
     */
    public Search() {
    }

    /**
     * Initialisiert das Search-Modul
     *
     * @param searchIndexDir - Wurzelverzeichnis der VKC-Struktur
     * @param create - falls true, wird der Index neu aufgebaut
     */
    public final static void init(String searchIndexDir, boolean create) {
        indexDir = new File(searchIndexDir);

        try {
            File vkcIndex = new File(indexDir, "KeyBase");

            if (!vkcIndex.exists() || create) {
                // Falls kein Index existiert, wird er automatisch
                // aufgebaut
                rebuildIndex();
            } else {
                // Index optimieren
                IndexWriter writer = new IndexWriter(indexDir, globalAnalyzer, false);
                writer.setMergeFactor(MERGE_FACTOR);
                writer.setMaxFieldLength(MAX_FIELD_LENGTH);
                writer.optimize();
                writer.close();
                loadKeyBase();
            }
        } catch (Exception e) {
            VKCLogManager.errorLog(Search.class, "", e.getMessage(), e);
        }

        //Zyklische Index-Optimierung schedulen
        try {
            JobDetail indexingJob = new JobDetail("INDEX_OPTIMIZATION", Scheduler.DEFAULT_GROUP, Search.class);
            optimizationPeriod = VKCSysParameterSerializer.retrieveIntValue("INDEX", "OptimizationPeriod", 60);

            SimpleTrigger optimizationTrigger = new SimpleTrigger("INDEX_OPTIMIZATION_TRIGGER", Scheduler.DEFAULT_GROUP, new Date(System.currentTimeMillis() + 5000), null,
                    SimpleTrigger.REPEAT_INDEFINITELY, optimizationPeriod * 60 * 1000L);

            WebShare.scheduler.scheduleJob(indexingJob, optimizationTrigger);

            VKCLogManager.infoLog(WebShare.class, indexingJob.getName() + " scheduled with repeat interval: " + DateUtils.formatDuration(optimizationTrigger.getRepeatInterval()));
        } catch (Exception e) {
        }
    }

    /**
     * Methode baut den Suchindex von Lucene neu auf
     */
    final static public void rebuildIndex() throws IOException {
        //falls der Searcher nicht gerade verwendet wird sofort schliessen
        if (searcher != null) {
            try {
                searcher.close();
            } catch (IOException e) {
                searcher = null;
            }
            
            searcher = null;
        }

        if (classificationSearcher != null) {
            try {
                classificationSearcher.close();
            } catch (IOException e) {
                classificationSearcher = null;
            }
            
            classificationSearcher = null;
        }
        
        synchronized (searcherInUse) {
            searcherInUse = new Integer(0);
        }

        IndexWriter writer = null;

        if (indexDir.exists()) {
            try {
                FSDirectory.getDirectory(indexDir, true);
            } catch (IOException e) {
            }
        }

        VKCLogManager.infoLog(Search.class, "creating new index in directory '" + indexDir + "'");

        //writer = new IndexWriter(indexDir, new StopAnalyzer(), true);
        writer = new IndexWriter(indexDir, globalAnalyzer, true);
        writer.setMergeFactor(MERGE_FACTOR);
        writer.setMaxFieldLength(MAX_FIELD_LENGTH);
        writer.close();

        try {
            // alle in der Datenbank vorhandenen Dokumente einfügen
            Collection docs = VKCDocumentSerializer.getAllDocumentIds();
            addDocuments(docs);
        } catch (Exception ex) {
            VKCLogManager.errorLog(Search.class, "", "Adding documents failed!", ex);
        }
    }

    /**
     * Fügt neue Dokumente in den Suchindex ein
     *
     * @param docs Collection mit Objekten vom Typ VKCDocument, die zum Index
     *        hinzugefügt werden soll
     */
    final static private void addDocuments(Collection docs) {
        synchronized (semaphor) {
            //IndexWriter anlegen
            IndexWriter writer = null;
            OpenIntIntHashMap docLangIds = new OpenIntIntHashMap(docs.size());

            try {
                writer = new IndexWriter(indexDir, globalAnalyzer, false);
                writer.setMergeFactor(MERGE_FACTOR);
                writer.setMaxFieldLength(MAX_FIELD_LENGTH);

                //Dokumente dem IndexWriter hinzufügen
                VKCDocument actDoc;
                Document doc;
                int currDocLangId = -1;

                for (Iterator it = docs.iterator(); it.hasNext();) {
                    Integer actID = (Integer) it.next();
                    actDoc = VKCDocumentSerializer.retrieveDocument(actID);

                    doc = createDocument(actDoc);

                    if (doc != null) {
                        writer.addDocument(doc);

                        Analyser analyser = ((TdfAnalyzer) globalAnalyzer).getAnalyser();

                        if (analyser != null) {
                            currDocLangId = analyser.getLangPackId().intValue();

                            if (currDocLangId != actDoc.getLangPackId().intValue()) {
                                //LangPackId ändern, falls der Analyser sich umentschieden hat.
                                actDoc.setLangPackId(analyser.getLangPackId());
                                VKCDocumentSerializer.updateLangPackId(actDoc);
                            }
                        } else {
                            currDocLangId = 0;
                        }

                        docLangIds.put(actID.intValue(), currDocLangId);
                    }

                    actID = null;
                }
            } catch (Exception e) {
                VKCLogManager.errorLog(Search.class, "13", "Can't finisch indexing!", e);
            } finally {
                try {
                    if (writer != null) {
                        writer.optimize();

                        writer.close();
                    }
                } catch (Exception ex) {
                }
            }

            //Searcher auf null setzen, weil sich der Index geändert hat
            //falls der Searcher nicht gerade verwendet wird sofort schliessen
            if (searcherInUse.intValue() == 0) {
                if (searcher != null) {
                    try {
                        searcher.close();
                    } catch (IOException e) {
                        searcher = null;
                    }
                    
                    searcher = null;
                }

                if (classificationSearcher != null) {
                    try {
                        classificationSearcher.close();
                    } catch (IOException e) {
                        classificationSearcher = null;
                    }
                    
                    classificationSearcher = null;
                }
                
                closeSearcher = false;
            } else {
                closeSearcher = true;// sonst den Flag auf true setzen um den Searcher später zu schließen
            }

            //Die neuen Dokumente der KeyBase hinzufügen.
            for (Iterator it = docs.iterator(); it.hasNext();) {
                Integer actID = (Integer) it.next();
                addToKeyBase(actID, docLangIds.get(actID.intValue()));
            }
            
            indexModified = new Date(System.currentTimeMillis());
        }
    }

    /**
     * Einfügemethode für einzelne VKCDocument-Objekte. Nach dem Aufruf muss
     * das übergebene Objekt in der Datenbank aktualisiert werden, da die
     * Keywords ergänzt werden.
     *
     * @param data - VKCDocument-Objekt, welches dem Index hinzugefügt werden
     *        soll
     * @param updateClassification
     */
    final public static void addDocument(VKCDocument data, boolean updateClassification) {
        if (debug) {
            VKCLogManager.infoLog(Search.class, "addDocument: " + data.getDocumentId());
        }

        Integer currDocLangId = null;

        synchronized (semaphor) {
            IndexWriter writer = null;

            // Analyser von der vorhergehenden Indizierung löschen.
            ((TdfAnalyzer) globalAnalyzer).setAnalyser(null);

            try {
                Document doc = createDocument(data);

                writer = new IndexWriter(indexDir, globalAnalyzer, false);
                writer.setMergeFactor(MERGE_FACTOR);
                writer.setMaxFieldLength(MAX_FIELD_LENGTH);
                writer.addDocument(doc);

                if (updateClassification) {
                    Analyser analyser = ((TdfAnalyzer) globalAnalyzer).getAnalyser();

                    if (analyser != null) {
                        currDocLangId = analyser.getLangPackId();
                    }

                    data.setLangPackId(currDocLangId);
                }
            } catch (Exception e) {
                VKCLogManager.errorLog(Search.class, "12", "Can't index Document!", e);
            } finally {
                try {
                    if (writer != null) {
                        writer.close();
                    }
                } catch (Exception ex) {
                }
            }

            //Searcher auf null setzen, weil sich der Index geändert hat
            //falls der Searcher nicht gerade verwendet wird sofort schliessen
            if (searcherInUse.intValue() == 0) {
                if (searcher != null) {
                    try {
                        searcher.close();
                    } catch (IOException e) {
                        searcher = null;
                    }
                    
                    searcher = null;
                }

                if (classificationSearcher != null) {
                    try {
                        classificationSearcher.close();
                    } catch (IOException e) {
                        classificationSearcher = null;
                    }
                    
                    classificationSearcher = null;
                }
                
                closeSearcher = false;
            } else {
                closeSearcher = true;//sonst den Flag auf true setzen um den Searcher später zu schließen
            }

            if (updateClassification) {
                if (currDocLangId != null) {
                    addToKeyBase(data.getDocumentId(), currDocLangId.intValue());
                }
            }
            
            indexModified = new Date(System.currentTimeMillis());
        }
    }

    /**
     * Fügt die Terme des übergebenen Dokuments in die interne KeyBase ein.
     */
    private static void addToKeyBase(Integer documentId, int currDocLangId) {
        TermFreqVector docVec = getTermFreqVector(documentId);

        if (docVec != null) {
            int[] freq = docVec.getTermFrequencies();
            String[] terms = docVec.getTerms();

            //Summe der Term-Frequenzen berechnen
            double freqSum = 0;

            for (int i = freq.length - 1; i >= 0; i--) {
                freqSum += freq[i];
            }

            //Terme in die KeyBase eintragen 
            for (int i = freq.length - 1; i >= 0; i--) {
                getKeyIndex(terms[i], currDocLangId, true, freq[i] / freqSum);
            }
        }
        
        Search.searchFinished();//Dem Search Bescheid geben dass der Suchvorgang beendet ist
    }

    static String stemmKey(String key, int currDocLangId) {
        String stemmedKey = "";

        if (key.length() > 0) {
            //TODO Testweise werden Zahlen auf "" abgebildet.
            if (key.charAt(0) > 64) {
                if (key.length() > 0) {
                    if (keyStemming) {
                        switch (currDocLangId) {
                            case 1:
                                //Deutsch
                                stemmedKey = germanStemmer.stem(key.toLowerCase());

                                if (Analyser_de.isStopWord(stemmedKey)) {
                                    return "";
                                }

                                break;

                            case 2:
                                //Spanisch
                                stemmedKey = key.toLowerCase();

                                if (Analyser_es.isStopWord(stemmedKey)) {
                                    return "";
                                }

                                break;

                            case 3:
                                //Englisch
                                stemmedKey = porterStemmer.stem(key.toLowerCase());

                                if (Analyser_gb.isStopWord(stemmedKey)) {
                                    return "";
                                }

                                break;

                            default:
                                stemmedKey = porterStemmer.stem(key.toLowerCase());
                        }
                    } else {
                        stemmedKey = key.toLowerCase();
                    }
                } else {
                    stemmedKey = "";
                }
            }
        }

        return stemmedKey;
    }

    private static void keyBaseModified() {
        if (keyBaseModified) {
            //Zeitpunkt der Index-Synchronisation verschieben
            try {
                SimpleTrigger synchronizationTrigger = new SimpleTrigger("INDEX_SYNCHRONIZATION_TRIGGER", Scheduler.DEFAULT_GROUP, "INDEX_SYNCHRONIZATION", Scheduler.DEFAULT_GROUP, new Date(System
                        .currentTimeMillis() + 15000), null, 0, 0);

                WebShare.scheduler.rescheduleJob("INDEX_SYNCHRONIZATION_TRIGGER", Scheduler.DEFAULT_GROUP, synchronizationTrigger);
            } catch (SchedulerException e) {
                VKCLogManager.errorLog(Search.class, "27", "Can't reschedule synchronization job!", e);
            }

            return;
        }

        keyBaseModified = true;

        try {
            File file = new File(indexDir.getCanonicalPath() + "/KeyBase");
            file.delete();
        } catch (Exception e) {
            VKCLogManager.errorLog(Search.class, "25", "Can't save key base!", e);
        }

        //Index-Synchronisation in 15 sec veranlassen
        try {
            JobDetail synchronizationJob = WebShare.scheduler.getJobDetail("INDEX_SYNCHRONIZATION", Scheduler.DEFAULT_GROUP);

            if (synchronizationJob == null) {
                synchronizationJob = new JobDetail("INDEX_SYNCHRONIZATION", Scheduler.DEFAULT_GROUP, Search.class);
                SimpleTrigger synchronizationTrigger = new SimpleTrigger(synchronizationJob.getName() + "_TRIGGER", Scheduler.DEFAULT_GROUP, new Date(System.currentTimeMillis() + 15000), null, 0, 0);

                WebShare.scheduler.scheduleJob(synchronizationJob, synchronizationTrigger);
            } else {
                SimpleTrigger synchronizationTrigger = new SimpleTrigger("INDEX_SYNCHRONIZATION_TRIGGER", Scheduler.DEFAULT_GROUP, "INDEX_SYNCHRONIZATION", Scheduler.DEFAULT_GROUP, new Date(System
                        .currentTimeMillis() + 15000), null, 0, 0);

                WebShare.scheduler.rescheduleJob("INDEX_SYNCHRONIZATION_TRIGGER", Scheduler.DEFAULT_GROUP, synchronizationTrigger);
            }
        } catch (SchedulerException e) {
            VKCLogManager.errorLog(Search.class, "26", "Can't create synchronization job!", e);
        }
    }

    /**
     * @param key Keyword
     * @param currDocLangId LangPackId (Sprache des Dokuments)
     * @return id des übergebenen Keywords
     */
    public static int getKeyIndex(String key, int currDocLangId, boolean count, double weight) {
        int keyIndex;
        String stemmedKey = stemmKey(key, currDocLangId).toLowerCase();

        if (stemmedKey.length() == 0) {
            //Stoppwort oder Zahl
            return 0;
        }

        int termId = keyIdBase.get(stemmedKey);

        synchronized (keyIdBase) {
            if (termId == 0) {
                if (count && (stemmedKey.length() > 0)) {
                    //Wenn das keyword zum ersten Mal auftaucht neue ID erzeugen
                    termId = ++keyBaseSize;

                    keyIdBase.add(stemmedKey, termId);
                    //                    System.out.print(key+":"+stemmedKey+":"+termId+" ");
                    idFreqBase.put(termId, weight);

                    keyIndex = keyBaseSize;
                    keyBaseModified();
                } else {
                    //Wenn das Keyword das erste mal auftritt und nicht gezählt werden
                    //soll.
                    keyIndex = 0;
                }
            } else {
                keyIndex = termId;

                if (count) {
                    idFreqBase.put(termId, idFreqBase.get(termId) + weight);
                    keyBaseModified();
                }
            }
        }

        return keyIndex;
    }

    /**
     * Entfernt die Terme des übergebenen Dokuments aus der internen KeyBase.
     */
    private static void removeFromKeyBase(Integer documentId) {
        TermFreqVector docVec = getTermFreqVector(documentId);

        if (docVec != null) {
            //Dokument-Sprache bestimmen
            VKCDocument currDoc;

            try {
                currDoc = VKCDocumentSerializer.retrieveDocument(documentId);
            } catch (DBException e) {
                VKCLogManager.errorLog(Search.class, "23", "Can't retrieve object for documentId: !" + documentId, e);
                Search.searchFinished();
                
                return;
            }

            int currDocLangId = currDoc.getLangPackId().intValue();

            int[] freq = docVec.getTermFrequencies();
            String[] terms = docVec.getTerms();

            //Summe der Term-Frequenzen berechnen
            double freqSum = 0;

            for (int i = freq.length - 1; i >= 0; i--) {
                freqSum += freq[i];
            }

            String stemmedKey;

            //Terme aus der KeyBase reduzieren 
            synchronized (keyIdBase) {
                for (int i = freq.length - 1; i >= 0; i--) {
                    stemmedKey = stemmKey(terms[i], currDocLangId);

                    int termId;

                    if (stemmedKey.length() > 0) {
                        termId = keyIdBase.get(stemmedKey);
                    } else {
                        //Stoppwort oder Zahl
                        termId = 0;
                    }

                    if (termId != 0) {
                        double termFrequency = idFreqBase.get(termId) - (freq[i] / freqSum);

                        if (termFrequency <= 0) {
                            keyIdBase.remove(stemmedKey);
                            idFreqBase.removeKey(termId);
                        } else {
                            idFreqBase.put(termId, termFrequency);
                        }

                        keyBaseModified();
                    }
                }
            }
        }
        
        Search.searchFinished();//Dem Search Bescheid geben dass der Suchvorgang beendet ist
    }

    /**
     * Sorgt dafür, dass die KeyBase auf die Platte geschrieben wird.
     */
    public static void saveKeyBase() {
        if (!keyBaseModified) {
            return;
        }

        synchronized (keyIdBase) {
            File file = null;
            FileOutputStream fos = null;
            ObjectOutputStream o = null;

            keyBaseModified = false;

            try {
                // File file = new File(indexDir.getCanonicalPath() + "/KeyBase");
                // file.delete();
                file = new File(indexDir.getCanonicalPath() + "/KeyBase");

                fos = new FileOutputStream(file);

                o = new ObjectOutputStream(fos);
                o.writeObject(keyIdBase);
                idFreqBase.trimToSize();
                o.writeObject(idFreqBase);
                o.writeInt(keyBaseSize);
                o.flush();
                o.close();
            } catch (Exception e) {
                keyBaseModified = true;
                VKCLogManager.errorLog(Search.class, "21", "Can't save key base!", e);
            }
        }
    }

    /**
     * Sorgt dafür, dass die KeyBase von Platte geladen wird.
     */
    private static void loadKeyBase() {
        synchronized (keyIdBase) {
            try {
                FileInputStream file = new FileInputStream(new File(indexDir.getCanonicalPath() + "/KeyBase"));
                ObjectInputStream o = new ObjectInputStream(file);
                keyIdBase = (StringIntHashMap) o.readObject();
                idFreqBase = (OpenIntDoubleHashMap) o.readObject();
                keyBaseSize = o.readInt();
                keyBaseModified = false;
                o.close();
            } catch (Exception e) {
                VKCLogManager.errorLog(Search.class, "22", "Can't load key base!", e);
            }
        }
    }

    /**
     * Methode zur Entfernung eines Dokuments aus dem Suchindex
     * 
     * @param documentId -
     *            Eindeutiger Bezeichner für einen Eintrag
     * @param updateClassification
     */
    final public static void removeDocument(Integer documentId, boolean updateClassification) throws IOException {
        IndexReader reader = null;

        if (debug) {
            VKCLogManager.infoLog(Search.class, "removeDocument: " + documentId);
        }

        synchronized (semaphor) {
            if (updateClassification) {
                removeFromKeyBase(documentId);
            }

            try {
                //Searcher auf null setzen, weil sich der Index geändert hat
                //falls der Searcher nicht gerade verwendet wird sofort schliessen
                if (searcherInUse.intValue() == 0) {
                    if (searcher != null) {
                        try {
                            searcher.close();
                        } catch (IOException e) {
                            searcher = null;
                        }
                        
                        searcher = null;
                    }

                    if (classificationSearcher != null) {
                        try {
                            classificationSearcher.close();
                        } catch (IOException e) {
                            classificationSearcher = null;
                        }
                        
                        classificationSearcher = null;
                    }
                    
                    closeSearcher = false;
                } else {
                    closeSearcher = true;//sonst den Flag auf true setzen um den Searcher später zu schließen
                }

                reader = IndexReader.open(indexDir);

                // Anhand der documentId das Dokument eliminieren
                reader.deleteDocuments(new Term("documentId", documentId.toString()));
                reader.close();
            } finally {
                try {
                    if (reader != null) {
                        reader.close();
                    }
                } catch (Exception ex) {
                }
            }
            
            indexModified = new Date(System.currentTimeMillis());
        }
    }

    /**
     * Updatemethode für einzelne VKCDocument-Objekte.
     *
     * @param data - VKCDokument-Objekt, welches im Index aktualisiert werden
     *        soll
     * @param updateClassification
     */
    final public static void updateDocument(VKCDocument data, boolean updateClassification) throws IOException {
        if (debug) {
            VKCLogManager.infoLog(Search.class, "updateDocument: " + data.getDocumentId());
        }

        removeDocument(data.getDocumentId(), updateClassification);
        addDocument(data, updateClassification);
    }

    /**
     * Diese Methode erzeugt aus dem übergebenen VKCDocument-Objekt ein
     * Lucene-Document-Objekt. DynaAttributes werden noch nicht indiziert.
     *
     * @param vkcdoc VKCDocument-Objekt
     *
     * @return Lucene-Document-Objekt
     */
    final static Document createDocument(VKCDocument vkcdoc) {
        try {
            Document doc = new Document();

            if (vkcdoc.getDocumentId() != null) {
                doc.add(new Field("documentId", vkcdoc.getDocumentId().toString(), Field.Store.YES, Field.Index.UN_TOKENIZED));
            }

            String accessCount = vkcdoc.getAccessCount() != null ? vkcdoc.getAccessCount().toString() : "0";
            doc.add(new Field("accessCount", accessCount, Field.Store.YES, Field.Index.UN_TOKENIZED));

            String docTypeName = StringUtils.normalize(vkcdoc.getDocumentType().getExtension());

            if (docTypeName.length() > 0) {
                doc.add(new Field("docTypeName", docTypeName, Field.Store.YES, Field.Index.UN_TOKENIZED));
            }

            if (vkcdoc.getPublisherId() != null) {
                doc.add(new Field("publisherId", vkcdoc.getPublisherId().toString(), Field.Store.YES, Field.Index.NO));
            }

            String authorName = StringUtils.normalize(vkcdoc.getAuthorName());

            if (authorName.length() > 0) {
                doc.add(new Field("authorname", VKCStringUtils.decodeXmlEntities(VKCStringUtils.filterUnicodeChars(authorName)), Field.Store.YES, Field.Index.TOKENIZED));
                doc.add(new Field("authorname_sort", VKCStringUtils.decodeXmlEntities(VKCStringUtils.filterUnicodeChars(authorName)), Field.Store.YES, Field.Index.UN_TOKENIZED));
            }

            String publisherName = StringUtils.normalize(vkcdoc.getPublisherName());

            if (publisherName.length() > 0) {
                doc.add(new Field("publishername", VKCStringUtils.decodeXmlEntities(VKCStringUtils.filterUnicodeChars(publisherName)), Field.Store.YES, Field.Index.TOKENIZED));
                doc.add(new Field("publishername_sort", VKCStringUtils.decodeXmlEntities(VKCStringUtils.filterUnicodeChars(publisherName)), Field.Store.YES, Field.Index.UN_TOKENIZED));
            }

            // Projekt-ID des Dokuments ggfs. speichern um schnell in Projekten suchen zu können
            Integer projectId = DocumentUtils.getProjectId(vkcdoc);

            if (projectId != null) {
                doc.add(new Field("projectId", projectId.toString(), Field.Store.YES, Field.Index.NO));
            }

            String name = StringUtils.normalize(vkcdoc.getName());

            if (name.length() > 0) {
                doc.add(new Field("name", VKCStringUtils.decodeXmlEntities(VKCStringUtils.filterUnicodeChars(name)), Field.Store.YES, Field.Index.TOKENIZED));
            }

            if (vkcdoc.getPublished() != null) {
                doc.add(new Field("published", DateTools.dateToString(vkcdoc.getPublished(), Resolution.MILLISECOND), Field.Store.YES, Field.Index.UN_TOKENIZED));
            }

            if (vkcdoc.getExpires() != null) {
                doc.add(new Field("expires", DateTools.dateToString(vkcdoc.getExpires(), Resolution.MILLISECOND), Field.Store.YES, Field.Index.UN_TOKENIZED));
            }

            if (vkcdoc.getCreationDate() != null) {
                doc.add(new Field("creationDate", DateTools.dateToString(vkcdoc.getCreationDate(), Resolution.MILLISECOND), Field.Store.YES, Field.Index.UN_TOKENIZED));
            }

            if (vkcdoc.getDocClassId() != null) {
                doc.add(new Field("docClassId", "classid" + StringUtils.encodeToLetter(vkcdoc.getDocClassId()), Field.Store.YES, Field.Index.UN_TOKENIZED));
            }

            Integer docStatusId = vkcdoc.getDocumentStatus().getStatus();

            if (docStatusId != null) {
                doc.add(new Field("docStatusId", "statusid" + StringUtils.encodeToLetter(docStatusId), Field.Store.YES, Field.Index.UN_TOKENIZED));
            }

            if (vkcdoc.getDocSize() != null) {
                doc.add(new Field("docSize", StringUtils.toString(vkcdoc.getDocSize()), Field.Store.YES, Field.Index.UN_TOKENIZED));
            }

            if (vkcdoc.getVersionHigh() != null) {
                doc.add(new Field("versionHigh", vkcdoc.getVersionHigh().toString(), Field.Store.YES, Field.Index.NO));
            }

            if (vkcdoc.getVersionLow() != null) {
                doc.add(new Field("versionLow", vkcdoc.getVersionLow().toString(), Field.Store.YES, Field.Index.NO));
            }

            if (vkcdoc.getVersionNo() != null) {
                doc.add(new Field("versionNo", vkcdoc.getVersionNo().toString(), Field.Store.YES, Field.Index.NO));
            }

            String versionDescr = StringUtils.normalize(vkcdoc.getVersionDescr());

            if (versionDescr.length() > 0) {
                doc.add(new Field("versionDescr", VKCStringUtils.decodeXmlEntities(VKCStringUtils.filterUnicodeChars(versionDescr)), Field.Store.YES, Field.Index.NO));
            }

            if (vkcdoc.getCategoryId() != null) {
                doc.add(new Field("categoryId", vkcdoc.getCategoryId().toString(), Field.Store.YES, Field.Index.UN_TOKENIZED));
            }

            //Keywords-String aufbauen
            String keywords = vkcdoc.getExternalKeywordsString();

            if (keywords.length() > 0) {
                doc.add(new Field("keywords", VKCStringUtils.decodeXmlEntities(VKCStringUtils.filterUnicodeChars(keywords)), Field.Store.YES, Field.Index.TOKENIZED));
            }

            String title = StringUtils.normalize(vkcdoc.getTitle());

            if (title.length() > 0) {
                doc.add(new Field("title", VKCStringUtils.decodeXmlEntities(VKCStringUtils.filterUnicodeChars(title)), Field.Store.YES, Field.Index.TOKENIZED));
                doc.add(new Field("title_sort", VKCStringUtils.decodeXmlEntities(VKCStringUtils.filterUnicodeChars(title)), Field.Store.YES, Field.Index.UN_TOKENIZED));
            }

            String description = StringUtils.normalize(vkcdoc.getDescription());

            if (description.length() > 0) {
                String s = VKCStringUtils.filterUnicodeChars(description);
                s = VKCStringUtils.htmlFormatedText(s);
                s = VKCStringUtils.decodeXmlEntities(s);
                s = VKCStringUtils.decodeNamedXmlEntitiesNotInSwing(s);
                s = VKCStringUtils.removeHtmlTags(s);

                doc.add(new Field("description", s, Field.Store.YES, Field.Index.TOKENIZED));
            }

            //TDF-Reader holen
            BufferedReader reader = null;
            InputStream docFile = null;
            Collection addDocFormats = vkcdoc.getAdditionalDocumentFormats();

            if (addDocFormats != null) {
                for (Iterator it = addDocFormats.iterator(); it.hasNext();) {
                    VKCAdditionalDocumentFormat actDocForm = (VKCAdditionalDocumentFormat) it.next();

                    if (actDocForm.getDocTypeId().intValue() == DocConverter.tdf) {
                        //TDF-Format gefunden, Reader holen
                        try {
                            docFile = WebShare.archive.getInputStream(actDocForm.getArchiveId().toString());

                            byte[] version = new byte[6];

                            docFile.read(version);

                            if (new String(version).equals("<V:1.0")) {
                                reader = new BufferedReader(new InputStreamReader(docFile));
                            } else {
                                reader = new BufferedReader(new InputStreamReader(docFile, "UTF-8"));
                            }

                            reader.readLine();
                        } catch (Exception e) {
                            VKCLogManager.errorLog(Search.class, "6", "Can't acces to Datastore", e);
                        }

                        break;
                    }
                }
            }

            if (reader != null) {
                doc.add(new Field("contents", reader, Field.TermVector.YES));
            }

            if ((vkcdoc.getDynaAttributes() != null) && !vkcdoc.getDynaAttributes().isEmpty()) {
                String dynaAttributeNames = "";
                String dynaAttributeValues = "";
                Iterator it = vkcdoc.getDynaAttributes().iterator();

                while (it.hasNext()) {
                    VKCDynaAttribute dynaAttr = (VKCDynaAttribute) it.next();

                    if (!"".equals(dynaAttributeNames)) {
                        dynaAttributeNames += (" " + dynaAttr.getName());
                    } else {
                        dynaAttributeNames = dynaAttr.getName();
                    }

                    if (!"".equals(dynaAttributeValues)) {
                        dynaAttributeValues += (" " + dynaAttr.getValue());
                    } else {
                        dynaAttributeValues = dynaAttr.getValue();
                    }
                }

                if (dynaAttributeNames.length() > 0) {
                    doc.add(new Field("dynaattributenames", VKCStringUtils.decodeXmlEntities(VKCStringUtils.filterUnicodeChars(dynaAttributeNames)), Field.Store.YES, Field.Index.TOKENIZED));
                }

                if (dynaAttributeValues.length() > 0) {
                    doc.add(new Field("dynaattributevalues", VKCStringUtils.decodeXmlEntities(VKCStringUtils.filterUnicodeChars(dynaAttributeValues)), Field.Store.YES, Field.Index.TOKENIZED));
                }

                if (vkcdoc.getDynaAttrsId() != null) {
                    doc.add(new Field("dynaAttrsId", vkcdoc.getDynaAttrsId().toString(), Field.Store.YES, Field.Index.NO));
                }
            }

            if ((vkcdoc.getAddAttributes() != null) && !vkcdoc.getAddAttributes().isEmpty()) {
                for (int i = 0; i < vkcdoc.getAddAttributes().size(); i++) {
                    VKCAddAttribute addAttr = (VKCAddAttribute) vkcdoc.getAddAttributes().get(i);

                    String value = addAttr.getValuestring();

                    if ((value != null) && (value.length() > 0)) {
                        doc.add(new Field("adddocattrId_" + addAttr.getAttrId(), VKCStringUtils.decodeXmlEntities(VKCStringUtils.filterUnicodeChars(value)), Field.Store.YES, Field.Index.TOKENIZED));
                    }
                }
            }

            return doc;
        } catch (Exception e) {
            VKCLogManager.errorLog(Search.class, "12", "Can't create index document!", e);

            return null;
        }
    }

    /**
     * Methode zum Suchen über die VKC-Dokumente
     *
     * @param query Suchanfrage
     * @param dateRangeMode gibt das Alter an das die Dokumente haben dürfen,
     *        die gesucht werden sollen
     * @param documentType gibt an, welcher Dokumenttyp in der Trefferliste
     *        auftreten darf. Falls documentType == *, werden alle Typen
     *        erlaubt. Ansonsten siehe ID-Konstanten in VKCFileTypeHandler.
     * @param user VKCUser für den die Ergebnisliste gefiltert werden soll.
     *        Wenn <code>null</code> wird nicht nach Benutzern gefiltert.
     * @param categoryId Wird hier eine gültige categoryId übergeben, so werden
     *        nur Ergebnisse angezeigt, die unterhalb dieser Kategorie liegen.
     *
     * @return Liste mit Objekten vom Typ ResultEntry der gefundenen Dokumente
     *         oder <code>null</code> falls etwas schief gegangen ist
     */
    public static final List search(String query, Date from, Date until, String documentType, VKCUser user, Integer categoryId) {
        query = VKCStringUtils.decodeXmlEntities(VKCStringUtils.filterUnicodeChars(query));

        if ((query == null) || (query.trim().length() == 0)) {
            return null;
        }

        if (debug) {
            System.out.println("User-Query: " + query);
        }

        Query userQuery = null;

        try {
            List fields = new ArrayList(Arrays.asList(searchFields));
            List addDocAttributes = VKCAddAttributeSerializer.getAllAddDocAttributes();

            if (!addDocAttributes.isEmpty()) {
                for (Iterator it = addDocAttributes.iterator(); it.hasNext();) {
                    VKCAddAttribute attr = (VKCAddAttribute) it.next();
                    fields.add("adddocattrId_" + attr.getAttrId());
                }
            }

            BooleanClause.Occur[] occurs = new BooleanClause.Occur[fields.size()];

            for (int i = 0; i < occurs.length; i++) {
                occurs[i] = BooleanClause.Occur.SHOULD;
            }

            // Suchstring parsen
            userQuery = MultiFieldQueryParser.parse(query, (String[]) fields.toArray(new String[0]), occurs, searchAnalyzer);
        } catch (ParseException e) {
            VKCLogManager.errorLog(Search.class, "1", "Can't parse search query: " + userQuery, e);

            return null;
        } catch (DBException e2) {
            VKCLogManager.errorLog(Search.class, "3", "Cant acces database!", e2);

            return null;
        }

        return search(userQuery, from, until, documentType, user, categoryId);
    }

    /**
     * Methode zum Suchen über die VKC-Dokumente
     *
     * @param query Suchanfrage
     * @param dateRangeMode gibt das Alter an das die Dokumente haben dürfen,
     *        die gesucht werden sollen
     * @param documentType gibt an, welcher Dokumenttyp in der Trefferliste
     *        auftreten darf. Falls documentType == *, werden alle Typen
     *        erlaubt. Ansonsten siehe ID-Konstanten in VKCFileTypeHandler.
     * @param user VKCUser für den die Ergebnisliste gefiltert werden soll.
     *        Wenn <code>null</code> wird nicht nach Benutzern gefiltert.
     * @param categoryId Wird hier eine gültige categoryId übergeben, so werden
     *        nur Ergebnisse angezeigt, die unterhalb dieser Kategorie liegen.
     *
     * @return Liste mit Objekten vom Typ ResultEntry der gefundenen Dokumente
     *         oder <code>null</code> falls etwas schief gegangen ist
     */
    public static final List search(Query userQuery, Date from, Date until, String documentType, VKCUser user, Integer categoryId) {
        ArrayList result = new ArrayList();

        // Set der documentId's holen die dem Benutzer angezeigt werden dürfen
        Set validCategories = null;

        try {
            if (user != null) {
                if (categoryId == null) {
                    // globale Suche
                    validCategories = CategoryUtils.getReadableCategoryIds(user);
                } else {
                    // Suche im Unterbaum
                    validCategories = CategoryUtils.getReadableCategoryIds(categoryId, user);
                }
            }

            // Filter für die Suche initialisieren
            ResultFilter filter = new ResultFilter(from, until, documentType, validCategories);

            if (debug) {
                System.out.println("Lucene-Query: " + userQuery);
            }

            // Suche durchführen
            startSearch();
            
            if (searcher == null) {
                // Suchobjekt initialisieren
                try {
                    searcher = new IndexSearcher(indexDir.getCanonicalPath());
                    searcherCreated = new Date(System.currentTimeMillis());
                } catch (IOException e) {
                    VKCLogManager.errorLog(Search.class, "14", "Can't create IndexSearcher", e);
                }
            }
            
            Hits hits = searcher.search(userQuery, filter);

            // Ergebnisliste aufbauen
            int hitsLength = hits.length();

            for (int i = 0; i < hitsLength; i++) {
                result.add(SearchResultEntry.toSearchResultEntry(hits.doc(i), hits.score(i)));
            }
        } catch (IOException e1) {
            VKCLogManager.errorLog(Search.class, "2", "Error in search call!", e1);
        } catch (ArrayIndexOutOfBoundsException e3) {
            VKCLogManager.errorLog(Search.class, "11", "Lucene cant parse query: " + userQuery, e3);
        } catch (java.text.ParseException e) {
            VKCLogManager.errorLog(Search.class, "2", "Error in search call!", e);
        } catch (DBException e2) {
            VKCLogManager.errorLog(Search.class, "18", "Cant acces database!", e2);
        }

        if (debug) {
            System.out.println("Search method finished!");
        }
        
        searchFinished();//Bescheid sagen daß der Suchvorgang beendet wird
        
        return result;
    }

    /**
     * Methode die beim Beenden des Suchprozesses den searcher freigibt.
     */
    public static void searchFinished() {
        synchronized (searcherInUse) {
            searcherInUse = new Integer(searcherInUse.intValue() - 1);

            if (searcherInUse.intValue() < 0) {
                searcherInUse = new Integer(0);
            }
            
        }

        if (searcherInUse.intValue() == 0 && closeSearcher || isIndexToOld()) {
            if (searcher != null) {
                try {
                    searcher.close();
                } catch (IOException e) {
                    searcher = null;
                }
                
                searcher = null;

            }

            if (classificationSearcher != null) {
                try {
                    classificationSearcher.close();
                } catch (IOException e) {
                } finally {
                    classificationSearcher = null;
                }
                
                classificationSearcher = null;
            }
            
            closeSearcher = false;
        }
    }

    private static boolean isIndexToOld() {
        if (searcherCreated != null && indexModified != null) {
            if ((indexModified.getTime() - searcherCreated.getTime()) > MAX_INDEX_AGE_ALLOWED) {
                return true;
            }
        }

        return false;
    }

    /**
     * Methode zum sperren des Searchers vor dem Schliessen wärend der Suche.
     * Die variable searcherInUse zeigt an, wieviele Suchprozesse aktiv sind,
     * solange >0 darf searcher nicht geschlossen werden
     *
     */
    static void startSearch() {
        synchronized (searcherInUse) {
            searcherInUse = new Integer(searcherInUse.intValue() + 1);
        }
    }

    static void printStackTrace(StackTraceElement[] trace) {
        for (int i = 1; i < Math.min(4, trace.length); i++)
            System.out.println("\tat " + trace[i]);
    }

    /**
     * Methode zum Suchen über die VKC-Dokumente. Nach der Verarbeitung der
     * Ergebnisliste muss unbedingt die Methode searchFinished aufgerufen
     * werden!
     * 
     * @param query
     *            Suchanfrage
     * @param dateRangeMode
     *            gibt das Alter an das die Dokumente haben dürfen, die gesucht
     *            werden sollen
     * @param documentType
     *            gibt an, welcher Dokumenttyp in der Trefferliste auftreten
     *            darf. Falls documentType == *, werden alle Typen erlaubt.
     *            Ansonsten siehe ID-Konstanten in VKCFileTypeHandler.
     * @param user
     *            VKCUser für den die Ergebnisliste gefiltert werden soll. Wenn
     *            <code>null</code> wird nicht nach Benutzern gefiltert.
     * @param categoryId
     *            Wird hier eine gültige categoryId übergeben, so werden nur
     *            Ergebnisse angezeigt, die unterhalb dieser Kategorie liegen.
     * @param documentIds
     *            Werden hier documentIds übergeben, so werden nur Ergebnisse
     *            angezeigt, die in dieser Menge vorkommen.
     * @param minCount
     *            Wird hier ein Integer ungleich null übergeben, so werden nur
     *            Ergebnisse angezeigt, dessen Zugriffsanzahl mindestens
     *            <code>minCount</code> beträgt.
     * @param sort
     *            Bestimmt wie die Ergebnisse sortiert werden.
     * 
     * @return Liste mit Objekten vom Typ
     *         <code>Hits<code> der gefundenen Dokumente
     *         oder <code>null</code> falls etwas schief gegangen ist
     */
    public static final Hits search(String query, Date from, Date until, String documentType, VKCUser user, Integer categoryId, Set documentIds, Integer minCount, Sort sort) {
        query = VKCStringUtils.decodeXmlEntities(VKCStringUtils.filterUnicodeChars(query));

        if ((query == null) || (query.trim().length() == 0)) {
            return null;
        }

        if (debug) {
            System.out.println("User-Query: " + query);
        }

        Query userQuery = null;

        try {
            List fields = new ArrayList(Arrays.asList(searchFields));
            List addDocAttributes = VKCAddAttributeSerializer.getAllAddDocAttributes();

            if (!addDocAttributes.isEmpty()) {
                for (Iterator it = addDocAttributes.iterator(); it.hasNext();) {
                    VKCAddAttribute attr = (VKCAddAttribute) it.next();
                    fields.add("adddocattrId_" + attr.getAttrId());
                }
            }

            BooleanClause.Occur[] occurs = new BooleanClause.Occur[fields.size()];

            for (int i = 0; i < occurs.length; i++) {
                occurs[i] = BooleanClause.Occur.SHOULD;
            }

            // Suchstring parsen
            userQuery = MultiFieldQueryParser.parse(query, (String[]) fields.toArray(new String[0]), occurs, searchAnalyzer);
        } catch (ParseException e) {
            VKCLogManager.errorLog(Search.class, "1", "Can't parse search query: " + userQuery, e);

            return null;
        } catch (DBException e2) {
            VKCLogManager.errorLog(Search.class, "3", "Cant acces database!", e2);

            return null;
        }

        return search(userQuery, from, until, documentType, user, categoryId, documentIds, minCount, sort);
    }

    /**
     * Methode zum Suchen über die VKC-Dokumente. Nach der Verarbeitung der
     * Ergebnisliste muss unbedingt die Methode searchFinished aufgerufen
     * werden!
     *
     * @param query Suchanfrage
     * @param dateRangeMode gibt das Alter an das die Dokumente haben dürfen,
     *        die gesucht werden sollen
     * @param documentType gibt an, welcher Dokumenttyp in der Trefferliste
     *        auftreten darf. Falls documentType == *, werden alle Typen
     *        erlaubt. Ansonsten siehe ID-Konstanten in VKCFileTypeHandler.
     * @param user VKCUser für den die Ergebnisliste gefiltert werden soll.
     *        Wenn <code>null</code> wird nicht nach Benutzern gefiltert.
     * @param categoryId Wird hier eine gültige categoryId übergeben, so werden
     *        nur Ergebnisse angezeigt, die unterhalb dieser Kategorie liegen.
     * @param documentIds Werden hier documentIds übergeben, so werden
     *        nur Ergebnisse angezeigt, die in dieser Menge vorkommen.
     * @param minCount Wird hier ein Integer ungleich null übergeben, so werden
     *        nur Ergebnisse angezeigt, dessen Zugriffsanzahl mindestens <code>minCount</code>
     *        beträgt.
     * @param sort Bestimmt wie die Ergebnisse sortiert werden.
     *
     *
     * @return Liste mit Objekten vom Typ <code>Hits<code> der gefundenen Dokumente
     *         oder <code>null</code> falls etwas schief gegangen ist
     */
    public static final Hits search(Query userQuery, Date from, Date until, String documentType, VKCUser user, Integer categoryId, Set documentIds, Integer minCount, Sort sort) {
        Hits hits = null;

        //Set der documentId's holen die dem Benutzer angezeigt werden dürfen
        Set validCategories = null;

        try {
            if (user != null) {
                if (categoryId == null) {
                    //globale Suche
                    validCategories = CategoryUtils.getReadableCategoryIds(user);
                } else {
                    //Suche im Unterbaum
                    validCategories = CategoryUtils.getReadableCategoryIds(categoryId, user);
                }
            }

            //Filter für die Suche initialisieren
            ResultFilter filter = new ResultFilter(from, until, documentType, validCategories);
            filter.validDocumentIds = documentIds;
            filter.minCount = minCount;

            if (debug) {
                System.out.println("Lucene-Query: " + userQuery);
            }

            //Suche durchführen
            startSearch();
            
            if (searcher == null) {
                //Suchobjekt initialisieren
                try {
                    searcher = new IndexSearcher(indexDir.getCanonicalPath());
                    searcherCreated = new Date(System.currentTimeMillis());
                } catch (IOException e) {
                    VKCLogManager.errorLog(Search.class, "14", "Can't create IndexSearcher", e);
                }
            }
            
            hits = searcher.search(userQuery, filter, sort);
        } catch (IOException e1) {
            VKCLogManager.errorLog(Search.class, "2", "Error in search call!", e1);
        } catch (ArrayIndexOutOfBoundsException e3) {
            VKCLogManager.errorLog(Search.class, "11", "Lucene cant parse query: " + userQuery, e3);
        } catch (DBException e2) {
            VKCLogManager.errorLog(Search.class, "18", "Cant acces database!", e2);
        } catch (Exception e3) {
            VKCLogManager.errorLog(Search.class, "28", "Unknown Exception by query: " + userQuery, e3);
        }
        
        if (debug) {
            System.out.println("Search method finished!");
        }
        
        return hits;
    }

    /**
     * Holt den TermFreqVector des Dokuments mit der übergebenen documentId.
     * Nach der Verarbeitung des TermFreqVector muss unbedingt die Methode
     * searchFinished aufgerufen werden!
     * 
     * @param documentId
     * @return Vektor mit Termfrequenzen
     */
    public static final TermFreqVector getTermFreqVector(Integer documentId) {
        startSearch();
        
        if (searcher == null) {
            //Suchobjekt initialisieren
            try {
                searcher = new IndexSearcher(indexDir.getCanonicalPath());
                searcherCreated = new Date(System.currentTimeMillis());
            } catch (IOException e) {
                VKCLogManager.errorLog(Search.class, "19", "Can't create IndexSearcher", e);
            }
        }

        try {
            Hits hits = searcher.search(new TermQuery(new Term("documentId", documentId.toString())));

            if (hits.length() != 1) {
                if (debug) {
                    VKCLogManager.errorLog(Search.class, "16", "Error by retrieving document vector for documentId " + documentId);
                }
                //searchFinished();
                return null;
            }

            TermFreqVector vector = searcher.getIndexReader().getTermFreqVector(hits.id(0), "contents");
            
            if ((vector != null) && (vector.size() > 0)) {
                //searchFinished();
                return vector;
            }

            if (debug) {
                VKCLogManager.errorLog(Search.class, "20", "Error by retrieving document vector for documentId " + documentId);
            }
            //searchFinished();
            return null;
        } catch (IOException e) {
            VKCLogManager.errorLog(Search.class, "15", "Error by retrieving document vector for documentId " + documentId, e);
            //searchFinished();
            return null;
        }
    }

    /**
     * Methode überprüft die Querysyntax auf ihre Korrektheit
     * 
     * @param query
     *            Suchanfrage
     * 
     * @return <code>true</code> falls die Syntax Koreekt ist
     *         <code>false</code> sonst
     */
    public static final boolean parseSearchQuerySyntax(String query) {
        query = VKCStringUtils.decodeXmlEntities(VKCStringUtils.filterUnicodeChars(query));

        if ((query == null) || (query.trim().length() == 0)) {
            return true;
        }

        try {
            List fields = new ArrayList(Arrays.asList(searchFields));
            List addDocAttributes = VKCAddAttributeSerializer.getAllAddDocAttributes();

            if (!addDocAttributes.isEmpty()) {
                for (Iterator it = addDocAttributes.iterator(); it.hasNext();) {
                    VKCAddAttribute attr = (VKCAddAttribute) it.next();
                    fields.add("adddocattrId_" + attr.getAttrId());
                }
            }

            BooleanClause.Occur[] occurs = new BooleanClause.Occur[fields.size()];

            for (int i = 0; i < occurs.length; i++) {
                occurs[i] = BooleanClause.Occur.SHOULD;
            }

            // Suchstring parsen
            MultiFieldQueryParser.parse(query, (String[]) fields.toArray(new String[0]), occurs, searchAnalyzer);

            return true;
        } catch (Exception e) {
            return false;
        }
    }

    // /**
    // * Holt den TermFreqVector des Dokuments mit der übergebenen documentId.
    // */
    // public static final TermFreqVector[] getCatTermFreqVectors(
    // Integer categoryId) {
    // if (searcher == null) {
    // //Suchobjekt initialisieren
    //            try {
    //                searcher = new IndexSearcher(indexDir.getCanonicalPath());
    //            } catch (IOException e) {
    //                VKCLogManager.errorLog("vkc.search.Search", "19",
    //                    "Can't create IndexSearcher", e);
    //            }
    //        }
    //
    //        try {
    //            Hits hits = searcher.search(new TermQuery(
    //                        new Term("categoryId", categoryId.toString())));
    //
    //            int hitsSize = hits.length();
    //
    //            if (hitsSize == 0) {
    //                return null;
    //            }
    //
    //            TermFreqVector[] catTermVectors = new TermFreqVector[hitsSize];
    //            IndexReader reader = searcher.getIndexReader();
    //
    //            for (int i = hitsSize - 1; i >= 0; i--) {
    //                TermFreqVector vector = reader.getTermFreqVector(hits.id(0),
    //                        "contents");
    //
    //                if ((vector != null) && (vector.size() > 0)) {
    //                    catTermVectors[i] = vector;
    //                } else {
    //                    catTermVectors[i] = null;
    //                }
    //            }
    //
    //            return catTermVectors;
    //        } catch (IOException e) {
    //            VKCLogManager.errorLog("vkc.search.Search", "15",
    //                "Error by retrieving document vectors for categoryId " +
    //                categoryId, e);
    //
    //            return null;
    //        }
    //    }

    /**
     * Liefert die Anzahl der Dokumente im Index.
     *
     * @return Anzahl der Dokumente im Index
     */
    public static int getNumDocs() {
        startSearch();
        
        if (searcher == null) {
            //Suchobjekt initialisieren
            try {
                searcher = new IndexSearcher(indexDir.getCanonicalPath());
                searcherCreated = new Date(System.currentTimeMillis());
            } catch (IOException e) {
                VKCLogManager.errorLog(Search.class, "19", "Can't create IndexSearcher", e);
            }
        }
        
        int a = searcher.getIndexReader().numDocs();
        searchFinished();
        
        return a;
    }

    /**
     * Diese Methode liefert zu dem Dokument mit der übergebenen ID einer Liste
     * aller ähnlichen Dokumente.
     *
     * @param document VKCDocument-Objekt zu dem ähnliche Dokumente gefunden
     *        werden sollen
     * @param dateRangeMode gibt das Alter an das die Dokumente haben dürfen,
     *        die gesucht werden sollen
     * @param documentType gibt an, welcher Dokumenttyp in der Trefferliste
     *        auftreten darf. Falls documentType == *, werden alle Typen
     *        erlaubt. Ansonsten siehe ID-Konstanten in VKCFileTypeHandler.
     * @param user VKCUser für den die Ergebnisliste gefiltert werden soll.
     *        Wenn <code>null</code> wird nicht nach Benutzern gefiltert.
     * @param categoryId Wird hier eine gültige categoryId übergeben, so werden
     *        nur Ergebnisse angezeigt, die unterhalb dieser Kategorie liegen.
     *
     * @return Liste mit Objekten vom Typ ResultEntry der ähnlichen Dokumente
     *         oder <code>null</code> falls etwas schief gegangen ist
     */
    public static final Collection whatsRelated(VKCDocument document, Date from, Date until, String documentType, VKCUser user, Integer categoryId) {
        TermFreqVector termFreqVector = getTermFreqVector(document.getDocumentId());

        if (termFreqVector == null) {
            if (debug) {
                VKCLogManager.errorLog(Search.class, "17", "Error by retrieving document vector for documentId " + document.getDocumentId());
            }
            
            Search.searchFinished();
            
            return null;
        }

        int[] freq = termFreqVector.getTermFrequencies();
        String[] terms = termFreqVector.getTerms();

        //gewichtete Suchanfrage aufbauen
        BooleanQuery query = new BooleanQuery();

        for (int i = freq.length - 1; i >= 0; i--) {
            TermQuery termQuery = new TermQuery(new Term("contents", terms[i]));
            termQuery.setBoost(freq[i]);
            query.add(termQuery, BooleanClause.Occur.SHOULD);
        }
        
        Search.searchFinished();//Dem Search Bescheid geben dass der Suchvorgang beendet ist
        
        return search(query, from, until, documentType, user, categoryId);
    }

    /**
     * Diese Methode liefert zu dem Dokument mit der übergebenen ID einer Liste
     * aller ähnlichen Dokumente. Nach der Verarbeitung der
     * Ergebnisliste muss unbedingt die Methode searchFinished aufgerufen
     * werden!
     *
     * @param document VKCDocument-Objekt zu dem ähnliche Dokumente gefunden
     *        werden sollen
     * @param dateRangeMode gibt das Alter an das die Dokumente haben dürfen,
     *        die gesucht werden sollen
     * @param documentType gibt an, welcher Dokumenttyp in der Trefferliste
     *        auftreten darf. Falls documentType == *, werden alle Typen
     *        erlaubt. Ansonsten siehe ID-Konstanten in VKCFileTypeHandler.
     * @param user VKCUser für den die Ergebnisliste gefiltert werden soll.
     *        Wenn <code>null</code> wird nicht nach Benutzern gefiltert.
     * @param categoryId Wird hier eine gültige categoryId übergeben, so werden
     *        nur Ergebnisse angezeigt, die unterhalb dieser Kategorie liegen.
     * @param documentIds Werden hier documentIds übergeben, so werden
     *        nur Ergebnisse angezeigt, die in dieser Menge vorkommen.
     * @param minCount Wird hier ein Integer ungleich null übergeben, so werden
     *        nur Ergebnisse angezeigt, dessen Zugriffsanzahl mindestens <code>minCount</code>
     *        beträgt.
     * @param sort Bestimmt wie die Ergebnisse sortiert werden.
     *
     * @return Liste mit Objekten vom Typ <code>Hits<code> der gefundenen Dokumente
     *         oder <code>null</code> falls etwas schief gegangen ist
     */
    public static final Hits whatsRelated(VKCDocument document, Date from, Date until, String documentType, VKCUser user, Integer categoryId, Set documentIds, Integer minCount, Sort sort) {
        TermFreqVector termFreqVector = getTermFreqVector(document.getDocumentId());

        if (termFreqVector == null) {
            if (debug) {
                VKCLogManager.errorLog(Search.class, "17", "Error by retrieving document vector for documentId " + document.getDocumentId());
            }
            
            Search.searchFinished();
            
            return null;
        }

        int[] freq = termFreqVector.getTermFrequencies();
        String[] terms = termFreqVector.getTerms();

        //gewichtete Suchanfrage aufbauen
        BooleanQuery query = new BooleanQuery();

        for (int i = freq.length - 1; i >= 0; i--) {
            TermQuery termQuery = new TermQuery(new Term("contents", terms[i]));
            termQuery.setBoost(freq[i]);
            query.add(termQuery, BooleanClause.Occur.SHOULD);
        }
        
        Search.searchFinished();//Dem Search Bescheid geben dass der Suchvorgang beendet ist
        
        return search(query, from, until, documentType, user, categoryId, documentIds, minCount, sort);
    }

    /**
     * Main-Methode für Tests
     */
    public static void main(java.lang.String[] args) {
        VKCLogManager.init("c:/temp/test_", ".log");
        VKCLogManager.setEnableLogging(true);
        VKCLogManager.setLogToConsole(true);

        init("E:\\work\\VKC3\\defaultroot\\searchindex", false);

        //search("data", -1, -1);
        //whatsRelated(createDocVector("46", -1), -1, -1);
    }

    /**
     * Wird zyklisch vom Scheduler gestartet.
     */
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDetail job = context.getJobDetail();

        //DB-Parameter auf Veränderung prüfen und eventuell Index-Optimierungs reschedulen
        try {
            int newOptimizationPeriod = VKCSysParameterSerializer.retrieveIntValue("INDEX", "OptimizationPeriod", 60);

            if (optimizationPeriod != newOptimizationPeriod) {
                SimpleTrigger optimizationTrigger = new SimpleTrigger("INDEX_OPTIMIZATION_TRIGGER", Scheduler.DEFAULT_GROUP, "INDEX_OPTIMIZATION", Scheduler.DEFAULT_GROUP, new Date(System
                        .currentTimeMillis()
                        + (newOptimizationPeriod * 60 * 1000L)), null, SimpleTrigger.REPEAT_INDEFINITELY, newOptimizationPeriod * 60 * 1000L);

                WebShare.scheduler.rescheduleJob("INDEX_OPTIMIZATION_TRIGGER", Scheduler.DEFAULT_GROUP, optimizationTrigger);

                optimizationPeriod = newOptimizationPeriod;
            }
        } catch (Exception e) {
        }

        if (job.getName().equals("INDEX_OPTIMIZATION")) {
            if (WebShare.classifier.optimizationIsRunning()) {
                //Optimierung nicht ausführen, wenn die Optimierung der Klassifikationsparameter läuft.
                return;
            }

            VKCLogManager.infoLog(this, "Executing Job " + job.getFullName());

            //Index optimieren
            try {
                IndexWriter writer = new IndexWriter(indexDir, globalAnalyzer, false);

                writer.setMergeFactor(MERGE_FACTOR);
                writer.setMaxFieldLength(MAX_FIELD_LENGTH);
                writer.optimize();
                writer.close();

                //falls der Searcher nicht gerade verwendet wird sofort schliessen
                if (searcherInUse.intValue() == 0) {
                    if (searcher != null) {
                        try {
                            searcher.close();
                        } catch (IOException e) {
                            searcher = null;
                        }
                        
                        searcher = null;
                    }

                    if (classificationSearcher != null) {
                        try {
                            classificationSearcher.close();
                        } catch (IOException e) {
                            classificationSearcher = null;
                        }
                        
                        classificationSearcher = null;
                    }
                    
                    closeSearcher = false;
                } else {
                    closeSearcher = true;//sonst den Flag auf true setzen um den Searcher später zu schließen
                }
            } catch (IOException e) {
                VKCLogManager.errorLog(this, "24", "Error in index optimization!", e);
            }
        } else {
            VKCLogManager.infoLog(this, "Executing Job " + job.getFullName());

            if (job.getName().equals("INDEX_SYNCHRONIZATION")) {
                //KeyBase mit Platte synchronisieren
                saveKeyBase();
            }
        }
    }
}

/**
 * Wird benötigt, weil die stem-Methode des GermanStemmer protected ist.
 *
 * @author lawern
 */
final class NNVKCGermanStemmer extends GermanStemmer {
    public String stem(String word) {
        return super.stem(word);
    }
}