/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * See LICENSE.txt included in this distribution for the specific * language governing permissions and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at LICENSE.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END */ /* * Copyright (c) 2005, 2010, Oracle and/or its affiliates. All rights reserved. */ package org.opensolaris.opengrok.search.context; import java.io.File; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.logging.Logger; import org.apache.lucene.search.Query; import org.opensolaris.opengrok.history.History; import org.opensolaris.opengrok.history.HistoryEntry; import org.opensolaris.opengrok.history.HistoryException; import org.opensolaris.opengrok.history.HistoryGuru; import org.opensolaris.opengrok.search.Hit; import org.opensolaris.opengrok.web.Prefix; import org.opensolaris.opengrok.web.Util; /** * it is supposed to get the matching lines from history log files. * since lucene does not easily give the match context. */ public class HistoryContext { private static final Logger logger = Logger.getLogger(HistoryContext.class.getName()); private final LineMatcher[] m; HistoryLineTokenizer tokens; /** * Map whose keys tell which fields to look for in the history, and * whose values tell if the field is case insensitive (true for * insensitivity, false for sensitivity). */ private static final Map tokenFields = Collections.singletonMap("hist", Boolean.TRUE); /** * Create a new context wrt. to the given query. * @param query query to use. */ public HistoryContext(Query query) { QueryMatchers qm = new QueryMatchers(); m = qm.getMatchers(query, tokenFields); if (m != null) { tokens = new HistoryLineTokenizer((Reader)null); } } /** * Check, whether one ore more line matchers (query terms) are present. * @return {@code true} if present. * @see #HistoryContext(Query) */ public boolean isEmpty() { return m == null; } /** * Obtain the history for the source file filename and append * matches to hits. * * @param filename the source file for which the history should be fetched * @param path the path of the source file (rooted at SOURCE_ROOT) * @param hits where to append matches. * @return {@code true} if matches where found. * @throws HistoryException */ public boolean getContext(String filename, String path, List hits) throws HistoryException { if (m == null) { return false; } File f = new File(filename); return getHistoryContext(HistoryGuru.getInstance().getHistory(f), path, null, hits, null); } /** * Obtain the history for the source file parent/filename and * write out matching history log entries htmlized. * * @param parent the directory which contains the source file * @param basename the basename of the source file * @param path the path of the source file (rooted at SOURCE_ROOT) * @param out where to write htmlized results * @param context the servlet context path of the application (the path * prefix for URLs) * @return {@code true} if matches where found and written out. * @throws HistoryException */ public boolean getContext(String parent, String basename, String path, Writer out, String context) throws HistoryException { return getContext(new File(parent, basename), path, out, context); } /** * Obtain the history for the source file src and write out * matching history log entries htmlized. * * @param src the source file represented by path * (SOURCE_ROOT + path) * @param path the path of the file (rooted at SOURCE_ROOT) * @param out write destination * @param context the servlet context path of the application (the path * prefix for URLs) * @return {@code true} if at least one line has been written out. * @throws HistoryException */ public boolean getContext(File src, String path, Writer out, String context) throws HistoryException { if (m == null) { return false; } History hist = HistoryGuru.getInstance().getHistory(src); return getHistoryContext(hist, path, out, null,context); } /** * Writes matching history log entries from 'in' either htmlized to 'out' * or append them to 'hits'. * @param in the history to fetch entries from * @param out to write matched context * @param path path to the file * @param hits list of hits * @param wcontext web context - beginning of url */ @SuppressWarnings("null") private boolean getHistoryContext(History in, String path, Writer out, List hits, String wcontext) { if ((out == null) == (hits == null)) { // There should be exactly one destination for the output. If // none or both are specified, it's a bug. throw new IllegalArgumentException( "Exactly one of out and hits should be non-null"); } if (m == null) { return false; } int matchedLines = 0; Iterator it = in.getHistoryEntries().iterator(); try { HistoryEntry he = null; HistoryEntry nhe = null; // next HE, the lookahead revision String nrev = null; while((it.hasNext() || (nhe != null)) && matchedLines < 10) { he = nhe == null ? it.next() : nhe; String line = he.getLine(); String rev = he.getRevision(); nhe = it.hasNext() // this prefetch mechanism is here because of the diff link // generation ? it.next() // we currently generate the diff to previous revision : null; nrev = nhe == null ? null : nhe.getRevision(); tokens.reInit(line); String token; int matchState; int start = -1; while ((token = tokens.next()) != null) { for (int i = 0; i< m.length; i++) { matchState = m[i].match(token); if (matchState == LineMatcher.MATCHED) { if (start < 0) { start = tokens.getMatchStart(); } int end = tokens.getMatchEnd(); if (out == null) { StringBuilder sb = new StringBuilder(); writeMatch(sb, line, start, end, true, path, wcontext, nrev, rev); hits.add(new Hit(path, sb.toString(), "", false, false)); } else { writeMatch(out, line, start, end, false, path, wcontext, nrev, rev); } matchedLines++; break; } else if (matchState == LineMatcher.WAIT) { if (start < 0) { start = tokens.getMatchStart(); } } else { start = -1; } } } } } catch (Exception e) { logger.warning("Could not get history context for '" + path + "': " + e.getMessage()); } return matchedLines > 0; } /** * Write a match htmlized to a stream. * * @param out the receiving stream * @param line the matching line * @param start start position of the match * @param end position of the first char after the match * @param flatten should multi-line log entries be flattened to a single * @param path path to the file * @param wcontext web context (begin of url). * @param nrev old revision * @param rev current revision * line? If {@code true}, replace newline with space. */ private static void writeMatch(Appendable out, String line, int start, int end, boolean flatten, String path, String wcontext, String nrev, String rev) throws IOException { String prefix = line.substring(0, start); String match = line.substring(start, end); String suffix = line.substring(end); if (wcontext != null && nrev != null && !wcontext.isEmpty()) { //does below need to be encoded? see bug 16985 String qv = Util.uriEncodeQueryValue(path); out.append("diff "); } String eol = flatten ? " " : "
"; out.append(Util.htmlize(prefix, eol)).append("") .append(Util.htmlize(match, eol)).append("") .append(Util.htmlize(suffix, eol)); } }