diff --git a/HaWeb/Controllers/APIController.cs b/HaWeb/Controllers/APIController.cs index 4771994..6e66c3e 100644 --- a/HaWeb/Controllers/APIController.cs +++ b/HaWeb/Controllers/APIController.cs @@ -116,7 +116,7 @@ public class APIController : Controller { return UnprocessableEntity(ModelState); //// 4. Stage: Is it a Hamann-Document? What kind? - var retdocs = _xmlService.ProbeHamannFile(xdocument, ModelState); + var retdocs = _xmlService.ProbeFile(xdocument, ModelState); if (!ModelState.IsValid || retdocs == null || !retdocs.Any()) return UnprocessableEntity(ModelState); diff --git a/HaWeb/Controllers/SucheController.cs b/HaWeb/Controllers/SucheController.cs index a0fe009..31e0a66 100644 --- a/HaWeb/Controllers/SucheController.cs +++ b/HaWeb/Controllers/SucheController.cs @@ -6,17 +6,20 @@ using HaDocument.Interfaces; using HaDocument.Models; using HaXMLReader.Interfaces; using System.Collections.Specialized; +using HaWeb.XMLParser; namespace HaWeb.Controllers; public class SucheController : Controller { private IHaDocumentWrappper _lib; private IReaderService _readerService; + private IXMLService _xmlService; private int _lettersForPage; - public SucheController(IHaDocumentWrappper lib, IReaderService readerService, IConfiguration config) { + public SucheController(IHaDocumentWrappper lib, IReaderService readerService, IXMLService service, IConfiguration config) { _lib = lib; _readerService = readerService; + _xmlService = service; _lettersForPage = config.GetValue("LettersOnPage"); } @@ -33,7 +36,6 @@ public class SucheController : Controller { [Route("Suche/{zhvolume}/{zhpage}")] public IActionResult GoToZH(string zhvolume, string zhpage) { - // TODO: Bug in letter parsing: dictionary is WRONG! if (String.IsNullOrWhiteSpace(zhvolume) || String.IsNullOrWhiteSpace(zhpage)) return _error404(); zhvolume = zhvolume.Trim(); zhpage = zhpage.Trim(); @@ -68,7 +70,7 @@ public class SucheController : Controller { List>? metasbyyear = null; if (search != null) { search = search.Trim(); - var res = _lib.SearchLetters(search, _readerService); + var res = _xmlService.SearchCollection("letters", search, _readerService); if (res == null || !res.Any()) return _error404(); var ret = res.ToDictionary( x => x.Index, diff --git a/HaWeb/FileHelpers/HaDocumentWrapper.cs b/HaWeb/FileHelpers/HaDocumentWrapper.cs index e75ce25..f5a25ce 100644 --- a/HaWeb/FileHelpers/HaDocumentWrapper.cs +++ b/HaWeb/FileHelpers/HaDocumentWrapper.cs @@ -6,19 +6,22 @@ using System.Collections.Concurrent; using System.Threading.Tasks; using HaXMLReader.Interfaces; using HaWeb.SearchHelpers; +using HaWeb.XMLParser; using System.Text; public class HaDocumentWrapper : IHaDocumentWrappper { private ILibrary Library; private IXMLProvider _xmlProvider; + private IXMLService _xmlService; public int StartYear { get; private set; } public int EndYear { get; private set; } - public List? SearchableLetters { get; private set; } + // public List? SearchableLetters { get; private set; } - public HaDocumentWrapper(IXMLProvider xmlProvider, IConfiguration configuration) { + public HaDocumentWrapper(IXMLProvider xmlProvider, IXMLService service, IConfiguration configuration) { _xmlProvider = xmlProvider; + _xmlService = service; StartYear = configuration.GetValue("AvailableStartYear"); EndYear = configuration.GetValue("AvailableEndYear"); var filelist = xmlProvider.GetHamannFiles(); @@ -41,52 +44,26 @@ public class HaDocumentWrapper : IHaDocumentWrappper { } public ILibrary? SetLibrary(string filepath, ModelStateDictionary? ModelState = null) { + var sw = new System.Diagnostics.Stopwatch(); + sw.Start(); try { Library = HaDocument.Document.Create(new HaWeb.Settings.HaDocumentOptions() { HamannXMLFilePath = filepath, AvailableYearRange = (StartYear, EndYear) }); } catch (Exception ex) { if (ModelState != null) ModelState.AddModelError("Error", "Das Dokument konnte nicht geparst werden: " + ex.Message); return null; } + sw.Stop(); + Console.WriteLine("ILIB: " + sw.ElapsedMilliseconds); + sw.Restart(); - var searchableletters = new ConcurrentBag(); - var letters = Library.Letters.Values; - - Parallel.ForEach(letters, letter => { - var o = new SearchHelpers.SeachableItem(letter.Index, _prepareSearch(letter)); - searchableletters.Add(o); - }); - - this.SearchableLetters = searchableletters.ToList(); + if (_xmlService != null) + _xmlService.SetInProduction(System.Xml.Linq.XDocument.Load(filepath, System.Xml.Linq.LoadOptions.PreserveWhitespace)); + sw.Stop(); + Console.WriteLine("COLLECTIONS: " + sw.ElapsedMilliseconds); return Library; } - public List<(string Index, List<(string Page, string Line, string Preview)> Results)>? SearchLetters(string searchword, IReaderService reader) { - if (SearchableLetters == null) return null; - var res = new ConcurrentBag<(string Index, List<(string Page, string Line, string preview)> Results)>(); - var sw = StringHelpers.NormalizeWhiteSpace(searchword.Trim()); - Parallel.ForEach(SearchableLetters, (letter) => { - var state = new SearchState(sw); - var rd = reader.RequestStringReader(letter.SearchText); - var parser = new HaWeb.HTMLParser.LineXMLHelper(state, rd, new StringBuilder(), null, null, null, SearchRules.TextRules, SearchRules.WhitespaceRules); - rd.Read(); - if (state.Results != null) - res.Add(( - letter.Index, - state.Results.Select(x => ( - x.Page, - x.Line, - parser.Lines != null ? - parser.Lines - .Where(y => y.Page == x.Page && y.Line == x.Line) - .Select(x => x.Text) - .FirstOrDefault(string.Empty) - : "" - )).ToList())); - }); - return res.ToList(); - } - public ILibrary GetLibrary() { return Library; } diff --git a/HaWeb/FileHelpers/IHaDocumentWrapper.cs b/HaWeb/FileHelpers/IHaDocumentWrapper.cs index 54ace8b..bec96ae 100644 --- a/HaWeb/FileHelpers/IHaDocumentWrapper.cs +++ b/HaWeb/FileHelpers/IHaDocumentWrapper.cs @@ -7,5 +7,4 @@ public interface IHaDocumentWrappper { public ILibrary ResetLibrary(); public ILibrary? SetLibrary(string filepath, ModelStateDictionary ModelState); public ILibrary GetLibrary(); - public List<(string Index, List<(string Page, string Line, string Preview)> Results)>? SearchLetters(string searchword, IReaderService reader); } \ No newline at end of file diff --git a/HaWeb/Models/CollectedItem.cs b/HaWeb/Models/CollectedItem.cs new file mode 100644 index 0000000..2d4054b --- /dev/null +++ b/HaWeb/Models/CollectedItem.cs @@ -0,0 +1,20 @@ +namespace HaWeb.Models; +using HaWeb.SearchHelpers; +using HaWeb.XMLParser; +using System.Xml.Linq; + +public class CollectedItem : ISearchable { + public string Index { get; private set; } + public string Collection { get; private set; } + public string? SearchText { get; private set; } + public XElement ELement { get; private set; } + public IXMLRoot Root { get; private set; } + + public CollectedItem(string index, XElement element, IXMLRoot root, string collection, string? searchtext = null) { + this.Index = index; + this.SearchText = searchtext; + this.Collection = collection; + this.Root = root; + this.ELement = element; + } +} \ No newline at end of file diff --git a/HaWeb/Models/FileList.cs b/HaWeb/Models/FileList.cs index 37096e4..0b9e240 100644 --- a/HaWeb/Models/FileList.cs +++ b/HaWeb/Models/FileList.cs @@ -32,9 +32,10 @@ public class FileList { public FileList Clone() { var ret = new FileList(this.XMLRoot); - foreach (var file in _Files) { - ret.Add(file); - } + if (_Files != null) + foreach (var file in _Files) { + ret.Add(file); + } return ret; } } \ No newline at end of file diff --git a/HaWeb/SearchHelpers/SearchableItem.cs b/HaWeb/SearchHelpers/SearchableItem.cs deleted file mode 100644 index 374de69..0000000 --- a/HaWeb/SearchHelpers/SearchableItem.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace HaWeb.SearchHelpers; - -public class SeachableItem : ISearchable { - public string Index { get; private set; } - public string SearchText { get; private set; } - - public SeachableItem(string index, string searchtext) { - this.Index = index; - this.SearchText = searchtext; - } -} \ No newline at end of file diff --git a/HaWeb/Settings/XMLRoots/CommentRoot.cs b/HaWeb/Settings/XMLRoots/CommentRoot.cs index b0cae8d..4168455 100644 --- a/HaWeb/Settings/XMLRoots/CommentRoot.cs +++ b/HaWeb/Settings/XMLRoots/CommentRoot.cs @@ -7,13 +7,23 @@ public class CommentRoot : HaWeb.XMLParser.IXMLRoot { public string Type { get; } = "Register"; public string Prefix { get; } = "register"; public string[] XPathContainer { get; } = { ".//data//kommentare/kommcat", ".//kommentare/kommcat" }; + public (string Key, string xPath, Func KeyFunc, bool Searchable)[]? XPathCollection { get; } = { + ("comments-register", "/opus/data/kommentare/kommcat[@value='neuzeit']/kommentar", GetKey, true), + ("comments-register", "/opus/kommentare/kommcat[@value='neuzeit']/kommentar", GetKey, true), + ("comments-edition", "/opus/data/kommentare/kommcat[@value='editionen']/kommentar", GetKey, true), + ("comments-edition", "/opus/kommentare/kommcat[@value='editionen']/kommentar", GetKey, true), + ("comments-forschung", "/opus/data/kommentare/kommcat[@value='forschung']/kommentar", GetKey, true), + ("comments-forschung", "/opus/kommentare/kommcat[@value='forschung']/kommentar", GetKey, true), + ("comments-bibel", "/opus/data/kommentare/kommcat[@value='bibel']/kommentar", GetKey, false), + ("comments-bibel", "/opus/kommentare/kommcat[@value='bibel']/kommentar", GetKey, false), + }; public Predicate IsCollectedObject { get; } = (elem) => { if (elem.Name == "kommentar") return true; else return false; }; - public Func GetKey { get; } = (elem) => { + public static Func GetKey { get; } = (elem) => { var index = elem.Attribute("id"); if (index != null && !String.IsNullOrWhiteSpace(index.Value)) return index.Value; diff --git a/HaWeb/Settings/XMLRoots/DescriptionsRoot.cs b/HaWeb/Settings/XMLRoots/DescriptionsRoot.cs index f89bf6b..981cf6d 100644 --- a/HaWeb/Settings/XMLRoots/DescriptionsRoot.cs +++ b/HaWeb/Settings/XMLRoots/DescriptionsRoot.cs @@ -7,18 +7,22 @@ public class DescriptionsRoot : HaWeb.XMLParser.IXMLRoot { public string Type { get; } = "Metadaten"; public string Prefix { get; } = "metadaten"; public string[] XPathContainer { get; } = { ".//data/descriptions", ".//descriptions" }; - + public (string Key, string xPath, Func KeyFunc, bool Searchable)[]? XPathCollection { get; } = { + ("metas", "/opus/descriptions/letterDesc", GetKey, false), + ("metas", "/opus/data/descriptions/letterDesc", GetKey, false) + }; + public Predicate IsCollectedObject { get; } = (elem) => { if (elem.Name == "letterDesc") return true; return false; }; - // public Func GetKey { get; } = (elem) => { - // var index = elem.Attribute("ref"); - // if (index != null && !String.IsNullOrWhiteSpace(index.Value)) - // return index.Value; - // else return null; - // }; + public static Func GetKey { get; } = (elem) => { + var index = elem.Attribute("ref"); + if (index != null && !String.IsNullOrWhiteSpace(index.Value)) + return index.Value; + return null; + }; public List<(string, string?)>? GenerateFields(XMLRootDocument document) { return null; diff --git a/HaWeb/Settings/XMLRoots/DocumentRoot.cs b/HaWeb/Settings/XMLRoots/DocumentRoot.cs index 984f29b..569e804 100644 --- a/HaWeb/Settings/XMLRoots/DocumentRoot.cs +++ b/HaWeb/Settings/XMLRoots/DocumentRoot.cs @@ -8,13 +8,17 @@ public class DocumentRoot : HaWeb.XMLParser.IXMLRoot { public string Type { get; } = "Brieftext"; public string Prefix { get; } = "brieftext"; public string[] XPathContainer { get; } = { ".//data/document", ".//document" }; + public (string Key, string xPath, Func KeyFunc, bool Searchable)[]? XPathCollection { get; } = { + ("letters", "/opus/data/document/letterText", GetKey, true), + ("letters", "/opus/document/letterText", GetKey, true) + }; public Predicate IsCollectedObject { get; } = (elem) => { if (elem.Name == "letterText") return true; else return false; }; - public Func GetKey { get; } = (elem) => { + public static Func GetKey { get; } = (elem) => { var index = elem.Attribute("index"); if (index != null && !String.IsNullOrWhiteSpace(index.Value)) return index.Value; diff --git a/HaWeb/Settings/XMLRoots/EditsRoot.cs b/HaWeb/Settings/XMLRoots/EditsRoot.cs index f914282..1fa1faf 100644 --- a/HaWeb/Settings/XMLRoots/EditsRoot.cs +++ b/HaWeb/Settings/XMLRoots/EditsRoot.cs @@ -7,13 +7,17 @@ public class EditsRoot : HaWeb.XMLParser.IXMLRoot { public string Type { get; } = "Texteingriffe"; public string Prefix { get; } = "texteingriffe"; public string[] XPathContainer { get; } = { ".//data/edits", ".//edits" }; - + public (string Key, string xPath, Func KeyFunc, bool Searchable)[]? XPathCollection { get; } = { + ("edits", "/data/edits/editreason", GetKey, true), + ("edits", "/edits/editreason", GetKey, true) + }; + public Predicate IsCollectedObject { get; } = (elem) => { if (elem.Name == "editreason") return true; else return false; }; - public Func GetKey { get; } = (elem) => { + public static Func GetKey { get; } = (elem) => { var index = elem.Attribute("index"); if (index != null && !String.IsNullOrWhiteSpace(index.Value)) return index.Value; diff --git a/HaWeb/Settings/XMLRoots/MarginalsRoot.cs b/HaWeb/Settings/XMLRoots/MarginalsRoot.cs index 796e873..e847c34 100644 --- a/HaWeb/Settings/XMLRoots/MarginalsRoot.cs +++ b/HaWeb/Settings/XMLRoots/MarginalsRoot.cs @@ -7,13 +7,17 @@ public class MarginalsRoot : HaWeb.XMLParser.IXMLRoot { public string Type { get; } = "Stellenkommentar"; public string Prefix { get; } = "stellenkommentar"; public string[] XPathContainer { get; } = { ".//data/marginalien", ".//marginalien" }; + public (string Key, string xPath, Func KeyFunc, bool Searchable)[]? XPathCollection { get; } = { + ("marginals", "/data/marginalien/marginal", GetKey, true), + ("marginals", "/marginalien/marginal", GetKey, true) + }; public Predicate IsCollectedObject { get; } = (elem) => { if (elem.Name == "marginal") return true; else return false; }; - public Func GetKey { get; } = (elem) => { + public static Func GetKey { get; } = (elem) => { var index = elem.Attribute("index"); if (index != null && !String.IsNullOrWhiteSpace(index.Value)) return index.Value; diff --git a/HaWeb/Settings/XMLRoots/ReferencesRoot.cs b/HaWeb/Settings/XMLRoots/ReferencesRoot.cs index 5406520..d04a534 100644 --- a/HaWeb/Settings/XMLRoots/ReferencesRoot.cs +++ b/HaWeb/Settings/XMLRoots/ReferencesRoot.cs @@ -7,6 +7,14 @@ public class ReferencesRoot : HaWeb.XMLParser.IXMLRoot { public string Type { get; } = "Personen / Orte"; public string Prefix { get; } = "personenorte"; public string[] XPathContainer { get; } = { ".//data/definitions", ".//definitions" }; + public (string Key, string xPath, Func KeyFunc, bool Searchable)[]? XPathCollection { get; } = { + ("person-definitions", "/opus/data/definitions/personDefs/personDef", GetKey, false), + ("person-definitions", "/opus/definitions/personDefs/personDef", GetKey, false), + ("hand-definitions", "/opus/data/definitions/handDefs/handDef", GetKey, false), + ("hand-definitions", "/opus/definitions/handDefs/handDef", GetKey, false), + ("location-definitions", "/opus/data/definitions/locationDefs/locationDef", GetKey, false), + ("location-definitions", "/opus/definitions/locationDefs/locationDef", GetKey, false) + }; public Predicate IsCollectedObject { get; } = (elem) => { if (elem.Name == "personDefs" || elem.Name == "structureDefs" || elem.Name == "handDefs" || elem.Name == "locationDefs") @@ -14,8 +22,8 @@ public class ReferencesRoot : HaWeb.XMLParser.IXMLRoot { return false; }; - public Func GetKey { get; } = (elem) => { - return elem.Name.ToString(); + public static Func GetKey { get; } = (elem) => { + return elem.Attribute("index") != null ? elem.Attribute("index")!.Value : null; }; public List<(string, string?)>? GenerateFields(XMLRootDocument document) { diff --git a/HaWeb/Settings/XMLRoots/TraditionsRoot.cs b/HaWeb/Settings/XMLRoots/TraditionsRoot.cs index 08abe30..1ad2ab5 100644 --- a/HaWeb/Settings/XMLRoots/TraditionsRoot.cs +++ b/HaWeb/Settings/XMLRoots/TraditionsRoot.cs @@ -7,17 +7,21 @@ public class TraditionsRoot : HaWeb.XMLParser.IXMLRoot { public string Type { get; } = "Überlieferung"; public string Prefix { get; } = "ueberlieferung"; public string[] XPathContainer { get; } = { ".//data/traditions", ".//traditions" }; + public (string Key, string xPath, Func KeyFunc, bool Searchable)[]? XPathCollection { get; } = { + ("tradition", "/opus/data/traditions/letterTradition", GetKey, true), + ("tradition", "/opus/traditions/letterTradition", GetKey, true) + }; public Predicate IsCollectedObject { get; } = (elem) => { if (elem.Name == "letterTradition") return true; else return false; }; - public Func GetKey { get; } = (elem) => { + public static Func GetKey { get; } = (elem) => { var index = elem.Attribute("ref"); if (index != null && !String.IsNullOrWhiteSpace(index.Value)) return index.Value; - else return null; + return null; }; public List<(string, string?)>? GenerateFields(XMLRootDocument document) { diff --git a/HaWeb/XMLParser/IXMLRoot.cs b/HaWeb/XMLParser/IXMLRoot.cs index a91b476..7a0b9b3 100644 --- a/HaWeb/XMLParser/IXMLRoot.cs +++ b/HaWeb/XMLParser/IXMLRoot.cs @@ -13,7 +13,15 @@ public interface IXMLRoot { // XPaths to determine if container is present public abstract string[] XPathContainer { get; } - // Tag Name of child objects to be collected + // Collections of Elements to be created from this Root + // Key: the key under which the element(s) will be files + // xPath: the (absolute) XPath to the element(s) + // KeyFunc: How to extrect an identifier for the single element in the collection + // Searchable: Will the element be indexed for full-text-search? + public abstract (string Key, string xPath, Func KeyFunc, bool Searchable)[]? XPathCollection { get; } + + // Determines child objects to be collected + // (deprecated see collections above; only used internally) public abstract Predicate IsCollectedObject { get; } // Gets the Key of a collected object diff --git a/HaWeb/XMLParser/IXMLService.cs b/HaWeb/XMLParser/IXMLService.cs index 6b68cc8..4a543fb 100644 --- a/HaWeb/XMLParser/IXMLService.cs +++ b/HaWeb/XMLParser/IXMLService.cs @@ -2,12 +2,13 @@ namespace HaWeb.XMLParser; using System.Xml.Linq; using Microsoft.AspNetCore.Mvc.ModelBinding; using HaWeb.Models; +using HaXMLReader.Interfaces; public interface IXMLService { public IXMLRoot? GetRoot(string name); public List? GetRootsList(); public Dictionary? GetRootsDictionary(); - public List? ProbeHamannFile(XDocument document, ModelStateDictionary ModelState); + public List? ProbeFile(XDocument document, ModelStateDictionary ModelState); public Dictionary? GetUsedDictionary(); public XElement? MergeUsedDocuments(ModelStateDictionary ModelState); public void Use(XMLRootDocument doc); @@ -17,4 +18,6 @@ public interface IXMLService { public void UnUse(string prefix); public void UnUseProduction(); public void SetInProduction(); + public void SetInProduction(XDocument document); + public List<(string Index, List<(string Page, string Line, string Preview)> Results)>? SearchCollection(string collection, string searchword, IReaderService reader); } \ No newline at end of file diff --git a/HaWeb/XMLParser/XMLService.cs b/HaWeb/XMLParser/XMLService.cs index 8f8810b..7bec00f 100644 --- a/HaWeb/XMLParser/XMLService.cs +++ b/HaWeb/XMLParser/XMLService.cs @@ -1,7 +1,13 @@ namespace HaWeb.XMLParser; using System.Xml.Linq; +using System.Xml.XPath; using Microsoft.AspNetCore.Mvc.ModelBinding; using HaWeb.Models; +using HaWeb.SearchHelpers; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using System.Text; +using HaXMLReader.Interfaces; public class XMLService : IXMLService { private Dictionary? _Used; @@ -9,6 +15,9 @@ public class XMLService : IXMLService { private Stack>? _InProduction; + private Dictionary> _collectedProduction; + private Dictionary> _collectedUsed; + public XMLService() { // Getting all classes which implement IXMLRoot for possible document endpoints var types = _GetAllTypesThatImplementInterface().ToList(); @@ -50,9 +59,63 @@ public class XMLService : IXMLService { _InProduction.Push(inProduction); } + public void SetInProduction(XDocument document) { + if (document == null || _Roots == null) return; + var ret = new ConcurrentDictionary>(); + Parallel.ForEach(_Roots, (root) => { + if (root.Value.XPathCollection != null) + foreach (var coll in root.Value.XPathCollection) { + var elem = document.XPathSelectElements(coll.xPath); + if (elem != null && elem.Any()) { + if (!ret.ContainsKey(coll.Key)) + ret[coll.Key] = new ConcurrentDictionary(); + foreach(var e in elem) { + var k = coll.KeyFunc(e); + if (k != null) { + var searchtext = coll.Searchable ? + StringHelpers.NormalizeWhiteSpace(e.ToString(), ' ', false) : + null; + ret[coll.Key][k] = new CollectedItem(k, e, root.Value, coll.Key, searchtext); + } + } + } + } + }); + _collectedProduction = ret.ToDictionary(x => x.Key, y => y.Value.ToDictionary(z => z.Key, f => f.Value, null), null); + } + + public List<(string Index, List<(string Page, string Line, string Preview)> Results)>? SearchCollection(string collection, string searchword, IReaderService reader) { + if (!_collectedProduction.ContainsKey(collection)) return null; + var searchableObjects = _collectedProduction[collection]; + var res = new ConcurrentBag<(string Index, List<(string Page, string Line, string preview)> Results)>(); + var sw = StringHelpers.NormalizeWhiteSpace(searchword.Trim()); + Parallel.ForEach(searchableObjects, (obj) => { + if (obj.Value.SearchText != null) { + var state = new SearchState(sw); + var rd = reader.RequestStringReader(obj.Value.SearchText); + var parser = new HaWeb.HTMLParser.LineXMLHelper(state, rd, new StringBuilder(), null, null, null, SearchRules.TextRules, SearchRules.WhitespaceRules); + rd.Read(); + if (state.Results != null) + res.Add(( + obj.Value.Index, + state.Results.Select(x => ( + x.Page, + x.Line, + parser.Lines != null ? + parser.Lines + .Where(y => y.Page == x.Page && y.Line == x.Line) + .Select(x => x.Text) + .FirstOrDefault(string.Empty) + : "" + )).ToList())); + } + }); + return res.ToList(); + } + public void UnUseProduction() => this._InProduction = null; - public List? ProbeHamannFile(XDocument document, ModelStateDictionary ModelState) { + public List? ProbeFile(XDocument document, ModelStateDictionary ModelState) { if (document.Root!.Name != "opus") { ModelState.AddModelError("Error", "A valid Hamann-Docuemnt must begin with "); return null;