--HG--
branch : dev
This commit is contained in:
Louis DeJardin
2010-06-07 16:03:18 -07:00
68 changed files with 1563 additions and 287 deletions

View File

@@ -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);
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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

View File

@@ -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&#39;s Tenants"
And I should see "<span class="tenantName">Default</span>"
And the status should be 200 OK

View File

@@ -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 --%>

View 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";
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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; }
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}

View 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; }
}
}
}

View File

@@ -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; }
}
}

View 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

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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" />

View 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)));
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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);
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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" />

View 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}
},
};
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,3 @@
#main button {
display:block;
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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><%
} %>

View File

@@ -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}) %><%
} %>

View File

@@ -64,6 +64,7 @@ namespace Orchard.Setup.Services {
"Navigation",
"Scheduling",
"Indexing",
"Localization",
"Settings",
"XmlRpc",
"Orchard.Users",

View File

@@ -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"]}))) { %>

View File

@@ -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;

View File

@@ -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"/>

View 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; }
}
}

View 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
}
}

View File

@@ -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());
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}
}
}

View 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();
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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"));

View File

@@ -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") {

View File

@@ -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,

View File

@@ -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;
}
}
}
}
}

View 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()
};
}
}
}

View File

@@ -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()
};
}
}
}

View File

@@ -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)

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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));
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View 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"*/);
}
}
}

View File

@@ -0,0 +1,5 @@
namespace Orchard.Indexing {
public interface IIndexNotifierHandler : IEvents {
void UpdateIndex(string indexName);
}
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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() {

View File

@@ -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>"

View File

@@ -6,5 +6,6 @@ namespace Orchard.Localization.Services {
IEnumerable<string> ListCultures();
void AddCulture(string cultureName);
string GetCurrentCulture(HttpContext requestContext);
int GetCultureIdByName(string cultureName);
}
}

View File

@@ -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)

View File

@@ -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" />

View File

@@ -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

View File

@@ -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 => { });
}

View File

@@ -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;
}