普通文本  |  288行  |  9.98 KB

// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/autocomplete/history_contents_provider.h"

#include "base/callback.h"
#include "base/metrics/histogram.h"
#include "base/string_util.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/autocomplete/autocomplete_match.h"
#include "chrome/browser/bookmarks/bookmark_model.h"
#include "chrome/browser/bookmarks/bookmark_utils.h"
#include "chrome/browser/history/query_parser.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/url_constants.h"
#include "googleurl/src/url_util.h"
#include "net/base/net_util.h"

using base::TimeTicks;

namespace {

// Number of days to search for full text results. The longer this is, the more
// time it will take.
const int kDaysToSearch = 30;

// When processing the results from the history query, this structure points to
// a single result. It allows the results to be sorted and processed without
// modifying the larger and slower results structure.
struct MatchReference {
  MatchReference(const history::URLResult* result, int relevance)
      : result(result),
        relevance(relevance) {
  }

  const history::URLResult* result;
  int relevance;  // Score of relevance computed by CalculateRelevance.
};

// This is a > operator for MatchReference.
bool CompareMatchRelevance(const MatchReference& a, const MatchReference& b) {
  if (a.relevance != b.relevance)
    return a.relevance > b.relevance;

  // Want results in reverse-chronological order all else being equal.
  return a.result->last_visit() > b.result->last_visit();
}

}  // namespace

using history::HistoryDatabase;

HistoryContentsProvider::HistoryContentsProvider(ACProviderListener* listener,
                                                 Profile* profile)
    : HistoryProvider(listener, profile, "HistoryContents"),
      star_title_count_(0),
      star_contents_count_(0),
      title_count_(0),
      contents_count_(0),
      input_type_(AutocompleteInput::INVALID),
      trim_http_(false),
      have_results_(false) {
}

void HistoryContentsProvider::Start(const AutocompleteInput& input,
                                    bool minimal_changes) {
  matches_.clear();

  if (input.text().empty() || (input.type() == AutocompleteInput::INVALID) ||
      !profile_ ||
      // The history service or bookmark bar model must exist.
      !(profile_->GetHistoryService(Profile::EXPLICIT_ACCESS) ||
        profile_->GetBookmarkModel())) {
    Stop();
    return;
  }

  // TODO(pkasting): http://b/888148 We disallow URL input and "URL-like" input
  // (REQUESTED_URL or UNKNOWN with dots) because we get poor results for it,
  // but we could get better results if we did better tokenizing instead.
  if ((input.type() == AutocompleteInput::URL) ||
      (((input.type() == AutocompleteInput::REQUESTED_URL) ||
        (input.type() == AutocompleteInput::UNKNOWN)) &&
       (input.text().find('.') != string16::npos))) {
    Stop();
    return;
  }

  if (input.matches_requested() == AutocompleteInput::BEST_MATCH) {
    // None of our results are applicable for best match.
    Stop();
    return;
  }

  // Change input type so matches will be marked up properly.
  input_type_ = input.type();
  trim_http_ = !HasHTTPScheme(input.text());

  // Decide what to do about any previous query/results.
  if (!minimal_changes) {
    // Any in-progress request is irrelevant, cancel it.
    Stop();
  } else if (have_results_) {
    // We finished the previous query and still have its results.  Mark them up
    // again for the new input.
    ConvertResults();
    return;
  } else if (!done_) {
    // We're still running the previous query on the HistoryService.  If we're
    // allowed to keep running it, do so, and when it finishes, its results will
    // get marked up for this new input.  In synchronous_only mode, cancel the
    // history query.
    if (input.matches_requested() != AutocompleteInput::ALL_MATCHES) {
      done_ = true;
      request_consumer_.CancelAllRequests();
    }
    ConvertResults();
    return;
  }

  if (!results_.empty()) {
    // Clear the results. We swap in an empty one as the easy way to clear it.
    history::QueryResults empty_results;
    results_.Swap(&empty_results);
  }

  // Querying bookmarks is synchronous, so we always do it.
  QueryBookmarks(input);

  // Convert the bookmark results.
  ConvertResults();

  if (input.matches_requested() == AutocompleteInput::ALL_MATCHES) {
    HistoryService* history =
        profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
    if (history) {
      done_ = false;

      history::QueryOptions options;
      options.SetRecentDayRange(kDaysToSearch);
      options.max_count = kMaxMatches;
      history->QueryHistory(input.text(), options,
          &request_consumer_,
          NewCallback(this, &HistoryContentsProvider::QueryComplete));
    }
  }
}

void HistoryContentsProvider::Stop() {
  done_ = true;
  request_consumer_.CancelAllRequests();

  // Clear the results. We swap in an empty one as the easy way to clear it.
  history::QueryResults empty_results;
  results_.Swap(&empty_results);
  have_results_ = false;
}

HistoryContentsProvider::~HistoryContentsProvider() {
}

void HistoryContentsProvider::QueryComplete(HistoryService::Handle handle,
                                            history::QueryResults* results) {
  results_.AppendResultsBySwapping(results, true);
  have_results_ = true;
  ConvertResults();

  done_ = true;
  if (listener_)
    listener_->OnProviderUpdate(!matches_.empty());
}

void HistoryContentsProvider::ConvertResults() {
  // Reset the relevance counters so that result relevance won't vary on
  // subsequent passes of ConvertResults.
  star_title_count_ = star_contents_count_ = title_count_ = contents_count_ = 0;

  // Make the result references and score the results.
  std::vector<MatchReference> result_refs;
  result_refs.reserve(results_.size());

  // Results are sorted in decreasing order so we run the loop backwards so that
  // the relevance increment favors the higher ranked results.
  for (std::vector<history::URLResult*>::const_reverse_iterator i =
       results_.rbegin(); i != results_.rend(); ++i) {
    history::URLResult* result = *i;
    MatchReference ref(result, CalculateRelevance(*result));
    result_refs.push_back(ref);
  }

  // Get the top matches and add them.
  size_t max_for_provider = std::min(kMaxMatches, result_refs.size());
  std::partial_sort(result_refs.begin(), result_refs.begin() + max_for_provider,
                    result_refs.end(), &CompareMatchRelevance);
  matches_.clear();
  for (size_t i = 0; i < max_for_provider; i++) {
    matches_.push_back(ResultToMatch(*result_refs[i].result,
                                     result_refs[i].relevance));
  }
}

static bool MatchInTitle(const history::URLResult& result) {
  return !result.title_match_positions().empty();
}

AutocompleteMatch HistoryContentsProvider::ResultToMatch(
    const history::URLResult& result,
    int score) {
  // TODO(sky): if matched title highlight matching words in title.
  // Also show star in popup.
  AutocompleteMatch match(this, score, true, MatchInTitle(result) ?
      AutocompleteMatch::HISTORY_TITLE : AutocompleteMatch::HISTORY_BODY);
  match.contents = StringForURLDisplay(result.url(), true, trim_http_);
  match.fill_into_edit =
      AutocompleteInput::FormattedStringWithEquivalentMeaning(result.url(),
                                                              match.contents);
  match.destination_url = result.url();
  match.contents_class.push_back(
      ACMatchClassification(0, ACMatchClassification::URL));
  match.description = result.title();
  match.starred =
      (profile_->GetBookmarkModel() &&
       profile_->GetBookmarkModel()->IsBookmarked(result.url()));

  ClassifyDescription(result, &match);
  return match;
}

void HistoryContentsProvider::ClassifyDescription(
    const history::URLResult& result,
    AutocompleteMatch* match) const {
  const Snippet::MatchPositions& title_matches = result.title_match_positions();

  size_t offset = 0;
  if (!title_matches.empty()) {
    // Classify matches in the title.
    for (Snippet::MatchPositions::const_iterator i = title_matches.begin();
         i != title_matches.end(); ++i) {
      if (i->first != offset) {
        match->description_class.push_back(
            ACMatchClassification(offset, ACMatchClassification::NONE));
      }
      match->description_class.push_back(
            ACMatchClassification(i->first, ACMatchClassification::MATCH));
      offset = i->second;
    }
  }
  if (offset != result.title().size()) {
    match->description_class.push_back(
        ACMatchClassification(offset, ACMatchClassification::NONE));
  }
}

int HistoryContentsProvider::CalculateRelevance(
    const history::URLResult& result) {
  const bool in_title = MatchInTitle(result);
  if (!profile_->GetBookmarkModel() ||
      !profile_->GetBookmarkModel()->IsBookmarked(result.url()))
    return in_title ? (700 + title_count_++) : (500 + contents_count_++);
  return in_title ?
      (1000 + star_title_count_++) : (550 + star_contents_count_++);
}

void HistoryContentsProvider::QueryBookmarks(const AutocompleteInput& input) {
  BookmarkModel* bookmark_model = profile_->GetBookmarkModel();
  if (!bookmark_model)
    return;

  DCHECK(results_.empty());

  TimeTicks start_time = TimeTicks::Now();
  std::vector<bookmark_utils::TitleMatch> matches;
  bookmark_model->GetBookmarksWithTitlesMatching(input.text(),
                                                 kMaxMatches, &matches);
  for (size_t i = 0; i < matches.size(); ++i)
    AddBookmarkTitleMatchToResults(matches[i]);
  UMA_HISTOGRAM_TIMES("Omnibox.QueryBookmarksTime",
                      TimeTicks::Now() - start_time);
}

void HistoryContentsProvider::AddBookmarkTitleMatchToResults(
    const bookmark_utils::TitleMatch& match) {
  history::URLResult url_result(match.node->GetURL(), match.match_positions);
  url_result.set_title(match.node->GetTitle());
  results_.AppendURLBySwapping(&url_result);
}