330 lines · 11.2 KB · C#
RawDownload
//******************************************************************************************************
//  Searcher.cs - Gbtc
//
//  Copyright © 2010, Grid Protection Alliance.  All Rights Reserved.
//
//  Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
//  the NOTICE file distributed with this work for additional information regarding copyright ownership.
//  The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may
//  not use this file except in compliance with the License. You may obtain a copy of the License at:
//
//      http://www.opensource.org/licenses/eclipse-1.0.php
//
//  Unless agreed to in writing, the subject software distributed under the License is distributed on an
//  "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
//  License for the specific language governing permissions and limitations.
//
//  Code Modification History:
//  ----------------------------------------------------------------------------------------------------
//  10/25/2012 - Stephen C. Wills
//       Generated original version of source code.
//
//******************************************************************************************************

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;

namespace GSF.Searching
{
    /// <summary>
    /// Uses reflection to search a collection of items for a given search string.
    /// </summary>
    /// <typeparam name="TSearch">The type of the objects to be searched.</typeparam>
    public class AsyncSearcher<TSearch>
    {
        #region [ Members ]

        // Events

        /// <summary>
        /// Event triggered when the searcher finds a match.
        /// </summary>
        public event EventHandler<EventArgs<IEnumerable<TSearch>>> MatchesFound;

        /// <summary>
        /// Event triggered when the searcher has finished searching all the items in its queue.
        /// </summary>
        public event EventHandler SearchComplete;

        // Fields
        private string m_searchText;
        private ICollection<string> m_searchCategories;
        private bool m_ignoreCase;
        private bool m_useWildcards;
        private bool m_useRegex;

        private string[] m_tokens;
        private PropertyInfo[] m_searchProperties;
        private RegexOptions m_regexOptions;
        private bool m_usingWildcards;

        private ConcurrentQueue<IEnumerable<TSearch>> m_itemsToSearch;
        private int m_searching;
        private int m_cancel;

        #endregion

        #region [ Constructors ]

        /// <summary>
        /// Creates a new instance of the <see cref="AsyncSearcher{T}"/> class.
        /// </summary>
        public AsyncSearcher()
        {
            m_searchCategories = new List<string>();
            m_itemsToSearch = new ConcurrentQueue<IEnumerable<TSearch>>();
        }

        #endregion

        #region [ Properties ]

        /// <summary>
        /// Gets or sets the text to be searched for.
        /// </summary>
        public string SearchText
        {
            get
            {
                return m_searchText;
            }
            set
            {
                m_searchText = value;
            }
        }

        /// <summary>
        /// Gets the names of the properties to which
        /// the search string will be applied.
        /// </summary>
        public ICollection<string> SearchCategories
        {
            get
            {
                return m_searchCategories;
            }
        }

        /// <summary>
        /// Gets or sets the flag that determines whether
        /// to ignore case when running the search.
        /// </summary>
        public bool IgnoreCase
        {
            get
            {
                return m_ignoreCase;
            }
            set
            {
                m_ignoreCase = value;
            }
        }

        /// <summary>
        /// Gets or sets the flag that determines whether to process
        /// <c>*</c>, <c>?</c>, and <c>-</c> characters as wildcards.
        /// </summary>
        /// <remarks>
        /// The <see cref="UseRegex"/> property supercedes this property.
        /// In order to use wildcards, <c>UseRegex</c> must be set to <c>false</c>.
        /// </remarks>
        public bool UseWildcards
        {
            get
            {
                return m_useWildcards;
            }
            set
            {
                m_useWildcards = value;
            }
        }

        /// <summary>
        /// Gets or sets the flag that determines whether to
        /// process the search tokens as regular expressions.
        /// </summary>
        /// <remarks>
        /// This property supercedes the <see cref="UseWildcards"/> property.
        /// In order to use wildcards, this property must be set to <c>false</c>.
        /// </remarks>
        public bool UseRegex
        {
            get
            {
                return m_useRegex;
            }
            set
            {
                m_useRegex = value;
            }
        }

        /// <summary>
        /// Gets the flag that indicates whether the <see cref="AsyncSearcher{T}"/>
        /// is in the middle of a search operation.
        /// </summary>
        public bool Searching
        {
            get
            {
                return (Interlocked.CompareExchange(ref m_searching, 0, 0) != 0);
            }
        }

        #endregion

        #region [ Methods ]

        /// <summary>
        /// Adds a collection of items to the search.
        /// </summary>
        /// <param name="itemsToSearch">The items to be searched.</param>
        public void Add(IEnumerable<TSearch> itemsToSearch)
        {
            IEnumerable<TSearch> dequeuedItems;

            if (!Searching)
            {
                // Parse the search text into tokens
                m_tokens = m_searchText.Split(' ');

                // Build the collection of properties to be searched
                m_searchProperties = m_searchCategories.Select(category => typeof(TSearch).GetProperty(category)).ToArray();

                // Assign regex options based on the ignore case option
                m_regexOptions = m_ignoreCase ? RegexOptions.IgnoreCase : RegexOptions.None;
                m_usingWildcards = false;

                if (!m_useRegex)
                {
                    // We are not using regex so escape the tokens...
                    m_tokens = m_tokens.Select(token => Regex.Escape(token)).ToArray();

                    if (m_useWildcards)
                    {
                        // ... however we are using wildcards so replace asterisks and question marks with their regex equivalents
                        m_tokens = m_tokens.Select(token => token.Replace(@"\*", ".*").Replace(@"\?", ".?")).ToArray();
                        m_usingWildcards = true;
                    }
                }
            }

            // Queue up the items in the search queue
            m_itemsToSearch.Enqueue(itemsToSearch);

            if (Interlocked.CompareExchange(ref m_searching, 1, 0) == 0)
            {
                // If we are not already searching, then begin the search
                if (m_itemsToSearch.TryDequeue(out dequeuedItems))
                    ThreadPool.QueueUserWorkItem(state => Search((IEnumerable<TSearch>)state), dequeuedItems);
            }
        }

        /// <summary>
        /// Clears the queue of items to search.
        /// </summary>
        /// <remarks>
        /// After a search is cancelled, there may still
        /// be items lingering in the queue. This method
        /// allows the user to clear them out.
        /// </remarks>
        public void Clear()
        {
            m_itemsToSearch = new ConcurrentQueue<IEnumerable<TSearch>>();
        }

        /// <summary>
        /// Cancels currently running searches.
        /// </summary>
        public void Cancel()
        {
            if (Searching)
                Interlocked.Exchange(ref m_cancel, 1);
        }

        private void Search(IEnumerable<TSearch> itemsToSearch)
        {
            IEnumerable<TSearch> dequeuedItems;
            List<TSearch> matches;
            string pattern;
            bool inverse;
            bool isMatch;

            matches = new List<TSearch>();

            foreach (TSearch item in itemsToSearch)
            {
                isMatch = true;

                foreach (string token in m_tokens)
                {
                    // Determine whether this token is being used for normal matching or inverse matching
                    inverse = m_usingWildcards && token.StartsWith("-");
                    pattern = inverse ? token.Substring(1) : token;

                    // If any of the properties that we are searching
                    // match the expression, then it counts as a match
                    isMatch = true; 

                    //m_searchProperties
                    //    .Select(property => property.GetValue(item).ToNonNullString())
                    //    .Select(value => Regex.IsMatch(value, pattern, m_regexOptions))
                    //    .Any(match => match);

                    // If the token is inverted,
                    // invert the match
                    if (inverse)
                        isMatch = !isMatch;

                    // If any one token fails to match, then
                    // the item does not match the search text
                    if (!isMatch)
                        break;
                }

                // If the item matches the search text,
                // add it to the collection of matches
                if (isMatch)
                    matches.Add(item);
            }

            // Notify if matches are found
            if (matches.Count > 0)
                OnMatchesFound(matches);

            if (Interlocked.CompareExchange(ref m_cancel, 1, 0) == 0 && m_itemsToSearch.TryDequeue(out dequeuedItems))
            {
                // If there are more items to be searched, continue the search
                ThreadPool.QueueUserWorkItem(state => Search((IEnumerable<TSearch>)state), dequeuedItems);
            }
            else
            {
                // ... otherwise, complete the search and notify
                Interlocked.Exchange(ref m_searching, 0);
                OnSearchComplete();
            }
        }

        private void OnMatchesFound(IEnumerable<TSearch> matches)
        {
            if ((object)MatchesFound != null)
                MatchesFound(this, new EventArgs<IEnumerable<TSearch>>(matches));
        }

        private void OnSearchComplete()
        {
            if ((object)SearchComplete != null)
                SearchComplete(this, EventArgs.Empty);
        }

        #endregion
    }
}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t