From 027f76af966c4c7e8c2733f7ea0ebb8690a5ca7a Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Sat, 5 Jan 2013 03:01:29 +0300 Subject: [PATCH] Working on #U4-1356 - Moving UmbracoExamine to core --- src/Umbraco.Web/Umbraco.Web.csproj | 6 +- src/UmbracoExamine/BaseUmbracoIndexer.cs | 349 +++++++++++ .../Config/IndexSetExtensions.cs | 64 ++ src/UmbracoExamine/ContentExtensions.cs | 72 +++ .../DataServices/IContentService.cs | 28 + .../DataServices/IDataService.cs | 14 + .../DataServices/ILogService.cs | 12 + .../DataServices/IMediaService.cs | 9 + .../DataServices/UmbracoContentService.cs | 164 +++++ .../DataServices/UmbracoDataService.cs | 25 + .../DataServices/UmbracoLogService.cs | 36 ++ .../DataServices/UmbracoMediaService.cs | 51 ++ src/UmbracoExamine/IndexTypes.cs | 38 ++ src/UmbracoExamine/LoggingLevel.cs | 12 + src/UmbracoExamine/Properties/AssemblyInfo.cs | 36 ++ .../SearchCriteria/ExamineValue.cs | 37 ++ .../SearchCriteria/LuceneBooleanOperation.cs | 70 +++ .../SearchCriteria/LuceneQuery.cs | 330 +++++++++++ .../SearchCriteria/LuceneSearchCriteria.cs | 559 ++++++++++++++++++ .../SearchCriteria/LuceneSearchExtensions.cs | 164 +++++ src/UmbracoExamine/UmbracoContentIndexer.cs | 375 ++++++++++++ src/UmbracoExamine/UmbracoEventManager.cs | 223 +++++++ src/UmbracoExamine/UmbracoExamine.csproj | 179 ++++++ src/UmbracoExamine/UmbracoExamineSearcher.cs | 83 +++ src/UmbracoExamine/UmbracoMemberIndexer.cs | 87 +++ src/UmbracoExamine/XsltExtensions.cs | 265 +++++++++ src/UmbracoExamine/packages.config | 5 + src/packages/repositories.config | 1 + src/umbraco.sln | 9 + 29 files changed, 3300 insertions(+), 3 deletions(-) create mode 100644 src/UmbracoExamine/BaseUmbracoIndexer.cs create mode 100644 src/UmbracoExamine/Config/IndexSetExtensions.cs create mode 100644 src/UmbracoExamine/ContentExtensions.cs create mode 100644 src/UmbracoExamine/DataServices/IContentService.cs create mode 100644 src/UmbracoExamine/DataServices/IDataService.cs create mode 100644 src/UmbracoExamine/DataServices/ILogService.cs create mode 100644 src/UmbracoExamine/DataServices/IMediaService.cs create mode 100644 src/UmbracoExamine/DataServices/UmbracoContentService.cs create mode 100644 src/UmbracoExamine/DataServices/UmbracoDataService.cs create mode 100644 src/UmbracoExamine/DataServices/UmbracoLogService.cs create mode 100644 src/UmbracoExamine/DataServices/UmbracoMediaService.cs create mode 100644 src/UmbracoExamine/IndexTypes.cs create mode 100644 src/UmbracoExamine/LoggingLevel.cs create mode 100644 src/UmbracoExamine/Properties/AssemblyInfo.cs create mode 100644 src/UmbracoExamine/SearchCriteria/ExamineValue.cs create mode 100644 src/UmbracoExamine/SearchCriteria/LuceneBooleanOperation.cs create mode 100644 src/UmbracoExamine/SearchCriteria/LuceneQuery.cs create mode 100644 src/UmbracoExamine/SearchCriteria/LuceneSearchCriteria.cs create mode 100644 src/UmbracoExamine/SearchCriteria/LuceneSearchExtensions.cs create mode 100644 src/UmbracoExamine/UmbracoContentIndexer.cs create mode 100644 src/UmbracoExamine/UmbracoEventManager.cs create mode 100644 src/UmbracoExamine/UmbracoExamine.csproj create mode 100644 src/UmbracoExamine/UmbracoExamineSearcher.cs create mode 100644 src/UmbracoExamine/UmbracoMemberIndexer.cs create mode 100644 src/UmbracoExamine/XsltExtensions.cs create mode 100644 src/UmbracoExamine/packages.config diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e165e4ee1a..8f828ade1f 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -42,7 +42,7 @@ true - bin\ + bin\Debug\ false 285212672 false @@ -66,14 +66,14 @@ AllRules.ruleset - bin\ + bin\Release\ false 285212672 false TRACE - bin\umbraco.xml + bin\Release\umbraco.xml false 4096 false diff --git a/src/UmbracoExamine/BaseUmbracoIndexer.cs b/src/UmbracoExamine/BaseUmbracoIndexer.cs new file mode 100644 index 0000000000..542706a99b --- /dev/null +++ b/src/UmbracoExamine/BaseUmbracoIndexer.cs @@ -0,0 +1,349 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Security; +using System.Text; +using System.Threading; +using System.Web; +using Examine.LuceneEngine.Providers; +using Lucene.Net.Analysis; +using umbraco.BasePages; +using umbraco.BusinessLogic; +using UmbracoExamine.DataServices; +using Examine; +using System.IO; +using System.Xml.Linq; + +namespace UmbracoExamine +{ + + /// + /// An abstract provider containing the basic functionality to be able to query against + /// Umbraco data. + /// + public abstract class BaseUmbracoIndexer : LuceneIndexer + { + #region Constructors + + /// + /// Default constructor + /// + protected BaseUmbracoIndexer() + : base() + { + } + + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + /// + /// + [SecuritySafeCritical] + protected BaseUmbracoIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, Analyzer analyzer, bool async) + : base(indexerData, indexPath, analyzer, async) + { + DataService = dataService; + } + + [SecuritySafeCritical] + protected BaseUmbracoIndexer(IIndexCriteria indexerData, Lucene.Net.Store.Directory luceneDirectory, IDataService dataService, Analyzer analyzer, bool async) + : base(indexerData, luceneDirectory, analyzer, async) + { + DataService = dataService; + } + + #endregion + + #region Properties + + /// + /// If true, the IndexingActionHandler will be run to keep the default index up to date. + /// + public bool EnableDefaultEventHandler { get; protected set; } + + /// + /// Determines if the manager will call the indexing methods when content is saved or deleted as + /// opposed to cache being updated. + /// + public bool SupportUnpublishedContent { get; protected set; } + + /// + /// The data service used for retreiving and submitting data to the cms + /// + public IDataService DataService { get; protected internal set; } + + /// + /// the supported indexable types + /// + protected abstract IEnumerable SupportedTypes { get; } + + #endregion + + #region Initialize + + + /// + /// Setup the properties for the indexer from the provider settings + /// + /// + /// + public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) + { + if (config["dataService"] != null && !string.IsNullOrEmpty(config["dataService"])) + { + //this should be a fully qualified type + var serviceType = Type.GetType(config["dataService"]); + DataService = (IDataService)Activator.CreateInstance(serviceType); + } + else if (DataService == null) + { + //By default, we will be using the UmbracoDataService + //generally this would only need to be set differently for unit testing + DataService = new UmbracoDataService(); + } + + DataService.LogService.LogLevel = LoggingLevel.Normal; + + if (config["logLevel"] != null && !string.IsNullOrEmpty(config["logLevel"])) + { + try + { + var logLevel = (LoggingLevel)Enum.Parse(typeof(LoggingLevel), config["logLevel"]); + DataService.LogService.LogLevel = logLevel; + } + catch (ArgumentException) + { + //FAILED + DataService.LogService.LogLevel = LoggingLevel.Normal; + } + } + + DataService.LogService.ProviderName = name; + + EnableDefaultEventHandler = true; //set to true by default + bool enabled; + if (bool.TryParse(config["enableDefaultEventHandler"], out enabled)) + { + EnableDefaultEventHandler = enabled; + } + + DataService.LogService.AddVerboseLog(-1, string.Format("{0} indexer initializing", name)); + + base.Initialize(name, config); + } + + #endregion + + //public override void RebuildIndex() + //{ + // //we can make the indexing rebuilding operation happen asynchronously in a web context by calling an http handler. + // //we should only do this when async='true', the current request is running in a web context and the current user is authenticated. + // if (RunAsync && HttpContext.Current != null) + // { + // if (UmbracoEnsuredPage.CurrentUser != null) + // { + // RebuildIndexAsync(); + // } + // else + // { + // //don't rebuild, user is not authenticated and if async is set then we shouldn't be generating the index files non-async either + // } + // } + // else + // { + // base.RebuildIndex(); + // } + //} + + #region Protected + + ///// + ///// Calls a web request in a worker thread to rebuild the indexes + ///// + //protected void RebuildIndexAsync() + //{ + // if (HttpContext.Current != null && UmbracoEnsuredPage.CurrentUser != null) + // { + // var handler = VirtualPathUtility.ToAbsolute(ExamineHandlerPath); + // var fullPath = HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + handler + "?index=" + Name; + // var userContext = BasePage.umbracoUserContextID; + // var userContextCookie = HttpContext.Current.Request.Cookies["UserContext"]; + // var thread = new Thread(() => + // { + // var request = (HttpWebRequest)WebRequest.Create(fullPath); + // request.CookieContainer = new CookieContainer(); + // request.CookieContainer.Add(new Cookie("UserContext", userContext, userContextCookie.Path, + // string.IsNullOrEmpty(userContextCookie.Domain) ? "localhost" : userContextCookie.Domain)); + // request.Timeout = Timeout.Infinite; + // request.UseDefaultCredentials = true; + // request.Method = "GET"; + // request.Proxy = null; + + // HttpWebResponse response; + // try + // { + // response = (HttpWebResponse)request.GetResponse(); + + // if (response.StatusCode != HttpStatusCode.OK) + // { + // Log.Add(LogTypes.Custom, -1, "[UmbracoExamine] ExamineHandler request ended with an error: " + response.StatusDescription); + // } + // } + // catch (WebException ex) + // { + // Log.Add(LogTypes.Custom, -1, "[UmbracoExamine] ExamineHandler request threw an exception: " + ex.Message); + // } + + // }) { IsBackground = true, Name = "ExamineAsyncHandler" }; + + // thread.Start(); + // } + //} + + /// + /// Ensures that the node being indexed is of a correct type and is a descendent of the parent id specified. + /// + /// + /// + protected override bool ValidateDocument(XElement node) + { + //check if this document is a descendent of the parent + if (IndexerData.ParentNodeId.HasValue && IndexerData.ParentNodeId.Value > 0) + if (!((string)node.Attribute("path")).Contains("," + IndexerData.ParentNodeId.Value.ToString() + ",")) + return false; + + return base.ValidateDocument(node); + } + + /// + /// Reindexes all supported types + /// + protected override void PerformIndexRebuild() + { + foreach (var t in SupportedTypes) + { + IndexAll(t); + } + } + + public override void ReIndexNode(XElement node, string type) + { + if (!SupportedTypes.Contains(type)) + return; + + base.ReIndexNode(node, type); + } + + /// + /// Builds an xpath statement to query against Umbraco data for the index type specified, then + /// initiates the re-indexing of the data matched. + /// + /// + protected override void PerformIndexAll(string type) + { + if (!SupportedTypes.Contains(type)) + return; + + var xPath = "//*[(number(@id) > 0 and (@isDoc or @nodeTypeAlias)){0}]"; //we'll add more filters to this below if needed + + var sb = new StringBuilder(); + + //create the xpath statement to match node type aliases if specified + if (IndexerData.IncludeNodeTypes.Count() > 0) + { + sb.Append("("); + foreach (var field in IndexerData.IncludeNodeTypes) + { + //this can be used across both schemas + const string nodeTypeAlias = "(@nodeTypeAlias='{0}' or (count(@nodeTypeAlias)=0 and name()='{0}'))"; + + sb.Append(string.Format(nodeTypeAlias, field)); + sb.Append(" or "); + } + sb.Remove(sb.Length - 4, 4); //remove last " or " + sb.Append(")"); + } + + //create the xpath statement to match all children of the current node. + if (IndexerData.ParentNodeId.HasValue && IndexerData.ParentNodeId.Value > 0) + { + if (sb.Length > 0) + sb.Append(" and "); + sb.Append("("); + sb.Append("contains(@path, '," + IndexerData.ParentNodeId.Value + ",')"); //if the path contains comma - id - comma then the nodes must be a child + sb.Append(")"); + } + + //create the full xpath statement to match the appropriate nodes. If there is a filter + //then apply it, otherwise just select all nodes. + var filter = sb.ToString(); + xPath = string.Format(xPath, filter.Length > 0 ? " and " + filter : ""); + + //raise the event and set the xpath statement to the value returned + var args = new IndexingNodesEventArgs(IndexerData, xPath, type); + OnNodesIndexing(args); + if (args.Cancel) + { + return; + } + + xPath = args.XPath; + + DataService.LogService.AddVerboseLog(-1, string.Format("({0}) PerformIndexAll with XPATH: {1}", this.Name, xPath)); + + AddNodesToIndex(xPath, type); + } + + /// + /// Returns an XDocument for the entire tree stored for the IndexType specified. + /// + /// The xpath to the node. + /// The type of data to request from the data service. + /// Either the Content or Media xml. If the type is not of those specified null is returned + protected virtual XDocument GetXDocument(string xPath, string type) + { + if (type == IndexTypes.Content) + { + if (this.SupportUnpublishedContent) + { + return DataService.ContentService.GetLatestContentByXPath(xPath); + } + else + { + return DataService.ContentService.GetPublishedContentByXPath(xPath); + } + } + else if (type == IndexTypes.Media) + { + return DataService.MediaService.GetLatestMediaByXpath(xPath); + } + return null; + } + #endregion + + #region Private + /// + /// Adds all nodes with the given xPath root. + /// + /// The x path. + /// The type. + private void AddNodesToIndex(string xPath, string type) + { + // Get all the nodes of nodeTypeAlias == nodeTypeAlias + XDocument xDoc = GetXDocument(xPath, type); + if (xDoc != null) + { + XElement rootNode = xDoc.Root; + + IEnumerable children = rootNode.Elements(); + + AddNodesToIndex(children, type); + } + + } + #endregion + } +} diff --git a/src/UmbracoExamine/Config/IndexSetExtensions.cs b/src/UmbracoExamine/Config/IndexSetExtensions.cs new file mode 100644 index 0000000000..1a3fe68866 --- /dev/null +++ b/src/UmbracoExamine/Config/IndexSetExtensions.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Examine; +using UmbracoExamine.DataServices; +using Examine.LuceneEngine.Config; + +namespace UmbracoExamine.Config +{ + /// + /// Extension methods for IndexSet + /// + public static class IndexSetExtensions + { + + private static readonly object Locker = new object(); + + /// + /// Convert the indexset to indexerdata. + /// This detects if there are no user/system fields specified and if not, uses the data service to look them + /// up and update the in memory IndexSet. + /// + /// + /// + /// + public static IIndexCriteria ToIndexCriteria(this IndexSet set, IDataService svc) + { + if (set.IndexUserFields.Count == 0) + { + lock (Locker) + { + //we need to add all user fields to the collection if it is empty (this is the default if none are specified) + var userFields = svc.ContentService.GetAllUserPropertyNames(); + foreach (var u in userFields) + { + set.IndexUserFields.Add(new IndexField() { Name = u }); + } + } + } + + if (set.IndexAttributeFields.Count == 0) + { + lock (Locker) + { + //we need to add all system fields to the collection if it is empty (this is the default if none are specified) + var sysFields = svc.ContentService.GetAllSystemPropertyNames(); + foreach (var s in sysFields) + { + set.IndexAttributeFields.Add(new IndexField() { Name = s }); + } + } + } + + return new IndexCriteria( + set.IndexAttributeFields.Cast().ToArray(), + set.IndexUserFields.Cast().ToArray(), + set.IncludeNodeTypes.ToList().Select(x => x.Name).ToArray(), + set.ExcludeNodeTypes.ToList().Select(x => x.Name).ToArray(), + set.IndexParentId); + } + + } +} diff --git a/src/UmbracoExamine/ContentExtensions.cs b/src/UmbracoExamine/ContentExtensions.cs new file mode 100644 index 0000000000..b2d82375ed --- /dev/null +++ b/src/UmbracoExamine/ContentExtensions.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security; +using System.Xml; +using System.Xml.Linq; +using System.Xml.XPath; +using umbraco; +using umbraco.cms.businesslogic; +using umbraco.cms.businesslogic.web; +using Examine.LuceneEngine; + +namespace UmbracoExamine +{ + /// + /// Static methods to help query umbraco xml + /// + public static class ContentExtensions + { + + /// + /// Converts a content node to XDocument + /// + /// + /// true if data is going to be returned from cache + /// + /// + /// If the type of node is not a Document, the cacheOnly has no effect, it will use the API to return + /// the xml. + /// + [SecuritySafeCritical] + public static XDocument ToXDocument(this Content node, bool cacheOnly) + { + if (cacheOnly && node.GetType().Equals(typeof(Document))) + { + var umbXml = library.GetXmlNodeById(node.Id.ToString()); + if (umbXml != null) + { + return umbXml.ToXDocument(); + } + } + + //this will also occur if umbraco hasn't cached content yet.... + + //if it's not a using cache and it's not cacheOnly, then retrieve the Xml using the API + return node.ToXDocument(); + } + + /// + /// Converts a content node to Xml + /// + /// + /// + [SecuritySafeCritical] + private static XDocument ToXDocument(this Content node) + { + var xDoc = new XmlDocument(); + var xNode = xDoc.CreateNode(XmlNodeType.Element, "node", ""); + node.XmlPopulate(xDoc, ref xNode, false); + + if (xNode.Attributes["nodeTypeAlias"] == null) + { + //we'll add the nodeTypeAlias ourselves + XmlAttribute d = xDoc.CreateAttribute("nodeTypeAlias"); + d.Value = node.ContentType.Alias; + xNode.Attributes.Append(d); + } + + return new XDocument(xNode.ToXElement()); + } + + } +} diff --git a/src/UmbracoExamine/DataServices/IContentService.cs b/src/UmbracoExamine/DataServices/IContentService.cs new file mode 100644 index 0000000000..502938fbd2 --- /dev/null +++ b/src/UmbracoExamine/DataServices/IContentService.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using System.Collections.Generic; + +namespace UmbracoExamine.DataServices +{ + public interface IContentService + { + XDocument GetLatestContentByXPath(string xpath); + XDocument GetPublishedContentByXPath(string xpath); + + /// + /// Returns a list of ALL properties names for all nodes defined in the data source + /// + /// + IEnumerable GetAllUserPropertyNames(); + + /// + /// Returns a list of ALL system property names for all nodes defined in the data source + /// + /// + IEnumerable GetAllSystemPropertyNames(); + + string StripHtml(string value); + bool IsProtected(int nodeId, string path); + } +} diff --git a/src/UmbracoExamine/DataServices/IDataService.cs b/src/UmbracoExamine/DataServices/IDataService.cs new file mode 100644 index 0000000000..190e195236 --- /dev/null +++ b/src/UmbracoExamine/DataServices/IDataService.cs @@ -0,0 +1,14 @@ +using System.Web; + + +namespace UmbracoExamine.DataServices +{ + public interface IDataService + { + IContentService ContentService { get; } + ILogService LogService { get; } + IMediaService MediaService { get; } + + string MapPath(string virtualPath); + } +} \ No newline at end of file diff --git a/src/UmbracoExamine/DataServices/ILogService.cs b/src/UmbracoExamine/DataServices/ILogService.cs new file mode 100644 index 0000000000..fb0e9dcc5d --- /dev/null +++ b/src/UmbracoExamine/DataServices/ILogService.cs @@ -0,0 +1,12 @@ +using System; +namespace UmbracoExamine.DataServices +{ + public interface ILogService + { + string ProviderName { get; set; } + void AddErrorLog(int nodeId, string msg); + void AddInfoLog(int nodeId, string msg); + void AddVerboseLog(int nodeId, string msg); + LoggingLevel LogLevel { get; set; } + } +} diff --git a/src/UmbracoExamine/DataServices/IMediaService.cs b/src/UmbracoExamine/DataServices/IMediaService.cs new file mode 100644 index 0000000000..88c07a281c --- /dev/null +++ b/src/UmbracoExamine/DataServices/IMediaService.cs @@ -0,0 +1,9 @@ +using System; +using System.Xml.Linq; +namespace UmbracoExamine.DataServices +{ + public interface IMediaService + { + XDocument GetLatestMediaByXpath(string xpath); + } +} diff --git a/src/UmbracoExamine/DataServices/UmbracoContentService.cs b/src/UmbracoExamine/DataServices/UmbracoContentService.cs new file mode 100644 index 0000000000..6ce1b3695e --- /dev/null +++ b/src/UmbracoExamine/DataServices/UmbracoContentService.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security; +using System.Text; +using umbraco; +using System.Xml.Linq; +using System.Xml; +using umbraco.cms.businesslogic.web; +using System.Collections; +using System.Xml.XPath; +using umbraco.DataLayer; +using umbraco.BusinessLogic; +using UmbracoExamine.Config; +using Examine.LuceneEngine; +using System.Data.SqlClient; +using System.Diagnostics; + +namespace UmbracoExamine.DataServices +{ + public class UmbracoContentService : UmbracoExamine.DataServices.IContentService + { + + /// + /// removes html markup from a string + /// + /// + /// + [SecuritySafeCritical] + public string StripHtml(string value) + { + return library.StripHtml(value); + } + + /// + /// Gets published content by xpath + /// + /// + /// + [SecuritySafeCritical] + public XDocument GetPublishedContentByXPath(string xpath) + { + return library.GetXmlNodeByXPath(xpath).ToXDocument(); + } + + /// + /// This is quite an intensive operation... + /// get all root content, then get the XML structure for all children, + /// then run xpath against the navigator that's created + /// + /// + /// + [SecuritySafeCritical] + public XDocument GetLatestContentByXPath(string xpath) + { + + var rootContent = Document.GetRootDocuments(); + var xmlContent = XDocument.Parse(""); + var xDoc = new XmlDocument(); + foreach (var c in rootContent) + { + var xNode = xDoc.CreateNode(XmlNodeType.Element, "node", ""); + c.XmlPopulate(xDoc, ref xNode, true); + + if (xNode.Attributes["nodeTypeAlias"] == null) + { + //we'll add the nodeTypeAlias ourselves + XmlAttribute d = xDoc.CreateAttribute("nodeTypeAlias"); + d.Value = c.ContentType.Alias; + xNode.Attributes.Append(d); + } + + xmlContent.Root.Add(xNode.ToXElement()); + } + var result = ((IEnumerable)xmlContent.XPathEvaluate(xpath)).Cast(); + return result.ToXDocument(); + } + + /// + /// Unfortunately, we need to implement our own IsProtected method since + /// the Umbraco core code requires an HttpContext for this method and when we're running + /// async, there is no context + /// + /// + /// + [SecuritySafeCritical] + private XmlNode GetPage(int documentId) + { + XmlNode x = Access.AccessXml.SelectSingleNode("/access/page [@id=" + documentId.ToString() + "]"); + return x; + } + + /// + /// Unfortunately, we need to implement our own IsProtected method since + /// the Umbraco core code requires an HttpContext for this method and when we're running + /// async, there is no context + /// + /// + /// + /// + public bool IsProtected(int nodeId, string path) + { + foreach (string id in path.Split(',')) + { + if (GetPage(int.Parse(id)) != null) + { + return true; + } + } + return false; + } + + /// + /// Returns a list of all of the user defined property names in Umbraco + /// + /// + [SecuritySafeCritical] + public IEnumerable GetAllUserPropertyNames() + { + //this is how umb codebase 4.0 does this... booo, should be in the data layer, will fix in 4.1 + + var aliases = new List(); + var fieldSql = "select distinct alias from cmsPropertyType order by alias"; + try + { + using (var dr = Application.SqlHelper.ExecuteReader(fieldSql)) + { + while (dr.Read()) + { + aliases.Add(dr.GetString("alias")); + } + } + } + catch (Exception ex) + { + if (ex is SqlHelperException || ex is SqlException) + { + //if this happens, it could be due to wrong connection string, or something else. + //we don't want to crash the app because of this so we'll actually swallow this + //exception... Unfortunately logging probably won't work in this situation either :( + + Debug.WriteLine("EXCEPTION OCCURRED reading GetAllUserPropertyNames: " + ex.Message, "Error"); + Trace.WriteLine("EXCEPTION OCCURRED reading GetAllUserPropertyNames: " + ex.Message, "Error"); + } + else + { + throw ex; + } + } + + return aliases; + } + + /// + /// Returns a list of all system field names in Umbraco + /// + /// + public IEnumerable GetAllSystemPropertyNames() + { + return UmbracoContentIndexer.IndexFieldPolicies.Select(x => x.Key); + } + + } +} diff --git a/src/UmbracoExamine/DataServices/UmbracoDataService.cs b/src/UmbracoExamine/DataServices/UmbracoDataService.cs new file mode 100644 index 0000000000..9665dc05e3 --- /dev/null +++ b/src/UmbracoExamine/DataServices/UmbracoDataService.cs @@ -0,0 +1,25 @@ +using System.Web; +using System.Web.Hosting; + +namespace UmbracoExamine.DataServices +{ + public class UmbracoDataService : IDataService + { + public UmbracoDataService() + { + ContentService = new UmbracoContentService(); + MediaService = new UmbracoMediaService(); + LogService = new UmbracoLogService(); + } + + public IContentService ContentService { get; protected set; } + public IMediaService MediaService { get; protected set; } + public ILogService LogService { get; protected set; } + + public string MapPath(string virtualPath) + { + return HostingEnvironment.MapPath(virtualPath); + } + + } +} diff --git a/src/UmbracoExamine/DataServices/UmbracoLogService.cs b/src/UmbracoExamine/DataServices/UmbracoLogService.cs new file mode 100644 index 0000000000..d63af55fcf --- /dev/null +++ b/src/UmbracoExamine/DataServices/UmbracoLogService.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security; +using System.Text; +using umbraco.BusinessLogic; + +namespace UmbracoExamine.DataServices +{ + public class UmbracoLogService : UmbracoExamine.DataServices.ILogService + { + public string ProviderName { get; set; } + + [SecuritySafeCritical] + public void AddInfoLog(int nodeId, string msg) + { + Log.Add(LogTypes.Custom, nodeId, "[UmbracoExamine] (" + ProviderName + ")" + msg); + } + + [SecuritySafeCritical] + public void AddErrorLog(int nodeId, string msg) + { + Log.Add(LogTypes.Error, nodeId, "[UmbracoExamine] (" + ProviderName + ")" + msg); + } + + [SecuritySafeCritical] + public void AddVerboseLog(int nodeId, string msg) + { + if (LogLevel == LoggingLevel.Verbose) + Log.Add(LogTypes.Custom, nodeId, "[UmbracoExamine] (" + ProviderName + ")" + msg); + } + + public LoggingLevel LogLevel { get; set; } + + } +} diff --git a/src/UmbracoExamine/DataServices/UmbracoMediaService.cs b/src/UmbracoExamine/DataServices/UmbracoMediaService.cs new file mode 100644 index 0000000000..0daed53207 --- /dev/null +++ b/src/UmbracoExamine/DataServices/UmbracoMediaService.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security; +using System.Text; +using System.Xml.XPath; +using System.Xml.Linq; +using umbraco.cms.businesslogic.media; +using System.Collections; +using Examine.LuceneEngine; + +namespace UmbracoExamine.DataServices +{ + + /// + /// Data service used to query for media + /// + public class UmbracoMediaService : UmbracoExamine.DataServices.IMediaService + { + + /// + /// This is quite an intensive operation... + /// get all root media, then get the XML structure for all children, + /// then run xpath against the navigator that's created + /// + /// + /// + [SecuritySafeCritical] + public XDocument GetLatestMediaByXpath(string xpath) + { + + Media[] rootMedia = Media.GetRootMedias(); + var xmlMedia = XDocument.Parse(""); + foreach (Media media in rootMedia) + { + xmlMedia.Root.Add(GetMediaItem(media.Id)); + } + var result = ((IEnumerable)xmlMedia.XPathEvaluate(xpath)).Cast(); + return result.ToXDocument(); + } + + [SecuritySafeCritical] + private XElement GetMediaItem(int nodeId) + { + var nodes = umbraco.library.GetMedia(nodeId, true); + return XElement.Parse(nodes.Current.OuterXml); + } + + + } +} diff --git a/src/UmbracoExamine/IndexTypes.cs b/src/UmbracoExamine/IndexTypes.cs new file mode 100644 index 0000000000..6d941d21ae --- /dev/null +++ b/src/UmbracoExamine/IndexTypes.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace UmbracoExamine +{ + /// + /// The index types stored in the Lucene Index + /// + public static class IndexTypes + { + + /// + /// The content index type + /// + /// + /// Is lower case because the Standard Analyzer requires lower case + /// + public const string Content = "content"; + + /// + /// The media index type + /// + /// + /// Is lower case because the Standard Analyzer requires lower case + /// + public const string Media = "media"; + + /// + /// The member index type + /// + /// + /// Is lower case because the Standard Analyzer requires lower case + /// + public const string Member = "member"; + } +} diff --git a/src/UmbracoExamine/LoggingLevel.cs b/src/UmbracoExamine/LoggingLevel.cs new file mode 100644 index 0000000000..36ece649af --- /dev/null +++ b/src/UmbracoExamine/LoggingLevel.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace UmbracoExamine +{ + public enum LoggingLevel + { + Verbose, Normal + } +} diff --git a/src/UmbracoExamine/Properties/AssemblyInfo.cs b/src/UmbracoExamine/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..4997aae313 --- /dev/null +++ b/src/UmbracoExamine/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; +using System.Security; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyCompany("umbraco")] +[assembly: AssemblyCopyright("Copyright © Umbraco 2012")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: AssemblyTitle("UmbracoExamine")] +[assembly: AssemblyDescription("Umbraco index & search providers based on the Examine model using Lucene.NET 2.9.2")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyProduct("UmbracoExamine")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("31c5b048-cfa8-49b4-8983-bdba0f99eef5")] + +[assembly: NeutralResourcesLanguage("en-US")] + +//NOTE: WE cannot make change the major version to be the same as Umbraco because of backwards compatibility, however we +// will make the minor version the same as the umbraco version +[assembly: AssemblyVersion("0.6.0.*")] +[assembly: AssemblyFileVersion("0.6.0.*")] + +[assembly: AllowPartiallyTrustedCallers] + +[assembly: InternalsVisibleTo("Umbraco.Tests")] diff --git a/src/UmbracoExamine/SearchCriteria/ExamineValue.cs b/src/UmbracoExamine/SearchCriteria/ExamineValue.cs new file mode 100644 index 0000000000..6a106269af --- /dev/null +++ b/src/UmbracoExamine/SearchCriteria/ExamineValue.cs @@ -0,0 +1,37 @@ +using Examine.SearchCriteria; + +namespace UmbracoExamine.SearchCriteria +{ + internal class ExamineValue : IExamineValue + { + public ExamineValue(Examineness vagueness, string value) : this(vagueness, value, 1) + { + } + + public ExamineValue(Examineness vagueness, string value, float level) + { + this.Examineness = vagueness; + this.Value = value; + this.Level = level; + } + + public Examineness Examineness + { + get; + private set; + } + + public string Value + { + get; + private set; + } + + public float Level + { + get; + private set; + } + + } +} diff --git a/src/UmbracoExamine/SearchCriteria/LuceneBooleanOperation.cs b/src/UmbracoExamine/SearchCriteria/LuceneBooleanOperation.cs new file mode 100644 index 0000000000..9845d9cf77 --- /dev/null +++ b/src/UmbracoExamine/SearchCriteria/LuceneBooleanOperation.cs @@ -0,0 +1,70 @@ +using Examine.SearchCriteria; +using Lucene.Net.Search; + +namespace UmbracoExamine.SearchCriteria +{ + /// + /// An implementation of the fluent API boolean operations + /// + public class LuceneBooleanOperation : IBooleanOperation + { + private LuceneSearchCriteria search; + + internal LuceneBooleanOperation(LuceneSearchCriteria search) + { + this.search = search; + } + + #region IBooleanOperation Members + + /// + /// Sets the next operation to be AND + /// + /// + public IQuery And() + { + return new LuceneQuery(this.search, BooleanClause.Occur.MUST); + } + + /// + /// Sets the next operation to be OR + /// + /// + public IQuery Or() + { + return new LuceneQuery(this.search, BooleanClause.Occur.SHOULD); + } + + /// + /// Sets the next operation to be NOT + /// + /// + public IQuery Not() + { + return new LuceneQuery(this.search, BooleanClause.Occur.MUST_NOT); + } + + /// + /// Compiles this instance for fluent API conclusion + /// + /// + public ISearchCriteria Compile() + { + if (!string.IsNullOrEmpty(this.search.SearchIndexType)) + { + var query = this.search.query; + + this.search.query = new BooleanQuery(); + this.search.query.Add(query, BooleanClause.Occur.MUST); + + //this.search.query.Add(this.search.queryParser.Parse("(" + query.ToString() + ")"), BooleanClause.Occur.MUST); + + this.search.FieldInternal(LuceneExamineIndexer.IndexTypeFieldName, new ExamineValue(Examineness.Explicit, this.search.SearchIndexType.ToString().ToLower()), BooleanClause.Occur.MUST); + } + + return this.search; + } + + #endregion + } +} diff --git a/src/UmbracoExamine/SearchCriteria/LuceneQuery.cs b/src/UmbracoExamine/SearchCriteria/LuceneQuery.cs new file mode 100644 index 0000000000..74d20f58a0 --- /dev/null +++ b/src/UmbracoExamine/SearchCriteria/LuceneQuery.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Examine.SearchCriteria; +using Lucene.Net.Search; + +namespace UmbracoExamine.SearchCriteria +{ + public class LuceneQuery : IQuery + { + private LuceneSearchCriteria search; + private BooleanClause.Occur occurance; + + /// + /// Initializes a new instance of the class. + /// + /// The search. + /// The occurance. + internal LuceneQuery(LuceneSearchCriteria search, BooleanClause.Occur occurance) + { + this.search = search; + this.occurance = occurance; + } + + /// + /// Gets the boolean operation which this query method will be added as + /// + /// The boolean operation. + public BooleanOperation BooleanOperation + { + get { return occurance.ToBooleanOperation(); } + } + + + #region ISearch Members + + /// + /// Query on the id + /// + /// The id. + /// A new with the clause appended + public IBooleanOperation Id(int id) + { + return this.search.IdInternal(id, this.occurance); + } + + /// + /// Query on the NodeName + /// + /// Name of the node. + /// A new with the clause appended + public IBooleanOperation NodeName(string nodeName) + { + return this.search.NodeNameInternal(new ExamineValue(Examineness.Explicit, nodeName), occurance); + } + + /// + /// Query on the NodeTypeAlias + /// + /// The node type alias. + /// A new with the clause appended + public IBooleanOperation NodeTypeAlias(string nodeTypeAlias) + { + return this.search.NodeTypeAliasInternal(new ExamineValue(Examineness.Explicit, nodeTypeAlias), occurance); + } + + /// + /// Query on the Parent ID + /// + /// The id of the parent. + /// A new with the clause appended + public IBooleanOperation ParentId(int id) + { + return this.search.ParentIdInternal(id, occurance); + } + + /// + /// Query on the specified field + /// + /// Name of the field. + /// The field value. + /// A new with the clause appended + public IBooleanOperation Field(string fieldName, string fieldValue) + { + return this.search.FieldInternal(fieldName, new ExamineValue(Examineness.Explicit, fieldValue), occurance); + } + + /// + /// Ranges the specified field name. + /// + /// Name of the field. + /// The start. + /// The end. + /// A new with the clause appended + public IBooleanOperation Range(string fieldName, DateTime start, DateTime end) + { + return this.Range(fieldName, start, end, true, true); + } + + /// + /// Ranges the specified field name. + /// + /// Name of the field. + /// The start. + /// The end. + /// if set to true [include lower]. + /// if set to true [include upper]. + /// A new with the clause appended + public IBooleanOperation Range(string fieldName, DateTime start, DateTime end, bool includeLower, bool includeUpper) + { + return this.search.Range(fieldName, start, end, includeLower, includeUpper); + } + + /// + /// Ranges the specified field name. + /// + /// Name of the field. + /// The start. + /// The end. + /// A new with the clause appended + public IBooleanOperation Range(string fieldName, int start, int end) + { + return this.Range(fieldName, start, end, true, true); + } + + /// + /// Ranges the specified field name. + /// + /// Name of the field. + /// The start. + /// The end. + /// if set to true [include lower]. + /// if set to true [include upper]. + /// A new with the clause appended + public IBooleanOperation Range(string fieldName, int start, int end, bool includeLower, bool includeUpper) + { + return this.search.RangeInternal(fieldName, start, end, includeLower, includeUpper, occurance); + } + + /// + /// Ranges the specified field name. + /// + /// Name of the field. + /// The start. + /// The end. + /// A new with the clause appended + public IBooleanOperation Range(string fieldName, string start, string end) + { + return this.Range(fieldName, start, end, true, true); + } + + /// + /// Ranges the specified field name. + /// + /// Name of the field. + /// The start. + /// The end. + /// if set to true [include lower]. + /// if set to true [include upper]. + /// A new with the clause appended + public IBooleanOperation Range(string fieldName, string start, string end, bool includeLower, bool includeUpper) + { + return this.search.RangeInternal(fieldName, start, end, includeLower, includeUpper, occurance); + } + + /// + /// Query on the NodeName + /// + /// Name of the node. + /// A new with the clause appended + public IBooleanOperation NodeName(IExamineValue nodeName) + { + return this.search.NodeNameInternal(nodeName, occurance); + } + + /// + /// Query on the NodeTypeAlias + /// + /// The node type alias. + /// A new with the clause appended + public IBooleanOperation NodeTypeAlias(IExamineValue nodeTypeAlias) + { + return this.search.NodeTypeAliasInternal(nodeTypeAlias, occurance); + } + + /// + /// Query on the specified field + /// + /// Name of the field. + /// The field value. + /// A new with the clause appended + public IBooleanOperation Field(string fieldName, IExamineValue fieldValue) + { + return this.search.FieldInternal(fieldName, fieldValue, occurance); + } + + /// + /// Queries multiple fields with each being an And boolean operation + /// + /// The fields. + /// The query. + /// A new with the clause appended + public IBooleanOperation GroupedAnd(IEnumerable fields, params string[] query) + { + var fieldVals = new List(); + foreach (var f in query) + { + fieldVals.Add(new ExamineValue(Examineness.Explicit, f)); + } + return this.search.GroupedAndInternal(fields.ToArray(), fieldVals.ToArray(), this.occurance); + } + + /// + /// Queries multiple fields with each being an And boolean operation + /// + /// The fields. + /// The query. + /// A new with the clause appended + public IBooleanOperation GroupedAnd(IEnumerable fields, params IExamineValue[] query) + { + throw new NotImplementedException(); + } + + /// + /// Queries multiple fields with each being an Or boolean operation + /// + /// The fields. + /// The query. + /// A new with the clause appended + public IBooleanOperation GroupedOr(IEnumerable fields, params string[] query) + { + var fieldVals = new List(); + foreach (var f in query) + { + fieldVals.Add(new ExamineValue(Examineness.Explicit, f)); + } + return this.search.GroupedOrInternal(fields.ToArray(), fieldVals.ToArray(), this.occurance); + } + + /// + /// Queries multiple fields with each being an Or boolean operation + /// + /// The fields. + /// The query. + /// A new with the clause appended + public IBooleanOperation GroupedOr(IEnumerable fields, params IExamineValue[] query) + { + return this.search.GroupedOrInternal(fields.ToArray(), query, this.occurance); + } + + /// + /// Queries multiple fields with each being an Not boolean operation + /// + /// The fields. + /// The query. + /// A new with the clause appended + public IBooleanOperation GroupedNot(IEnumerable fields, params string[] query) + { + var fieldVals = new List(); + foreach (var f in query) + { + fieldVals.Add(new ExamineValue(Examineness.Explicit, f)); + } + return this.search.GroupedNotInternal(fields.ToArray(), fieldVals.ToArray(), this.occurance); + } + + /// + /// Queries multiple fields with each being an Not boolean operation + /// + /// The fields. + /// The query. + /// A new with the clause appended + public IBooleanOperation GroupedNot(IEnumerable fields, params IExamineValue[] query) + { + return this.search.GroupedNotInternal(fields.ToArray(), query, this.occurance); + } + + /// + /// Queries on multiple fields with their inclusions customly defined + /// + /// The fields. + /// The operations. + /// The query. + /// A new with the clause appended + public IBooleanOperation GroupedFlexible(IEnumerable fields, IEnumerable operations, params string[] query) + { + var fieldVals = new List(); + foreach (var f in query) + { + fieldVals.Add(new ExamineValue(Examineness.Explicit, f)); + } + return this.search.GroupedFlexibleInternal(fields.ToArray(), operations.ToArray(), fieldVals.ToArray(), occurance); + } + + /// + /// Queries on multiple fields with their inclusions customly defined + /// + /// The fields. + /// The operations. + /// The query. + /// A new with the clause appended + public IBooleanOperation GroupedFlexible(IEnumerable fields, IEnumerable operations, params IExamineValue[] query) + { + return this.search.GroupedFlexibleInternal(fields.ToArray(), operations.ToArray(), query, occurance); + } + + /// + /// Orders the results by the specified fields + /// + /// The field names. + /// A new with the clause appended + public IBooleanOperation OrderBy(params string[] fieldNames) + { + return this.search.OrderBy(fieldNames); + } + + /// + /// Orders the results by the specified fields in a descending order + /// + /// The field names. + /// A new with the clause appended + public IBooleanOperation OrderByDescending(params string[] fieldNames) + { + return this.search.OrderByDescending(fieldNames); + } + + #endregion + + } +} diff --git a/src/UmbracoExamine/SearchCriteria/LuceneSearchCriteria.cs b/src/UmbracoExamine/SearchCriteria/LuceneSearchCriteria.cs new file mode 100644 index 0000000000..e11c50b88f --- /dev/null +++ b/src/UmbracoExamine/SearchCriteria/LuceneSearchCriteria.cs @@ -0,0 +1,559 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Examine; +using Examine.SearchCriteria; +using Lucene.Net.Analysis; +using Lucene.Net.QueryParsers; +using Lucene.Net.Search; +using Lucene.Net.Search.Spans; +using Lucene.Net.Index; +using Lucene.Net.Documents; + +namespace UmbracoExamine.SearchCriteria +{ + /// + /// This class is used to query against Lucene.Net + /// + public class LuceneSearchCriteria : ISearchCriteria + { + internal MultiFieldQueryParser queryParser; + internal BooleanQuery query; + internal List sortFields = new List(); + private readonly BooleanClause.Occur occurance; + private readonly Lucene.Net.Util.Version luceneVersion = Lucene.Net.Util.Version.LUCENE_29; + + internal LuceneSearchCriteria(string type, Analyzer analyzer, string[] fields, bool allowLeadingWildcards, BooleanOperation occurance) + { + Enforcer.ArgumentNotNull(fields, "fields"); + + SearchIndexType = type; + query = new BooleanQuery(); + this.BooleanOperation = occurance; + this.queryParser = new MultiFieldQueryParser(luceneVersion, fields, analyzer); + this.queryParser.SetAllowLeadingWildcard(allowLeadingWildcards); + this.occurance = occurance.ToLuceneOccurance(); + } + + /// + /// Gets the boolean operation which this query method will be added as + /// + /// The boolean operation. + public BooleanOperation BooleanOperation + { + get; + protected set; + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + return string.Format("{{ SearchIndexType: {0}, LuceneQuery: {1} }}", this.SearchIndexType, this.query.ToString()); + } + + private static void ValidateIExamineValue(IExamineValue v) + { + var ev = v as ExamineValue; + if (ev == null) + { + throw new ArgumentException("IExamineValue was not created from this provider. Ensure that it is created from the ISearchCriteria this provider exposes"); + } + } + + #region ISearchCriteria Members + + public string SearchIndexType + { + get; + protected set; + } + + public bool IncludeHitCount + { + get; + set; + } + + public int TotalHits + { + get; + internal protected set; + } + + #endregion + + #region ISearch Members + + /// + /// Query on the id + /// + /// The id. + /// A new with the clause appended + public IBooleanOperation Id(int id) + { + return IdInternal(id, occurance); + } + + internal protected IBooleanOperation IdInternal(int id, BooleanClause.Occur occurance) + { + //use a query parser (which uses the analyzer) to build up the field query which we want + query.Add(this.queryParser.GetFieldQuery(LuceneExamineIndexer.IndexNodeIdFieldName, id.ToString()), occurance); + + return new LuceneBooleanOperation(this); + } + + /// + /// Query on the NodeName + /// + /// Name of the node. + /// A new with the clause appended + public IBooleanOperation NodeName(string nodeName) + { + Enforcer.ArgumentNotNull(nodeName, "nodeName"); + return NodeName(new ExamineValue(Examineness.Explicit, nodeName)); + } + + /// + /// Query on the NodeName + /// + /// Name of the node. + /// A new with the clause appended + public IBooleanOperation NodeName(IExamineValue nodeName) + { + Enforcer.ArgumentNotNull(nodeName, "nodeName"); + return this.NodeNameInternal(nodeName, occurance); + } + + internal protected IBooleanOperation NodeNameInternal(IExamineValue examineValue, BooleanClause.Occur occurance) + { + return this.FieldInternal("nodeName", examineValue, occurance); + } + + /// + /// Query on the NodeTypeAlias + /// + /// The node type alias. + /// A new with the clause appended + public IBooleanOperation NodeTypeAlias(string nodeTypeAlias) + { + Enforcer.ArgumentNotNull(nodeTypeAlias, "nodeTypeAlias"); + return this.NodeTypeAlias(new ExamineValue(Examineness.Explicit, nodeTypeAlias)); + } + + /// + /// Query on the NodeTypeAlias + /// + /// The node type alias. + /// A new with the clause appended + public IBooleanOperation NodeTypeAlias(IExamineValue nodeTypeAlias) + { + Enforcer.ArgumentNotNull(nodeTypeAlias, "nodeTypeAlias"); + return this.NodeTypeAliasInternal(nodeTypeAlias, occurance); + } + + internal protected IBooleanOperation NodeTypeAliasInternal(IExamineValue examineValue, BooleanClause.Occur occurance) + { + return this.FieldInternal("nodeTypeAlias", examineValue, occurance); + } + + /// + /// Query on the Parent ID + /// + /// The id of the parent. + /// A new with the clause appended + public IBooleanOperation ParentId(int id) + { + return this.ParentIdInternal(id, occurance); + } + + internal protected IBooleanOperation ParentIdInternal(int id, BooleanClause.Occur occurance) + { + query.Add(this.queryParser.GetFieldQuery("parentID", id.ToString()), occurance); + + return new LuceneBooleanOperation(this); + } + + /// + /// Query on the specified field + /// + /// Name of the field. + /// The field value. + /// A new with the clause appended + public IBooleanOperation Field(string fieldName, string fieldValue) + { + Enforcer.ArgumentNotNull(fieldName, "fieldName"); + Enforcer.ArgumentNotNull(fieldValue, "fieldValue"); + return this.FieldInternal(fieldName, new ExamineValue(Examineness.Explicit, fieldValue), occurance); + } + + /// + /// Query on the specified field + /// + /// Name of the field. + /// The field value. + /// A new with the clause appended + public IBooleanOperation Field(string fieldName, IExamineValue fieldValue) + { + Enforcer.ArgumentNotNull(fieldName, "fieldName"); + Enforcer.ArgumentNotNull(fieldValue, "fieldValue"); + return this.FieldInternal(fieldName, fieldValue, occurance); + } + + /// + /// Returns the Lucene query object for a field given an IExamineValue + /// + /// + /// + /// A new with the clause appended + internal protected Query GetFieldInternalQuery(string fieldName, IExamineValue fieldValue) + { + Query queryToAdd; + + switch (fieldValue.Examineness) + { + case Examineness.Fuzzy: + queryToAdd = this.queryParser.GetFuzzyQuery(fieldName, fieldValue.Value, fieldValue.Level); + break; + case Examineness.SimpleWildcard: + case Examineness.ComplexWildcard: + queryToAdd = this.queryParser.GetWildcardQuery(fieldName, fieldValue.Value); + break; + case Examineness.Boosted: + queryToAdd = this.queryParser.GetFieldQuery(fieldName, fieldValue.Value); + queryToAdd.SetBoost(fieldValue.Level); + break; + case Examineness.Proximity: + //This is how you are supposed to do this based on this doc here: + //http://lucene.apache.org/java/2_4_1/api/org/apache/lucene/search/spans/package-summary.html#package_description + //but i think that lucene.net has an issue with it's internal parser since it parses to a very strange query + //we'll just manually make it instead below + + //var spans = new List(); + //foreach (var s in fieldValue.Value.Split(' ')) + //{ + // spans.Add(new SpanTermQuery(new Term(fieldName, s))); + //} + //queryToAdd = new SpanNearQuery(spans.ToArray(), Convert.ToInt32(fieldValue.Level), true); + + var proxQuery = fieldName + ":\"" + fieldValue.Value + "\"~" + Convert.ToInt32(fieldValue.Level).ToString(); + queryToAdd = queryParser.Parse(proxQuery); + + break; + case Examineness.Explicit: + default: + queryToAdd = this.queryParser.GetFieldQuery(fieldName, fieldValue.Value); + break; + } + return queryToAdd; + } + + internal protected IBooleanOperation FieldInternal(string fieldName, IExamineValue fieldValue, BooleanClause.Occur occurance) + { + Query queryToAdd = GetFieldInternalQuery(fieldName, fieldValue); + + if (queryToAdd != null) + query.Add(queryToAdd, occurance); + + return new LuceneBooleanOperation(this); + } + + public IBooleanOperation Range(string fieldName, DateTime start, DateTime end) + { + return this.Range(fieldName, start, end, true, true); + } + + public IBooleanOperation Range(string fieldName, DateTime start, DateTime end, bool includeLower, bool includeUpper) + { + //since lucene works on string's for all searching we need to flatten the date + return this.RangeInternal(fieldName, DateTools.DateToString(start, DateTools.Resolution.MILLISECOND), DateTools.DateToString(end, DateTools.Resolution.MILLISECOND), includeLower, includeUpper, occurance); + } + + public IBooleanOperation Range(string fieldName, int start, int end) + { + Enforcer.ArgumentNotNull(fieldName, "fieldName"); + return this.Range(fieldName, start, end, true, true); + } + + public IBooleanOperation Range(string fieldName, int start, int end, bool includeLower, bool includeUpper) + { + return this.RangeInternal(fieldName, start, end, includeLower, includeUpper, occurance); + } + + protected internal IBooleanOperation RangeInternal(string fieldName, int start, int end, bool includeLower, bool includeUpper, BooleanClause.Occur occurance) + { + query.Add(NumericRangeQuery.NewIntRange(fieldName, start, end, includeLower, includeUpper), occurance); + return new LuceneBooleanOperation(this); + } + + public IBooleanOperation Range(string fieldName, string start, string end) + { + Enforcer.ArgumentNotNull(fieldName, "fieldName"); + Enforcer.ArgumentNotNull(start, "start"); + Enforcer.ArgumentNotNull(end, "end"); + return this.Range(fieldName, start, end, true, true); + } + + public IBooleanOperation Range(string fieldName, string start, string end, bool includeLower, bool includeUpper) + { + Enforcer.ArgumentNotNull(fieldName, "fieldName"); + Enforcer.ArgumentNotNull(start, "start"); + Enforcer.ArgumentNotNull(end, "end"); + return this.RangeInternal(fieldName, start, end, includeLower, includeUpper, occurance); + } + + protected internal IBooleanOperation RangeInternal(string fieldName, string start, string end, bool includeLower, bool includeUpper, BooleanClause.Occur occurance) + { + query.Add(new TermRangeQuery(fieldName, start, end, includeLower, includeUpper), occurance); + + return new LuceneBooleanOperation(this); + } + + public IBooleanOperation GroupedAnd(IEnumerable fields, params string[] query) + { + Enforcer.ArgumentNotNull(fields, "fields"); + Enforcer.ArgumentNotNull(query, "query"); + + var fieldVals = new List(); + foreach (var f in query) + { + fieldVals.Add(new ExamineValue(Examineness.Explicit, f)); + } + return this.GroupedAnd(fields.ToArray(), fieldVals.ToArray()); + } + + public IBooleanOperation GroupedAnd(IEnumerable fields, IExamineValue[] fieldVals) + { + Enforcer.ArgumentNotNull(fields, "fields"); + Enforcer.ArgumentNotNull(query, "fieldVals"); + + return this.GroupedAndInternal(fields.ToArray(), fieldVals.ToArray(), occurance); + } + + protected internal IBooleanOperation GroupedAndInternal(string[] fields, IExamineValue[] fieldVals, BooleanClause.Occur occurance) + { + + //if there's only 1 query text we want to build up a string like this: + //(+field1:query +field2:query +field3:query) + //but Lucene will bork if you provide an array of length 1 (which is != to the field length) + + query.Add(GetMultiFieldQuery(fields, fieldVals, BooleanClause.Occur.MUST), occurance); + + return new LuceneBooleanOperation(this); + } + + public IBooleanOperation GroupedOr(IEnumerable fields, params string[] query) + { + Enforcer.ArgumentNotNull(fields, "fields"); + Enforcer.ArgumentNotNull(query, "query"); + + var fieldVals = new List(); + foreach (var f in query) + { + fieldVals.Add(new ExamineValue(Examineness.Explicit, f)); + } + + return this.GroupedOr(fields.ToArray(), fieldVals.ToArray()); + } + + public IBooleanOperation GroupedOr(IEnumerable fields, params IExamineValue[] fieldVals) + { + Enforcer.ArgumentNotNull(fields, "fields"); + Enforcer.ArgumentNotNull(query, "query"); + + return this.GroupedOrInternal(fields.ToArray(), fieldVals, occurance); + } + + protected internal IBooleanOperation GroupedOrInternal(string[] fields, IExamineValue[] fieldVals, BooleanClause.Occur occurance) + { + //if there's only 1 query text we want to build up a string like this: + //(field1:query field2:query field3:query) + //but Lucene will bork if you provide an array of length 1 (which is != to the field length) + + query.Add(GetMultiFieldQuery(fields, fieldVals, BooleanClause.Occur.SHOULD), occurance); + + return new LuceneBooleanOperation(this); + } + + public IBooleanOperation GroupedNot(IEnumerable fields, params string[] query) + { + Enforcer.ArgumentNotNull(fields, "fields"); + Enforcer.ArgumentNotNull(query, "query"); + + var fieldVals = new List(); + foreach (var f in query) + { + fieldVals.Add(new ExamineValue(Examineness.Explicit, f)); + } + + return this.GroupedNot(fields.ToArray(), fieldVals.ToArray()); + } + + public IBooleanOperation GroupedNot(IEnumerable fields, params IExamineValue[] query) + { + Enforcer.ArgumentNotNull(fields, "fields"); + Enforcer.ArgumentNotNull(query, "query"); + + return this.GroupedNotInternal(fields.ToArray(), query, occurance); + } + + protected internal IBooleanOperation GroupedNotInternal(string[] fields, IExamineValue[] fieldVals, BooleanClause.Occur occurance) + { + //if there's only 1 query text we want to build up a string like this: + //(!field1:query !field2:query !field3:query) + //but Lucene will bork if you provide an array of length 1 (which is != to the field length) + + query.Add(GetMultiFieldQuery(fields, fieldVals, BooleanClause.Occur.MUST_NOT), occurance); + + return new LuceneBooleanOperation(this); + } + + /// + /// Creates our own style 'multi field query' used internal for the grouped operations + /// + /// + /// + /// + /// A new with the clause appended + protected internal BooleanQuery GetMultiFieldQuery(string[] fields, IExamineValue[] fieldVals, BooleanClause.Occur occurance) + { + //if there's only 1 query text we want to build up a string like this: + //(!field1:query !field2:query !field3:query) + //but Lucene will bork if you provide an array of length 1 (which is != to the field length) + + var queryVals = new IExamineValue[fields.Length]; + if (fieldVals.Length == 1) + { + for (int i = 0; i < queryVals.Length; i++) + queryVals[i] = fieldVals[0]; + } + else + { + queryVals = fieldVals; + } + + var qry = new BooleanQuery(); + for (int i = 0; i < fields.Length; i++) + { + qry.Add(this.GetFieldInternalQuery(fields[i], queryVals[i]), occurance); + } + + return qry; + } + + public IBooleanOperation GroupedFlexible(IEnumerable fields, IEnumerable operations, params string[] query) + { + Enforcer.ArgumentNotNull(fields, "fields"); + Enforcer.ArgumentNotNull(query, "query"); + Enforcer.ArgumentNotNull(operations, "operations"); + + var fieldVals = new List(); + foreach (var f in query) + { + fieldVals.Add(new ExamineValue(Examineness.Explicit, f)); + } + + return this.GroupedFlexible(fields.ToArray(), operations.ToArray(), fieldVals.ToArray()); + } + + public IBooleanOperation GroupedFlexible(IEnumerable fields, IEnumerable operations, params IExamineValue[] fieldVals) + { + Enforcer.ArgumentNotNull(fields, "fields"); + Enforcer.ArgumentNotNull(query, "query"); + Enforcer.ArgumentNotNull(operations, "operations"); + + return this.GroupedFlexibleInternal(fields.ToArray(), operations.ToArray(), fieldVals, occurance); + } + + protected internal IBooleanOperation GroupedFlexibleInternal(string[] fields, BooleanOperation[] operations, IExamineValue[] fieldVals, BooleanClause.Occur occurance) + { + //if there's only 1 query text we want to build up a string like this: + //(field1:query field2:query field3:query) + //but Lucene will bork if you provide an array of length 1 (which is != to the field length) + + var flags = new BooleanClause.Occur[operations.Count()]; + for (int i = 0; i < flags.Length; i++) + flags[i] = operations.ElementAt(i).ToLuceneOccurance(); + + var queryVals = new IExamineValue[fields.Length]; + if (fieldVals.Length == 1) + { + for (int i = 0; i < queryVals.Length; i++) + queryVals[i] = fieldVals[0]; + } + else + { + queryVals = fieldVals; + } + + var qry = new BooleanQuery(); + for (int i = 0; i < fields.Length; i++) + { + qry.Add(this.GetFieldInternalQuery(fields[i], queryVals[i]), flags[i]); + } + + this.query.Add(qry, occurance); + + return new LuceneBooleanOperation(this); + } + + /// + /// Passes a raw search query to the provider to handle + /// + /// The query. + /// A new with the clause appended + public ISearchCriteria RawQuery(string query) + { + this.query.Add(this.queryParser.Parse(query), this.occurance); + + return this; + } + + /// + /// Orders the results by the specified fields + /// + /// The field names. + /// A new with the clause appended + public IBooleanOperation OrderBy(params string[] fieldNames) + { + Enforcer.ArgumentNotNull(fieldNames, "fieldNames"); + + return this.OrderByInternal(false, fieldNames); + } + + /// + /// Orders the results by the specified fields in a descending order + /// + /// The field names. + /// A new with the clause appended + public IBooleanOperation OrderByDescending(params string[] fieldNames) + { + Enforcer.ArgumentNotNull(fieldNames, "fieldNames"); + + return this.OrderByInternal(true, fieldNames); + } + + /// + /// Internal operation for adding the ordered results + /// + /// if set to true [descending]. + /// The field names. + /// A new with the clause appended + protected internal IBooleanOperation OrderByInternal(bool descending, params string[] fieldNames) + { + foreach (var fieldName in fieldNames) + { + this.sortFields.Add(new SortField(LuceneExamineIndexer.SortedFieldNamePrefix + fieldName, SortField.STRING, descending)); + } + + return new LuceneBooleanOperation(this); + } + + #endregion + } +} diff --git a/src/UmbracoExamine/SearchCriteria/LuceneSearchExtensions.cs b/src/UmbracoExamine/SearchCriteria/LuceneSearchExtensions.cs new file mode 100644 index 0000000000..3681c98e47 --- /dev/null +++ b/src/UmbracoExamine/SearchCriteria/LuceneSearchExtensions.cs @@ -0,0 +1,164 @@ +using Examine.SearchCriteria; +using Lucene.Net.Search; +using Lucene.Net.QueryParsers; +using System; + +namespace UmbracoExamine.SearchCriteria +{ + public static class LuceneSearchExtensions + { + /// + /// Adds a single character wildcard to the string for Lucene wildcard matching + /// + /// The string to wildcard. + /// An IExamineValue for the required operation + /// Thrown when the string is null or empty + public static IExamineValue SingleCharacterWildcard(this string s) + { + if (System.String.IsNullOrEmpty(s)) + throw new ArgumentException("Supplied string is null or empty.", "s"); + + return new ExamineValue(Examineness.SimpleWildcard, s + "?"); + } + + /// + /// Adds a multi-character wildcard to a string for Lucene wildcard matching + /// + /// The string to wildcard. + /// An IExamineValue for the required operation + /// Thrown when the string is null or empty + public static IExamineValue MultipleCharacterWildcard(this string s) + { + if (String.IsNullOrEmpty(s)) + throw new ArgumentException("Supplied string is null or empty.", "s"); + return new ExamineValue(Examineness.ComplexWildcard, s + "*"); + } + + /// + /// Configures the string for fuzzy matching in Lucene using the default fuzziness level + /// + /// The string to configure fuzzy matching on. + /// An IExamineValue for the required operation + /// Thrown when the string is null or empty + public static IExamineValue Fuzzy(this string s) + { + return Fuzzy(s, 0.5f); + } + + /// + /// Configures the string for fuzzy matching in Lucene using the supplied fuzziness level + /// + /// The string to configure fuzzy matching on. + /// The fuzzieness level. + /// + /// An IExamineValue for the required operation + /// + /// Thrown when the string is null or empty + public static IExamineValue Fuzzy(this string s, float fuzzieness) + { + if (String.IsNullOrEmpty(s)) + throw new ArgumentException("Supplied string is null or empty.", "s"); + return new ExamineValue(Examineness.Fuzzy, s, fuzzieness); + } + + /// + /// Configures the string for boosting in Lucene + /// + /// The string to wildcard. + /// The boost level. + /// + /// An IExamineValue for the required operation + /// + /// Thrown when the string is null or empty + public static IExamineValue Boost(this string s, float boost) + { + if (String.IsNullOrEmpty(s)) + throw new ArgumentException("Supplied string is null or empty.", "s"); + return new ExamineValue(Examineness.Boosted, s + "^", boost); + } + + /// + /// Configures the string for proximity matching + /// + /// The string to wildcard. + /// The proximity level. + /// + /// An IExamineValue for the required operation + /// + /// Thrown when the string is null or empty + public static IExamineValue Proximity(this string s, int proximity) + { + if (String.IsNullOrEmpty(s)) + throw new ArgumentException("Supplied string is null or empty.", "s"); + return new ExamineValue(Examineness.Proximity, s + "~", Convert.ToSingle(proximity)); + } + + /// + /// Escapes the string within Lucene + /// + /// The string to wildcard. + /// An IExamineValue for the required operation + /// Thrown when the string is null or empty + public static IExamineValue Escape(this string s) + { + if (String.IsNullOrEmpty(s)) + throw new ArgumentException("Supplied string is null or empty.", "s"); + return new ExamineValue(Examineness.Escaped, QueryParser.Escape(s)); + } + + /// + /// Sets up an for an additional Examiness + /// + /// The IExamineValue to continue working with. + /// The string to postfix. + /// Combined strings + public static string Then(this IExamineValue examineValue, string s) + { + if (examineValue == null) + throw new ArgumentNullException("examineValue", "examineValue is null."); + if (String.IsNullOrEmpty(s)) + throw new ArgumentException("Supplied string is null or empty.", "s"); + return examineValue.Value + s; + } + + /// + /// Converts an Examine boolean operation to a Lucene representation + /// + /// The operation. + /// The translated Boolean operation + public static BooleanClause.Occur ToLuceneOccurance(this BooleanOperation o) + { + switch (o) + { + case BooleanOperation.And: + return BooleanClause.Occur.MUST; + case BooleanOperation.Not: + return BooleanClause.Occur.MUST_NOT; + case BooleanOperation.Or: + default: + return BooleanClause.Occur.SHOULD; + } + } + + /// + /// Converts a Lucene boolean occurrence to an Examine representation + /// + /// The occurrence to translate. + /// The translated boolean occurrence + public static BooleanOperation ToBooleanOperation(this BooleanClause.Occur o) + { + if (o == BooleanClause.Occur.MUST) + { + return BooleanOperation.And; + } + else if (o == BooleanClause.Occur.MUST_NOT) + { + return BooleanOperation.Not; + } + else + { + return BooleanOperation.Or; + } + } + } +} diff --git a/src/UmbracoExamine/UmbracoContentIndexer.cs b/src/UmbracoExamine/UmbracoContentIndexer.cs new file mode 100644 index 0000000000..f58bec2dad --- /dev/null +++ b/src/UmbracoExamine/UmbracoContentIndexer.cs @@ -0,0 +1,375 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Linq; +using System.Security; +using System.Text; +using System.Web; +using System.Xml.Linq; +using Examine; +using Examine.Config; +using Examine.Providers; +using umbraco.cms.businesslogic; +using UmbracoExamine.DataServices; +using Examine.LuceneEngine; +using Examine.LuceneEngine.Config; +using UmbracoExamine.Config; +using Examine.LuceneEngine.Providers; +using Lucene.Net.Analysis; +using umbraco.BasePages; + + +namespace UmbracoExamine +{ + /// + /// + /// + public class UmbracoContentIndexer : BaseUmbracoIndexer + { + #region Constructors + + /// + /// Default constructor + /// + public UmbracoContentIndexer() + : base() { } + + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + /// + /// + [SecuritySafeCritical] + public UmbracoContentIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, Analyzer analyzer, bool async) + : base(indexerData, indexPath, dataService, analyzer, async) { } + + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + /// + /// + /// + [SecuritySafeCritical] + public UmbracoContentIndexer(IIndexCriteria indexerData, Lucene.Net.Store.Directory luceneDirectory, IDataService dataService, Analyzer analyzer, bool async) + : base(indexerData, luceneDirectory, dataService, analyzer, async) { } + + #endregion + + #region Constants & Fields + + /// + /// Used to store the path of a content object + /// + public const string IndexPathFieldName = "__Path"; + public const string NodeTypeAliasFieldName = "__NodeTypeAlias"; + + /// + /// A type that defines the type of index for each Umbraco field (non user defined fields) + /// Alot of standard umbraco fields shouldn't be tokenized or even indexed, just stored into lucene + /// for retreival after searching. + /// + internal static readonly Dictionary IndexFieldPolicies + = new Dictionary() + { + { "id", FieldIndexTypes.NOT_ANALYZED}, + { "version", FieldIndexTypes.NOT_ANALYZED}, + { "parentID", FieldIndexTypes.NOT_ANALYZED}, + { "level", FieldIndexTypes.NOT_ANALYZED}, + { "writerID", FieldIndexTypes.NOT_ANALYZED}, + { "creatorID", FieldIndexTypes.NOT_ANALYZED}, + { "nodeType", FieldIndexTypes.NOT_ANALYZED}, + { "template", FieldIndexTypes.NOT_ANALYZED}, + { "sortOrder", FieldIndexTypes.NOT_ANALYZED}, + { "createDate", FieldIndexTypes.NOT_ANALYZED}, + { "updateDate", FieldIndexTypes.NOT_ANALYZED}, + { "nodeName", FieldIndexTypes.ANALYZED}, + { "urlName", FieldIndexTypes.NOT_ANALYZED}, + { "writerName", FieldIndexTypes.ANALYZED}, + { "creatorName", FieldIndexTypes.ANALYZED}, + { "nodeTypeAlias", FieldIndexTypes.ANALYZED}, + { "path", FieldIndexTypes.NOT_ANALYZED} + }; + + #endregion + + #region Initialize + + /// + /// Set up all properties for the indexer based on configuration information specified. This will ensure that + /// all of the folders required by the indexer are created and exist. This will also create an instruction + /// file declaring the computer name that is part taking in the indexing. This file will then be used to + /// determine the master indexer machine in a load balanced environment (if one exists). + /// + /// The friendly name of the provider. + /// A collection of the name/value pairs representing the provider-specific attributes specified in the configuration for this provider. + /// + /// The name of the provider is null. + /// + /// + /// The name of the provider has a length of zero. + /// + /// + /// An attempt is made to call on a provider after the provider has already been initialized. + /// + public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) + { + + //check if there's a flag specifying to support unpublished content, + //if not, set to false; + bool supportUnpublished; + if (config["supportUnpublished"] != null && bool.TryParse(config["supportUnpublished"], out supportUnpublished)) + SupportUnpublishedContent = supportUnpublished; + else + SupportUnpublishedContent = false; + + + //check if there's a flag specifying to support protected content, + //if not, set to false; + bool supportProtected; + if (config["supportProtected"] != null && bool.TryParse(config["supportProtected"], out supportProtected)) + SupportProtectedContent = supportProtected; + else + SupportProtectedContent = false; + + + base.Initialize(name, config); + } + + #endregion + + #region Properties + + /// + /// By default this is false, if set to true then the indexer will include indexing content that is flagged as publicly protected. + /// This property is ignored if SupportUnpublishedContent is set to true. + /// + public bool SupportProtectedContent { get; protected internal set; } + + protected override IEnumerable SupportedTypes + { + get + { + return new string[] { IndexTypes.Content, IndexTypes.Media }; + } + } + + #endregion + + #region Event handlers + + protected override void OnIndexingError(IndexingErrorEventArgs e) + { + DataService.LogService.AddErrorLog(e.NodeId, string.Format("{0},{1}, IndexSet: {2}", e.Message, e.InnerException != null ? e.InnerException.Message : "", this.IndexSetName)); + base.OnIndexingError(e); + } + + //protected override void OnDocumentWriting(DocumentWritingEventArgs docArgs) + //{ + // DataService.LogService.AddVerboseLog(docArgs.NodeId, string.Format("({0}) DocumentWriting event for node ({1})", this.Name, LuceneIndexFolder.FullName)); + // base.OnDocumentWriting(docArgs); + //} + + protected override void OnNodeIndexed(IndexedNodeEventArgs e) + { + DataService.LogService.AddVerboseLog(e.NodeId, string.Format("Index created for node")); + base.OnNodeIndexed(e); + } + + protected override void OnIndexDeleted(DeleteIndexEventArgs e) + { + DataService.LogService.AddVerboseLog(-1, string.Format("Index deleted for term: {0} with value {1}", e.DeletedTerm.Key, e.DeletedTerm.Value)); + base.OnIndexDeleted(e); + } + + protected override void OnIndexOptimizing(EventArgs e) + { + DataService.LogService.AddInfoLog(-1, string.Format("Index is being optimized")); + base.OnIndexOptimizing(e); + } + + #endregion + + #region Public methods + + + /// + /// Overridden for logging + /// + /// + /// + public override void ReIndexNode(XElement node, string type) + { + if (!SupportedTypes.Contains(type)) + return; + + DataService.LogService.AddVerboseLog((int)node.Attribute("id"), string.Format("ReIndexNode with type: {0}", type)); + base.ReIndexNode(node, type); + } + + /// + /// Deletes a node from the index. + /// + /// + /// When a content node is deleted, we also need to delete it's children from the index so we need to perform a + /// custom Lucene search to find all decendents and create Delete item queues for them too. + /// + /// ID of the node to delete + public override void DeleteFromIndex(string nodeId) + { + //find all descendants based on path + var descendantPath = string.Format(@"\-1\,*{0}\,*", nodeId); + var rawQuery = string.Format("{0}:{1}", IndexPathFieldName, descendantPath); + var c = InternalSearcher.CreateSearchCriteria(); + var filtered = c.RawQuery(rawQuery); + var results = InternalSearcher.Search(filtered); + + DataService.LogService.AddVerboseLog(int.Parse(nodeId), string.Format("DeleteFromIndex with query: {0} (found {1} results)", rawQuery, results.Count())); + + //need to create a delete queue item for each one found + foreach (var r in results) + { + EnqueueIndexOperation(new IndexOperation() + { + Operation = IndexOperationType.Delete, + Item = new IndexItem(null, "", r.Id.ToString()) + }); + //SaveDeleteIndexQueueItem(new KeyValuePair(IndexNodeIdFieldName, r.Id.ToString())); + } + + base.DeleteFromIndex(nodeId); + } + #endregion + + #region Protected + + + + /// + /// Overridden for logging. + /// + /// + /// + protected override void AddSingleNodeToIndex(XElement node, string type) + { + DataService.LogService.AddVerboseLog((int)node.Attribute("id"), string.Format("AddSingleNodeToIndex with type: {0}", type)); + base.AddSingleNodeToIndex(node, type); + } + + public override void RebuildIndex() + { + DataService.LogService.AddVerboseLog(-1, "Rebuilding index"); + base.RebuildIndex(); + } + + /// + /// Override this method to strip all html from all user fields before raising the event, then after the event + /// ensure our special Path field is added to the collection + /// + /// + protected override void OnGatheringNodeData(IndexingNodeDataEventArgs e) + { + //strip html of all users fields + // Get all user data that we want to index and store into a dictionary + foreach (var field in IndexerData.UserFields) + { + if (e.Fields.ContainsKey(field.Name)) + { + e.Fields[field.Name] = DataService.ContentService.StripHtml(e.Fields[field.Name]); + } + } + + base.OnGatheringNodeData(e); + + //ensure the special path and node type alis fields is added to the dictionary to be saved to file + var path = e.Node.Attribute("path").Value; + if (!e.Fields.ContainsKey(IndexPathFieldName)) + e.Fields.Add(IndexPathFieldName, path); + + //this needs to support both schemas so get the nodeTypeAlias if it exists, otherwise the name + var nodeTypeAlias = e.Node.Attribute("nodeTypeAlias") == null ? e.Node.Name.LocalName : e.Node.Attribute("nodeTypeAlias").Value; + if (!e.Fields.ContainsKey(NodeTypeAliasFieldName)) + e.Fields.Add(NodeTypeAliasFieldName, nodeTypeAlias); + } + + /// + /// Called when a duplicate field is detected in the dictionary that is getting indexed. + /// + /// + /// + /// + protected override void OnDuplicateFieldWarning(int nodeId, string indexSetName, string fieldName) + { + base.OnDuplicateFieldWarning(nodeId, indexSetName, fieldName); + + this.DataService.LogService.AddInfoLog(nodeId, "Field \"" + fieldName + "\" is listed multiple times in the index set \"" + indexSetName + "\". Please ensure all names are unique"); + } + + /// + /// Overridden to add the path property to the special fields to index + /// + /// + /// + protected override Dictionary GetSpecialFieldsToIndex(Dictionary allValuesForIndexing) + { + var fields = base.GetSpecialFieldsToIndex(allValuesForIndexing); + + //adds the special path property to the index + fields.Add(IndexPathFieldName, allValuesForIndexing[IndexPathFieldName]); + + //adds the special node type alias property to the index + fields.Add(NodeTypeAliasFieldName, allValuesForIndexing[NodeTypeAliasFieldName]); + + return fields; + + } + + /// + /// Creates an IIndexCriteria object based on the indexSet passed in and our DataService + /// + /// + /// + protected override IIndexCriteria GetIndexerData(IndexSet indexSet) + { + return indexSet.ToIndexCriteria(DataService); + } + + /// + /// return the index policy for the field name passed in, if not found, return normal + /// + /// + /// + protected override FieldIndexTypes GetPolicy(string fieldName) + { + var def = IndexFieldPolicies.Where(x => x.Key == fieldName); + return (def.Count() == 0 ? FieldIndexTypes.ANALYZED : def.Single().Value); + } + + /// + /// Ensure that the content of this node is available for indexing (i.e. don't allow protected + /// content to be indexed when this is disabled). + /// + /// + protected override bool ValidateDocument(XElement node) + { + var nodeId = int.Parse(node.Attribute("id").Value); + // Test for access if we're only indexing published content + // return nothing if we're not supporting protected content and it is protected, and we're not supporting unpublished content + if (!SupportUnpublishedContent + && (!SupportProtectedContent + && DataService.ContentService.IsProtected(nodeId, node.Attribute("path").Value))) + { + return false; + } + + return base.ValidateDocument(node); + } + + #endregion + } +} diff --git a/src/UmbracoExamine/UmbracoEventManager.cs b/src/UmbracoExamine/UmbracoEventManager.cs new file mode 100644 index 0000000000..cebb837670 --- /dev/null +++ b/src/UmbracoExamine/UmbracoEventManager.cs @@ -0,0 +1,223 @@ +using System; +using System.Linq; +using System.Net; +using System.Security; +using System.Threading; +using System.Web; +using Examine.Config; +using umbraco; +using umbraco.BusinessLogic; +using umbraco.cms.businesslogic; +using umbraco.cms.businesslogic.media; +using umbraco.cms.businesslogic.web; +using Examine; +using Examine.Providers; +using umbraco.cms.businesslogic.member; +using Examine.LuceneEngine; +using Examine.LuceneEngine.Providers; +using Lucene.Net.Index; +using Lucene.Net.Store; + +namespace UmbracoExamine +{ + /// + /// An instance for wiring up Examine to the Umbraco events system + /// + public class UmbracoEventManager : ApplicationBase + { + /// + /// Creates a new instance of the class + /// + [SecuritySafeCritical] + public UmbracoEventManager() + { + var registeredProviders = ExamineManager.Instance.IndexProviderCollection.OfType() + .Where(x => x.EnableDefaultEventHandler) + .Count(); + + Log.Add(LogTypes.Custom, -1, "[UmbracoExamine] Adding examine event handlers for index providers: " + registeredProviders.ToString()); + + //don't bind event handlers if we're not suppose to listen + if (registeredProviders == 0) + return; + + Media.AfterSave += Media_AfterSave; + Media.AfterDelete += Media_AfterDelete; + CMSNode.AfterMove += Media_AfterMove; + + //These should only fire for providers that DONT have SupportUnpublishedContent set to true + content.AfterUpdateDocumentCache += content_AfterUpdateDocumentCache; + content.AfterClearDocumentCache += content_AfterClearDocumentCache; + + //These should only fire for providers that have SupportUnpublishedContent set to true + Document.AfterSave += Document_AfterSave; + Document.AfterDelete += Document_AfterDelete; + + Member.AfterSave += Member_AfterSave; + Member.AfterDelete += Member_AfterDelete; + } + + //This does work, however we need to lock down the httphandler and thats an issue... so i've removed this + //if people don't want to rebuild on app startup then they can disable it and reindex manually. + + [SecuritySafeCritical] + private void Member_AfterSave(Member sender, SaveEventArgs e) + { + //ensure that only the providers are flagged to listen execute + var xml = sender.ToXml(new System.Xml.XmlDocument(), false).ToXElement(); + var providers = ExamineManager.Instance.IndexProviderCollection.OfType() + .Where(x => x.EnableDefaultEventHandler); + ExamineManager.Instance.ReIndexNode(xml, IndexTypes.Member, providers); + } + + [SecuritySafeCritical] + private void Member_AfterDelete(Member sender, DeleteEventArgs e) + { + var nodeId = sender.Id.ToString(); + + //ensure that only the providers are flagged to listen execute + ExamineManager.Instance.DeleteFromIndex(nodeId, + ExamineManager.Instance.IndexProviderCollection.OfType() + .Where(x => x.EnableDefaultEventHandler)); + } + + /// + /// Only index using providers that SupportUnpublishedContent + /// + /// + /// + [SecuritySafeCritical] + private void Document_AfterSave(Document sender, SaveEventArgs e) + { + //ensure that only the providers that have unpublishing support enabled + //that are also flagged to listen + + //there's a bug in 4.0.x that fires the Document saving event handler for media when media is moved, + //therefore, we need to try to figure out if this is media or content. Currently one way to do this + //is by checking the creator ID property as it will be null if it is media. We then need to try to + //create the media object, see if it exists, and pass it to the media save event handler... yeah i know, + //pretty horrible but has to be done. + + try + { + var creator = sender.Creator; + if (creator != null) + { + //it's def a Document + ExamineManager.Instance.ReIndexNode(sender.ToXDocument(false).Root, IndexTypes.Content, + ExamineManager.Instance.IndexProviderCollection.OfType() + .Where(x => x.SupportUnpublishedContent + && x.EnableDefaultEventHandler)); + + return; //exit, we've indexed the content + } + } + catch (Exception) + { + //if we get this exception, it means it's most likely media, so well do our check next. + } + + //this is most likely media, not sure what kind of exception might get thrown in 4.0.x or 4.1 if accessing a null + //creator, so we catch everything. + + var m = new Media(sender.Id); + if (!string.IsNullOrEmpty(m.Path)) + { + //this is a media item, no exception occurred on access to path and it's not empty which means it was found + Media_AfterSave(m, e); + return; + } + + + } + + /// + /// Only remove indexes using providers that SupportUnpublishedContent + /// + /// + /// + [SecuritySafeCritical] + private void Document_AfterDelete(Document sender, DeleteEventArgs e) + { + var nodeId = sender.Id.ToString(); + + //ensure that only the providers that have unpublishing support enabled + //that are also flagged to listen + ExamineManager.Instance.DeleteFromIndex(nodeId, + ExamineManager.Instance.IndexProviderCollection.OfType() + .Where(x => x.SupportUnpublishedContent + && x.EnableDefaultEventHandler)); + } + + [SecuritySafeCritical] + private void Media_AfterDelete(Media sender, DeleteEventArgs e) + { + var nodeId = sender.Id.ToString(); + + //ensure that only the providers are flagged to listen execute + ExamineManager.Instance.DeleteFromIndex(nodeId, + ExamineManager.Instance.IndexProviderCollection.OfType() + .Where(x => x.EnableDefaultEventHandler)); + } + + private void Media_AfterSave(Media sender, umbraco.cms.businesslogic.SaveEventArgs e) + { + //ensure that only the providers are flagged to listen execute + IndexMedia(sender); + } + + private void IndexMedia(Media sender) + { + ExamineManager.Instance.ReIndexNode(sender.ToXDocument(true).Root, IndexTypes.Media, + ExamineManager.Instance.IndexProviderCollection.OfType() + .Where(x => x.EnableDefaultEventHandler)); + } + + /// + /// When media is moved, re-index + /// + /// + /// + private void Media_AfterMove(object sender, MoveEventArgs e) + { + if (sender is Media) + { + Media m = (Media)sender; + IndexMedia(m); + } + } + + /// + /// Only Update indexes for providers that dont SupportUnpublishedContent + /// + /// + /// + private void content_AfterUpdateDocumentCache(Document sender, umbraco.cms.businesslogic.DocumentCacheEventArgs e) + { + //ensure that only the providers that have DONT unpublishing support enabled + //that are also flagged to listen + ExamineManager.Instance.ReIndexNode(sender.ToXDocument(true).Root, IndexTypes.Content, + ExamineManager.Instance.IndexProviderCollection.OfType() + .Where(x => !x.SupportUnpublishedContent + && x.EnableDefaultEventHandler)); + } + + /// + /// Only update indexes for providers that don't SupportUnpublishedContnet + /// + /// + /// + [SecuritySafeCritical] + private void content_AfterClearDocumentCache(Document sender, DocumentCacheEventArgs e) + { + var nodeId = sender.Id.ToString(); + //ensure that only the providers that DONT have unpublishing support enabled + //that are also flagged to listen + ExamineManager.Instance.DeleteFromIndex(nodeId, + ExamineManager.Instance.IndexProviderCollection.OfType() + .Where(x => !x.SupportUnpublishedContent + && x.EnableDefaultEventHandler)); + } + + } +} diff --git a/src/UmbracoExamine/UmbracoExamine.csproj b/src/UmbracoExamine/UmbracoExamine.csproj new file mode 100644 index 0000000000..108e745f4f --- /dev/null +++ b/src/UmbracoExamine/UmbracoExamine.csproj @@ -0,0 +1,179 @@ + + + + Debug + AnyCPU + 9.0.21022 + 2.0 + {07FBC26B-2927-4A22-8D96-D644C667FECC} + Library + Properties + UmbracoExamine + UmbracoExamine + v4.0 + 512 + + + + + + + + + + + + + 3.5 + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + ..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + SecurityRules.ruleset + false + + + false + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + AllRules.ruleset + bin\Release\UmbracoExamine.XML + false + + + false + + + true + + + ..\Solution Items\TheFARM-Public.snk + + + + ..\packages\Examine.0.1.42.2941\lib\Examine.dll + + + False + ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll + + + False + ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll + + + + + 3.5 + + + + + + 3.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + true + + + False + Windows Installer 3.1 + true + + + + + {e469a9ce-1bec-423f-ac44-713cd72457ea} + umbraco.businesslogic + + + {ccd75ec3-63db-4184-b49d-51c1dd337230} + umbraco.cms + + + {c7cb79f0-1c97-4b33-bfa7-00731b579ae2} + umbraco.datalayer + + + {511f6d8d-7717-440a-9a57-a507e9a8b27f} + umbraco.interfaces + + + {651e1350-91b6-44b7-bd60-7207006d7003} + Umbraco.Web + + + + + + + + + \ No newline at end of file diff --git a/src/UmbracoExamine/UmbracoExamineSearcher.cs b/src/UmbracoExamine/UmbracoExamineSearcher.cs new file mode 100644 index 0000000000..5fe5d5ba87 --- /dev/null +++ b/src/UmbracoExamine/UmbracoExamineSearcher.cs @@ -0,0 +1,83 @@ +using System; +using System.IO; +using System.Linq; +using System.Security; +using Examine; +using Examine.Providers; +using Examine.SearchCriteria; +using UmbracoExamine.Config; +using Examine.LuceneEngine; +using Examine.LuceneEngine.Providers; +using Examine.LuceneEngine.SearchCriteria; +using Lucene.Net.Analysis; + + +namespace UmbracoExamine +{ + /// + /// An Examine searcher which uses Lucene.Net as the + /// + public class UmbracoExamineSearcher : LuceneSearcher + { + + #region Constructors + + /// + /// Default constructor + /// + public UmbracoExamineSearcher() + : base() + { + } + + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + [SecuritySafeCritical] + public UmbracoExamineSearcher(DirectoryInfo indexPath, Analyzer analyzer) + : base(indexPath, analyzer) + { + } + + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + [SecuritySafeCritical] + public UmbracoExamineSearcher(Lucene.Net.Store.Directory luceneDirectory, Analyzer analyzer) + : base(luceneDirectory, analyzer) + { + } + + #endregion + + /// + /// Override in order to set the nodeTypeAlias field name of the underlying SearchCriteria to __NodeTypeAlias + /// + /// + /// + /// + public override ISearchCriteria CreateSearchCriteria(string type, BooleanOperation defaultOperation) + { + var criteria = base.CreateSearchCriteria(type, defaultOperation) as LuceneSearchCriteria; + criteria.NodeTypeAliasField = UmbracoContentIndexer.NodeTypeAliasFieldName; + return criteria; + } + + /// + /// Returns a list of fields to search on, this will also exclude the IndexPathFieldName and node type alias + /// + /// + protected internal override string[] GetSearchFields() + { + var fields = base.GetSearchFields(); + return fields + .Where(x => x != UmbracoContentIndexer.IndexPathFieldName) + .Where(x => x != UmbracoContentIndexer.NodeTypeAliasFieldName) + .ToArray(); + } + } +} diff --git a/src/UmbracoExamine/UmbracoMemberIndexer.cs b/src/UmbracoExamine/UmbracoMemberIndexer.cs new file mode 100644 index 0000000000..94a82b9818 --- /dev/null +++ b/src/UmbracoExamine/UmbracoMemberIndexer.cs @@ -0,0 +1,87 @@ +using System.Collections; +using System.Linq; +using System.Security; +using System.Xml.Linq; +using System.Xml.XPath; +using umbraco.cms.businesslogic.member; +using Examine.LuceneEngine; +using System.Collections.Generic; +using Examine; +using System.IO; +using UmbracoExamine.DataServices; +using Lucene.Net.Analysis; + +namespace UmbracoExamine +{ + /// + /// + /// + public class UmbracoMemberIndexer : UmbracoContentIndexer + { + + /// + /// Default constructor + /// + public UmbracoMemberIndexer() + : base() { } + + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + /// + /// + [SecuritySafeCritical] + public UmbracoMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, Analyzer analyzer, bool async) + : base(indexerData, indexPath, dataService, analyzer, async) { } + + /// + /// The supported types for this indexer + /// + protected override IEnumerable SupportedTypes + { + get + { + return new string[] { IndexTypes.Member }; + } + } + + [SecuritySafeCritical] + protected override XDocument GetXDocument(string xPath, string type) + { + if (type == IndexTypes.Member) + { + Member[] rootMembers = Member.GetAll; + var xmlMember = XDocument.Parse(""); + foreach (Member member in rootMembers) + { + xmlMember.Root.Add(GetMemberItem(member.Id)); + } + var result = ((IEnumerable)xmlMember.XPathEvaluate(xPath)).Cast(); + return result.ToXDocument(); + } + + return null; + } + + protected override System.Collections.Generic.Dictionary GetDataToIndex(XElement node, string type) + { + var data = base.GetDataToIndex(node, type); + + if (data.ContainsKey("email")) + { + data.Add("__email",data["email"].Replace("."," ").Replace("@"," ")); + } + + return data; + } + + [SecuritySafeCritical] + private XElement GetMemberItem(int nodeId) + { + var nodes = umbraco.library.GetMember(nodeId); + return XElement.Parse(nodes.Current.OuterXml); + } + } +} diff --git a/src/UmbracoExamine/XsltExtensions.cs b/src/UmbracoExamine/XsltExtensions.cs new file mode 100644 index 0000000000..7944db42da --- /dev/null +++ b/src/UmbracoExamine/XsltExtensions.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using System.Xml.XPath; +using Examine; +using Examine.LuceneEngine.Providers; +using Examine.LuceneEngine.SearchCriteria; +using Examine.SearchCriteria; +using Examine.Providers; +using UmbracoExamine.DataServices; + +namespace UmbracoExamine +{ + /// + /// Methods to support Umbraco XSLT extensions. + /// + /// + /// XSLT extensions will ONLY work for provider that have a base class of BaseUmbracoIndexer + /// + public class XsltExtensions + { + /// + /// Uses the provider specified to search, returning an XPathNodeIterator + /// + /// + /// + /// + /// + /// + internal static XPathNodeIterator Search(string searchText, bool useWildcards, LuceneSearcher provider, string indexType) + { + if (provider == null) throw new ArgumentNullException("provider"); + + var results = provider.Search(searchText, useWildcards, indexType); + return GetResultsAsXml(results); + } + + /// + /// Uses the provider specified to search, returning an XPathNodeIterator + /// + /// The search text. + /// if set to true [use wildcards]. + /// Name of the provider. + /// Type of the index. + /// + public static XPathNodeIterator Search(string searchText, bool useWildcards, string providerName, string indexType) + { + EnsureProvider(ExamineManager.Instance.SearchProviderCollection[providerName]); + + var provider = ExamineManager.Instance.SearchProviderCollection[providerName] as LuceneSearcher; + + return Search(searchText, useWildcards, provider, indexType); + } + + /// + /// Uses the provider specified to search, returning an XPathNodeIterator + /// + /// + /// + /// + /// + public static XPathNodeIterator Search(string searchText, bool useWildcards, string providerName) + { + return Search(searchText, useWildcards, providerName, string.Empty); + } + + /// + /// Uses the default provider specified to search, returning an XPathNodeIterator + /// + /// The search query + /// Enable a wildcard search query + /// A node-set of the search results + public static XPathNodeIterator Search(string searchText, bool useWildcards) + { + return Search(searchText, useWildcards, ExamineManager.Instance.DefaultSearchProvider.Name); + } + + /// + /// Uses the default provider specified to search, returning an XPathNodeIterator + /// + /// The search query + /// A node-set of the search results + public static XPathNodeIterator Search(string searchText) + { + return Search(searchText, true); + } + + /// + /// Will perform a search against the media index type only + /// + /// + /// + /// + /// + public static XPathNodeIterator SearchMediaOnly(string searchText, bool useWildcards, string providerName) + { + return Search(searchText, useWildcards, providerName, IndexTypes.Media); + } + + /// + /// Will perform a search against the media index type only + /// + /// + /// + /// + public static XPathNodeIterator SearchMediaOnly(string searchText, bool useWildcards) + { + return SearchMediaOnly(searchText, useWildcards, ExamineManager.Instance.DefaultSearchProvider.Name); + } + + /// + /// Will perform a search against the media index type only + /// + /// + /// + public static XPathNodeIterator SearchMediaOnly(string searchText) + { + return SearchMediaOnly(searchText, true); + } + + /// + /// Searches the member only. + /// + /// The search text. + /// if set to true [use wildcards]. + /// Name of the provider. + /// + public static XPathNodeIterator SearchMemberOnly(string searchText, bool useWildcards, string providerName) + { + return Search(searchText, useWildcards, providerName, IndexTypes.Member); + } + + /// + /// Searches the member only. + /// + /// The search text. + /// if set to true [use wildcards]. + /// + public static XPathNodeIterator SearchMemberOnly(string searchText, bool useWildcards) + { + return SearchMemberOnly(searchText, useWildcards, ExamineManager.Instance.DefaultSearchProvider.Name); + } + + /// + /// Searches the member only. + /// + /// The search text. + /// + public static XPathNodeIterator SearchMemberOnly(string searchText) + { + return SearchMemberOnly(searchText, true); + } + + /// + /// Will perform a search against the content index type only + /// + /// + /// + /// + /// + public static XPathNodeIterator SearchContentOnly(string searchText, bool useWildcards, string providerName) + { + return Search(searchText, useWildcards, providerName, IndexTypes.Content); + } + + + /// + /// Will perform a search against the content index type only + /// + /// + /// + /// + public static XPathNodeIterator SearchContentOnly(string searchText, bool useWildcards) + { + return SearchContentOnly(searchText, useWildcards, ExamineManager.Instance.DefaultSearchProvider.Name); + } + + /// + /// Will perform a search against the content index type only + /// + /// + /// + public static XPathNodeIterator SearchContentOnly(string searchText) + { + return SearchContentOnly(searchText, true); + } + + /// + /// Ensures the provider. + /// + /// The provider. + private static void EnsureProvider(BaseSearchProvider p) + { + if (!(p is LuceneSearcher)) + { + throw new NotSupportedException("XSLT Extensions are only support for providers of type LuceneSearcher"); + } + } + + /// + /// Gets the results as XML. + /// + /// The results. + /// + private static XPathNodeIterator GetResultsAsXml(ISearchResults results) + { + // create the XDocument + XDocument doc = new XDocument(); + + // check there are any search results + if (results.TotalItemCount > 0) + { + // create the root element + XElement root = new XElement("nodes"); + + // iterate through the search results + foreach (SearchResult result in results) + { + // create a new element + XElement node = new XElement("node"); + + // create the @id attribute + XAttribute nodeId = new XAttribute("id", result.Id); + + // create the @score attribute + XAttribute nodeScore = new XAttribute("score", result.Score); + + // add the content + node.Add(nodeId, nodeScore); + + foreach (KeyValuePair field in result.Fields) + { + // create a new element + XElement data = new XElement("data"); + + // create the @alias attribute + XAttribute alias = new XAttribute("alias", field.Key); + + // assign the value to a CDATA section + XCData value = new XCData(field.Value); + + // append the content + data.Add(alias, value); + + // append the element + node.Add(data); + } + + // add the node + root.Add(node); + } + + // add the root node + doc.Add(root); + } + else + { + doc.Add(new XElement("error", "There were no search results.")); + } + + return doc.CreateNavigator().Select("/"); + } + } +} \ No newline at end of file diff --git a/src/UmbracoExamine/packages.config b/src/UmbracoExamine/packages.config new file mode 100644 index 0000000000..0aa01cdb4a --- /dev/null +++ b/src/UmbracoExamine/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/packages/repositories.config b/src/packages/repositories.config index f811520cac..dc0774dd5e 100644 --- a/src/packages/repositories.config +++ b/src/packages/repositories.config @@ -12,4 +12,5 @@ + \ No newline at end of file diff --git a/src/umbraco.sln b/src/umbraco.sln index 87674b9000..2e4bfa86e6 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -59,6 +59,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{66AB04 .nuget\NuGet.targets = .nuget\NuGet.targets EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UmbracoExamine", "UmbracoExamine\UmbracoExamine.csproj", "{07FBC26B-2927-4A22-8D96-D644C667FECC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UmbracoExamineLibs", "UmbracoExamineLibs", "{DD32977B-EF54-475B-9A1B-B97A502C6E58}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -125,11 +129,16 @@ Global {5D3B8245-ADA6-453F-A008-50ED04BFE770}.Debug|Any CPU.Build.0 = Debug|Any CPU {5D3B8245-ADA6-453F-A008-50ED04BFE770}.Release|Any CPU.ActiveCfg = Release|Any CPU {5D3B8245-ADA6-453F-A008-50ED04BFE770}.Release|Any CPU.Build.0 = Release|Any CPU + {07FBC26B-2927-4A22-8D96-D644C667FECC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07FBC26B-2927-4A22-8D96-D644C667FECC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07FBC26B-2927-4A22-8D96-D644C667FECC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07FBC26B-2927-4A22-8D96-D644C667FECC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {5D3B8245-ADA6-453F-A008-50ED04BFE770} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} + {07FBC26B-2927-4A22-8D96-D644C667FECC} = {DD32977B-EF54-475B-9A1B-B97A502C6E58} EndGlobalSection EndGlobal