Newer
Older
AutoTypeSearch / AutoTypeSearch / Searcher.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using AutoTypeSearch.Properties;
using KeePassLib;

namespace AutoTypeSearch
{
	internal class Searcher
	{
		private readonly PwDatabase[] mDatabases;
		private readonly Dictionary<string, SearchResults> mSearches = new Dictionary<string, SearchResults>();

		public Searcher(PwDatabase[] databases)
		{
			mDatabases = databases;
		}

		public SearchResults Search(string term)
		{
			if (term.Length < 2)
			{
				throw new ArgumentException("Search term must be at least 2 characters");
			}

			SearchResults parentResults = null;

			var termParent = term;
			while (termParent.Length >= 2)
			{
				if (mSearches.TryGetValue(termParent, out parentResults))
				{
					if (termParent == term)
					{
						// This is an exact duplicate search, so return it.
						return parentResults;
					}

					// Found an existing search for a parent of the term, start from there.
					break;
				}
		
				// No existing search for termParent found, try less.
				termParent = termParent.Remove(termParent.Length - 1, 1);
			}

			SearchResults searchResults;
			if (parentResults == null)
			{
				// No parent found at all, start from scratch
				searchResults = new SearchResults(GetCountOfAllDatabaseEntries(), term);

				var rootSearchThread = new Thread(RootSearchWorker) { Name = term };
				rootSearchThread.Start(searchResults);
			}
			else
			{
				searchResults = parentResults.CreateChildResults(term);

				var childSearchThread = new Thread(ChildSearchWorker) { Name = term };
				childSearchThread.Start(new ChildSearchWorkerState{ Source = parentResults, Results = searchResults });
			}

			mSearches.Add(term, searchResults);

			return searchResults;
		}

		private int GetCountOfAllDatabaseEntries()
		{
			return (from database in mDatabases select (int)database.RootGroup.GetEntriesCount(true)).Sum();
		}

		private void RootSearchWorker(object stateObject)
		{
			var results = (SearchResults)stateObject;
			var excludeExpired = Settings.Default.ExcludeExpired;
			var searchStartTime = DateTime.Now;

			foreach (var database in mDatabases)
			{
				SearchGroup(database, database.RootGroup, results, excludeExpired, searchStartTime);	
			}

			results.SetComplete();
		}

		/// <summary>
		/// Recursively search <paramref name="group"/> and its children, adding results to <paramref name="results"/>
		/// </summary>
		private void SearchGroup(PwDatabase context, PwGroup group, SearchResults results, bool excludeExpired, DateTime searchStartTime)
		{
			if (group.EnableSearching ?? true) // Group will only be searched if it's parent enabled searching, so if it is inherit (null) or true, search it.
			{
				foreach (var childGroup in group.Groups)
				{
					SearchGroup(context, childGroup, results, excludeExpired, searchStartTime);
				}

				foreach (var entry in group.Entries)
				{
					if (!(excludeExpired && entry.Expires && searchStartTime > entry.ExpiryTime))
					{
						results.AddResultIfMatchesTerm(context, entry);
					}
				}
			}
		}

		private struct ChildSearchWorkerState
		{
			public SearchResults Source;
			public SearchResults Results;
		}
		private void ChildSearchWorker(object stateObject)
		{
			var state = (ChildSearchWorkerState)stateObject;

			bool complete;
			var index = 0;
			do
			{
				foreach (var entry in state.Source.GetAvailableResults(ref index, out complete))
				{
					state.Results.AddResultIfMatchesTerm(entry);
				}
			} while (!complete);

			state.Results.SetComplete();
		}
	}
}