using AqDWeb.Attributes; using AqDWeb.Exceptions; using AqDWeb.Services; using Lucene.Net.Documents; using NHibernate.Type; using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using System.Web; using YaDiary.Base; namespace YaDiary.Domain { /// A generic service for . It hooks /// into the NHibernate interceptor to update DotLucene indexes incrementally. /// [GenericService(typeof(IndexedArticle))] public class IndexedArticleService: VersionedEntityService where T: IndexedArticle { public delegate string Translate(string text); public override bool OnSave(object entity, object id, IDictionary namedState, IDictionary namedTypes) { T e = entity as T; // Skip historical objects. if (e.EntityTimestamp == DateTime.MinValue && IsSearchableEntity(namedState)) { // Set EntityID manually here because it's going to be used when saving // index. The change will be overrided when NHibernate refreshs the // entity from state (base.OnSave sets the same EntityID in state). e.EntityID = (Guid) id; SaveIndexes(new IndexedArticle[]{e}); } return base.OnSave(entity, id, namedState, namedTypes); } public override bool OnFlushDirty(object entity, object id, IDictionary namedCurrentState, IDictionary namedPreviousState, IDictionary namedTypes) { T e = entity as T; bool currentlySearchable = IsSearchableEntity(namedCurrentState); bool previouslySearchable = IsSearchableEntity(namedPreviousState); if (currentlySearchable && previouslySearchable && e.IsEntityDeleted) DeleteIndexes(new IndexedArticle[]{e}); else if (currentlySearchable && !previouslySearchable) SaveIndexes(new IndexedArticle[]{e}); else if (currentlySearchable) UpdateIndexes(new IndexedArticle[]{e}); else if (!currentlySearchable && previouslySearchable) DeleteIndexes(new IndexedArticle[]{e}); return base.OnFlushDirty(entity, id, namedCurrentState, namedPreviousState, namedTypes); } public virtual void ClearAllIndexes() { YaDiarySearchModule.Instance.ClearIndexes(); } public virtual void OptimizeAllIndexes() { YaDiarySearchModule.Instance.OptimizeIndexes(); } public virtual void SaveIndexes(IList entities) { UpdateIndexes(entities, true); } public virtual void UpdateIndexes(IList entities) { UpdateIndexes(entities, false); } public virtual void UpdateIndexes(IList entities, bool saveOnly) { YaDiarySearchModule.Instance.BatchUpdateIndexes( entities, delegate(IndexedArticle entity, YaDiarySearchModule.CreateDocument createDocument) { Document zhTwDoc = createDocument(); zhTwDoc.Add(Field.Keyword("language", "zh-TW")); SetupIndexDocument( entity, zhTwDoc, delegate (string text) { return text; } ); Document zhCnDoc = createDocument(); zhCnDoc.Add(Field.Keyword("language", "zh-CN")); ChineseConversionModule translator = ChineseConversionModule.Instance; SetupIndexDocument( entity, zhCnDoc, delegate (string text) { return translator.TranslateTraditionalToSimple(text); } ); return new Document[] {zhTwDoc, zhCnDoc}; }, saveOnly ); } public virtual void DeleteIndexes(IList entities) { YaDiarySearchModule.Instance.BatchDeleteIndexes(entities); } protected virtual void SetupIndexDocument(IndexedArticle entity, Document doc, Translate translate) { char splitChar = (entity.Keywords.Contains(",") ? ',': ' '); foreach (string keyword in entity.Keywords.Split(splitChar)) { Field kf = Field.Text("keyword", translate(keyword.Trim())); kf.SetBoost(3.0f); doc.Add(kf); } Field sf = Field.Text("subject", translate(entity.Subject)); sf.SetBoost(2.0f); doc.Add(sf); Field af = Field.Text("author", translate(entity.Author.Name)); af.SetBoost(1.5f); doc.Add(af); doc.Add(Field.Keyword("postdate", entity.PostDate)); doc.Add( Field.Text( "text", new StringReader(translate(ExtractHtmlBodyText(entity.Text))) ) ); if (entity is DiscussionAnswer) { doc.Add( Field.Keyword( "threadID", (entity as DiscussionAnswer).Question.ID.ToString()) ); } } protected virtual string ExtractHtmlBodyText(string html) { // Strip all tags and convert back html entity chars, this should be // sufficient for simple HTML body without javascript return HttpUtility.HtmlDecode(Regex.Replace(html, "<[^>]*>", "")); } protected virtual bool IsSearchableEntity(IDictionary namedState) { if (typeof(Discussion).IsAssignableFrom(typeof(T))) { try { return (bool) namedState["IsPublic"]; } catch (KeyNotFoundException ex) { throw new InvalidUserCodeException( typeof(T).FullName + " contains no \"IsPublic\" property?!", ex); } } else return true; } } }