diff --git a/GlyssenEngine/Project.cs b/GlyssenEngine/Project.cs index 7985d564..3e82d65b 100644 --- a/GlyssenEngine/Project.cs +++ b/GlyssenEngine/Project.cs @@ -84,6 +84,86 @@ public class Project : ProjectBase, IUserProject public Func IsOkayToClearExistingRefBlocksWhenChangingReferenceText { get; set; } + /// + /// Creates a project and parses it from a Paratext project's Scriptures. + /// + /// This factory method pattern is an alternative to some of the constructors in this class which launch + /// a background worker. It enables Vessel to await for the parsing to be finished, and to embed the entire + /// process within a try-catch. + public static async Task CreateAndLoadAsync(ParatextScrTextWrapper paratextProject, Action reportProgressAsPercent) + { + // Use the constructor that does not launch the ParseAndSetBooks background worker + var glyssenProject = new Project(paratextProject.GlyssenDblTextMetadata, null, false, paratextProject.WritingSystem); + + // From the other constructor + Writer.SetUpProjectPersistence(glyssenProject); + ProcessParatextProjectQuotationRules(glyssenProject, paratextProject); + + // Sanity Checks + if (glyssenProject.m_books.Any()) + throw new InvalidOperationException("Project already contains books. If the intention is to replace the existing ones, let's clear the list first. Otherwise, call ParseAndIncludeBooks."); + if (glyssenProject.Versification == null) + throw new NullReferenceException("Project is missing versification"); + + // Replaces background worker approach with an awaitable + await glyssenProject.ParseAndSetBooksAsync(paratextProject, reportProgressAsPercent); + return glyssenProject; + } + + /// + /// Parses the Paratext books of Scripture, converts them into BookScripts, places them into the project, + /// and performs various initializations. + /// + /// TODO: This method has the code from UsxWorker_DoWork and UsxWorker_RunWorkerCompleted. A complete violation of DRY, + /// in that this code now lives in the two places. A reasonable refactoring would be to use this Factory approach of CreateAndLoadAsync + /// instead of launching the background worker in the Project constructor. I (JSW) have insufficient understanding of Glyssen + /// architecture to attempt that myself. Aug 17, 2020. + private async Task ParseAndSetBooksAsync(ParatextScrTextWrapper paratextProject, Action reportProgressAsPercent) + { + await Task.Run(() => + { + ProjectState = ProjectState.Initial | (ProjectState & ProjectState.WritingSystemRecoveryInProcess); + + // Relevant code from UsxWorker_DoWork. + var bookScripts = UsxParser.ParseBooks(paratextProject.UsxDocumentsForIncludedBooks, + paratextProject.Stylesheet, reportProgressAsPercent); + + // TODO: The Vessel use-case does not require a postParseAction, thus it is not included here. + + foreach (var bookScript in bookScripts) + { + ThrowIfNullBookScript(bookScripts, bookScript); + bookScript.Initialize(Versification); + } + + ProcessNewProjectBooks(bookScripts); + ProcessQuotes(bookScripts); + }); + } + + /// + /// This code is an attempt to figure out how we are getting null reference exceptions when using the + /// objects in the list (See PG-275 & PG-287) + /// + private void ThrowIfNullBookScript(ICollection bookScripts, BookScript bookScript) + { + if (bookScript?.BookId != null) + return; + + var nonNullBookScripts = bookScripts + .Where(b => b != null) + .Select(b => b.BookId); + + var nonNullBookScriptsStr = Join(";", nonNullBookScripts); + + var initialMessage = bookScript == null + ? "BookScript is null." + : "BookScript has null BookId."; + + throw new ApplicationException($"{initialMessage} Number of BookScripts: {bookScripts.Count}. " + + $"BookScripts which are NOT null: {nonNullBookScriptsStr}"); + } + /// Paratext was unable to access the project (only pertains to /// Glyssen projects that are associated with a live Paratext project) public Project(GlyssenDblTextMetadata metadata, string recordingProjectName = null, bool installFonts = false, @@ -199,13 +279,17 @@ public Project(ParatextScrTextWrapper paratextProject) : this(paratextProject.GlyssenDblTextMetadata, null, false, paratextProject.WritingSystem) { Writer.SetUpProjectPersistence(this); + ProcessParatextProjectQuotationRules(this, paratextProject); + ParseAndSetBooks(paratextProject.UsxDocumentsForIncludedBooks, paratextProject.Stylesheet); + } + + public static void ProcessParatextProjectQuotationRules(Project glyssenProject, ParatextScrTextWrapper paratextProject) + { if (paratextProject.HasQuotationRulesSet) { - QuoteSystemStatus = QuoteSystemStatus.Obtained; - SetWsQuotationMarksUsingFullySpecifiedContinuers(paratextProject.QuotationMarks); + glyssenProject.QuoteSystemStatus = QuoteSystemStatus.Obtained; + glyssenProject.SetWsQuotationMarksUsingFullySpecifiedContinuers(paratextProject.QuotationMarks); } - - ParseAndSetBooks(paratextProject.UsxDocumentsForIncludedBooks, paratextProject.Stylesheet); } /// @@ -1570,15 +1654,25 @@ private void UsxWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEvent } else { - m_books.AddRange(bookScripts); - m_projectMetadata.ParserVersion = kParserVersion; - if (m_books.All(b => IsNullOrEmpty(b.PageHeader))) - ChapterAnnouncementStyle = ChapterAnnouncement.ChapterLabel; - UpdateControlFileVersion(); - RemoveAvailableBooksThatDoNotCorrespondToExistingBooks(); - AddMissingAvailableBooks(); + ProcessNewProjectBooks(bookScripts); } + ProcessQuotes(bookScripts); + } + + public void ProcessNewProjectBooks(List bookScripts) + { + m_books.AddRange(bookScripts); + m_projectMetadata.ParserVersion = kParserVersion; + if (m_books.All(b => IsNullOrEmpty(b.PageHeader))) + ChapterAnnouncementStyle = ChapterAnnouncement.ChapterLabel; + UpdateControlFileVersion(); + RemoveAvailableBooksThatDoNotCorrespondToExistingBooks(); + AddMissingAvailableBooks(); + } + + public void ProcessQuotes(List bookScripts) + { if (QuoteSystem == null) GuessAtQuoteSystem(); else if (IsQuoteSystemReadyForParse)