001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the m_terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.search.documents;
029
030import org.opencms.i18n.CmsEncoder;
031import org.opencms.main.OpenCms;
032import org.opencms.search.CmsSearchIndex;
033import org.opencms.search.CmsSearchParameters;
034import org.opencms.search.fields.CmsLuceneFieldConfiguration;
035
036import java.io.IOException;
037import java.io.StringReader;
038import java.util.Iterator;
039
040import org.apache.lucene.analysis.Analyzer;
041import org.apache.lucene.analysis.TokenStream;
042import org.apache.lucene.document.Document;
043import org.apache.lucene.search.Query;
044import org.apache.lucene.search.highlight.Highlighter;
045import org.apache.lucene.search.highlight.InvalidTokenOffsetsException;
046import org.apache.lucene.search.highlight.QueryTermScorer;
047
048/**
049 * Default highlighter implementation used for generation of search excerpts.<p>
050 *
051 * @since 6.0.0
052 */
053public class CmsTermHighlighterHtml implements I_CmsTermHighlighter {
054
055    /** Separator for the search excerpt fragments. */
056    private static final String EXCERPT_FRAGMENT_SEPARATOR = " ... ";
057
058    /** Fragments required in excerpt. */
059    private static final int EXCERPT_REQUIRED_FRAGMENTS = 5;
060
061    /**
062     * @see org.opencms.search.documents.I_CmsTermHighlighter#getExcerpt(org.apache.lucene.document.Document, org.opencms.search.CmsSearchIndex, org.opencms.search.CmsSearchParameters, org.apache.lucene.search.Query, org.apache.lucene.analysis.Analyzer)
063     */
064    public String getExcerpt(
065        Document doc,
066        CmsSearchIndex index,
067        CmsSearchParameters params,
068        Query query,
069        Analyzer analyzer)
070    throws IOException, InvalidTokenOffsetsException {
071
072        if ((doc == null) || (index == null) || (params == null) || (analyzer == null) || (query == null)) {
073            return null;
074        }
075        if (!(index.getFieldConfiguration() instanceof CmsLuceneFieldConfiguration)) {
076            // also return null if the field configuration is not a lucene field configuration
077            return null;
078        }
079        Highlighter highlighter = null;
080        CmsLuceneFieldConfiguration conf = (CmsLuceneFieldConfiguration)index.getFieldConfiguration();
081        Iterator<String> excerptFieldNames = conf.getExcerptFieldNames().iterator();
082        StringBuffer excerptBuffer = new StringBuffer();
083        while (excerptFieldNames.hasNext()) {
084            String fieldName = excerptFieldNames.next();
085            boolean createExcerpt = !params.isExcerptOnlySearchedFields() || params.getFields().contains(fieldName);
086            if (createExcerpt && (doc.getField(fieldName) != null)) {
087                // only generate field excerpt if the field is available in the document
088                String text = doc.getField(fieldName).stringValue();
089                // make sure all XML in the text is escaped, otherwise excerpt HTML output may be garbled
090                text = CmsEncoder.escapeXml(text);
091
092                TokenStream stream = analyzer.tokenStream(fieldName, new StringReader(text));
093
094                if (params.isExcerptOnlySearchedFields()) {
095                    // highlight the search query only in the matching fields
096                    highlighter = new Highlighter(new QueryTermScorer(query, fieldName));
097                } else {
098                    // highlight search query in all fields
099                    if (highlighter == null) {
100                        highlighter = new Highlighter(new QueryTermScorer(query));
101                    }
102                }
103                String fragment = highlighter.getBestFragments(
104                    stream,
105                    text,
106                    EXCERPT_REQUIRED_FRAGMENTS,
107                    EXCERPT_FRAGMENT_SEPARATOR);
108
109                // kill all unwanted chars in the excerpt
110                fragment = fragment.replace('\t', ' ');
111                fragment = fragment.replace('\n', ' ');
112                fragment = fragment.replace('\r', ' ');
113                fragment = fragment.replace('\f', ' ');
114
115                if (excerptBuffer.length() > 0) {
116                    // this is not the first fragment
117                    excerptBuffer.append(EXCERPT_FRAGMENT_SEPARATOR);
118                }
119                excerptBuffer.append(fragment);
120            }
121        }
122
123        String result = null;
124        if (excerptBuffer.length() > 0) {
125            result = excerptBuffer.toString();
126        }
127
128        int maxLength = OpenCms.getSearchManager().getMaxExcerptLength();
129        if ((result != null) && (result.length() > maxLength)) {
130            result = result.substring(0, maxLength);
131        }
132
133        return result;
134    }
135}