xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/search/context/HistoryContext.java (revision d6df19e1b22784c78f567cf74c42f18e3901b900)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * See LICENSE.txt included in this distribution for the specific
9  * language governing permissions and limitations under the License.
10  *
11  * When distributing Covered Code, include this CDDL HEADER in each
12  * file and include the License file at LICENSE.txt.
13  * If applicable, add the following below this CDDL HEADER, with the
14  * fields enclosed by brackets "[]" replaced with your own identifying
15  * information: Portions Copyright [yyyy] [name of copyright owner]
16  *
17  * CDDL HEADER END
18  */
19 
20 /*
21  * Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
22  * Portions Copyright (c) 2017, 2020, Chris Fraire <cfraire@me.com>.
23  */
24 package org.opengrok.indexer.search.context;
25 
26 import java.io.File;
27 import java.io.IOException;
28 import java.io.Writer;
29 import java.util.Collections;
30 import java.util.Iterator;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.logging.Level;
34 import java.util.logging.Logger;
35 
36 import org.apache.lucene.search.Query;
37 import org.opengrok.indexer.history.History;
38 import org.opengrok.indexer.history.HistoryEntry;
39 import org.opengrok.indexer.history.HistoryException;
40 import org.opengrok.indexer.history.HistoryGuru;
41 import org.opengrok.indexer.logger.LoggerFactory;
42 import org.opengrok.indexer.search.Hit;
43 import org.opengrok.indexer.search.QueryBuilder;
44 import org.opengrok.indexer.web.Prefix;
45 import org.opengrok.indexer.web.QueryParameters;
46 import org.opengrok.indexer.web.Util;
47 
48 /**
49  * it is supposed to get the matching lines from history log files.
50  * since Lucene does not easily give the match context.
51  */
52 public class HistoryContext {
53 
54     private static final Logger LOGGER = LoggerFactory.getLogger(HistoryContext.class);
55 
56     private final LineMatcher[] m;
57     HistoryLineTokenizer tokens;
58 
59     /**
60      * Map whose keys tell which fields to look for in the history, and
61      * whose values tell if the field is case insensitive (true for
62      * insensitivity, false for sensitivity).
63      */
64     private static final Map<String, Boolean> tokenFields =
65             Collections.singletonMap(QueryBuilder.HIST, Boolean.TRUE);
66 
HistoryContext(Query query)67     public HistoryContext(Query query) {
68         QueryMatchers qm = new QueryMatchers();
69         m = qm.getMatchers(query, tokenFields);
70         if (m != null) {
71             tokens = new HistoryLineTokenizer(null);
72         }
73     }
isEmpty()74     public boolean isEmpty() {
75         return m == null;
76     }
77 
getContext(String filename, String path, List<Hit> hits)78     public boolean getContext(String filename, String path, List<Hit> hits) throws HistoryException {
79         if (m == null) {
80             return false;
81         }
82         File f = new File(filename);
83         History history = HistoryGuru.getInstance().getHistory(f);
84         if (history == null) {
85             LOGGER.log(Level.INFO, "Null history got for {0}", f);
86             return false;
87         }
88         return getHistoryContext(history, path, null, hits, null);
89 
90     }
91 
getContext(String parent, String basename, String path, Writer out, String context)92     public boolean getContext(String parent, String basename, String path, Writer out, String context)
93             throws HistoryException {
94         return getContext(new File(parent, basename), path, out, context);
95     }
96 
97     /**
98      * Obtain the history for the source file <var>src</var> and write out
99      * matching History log entries.
100      *
101      * @param src       the source file represented by <var>path</var>
102      *                  (SOURCE_ROOT + path)
103      * @param path      the path of the file (rooted at SOURCE_ROOT)
104      * @param out       write destination
105      * @param context   the servlet context path of the application (the path
106      *  prefix for URLs)
107      * @return {@code true} if at least one line has been written out.
108      * @throws HistoryException history exception
109      */
getContext(File src, String path, Writer out, String context)110     public boolean getContext(File src, String path, Writer out, String context) throws HistoryException {
111         if (m == null) {
112             return false;
113         }
114         History hist = HistoryGuru.getInstance().getHistory(src);
115         if (hist == null) {
116             LOGGER.log(Level.INFO, "Null history got for {0}", src);
117             return false;
118         }
119         return getHistoryContext(hist, path, out, null, context);
120     }
121 
122     /**
123      * Writes matching History log entries from 'in' to 'out' or to 'hits'.
124      * @param in the history to fetch entries from
125      * @param out to write matched context
126      * @param path path to the file
127      * @param hits list of hits
128      * @param wcontext web context - beginning of url
129      */
getHistoryContext( History in, String path, Writer out, List<Hit> hits, String wcontext)130     private boolean getHistoryContext(
131             History in, String path, Writer out, List<Hit> hits, String wcontext) {
132         if (in == null) {
133             throw new IllegalArgumentException("`in' is null");
134         }
135         if ((out == null) == (hits == null)) {
136             // There should be exactly one destination for the output. If
137             // none or both are specified, it's a bug.
138             throw new IllegalArgumentException(
139                     "Exactly one of out and hits should be non-null");
140         }
141 
142         if (m == null) {
143             return false;
144         }
145 
146         int matchedLines = 0;
147         Iterator<HistoryEntry> it = in.getHistoryEntries().iterator();
148         try {
149             HistoryEntry he;
150             HistoryEntry nhe = null;
151             String nrev;
152             while ((it.hasNext() || (nhe != null)) && matchedLines < 10) {
153                 if (nhe == null) {
154                     he = it.next();
155                 } else {
156                     he = nhe;  //nhe is the lookahead revision
157                 }
158                 String line = he.getLine();
159                 String rev = he.getRevision();
160                 if (it.hasNext()) {
161                     nhe = it.next();
162                 } else {
163                     // this prefetch mechanism is here because of the diff link generation
164                     // we currently generate the diff to previous revision
165                     nhe = null;
166                 }
167                 if (nhe == null) {
168                     nrev = null;
169                 } else {
170                     nrev = nhe.getRevision();
171                 }
172                 tokens.reInit(line);
173                 String token;
174                 int matchState;
175                 long start = -1;
176                 while ((token = tokens.next()) != null) {
177                     for (LineMatcher lineMatcher : m) {
178                         matchState = lineMatcher.match(token);
179                         if (matchState == LineMatcher.MATCHED) {
180                             if (start < 0) {
181                                 start = tokens.getMatchStart();
182                             }
183                             long end = tokens.getMatchEnd();
184                             if (start > Integer.MAX_VALUE || end > Integer.MAX_VALUE) {
185                                 LOGGER.log(Level.INFO, "Unexpected out of bounds for {0}", path);
186                             } else if (out == null) {
187                                 StringBuilder sb = new StringBuilder();
188                                 writeMatch(sb, line, (int) start, (int) end,
189                                         true, path, wcontext, nrev, rev);
190                                 hits.add(new Hit(path, sb.toString(), "", false, false));
191                             } else {
192                                 writeMatch(out, line, (int) start, (int) end,
193                                         false, path, wcontext, nrev, rev);
194                             }
195                             matchedLines++;
196                             break;
197                         } else if (matchState == LineMatcher.WAIT) {
198                             if (start < 0) {
199                                 start = tokens.getMatchStart();
200                             }
201                         } else {
202                             start = -1;
203                         }
204                     }
205                 }
206             }
207         } catch (Exception e) {
208             LOGGER.log(Level.WARNING, "Could not get history context for " + path, e);
209         }
210         return matchedLines > 0;
211     }
212 
213     /**
214      * Write a match to a stream.
215      *
216      * @param out the receiving stream
217      * @param line the matching line
218      * @param start start position of the match
219      * @param end position of the first char after the match
220      * @param flatten should multi-line log entries be flattened to a single
221      * @param path path to the file
222      * @param wcontext web context (begin of URL)
223      * @param nrev old revision
224      * @param rev current revision
225      * line? If {@code true}, replace newline with space.
226      * @throws IOException IO exception
227      */
writeMatch(Appendable out, String line, int start, int end, boolean flatten, String path, String wcontext, String nrev, String rev)228     protected static void writeMatch(Appendable out, String line,
229                             int start, int end, boolean flatten, String path,
230                             String wcontext, String nrev, String rev)
231             throws IOException {
232 
233         String prefix = line.substring(0, start);
234         String match = line.substring(start, end);
235         String suffix = line.substring(end);
236 
237         if (wcontext != null && nrev != null && !wcontext.isEmpty()) {
238             out.append("<a href=\"");
239             printHTML(out, wcontext + Prefix.DIFF_P +
240                     Util.uriEncodePath(path) +
241                     "?" + QueryParameters.REVISION_2_PARAM_EQ + Util.uriEncodePath(path) + "@" +
242                     rev + "&" + QueryParameters.REVISION_1_PARAM_EQ + Util.uriEncodePath(path) +
243                     "@" + nrev + "\" title=\"diff to previous version\"", flatten);
244             out.append(">diff</a> ");
245         }
246 
247         printHTML(out, prefix, flatten);
248         out.append("<b>");
249         printHTML(out, match, flatten);
250         out.append("</b>");
251         printHTML(out, suffix, flatten);
252     }
253 
254     /**
255      * Output a string as HTML.
256      *
257      * @param out where to write the HTML
258      * @param str the string to print
259      * @param flatten should multi-line strings be flattened to a single
260      * line? If {@code true}, replace newline with space.
261      */
printHTML(Appendable out, String str, boolean flatten)262     private static void printHTML(Appendable out, String str, boolean flatten)
263             throws IOException {
264         for (int i = 0; i < str.length(); i++) {
265             char ch = str.charAt(i);
266             switch (ch) {
267                 case '\n':
268                     out.append(flatten ? " " : "<br/>");
269                     break;
270                 case '<':
271                     out.append("&lt;");
272                     break;
273                 case '>':
274                     out.append("&gt;");
275                     break;
276                 case '&':
277                     out.append("&amp;");
278                     break;
279                 default:
280                     out.append(ch);
281             }
282         }
283     }
284 }
285