mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-15 19:54:57 +08:00
Merge
--HG-- branch : dev
This commit is contained in:
@@ -58,7 +58,11 @@ namespace Orchard.Tests.Indexing {
|
||||
[Test]
|
||||
public void IndexProviderShouldOverwriteAlreadyExistingIndex() {
|
||||
_provider.CreateIndex("default");
|
||||
_provider.Store("default", _provider.New(1).Add("body", null));
|
||||
Assert.That(_provider.IsEmpty("default"), Is.False);
|
||||
|
||||
_provider.CreateIndex("default");
|
||||
Assert.That(_provider.IsEmpty("default"), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -181,5 +185,36 @@ namespace Orchard.Tests.Indexing {
|
||||
Assert.That(searchBuilder.Get(2).Id, Is.EqualTo(2));
|
||||
Assert.That(searchBuilder.Get(3).Id, Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ProviderShouldStoreSettings() {
|
||||
_provider.CreateIndex("default");
|
||||
Assert.That(_provider.GetLastIndexUtc("default"), Is.EqualTo(DefaultIndexProvider.DefaultMinDateTime));
|
||||
|
||||
_provider.SetLastIndexUtc("default", new DateTime(2010, 1, 1, 1, 1, 1, 1));
|
||||
Assert.That(_provider.GetLastIndexUtc("default"), Is.EqualTo(new DateTime(2010, 1, 1, 1, 1, 1, 0)));
|
||||
|
||||
_provider.SetLastIndexUtc("default", new DateTime(1901, 1, 1, 1, 1, 1, 1));
|
||||
Assert.That(_provider.GetLastIndexUtc("default"), Is.EqualTo(DefaultIndexProvider.DefaultMinDateTime));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsEmptyShouldBeTrueForNoneExistingIndexes() {
|
||||
_provider.IsEmpty("dummy");
|
||||
Assert.That(_provider.IsEmpty("default"), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsEmptyShouldBeTrueForJustNewIndexes() {
|
||||
_provider.CreateIndex("default");
|
||||
Assert.That(_provider.IsEmpty("default"), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsEmptyShouldBeFalseWhenThereIsADocument() {
|
||||
_provider.CreateIndex("default");
|
||||
_provider.Store("default", _provider.New(1).Add("body", null));
|
||||
Assert.That(_provider.IsEmpty("default"), Is.False);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -177,5 +177,19 @@ namespace Orchard.Tests.Indexing {
|
||||
Assert.That(date[0].GetDateTime("date") < date[1].GetDateTime("date"), Is.True);
|
||||
Assert.That(date[1].GetDateTime("date") < date[2].GetDateTime("date"), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ShouldEscapeSpecialChars() {
|
||||
_provider.CreateIndex("default");
|
||||
_provider.Store("default", _provider.New(1).Add("body", "Orchard has been developped in C#"));
|
||||
_provider.Store("default", _provider.New(2).Add("body", "Windows has been developped in C++"));
|
||||
|
||||
var cs = _searchBuilder.WithField("body", "C#").Search().ToList();
|
||||
Assert.That(cs.Count(), Is.EqualTo(2));
|
||||
|
||||
var cpp = _searchBuilder.WithField("body", "C++").Search().ToList();
|
||||
Assert.That(cpp.Count(), Is.EqualTo(2));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,13 +7,7 @@ Scenario: Installed modules are listed
|
||||
Given I have installed Orchard
|
||||
When I go to "admin/modules"
|
||||
Then I should see "<h1>Installed Modules</h1>"
|
||||
And I should see "<h3>Themes</h3>"
|
||||
And the status should be 200 OK
|
||||
|
||||
Scenario: Edit module shows its features
|
||||
Given I have installed Orchard
|
||||
When I go to "admin/modules/Edit/Orchard.Themes"
|
||||
Then I should see "<h1>Edit Module: Themes</h1>"
|
||||
And I should see "<h2>Themes"
|
||||
And the status should be 200 OK
|
||||
|
||||
Scenario: Features of installed modules are listed
|
||||
|
@@ -7,7 +7,7 @@ Scenario: Default site is listed
|
||||
Given I have installed Orchard
|
||||
And I have installed "Orchard.MultiTenancy"
|
||||
When I go to "Admin/MultiTenancy"
|
||||
Then I should see "List of Site's Tenants"
|
||||
Then I should see "List of Site's Tenants"
|
||||
And I should see "<span class="tenantName">Default</span>"
|
||||
And the status should be 200 OK
|
||||
|
||||
|
@@ -1,2 +1,7 @@
|
||||
<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl<BodyDisplayViewModel>" %>
|
||||
<%@ Import Namespace="Orchard.Core.Common.ViewModels"%>
|
||||
<%-- begin: knowingly broken HTML (hence the ManageWrapperPre and ManageWrapperPost templates)
|
||||
we need "wrapper templates" (among other functionality) in the future of UI composition
|
||||
please do not delete or the front end will be broken when the user is authenticated. --%>
|
||||
</div>
|
||||
<%-- begin: knowingly broken HTML --%>
|
142
src/Orchard.Web/Core/Indexing/Commands/IndexingCommands.cs
Normal file
142
src/Orchard.Web/Core/Indexing/Commands/IndexingCommands.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Orchard.Commands;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Indexing;
|
||||
using Orchard.Security;
|
||||
using Orchard.Tasks.Indexing;
|
||||
|
||||
namespace Orchard.Core.Indexing.Commands {
|
||||
public class IndexingCommands : DefaultOrchardCommandHandler {
|
||||
private readonly IEnumerable<IIndexNotifierHandler> _indexNotifierHandlers;
|
||||
private readonly IIndexManager _indexManager;
|
||||
private readonly IIndexingTaskManager _indexingTaskManager;
|
||||
private readonly IContentManager _contentManager;
|
||||
private const string SearchIndexName = "Search";
|
||||
|
||||
public IndexingCommands(
|
||||
IEnumerable<IIndexNotifierHandler> indexNotifierHandlers,
|
||||
IIndexManager indexManager,
|
||||
IIndexingTaskManager indexingTaskManager,
|
||||
IContentManager contentManager) {
|
||||
_indexNotifierHandlers = indexNotifierHandlers;
|
||||
_indexingTaskManager = indexingTaskManager;
|
||||
_contentManager = contentManager;
|
||||
_indexManager = indexManager;
|
||||
}
|
||||
|
||||
[OrchardSwitch]
|
||||
public string IndexName { get; set; }
|
||||
|
||||
[OrchardSwitch]
|
||||
public string Query { get; set; }
|
||||
|
||||
[OrchardSwitch]
|
||||
public string ContentItemId { get; set; }
|
||||
|
||||
[CommandName("index update")]
|
||||
[CommandHelp("index update [/IndexName:<index name>]\r\n\t" + "Updates the index with the specified <index name>, or the search index if not specified")]
|
||||
[OrchardSwitches("IndexName")]
|
||||
public string Update() {
|
||||
if ( !_indexManager.HasIndexProvider() ) {
|
||||
return "No index available";
|
||||
}
|
||||
|
||||
var indexName = String.IsNullOrWhiteSpace(IndexName) ? SearchIndexName : IndexName;
|
||||
foreach ( var handler in _indexNotifierHandlers ) {
|
||||
handler.UpdateIndex(indexName);
|
||||
}
|
||||
|
||||
return "Index is now being updated...";
|
||||
}
|
||||
|
||||
[CommandName("index rebuild")]
|
||||
[CommandHelp("index rebuild [/IndexName:<index name>]\r\n\t" + "Rebuilds the index with the specified <index name>, or the search index if not specified")]
|
||||
[OrchardSwitches("IndexName")]
|
||||
public string Rebuild() {
|
||||
if ( !_indexManager.HasIndexProvider() ) {
|
||||
return "No index available";
|
||||
}
|
||||
|
||||
var indexName = String.IsNullOrWhiteSpace(IndexName) ? SearchIndexName : IndexName;
|
||||
var searchProvider = _indexManager.GetSearchIndexProvider();
|
||||
if ( searchProvider.Exists(indexName) )
|
||||
searchProvider.DeleteIndex(indexName);
|
||||
|
||||
searchProvider.CreateIndex(indexName);
|
||||
return "Index is now being rebuilt...";
|
||||
}
|
||||
|
||||
[CommandName("index search")]
|
||||
[CommandHelp("index search /Query:<query> [/IndexName:<index name>]\r\n\t" + "Searches the specified <query> terms in the index with the specified <index name>, or in the search index if not specified")]
|
||||
[OrchardSwitches("Query,IndexName")]
|
||||
public string Search() {
|
||||
if ( !_indexManager.HasIndexProvider() ) {
|
||||
return "No index available";
|
||||
}
|
||||
var indexName = String.IsNullOrWhiteSpace(IndexName) ? SearchIndexName : IndexName;
|
||||
var searchBuilder = _indexManager.GetSearchIndexProvider().CreateSearchBuilder(indexName);
|
||||
var results = searchBuilder.WithField("body", Query).WithField("title", Query).Search();
|
||||
|
||||
Context.Output.WriteLine("{0} result{1}\r\n-----------------\r\n", results.Count(), results.Count() > 0 ? "s" : "");
|
||||
|
||||
Context.Output.WriteLine("┌──────────────────────────────────────────────────────────────┬────────┐");
|
||||
Context.Output.WriteLine("│ {0} │ {1,6} │", "Title" + new string(' ', 60 - "Title".Length), "Score");
|
||||
Context.Output.WriteLine("├──────────────────────────────────────────────────────────────┼────────┤");
|
||||
foreach ( var searchHit in results ) {
|
||||
var title = searchHit.GetString("title");
|
||||
title = title.Substring(0, Math.Min(60, title.Length)) ?? "- no title -";
|
||||
var score = searchHit.Score;
|
||||
Context.Output.WriteLine("│ {0} │ {1,6} │", title + new string(' ', 60 - title.Length), score);
|
||||
}
|
||||
Context.Output.WriteLine("└──────────────────────────────────────────────────────────────┴────────┘");
|
||||
|
||||
Context.Output.WriteLine();
|
||||
return "End of search results";
|
||||
}
|
||||
|
||||
[CommandName("index stats")]
|
||||
[CommandHelp("index stats [/IndexName:<index name>]\r\n\t" + "Displays some statistics about the index with the specified <index name>, or in the search index if not specified")]
|
||||
[OrchardSwitches("IndexName")]
|
||||
public string Stats() {
|
||||
if ( !_indexManager.HasIndexProvider() ) {
|
||||
return "No index available";
|
||||
}
|
||||
var indexName = String.IsNullOrWhiteSpace(IndexName) ? SearchIndexName : IndexName;
|
||||
Context.Output.WriteLine("Number of indexed documents: {0}", _indexManager.GetSearchIndexProvider().NumDocs(indexName));
|
||||
return "";
|
||||
}
|
||||
|
||||
[CommandName("index refresh")]
|
||||
[CommandHelp("index refresh /ContenItem:<content item id> \r\n\t" + "Refreshes the index for the specifed <content item id>")]
|
||||
[OrchardSwitches("ContentItem")]
|
||||
public string Refresh() {
|
||||
int contenItemId;
|
||||
if ( !int.TryParse(ContentItemId, out contenItemId) ) {
|
||||
return "Invalid content item id. Not an integer.";
|
||||
}
|
||||
|
||||
var contentItem = _contentManager.Get(contenItemId);
|
||||
_indexingTaskManager.CreateUpdateIndexTask(contentItem);
|
||||
|
||||
return "Content Item marked for reindexing";
|
||||
}
|
||||
|
||||
[CommandName("index delete")]
|
||||
[CommandHelp("index delete /ContenItem:<content item id>\r\n\t" + "Deletes the specifed <content item id> fromthe index")]
|
||||
[OrchardSwitches("ContentItem")]
|
||||
public string Delete() {
|
||||
int contenItemId;
|
||||
if(!int.TryParse(ContentItemId, out contenItemId)) {
|
||||
return "Invalid content item id. Not an integer.";
|
||||
}
|
||||
|
||||
var contentItem = _contentManager.Get(contenItemId);
|
||||
_indexingTaskManager.CreateDeleteIndexTask(contentItem);
|
||||
|
||||
return "Content Item marked for deletion";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -13,6 +13,7 @@ using Orchard.Indexing;
|
||||
using Directory = Lucene.Net.Store.Directory;
|
||||
using Version = Lucene.Net.Util.Version;
|
||||
using Orchard.Logging;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Orchard.Core.Indexing.Lucene {
|
||||
/// <summary>
|
||||
@@ -22,14 +23,18 @@ namespace Orchard.Core.Indexing.Lucene {
|
||||
private readonly IAppDataFolder _appDataFolder;
|
||||
private readonly ShellSettings _shellSettings;
|
||||
public static readonly Version LuceneVersion = Version.LUCENE_29;
|
||||
private readonly Analyzer _analyzer = new StandardAnalyzer(LuceneVersion);
|
||||
private readonly Analyzer _analyzer ;
|
||||
private readonly string _basePath;
|
||||
public static readonly DateTime DefaultMinDateTime = new DateTime(1980, 1, 1);
|
||||
public static readonly string Settings = "Settings";
|
||||
public static readonly string LastIndexUtc = "LastIndexedUtc";
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public DefaultIndexProvider(IAppDataFolder appDataFolder, ShellSettings shellSettings) {
|
||||
_appDataFolder = appDataFolder;
|
||||
_shellSettings = shellSettings;
|
||||
_analyzer = CreateAnalyzer();
|
||||
|
||||
// TODO: (sebros) Find a common way to get where tenant's specific files should go. "Sites/Tenant" is hard coded in multiple places
|
||||
_basePath = Path.Combine("Sites", _shellSettings.Name, "Indexes");
|
||||
@@ -37,6 +42,15 @@ namespace Orchard.Core.Indexing.Lucene {
|
||||
Logger = NullLogger.Instance;
|
||||
|
||||
// Ensures the directory exists
|
||||
EnsureDirectoryExists();
|
||||
}
|
||||
|
||||
public static Analyzer CreateAnalyzer() {
|
||||
// StandardAnalyzer does lower-case and stop-word filtering. It also removes punctuation
|
||||
return new StandardAnalyzer(LuceneVersion);
|
||||
}
|
||||
|
||||
private void EnsureDirectoryExists() {
|
||||
var directory = new DirectoryInfo(_appDataFolder.MapPath(_basePath));
|
||||
if(!directory.Exists) {
|
||||
directory.Create();
|
||||
@@ -62,6 +76,36 @@ namespace Orchard.Core.Indexing.Lucene {
|
||||
return new DirectoryInfo(_appDataFolder.MapPath(Path.Combine(_basePath, indexName))).Exists;
|
||||
}
|
||||
|
||||
public bool IsEmpty(string indexName) {
|
||||
if ( !Exists(indexName) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var reader = IndexReader.Open(GetDirectory(indexName), true);
|
||||
|
||||
try {
|
||||
return reader.NumDocs() == 0;
|
||||
}
|
||||
finally {
|
||||
reader.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public int NumDocs(string indexName) {
|
||||
if ( !Exists(indexName) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var reader = IndexReader.Open(GetDirectory(indexName), true);
|
||||
|
||||
try {
|
||||
return reader.NumDocs();
|
||||
}
|
||||
finally {
|
||||
reader.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public void CreateIndex(string indexName) {
|
||||
var writer = new IndexWriter(GetDirectory(indexName), _analyzer, true, IndexWriter.MaxFieldLength.UNLIMITED);
|
||||
writer.Close();
|
||||
@@ -72,6 +116,11 @@ namespace Orchard.Core.Indexing.Lucene {
|
||||
public void DeleteIndex(string indexName) {
|
||||
new DirectoryInfo(Path.Combine(_appDataFolder.MapPath(Path.Combine(_basePath, indexName))))
|
||||
.Delete(true);
|
||||
|
||||
var settingsFileName = GetSettingsFileName(indexName);
|
||||
if(File.Exists(settingsFileName)) {
|
||||
File.Delete(settingsFileName);
|
||||
}
|
||||
}
|
||||
|
||||
public void Store(string indexName, IIndexDocument indexDocument) {
|
||||
@@ -105,7 +154,6 @@ namespace Orchard.Core.Indexing.Lucene {
|
||||
writer.Optimize();
|
||||
writer.Close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void Delete(string indexName, int documentId) {
|
||||
@@ -148,5 +196,38 @@ namespace Orchard.Core.Indexing.Lucene {
|
||||
return new DefaultSearchBuilder(GetDirectory(indexName));
|
||||
}
|
||||
|
||||
private string GetSettingsFileName(string indexName) {
|
||||
return Path.Combine(_appDataFolder.MapPath(_basePath), indexName + ".settings.xml");
|
||||
}
|
||||
|
||||
public DateTime GetLastIndexUtc(string indexName) {
|
||||
var settingsFileName = GetSettingsFileName(indexName);
|
||||
|
||||
return File.Exists(settingsFileName)
|
||||
? DateTime.Parse(XDocument.Load(settingsFileName).Descendants(LastIndexUtc).First().Value)
|
||||
: DefaultMinDateTime;
|
||||
}
|
||||
|
||||
public void SetLastIndexUtc(string indexName, DateTime lastIndexUtc) {
|
||||
if ( lastIndexUtc < DefaultMinDateTime ) {
|
||||
lastIndexUtc = DefaultMinDateTime;
|
||||
}
|
||||
|
||||
XDocument doc;
|
||||
var settingsFileName = GetSettingsFileName(indexName);
|
||||
if ( !File.Exists(settingsFileName) ) {
|
||||
EnsureDirectoryExists();
|
||||
doc = new XDocument(
|
||||
new XElement(Settings,
|
||||
new XElement(LastIndexUtc, lastIndexUtc.ToString("s"))));
|
||||
}
|
||||
else {
|
||||
doc = XDocument.Load(settingsFileName);
|
||||
doc.Element(Settings).Element(LastIndexUtc).Value = lastIndexUtc.ToString("s");
|
||||
}
|
||||
|
||||
doc.Save(settingsFileName);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -2,12 +2,15 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Lucene.Net.Analysis;
|
||||
using Lucene.Net.Analysis.Tokenattributes;
|
||||
using Lucene.Net.Index;
|
||||
using Lucene.Net.Search;
|
||||
using Lucene.Net.Store;
|
||||
using Orchard.Logging;
|
||||
using Lucene.Net.Documents;
|
||||
using Orchard.Indexing;
|
||||
using Lucene.Net.QueryParsers;
|
||||
|
||||
namespace Orchard.Core.Indexing.Lucene {
|
||||
public class DefaultSearchBuilder : ISearchBuilder {
|
||||
@@ -23,6 +26,9 @@ namespace Orchard.Core.Indexing.Lucene {
|
||||
private readonly Dictionary<string, DateTime> _after;
|
||||
private string _sort;
|
||||
private bool _sortDescending;
|
||||
private string _parse;
|
||||
private readonly Analyzer _analyzer;
|
||||
private string _defaultField;
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
@@ -37,9 +43,21 @@ namespace Orchard.Core.Indexing.Lucene {
|
||||
_fields = new Dictionary<string, Query[]>();
|
||||
_sort = String.Empty;
|
||||
_sortDescending = true;
|
||||
_parse = String.Empty;
|
||||
_analyzer = DefaultIndexProvider.CreateAnalyzer();
|
||||
}
|
||||
|
||||
public ISearchBuilder Parse(string query) {
|
||||
public ISearchBuilder Parse(string defaultField, string query) {
|
||||
if ( String.IsNullOrWhiteSpace(defaultField) ) {
|
||||
throw new ArgumentException("Default field can't be empty");
|
||||
}
|
||||
|
||||
if ( String.IsNullOrWhiteSpace(query) ) {
|
||||
throw new ArgumentException("Query can't be empty");
|
||||
}
|
||||
|
||||
_defaultField = defaultField;
|
||||
_parse = query;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -49,8 +67,17 @@ namespace Orchard.Core.Indexing.Lucene {
|
||||
|
||||
public ISearchBuilder WithField(string field, string value, bool wildcardSearch) {
|
||||
|
||||
_fields[field] = value.Split(' ')
|
||||
var tokens = new List<string>();
|
||||
using(var sr = new System.IO.StringReader(value)) {
|
||||
var stream = _analyzer.TokenStream(field, sr);
|
||||
while(stream.IncrementToken()) {
|
||||
tokens.Add(((TermAttribute)stream.GetAttribute(typeof(TermAttribute))).Term());
|
||||
}
|
||||
}
|
||||
|
||||
_fields[field] = tokens
|
||||
.Where(k => !String.IsNullOrWhiteSpace(k))
|
||||
.Select(QueryParser.Escape)
|
||||
.Select(k => wildcardSearch ? (Query)new PrefixQuery(new Term(field, k)) : new TermQuery(new Term(k)))
|
||||
.ToArray();
|
||||
|
||||
@@ -93,6 +120,10 @@ namespace Orchard.Core.Indexing.Lucene {
|
||||
}
|
||||
|
||||
private Query CreateQuery() {
|
||||
if(!String.IsNullOrWhiteSpace(_parse)) {
|
||||
return new QueryParser(DefaultIndexProvider.LuceneVersion, _defaultField, DefaultIndexProvider.CreateAnalyzer()).Parse(_parse);
|
||||
}
|
||||
|
||||
var query = new BooleanQuery();
|
||||
|
||||
if ( _fields.Keys.Count > 0 ) { // apply specific filters if defined
|
||||
|
@@ -1,8 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Orchard.Core.Indexing.Models {
|
||||
public class IndexingSettingsRecord {
|
||||
public virtual int Id { get; set; }
|
||||
public virtual DateTime? LatestIndexingUtc { get; set; }
|
||||
}
|
||||
}
|
@@ -10,59 +10,107 @@ using Orchard.Logging;
|
||||
using Orchard.Services;
|
||||
using Orchard.Tasks;
|
||||
using Orchard.Core.Indexing.Models;
|
||||
using Orchard.Tasks.Indexing;
|
||||
using Orchard.Indexing;
|
||||
|
||||
namespace Orchard.Core.Indexing.Services {
|
||||
/// <summary>
|
||||
/// Contains the logic which is regularly executed to retrieve index information from multiple content handlers.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class IndexingTaskExecutor : IBackgroundTask {
|
||||
public class IndexingTaskExecutor : IBackgroundTask, IIndexNotifierHandler {
|
||||
private readonly IClock _clock;
|
||||
private readonly IRepository<IndexingTaskRecord> _repository;
|
||||
private readonly IRepository<IndexingSettingsRecord> _settings;
|
||||
private readonly IEnumerable<IContentHandler> _handlers;
|
||||
private IIndexProvider _indexProvider;
|
||||
private readonly IIndexManager _indexManager;
|
||||
private readonly IIndexingTaskManager _indexingTaskManager;
|
||||
private readonly IContentManager _contentManager;
|
||||
private const string SearchIndexName = "Search";
|
||||
|
||||
private readonly object _synLock = new object();
|
||||
|
||||
public IndexingTaskExecutor(
|
||||
IClock clock,
|
||||
IRepository<IndexingTaskRecord> repository,
|
||||
IRepository<IndexingSettingsRecord> settings,
|
||||
IEnumerable<IContentHandler> handlers,
|
||||
IIndexManager indexManager,
|
||||
IIndexingTaskManager indexingTaskManager,
|
||||
IContentManager contentManager) {
|
||||
_clock = clock;
|
||||
_repository = repository;
|
||||
_settings = settings;
|
||||
_indexManager = indexManager;
|
||||
_handlers = handlers;
|
||||
_indexingTaskManager = indexingTaskManager;
|
||||
_contentManager = contentManager;
|
||||
Logger = NullLogger.Instance;
|
||||
}
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public void UpdateIndex(string indexName) {
|
||||
if (indexName == SearchIndexName) {
|
||||
Sweep();
|
||||
}
|
||||
}
|
||||
|
||||
public void Sweep() {
|
||||
|
||||
if ( !System.Threading.Monitor.TryEnter(_synLock) ) {
|
||||
Logger.Information("Index was requested but was already running");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
if (!_indexManager.HasIndexProvider()) {
|
||||
return;
|
||||
}
|
||||
|
||||
_indexProvider = _indexManager.GetSearchIndexProvider();
|
||||
var updateIndexDocuments = new List<IIndexDocument>();
|
||||
var lastIndexing = DateTime.UtcNow;
|
||||
|
||||
// retrieve last processed index time
|
||||
var settingsRecord = _settings.Table.FirstOrDefault();
|
||||
// Do we need to rebuild the full index (first time module is used, or rebuild index requested) ?
|
||||
if (_indexProvider.IsEmpty(SearchIndexName)) {
|
||||
Logger.Information("Rebuild index started");
|
||||
|
||||
if ( settingsRecord == null ) {
|
||||
_settings.Create(settingsRecord = new IndexingSettingsRecord { LatestIndexingUtc = new DateTime(1980, 1, 1) });
|
||||
// mark current last task, as we should process older ones (in case of rebuild index only)
|
||||
lastIndexing = _indexingTaskManager.GetLastTaskDateTime();
|
||||
|
||||
// get every existing content item to index it
|
||||
foreach (var contentItem in _contentManager.Query(VersionOptions.Published).List()) {
|
||||
try {
|
||||
var context = new IndexContentContext {
|
||||
ContentItem = contentItem,
|
||||
IndexDocument = _indexProvider.New(contentItem.Id)
|
||||
};
|
||||
|
||||
// dispatch to handlers to retrieve index information
|
||||
foreach (var handler in _handlers) {
|
||||
handler.Indexing(context);
|
||||
}
|
||||
|
||||
var lastIndexing = settingsRecord.LatestIndexingUtc;
|
||||
settingsRecord.LatestIndexingUtc = _clock.UtcNow;
|
||||
updateIndexDocuments.Add(context.IndexDocument);
|
||||
|
||||
// retrieved not yet processed tasks
|
||||
foreach (var handler in _handlers) {
|
||||
handler.Indexed(context);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
Logger.Warning(ex, "Unable to index content item #{0} during rebuild", contentItem.Id);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
// retrieve last processed index time
|
||||
lastIndexing = _indexProvider.GetLastIndexUtc(SearchIndexName);
|
||||
}
|
||||
|
||||
_indexProvider.SetLastIndexUtc(SearchIndexName, _clock.UtcNow);
|
||||
|
||||
// retrieve not yet processed tasks
|
||||
var taskRecords = _repository.Fetch(x => x.CreatedUtc >= lastIndexing)
|
||||
.ToArray();
|
||||
|
||||
@@ -71,35 +119,16 @@ namespace Orchard.Core.Indexing.Services {
|
||||
|
||||
Logger.Information("Processing {0} indexing tasks", taskRecords.Length);
|
||||
|
||||
|
||||
if (!_indexProvider.Exists(SearchIndexName)) {
|
||||
_indexProvider.CreateIndex(SearchIndexName);
|
||||
}
|
||||
|
||||
var updateIndexDocuments = new List<IIndexDocument>();
|
||||
var deleteIndexDocuments = new List<int>();
|
||||
|
||||
// process Delete tasks
|
||||
foreach ( var taskRecord in taskRecords.Where(t => t.Action == IndexingTaskRecord.Delete) ) {
|
||||
var task = new IndexingTask(_contentManager, taskRecord);
|
||||
deleteIndexDocuments.Add(taskRecord.ContentItemRecord.Id);
|
||||
|
||||
try {
|
||||
_repository.Delete(taskRecord);
|
||||
_indexProvider.Delete(SearchIndexName, taskRecords.Where(t => t.Action == IndexingTaskRecord.Delete).Select(t => t.Id));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
Logger.Error(ex, "Could not delete task #{0}", taskRecord.Id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
if ( deleteIndexDocuments.Count > 0 ) {
|
||||
_indexProvider.Delete(SearchIndexName, deleteIndexDocuments);
|
||||
}
|
||||
}
|
||||
catch ( Exception ex ) {
|
||||
Logger.Warning(ex, "An error occured while remove a document from the index");
|
||||
Logger.Warning(ex, "An error occured while removing a document from the index");
|
||||
}
|
||||
|
||||
// process Update tasks
|
||||
@@ -128,16 +157,18 @@ namespace Orchard.Core.Indexing.Services {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (updateIndexDocuments.Count > 0) {
|
||||
try {
|
||||
_indexProvider.Store(SearchIndexName, updateIndexDocuments);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
Logger.Warning(ex, "An error occured while adding a document to the index");
|
||||
}
|
||||
|
||||
_settings.Update(settingsRecord);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
System.Threading.Monitor.Exit(_synLock);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -16,18 +16,15 @@ namespace Orchard.Core.Indexing.Services {
|
||||
public class IndexingTaskManager : IIndexingTaskManager {
|
||||
private readonly IContentManager _contentManager;
|
||||
private readonly IRepository<IndexingTaskRecord> _repository;
|
||||
private readonly IRepository<IndexingSettingsRecord> _settings;
|
||||
private readonly IClock _clock;
|
||||
|
||||
public IndexingTaskManager(
|
||||
IContentManager contentManager,
|
||||
IRepository<IndexingTaskRecord> repository,
|
||||
IRepository<IndexingSettingsRecord> settings,
|
||||
IClock clock) {
|
||||
_clock = clock;
|
||||
_repository = repository;
|
||||
_contentManager = contentManager;
|
||||
_settings = settings;
|
||||
Logger = NullLogger.Instance;
|
||||
}
|
||||
|
||||
@@ -38,14 +35,7 @@ namespace Orchard.Core.Indexing.Services {
|
||||
throw new ArgumentNullException("contentItem");
|
||||
}
|
||||
|
||||
// remove previous tasks for the same content item
|
||||
var tasks = _repository
|
||||
.Fetch(x => x.ContentItemRecord.Id == contentItem.Id)
|
||||
.ToArray();
|
||||
|
||||
foreach ( var task in tasks ) {
|
||||
_repository.Delete(task);
|
||||
}
|
||||
DeleteTasks(contentItem);
|
||||
|
||||
var taskRecord = new IndexingTaskRecord {
|
||||
CreatedUtc = _clock.UtcNow,
|
||||
@@ -69,45 +59,20 @@ namespace Orchard.Core.Indexing.Services {
|
||||
Logger.Information("Deleting index task created for [{0}:{1}]", contentItem.ContentType, contentItem.Id);
|
||||
}
|
||||
|
||||
public IEnumerable<IIndexingTask> GetTasks(DateTime? createdAfter) {
|
||||
return _repository
|
||||
.Fetch(x => x.CreatedUtc > createdAfter)
|
||||
.Select(x => new IndexingTask(_contentManager, x))
|
||||
.Cast<IIndexingTask>()
|
||||
.ToReadOnlyCollection();
|
||||
}
|
||||
|
||||
public void DeleteTasks(DateTime? createdBefore) {
|
||||
Logger.Debug("Deleting Indexing tasks created before {0}", createdBefore);
|
||||
|
||||
var tasks = _repository
|
||||
.Fetch(x => x.CreatedUtc <= createdBefore);
|
||||
|
||||
foreach (var task in tasks) {
|
||||
_repository.Delete(task);
|
||||
}
|
||||
public DateTime GetLastTaskDateTime() {
|
||||
return _repository.Table.Max(t => t.CreatedUtc) ?? DateTime.MinValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes existing tasks for the specified content item
|
||||
/// </summary>
|
||||
public void DeleteTasks(ContentItem contentItem) {
|
||||
Logger.Debug("Deleting Indexing tasks for ContentItem [{0}:{1}]", contentItem.ContentType, contentItem.Id);
|
||||
|
||||
var tasks = _repository
|
||||
.Fetch(x => x.Id == contentItem.Id);
|
||||
|
||||
.Fetch(x => x.ContentItemRecord.Id == contentItem.Id)
|
||||
.ToArray();
|
||||
foreach (var task in tasks) {
|
||||
_repository.Delete(task);
|
||||
}
|
||||
}
|
||||
|
||||
public void RebuildIndex() {
|
||||
var settingsRecord = _settings.Table.FirstOrDefault();
|
||||
if (settingsRecord == null) {
|
||||
_settings.Create(settingsRecord = new IndexingSettingsRecord() );
|
||||
}
|
||||
|
||||
settingsRecord.LatestIndexingUtc = new DateTime(1980, 1, 1);
|
||||
_settings.Update(settingsRecord);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
20
src/Orchard.Web/Core/Localization/Models/Localized.cs
Normal file
20
src/Orchard.Web/Core/Localization/Models/Localized.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System.Web.Mvc;
|
||||
using Orchard.ContentManagement;
|
||||
|
||||
namespace Orchard.Core.Localization.Models {
|
||||
public sealed class Localized : ContentPart<LocalizedRecord> {
|
||||
[HiddenInput(DisplayValue = false)]
|
||||
public int Id { get { return ContentItem.Id; } }
|
||||
|
||||
public int CultureId {
|
||||
get { return Record.CultureId; }
|
||||
set { Record.CultureId = value; }
|
||||
}
|
||||
|
||||
public int MasterContentItemId {
|
||||
get { return Record.MasterContentItemId; }
|
||||
set { Record.MasterContentItemId = value; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
using Orchard.ContentManagement.Records;
|
||||
|
||||
namespace Orchard.Core.Localization.Models {
|
||||
public class LocalizedRecord : ContentPartRecord {
|
||||
public virtual int CultureId { get; set; }
|
||||
public virtual int MasterContentItemId { get; set; }
|
||||
}
|
||||
}
|
11
src/Orchard.Web/Core/Localization/Module.txt
Normal file
11
src/Orchard.Web/Core/Localization/Module.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
name: Localization
|
||||
antiforgery: enabled
|
||||
author: The Orchard Team
|
||||
website: http://orchardproject.net
|
||||
version: 0.1
|
||||
orchardversion: 0.1.2010.0312
|
||||
description: Support for localizing content items for cultures.
|
||||
features:
|
||||
Localization:
|
||||
Description: Localize content items.
|
||||
Category: Core
|
@@ -0,0 +1,40 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Core.Localization.Models;
|
||||
using Orchard.Localization.Services;
|
||||
using Localized = Orchard.Core.Localization.Models.Localized;
|
||||
|
||||
namespace Orchard.Core.Localization.Services {
|
||||
[UsedImplicitly]
|
||||
public class ContentItemLocalizationService : IContentItemLocalizationService {
|
||||
private readonly IContentManager _contentManager;
|
||||
private readonly ICultureManager _cultureManager;
|
||||
|
||||
public ContentItemLocalizationService(IContentManager contentManager, ICultureManager cultureManager) {
|
||||
_contentManager = contentManager;
|
||||
_cultureManager = cultureManager;
|
||||
}
|
||||
|
||||
public IEnumerable<Localized> Get() {
|
||||
return _contentManager.Query<Localized, LocalizedRecord>().List();
|
||||
}
|
||||
|
||||
public Localized Get(int localizedId) {
|
||||
return _contentManager.Get<Localized>(localizedId);
|
||||
}
|
||||
|
||||
public Localized GetLocalizationForCulture(int masterId, string cultureName) {
|
||||
var cultures = _cultureManager.ListCultures();
|
||||
if (cultures.Contains(cultureName)) {
|
||||
int cultureId = _cultureManager.GetCultureIdByName(cultureName);
|
||||
if (cultureId != 0) {
|
||||
return _contentManager.Query<Localized, LocalizedRecord>()
|
||||
.Where(x => x.MasterContentItemId == masterId && x.CultureId == cultureId).List().FirstOrDefault();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard.Core.Localization.Models;
|
||||
|
||||
namespace Orchard.Core.Localization.Services {
|
||||
public interface IContentItemLocalizationService : IDependency {
|
||||
IEnumerable<Localized> Get();
|
||||
Localized Get(int localizedId);
|
||||
Localized GetLocalizationForCulture(int masterId, string cultureName);
|
||||
}
|
||||
}
|
@@ -115,16 +115,20 @@
|
||||
<Compile Include="Feeds\Rss\RssResult.cs" />
|
||||
<Compile Include="HomePage\Controllers\HomeController.cs" />
|
||||
<Compile Include="HomePage\Routes.cs" />
|
||||
<Compile Include="Indexing\Commands\IndexingCommands.cs" />
|
||||
<Compile Include="Indexing\Lucene\DefaultIndexDocument.cs" />
|
||||
<Compile Include="Indexing\Lucene\DefaultIndexProvider.cs" />
|
||||
<Compile Include="Indexing\Lucene\DefaultSearchBuilder.cs" />
|
||||
<Compile Include="Indexing\Lucene\DefaultSearchHit.cs" />
|
||||
<Compile Include="Indexing\Models\IndexingSettingsRecord.cs" />
|
||||
<Compile Include="Indexing\Models\IndexingTask.cs" />
|
||||
<Compile Include="Indexing\Models\IndexingTaskRecord.cs" />
|
||||
<Compile Include="Indexing\Services\CreateIndexingTaskHandler.cs" />
|
||||
<Compile Include="Indexing\Services\IndexingTaskExecutor.cs" />
|
||||
<Compile Include="Indexing\Services\IndexingTaskManager.cs" />
|
||||
<Compile Include="Localization\Models\Localized.cs" />
|
||||
<Compile Include="Localization\Models\LocalizedRecord.cs" />
|
||||
<Compile Include="Localization\Services\ContentItemLocalizationService.cs" />
|
||||
<Compile Include="Localization\Services\IContentItemLocalizationService.cs" />
|
||||
<Compile Include="Navigation\AdminMenu.cs" />
|
||||
<Compile Include="Navigation\Controllers\AdminController.cs" />
|
||||
<Compile Include="Navigation\Models\MenuItem.cs" />
|
||||
@@ -193,6 +197,7 @@
|
||||
<Content Include="Contents\Views\Admin\Create.aspx" />
|
||||
<Content Include="Contents\Views\EditorTemplates\Items\Contents.Item.ascx" />
|
||||
<Content Include="Indexing\Module.txt" />
|
||||
<Content Include="Localization\Module.txt" />
|
||||
<Content Include="Settings\Module.txt" />
|
||||
<Content Include="Settings\Views\Admin\Index.ascx" />
|
||||
<Content Include="Web.config" />
|
||||
|
16
src/Orchard.Web/Modules/Orchard.Search/AdminMenu.cs
Normal file
16
src/Orchard.Web/Modules/Orchard.Search/AdminMenu.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Orchard.Localization;
|
||||
using Orchard.UI.Navigation;
|
||||
|
||||
namespace Orchard.Search {
|
||||
public class AdminMenu : INavigationProvider {
|
||||
public Localizer T { get; set; }
|
||||
public string MenuName { get { return "admin"; } }
|
||||
|
||||
public void GetNavigation(NavigationBuilder builder) {
|
||||
builder.Add(T("Site"), "11",
|
||||
menu => menu
|
||||
.Add(T("Search Index"), "10.0", item => item.Action("Index", "Admin", new {area = "Orchard.Search"})
|
||||
.Permission(Permissions.ManageSearchIndex)));
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Web.Mvc;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Search.Services;
|
||||
using Orchard.Search.ViewModels;
|
||||
using Orchard.UI.Notify;
|
||||
|
||||
namespace Orchard.Search.Controllers {
|
||||
public class AdminController : Controller {
|
||||
private readonly ISearchService _searchService;
|
||||
|
||||
public AdminController(ISearchService searchService, IOrchardServices services) {
|
||||
_searchService = searchService;
|
||||
Services = services;
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
|
||||
public IOrchardServices Services { get; private set; }
|
||||
public Localizer T { get; set; }
|
||||
|
||||
public ActionResult Index() {
|
||||
var viewModel = new SearchIndexViewModel {HasIndexToManage = _searchService.HasIndexToManage, IndexUpdatedUtc = _searchService.GetIndexUpdatedUtc()};
|
||||
|
||||
if (!viewModel.HasIndexToManage)
|
||||
Services.Notifier.Information(T("There is not search index to manage for this site."));
|
||||
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public ActionResult Update() {
|
||||
if (!Services.Authorizer.Authorize(Permissions.ManageSearchIndex, T("Not allowed to manage the search index.")))
|
||||
return new HttpUnauthorizedResult();
|
||||
|
||||
_searchService.UpdateIndex();
|
||||
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public ActionResult Rebuild() {
|
||||
if (!Services.Authorizer.Authorize(Permissions.ManageSearchIndex, T("Not allowed to manage the search index.")))
|
||||
return new HttpUnauthorizedResult();
|
||||
|
||||
_searchService.RebuildIndex();
|
||||
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Web.Mvc;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Search.Services;
|
||||
@@ -14,14 +15,17 @@ namespace Orchard.Search.Controllers {
|
||||
_contentManager = contentManager;
|
||||
}
|
||||
|
||||
public ActionResult Index(string q) {
|
||||
var searchViewModel = new SearchViewModel {Query = q};
|
||||
public ActionResult Index(string q, int page = 1, int pageSize = 10) {
|
||||
var searchViewModel = new SearchViewModel {
|
||||
Query = q,
|
||||
DefaultPageSize = 10, // <- yeah, I know :|
|
||||
PageOfResults = _searchService.Query(q, page, pageSize, searchHit => new SearchResultViewModel {
|
||||
Content = _contentManager.BuildDisplayModel(_contentManager.Get(searchHit.Id), "SummaryForSearch"),
|
||||
SearchHit = searchHit
|
||||
})
|
||||
};
|
||||
|
||||
var results = _searchService.Query(q);
|
||||
searchViewModel.Results = results.Select(result => new SearchResultViewModel {
|
||||
Content = _contentManager.BuildDisplayModel(_contentManager.Get(result.Id), "SummaryForSearch"),
|
||||
SearchHit = result
|
||||
}).ToList();
|
||||
//todo: deal with page requests beyond result count
|
||||
|
||||
return View(searchViewModel);
|
||||
}
|
||||
|
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard.Indexing;
|
||||
|
||||
namespace Orchard.Search.Models {
|
||||
public interface ISearchResult {
|
||||
IEnumerable<ISearchHit> Page { get; set; }
|
||||
int TotalCount { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard.Indexing;
|
||||
|
||||
namespace Orchard.Search.Models {
|
||||
public class SearchResult : ISearchResult {
|
||||
public IEnumerable<ISearchHit> Page { get; set; }
|
||||
public int TotalCount { get; set; }
|
||||
}
|
||||
}
|
@@ -65,11 +65,17 @@
|
||||
<Reference Include="System.EnterpriseServices" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="AdminMenu.cs" />
|
||||
<Compile Include="Controllers\AdminController.cs" />
|
||||
<Compile Include="Controllers\SearchController.cs" />
|
||||
<Compile Include="Filters\SearchFilter.cs" />
|
||||
<Compile Include="Permissions.cs" />
|
||||
<Compile Include="Routes.cs" />
|
||||
<Compile Include="Models\ISearchResult.cs" />
|
||||
<Compile Include="Services\ISearchService.cs" />
|
||||
<Compile Include="Models\SearchResult.cs" />
|
||||
<Compile Include="Services\SearchService.cs" />
|
||||
<Compile Include="ViewModels\SearchIndexViewModel.cs" />
|
||||
<Compile Include="ViewModels\SearchResultViewModel.cs" />
|
||||
<Compile Include="ViewModels\SearchViewModel.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
@@ -82,7 +88,9 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Module.txt" />
|
||||
<Content Include="Styles\admin.css" />
|
||||
<Content Include="Styles\search.css" />
|
||||
<Content Include="Views\Admin\Index.ascx" />
|
||||
<Content Include="Views\SearchForm.ascx" />
|
||||
<Content Include="Views\Search\Index.ascx" />
|
||||
<Content Include="Views\Web.config" />
|
||||
|
29
src/Orchard.Web/Modules/Orchard.Search/Permissions.cs
Normal file
29
src/Orchard.Web/Modules/Orchard.Search/Permissions.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard.Security.Permissions;
|
||||
|
||||
namespace Orchard.Search {
|
||||
public class Permissions : IPermissionProvider {
|
||||
public static readonly Permission ManageSearchIndex = new Permission { Description = "Manage Search Index", Name = "ManageSearchIndex" };
|
||||
|
||||
public string ModuleName {
|
||||
get {
|
||||
return "Search";
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<Permission> GetPermissions() {
|
||||
return new Permission[] {
|
||||
ManageSearchIndex,
|
||||
};
|
||||
}
|
||||
|
||||
public IEnumerable<PermissionStereotype> GetDefaultStereotypes() {
|
||||
return new[] {
|
||||
new PermissionStereotype {
|
||||
Name = "Administrator",
|
||||
Permissions = new[] {ManageSearchIndex}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,8 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using Orchard.Collections;
|
||||
using Orchard.Indexing;
|
||||
|
||||
namespace Orchard.Search.Services {
|
||||
public interface ISearchService : IDependency {
|
||||
IEnumerable<ISearchHit> Query(string term);
|
||||
bool HasIndexToManage { get; }
|
||||
IPageOfItems<T> Query<T>(string query, int skip, int? take, Func<ISearchHit, T> shapeResult);
|
||||
void RebuildIndex();
|
||||
void UpdateIndex();
|
||||
DateTime GetIndexUpdatedUtc();
|
||||
}
|
||||
}
|
@@ -1,25 +1,84 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Orchard.Collections;
|
||||
using Orchard.Indexing;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Search.Models;
|
||||
using Orchard.UI.Notify;
|
||||
|
||||
namespace Orchard.Search.Services
|
||||
{
|
||||
public class SearchService : ISearchService
|
||||
{
|
||||
private const string SearchIndexName = "Search";
|
||||
private readonly IIndexManager _indexManager;
|
||||
private readonly IEnumerable<IIndexNotifierHandler> _indexNotifierHandlers;
|
||||
|
||||
public SearchService(IIndexManager indexManager) {
|
||||
public SearchService(IOrchardServices services, IIndexManager indexManager, IEnumerable<IIndexNotifierHandler> indexNotifierHandlers) {
|
||||
Services = services;
|
||||
_indexManager = indexManager;
|
||||
_indexNotifierHandlers = indexNotifierHandlers;
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
|
||||
public IEnumerable<ISearchHit> Query(string term) {
|
||||
if (string.IsNullOrWhiteSpace(term) || !_indexManager.HasIndexProvider())
|
||||
return Enumerable.Empty<ISearchHit>();
|
||||
public IOrchardServices Services { get; set; }
|
||||
public Localizer T { get; set; }
|
||||
|
||||
return _indexManager.GetSearchIndexProvider().CreateSearchBuilder("search")
|
||||
.WithField("title", term)
|
||||
.WithField("body", term)
|
||||
.Search();
|
||||
public bool HasIndexToManage {
|
||||
get { return _indexManager.HasIndexProvider(); }
|
||||
}
|
||||
|
||||
IPageOfItems<T> ISearchService.Query<T>(string query, int page, int? pageSize, Func<ISearchHit, T> shapeResult) {
|
||||
if (string.IsNullOrWhiteSpace(query) || !_indexManager.HasIndexProvider())
|
||||
return null;
|
||||
|
||||
var searchBuilder = _indexManager.GetSearchIndexProvider().CreateSearchBuilder(SearchIndexName)
|
||||
.WithField("title", query)
|
||||
.WithField("body", query);
|
||||
|
||||
var totalCount = searchBuilder.Count();
|
||||
if (pageSize != null)
|
||||
searchBuilder = searchBuilder
|
||||
.Slice((page > 0 ? page - 1 : 0) * (int)pageSize, (int)pageSize);
|
||||
|
||||
|
||||
var pageOfItems = new PageOfItems<T>(searchBuilder.Search().Select(shapeResult)) {
|
||||
PageNumber = page,
|
||||
PageSize = pageSize != null ? (int) pageSize : totalCount,
|
||||
TotalItemCount = totalCount
|
||||
};
|
||||
|
||||
return pageOfItems;
|
||||
}
|
||||
|
||||
void ISearchService.RebuildIndex() {
|
||||
if (!_indexManager.HasIndexProvider()) {
|
||||
Services.Notifier.Warning(T("There is no search index to rebuild."));
|
||||
return;
|
||||
}
|
||||
|
||||
var searchProvider = _indexManager.GetSearchIndexProvider();
|
||||
if (searchProvider.Exists(SearchIndexName))
|
||||
searchProvider.DeleteIndex(SearchIndexName);
|
||||
|
||||
searchProvider.CreateIndex(SearchIndexName); // or just reset the updated date and let the background process recreate the index
|
||||
Services.Notifier.Information(T("The search index has been rebuilt."));
|
||||
}
|
||||
|
||||
void ISearchService.UpdateIndex() {
|
||||
|
||||
foreach(var handler in _indexNotifierHandlers) {
|
||||
handler.UpdateIndex(SearchIndexName);
|
||||
}
|
||||
|
||||
Services.Notifier.Information(T("The search index has been updated."));
|
||||
}
|
||||
|
||||
DateTime ISearchService.GetIndexUpdatedUtc() {
|
||||
return !HasIndexToManage
|
||||
? DateTime.MinValue
|
||||
: _indexManager.GetSearchIndexProvider().GetLastIndexUtc(SearchIndexName);
|
||||
}
|
||||
}
|
||||
}
|
3
src/Orchard.Web/Modules/Orchard.Search/Styles/admin.css
Normal file
3
src/Orchard.Web/Modules/Orchard.Search/Styles/admin.css
Normal file
@@ -0,0 +1,3 @@
|
||||
#main button {
|
||||
display:block;
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using Orchard.Mvc.ViewModels;
|
||||
|
||||
namespace Orchard.Search.ViewModels {
|
||||
public class SearchIndexViewModel : BaseViewModel {
|
||||
public bool HasIndexToManage { get; set; }
|
||||
//todo: hang the index updated date off here to show in the admin UI (e.g. -> index updated: June 4, 2010 [update index])
|
||||
public DateTime IndexUpdatedUtc { get; set; }
|
||||
}
|
||||
}
|
@@ -1,9 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard.Collections;
|
||||
using Orchard.Mvc.ViewModels;
|
||||
|
||||
namespace Orchard.Search.ViewModels {
|
||||
public class SearchViewModel : BaseViewModel {
|
||||
public IEnumerable<SearchResultViewModel> Results { get; set; }
|
||||
public string Query { get; set; }
|
||||
public int DefaultPageSize { get; set; }
|
||||
public IPageOfItems<SearchResultViewModel> PageOfResults { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl<Orchard.Search.ViewModels.SearchIndexViewModel>" %>
|
||||
<%@ Import Namespace="Orchard.Mvc.Html" %><%
|
||||
Html.RegisterStyle("admin.css"); %>
|
||||
<h1><%=Html.TitleForPage(T("Search Index Mangement").ToString()) %></h1><%
|
||||
using (Html.BeginForm("update", "admin", FormMethod.Post, new {area = "Orchard.Search"})) { %>
|
||||
<fieldset>
|
||||
<p><%=T("The search index was last updated {0}. <button type=\"submit\" title=\"Update the search index.\" class=\"primaryAction\">Update</button>", Html.DateTimeRelative(Model.IndexUpdatedUtc))%></p>
|
||||
<%=Html.AntiForgeryTokenOrchard() %>
|
||||
</fieldset><%
|
||||
}
|
||||
using (Html.BeginForm("rebuild", "admin", FormMethod.Post, new {area = "Orchard.Search"})) { %>
|
||||
<fieldset>
|
||||
<p><%=T("Rebuild the search index for a fresh start. <button type=\"submit\" title=\"Rebuild the search index.\">Rebuild</button>") %></p>
|
||||
<%=Html.AntiForgeryTokenOrchard() %>
|
||||
</fieldset><%
|
||||
} %>
|
@@ -1,14 +1,17 @@
|
||||
<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl<Orchard.Search.ViewModels.SearchViewModel>" %>
|
||||
<%@ Import Namespace="Orchard.Mvc.Html" %><%
|
||||
Html.RegisterStyle("search.css"); %>
|
||||
<h1><%=Html.TitleForPage(T("Search"))%></h1>
|
||||
<%
|
||||
Html.Zone("search"); %><%
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Model.Query)) { %>
|
||||
<p class="search-summary"><%=T("<em>{0}</em> results", Model.Results.Count()) %></p><%
|
||||
<h1><%=Html.TitleForPage(T("Search").Text)%></h1><%
|
||||
Html.Zone("search");
|
||||
if (!string.IsNullOrWhiteSpace(Model.Query)) {
|
||||
if (Model.PageOfResults.Count() == 0) { %>
|
||||
<p class="search-summary"><%=T("<em>zero</em> results") %></p><%
|
||||
}
|
||||
|
||||
if (Model.Results.Count() > 0) { %>
|
||||
<%=Html.UnorderedList(Model.Results, (r, i) => Html.DisplayForItem(r.Content).ToHtmlString(), "search-results contentItems") %><%
|
||||
else { %>
|
||||
<p class="search-summary"><%=T("<em>{0} - {1}</em> of <em>{2}</em> results", Model.PageOfResults.StartPosition, Model.PageOfResults.EndPosition, Model.PageOfResults.TotalItemCount)%></p><%
|
||||
}
|
||||
}
|
||||
if (Model.PageOfResults != null && Model.PageOfResults.Count() > 0) { %>
|
||||
<%=Html.UnorderedList(Model.PageOfResults, (r, i) => Html.DisplayForItem(r.Content).ToHtmlString() , "search-results contentItems") %>
|
||||
<%=Html.Pager(Model.PageOfResults, Model.PageOfResults.PageNumber, Model.DefaultPageSize, new {q = Model.Query}) %><%
|
||||
} %>
|
@@ -64,6 +64,7 @@ namespace Orchard.Setup.Services {
|
||||
"Navigation",
|
||||
"Scheduling",
|
||||
"Indexing",
|
||||
"Localization",
|
||||
"Settings",
|
||||
"XmlRpc",
|
||||
"Orchard.Users",
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl<LogOnViewModel>" %>
|
||||
<%@ Import Namespace="Orchard.Users.ViewModels"%>
|
||||
<h1><%=Html.TitleForPage(Model.Title)%></h1>
|
||||
<p><%=_Encoded("Please enter your username and password.")%> <%= Html.ActionLink(T("Register"), "Register")%><%=_Encoded(" if you don't have an account.")%></p>
|
||||
<p><%=_Encoded("Please enter your username and password.")%> <%= Html.ActionLink(T("Register").Text, "Register")%><%=_Encoded(" if you don't have an account.")%></p>
|
||||
<%= Html.ValidationSummary(T("Login was unsuccessful. Please correct the errors and try again.").ToString())%>
|
||||
<%
|
||||
using (Html.BeginFormAntiForgeryPost(Url.Action("LogOn", new {ReturnUrl = Request.QueryString["ReturnUrl"]}))) { %>
|
||||
|
@@ -57,7 +57,6 @@ body {
|
||||
}
|
||||
button {
|
||||
font-family:Segoe UI,Trebuchet,Arial,Sans-Serif;
|
||||
font-size:1.01em;
|
||||
}
|
||||
body#preview {
|
||||
min-width:0;
|
||||
@@ -157,6 +156,7 @@ table.items th, table.items td, table.items caption { font-size:1.4em; line-heig
|
||||
table.items p, table.items label, table.items input, table.items .button { font-size:1em; line-height:1em; }
|
||||
p .button { font-size:inherit; }
|
||||
.meta, .hint { font-size:1.2em; } /* 12px */
|
||||
form.link button { font-size:1.01em; }
|
||||
|
||||
/* Links
|
||||
----------------------------------------------------------*/
|
||||
@@ -450,9 +450,6 @@ button, .button, .button:link, .button:visited {
|
||||
text-align:center;
|
||||
padding:0 .8em .1em;
|
||||
}
|
||||
button {
|
||||
padding-top:.08em;
|
||||
}
|
||||
form.link button {
|
||||
background:inherit;
|
||||
border:0;
|
||||
|
@@ -8,6 +8,7 @@
|
||||
\Windows\Microsoft.Net\Framework\v2.x\Config
|
||||
-->
|
||||
<configuration>
|
||||
|
||||
<appSettings/>
|
||||
<system.diagnostics configSource="Config\Diagnostics.config"/>
|
||||
<!--
|
||||
@@ -26,6 +27,9 @@
|
||||
during development.
|
||||
-->
|
||||
<compilation debug="true" targetFramework="4.0">
|
||||
<buildProviders>
|
||||
<add extension=".csproj" type="Orchard.Environment.Extensions.Compilers.CSharpExtensionBuildProvider"/>
|
||||
</buildProviders>
|
||||
<assemblies>
|
||||
<add assembly="System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
|
||||
<add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
|
||||
@@ -115,6 +119,7 @@
|
||||
</system.webServer>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<probing privatePath="App_Data/Dependencies"/>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35"/>
|
||||
<bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0"/>
|
||||
|
12
src/Orchard/Collections/IPageOfItems.cs
Normal file
12
src/Orchard/Collections/IPageOfItems.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Orchard.Collections {
|
||||
public interface IPageOfItems<out T> : IEnumerable<T> {
|
||||
int PageNumber { get; set; }
|
||||
int PageSize { get; set; }
|
||||
int TotalItemCount { get; set; }
|
||||
int TotalPageCount { get; }
|
||||
int StartPosition { get; }
|
||||
int EndPosition { get; }
|
||||
}
|
||||
}
|
28
src/Orchard/Collections/PageOfItems.cs
Normal file
28
src/Orchard/Collections/PageOfItems.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Orchard.Collections {
|
||||
public class PageOfItems<T> : List<T>, IPageOfItems<T> {
|
||||
public PageOfItems(IEnumerable<T> items) {
|
||||
AddRange(items);
|
||||
}
|
||||
|
||||
#region IPageOfItems<T> Members
|
||||
|
||||
public int PageNumber { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public int TotalItemCount { get; set; }
|
||||
|
||||
public int TotalPageCount {
|
||||
get { return (int) Math.Ceiling((double) TotalItemCount/PageSize); }
|
||||
}
|
||||
public int StartPosition {
|
||||
get { return (PageNumber - 1)*PageSize + 1; }
|
||||
}
|
||||
public int EndPosition {
|
||||
get { return PageNumber * PageSize > TotalItemCount ? TotalItemCount : PageNumber * PageSize; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@@ -45,7 +45,7 @@ namespace Orchard.Commands {
|
||||
propertyInfo.SetValue(this, stringValue, null);
|
||||
}
|
||||
else {
|
||||
throw new InvalidOperationException(T("No property named {0} found of type bool, int or string.", commandSwitch));
|
||||
throw new InvalidOperationException(T("No property named {0} found of type bool, int or string.", commandSwitch).ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ namespace Orchard.Commands {
|
||||
CheckMethodForSwitches(context.CommandDescriptor.MethodInfo, context.Switches);
|
||||
object[] invokeParameters = GetInvokeParametersForMethod(context.CommandDescriptor.MethodInfo, (context.Arguments ?? Enumerable.Empty<string>()).ToArray());
|
||||
if (invokeParameters == null) {
|
||||
throw new InvalidOperationException(T("Command arguments don't match"));
|
||||
throw new InvalidOperationException(T("Command arguments don't match").ToString());
|
||||
}
|
||||
|
||||
this.Context = context;
|
||||
@@ -105,7 +105,7 @@ namespace Orchard.Commands {
|
||||
}
|
||||
foreach (var commandSwitch in switches.Keys) {
|
||||
if (!supportedSwitches.Contains(commandSwitch)) {
|
||||
throw new InvalidOperationException(T("Method {0} does not support switch {1}.", methodInfo.Name, commandSwitch));
|
||||
throw new InvalidOperationException(T("Method {0} does not support switch {1}.", methodInfo.Name, commandSwitch).ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,21 @@
|
||||
using System.Web.Compilation;
|
||||
|
||||
namespace Orchard.Environment.Extensions.Compilers {
|
||||
public class CSharpExtensionBuildProvider : BuildProvider {
|
||||
private readonly CompilerType _codeCompilerType;
|
||||
|
||||
public CSharpExtensionBuildProvider() {
|
||||
_codeCompilerType = GetDefaultCompilerTypeForLanguage("C#");
|
||||
}
|
||||
|
||||
public override CompilerType CodeCompilerType { get { return _codeCompilerType; } }
|
||||
|
||||
public override void GenerateCode(AssemblyBuilder assemblyBuilder) {
|
||||
var virtualPathProvider = new DefaultVirtualPathProvider();
|
||||
var compiler = new CSharpProjectMediumTrustCompiler(virtualPathProvider);
|
||||
|
||||
var aspNetAssemblyBuilder = new AspNetAssemblyBuilder(assemblyBuilder, this);
|
||||
compiler.CompileProject(this.VirtualPath, aspNetAssemblyBuilder);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
using System.CodeDom.Compiler;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Web.Compilation;
|
||||
|
||||
namespace Orchard.Environment.Extensions.Compilers {
|
||||
/// <summary>
|
||||
/// Compile a C# extension into an assembly given a directory location
|
||||
/// </summary>
|
||||
public class CSharpExtensionCompiler {
|
||||
public CompilerResults CompileProject(string location) {
|
||||
var codeProvider = CodeDomProvider.CreateProvider("cs");
|
||||
|
||||
var references = GetAssemblyReferenceNames();
|
||||
var options = new CompilerParameters(references.ToArray());
|
||||
|
||||
var fileNames = GetSourceFileNames(location);
|
||||
var results = codeProvider.CompileAssemblyFromFile(options, fileNames.ToArray());
|
||||
return results;
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetAssemblyReferenceNames() {
|
||||
return Enumerable.Distinct<string>(BuildManager.GetReferencedAssemblies()
|
||||
.OfType<Assembly>()
|
||||
.Select(x => x.Location)
|
||||
.Where(x => !string.IsNullOrEmpty(x)));
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetSourceFileNames(string path) {
|
||||
foreach (var file in Directory.GetFiles(path, "*.cs")) {
|
||||
yield return file;
|
||||
}
|
||||
|
||||
foreach (var folder in Directory.GetDirectories(path)) {
|
||||
if (Path.GetFileName(folder).StartsWith("."))
|
||||
continue;
|
||||
|
||||
foreach (var file in GetSourceFileNames(folder)) {
|
||||
yield return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
using System.CodeDom.Compiler;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Orchard.Environment.Extensions.Compilers {
|
||||
/// <summary>
|
||||
/// Compile a C# extension into an assembly given a directory location
|
||||
/// </summary>
|
||||
public class CSharpProjectFullTrustCompiler {
|
||||
private readonly IVirtualPathProvider _virtualPathProvider;
|
||||
private readonly IBuildManager _buildManager;
|
||||
|
||||
public CSharpProjectFullTrustCompiler(IVirtualPathProvider virtualPathProvider, IBuildManager buildManager) {
|
||||
_virtualPathProvider = virtualPathProvider;
|
||||
_buildManager = buildManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compile a csproj file given its virtual path. Use the CSharp CodeDomProvider
|
||||
/// class, which is only available in full trust.
|
||||
/// </summary>
|
||||
public CompilerResults CompileProject(string virtualPath, string outputDirectory) {
|
||||
var codeProvider = CodeDomProvider.CreateProvider("cs");
|
||||
var directory = _virtualPathProvider.GetDirectoryName(virtualPath);
|
||||
|
||||
using (var stream = _virtualPathProvider.OpenFile(virtualPath)) {
|
||||
var descriptor = new CSharpProjectParser().Parse(stream);
|
||||
|
||||
var references = GetReferencedAssembliesLocation();
|
||||
var options = new CompilerParameters(references.ToArray());
|
||||
options.GenerateExecutable = false;
|
||||
options.OutputAssembly = Path.Combine(outputDirectory, descriptor.AssemblyName + ".dll");
|
||||
|
||||
var fileNames = descriptor.SourceFilenames
|
||||
.Select(f => _virtualPathProvider.Combine(directory, f))
|
||||
.Select(f => _virtualPathProvider.MapPath(f));
|
||||
|
||||
var results = codeProvider.CompileAssemblyFromFile(options, fileNames.ToArray());
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetReferencedAssembliesLocation() {
|
||||
return _buildManager.GetReferencedAssemblies()
|
||||
.Select(a => a.Location)
|
||||
.Where(a => !string.IsNullOrEmpty(a))
|
||||
.Distinct();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
using System.CodeDom;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Orchard.Environment.Extensions.Compilers {
|
||||
/// <summary>
|
||||
/// Compile a C# extension into an assembly given a directory location
|
||||
/// </summary>
|
||||
public class CSharpProjectMediumTrustCompiler {
|
||||
private readonly IVirtualPathProvider _virtualPathProvider;
|
||||
|
||||
public CSharpProjectMediumTrustCompiler(IVirtualPathProvider virtualPathProvider) {
|
||||
_virtualPathProvider = virtualPathProvider;
|
||||
}
|
||||
/// <summary>
|
||||
/// Compile a csproj file given its virtual path, a build provider and an assembly builder.
|
||||
/// This method works in medium trust.
|
||||
/// </summary>
|
||||
public void CompileProject(string virtualPath, IAssemblyBuilder assemblyBuilder) {
|
||||
using (var stream = _virtualPathProvider.OpenFile(virtualPath)) {
|
||||
var descriptor = new CSharpProjectParser().Parse(stream);
|
||||
|
||||
var directory = _virtualPathProvider.GetDirectoryName(virtualPath);
|
||||
foreach (var filename in descriptor.SourceFilenames.Select(f => _virtualPathProvider.Combine(directory, f))) {
|
||||
assemblyBuilder.AddCodeCompileUnit(CreateCompileUnit(filename));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private CodeCompileUnit CreateCompileUnit(string virtualPath) {
|
||||
var contents = GetContents(virtualPath);
|
||||
var unit = new CodeSnippetCompileUnit(contents);
|
||||
var physicalPath = _virtualPathProvider.MapPath(virtualPath);
|
||||
if (!string.IsNullOrEmpty(physicalPath)) {
|
||||
unit.LinePragma = new CodeLinePragma(physicalPath, 1);
|
||||
}
|
||||
return unit;
|
||||
}
|
||||
|
||||
private string GetContents(string virtualPath) {
|
||||
string contents;
|
||||
using (var stream = _virtualPathProvider.OpenFile(virtualPath)) {
|
||||
using (var reader = new StreamReader(stream)) {
|
||||
contents = reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Orchard.Environment.Extensions.Compilers {
|
||||
public class CSharpProjectDescriptor {
|
||||
public string AssemblyName { get; set; }
|
||||
public IEnumerable<string> SourceFilenames { get; set; }
|
||||
public IEnumerable<ReferenceDescriptor> References { get; set; }
|
||||
}
|
||||
|
||||
public class ReferenceDescriptor {
|
||||
public string AssemblyName { get; set; }
|
||||
|
||||
public override string ToString() {
|
||||
return "{" + (AssemblyName ?? "") + "}";
|
||||
}
|
||||
}
|
||||
|
||||
public class CSharpProjectParser {
|
||||
public CSharpProjectDescriptor Parse(Stream stream) {
|
||||
var document = XDocument.Load(XmlReader.Create(stream));
|
||||
return new CSharpProjectDescriptor {
|
||||
AssemblyName = GetAssemblyName(document),
|
||||
SourceFilenames = GetSourceFilenames(document).ToArray(),
|
||||
References = GetReferences(document).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
private string GetAssemblyName(XDocument document) {
|
||||
return document
|
||||
.Elements(ns("Project"))
|
||||
.Elements(ns("PropertyGroup"))
|
||||
.Elements(ns("AssemblyName"))
|
||||
.Single()
|
||||
.Value;
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetSourceFilenames(XDocument document) {
|
||||
return document
|
||||
.Elements(ns("Project"))
|
||||
.Elements(ns("ItemGroup"))
|
||||
.Elements(ns("Compile"))
|
||||
.Attributes("Include")
|
||||
.Select(c => c.Value);
|
||||
}
|
||||
|
||||
private IEnumerable<ReferenceDescriptor> GetReferences(XDocument document) {
|
||||
return document
|
||||
.Elements(ns("Project"))
|
||||
.Elements(ns("ItemGroup"))
|
||||
.Elements(ns("Reference"))
|
||||
.Attributes("Include")
|
||||
.Select(c => new ReferenceDescriptor { AssemblyName = c.Value });
|
||||
}
|
||||
|
||||
private static XName ns(string name) {
|
||||
return XName.Get(name, "http://schemas.microsoft.com/developer/msbuild/2003");
|
||||
}
|
||||
}
|
||||
}
|
@@ -53,7 +53,7 @@ namespace Orchard.Environment.Extensions {
|
||||
private Feature LoadFeature(FeatureDescriptor featureDescriptor) {
|
||||
var featureName = featureDescriptor.Name;
|
||||
string extensionName = GetExtensionForFeature(featureName);
|
||||
if (extensionName == null) throw new ArgumentException(T("Feature {0} was not found in any of the installed extensions", featureName));
|
||||
if (extensionName == null) throw new ArgumentException(T("Feature {0} was not found in any of the installed extensions", featureName).ToString());
|
||||
var extension = BuildActiveExtensions().Where(x => x.Descriptor.Name == extensionName).FirstOrDefault();
|
||||
if (extension == null) throw new InvalidOperationException(T("Extension ") + extensionName + T(" is not active"));
|
||||
|
||||
|
@@ -5,7 +5,7 @@ using Orchard.Environment.Extensions.Models;
|
||||
|
||||
namespace Orchard.Environment.Extensions.Loaders {
|
||||
public class AreaExtensionLoader : IExtensionLoader {
|
||||
public int Order { get { return 5; } }
|
||||
public int Order { get { return 50; } }
|
||||
|
||||
public ExtensionEntry Load(ExtensionDescriptor descriptor) {
|
||||
if (descriptor.Location == "~/Areas") {
|
||||
|
@@ -4,12 +4,14 @@ using System.Reflection;
|
||||
using Orchard.Environment.Extensions.Models;
|
||||
|
||||
namespace Orchard.Environment.Extensions.Loaders {
|
||||
/// <summary>
|
||||
/// Load an extension by looking into specific namespaces of the "Orchard.Core" assembly
|
||||
/// </summary>
|
||||
public class CoreExtensionLoader : IExtensionLoader {
|
||||
public int Order { get { return 1; } }
|
||||
public int Order { get { return 10; } }
|
||||
|
||||
public ExtensionEntry Load(ExtensionDescriptor descriptor) {
|
||||
if (descriptor.Location == "~/Core") {
|
||||
|
||||
var assembly = Assembly.Load("Orchard.Core");
|
||||
return new ExtensionEntry {
|
||||
Descriptor = descriptor,
|
||||
|
@@ -1,59 +1,41 @@
|
||||
using System.CodeDom.Compiler;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Web.Compilation;
|
||||
using System.Web.Hosting;
|
||||
using System;
|
||||
using Orchard.Environment.Extensions.Models;
|
||||
using Orchard.FileSystems.Dependencies;
|
||||
|
||||
namespace Orchard.Environment.Extensions.Loaders {
|
||||
public class DynamicExtensionLoader : IExtensionLoader {
|
||||
public int Order { get { return 10; } }
|
||||
private readonly IHostEnvironment _hostEnvironment;
|
||||
private readonly IBuildManager _buildManager;
|
||||
private readonly IVirtualPathProvider _virtualPathProvider;
|
||||
private readonly IDependenciesFolder _dependenciesFolder;
|
||||
|
||||
public DynamicExtensionLoader(IHostEnvironment hostEnvironment, IBuildManager buildManager, IVirtualPathProvider virtualPathProvider, IDependenciesFolder dependenciesFolder) {
|
||||
_hostEnvironment = hostEnvironment;
|
||||
_buildManager = buildManager;
|
||||
_virtualPathProvider = virtualPathProvider;
|
||||
_dependenciesFolder = dependenciesFolder;
|
||||
}
|
||||
|
||||
public int Order { get { return 100; } }
|
||||
|
||||
public ExtensionEntry Load(ExtensionDescriptor descriptor) {
|
||||
if (HostingEnvironment.IsHosted == false)
|
||||
string projectPath = _virtualPathProvider.Combine(descriptor.Location, descriptor.Name,
|
||||
descriptor.Name + ".csproj");
|
||||
if (!_virtualPathProvider.FileExists(projectPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var codeProvider = CodeDomProvider.CreateProvider("cs");
|
||||
var assembly = _buildManager.GetCompiledAssembly(projectPath);
|
||||
|
||||
var references = GetAssemblyReferenceNames();
|
||||
var options = new CompilerParameters(references.ToArray());
|
||||
|
||||
var locationPath = HostingEnvironment.MapPath(descriptor.Location);
|
||||
var extensionPath = Path.Combine(locationPath, descriptor.Name);
|
||||
|
||||
var fileNames = GetSourceFileNames(extensionPath);
|
||||
var results = codeProvider.CompileAssemblyFromFile(options, fileNames.ToArray());
|
||||
if (_hostEnvironment.IsFullTrust) {
|
||||
_dependenciesFolder.StoreAssemblyFile(descriptor.Name, assembly.Location);
|
||||
}
|
||||
|
||||
return new ExtensionEntry {
|
||||
Descriptor = descriptor,
|
||||
Assembly = results.CompiledAssembly,
|
||||
ExportedTypes = results.CompiledAssembly.GetExportedTypes(),
|
||||
Assembly = assembly,
|
||||
ExportedTypes = assembly.GetExportedTypes(),
|
||||
};
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetAssemblyReferenceNames() {
|
||||
return BuildManager.GetReferencedAssemblies()
|
||||
.OfType<Assembly>()
|
||||
.Select(x => x.Location)
|
||||
.Where(x => !string.IsNullOrEmpty(x))
|
||||
.Distinct();
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetSourceFileNames(string path) {
|
||||
foreach (var file in Directory.GetFiles(path, "*.cs")) {
|
||||
yield return file;
|
||||
}
|
||||
|
||||
foreach (var folder in Directory.GetDirectories(path)) {
|
||||
if (Path.GetFileName(folder).StartsWith("."))
|
||||
continue;
|
||||
|
||||
foreach (var file in GetSourceFileNames(folder)) {
|
||||
yield return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,17 +1,39 @@
|
||||
using Orchard.Environment.Extensions.Models;
|
||||
using Orchard.FileSystems.Dependencies;
|
||||
|
||||
namespace Orchard.Environment.Extensions.Loaders {
|
||||
/// <summary>
|
||||
/// Load an extension by looking into the "bin" subdirectory of an
|
||||
/// extension directory.
|
||||
/// </summary>
|
||||
public class PrecompiledExtensionLoader : IExtensionLoader {
|
||||
public int Order { get { return 3; } }
|
||||
private readonly IDependenciesFolder _folder;
|
||||
private readonly IVirtualPathProvider _virtualPathProvider;
|
||||
|
||||
public PrecompiledExtensionLoader(IDependenciesFolder folder, IVirtualPathProvider virtualPathProvider) {
|
||||
_folder = folder;
|
||||
_virtualPathProvider = virtualPathProvider;
|
||||
}
|
||||
|
||||
public int Order { get { return 30; } }
|
||||
|
||||
public ExtensionEntry Load(ExtensionDescriptor descriptor) {
|
||||
//var assembly = Assembly.Load(descriptor.Name);
|
||||
//return new ModuleEntry {
|
||||
// Descriptor = descriptor,
|
||||
// Assembly = assembly,
|
||||
// ExportedTypes = assembly.GetExportedTypes()
|
||||
//};
|
||||
var extensionPath = _virtualPathProvider.Combine(descriptor.Location, descriptor.Name, "bin",
|
||||
descriptor.Name + ".dll");
|
||||
if (!_virtualPathProvider.FileExists(extensionPath))
|
||||
return null;
|
||||
|
||||
_folder.StoreAssemblyFile(descriptor.Name, _virtualPathProvider.MapPath(extensionPath));
|
||||
|
||||
var assembly = _folder.LoadAssembly(descriptor.Name);
|
||||
if (assembly == null)
|
||||
return null;
|
||||
|
||||
return new ExtensionEntry {
|
||||
Descriptor = descriptor,
|
||||
Assembly = assembly,
|
||||
ExportedTypes = assembly.GetExportedTypes()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
using Orchard.Environment.Extensions.Models;
|
||||
using Orchard.FileSystems.Dependencies;
|
||||
|
||||
namespace Orchard.Environment.Extensions.Loaders {
|
||||
/// <summary>
|
||||
/// Load an extension using the "Assembly.Load" method if the
|
||||
/// file can be found in the "App_Data/Dependencies" folder.
|
||||
/// </summary>
|
||||
public class ProbingExtensionLoader : IExtensionLoader {
|
||||
private readonly IDependenciesFolder _folder;
|
||||
|
||||
public ProbingExtensionLoader(IDependenciesFolder folder) {
|
||||
_folder = folder;
|
||||
}
|
||||
|
||||
public int Order { get { return 40; } }
|
||||
|
||||
public ExtensionEntry Load(ExtensionDescriptor descriptor) {
|
||||
var assembly = _folder.LoadAssembly(descriptor.Name);
|
||||
if (assembly == null)
|
||||
return null;
|
||||
|
||||
return new ExtensionEntry {
|
||||
Descriptor = descriptor,
|
||||
Assembly = assembly,
|
||||
ExportedTypes = assembly.GetExportedTypes()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,8 +5,11 @@ using System.Web.Hosting;
|
||||
using Orchard.Environment.Extensions.Models;
|
||||
|
||||
namespace Orchard.Environment.Extensions.Loaders {
|
||||
/// <summary>
|
||||
/// Load an extension by looking through the BuildManager referenced assemblies
|
||||
/// </summary>
|
||||
public class ReferencedExtensionLoader : IExtensionLoader {
|
||||
public int Order { get { return 2; } }
|
||||
public int Order { get { return 20; } }
|
||||
|
||||
public ExtensionEntry Load(ExtensionDescriptor descriptor) {
|
||||
if (HostingEnvironment.IsHosted == false)
|
||||
|
22
src/Orchard/Environment/IAssemblyBuilder.cs
Normal file
22
src/Orchard/Environment/IAssemblyBuilder.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.CodeDom;
|
||||
using System.Web.Compilation;
|
||||
|
||||
namespace Orchard.Environment {
|
||||
public interface IAssemblyBuilder {
|
||||
void AddCodeCompileUnit(CodeCompileUnit compileUnit);
|
||||
}
|
||||
|
||||
public class AspNetAssemblyBuilder : IAssemblyBuilder {
|
||||
private readonly AssemblyBuilder _assemblyBuilder;
|
||||
private readonly BuildProvider _buildProvider;
|
||||
|
||||
public AspNetAssemblyBuilder(AssemblyBuilder assemblyBuilder, BuildProvider buildProvider) {
|
||||
_assemblyBuilder = assemblyBuilder;
|
||||
_buildProvider = buildProvider;
|
||||
}
|
||||
|
||||
public void AddCodeCompileUnit(CodeCompileUnit compileUnit) {
|
||||
_assemblyBuilder.AddCodeCompileUnit(_buildProvider, compileUnit);
|
||||
}
|
||||
}
|
||||
}
|
21
src/Orchard/Environment/IBuildManager.cs
Normal file
21
src/Orchard/Environment/IBuildManager.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Web.Compilation;
|
||||
|
||||
namespace Orchard.Environment {
|
||||
public interface IBuildManager : IDependency {
|
||||
IEnumerable<Assembly> GetReferencedAssemblies();
|
||||
Assembly GetCompiledAssembly(string virtualPath);
|
||||
}
|
||||
|
||||
public class DefaultBuildManager : IBuildManager {
|
||||
public IEnumerable<Assembly> GetReferencedAssemblies() {
|
||||
return BuildManager.GetReferencedAssemblies().OfType<Assembly>();
|
||||
}
|
||||
|
||||
public Assembly GetCompiledAssembly(string virtualPath) {
|
||||
return BuildManager.GetCompiledAssembly(virtualPath);
|
||||
}
|
||||
}
|
||||
}
|
22
src/Orchard/Environment/IHostEnvironment.cs
Normal file
22
src/Orchard/Environment/IHostEnvironment.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Web.Hosting;
|
||||
|
||||
namespace Orchard.Environment {
|
||||
/// <summary>
|
||||
/// Abstraction of the running environment
|
||||
/// </summary>
|
||||
public interface IHostEnvironment : IDependency {
|
||||
bool IsFullTrust { get; }
|
||||
string MapPath(string virtualPath);
|
||||
}
|
||||
|
||||
public class DefaultHostEnvironment : IHostEnvironment {
|
||||
public bool IsFullTrust {
|
||||
get { return AppDomain.CurrentDomain.IsFullyTrusted; }
|
||||
}
|
||||
|
||||
public string MapPath(string virtualPath) {
|
||||
return HostingEnvironment.MapPath(virtualPath);
|
||||
}
|
||||
}
|
||||
}
|
50
src/Orchard/Environment/IVirtualPathProvider.cs
Normal file
50
src/Orchard/Environment/IVirtualPathProvider.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.IO;
|
||||
using System.Web.Hosting;
|
||||
using Orchard.Caching;
|
||||
|
||||
namespace Orchard.Environment {
|
||||
public interface IVirtualPathProvider : IVolatileProvider {
|
||||
string GetDirectoryName(string virtualPath);
|
||||
string Combine(params string[] paths);
|
||||
Stream OpenFile(string virtualPath);
|
||||
StreamWriter CreateText(string virtualPath);
|
||||
string MapPath(string virtualPath);
|
||||
bool FileExists(string virtualPath);
|
||||
bool DirectoryExists(string virtualPath);
|
||||
void CreateDirectory(string virtualPath);
|
||||
}
|
||||
|
||||
public class DefaultVirtualPathProvider : IVirtualPathProvider {
|
||||
public string GetDirectoryName(string virtualPath) {
|
||||
return Path.GetDirectoryName(virtualPath).Replace('\\', '/');
|
||||
}
|
||||
|
||||
public string Combine(params string[] paths) {
|
||||
return Path.Combine(paths).Replace('\\', '/');
|
||||
}
|
||||
|
||||
public Stream OpenFile(string virtualPath) {
|
||||
return HostingEnvironment.VirtualPathProvider.GetFile(virtualPath).Open();
|
||||
}
|
||||
|
||||
public StreamWriter CreateText(string virtualPath) {
|
||||
return File.CreateText(MapPath(virtualPath));
|
||||
}
|
||||
|
||||
public string MapPath(string virtualPath) {
|
||||
return HostingEnvironment.MapPath(virtualPath);
|
||||
}
|
||||
|
||||
public bool FileExists(string virtualPath) {
|
||||
return HostingEnvironment.VirtualPathProvider.FileExists(virtualPath);
|
||||
}
|
||||
|
||||
public bool DirectoryExists(string virtualPath) {
|
||||
return HostingEnvironment.VirtualPathProvider.DirectoryExists(virtualPath);
|
||||
}
|
||||
|
||||
public void CreateDirectory(string virtualPath) {
|
||||
Directory.CreateDirectory(MapPath(virtualPath));
|
||||
}
|
||||
}
|
||||
}
|
@@ -15,6 +15,7 @@ using Orchard.Environment.State;
|
||||
using Orchard.Environment.Topology;
|
||||
using Orchard.Events;
|
||||
using Orchard.FileSystems.AppData;
|
||||
using Orchard.FileSystems.Dependencies;
|
||||
using Orchard.FileSystems.WebSite;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Services;
|
||||
@@ -30,10 +31,14 @@ namespace Orchard.Environment {
|
||||
// a single default host implementation is needed for bootstrapping a web app domain
|
||||
builder.RegisterType<DefaultOrchardEventBus>().As<IEventBus>().SingleInstance();
|
||||
builder.RegisterType<DefaultCacheHolder>().As<ICacheHolder>().SingleInstance();
|
||||
builder.RegisterType<DefaultHostEnvironment>().As<IHostEnvironment>().SingleInstance();
|
||||
builder.RegisterType<DefaultBuildManager>().As<IBuildManager>().SingleInstance();
|
||||
|
||||
RegisterVolatileProvider<WebSiteFolder, IWebSiteFolder>(builder);
|
||||
RegisterVolatileProvider<AppDataFolder, IAppDataFolder>(builder);
|
||||
RegisterVolatileProvider<Clock, IClock>(builder);
|
||||
RegisterVolatileProvider<DefaultDependenciesFolder, IDependenciesFolder>(builder);
|
||||
RegisterVolatileProvider<DefaultVirtualPathProvider, IVirtualPathProvider>(builder);
|
||||
|
||||
builder.RegisterType<DefaultOrchardHost>().As<IOrchardHost>().As<IEventHandler>().SingleInstance();
|
||||
{
|
||||
@@ -63,6 +68,7 @@ namespace Orchard.Environment {
|
||||
builder.RegisterType<CoreExtensionLoader>().As<IExtensionLoader>().SingleInstance();
|
||||
builder.RegisterType<ReferencedExtensionLoader>().As<IExtensionLoader>().SingleInstance();
|
||||
builder.RegisterType<PrecompiledExtensionLoader>().As<IExtensionLoader>().SingleInstance();
|
||||
builder.RegisterType<ProbingExtensionLoader>().As<IExtensionLoader>().SingleInstance();
|
||||
builder.RegisterType<DynamicExtensionLoader>().As<IExtensionLoader>().SingleInstance();
|
||||
}
|
||||
}
|
||||
|
@@ -148,13 +148,13 @@ namespace Orchard.Environment.State {
|
||||
}));
|
||||
|
||||
// lower enabled states in reverse order
|
||||
foreach (var entry in allEntries.Where(entry => entry.FeatureState.EnableState == ShellFeatureState.State.Falling)) {
|
||||
foreach (var entry in allEntries.Reverse().Where(entry => entry.FeatureState.EnableState == ShellFeatureState.State.Falling)) {
|
||||
_featureEvents.Disable(entry.Feature);
|
||||
_stateManager.UpdateEnabledState(entry.FeatureState, ShellFeatureState.State.Down);
|
||||
}
|
||||
|
||||
// lower installed states in reverse order
|
||||
foreach (var entry in allEntries.Where(entry => entry.FeatureState.InstallState == ShellFeatureState.State.Falling)) {
|
||||
foreach (var entry in allEntries.Reverse().Where(entry => entry.FeatureState.InstallState == ShellFeatureState.State.Falling)) {
|
||||
_featureEvents.Uninstall(entry.Feature);
|
||||
_stateManager.UpdateInstalledState(entry.FeatureState, ShellFeatureState.State.Down);
|
||||
}
|
||||
|
124
src/Orchard/FileSystems/Dependencies/IDependenciesFolder.cs
Normal file
124
src/Orchard/FileSystems/Dependencies/IDependenciesFolder.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Xml.Linq;
|
||||
using Orchard.Caching;
|
||||
using Orchard.Environment;
|
||||
|
||||
namespace Orchard.FileSystems.Dependencies {
|
||||
public interface IDependenciesFolder : IVolatileProvider {
|
||||
void StoreAssemblyFile(string assemblyName, string assemblyFileName);
|
||||
Assembly LoadAssembly(string assemblyName);
|
||||
}
|
||||
|
||||
public class DefaultDependenciesFolder : IDependenciesFolder {
|
||||
private const string _basePath = "~/App_Data/Dependencies";
|
||||
private readonly IVirtualPathProvider _virtualPathProvider;
|
||||
|
||||
public DefaultDependenciesFolder(IVirtualPathProvider virtualPathProvider) {
|
||||
_virtualPathProvider = virtualPathProvider;
|
||||
}
|
||||
|
||||
private string BasePath {
|
||||
get {
|
||||
return _basePath;
|
||||
}
|
||||
}
|
||||
|
||||
private string PersistencePath {
|
||||
get {
|
||||
return _virtualPathProvider.Combine(BasePath, "dependencies.xml");
|
||||
}
|
||||
}
|
||||
|
||||
public void StoreAssemblyFile(string assemblyName, string assemblyFileName) {
|
||||
_virtualPathProvider.CreateDirectory(BasePath);
|
||||
|
||||
// Only store assembly if it's more recent that what we have stored already (if anything)
|
||||
if (IsNewerAssembly(assemblyName, assemblyFileName)) {
|
||||
var destinationFileName = Path.GetFileName(assemblyFileName);
|
||||
var destinationPath = _virtualPathProvider.MapPath(_virtualPathProvider.Combine(BasePath, destinationFileName));
|
||||
File.Copy(assemblyFileName, destinationPath);
|
||||
|
||||
StoreDepencyInformation(assemblyName, destinationFileName);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsNewerAssembly(string assemblyName, string assemblyFileName) {
|
||||
var dependency = ReadDependencies().SingleOrDefault(d => d.Name == assemblyName);
|
||||
if (dependency == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var existingFileName = _virtualPathProvider.MapPath(_virtualPathProvider.Combine(BasePath, dependency.FileName));
|
||||
if (!File.Exists(existingFileName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (File.GetCreationTimeUtc(existingFileName) <= File.GetCreationTimeUtc(assemblyFileName));
|
||||
}
|
||||
|
||||
private void StoreDepencyInformation(string name, string fileName) {
|
||||
var dependencies = ReadDependencies().ToList();
|
||||
|
||||
var dependency = dependencies.SingleOrDefault(d => d.Name == name);
|
||||
if (dependency == null) {
|
||||
dependency = new DependencyDescritpor { Name = name, FileName = fileName };
|
||||
dependencies.Add(dependency);
|
||||
}
|
||||
dependency.FileName = fileName;
|
||||
|
||||
WriteDependencies(dependencies);
|
||||
}
|
||||
|
||||
public Assembly LoadAssembly(string assemblyName) {
|
||||
_virtualPathProvider.CreateDirectory(BasePath);
|
||||
|
||||
var dependency = ReadDependencies().SingleOrDefault(d => d.Name == assemblyName);
|
||||
if (dependency == null)
|
||||
return null;
|
||||
|
||||
if (!_virtualPathProvider.FileExists(_virtualPathProvider.Combine(BasePath, dependency.FileName)))
|
||||
return null;
|
||||
|
||||
return Assembly.Load(Path.GetFileNameWithoutExtension(dependency.FileName));
|
||||
}
|
||||
|
||||
private class DependencyDescritpor {
|
||||
public string Name { get; set; }
|
||||
public string FileName { get; set; }
|
||||
}
|
||||
|
||||
private IEnumerable<DependencyDescritpor> ReadDependencies() {
|
||||
if (!_virtualPathProvider.FileExists(PersistencePath))
|
||||
return Enumerable.Empty<DependencyDescritpor>();
|
||||
|
||||
using (var stream = _virtualPathProvider.OpenFile(PersistencePath)) {
|
||||
XDocument document = XDocument.Load(stream);
|
||||
return document
|
||||
.Elements(ns("Dependencies"))
|
||||
.Elements(ns("Dependency"))
|
||||
.Select(e => new DependencyDescritpor { Name = e.Element("Name").Value, FileName = e.Element("FileName").Value })
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteDependencies(IEnumerable<DependencyDescritpor> dependencies) {
|
||||
var document = new XDocument();
|
||||
document.Add(new XElement(ns("Dependencies")));
|
||||
var elements = dependencies.Select(d => new XElement("Dependency",
|
||||
new XElement(ns("Name"), d.Name),
|
||||
new XElement(ns("FileName"), d.FileName)));
|
||||
document.Root.Add(elements);
|
||||
|
||||
using (var stream = _virtualPathProvider.CreateText(PersistencePath)) {
|
||||
document.Save(stream, SaveOptions.None);
|
||||
}
|
||||
}
|
||||
|
||||
private static XName ns(string name) {
|
||||
return XName.Get(name/*, "http://schemas.microsoft.com/developer/msbuild/2003"*/);
|
||||
}
|
||||
}
|
||||
}
|
5
src/Orchard/Indexing/IIndexNotifierHandler.cs
Normal file
5
src/Orchard/Indexing/IIndexNotifierHandler.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace Orchard.Indexing {
|
||||
public interface IIndexNotifierHandler : IEvents {
|
||||
void UpdateIndex(string indexName);
|
||||
}
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Orchard.Indexing {
|
||||
public interface IIndexProvider : IDependency {
|
||||
@@ -17,6 +18,16 @@ namespace Orchard.Indexing {
|
||||
/// </summary>
|
||||
void DeleteIndex(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Whether an index is empty or not
|
||||
/// </summary>
|
||||
bool IsEmpty(string indexName);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of indexed documents
|
||||
/// </summary>
|
||||
int NumDocs(string indexName);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an empty document
|
||||
/// </summary>
|
||||
@@ -48,5 +59,16 @@ namespace Orchard.Indexing {
|
||||
/// </summary>
|
||||
/// <returns>A search builder instance</returns>
|
||||
ISearchBuilder CreateSearchBuilder(string indexName);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the date and time when the index was last processed
|
||||
/// </summary>
|
||||
DateTime GetLastIndexUtc(string indexName);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the date and time when the index was last processed
|
||||
/// </summary>
|
||||
void SetLastIndexUtc(string indexName, DateTime lastIndexUtc);
|
||||
|
||||
}
|
||||
}
|
@@ -4,7 +4,7 @@ using System.Collections.Generic;
|
||||
namespace Orchard.Indexing {
|
||||
public interface ISearchBuilder {
|
||||
|
||||
ISearchBuilder Parse(string query);
|
||||
ISearchBuilder Parse(string defaultField, string query);
|
||||
|
||||
ISearchBuilder WithField(string field, string value);
|
||||
ISearchBuilder WithField(string field, string value, bool wildcardSearch);
|
||||
|
@@ -12,12 +12,12 @@ namespace Orchard.Localization {
|
||||
return new LocalizedString(x);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return _localized;
|
||||
public string Text {
|
||||
get { return _localized; }
|
||||
}
|
||||
|
||||
public static implicit operator string(LocalizedString x) {
|
||||
return x._localized;
|
||||
public override string ToString() {
|
||||
return _localized;
|
||||
}
|
||||
|
||||
public override int GetHashCode() {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web;
|
||||
@@ -46,6 +47,13 @@ namespace Orchard.Localization.Services {
|
||||
return String.Empty;
|
||||
}
|
||||
|
||||
public int GetCultureIdByName(string cultureName) {
|
||||
if (!IsValidCulture(cultureName)) {
|
||||
throw new ArgumentException("cultureName");
|
||||
}
|
||||
return _cultureRepository.Get(x => x.Culture == cultureName).Id;
|
||||
}
|
||||
|
||||
// "<languagecode2>" or
|
||||
// "<languagecode2>-<country/regioncode2>" or
|
||||
// "<languagecode2>-<scripttag>-<country/regioncode2>"
|
||||
|
@@ -6,5 +6,6 @@ namespace Orchard.Localization.Services {
|
||||
IEnumerable<string> ListCultures();
|
||||
void AddCulture(string cultureName);
|
||||
string GetCurrentCulture(HttpContext requestContext);
|
||||
int GetCultureIdByName(string cultureName);
|
||||
}
|
||||
}
|
||||
|
@@ -3,10 +3,10 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web.Mvc;
|
||||
using System.Web.Mvc.Html;
|
||||
using System.Web.Routing;
|
||||
using Orchard.Collections;
|
||||
using Orchard.Mvc.ViewModels;
|
||||
using Orchard.Services;
|
||||
using Orchard.Settings;
|
||||
@@ -41,6 +41,74 @@ namespace Orchard.Mvc.Html {
|
||||
return MvcHtmlString.Create(builder.ToString(TagRenderMode.Normal));
|
||||
}
|
||||
|
||||
#region Pager
|
||||
|
||||
public static string Pager<T>(this HtmlHelper html, IPageOfItems<T> pageOfItems, int currentPage, int defaultPageSize, object values = null, string previousText = "<", string nextText = ">", bool alwaysShowPreviousAndNext = false) {
|
||||
if (pageOfItems.TotalPageCount < 2)
|
||||
return "";
|
||||
|
||||
var sb = new StringBuilder(75);
|
||||
var rvd = new RouteValueDictionary {{"q", ""},{"page", 0}};
|
||||
var viewContext = html.ViewContext;
|
||||
var urlHelper = new UrlHelper(viewContext.RequestContext);
|
||||
|
||||
if (pageOfItems.PageSize != defaultPageSize)
|
||||
rvd.Add("pagesize", pageOfItems.PageSize);
|
||||
|
||||
foreach (var item in viewContext.RouteData.Values) {
|
||||
rvd.Add(item.Key, item.Value);
|
||||
}
|
||||
|
||||
|
||||
if (values != null) {
|
||||
var rvd2 = new RouteValueDictionary(values);
|
||||
|
||||
foreach (var item in rvd2) {
|
||||
rvd[item.Key] = item.Value;
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("<p class=\"pager\">");
|
||||
|
||||
if (currentPage > 1 || alwaysShowPreviousAndNext) {
|
||||
if (currentPage == 2)
|
||||
rvd.Remove("page");
|
||||
else
|
||||
rvd["page"] = currentPage - 1;
|
||||
|
||||
sb.AppendFormat(" <a href=\"{1}\" class=\"previous\">{0}</a>", previousText,
|
||||
urlHelper.RouteUrl(rvd));
|
||||
}
|
||||
|
||||
//todo: when there are many pages (> 15?) maybe do something like 1 2 3...6 7 8...13 14 15
|
||||
for (var p = 1; p <= pageOfItems.TotalPageCount; p++) {
|
||||
if (p == currentPage) {
|
||||
sb.AppendFormat(" <span>{0}</span>", p);
|
||||
}
|
||||
else {
|
||||
if (p == 1)
|
||||
rvd.Remove("page");
|
||||
else
|
||||
rvd["page"] = p;
|
||||
|
||||
sb.AppendFormat(" <a href=\"{1}\">{0}</a>", p,
|
||||
urlHelper.RouteUrl(rvd));
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPage < pageOfItems.TotalPageCount || alwaysShowPreviousAndNext) {
|
||||
rvd["page"] = currentPage + 1;
|
||||
sb.AppendFormat("<a href=\"{1}\" class=\"next\">{0}</a>", nextText,
|
||||
urlHelper.RouteUrl(rvd));
|
||||
}
|
||||
|
||||
sb.Append("</p>");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UnorderedList
|
||||
|
||||
public static string UnorderedList<T>(this HtmlHelper htmlHelper, IEnumerable<T> items, Func<T, int, string> generateContent, string cssClass) {
|
||||
@@ -111,7 +179,7 @@ namespace Orchard.Mvc.Html {
|
||||
TimeSpan time = htmlHelper.Resolve<IClock>().UtcNow - value;
|
||||
|
||||
if (time.TotalDays > 7)
|
||||
return "at " + htmlHelper.DateTime(value);
|
||||
return "on " + htmlHelper.DateTime(value, "MMM d yyyy 'at' h:mm tt");
|
||||
if (time.TotalHours > 24)
|
||||
return string.Format("{0} day{1} ago", time.Days, time.Days == 1 ? "" : "s");
|
||||
if (time.TotalMinutes > 60)
|
||||
|
@@ -130,6 +130,8 @@
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Collections\IPageOfItems.cs" />
|
||||
<Compile Include="Collections\PageOfItems.cs" />
|
||||
<Compile Include="ContentManagement\Aspects\ICommonAspect.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
@@ -345,6 +347,17 @@
|
||||
<Compile Include="Data\DataModule.cs" />
|
||||
<Compile Include="Data\Orderable.cs" />
|
||||
<Compile Include="Environment\DefaultOrchardShell.cs" />
|
||||
<Compile Include="Environment\IHostEnvironment.cs" />
|
||||
<Compile Include="FileSystems\Dependencies\IDependenciesFolder.cs" />
|
||||
<Compile Include="Environment\Extensions\Loaders\ProbingExtensionLoader.cs" />
|
||||
<Compile Include="Environment\Extensions\Compilers\CSharpExtensionBuildProvider.cs" />
|
||||
<Compile Include="Environment\Extensions\Compilers\CSharpExtensionCompiler.cs" />
|
||||
<Compile Include="Environment\Extensions\Compilers\CSharpProjectMediumTrustCompiler.cs" />
|
||||
<Compile Include="Environment\Extensions\Compilers\CSharpProjectFullTrustCompiler.cs" />
|
||||
<Compile Include="Environment\Extensions\Compilers\CSharpProjectParser.cs" />
|
||||
<Compile Include="Environment\IAssemblyBuilder.cs" />
|
||||
<Compile Include="Environment\IBuildManager.cs" />
|
||||
<Compile Include="Environment\IVirtualPathProvider.cs" />
|
||||
<Compile Include="Environment\IOrchardShell.cs" />
|
||||
<Compile Include="Environment\OrchardStarter.cs" />
|
||||
<Compile Include="Environment\State\DefaultProcessingEngine.cs" />
|
||||
@@ -353,6 +366,7 @@
|
||||
<Compile Include="Environment\State\IShellStateManager.cs" />
|
||||
<Compile Include="Environment\State\ShellStateCoordinator.cs" />
|
||||
<Compile Include="IDependency.cs" />
|
||||
<Compile Include="Indexing\IIndexNotifierHandler.cs" />
|
||||
<Compile Include="Localization\Services\DefaultCultureManager.cs" />
|
||||
<Compile Include="Localization\Services\DefaultResourceManager.cs" />
|
||||
<Compile Include="Indexing\DefaultIndexManager.cs" />
|
||||
|
@@ -15,14 +15,9 @@ namespace Orchard.Tasks.Indexing {
|
||||
void CreateDeleteIndexTask(ContentItem contentItem);
|
||||
|
||||
/// <summary>
|
||||
/// Loads all indexing tasks created after to a specific date and time
|
||||
/// Returns the Date Time of the last task created
|
||||
/// </summary>
|
||||
IEnumerable<IIndexingTask> GetTasks(DateTime? createdAfter);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all indexing tasks previous to a specific date and time
|
||||
/// </summary>
|
||||
void DeleteTasks(DateTime? createdBefore);
|
||||
DateTime GetLastTaskDateTime();
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all indexing tasks assigned to a specific content item
|
||||
|
@@ -1,35 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Orchard.Localization;
|
||||
|
||||
namespace Orchard.UI.Navigation {
|
||||
public class NavigationBuilder {
|
||||
IEnumerable<MenuItem> Contained { get; set; }
|
||||
|
||||
public NavigationBuilder Add(string caption, string position, Action<NavigationItemBuilder> itemBuilder) {
|
||||
public NavigationBuilder Add(LocalizedString caption, string position, Action<NavigationItemBuilder> itemBuilder) {
|
||||
var childBuilder = new NavigationItemBuilder();
|
||||
|
||||
if (!string.IsNullOrEmpty(caption))
|
||||
childBuilder.Caption(caption);
|
||||
|
||||
if (!string.IsNullOrEmpty(position))
|
||||
childBuilder.Position(position);
|
||||
|
||||
itemBuilder(childBuilder);
|
||||
Contained = (Contained ?? Enumerable.Empty<MenuItem>()).Concat(childBuilder.Build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public NavigationBuilder Add(string caption, Action<NavigationItemBuilder> itemBuilder) {
|
||||
public NavigationBuilder Add(LocalizedString caption, Action<NavigationItemBuilder> itemBuilder) {
|
||||
return Add(caption, null, itemBuilder);
|
||||
}
|
||||
public NavigationBuilder Add(Action<NavigationItemBuilder> itemBuilder) {
|
||||
return Add(null, null, itemBuilder);
|
||||
}
|
||||
public NavigationBuilder Add(string caption, string position) {
|
||||
public NavigationBuilder Add(LocalizedString caption, string position) {
|
||||
return Add(caption, position, x=> { });
|
||||
}
|
||||
public NavigationBuilder Add(string caption) {
|
||||
public NavigationBuilder Add(LocalizedString caption) {
|
||||
return Add(caption, null, x => { });
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Web.Routing;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Security.Permissions;
|
||||
|
||||
namespace Orchard.UI.Navigation {
|
||||
@@ -11,8 +12,9 @@ namespace Orchard.UI.Navigation {
|
||||
_item = new MenuItem();
|
||||
}
|
||||
|
||||
public NavigationItemBuilder Caption(string caption) {
|
||||
_item.Text = caption;
|
||||
public NavigationItemBuilder Caption(LocalizedString caption) {
|
||||
if (caption != null)
|
||||
_item.Text = caption.Text;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user