Projekt

Obecné

Profil

Stáhnout (39.4 KB) Statistiky
| Větev: | Tag: | Revize:
1
using Core.Contexts;
2
using Core.Entities;
3
using Models.Annotations;
4
using Models.Enums;
5
using Serilog;
6
using System;
7
using System.Collections.Generic;
8
using System.Linq;
9
using System.Text;
10
using System.Threading.Tasks;
11
using Microsoft.EntityFrameworkCore;
12
using AutoMapper;
13
using Models.Tags;
14
using Ganss.XSS;
15
using HtmlAgilityPack;
16
using System.Text.RegularExpressions;
17
using Newtonsoft.Json;
18
using Core.GraphUtils;
19

    
20
namespace Core.Services.AnnotationService
21
{
22
    public class AnnotationServiceEF : IAnnotationService
23
    {
24
        private readonly DatabaseContext context;
25
        private readonly ILogger logger;
26
        private readonly IMapper mapper;
27

    
28
        private const string TAG_ID_ATTRIBUTE_NAME = "aswi-tag-id";
29
        private const string TAG_INSTANCE_ATTRIBUTE_NAME = "aswi-tag-instance";
30
        private const string TAG_EF_ID_ATTRIBUTE_NAME = "aswi-tag-ef-id";
31

    
32
        public AnnotationServiceEF(DatabaseContext context, ILogger logger, IMapper mapper)
33
        {
34
            this.context = context;
35
            this.logger = logger;
36
            this.mapper = mapper;
37
        }
38

    
39
        public void CreateDocumentAnnotations(AnnotationsAddRequest request, Guid clientUserId)
40
        {
41
            User addingUser = context.Users.Single(u => u.Id == clientUserId);
42

    
43
            // Check the documents exist
44
            var documents = context.Documents.Where(d => request.DocumentIdList.Contains(d.Id)).ToList();
45
            if (documents.Count() != request.DocumentIdList.Count)
46
            {
47
                logger.Information($"Received a non-existent Document ID when assigning documents to users");
48
                throw new InvalidOperationException($"{request.DocumentIdList.Count - documents.Count()} of the received documents do not exist");
49
            }
50

    
51
            var users = context.Users.Where(u => request.UserIdList.Contains(u.Id)).ToList();
52
            foreach (var user in users)
53
            {
54
                var userAnnotatedDocuments = context.Annotations.Where(a => a.User == user).Select(a => a.Document).ToList();
55
                foreach (var doc in documents)
56
                {
57
                    if (userAnnotatedDocuments.Contains(doc))
58
                    {
59
                        logger.Information($"User {user.Username} has already been assigned the document {doc.Id}, ignoring");
60
                        continue;
61
                    }
62

    
63
                    context.Annotations.Add(new Annotation()
64
                    {
65
                        User = user,
66
                        UserAssigned = addingUser,
67
                        DateAssigned = DateTime.Now,
68
                        DateLastChanged = DateTime.Now,
69
                        Document = doc,
70
                        State = EState.NEW,
71
                        Note = ""
72
                    });
73
                }
74
            }
75

    
76
            context.SaveChanges();
77
        }
78

    
79
        public AnnotationListResponse GetUserAnnotations(Guid userId)
80
        {
81
            var annotations = context.Annotations.Where(a => a.User.Id == userId).Include(a => a.Document).ToList();
82
            var documentIds = annotations.Select(a => a.Document.Id).ToList();
83
            var documents = context.Documents.Where(d => documentIds.Contains(d.Id));
84
            var infos = new List<AnnotationListInfo>();
85

    
86
            var annotationsDocuments = annotations.Zip(documents, (a, d) => new { Annotation = a, Document = d });
87
            foreach (var ad in annotationsDocuments)
88
            {
89
                infos.Add(new AnnotationListInfo()
90
                {
91
                    AnnotationId = ad.Annotation.Id,
92
                    DocumentName = ad.Document.Name,
93
                    State = ad.Annotation.State
94
                });
95
            }
96

    
97
            return new AnnotationListResponse()
98
            {
99
                Annotations = infos
100
            };
101
        }
102

    
103
        public void AddNoteToAnnotation(Guid annotationId, Guid userId, ERole userRole, AddNoteToAnnotationRequest request)
104
        {
105
            Annotation annotation = null;
106
            try
107
            {
108
                annotation = context.Annotations.Include(a => a.User).First(a => a.Id == annotationId);
109
            }
110
            catch (Exception)
111
            {
112
                throw new InvalidOperationException("Annotation not found");
113
            }
114

    
115
            if (userRole < ERole.ADMINISTRATOR && annotation.User.Id != userId)
116
            {
117
                throw new UnauthorizedAccessException("User does not have access to this annotation");
118
            }
119

    
120
            annotation.Note = request.Note;
121
            context.SaveChanges();
122
        }
123

    
124

    
125
        public AnnotationInfo GetAnnotation(Guid annotationId, Guid userId, ERole userRole)
126
        {
127
            var annotation = context.Annotations
128
                .Where(a => a.Id == annotationId)
129
                .Include(a => a.User)
130
                .Include(a => a.Document).ThenInclude(d => d.Content)
131
                .First();
132

    
133
            if (userRole < ERole.ADMINISTRATOR)
134
            {
135
                if (annotation.User.Id != userId)
136
                {
137
                    throw new UnauthorizedAccessException($"User {userId} does not have assigned annotation {annotationId}");
138
                }
139
            }
140

    
141
            var documentContent = context.Documents.Where(d => d.Id == annotation.Document.Id).Select(d => d.Content).First();
142

    
143
            var tags = context.AnnotationTags.Where(at => at.Annotation.Id == annotationId)
144
                .Include(at => at.Tag)
145
                .ThenInclude(t => t.Category)
146
                .Include(at => at.SubTag)
147
                .OrderBy(at => at.Position)
148
                .ToList();
149

    
150
            List<TagInstanceInfo> tagInstanceInfos = new();
151
            foreach (var tag in tags)
152
            {
153
                var tagInstance = mapper.Map<TagInstanceInfo>(tag);
154
                tagInstanceInfos.Add(tagInstance);
155
            }
156

    
157
            var docToRender = "";
158
            if (annotation.CachedDocumentHTML == "")
159
            {
160
                docToRender = FullPreprocessHTML(documentContent.Content, tags);
161
                annotation.CachedStartPositions = JsonConvert.SerializeObject(TagStartPositions);
162
                annotation.CachedLengths = JsonConvert.SerializeObject(TagStartLengths);
163
                annotation.CachedClosingPositions = JsonConvert.SerializeObject(TagClosingPositions);
164
                annotation.CachedClosingLengths = JsonConvert.SerializeObject(TagClosingLengths);
165
                annotation.CachedCSS = JsonConvert.SerializeObject(TagInstanceCSS);
166
                annotation.ModifiedType = EModified.NONE;
167
                annotation.CachedDocumentHTML = docToRender;
168
                context.SaveChanges();
169
            }
170
            else
171
            {
172
                docToRender = annotation.CachedDocumentHTML;
173
                TagStartPositions = JsonConvert.DeserializeObject<List<int>>(annotation.CachedStartPositions);
174
                TagStartLengths = JsonConvert.DeserializeObject<List<int>>(annotation.CachedLengths);
175
                TagClosingPositions = JsonConvert.DeserializeObject<List<int>>(annotation.CachedClosingPositions);
176
                TagClosingLengths = JsonConvert.DeserializeObject<List<int>>(annotation.CachedClosingLengths);
177
                //TagInstanceCSS = JsonConvert.DeserializeObject<List<TagInstanceCSSInfo>>(annotation.CachedCSS);
178

    
179
                // The annotation has been modified and we need to either add the new tag or remove the tag
180
                if (annotation.ModifiedType != EModified.NONE)
181
                {
182
                    if (annotation.ModifiedType == EModified.ADDED)
183
                    {
184
                        var lastModifiedTag = context.AnnotationTags.Where(at => at.Id == annotation.LastModifiedTagId).First();
185
                        docToRender = PartialPreprocessHTMLAddTag(docToRender, documentContent.Content, lastModifiedTag, tags);
186
                    }
187
                    else if (annotation.ModifiedType == EModified.REMOVED)
188
                    {
189
                        docToRender = PartialPreprocessHTMLRemoveTag(docToRender, documentContent.Content, new AnnotationTag() { Id = annotation.LastModifiedTagId.Value }, tags);
190
                    }
191

    
192
                    annotation.ModifiedType = EModified.NONE;
193
                    annotation.CachedStartPositions = JsonConvert.SerializeObject(TagStartPositions);
194
                    annotation.CachedLengths = JsonConvert.SerializeObject(TagStartLengths);
195
                    annotation.CachedClosingPositions = JsonConvert.SerializeObject(TagClosingPositions);
196
                    annotation.CachedClosingLengths = JsonConvert.SerializeObject(TagClosingLengths);
197
                    annotation.CachedDocumentHTML = docToRender;
198
                    annotation.CachedCSS = JsonConvert.SerializeObject(TagInstanceCSS);
199
                    context.SaveChanges();
200
                }
201
            }
202

    
203
            // We probably cannot use AutoMapper since we are dealing with too many different entities
204
            AnnotationInfo annotationInfo = new()
205
            {
206
                SourceDocumentContent = documentContent.Content,
207
                DocumentToRender = docToRender,
208
                TagStartPositions = TagStartPositions.ToArray(),
209
                TagLengths = TagStartLengths.ToArray(),
210
                Note = annotation.Note,
211
                State = annotation.State,
212
                Type = IsHtml(documentContent.Content) ? EDocumentType.HTML : EDocumentType.TEXT,
213
                TagInstances = tagInstanceInfos,
214
                CSSInfo = TagInstanceCSS
215
            };
216

    
217
            NodeDict.Clear();
218

    
219
            return annotationInfo;
220
        }
221

    
222
        private List<int> TagStartPositions = new();
223
        private List<int> TagStartLengths = new();
224
        private List<int> TagClosingPositions = new();
225
        private List<int> TagClosingLengths = new();
226
        private Dictionary<HtmlNode, HtmlNode> NodeDict = new();
227
        private List<TagInstanceCSSInfo> TagInstanceCSS = new();
228

    
229
        /*
230
         *      Full HTML Preprocessing -------------------------------------------------------------------------------
231
         */
232

    
233

    
234
        private string FullPreprocessHTML(string htmlSource, List<AnnotationTag> tags)
235
        {
236
            var docOriginal = new HtmlDocument();
237
            docOriginal.LoadHtml(htmlSource);
238
            var docToEdit = new HtmlDocument();
239
            docToEdit.LoadHtml(htmlSource);
240

    
241
            var descendantsOriginal = docOriginal.DocumentNode.DescendantsAndSelf();
242
            var descendantsToEdit = docToEdit.DocumentNode.DescendantsAndSelf().ToList();
243

    
244
            int currentId = 0;
245

    
246
            FillNodeDict(descendantsOriginal, descendantsToEdit);
247
            AssignIdsToOriginalDocument(descendantsOriginal, ref currentId);
248

    
249
            WrapTextInSpan(descendantsOriginal, docToEdit);
250
            int descCount = descendantsToEdit.Count;
251

    
252
            foreach (var tag in tags)
253
            {
254
                int i = 0;
255
                List<HtmlNode> addedForSelection = new();
256
                while (i < descCount)
257
                {
258
                    for (; i < descCount; i++)
259
                    {
260
                        var node = descendantsToEdit.ElementAt(i);
261
                        if (!node.Name.Contains("#text") || addedForSelection.Contains(node) || addedForSelection.Contains(node.ParentNode) ||
262
                            node.ParentNode.Name == "style")
263
                        {
264
                            continue;
265
                        }
266

    
267
                        int nodeId = node.ParentNode.GetAttributeValue(TAG_ID_ATTRIBUTE_NAME, -1);
268

    
269
                        var start = TagStartPositions[nodeId] + TagStartLengths[nodeId];
270
                        var end = TagClosingPositions[nodeId];
271

    
272
                        int selectionStart = tag.Position;
273
                        int selectionEnd = tag.Position + tag.Length;
274

    
275
                        if (selectionStart < end && selectionEnd > start)
276
                        {
277
                            if (selectionStart <= start && selectionEnd >= end)
278
                            {
279
                                addedForSelection.Add(SolveFullFill(node, selectionStart, selectionEnd, start, end, docToEdit, tag));
280
                            }
281
                            else if (selectionStart <= start)
282
                            {
283
                                addedForSelection.AddRange(SolveRightGap(node, selectionStart, selectionEnd, start, end, docToEdit, tag));
284
                            }
285
                            else if (selectionEnd >= end)
286
                            {
287
                                addedForSelection.AddRange(SolveLeftGap(node, selectionStart, selectionEnd, start, end, docToEdit, tag));
288
                            }
289
                            else
290
                            {
291
                                addedForSelection.AddRange(SolveLeftRightGap(node, selectionStart, selectionEnd, start, end, docToEdit, tag));
292
                            }
293
                            descendantsToEdit = docToEdit.DocumentNode.DescendantsAndSelf().ToList();
294
                            descCount = descendantsToEdit.Count;
295
                            break;
296
                        }
297
                    }
298
                }
299

    
300
            }
301

    
302
            ModifyLinks(descendantsToEdit);
303
            string docToRender = docToEdit.DocumentNode.OuterHtml;
304
            HtmlSanitizer sanitizer = new HtmlSanitizer();
305
            sanitizer.AllowedAttributes.Clear();
306
            sanitizer.AllowedAttributes.Add(TAG_ID_ATTRIBUTE_NAME);
307
            sanitizer.AllowedAttributes.Add(TAG_INSTANCE_ATTRIBUTE_NAME);
308
            sanitizer.AllowedAttributes.Add("href");
309
            sanitizer.AllowedAttributes.Add("end");
310
            sanitizer.AllowedAttributes.Add("start");
311
            sanitizer.AllowedAttributes.Add("class");
312
            sanitizer.AllowedAttributes.Add("target");
313
            if (sanitizer.AllowedTags.Contains("script"))
314
            {
315
                sanitizer.AllowedTags.Remove("script");
316
            }
317
            if (!sanitizer.AllowedTags.Contains("style"))
318
            {
319
                sanitizer.AllowedTags.Add("style");
320
            }
321
            sanitizer.AllowedTags.Add("a");
322
            docToRender = sanitizer.Sanitize(docToRender);
323
            GenerateCSS(tags);
324
            return docToRender;
325
        }
326

    
327
        private HtmlNode SolveFullFill(HtmlNode node, int selectionStart, int selectionEnd, int start, int end, HtmlDocument docToEdit, AnnotationTag tag)
328
        {
329
            // full fill
330
            string textSelected = node.InnerText;
331

    
332
            var parentNode = node.ParentNode;
333
            int nodeIndex = parentNode.ChildNodes.IndexOf(node);
334
            parentNode.ChildNodes.RemoveAt(nodeIndex);
335

    
336
            EPosition markerPosition = EPosition.MARK_NONE;
337
            if (selectionEnd == end && selectionStart == start)
338
            {
339
                markerPosition = EPosition.MARK_LEFT_RIGHT;
340
            }
341

    
342
            HtmlNode spanSelected = CreateSpan(docToEdit, textSelected, TagStartPositions.Count, tag.Instance, tag.Id, start, markerPosition);
343
            parentNode.ChildNodes.Insert(nodeIndex, spanSelected);
344

    
345
            return spanSelected;
346
        }
347

    
348
        private List<HtmlNode> SolveRightGap(HtmlNode node, int selectionStart, int selectionEnd, int start, int end, HtmlDocument docToEdit,
349
                                             AnnotationTag tag)
350
        {
351
            // partial fill, end gap
352
            string text = node.InnerText;
353
            string textAfter = text.Substring(Math.Min(selectionStart - start + tag.Length, text.Length));
354
            string textSelected = text.Substring(0, selectionEnd - start);
355

    
356
            var parentNode = node.ParentNode;
357
            int nodeIndex = parentNode.ChildNodes.IndexOf(node);
358
            parentNode.ChildNodes.RemoveAt(nodeIndex);
359

    
360
            int spanSelectedStart = start;
361
            int spanAfterStart = start + textSelected.Length;
362

    
363
            HtmlNode spanSelected = CreateSpan(docToEdit, textSelected, TagStartPositions.Count, tag.Instance, tag.Id, spanSelectedStart, EPosition.MARK_RIGHT);
364
            parentNode.ChildNodes.Insert(nodeIndex, spanSelected);
365

    
366
            HtmlNode spanAfter = CreateSpan(docToEdit, textAfter, TagStartPositions.Count, null, null, spanAfterStart);
367
            parentNode.ChildNodes.Insert(nodeIndex + 1, spanAfter);
368

    
369
            return new() { spanSelected, spanAfter };
370
        }
371

    
372
        private List<HtmlNode> SolveLeftGap(HtmlNode node, int selectionStart, int selectionEnd, int start, int end, HtmlDocument docToEdit,
373
                                             AnnotationTag tag)
374
        {
375
            // partial fill, start gap
376
            string text = node.InnerText;
377
            string textBefore = text.Substring(0, selectionStart - start);
378
            string textSelected = text.Substring(selectionStart - start, Math.Min(tag.Length, text.Length - textBefore.Length));
379

    
380
            var parentNode = node.ParentNode;
381
            int nodeIndex = parentNode.ChildNodes.IndexOf(node);
382
            parentNode.ChildNodes.RemoveAt(nodeIndex);
383

    
384
            int spanBeforeStart = start;
385
            int spanSelectedStart = start + textBefore.Length;
386

    
387
            HtmlNode spanBefore = CreateSpan(docToEdit, textBefore, TagStartPositions.Count, null, null, spanBeforeStart);
388
            parentNode.ChildNodes.Insert(nodeIndex, spanBefore);
389

    
390
            HtmlNode spanSelected = CreateSpan(docToEdit, textSelected, TagStartPositions.Count, tag.Instance, tag.Id, spanSelectedStart, EPosition.MARK_LEFT);
391
            parentNode.ChildNodes.Insert(nodeIndex + 1, spanSelected);
392

    
393
            return new() { spanSelected, spanBefore };
394
        }
395

    
396
        private List<HtmlNode> SolveLeftRightGap(HtmlNode node, int selectionStart, int selectionEnd, int start, int end, HtmlDocument docToEdit,
397
                                                 AnnotationTag tag)
398
        {
399
            // partial fill, start gap end gap
400
            string text = node.InnerText;
401
            string textBefore = text.Substring(0, selectionStart - start);
402
            string textAfter = text.Substring(selectionStart - start + tag.Length);
403
            string textSelected = text.Substring(selectionStart - start, tag.Length);
404

    
405
            var parentNode = node.ParentNode;
406
            int nodeIndex = parentNode.ChildNodes.IndexOf(node);
407
            parentNode.ChildNodes.RemoveAt(nodeIndex);
408

    
409
            int spanBeforeStart = start;
410
            int spanSelectedStart = start + textBefore.Length;
411
            int spanAfterStart = start + textBefore.Length + textSelected.Length;
412

    
413
            HtmlNode spanBefore = CreateSpan(docToEdit, textBefore, TagStartPositions.Count, null, null, spanBeforeStart);
414
            parentNode.ChildNodes.Insert(nodeIndex, spanBefore);
415

    
416
            HtmlNode spanSelected = CreateSpan(docToEdit, textSelected, TagStartPositions.Count, tag.Instance, tag.Id, spanSelectedStart, EPosition.MARK_LEFT_RIGHT);
417
            parentNode.ChildNodes.Insert(nodeIndex + 1, spanSelected);
418

    
419
            HtmlNode spanAfter = CreateSpan(docToEdit, textAfter, TagStartPositions.Count, null, null, spanAfterStart);
420
            parentNode.ChildNodes.Insert(nodeIndex + 2, spanAfter);
421

    
422
            return new() { spanSelected, spanBefore, spanAfter };
423
        }
424

    
425
        private HtmlNode CreateSpan(HtmlDocument doc, string text, int tagId, Guid? instanceId, Guid? entityId, int startPosition, EPosition position = EPosition.MARK_NONE)
426
        {
427
            HtmlNode span = doc.CreateElement("span");
428
            span.InnerHtml = text;
429
            TagStartPositions.Add(startPosition);
430
            TagStartLengths.Add(0);
431
            TagClosingPositions.Add(startPosition + text.Length);
432
            TagClosingLengths.Add(0);
433
            span.Attributes.Add(TAG_ID_ATTRIBUTE_NAME, tagId.ToString());
434

    
435
            if (instanceId != null)
436
            {
437
                span.AddClass("annotation");
438
                span.Attributes.Add(TAG_INSTANCE_ATTRIBUTE_NAME, instanceId.Value.ToString());
439
                span.Attributes.Add(TAG_EF_ID_ATTRIBUTE_NAME, entityId.ToString());
440

    
441
                if (position == EPosition.MARK_LEFT || position == EPosition.MARK_LEFT_RIGHT)
442
                {
443
                    span.Attributes.Add("start", "1");
444
                }
445
                if (position == EPosition.MARK_RIGHT || position == EPosition.MARK_LEFT_RIGHT)
446
                {
447
                    span.Attributes.Add("end", "1");
448
                }
449
            }
450

    
451
            return span;
452
        }
453

    
454
        private enum EPosition
455
        {
456
            MARK_LEFT = 5,
457
            MARK_RIGHT = 3,
458
            MARK_LEFT_RIGHT = 2,
459
            MARK_NONE = 0
460
        }
461

    
462
        private void ModifyLinks(IEnumerable<HtmlNode> descendantsOriginal)
463
        {
464
            foreach (var descendant in descendantsOriginal)
465
            {
466
                if (descendant.Name == "a")
467
                {
468
                    if (descendant.Attributes.Contains("href"))
469
                    {
470
                        descendant.SetAttributeValue("href", "/link?url=" + descendant.Attributes["href"].Value);
471
                        descendant.SetAttributeValue("target", "_blank");
472
                    }
473
                }
474
            }
475
        }
476

    
477
        private void WrapTextInSpan(IEnumerable<HtmlNode> descendantsOriginal, HtmlDocument docToEdit)
478
        {
479
            // Special case for non-html documents
480
            if (descendantsOriginal.Count() == 2)
481
            {
482
                var documentNode = descendantsOriginal.ElementAt(0);
483
                var childNode = descendantsOriginal.ElementAt(1);
484
                if (documentNode.Name == "#document" && childNode.Name == "#text")
485
                {
486
                    HtmlNode coveringSpan = docToEdit.CreateElement("span");
487
                    coveringSpan.InnerHtml = childNode.InnerHtml;
488
                    TagStartPositions.Add(childNode.InnerStartIndex);
489
                    TagStartLengths.Add(0);
490
                    TagClosingPositions.Add(childNode.InnerStartIndex + childNode.InnerLength);
491
                    TagClosingLengths.Add(0);
492
                    coveringSpan.Attributes.Add(TAG_ID_ATTRIBUTE_NAME, (TagStartPositions.Count - 1).ToString());
493

    
494
                    var parent = NodeDict[documentNode];
495

    
496
                    parent.ChildNodes.RemoveAt(0);
497
                    parent.ChildNodes.Add(coveringSpan);
498

    
499
                    return;
500
                }
501
            }
502

    
503
            foreach (var node in descendantsOriginal)
504
            {
505
                var originalNode = node;
506
                var toEditNode = NodeDict[node];
507

    
508
                if (originalNode.Name.Contains("#"))
509
                {
510
                    continue;
511
                }
512
                else
513
                {
514
                    bool onlyText = true;
515
                    bool onlySubtags = true;
516

    
517
                    foreach (var child in node.ChildNodes)
518
                    {
519
                        if (child.Name.Contains("#"))
520
                        {
521
                            onlySubtags = false;
522
                        }
523
                        else
524
                        {
525
                            onlyText = false;
526
                        }
527
                    }
528

    
529
                    if (onlyText || onlySubtags)
530
                    {
531
                        continue;
532
                    }
533
                    else
534
                    {
535

    
536
                        foreach (var child in node.ChildNodes)
537
                        {
538
                            if (child.Name.Contains("#text"))
539
                            {
540
                                HtmlNode coveringSpan = docToEdit.CreateElement("span");
541
                                coveringSpan.InnerHtml = child.InnerHtml;
542
                                TagStartPositions.Add(child.InnerStartIndex);
543
                                TagStartLengths.Add(0);
544
                                TagClosingPositions.Add(child.InnerStartIndex + child.InnerLength);
545
                                TagClosingLengths.Add(0);
546
                                coveringSpan.Attributes.Add(TAG_ID_ATTRIBUTE_NAME, (TagStartPositions.Count - 1).ToString());
547

    
548
                                var parent = NodeDict[node];
549
                                var index = parent.ChildNodes.IndexOf(NodeDict[child]);
550

    
551
                                parent.ChildNodes.RemoveAt(index);
552
                                parent.ChildNodes.Insert(index, coveringSpan);
553
                            }
554
                        }
555
                    }
556
                }
557
            }
558
        }
559

    
560
        private void GenerateCSS(List<AnnotationTag> tags)
561
        {
562
            /*string inner = "span.annotation {border-bottom: 2px solid;}";
563
            inner += "span {line-height: 30px}\n";*/
564

    
565
            var tagPaddingDict = Intersections.ColorGraph(Intersections.FindIntersections(tags));
566
            foreach (var tag in tags.DistinctBy(t => t.Instance))
567
            {
568
                var padding = (tagPaddingDict[tag] + 1) * 2;
569
                TagInstanceCSS.Add(new()
570
                {
571
                    InstanceId = tag.Instance,
572
                    Color = tag.Tag.Color,
573
                    Padding = padding
574
                });
575
            }
576
        }
577

    
578
        private void AssignIdsToOriginalDocument(IEnumerable<HtmlNode> descendantsOriginal, ref int currentId)
579
        {
580
            foreach (var node in descendantsOriginal)
581
            {
582
                var originalNode = node;
583
                var toEditNode = NodeDict[node];
584

    
585
                if (originalNode.Name.Contains("#"))
586
                {
587
                    continue;
588
                }
589
                else
590
                {
591
                    TagStartPositions.Add(originalNode.OuterStartIndex);
592
                    TagStartLengths.Add(originalNode.InnerStartIndex - originalNode.OuterStartIndex);
593
                    currentId = TagStartPositions.Count - 1;
594
                    toEditNode.Attributes.Add(TAG_ID_ATTRIBUTE_NAME, currentId.ToString());
595

    
596
                    TagClosingPositions.Add(originalNode.InnerStartIndex + originalNode.InnerLength);
597
                    TagClosingLengths.Add((originalNode.OuterStartIndex + originalNode.OuterLength) - (originalNode.InnerStartIndex + originalNode.InnerLength));
598
                }
599
            }
600
        }
601

    
602
        private void FillNodeDict(IEnumerable<HtmlNode> descendantsOriginal, IEnumerable<HtmlNode> descendantsToEdit)
603
        {
604
            var zipped = descendantsOriginal.Zip(descendantsToEdit, (orig, toEdit) => new
605
            {
606
                Original = orig,
607
                ToEdit = toEdit
608
            });
609
            foreach (var node in zipped)
610
            {
611
                var originalNode = node.Original;
612
                var toEditNode = node.ToEdit;
613
                NodeDict.Add(originalNode, toEditNode);
614
            }
615
        }
616

    
617

    
618
        /*
619
         *      Full HTML Preprocessing -------------------------------------------------------------------------------
620
         */
621

    
622
        /*
623
         *      Partial HTML Preprocessing ----------------------------------------------------------------------------
624
         */
625

    
626
        private string PartialPreprocessHTMLAddTag(string htmlToEdit, string htmlOriginal, AnnotationTag tagToAdd, List<AnnotationTag> tags)
627
        {
628
            var docOriginal = new HtmlDocument();
629
            docOriginal.LoadHtml(htmlOriginal);
630
            var docToEdit = new HtmlDocument();
631
            docToEdit.LoadHtml(htmlToEdit);
632

    
633
            var descendantsToEdit = docToEdit.DocumentNode.DescendantsAndSelf().ToList();
634

    
635
            int i = 0;
636
            List<HtmlNode> addedForSelection = new();
637

    
638
            int descendantsCount = descendantsToEdit.Count();
639
            while (i < descendantsCount)
640
            {
641
                for (; i < descendantsCount; i++)
642
                {
643
                    var node = descendantsToEdit.ElementAt(i);
644
                    if (!node.Name.Contains("#text") || addedForSelection.Contains(node) || addedForSelection.Contains(node.ParentNode) ||
645
                        node.ParentNode.Name == "style" || node.ParentNode.Name.StartsWith("#"))
646
                    {
647
                        continue;
648
                    }
649

    
650
                    int nodeId = node.ParentNode.GetAttributeValue(TAG_ID_ATTRIBUTE_NAME, -1);
651

    
652
                    var start = TagStartPositions[nodeId] + TagStartLengths[nodeId];
653
                    var end = TagClosingPositions[nodeId];
654

    
655
                    int selectionStart = tagToAdd.Position;
656
                    int selectionEnd = tagToAdd.Position + tagToAdd.Length;
657

    
658
                    if (selectionStart < end && selectionEnd > start)
659
                    {
660
                        if (selectionStart <= start && selectionEnd >= end)
661
                        {
662
                            addedForSelection.Add(SolveFullFill(node, selectionStart, selectionEnd, start, end, docToEdit, tagToAdd));
663
                        }
664
                        else if (selectionStart <= start)
665
                        {
666
                            addedForSelection.AddRange(SolveRightGap(node, selectionStart, selectionEnd, start, end, docToEdit, tagToAdd));
667
                        }
668
                        else if (selectionEnd >= end)
669
                        {
670
                            addedForSelection.AddRange(SolveLeftGap(node, selectionStart, selectionEnd, start, end, docToEdit, tagToAdd));
671
                        }
672
                        else
673
                        {
674
                            addedForSelection.AddRange(SolveLeftRightGap(node, selectionStart, selectionEnd, start, end, docToEdit, tagToAdd));
675
                        }
676
                        descendantsToEdit = docToEdit.DocumentNode.DescendantsAndSelf().ToList();
677
                        descendantsCount = descendantsToEdit.Count();
678
                        break;
679
                    }
680
                }
681
            }
682

    
683
            GenerateCSS(tags);
684
            return docToEdit.DocumentNode.OuterHtml;
685
        }
686

    
687

    
688
        private string PartialPreprocessHTMLRemoveTag(string htmlToEdit, string htmlOriginal, AnnotationTag tagToRemove, List<AnnotationTag> tags)
689
        {
690
            var docOriginal = new HtmlDocument();
691
            docOriginal.LoadHtml(htmlOriginal);
692
            var docToEdit = new HtmlDocument();
693
            docToEdit.LoadHtml(htmlToEdit);
694

    
695
            var descendantsToEdit = docToEdit.DocumentNode.DescendantsAndSelf().ToList();
696

    
697
            int i = 0;
698
            int descendantsCount = descendantsToEdit.Count();
699
            while (i < descendantsCount)
700
            {
701
                for (; i < descendantsCount; i++)
702
                {
703
                    var node = descendantsToEdit.ElementAt(i);
704
                    if (!node.Attributes.Contains(TAG_EF_ID_ATTRIBUTE_NAME))
705
                    {
706
                        continue;
707
                    }
708
                    else
709
                    {
710
                        if (node.Attributes[TAG_EF_ID_ATTRIBUTE_NAME].Value != tagToRemove.Id.ToString())
711
                        {
712
                            continue;
713
                        }
714

    
715
                        node.Attributes.Remove(TAG_EF_ID_ATTRIBUTE_NAME);
716
                        node.Attributes.Remove(TAG_INSTANCE_ATTRIBUTE_NAME);
717
                        node.Attributes.Remove("class");
718
                        node.Attributes.Remove("start");
719
                        node.Attributes.Remove("end");
720

    
721
                        /*var parent = node.ParentNode;
722
                        var contents = node.ChildNodes;
723
                        int index = parent.ChildNodes.IndexOf(node);
724
                        parent.ChildNodes.RemoveAt(index);
725

    
726
                        List<HtmlNode> newChildren = new();
727
                        for (int j = 0; j < index; j++)
728
                        {
729
                            newChildren.Add(parent.ChildNodes[j]);
730
                        }
731
                        for (int j = 0; j < contents.Count; j++)
732
                        {
733
                            newChildren.Add(contents[j]);
734
                        }
735
                        for (int j = index; j < parent.ChildNodes.Count; j++)
736
                        {
737
                            newChildren.Add(parent.ChildNodes[j]);
738
                        }
739

    
740
                        parent.ChildNodes.Clear();
741
                        foreach (var child in newChildren) { parent.ChildNodes.Add(child); }*/
742

    
743
                        descendantsToEdit = docToEdit.DocumentNode.DescendantsAndSelf().ToList();
744
                        descendantsCount = descendantsToEdit.Count();
745
                        break;
746
                    }
747
                }
748
            }
749

    
750
            GenerateCSS(tags);
751
            return docToEdit.DocumentNode.OuterHtml;
752
        }
753

    
754
        /*
755
         *      Partial HTML Preprocessing ----------------------------------------------------------------------------
756
         */
757

    
758

    
759
        // TODO temporary
760
        private bool IsHtml(string text)
761
        {
762
            return text.Contains("<html>");
763
        }
764

    
765
        public void AddAnnotationInstance(Guid annotationId, Guid userId, ERole userRole, AnnotationInstanceAddRequest request)
766
        {
767
            var annotation = context.Annotations
768
               .Where(a => a.Id == annotationId)
769
               .Include(a => a.User)
770
               .Include(a => a.Document).ThenInclude(d => d.Content)
771
               .First();
772

    
773
            if (userRole < ERole.ADMINISTRATOR)
774
            {
775
                if (annotation.User.Id != userId)
776
                {
777
                    throw new UnauthorizedAccessException($"User {userId} does not have assigned annotation {annotationId}");
778
                }
779
            }
780

    
781
            if (annotation.State == EState.NEW)
782
            {
783
                annotation.State = EState.IN_PROGRESS;
784
            }
785

    
786
            AnnotationTag annotationTag = new()
787
            {
788
                Id = Guid.NewGuid(),
789
                Annotation = annotation,
790
                Instance = request.InstanceId == null ? Guid.NewGuid() : request.InstanceId.Value,
791
                Length = request.Length,
792
                Position = request.Position,
793
                SelectedText = request.SelectedText,
794
                Note = ""
795
            };
796

    
797
            if (request.Type == ETagType.TAG)
798
            {
799
                annotationTag.Tag = context.Tags.Where(t => t.Id == request.Id).Single();
800
                annotationTag.SubTag = null;
801

    
802
                if (annotationTag.Tag.SentimentEnabled)
803
                {
804
                    annotationTag.Sentiment = ETagSentiment.NEUTRAL;
805
                }
806

    
807
                // If for the same annotation exists a tag with same position and length and of the same type, ignore
808
                if (context.AnnotationTags.Any(at =>
809
                at.Position == annotationTag.Position &&
810
                at.Length == annotationTag.Length &&
811
                at.Annotation == annotation &&
812
                at.Tag == annotationTag.Tag))
813
                {
814
                    throw new InvalidOperationException("Duplicate tag");
815
                }
816

    
817
            }
818
            else if (request.Type == ETagType.SUBTAG)
819
            {
820
                var subTag = context.SubTags.Where(st => st.Id == request.Id).Include(st => st.Tag).Single();
821
                annotationTag.SubTag = subTag;
822
                annotationTag.Tag = subTag.Tag;
823

    
824
                if (annotationTag.SubTag.SentimentEnabled)
825
                {
826
                    annotationTag.Sentiment = ETagSentiment.NEUTRAL;
827
                }
828

    
829
                if (context.AnnotationTags.Any(at =>
830
                at.Position == annotationTag.Position &&
831
                at.Length == annotationTag.Length &&
832
                at.Annotation == annotation &&
833
                at.Tag == annotationTag.Tag &&
834
                at.SubTag == annotationTag.SubTag))
835
                {
836
                    throw new InvalidOperationException("Duplicate tag");
837
                }
838
            }
839
            else
840
            {
841
                throw new ArgumentException($"Unknown tag type {request.Type}");
842
            }
843

    
844
            annotation.LastModifiedTagId = annotationTag.Id;
845
            annotation.ModifiedType = EModified.ADDED;
846

    
847
            context.AnnotationTags.Add(annotationTag);
848
            context.SaveChanges();
849
        }
850

    
851
        public void DeleteAnnotationInstance(Guid annotationId, Guid tagInstanceId, Guid loggedUserId, ERole userRole)
852
        {
853
            Annotation annotation = null;
854
            try
855
            {
856
                annotation = context.Annotations
857
                   .Where(a => a.Id == annotationId)
858
                   .Include(a => a.User)
859
                   .Include(a => a.Document).ThenInclude(d => d.Content)
860
                   .First();
861

    
862
            }
863
            catch (Exception ex)
864
            {
865
                throw new InvalidOperationException("Could not find annotation");
866
            }
867

    
868

    
869
            if (userRole < ERole.ADMINISTRATOR)
870
            {
871
                if (annotation.User.Id != loggedUserId)
872
                {
873
                    throw new UnauthorizedAccessException($"User {loggedUserId} does not have assigned annotation {annotationId}");
874
                }
875
            }
876

    
877
            if (!context.AnnotationTags.Any(at => at.Id == tagInstanceId))
878
            {
879
                throw new InvalidOperationException("Could not find tag instance");
880
            }
881

    
882

    
883
            var annotationTag = context.AnnotationTags.First(at => at.Id == tagInstanceId);
884
            annotation.LastModifiedTagId = annotationTag.Id;
885
            annotation.ModifiedType = EModified.REMOVED;
886

    
887
            context.AnnotationTags.Remove(annotationTag);
888

    
889
            context.SaveChanges();
890
        }
891

    
892
        public void SetTagInstanceSentiment(Guid annotationId, Guid instanceId, Guid userId, ERole userRole, ETagSentiment sentiment)
893
        {
894
            Annotation annotation = null;
895
            try
896
            {
897
                annotation = context.Annotations
898
                   .Where(a => a.Id == annotationId)
899
                   .Include(a => a.User)
900
                   .Include(a => a.Document).ThenInclude(d => d.Content)
901
                   .First();
902

    
903
            }
904
            catch (Exception ex)
905
            {
906
                throw new InvalidOperationException("Could not find annotation");
907
            }
908

    
909

    
910
            if (userRole < ERole.ADMINISTRATOR)
911
            {
912
                if (annotation.User.Id != userId)
913
                {
914
                    throw new UnauthorizedAccessException($"User {userId} does not have assigned annotation {annotationId}");
915
                }
916
            }
917

    
918

    
919
            var tagInstances = context.AnnotationTags.Where(at => at.Instance == instanceId).ToList();
920
            if (tagInstances.Count() == 0)
921
            {
922
                throw new InvalidOperationException("No such instance found");
923
            }
924

    
925
            foreach (var tagInstance in tagInstances)
926
            {
927
                tagInstance.Sentiment = sentiment;
928
            }
929

    
930
            context.SaveChanges();
931
        }
932

    
933
        public void MarkAnnotationAsDone(Guid annotationId, Guid userId, ERole userRole, bool done)
934
        {
935
            Annotation annotation = null;
936
            try
937
            {
938
                annotation = context.Annotations
939
                   .Where(a => a.Id == annotationId)
940
                   .Include(a => a.User)
941
                   .Include(a => a.Document).ThenInclude(d => d.Content)
942
                   .First();
943

    
944
            }
945
            catch (Exception ex)
946
            {
947
                throw new InvalidOperationException("Could not find annotation");
948
            }
949

    
950

    
951
            if (userRole < ERole.ADMINISTRATOR)
952
            {
953
                if (annotation.User.Id != userId)
954
                {
955
                    throw new UnauthorizedAccessException($"User {userId} does not have assigned annotation {annotationId}");
956
                }
957
            }
958

    
959
            annotation.State = done ? EState.DONE : EState.IN_PROGRESS;
960
            context.SaveChanges();
961
        }
962
    }
963
}
(1-1/2)