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("<"); 272 break; 273 case '>': 274 out.append(">"); 275 break; 276 case '&': 277 out.append("&"); 278 break; 279 default: 280 out.append(ch); 281 } 282 } 283 } 284 } 285