diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index 4479ebd3b6..2ee979cdf6 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -456,6 +456,41 @@ namespace Umbraco.Core.Models return ApplicationContext.Current.Services.ContentService.HasPublishedVersion(content.Id); } + /// + /// Creates the full xml representation for the object and all of it's descendants + /// + /// to generate xml for + /// Xml representation of the passed in + internal static XElement ToDeepXml(this IContent content) + { + var xml = content.ToXml(); + + var descendants = content.Descendants().ToArray(); + var currentChildren = descendants.Where(x => x.ParentId == content.Id); + AddChildXml(descendants, currentChildren, xml); + + return xml; + } + + private static void AddChildXml( + IContent[] originalDescendants, + IEnumerable currentChildren, + XElement currentXml) + { + foreach (var child in currentChildren) + { + //add the child's xml + var childXml = child.ToXml(); + currentXml.Add(childXml); + //copy local (out of closure) + var c = child; + //get this item's children + var children = originalDescendants.Where(x => x.ParentId == c.Id); + //recurse and add it's children to the child xml element + AddChildXml(originalDescendants, children, childXml); + } + } + /// /// Creates the xml representation for the object /// diff --git a/src/Umbraco.Web.UI/umbraco/dashboard/ExamineManagement.ascx b/src/Umbraco.Web.UI/umbraco/dashboard/ExamineManagement.ascx index 080041b7c2..3c49b20c10 100644 --- a/src/Umbraco.Web.UI/umbraco/dashboard/ExamineManagement.ascx +++ b/src/Umbraco.Web.UI/umbraco/dashboard/ExamineManagement.ascx @@ -45,15 +45,19 @@
- Index info & tools -
+ Index info & tools +
- - -
+
+ + +
+
+ The process is taking longer than expected, check the umbraco log to see if there have been any errors during this operation +
@@ -152,13 +156,57 @@
+ +
+ +
+ Search tools +
+ Hide search results + + + + + + +
+
+ +
+
+ + + + + + + + + + + + + + +
ScoreIdValues
+ + +
+
+
+
- - - - - -
+
+ Provider properties + + + + + +
+
+ +
diff --git a/src/Umbraco.Web.UI/umbraco/dashboard/ExamineManagement.ascx.designer.cs b/src/Umbraco.Web.UI/umbraco/dashboard/ExamineManagement.ascx.designer.cs index 9a6f8d4804..71251692aa 100644 --- a/src/Umbraco.Web.UI/umbraco/dashboard/ExamineManagement.ascx.designer.cs +++ b/src/Umbraco.Web.UI/umbraco/dashboard/ExamineManagement.ascx.designer.cs @@ -47,5 +47,14 @@ namespace Umbraco.Web.UI.Umbraco.Dashboard { /// To modify move field declaration from designer file to code-behind file. /// protected global::Umbraco.Web.UI.Controls.ProgressBar ProgressBar1; + + /// + /// ProgressBar2 control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::Umbraco.Web.UI.Controls.ProgressBar ProgressBar2; } } diff --git a/src/Umbraco.Web.UI/umbraco_client/Dashboards/ExamineManagement.css b/src/Umbraco.Web.UI/umbraco_client/Dashboards/ExamineManagement.css index b08dab3322..e20dea8189 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Dashboards/ExamineManagement.css +++ b/src/Umbraco.Web.UI/umbraco_client/Dashboards/ExamineManagement.css @@ -1,8 +1,10 @@ -#examineManagement .provider { +#examineManagement .provider +{ padding-top: 3px; } -#examineManagement .propertyPane { +#examineManagement .propertyPane +{ padding: 5px; } @@ -12,46 +14,111 @@ color: #333; width: 200px; } -#examineManagement table td { + +#examineManagement table td +{ color: #999; } -#examineManagement table td.highlight { - font-style: italic; -} + #examineManagement table td.highlight + { + font-style: italic; + } #examineManagement .propertyPane > a -{ +{ padding: 3px; display: block; } -#examineManagement .propertyPane > a.expanded -{ - border-bottom: 1px solid #CCC; - margin-bottom: 5px; - padding-bottom: 5px; -} + #examineManagement .propertyPane > a.expanded + { + border-bottom: 1px solid #CCC; + margin-bottom: 5px; + padding-bottom: 5px; + } -#examineManagement .provider > a { +#examineManagement .provider > a +{ padding: 7px; display: block; font-size: 1.1em; } -#examineManagement .provider > div { +#examineManagement .provider > div +{ margin-left: 15px; } -#examineManagement a { +#examineManagement a +{ text-decoration: none; } -#examineManagement a:hover { - text-decoration: underline; + + #examineManagement a:hover + { + text-decoration: underline; + } + +#examineManagement .index-tools table +{ + width: 350px; } -#examineManagement .index-tools table { - width:350px; -} -#examineManagement .index-tools .index-actions { + +#examineManagement .index-tools .index-actions +{ float: right; -} \ No newline at end of file +} + + #examineManagement .index-tools .index-actions .error + { + width: 560px; + padding: 5px; + } + +#examineManagement .search-tools input[type="text"] +{ + width: 400px; +} + +#examineManagement .search-tools a.hide +{ + float: right; +} + +#examineManagement .search-tools .search-results +{ + margin-top: 5px; +} + + #examineManagement .search-tools .search-results table + { + border-collapse: collapse; + } + + #examineManagement .search-tools .search-results th.score + { + width: 50px; + } + + #examineManagement .search-tools .search-results th.id + { + width: 50px; + } + + #examineManagement .search-tools .search-results tbody tr td + { + border-bottom: 1px solid #DDD; + padding: 3px; + } + #examineManagement .search-tools .search-results tbody tr td span.key { + display: inline-block; + color: rgb(0, 64, 201); + font-style: italic; + } + #examineManagement .search-tools .search-results tbody tr td span.value { + font-weight: normal; + display: inline-block; + padding-right: 5px; + color: black; + } diff --git a/src/Umbraco.Web.UI/umbraco_client/Dashboards/ExamineManagement.js b/src/Umbraco.Web.UI/umbraco_client/Dashboards/ExamineManagement.js index 216374ef9d..699b21637b 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Dashboards/ExamineManagement.js +++ b/src/Umbraco.Web.UI/umbraco_client/Dashboards/ExamineManagement.js @@ -8,7 +8,40 @@ _opts: null, _koViewModel: null, - _mapSearcherModelProperties : function(indexerModel) { + _mapSearcherModelProperties: function (indexerModel) { + var self = this; + + var viewModel = self._mapBaseProviderModelProperties(indexerModel); + + //add custom searcher props + viewModel.searchText = ko.observable(""); + viewModel.searchType = ko.observable("text"); + viewModel.isSearching = ko.observable(false); + viewModel.closeSearch = function() { + this.isSearching(false); + }; + //add flag properties to determine if either button has been pressed + viewModel.isProcessing = ko.observable(false); + //don't need an observable array since it does not change, just need an observable to hold an array. + viewModel.searchResults = ko.observable([]); + viewModel.search = function () { + //NOTE: 'this' is the ko view model + this.isSearching(true); + self._doSearch(this); + }; + viewModel.handleEnter = function(vm, event) { + var keyCode = (event.which ? event.which : event.keyCode); + if (keyCode === 13) { + vm.search(); + return false; + } + return true; + }; + + return viewModel; + }, + + _mapBaseProviderModelProperties : function(indexerModel) { var self = this; //do the ko auto-mapping @@ -24,6 +57,14 @@ viewModel.toggleProperties = function () { this.showProperties(!this.showProperties()); }; + viewModel.showProviderProperties = ko.observable(false); + viewModel.toggleProviderProperties = function () { + this.showProviderProperties(!this.showProviderProperties()); + }; + viewModel.showTools = ko.observable(false); + viewModel.toggleTools = function () { + this.showTools(!this.showTools()); + }; return viewModel; }, @@ -37,8 +78,10 @@ if (!isUpdate) { //do the ko auto-mapping to the new object and create additional properties - viewModel = self._mapSearcherModelProperties(indexerModel); + viewModel = self._mapBaseProviderModelProperties(indexerModel); + //property to track how many attempts have been made to check if the index is optimized or rebuilt + viewModel.processingAttempts = ko.observable(0); //add a hasDeletions prop viewModel.hasDeletions = ko.observable(indexerModel.DeletionCount > 0); //add toggle and show properties @@ -54,15 +97,7 @@ viewModel.showNodeTypes = ko.observable(false); viewModel.toggleNodeTypes = function () { this.showNodeTypes(!this.showNodeTypes()); - }; - viewModel.showProviderProperties = ko.observable(false); - viewModel.toggleProviderProperties = function () { - this.showProviderProperties(!this.showProviderProperties()); - }; - viewModel.showIndexTools = ko.observable(false); - viewModel.toggleIndexTools = function () { - this.showIndexTools(!this.showIndexTools()); - }; + }; //add flag properties to determine if either button has been pressed viewModel.isProcessing = ko.observable(false); //add the button methods @@ -71,7 +106,8 @@ "Depending on how much content there is in your site this could take a while. " + "It is not recommended to rebuild an index during times of high website traffic " + "or when editors are editing content.")) { - + //NOTE: 'this' is the knockoutjs model that is bound + self._doProcessing(this.Name(), this, "PostRebuildIndex", "PostCheckRebuildIndex"); } }; viewModel.optimizeIndex = function () { @@ -79,7 +115,7 @@ "It is not recommended to optimize an index during times of high website traffic " + "or when editors are editing content.")) { //NOTE: 'this' is the knockoutjs model that is bound - self._optimizeIndex(indexerModel.Name, this); + self._doProcessing(this.Name(), this, "PostOptimizeIndex", "PostCheckOptimizeIndex"); } }; } @@ -100,18 +136,34 @@ return viewModel; }, + + _doSearch: function(viewModel) { + var self = this; + viewModel.isProcessing(true); + $.get(self._opts.restServiceLocation + "GetSearchResults?searcherName=" + viewModel.Name() + "&query=" + viewModel.searchText() + "&queryType=" + viewModel.searchType(), + function(searchResults) { + viewModel.isProcessing(false); + //re-map the fields dictionary to array + for (var s in searchResults) { + searchResults[s].Fields = self._mapDictionaryToArray(searchResults[s].Fields); + } + viewModel.searchResults(searchResults); + }).fail(function(a, b, c) { + alert(b + ": " + a.responseText); + }); + }, - _optimizeIndex: function (name, viewModel) { + _doProcessing: function (name, viewModel, processingActionName, pollActionName) { var self = this; viewModel.isProcessing(true); //set the model processing - $.post(self._opts.restServiceLocation + "PostOptimizeIndex?indexerName=" + name, + $.post(self._opts.restServiceLocation + processingActionName + "?indexerName=" + name, function (data) { //optimization has started, nothing is returned accept a 200 status code. //lets poll to see if it is done. setTimeout(function() { - self._checkOptimizeIndex(name, viewModel); + self._checkProcessing(name, viewModel, pollActionName); }, 1000); }).fail(function (a, b, c) { @@ -119,10 +171,10 @@ }); }, - _checkOptimizeIndex: function (name, viewModel) { + _checkProcessing: function (name, viewModel, actionName) { var self = this; - $.post(self._opts.restServiceLocation + "PostCheckOptimizeIndex?indexerName=" + name, + $.post(self._opts.restServiceLocation + actionName + "?indexerName=" + name, function (data) { if (data) { //success! now, we need to re-update the whole indexer model @@ -130,8 +182,20 @@ viewModel.isProcessing(false); } else { + //copy local from closure + var vm = viewModel; + var an = actionName; setTimeout(function () { - self._checkOptimizeIndex(name); + //don't continue if we've tried 100 times + if (vm.processingAttempts() < 100) { + self._checkProcessing(name, vm, an); + //add an attempt + vm.processingAttempts(vm.processingAttempts() + 1); + } + else { + //we've exceeded 100 attempts, stop processing + viewModel.isProcessing(false); + } }, 1000); } }).fail(function (a, b, c) { diff --git a/src/Umbraco.Web/Search/ExamineEvents.cs b/src/Umbraco.Web/Search/ExamineEvents.cs index dcd5742694..b439a2f0e8 100644 --- a/src/Umbraco.Web/Search/ExamineEvents.cs +++ b/src/Umbraco.Web/Search/ExamineEvents.cs @@ -237,25 +237,10 @@ namespace Umbraco.Web.Search /// /// 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] + [Obsolete("This method is no longer used and will be removed from the core in future versions, the cacheOnly parameter has no effect. Use the other ToXDocument overload instead")] public static XDocument ToXDocument(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 ToXDocument(node); } @@ -267,6 +252,16 @@ namespace Umbraco.Web.Search [SecuritySafeCritical] private static XDocument ToXDocument(Content node) { + if (TypeHelper.IsTypeAssignableFrom(node)) + { + return new XDocument(((Document) node).Content.ToXml()); + } + + if (TypeHelper.IsTypeAssignableFrom(node)) + { + return new XDocument(((global::umbraco.cms.businesslogic.media.Media) node).MediaItem.ToXml()); + } + var xDoc = new XmlDocument(); var xNode = xDoc.CreateNode(XmlNodeType.Element, "node", ""); node.XmlPopulate(xDoc, ref xNode, false); diff --git a/src/Umbraco.Web/Search/LuceneIndexerExtensions.cs b/src/Umbraco.Web/Search/LuceneIndexerExtensions.cs index 0140c9e2ec..11615e1065 100644 --- a/src/Umbraco.Web/Search/LuceneIndexerExtensions.cs +++ b/src/Umbraco.Web/Search/LuceneIndexerExtensions.cs @@ -61,6 +61,20 @@ namespace Umbraco.Web.Search return indexer.GetSearcherForIndexer().GetIndexReaderForSearcher().IsOptimized(); } + /// + /// Check if the index is locked + /// + /// + /// + /// + /// If the index does not exist we'll consider it locked + /// + public static bool IsIndexLocked(this LuceneIndexer indexer) + { + return !indexer.IndexExists() + || IndexWriter.IsLocked(indexer.GetSearcherForIndexer().GetIndexReaderForSearcher().Directory()); + } + /// /// The number of documents deleted in the index /// diff --git a/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs b/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs index df6cfcf8ec..0bbf96f77d 100644 --- a/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs +++ b/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs @@ -1,9 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using Examine; +using Examine.LuceneEngine; using Examine.LuceneEngine.Providers; using Examine.Providers; using Lucene.Net.Search; @@ -19,7 +21,6 @@ namespace Umbraco.Web.WebServices /// Get the details for indexers /// /// - [HttpGet] public IEnumerable GetIndexerDetails() { return ExamineManager.Instance.IndexProviderCollection.Select(CreateModel); @@ -29,7 +30,6 @@ namespace Umbraco.Web.WebServices /// Get the details for searchers /// /// - [HttpGet] public IEnumerable GetSearcherDetails() { var model = new List( @@ -52,10 +52,34 @@ namespace Umbraco.Web.WebServices return model; } + public ISearchResults GetSearchResults(string searcherName, string query, string queryType) + { + if (queryType == null) + throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound)); + + if (query.IsNullOrWhiteSpace()) + return SearchResults.Empty(); + + LuceneSearcher searcher; + var msg = ValidateLuceneSearcher(searcherName, out searcher); + if (msg.IsSuccessStatusCode) + { + if (queryType.InvariantEquals("text")) + { + return searcher.Search(query, false); + } + if (queryType.InvariantEquals("lucene")) + { + return searcher.Search(searcher.CreateSearchCriteria().RawQuery(query)); + } + throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound)); + } + throw new HttpResponseException(msg); + } + /// /// Optimizes an index /// - [HttpPost] public HttpResponseMessage PostOptimizeIndex(string indexerName) { LuceneIndexer indexer; @@ -78,12 +102,61 @@ namespace Umbraco.Web.WebServices return msg; } + /// + /// Rebuilds the index + /// + /// + /// + public HttpResponseMessage PostRebuildIndex(string indexerName) + { + LuceneIndexer indexer; + var msg = ValidateLuceneIndexer(indexerName, out indexer); + if (msg.IsSuccessStatusCode) + { + try + { + indexer.RebuildIndex(); + } + catch (System.Exception ex) + { + return new HttpResponseMessage(HttpStatusCode.Conflict) + { + Content = new StringContent(string.Format("The index could not be rebuilt at this time, most likely there is another thread currently writing to the index. Error: {0}", ex)), + ReasonPhrase = "Could Not Rebuild" + }; + } + } + return msg; + } + + /// + /// Check if the index has been rebuilt + /// + /// + /// + /// + /// This is kind of rudementary since there's no way we can know that the index has rebuilt, we'll just check + /// if the index is locked based on Lucene apis + /// + public ExamineIndexerModel PostCheckRebuildIndex(string indexerName) + { + LuceneIndexer indexer; + var msg = ValidateLuceneIndexer(indexerName, out indexer); + if (msg.IsSuccessStatusCode) + { + var isLocked = indexer.IsIndexLocked(); + return isLocked + ? null + : CreateModel(indexer); + } + throw new HttpResponseException(msg); + } + /// /// Checks if the index is optimized /// /// /// - [HttpPost] public ExamineIndexerModel PostCheckOptimizeIndex(string indexerName) { LuceneIndexer indexer; @@ -126,6 +199,31 @@ namespace Umbraco.Web.WebServices return indexerModel; } + private HttpResponseMessage ValidateLuceneSearcher(string searcherName, out LuceneSearcher searcher) + { + if (ExamineManager.Instance.SearchProviderCollection.Cast().Any(x => x.Name == searcherName)) + { + searcher = ExamineManager.Instance.SearchProviderCollection[searcherName] as LuceneSearcher; + if (searcher == null) + { + return new HttpResponseMessage(HttpStatusCode.BadRequest) + { + Content = new StringContent(string.Format("The searcher {0} is not of type {1}", searcherName, typeof(LuceneSearcher))), + ReasonPhrase = "Wrong Searcher Type" + }; + } + //return Ok! + return Request.CreateResponse(HttpStatusCode.OK); + } + + searcher = null; + return new HttpResponseMessage(HttpStatusCode.BadRequest) + { + Content = new StringContent(string.Format("No searcher found with name = {0}", searcherName)), + ReasonPhrase = "Searcher Not Found" + }; + } + private HttpResponseMessage ValidateLuceneIndexer(string indexerName, out LuceneIndexer indexer) { if (ExamineManager.Instance.IndexProviderCollection.Any(x => x.Name == indexerName)) diff --git a/src/UmbracoExamine/DataServices/UmbracoContentService.cs b/src/UmbracoExamine/DataServices/UmbracoContentService.cs index d2025e1f83..fe55a9963d 100644 --- a/src/UmbracoExamine/DataServices/UmbracoContentService.cs +++ b/src/UmbracoExamine/DataServices/UmbracoContentService.cs @@ -80,7 +80,7 @@ namespace UmbracoExamine.DataServices var xmlContent = XDocument.Parse(""); foreach (var c in _applicationContext.Services.ContentService.GetRootContent()) { - xmlContent.Root.Add(c.ToXml()); + xmlContent.Root.Add(c.ToDeepXml()); } var result = ((IEnumerable)xmlContent.XPathEvaluate(xpath)).Cast(); return result.ToXDocument(); diff --git a/src/UmbracoExamine/UmbracoContentIndexer.cs b/src/UmbracoExamine/UmbracoContentIndexer.cs index 6be57db384..d5cca06462 100644 --- a/src/UmbracoExamine/UmbracoContentIndexer.cs +++ b/src/UmbracoExamine/UmbracoContentIndexer.cs @@ -208,8 +208,16 @@ namespace UmbracoExamine if (!SupportedTypes.Contains(type)) return; - DataService.LogService.AddVerboseLog((int)node.Attribute("id"), string.Format("ReIndexNode with type: {0}", type)); - base.ReIndexNode(node, type); + if (node.Attribute("id") != null) + { + DataService.LogService.AddVerboseLog((int) node.Attribute("id"), string.Format("ReIndexNode with type: {0}", type)); + base.ReIndexNode(node, type); + } + else + { + DataService.LogService.AddErrorLog(-1, string.Format("ReIndexNode cannot proceed, the format of the XElement is invalid, the xml has no 'id' attribute. {0}", node)); + } + } ///