1 /*
2  *  "GEDKeeper", the personal genealogical database editor.
3  *  Copyright (C) 2009-2021 by Sergey V. Zhdanovskih.
4  *
5  *  This file is part of "GEDKeeper".
6  *
7  *  This program is free software: you can redistribute it and/or modify
8  *  it under the terms of the GNU General Public License as published by
9  *  the Free Software Foundation, either version 3 of the License, or
10  *  (at your option) any later version.
11  *
12  *  This program is distributed in the hope that it will be useful,
13  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  *  GNU General Public License for more details.
16  *
17  *  You should have received a copy of the GNU General Public License
18  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 using System;
22 using BSLib;
23 using GKCore;
24 using GKCore.Interfaces;
25 using GKCore.Types;
26 
27 namespace GDModel.Providers.GEDCOM
28 {
29     /// <summary>
30     /// Class to check the GEDCOM format.
31     /// </summary>
32     public class GEDCOMChecker
33     {
34         private IBaseContext fBaseContext;
35         private GEDCOMFormat fFormat;
36         private IProgressController fProgress;
37         private GDMTree fTree;
38 
GEDCOMChecker(IBaseContext baseContext, IProgressController progress)39         private GEDCOMChecker(IBaseContext baseContext, IProgressController progress)
40         {
41             fBaseContext = baseContext;
42             fTree = fBaseContext.Tree;
43             fFormat = GEDCOMProvider.GetGEDCOMFormat(fTree);
44             fProgress = progress;
45         }
46 
TransformNote(GDMNotes note)47         private void TransformNote(GDMNotes note)
48         {
49             GDMNoteRecord noteRec = fTree.CreateNote();
50             noteRec.Lines.Assign(note.Lines);
51 
52             note.Clear();
53             note.XRef = noteRec.XRef;
54         }
55 
TransformMultimediaLink(GDMMultimediaLink mmLink)56         private void TransformMultimediaLink(GDMMultimediaLink mmLink)
57         {
58             string title = mmLink.Title;
59             GDMMultimediaRecord mmRec = fTree.CreateMultimedia();
60 
61             int num = mmLink.FileReferences.Count;
62             for (int i = 0; i < num; i++) {
63                 GDMFileReference srcFileRef = mmLink.FileReferences[i];
64                 GDMFileReferenceWithTitle tgtFileRef = new GDMFileReferenceWithTitle();
65 
66                 tgtFileRef.LinkFile(srcFileRef.StringValue);
67 
68                 if (srcFileRef.MultimediaFormat != GDMMultimediaFormat.mfNone) {
69                     tgtFileRef.MultimediaFormat = srcFileRef.MultimediaFormat;
70                 }
71                 if (srcFileRef.MediaType != GDMMediaType.mtUnknown) {
72                     tgtFileRef.MediaType = srcFileRef.MediaType;
73                 }
74                 tgtFileRef.Title = title;
75 
76                 mmRec.FileReferences.Add(tgtFileRef);
77             }
78 
79             mmLink.Clear();
80             mmLink.XRef = mmRec.XRef;
81         }
82 
TransformSourceCitation(GDMSourceCitation sourCit)83         private void TransformSourceCitation(GDMSourceCitation sourCit)
84         {
85             GDMSourceRecord sourRec = fTree.CreateSource();
86 
87             sourRec.Title.Lines.Assign(sourCit.Description);
88             sourRec.Text.Lines.Assign(sourCit.Text.Lines);
89 
90             // transfers notes and multimedia from the citation to the source record
91             sourRec.AssignList(sourCit.Notes, sourRec.Notes);
92             sourRec.AssignList(sourCit.MultimediaLinks, sourRec.MultimediaLinks);
93 
94             sourCit.Description.Clear();
95             sourCit.Text.Clear();
96             sourCit.Notes.Clear();
97             sourCit.MultimediaLinks.Clear();
98             sourCit.XRef = sourRec.XRef;
99 
100             CheckTagWithNotes(sourRec);
101             CheckTagWithMultimediaLinks(sourRec);
102         }
103 
CheckTagWithNotes(IGDMStructWithNotes tag)104         private void CheckTagWithNotes(IGDMStructWithNotes tag)
105         {
106             if (!tag.HasNotes) return;
107 
108             for (int i = tag.Notes.Count - 1; i >= 0; i--) {
109                 GDMNotes note = tag.Notes[i];
110                 if (!note.IsPointer) {
111                     TransformNote(note);
112                 } else {
113                     var noteRec = fTree.GetPtrValue<GDMNoteRecord>(note);
114                     if (noteRec == null) tag.Notes.DeleteAt(i);
115                 }
116             }
117         }
118 
CheckTagWithSourceCitations(IGDMStructWithSourceCitations tag)119         private void CheckTagWithSourceCitations(IGDMStructWithSourceCitations tag)
120         {
121             if (!tag.HasSourceCitations) return;
122 
123             for (int i = tag.SourceCitations.Count - 1; i >= 0; i--) {
124                 GDMSourceCitation sourCit = tag.SourceCitations[i];
125                 if (!sourCit.IsPointer) {
126                     TransformSourceCitation(sourCit);
127                 } else {
128                     var sourRec = fTree.GetPtrValue<GDMSourceRecord>(sourCit);
129                     if (sourRec == null) tag.SourceCitations.DeleteAt(i);
130                 }
131             }
132         }
133 
CheckTagWithMultimediaLinks(IGDMStructWithMultimediaLinks tag)134         private void CheckTagWithMultimediaLinks(IGDMStructWithMultimediaLinks tag)
135         {
136             if (!tag.HasMultimediaLinks) return;
137 
138             for (int i = tag.MultimediaLinks.Count - 1; i >= 0; i--) {
139                 GDMMultimediaLink mmLink = tag.MultimediaLinks[i];
140                 if (!mmLink.IsPointer) {
141                     TransformMultimediaLink(mmLink);
142                 } else {
143                     var mmRec = fTree.GetPtrValue<GDMMultimediaRecord>(mmLink);
144                     if (mmRec == null) tag.MultimediaLinks.DeleteAt(i);
145                 }
146             }
147         }
148 
CheckPointerWithNotes(GDMPointerWithNotes ptr)149         private void CheckPointerWithNotes(GDMPointerWithNotes ptr)
150         {
151             GDMRecord val = fTree.GetPtrValue<GDMRecord>(ptr);
152             if (!string.IsNullOrEmpty(ptr.XRef) && val == null) {
153                 ptr.XRef = string.Empty;
154             }
155 
156             CheckTagWithNotes(ptr);
157         }
158 
CheckStructWL(IGDMStructWithLists swl)159         private void CheckStructWL(IGDMStructWithLists swl)
160         {
161             CheckTagWithNotes(swl);
162             CheckTagWithMultimediaLinks(swl);
163             CheckTagWithSourceCitations(swl);
164         }
165 
CheckEventPlace(GDMPlace place)166         private void CheckEventPlace(GDMPlace place)
167         {
168             GDMPointer placeLocation = place.Location;
169             GDMLocationRecord locRec = fTree.GetPtrValue<GDMLocationRecord>(placeLocation);
170 
171             if (placeLocation.XRef != "" && locRec == null) {
172                 placeLocation.XRef = "";
173             }
174 
175             if (place.StringValue != "") {
176                 if (locRec != null && place.StringValue != locRec.LocationName) {
177                     place.StringValue = locRec.LocationName;
178                 }
179             }
180 
181             CheckTagWithNotes(place);
182         }
183 
CheckEvent(GDMCustomEvent evt)184         private void CheckEvent(GDMCustomEvent evt)
185         {
186             CheckStructWL(evt);
187 
188             // Fix for Family Tree Maker 2008 which exports occupation as generic EVEN events
189             if (fFormat == GEDCOMFormat.gf_FamilyTreeMaker) {
190                 string subtype = evt.Classification.ToLower();
191                 if (evt.Id == (int)GEDCOMTagType.EVEN && subtype == "occupation") {
192                     evt.SetName(GEDCOMTagType.OCCU);
193                     evt.Classification = string.Empty;
194                 }
195             }
196 
197             if (evt.HasPlace) {
198                 CheckEventPlace(evt.Place);
199             }
200         }
201 
CheckUserRef(GDMRecord rec, GDMUserReference userRef)202         private void CheckUserRef(GDMRecord rec, GDMUserReference userRef)
203         {
204         }
205 
CheckPersonalName(GDMPersonalName persName)206         private void CheckPersonalName(GDMPersonalName persName)
207         {
208             CheckTagWithNotes(persName);
209             CheckTagWithSourceCitations(persName);
210 
211             fBaseContext.CollectNameLangs(persName);
212         }
213 
CheckIndividualRecord(GDMIndividualRecord iRec)214         private void CheckIndividualRecord(GDMIndividualRecord iRec)
215         {
216             if (iRec.HasEvents) {
217                 for (int i = 0, num = iRec.Events.Count; i < num; i++) {
218                     GDMCustomEvent evt = iRec.Events[i];
219 
220                     CheckEvent(evt);
221 
222                     fBaseContext.CollectEventValues(evt);
223                 }
224             }
225 
226             for (int i = 0, num = iRec.PersonalNames.Count; i < num; i++) {
227                 CheckPersonalName(iRec.PersonalNames[i]);
228             }
229 
230             for (int i = iRec.ChildToFamilyLinks.Count - 1; i >= 0; i--) {
231                 var cfl = iRec.ChildToFamilyLinks[i];
232                 if (fTree.GetPtrValue(cfl) == null) {
233                     iRec.ChildToFamilyLinks.DeleteAt(i);
234                 } else {
235                     CheckPointerWithNotes(cfl);
236                 }
237             }
238 
239             for (int i = iRec.SpouseToFamilyLinks.Count - 1; i >= 0; i--) {
240                 var sfl = iRec.SpouseToFamilyLinks[i];
241                 if (fTree.GetPtrValue(sfl) == null) {
242                     iRec.SpouseToFamilyLinks.DeleteAt(i);
243                 } else {
244                     CheckPointerWithNotes(sfl);
245                 }
246             }
247 
248             if (iRec.HasAssociations) {
249                 for (int i = 0, num = iRec.Associations.Count; i < num; i++) {
250                     var asso = iRec.Associations[i];
251                     CheckPointerWithNotes(asso);
252                     CheckTagWithSourceCitations(asso);
253                 }
254             }
255 
256             fBaseContext.ImportNames(iRec);
257         }
258 
CheckChildLink(GDMFamilyRecord fam, int index)259         private void CheckChildLink(GDMFamilyRecord fam, int index)
260         {
261             GDMIndividualLink childLink = fam.Children[index];
262             var childRec = fTree.GetPtrValue<GDMIndividualRecord>(childLink);
263             if (childRec == null) {
264                 fam.Children.DeleteAt(index);
265                 return;
266             }
267 
268             if (fFormat == GEDCOMFormat.gf_AGES) {
269                 var frelTag = FindSubTagValue(childLink, "_FREL");
270                 var mrelTag = FindSubTagValue(childLink, "_MREL");
271                 if (frelTag == "ADOPTED" && mrelTag == "ADOPTED") {
272                     GDMChildToFamilyLink ctfLink = childRec.FindChildToFamilyLink(fam);
273                     if (ctfLink != null) {
274                         ctfLink.PedigreeLinkageType = GDMPedigreeLinkageType.plAdopted;
275 
276                         childLink.DeleteTag("_FREL");
277                         childLink.DeleteTag("_MREL");
278                     }
279                 }
280             }
281         }
282 
FindSubTagValue(GDMTag tag, string subTagName)283         private static string FindSubTagValue(GDMTag tag, string subTagName)
284         {
285             var subTag = tag.FindTag(subTagName, 0);
286             return (subTag == null) ? string.Empty : subTag.StringValue;
287         }
288 
CheckFamilyRecord(GDMFamilyRecord fam)289         private void CheckFamilyRecord(GDMFamilyRecord fam)
290         {
291             if (fam.HasEvents) {
292                 for (int i = 0, num = fam.Events.Count; i < num; i++) {
293                     GDMCustomEvent evt = fam.Events[i];
294                     CheckEvent(evt);
295                 }
296             }
297 
298             for (int i = fam.Children.Count - 1; i >= 0; i--) {
299                 CheckChildLink(fam, i);
300             }
301 
302             GDMRecord val = fTree.GetPtrValue<GDMIndividualRecord>(fam.Husband);
303             if (!string.IsNullOrEmpty(fam.Husband.XRef) && val == null) {
304                 fam.Husband.XRef = string.Empty;
305             }
306 
307             val = fTree.GetPtrValue<GDMIndividualRecord>(fam.Wife);
308             if (!string.IsNullOrEmpty(fam.Wife.XRef) && val == null) {
309                 fam.Wife.XRef = string.Empty;
310             }
311         }
312 
CheckGroupRecord(GDMGroupRecord group)313         private void CheckGroupRecord(GDMGroupRecord group)
314         {
315             for (int i = group.Members.Count - 1; i >= 0; i--) {
316                 GDMIndividualRecord mbr = fTree.GetPtrValue(group.Members[i]);
317                 if (mbr == null) {
318                     group.Members.DeleteAt(i);
319                 } else {
320                     if (mbr.IndexOfGroup(group) < 0) {
321                         group.Members.DeleteAt(i);
322                     }
323                 }
324             }
325         }
326 
CheckSourceRecord(GDMSourceRecord src)327         private void CheckSourceRecord(GDMSourceRecord src)
328         {
329             for (int i = src.RepositoryCitations.Count - 1; i >= 0; i--) {
330                 GDMRecord val = fTree.GetPtrValue<GDMRecord>(src.RepositoryCitations[i]);
331                 if (val == null) {
332                     src.RepositoryCitations.DeleteAt(i);
333                 }
334             }
335         }
336 
CheckMultimediaRecord(GDMMultimediaRecord mmRec, int fileVer)337         private void CheckMultimediaRecord(GDMMultimediaRecord mmRec, int fileVer)
338         {
339             for (int i = 0; i < mmRec.FileReferences.Count; i++) {
340                 GDMFileReferenceWithTitle fileRef = mmRec.FileReferences[i];
341 
342                 GDMMultimediaFormat mmFormat = fileRef.MultimediaFormat;
343                 if (mmFormat == GDMMultimediaFormat.mfUnknown || mmFormat == GDMMultimediaFormat.mfNone) {
344                     // tag 'FORM' can be corrupted or GEDCOMCore in past not recognize format attempt recovery
345                     fileRef.MultimediaFormat = GDMFileReference.RecognizeFormat(fileRef.StringValue);
346                 }
347 
348                 if (fFormat == GEDCOMFormat.gf_Native && fileVer == 39) {
349                     // the transition to normalized names after GKv39
350                     // only for not direct references AND not relative references (platform specific paths)
351 
352                     var mediaStore = GKUtils.GetStoreType(fileRef);
353                     if (mediaStore.StoreType != MediaStoreType.mstReference
354                         && mediaStore.StoreType != MediaStoreType.mstRelativeReference) {
355                         fileRef.StringValue = FileHelper.NormalizeFilename(fileRef.StringValue);
356                     }
357                 }
358             }
359         }
360 
CheckRecord(GDMRecord rec, int fileVer)361         private void CheckRecord(GDMRecord rec, int fileVer)
362         {
363             CheckStructWL(rec);
364 
365             if (rec.HasUserReferences) {
366                 for (int i = 0, num = rec.UserReferences.Count; i < num; i++) {
367                     CheckUserRef(rec, rec.UserReferences[i]);
368                 }
369             }
370 
371             // TODO
372             // INDI: remove AFN, RFN - discuss???
373             // INDI,FAM: remove SUBM - discuss???
374 
375             switch (rec.RecordType) {
376                 case GDMRecordType.rtIndividual:
377                     CheckIndividualRecord(rec as GDMIndividualRecord);
378                     break;
379 
380                 case GDMRecordType.rtFamily:
381                     CheckFamilyRecord(rec as GDMFamilyRecord);
382                     break;
383 
384                 case GDMRecordType.rtGroup:
385                     CheckGroupRecord(rec as GDMGroupRecord);
386                     break;
387 
388                 case GDMRecordType.rtSource:
389                     CheckSourceRecord(rec as GDMSourceRecord);
390                     break;
391 
392                 case GDMRecordType.rtMultimedia:
393                     CheckMultimediaRecord(rec as GDMMultimediaRecord, fileVer);
394                     break;
395             }
396         }
397 
CheckRecordXRef(GDMRecord record)398         private bool CheckRecordXRef(GDMRecord record)
399         {
400             string stdSign = GEDCOMUtils.GetSignByRecord(record);
401             string xrefNum = record.GetXRefNum();
402             string recXRef = record.XRef;
403 
404             return ((recXRef == stdSign + xrefNum) && record.GetId() >= 0);
405         }
406 
ConvertIdentifiers()407         private void ConvertIdentifiers()
408         {
409             fProgress.ProgressInit(LangMan.LS(LSID.LSID_IDsCorrect), fTree.RecordsCount * 2);
410             GDMXRefReplacer repMap = new GDMXRefReplacer();
411             try {
412                 int recsCount = fTree.RecordsCount;
413                 for (int i = 0; i < recsCount; i++) {
414                     GDMRecord rec = fTree[i];
415                     if (!CheckRecordXRef(rec)) {
416                         string oldXRef = rec.XRef;
417                         string newXRef = fTree.NewXRef(rec, true);
418                         repMap.AddXRef(rec, oldXRef, newXRef);
419                     }
420                     fProgress.ProgressStep();
421                 }
422 
423                 fTree.Header.ReplaceXRefs(repMap);
424                 for (int i = 0; i < recsCount; i++) {
425                     GDMRecord rec = fTree[i];
426                     rec.ReplaceXRefs(repMap);
427                     fProgress.ProgressStep();
428                 }
429             } finally {
430                 repMap.Dispose();
431                 fProgress.ProgressDone();
432             }
433         }
434 
CheckFormat()435         private bool CheckFormat()
436         {
437             bool result = false;
438 
439             try {
440                 int fileVer;
441                 // remove a deprecated features
442                 if (fFormat == GEDCOMFormat.gf_Native) {
443                     GDMHeader header = fTree.Header;
444                     GDMTag tag;
445 
446                     tag = header.FindTag("_ADVANCED", 0);
447                     if (tag != null) header.DeleteTag("_ADVANCED");
448 
449                     tag = header.FindTag("_EXT_NAME", 0);
450                     if (tag != null) header.DeleteTag("_EXT_NAME");
451 
452                     fileVer = ConvertHelper.ParseInt(header.Source.Version, GKData.APP_FORMAT_DEFVER);
453                 } else {
454                     fileVer = -1;
455                 }
456 
457                 fProgress.ProgressInit(LangMan.LS(LSID.LSID_FormatCheck), 100);
458                 try {
459                     bool xrefValid = true;
460                     bool isExtraneous = (fFormat != GEDCOMFormat.gf_Native);
461 
462                     int progress = 0;
463                     int num = fTree.RecordsCount;
464                     for (int i = 0; i < num; i++) {
465                         GDMRecord rec = fTree[i];
466                         CheckRecord(rec, fileVer);
467 
468                         if (isExtraneous && xrefValid && !CheckRecordXRef(rec)) {
469                             xrefValid = false;
470                         }
471 
472                         int newProgress = (int)Math.Min(100, ((i + 1) * 100.0f) / num);
473                         if (progress != newProgress) {
474                             progress = newProgress;
475                             fProgress.ProgressStep(progress);
476                         }
477                     }
478 
479                     // obsolete: AppHost.StdDialogs.ShowQuestionYN(LangMan.LS(LSID.LSID_IDsCorrectNeed))
480                     if (!xrefValid) {
481                         ConvertIdentifiers();
482                     }
483 
484                     fTree.TrimExcess();
485 
486                     result = true;
487                 } finally {
488                     fProgress.ProgressDone();
489                 }
490             } catch (Exception ex) {
491                 Logger.WriteError("GEDCOMChecker.CheckFormat()", ex);
492                 AppHost.StdDialogs.ShowError(LangMan.LS(LSID.LSID_CheckGedComFailed));
493             }
494 
495             return result;
496         }
497 
CheckGEDCOMFormat(IBaseContext baseContext, IProgressController pc)498         public static bool CheckGEDCOMFormat(IBaseContext baseContext, IProgressController pc)
499         {
500             if (baseContext == null)
501                 throw new ArgumentNullException("baseContext");
502 
503             if (pc == null)
504                 throw new ArgumentNullException("pc");
505 
506             var instance = new GEDCOMChecker(baseContext, pc);
507             return instance.CheckFormat();
508         }
509     }
510 }
511