Projekt

Obecné

Profil

Stáhnout (23.5 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
        private readonly IHTMLService htmlService;
28

    
29

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

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

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

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

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

    
75
            context.SaveChanges();
76
        }
77

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

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

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

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

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

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

    
123

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

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

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

    
142
            var tags = context.AnnotationTags.Where(at => at.Annotation.Id == annotationId)
143
                .Include(at => at.Tag)
144
                .ThenInclude(t => t.Category)
145
                .Include(at => at.SubTag)
146
                .OrderBy(at => at.Position)
147
                .Select(at => at as AnnotationTagGeneric)
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
            HTMLService.CachedInfo cachedInfoToReturn = new(); 
159
            if (annotation.CachedDocumentHTML == "")
160
            {
161
                var result = htmlService.FullPreprocessHTML(documentContent.Content, tags);
162
                cachedInfoToReturn = result.Item2;
163
                docToRender = result.Item1;
164

    
165
                annotation.CachedStartPositions = JsonConvert.SerializeObject(cachedInfoToReturn.TagStartPositions);
166
                annotation.CachedLengths = JsonConvert.SerializeObject(cachedInfoToReturn.TagStartLengths);
167
                annotation.CachedClosingPositions = JsonConvert.SerializeObject(cachedInfoToReturn.TagClosingPositions);
168
                annotation.CachedClosingLengths = JsonConvert.SerializeObject(cachedInfoToReturn.TagClosingLengths);
169
                annotation.CachedCSS = JsonConvert.SerializeObject(cachedInfoToReturn.TagInstanceCSS);
170
                annotation.ModifiedType = EModified.NONE;
171
                annotation.CachedDocumentHTML = docToRender;
172
                context.SaveChanges();
173
            }
174
            else
175
            {
176
                docToRender = annotation.CachedDocumentHTML;
177
                cachedInfoToReturn = new()
178
                {
179
                    TagStartPositions = JsonConvert.DeserializeObject<List<int>>(annotation.CachedStartPositions),
180
                    TagStartLengths = JsonConvert.DeserializeObject<List<int>>(annotation.CachedLengths),
181
                    TagClosingPositions = JsonConvert.DeserializeObject<List<int>>(annotation.CachedClosingPositions),
182
                    TagClosingLengths = JsonConvert.DeserializeObject<List<int>>(annotation.CachedClosingLengths),
183
                    TagInstanceCSS = JsonConvert.DeserializeObject<List<TagInstanceCSSInfo>>(annotation.CachedCSS)
184
                };
185
                
186
                // The annotation has been modified and we need to either add the new tag or remove the tag
187
                if (annotation.ModifiedType != EModified.NONE)
188
                {
189
                    if (annotation.ModifiedType == EModified.ADDED)
190
                    {
191
                        var lastModifiedTag = context.AnnotationTags.Where(at => at.Id == annotation.LastModifiedTagId).First();
192
                        var result = htmlService.PartialPreprocessHTMLAddTag(docToRender, documentContent.Content, lastModifiedTag, tags, cachedInfoToReturn);
193
                        docToRender = result.Item1;
194
                        cachedInfoToReturn = result.Item2;
195
                    }
196
                    else if (annotation.ModifiedType == EModified.REMOVED)
197
                    {
198
                        var result = htmlService.PartialPreprocessHTMLRemoveTag(docToRender, documentContent.Content, new AnnotationTag() { Id = annotation.LastModifiedTagId.Value }, tags, cachedInfoToReturn);
199
                        docToRender = result.Item1;
200
                        cachedInfoToReturn = result.Item2;
201
                    }
202

    
203
                    annotation.ModifiedType = EModified.NONE;
204
                    annotation.CachedStartPositions = JsonConvert.SerializeObject(cachedInfoToReturn.TagStartPositions);
205
                    annotation.CachedLengths = JsonConvert.SerializeObject(cachedInfoToReturn.TagStartLengths);
206
                    annotation.CachedClosingPositions = JsonConvert.SerializeObject(cachedInfoToReturn.TagClosingPositions);
207
                    annotation.CachedClosingLengths = JsonConvert.SerializeObject(cachedInfoToReturn.TagClosingLengths);
208
                    annotation.CachedDocumentHTML = docToRender;
209
                    annotation.CachedCSS = JsonConvert.SerializeObject(cachedInfoToReturn.TagInstanceCSS);
210
                    context.SaveChanges();
211
                }
212
            }
213

    
214
            // We probably cannot use AutoMapper since we are dealing with too many different entities
215
            AnnotationInfo annotationInfo = new()
216
            {
217
                SourceDocumentContent = documentContent.Content,
218
                DocumentToRender = docToRender,
219
                TagStartPositions = cachedInfoToReturn.TagStartPositions.ToArray(),
220
                TagLengths = cachedInfoToReturn.TagStartLengths.ToArray(),
221
                Note = annotation.Note,
222
                State = annotation.State,
223
                Type = IsHtml(documentContent.Content) ? EDocumentType.HTML : EDocumentType.TEXT,
224
                TagInstances = tagInstanceInfos,
225
                CSSInfo = cachedInfoToReturn.TagInstanceCSS
226
            };
227

    
228
            return annotationInfo;
229
        }
230

    
231
        // TODO temporary
232
        private bool IsHtml(string text)
233
        {
234
            return text.Contains("<html>");
235
        }
236

    
237
        public void AddAnnotationInstance(Guid annotationId, Guid userId, ERole userRole, AnnotationInstanceAddRequest request)
238
        {
239
            var annotation = context.Annotations
240
               .Where(a => a.Id == annotationId)
241
               .Include(a => a.User)
242
               .Include(a => a.Document).ThenInclude(d => d.Content)
243
               .First();
244

    
245
            if (userRole < ERole.ADMINISTRATOR)
246
            {
247
                if (annotation.User.Id != userId)
248
                {
249
                    throw new UnauthorizedAccessException($"User {userId} does not have assigned annotation {annotationId}");
250
                }
251
            }
252

    
253
            AnnotationTag annotationTag = new()
254
            {
255
                Id = Guid.NewGuid(),
256
                Annotation = annotation,
257
                Instance = request.InstanceId == null ? Guid.NewGuid() : request.InstanceId.Value,
258
                Length = request.Length,
259
                Position = request.Position,
260
                SelectedText = request.SelectedText,
261
                Note = ""
262
            };
263

    
264
            if (request.Type == ETagType.TAG)
265
            {
266
                annotationTag.Tag = context.Tags.Where(t => t.Id == request.Id).Single();
267
                annotationTag.SubTag = null;
268

    
269
                if (annotationTag.Tag.SentimentEnabled)
270
                {
271
                    annotationTag.Sentiment = ETagSentiment.NEUTRAL;
272
                }
273

    
274
                // If for the same annotation exists a tag with same position and length and of the same type, ignore
275
                if (context.AnnotationTags.Any(at =>
276
                at.Position == annotationTag.Position &&
277
                at.Length == annotationTag.Length &&
278
                at.Annotation == annotation &&
279
                at.Tag == annotationTag.Tag))
280
                {
281
                    throw new InvalidOperationException("Duplicate tag");
282
                }
283

    
284
            }
285
            else if (request.Type == ETagType.SUBTAG)
286
            {
287
                var subTag = context.SubTags.Where(st => st.Id == request.Id).Include(st => st.Tag).Single();
288
                annotationTag.SubTag = subTag;
289
                annotationTag.Tag = subTag.Tag;
290

    
291
                if (annotationTag.SubTag.SentimentEnabled)
292
                {
293
                    annotationTag.Sentiment = ETagSentiment.NEUTRAL;
294
                }
295

    
296
                if (context.AnnotationTags.Any(at =>
297
                at.Position == annotationTag.Position &&
298
                at.Length == annotationTag.Length &&
299
                at.Annotation == annotation &&
300
                at.Tag == annotationTag.Tag &&
301
                at.SubTag == annotationTag.SubTag))
302
                {
303
                    throw new InvalidOperationException("Duplicate tag");
304
                }
305
            }
306
            else
307
            {
308
                throw new ArgumentException($"Unknown tag type {request.Type}");
309
            }
310

    
311
            if (annotation.State == EState.NEW)
312
            {
313
                annotation.State = EState.IN_PROGRESS;
314
            }
315

    
316
            annotation.LastModifiedTagId = annotationTag.Id;
317
            annotation.ModifiedType = EModified.ADDED;
318

    
319
            context.AnnotationTags.Add(annotationTag);
320
            context.SaveChanges();
321
        }
322

    
323
        public void DeleteAnnotationInstance(Guid annotationId, Guid tagInstanceId, Guid loggedUserId, ERole userRole)
324
        {
325
            Annotation annotation = null;
326
            try
327
            {
328
                annotation = context.Annotations
329
                   .Where(a => a.Id == annotationId)
330
                   .Include(a => a.User)
331
                   .Include(a => a.Document).ThenInclude(d => d.Content)
332
                   .First();
333

    
334
            }
335
            catch (Exception ex)
336
            {
337
                throw new InvalidOperationException("Could not find annotation");
338
            }
339

    
340

    
341
            if (userRole < ERole.ADMINISTRATOR)
342
            {
343
                if (annotation.User.Id != loggedUserId)
344
                {
345
                    throw new UnauthorizedAccessException($"User {loggedUserId} does not have assigned annotation {annotationId}");
346
                }
347
            }
348

    
349
            if (!context.AnnotationTags.Any(at => at.Id == tagInstanceId))
350
            {
351
                throw new InvalidOperationException("Could not find tag instance");
352
            }
353

    
354

    
355
            var annotationTag = context.AnnotationTags.First(at => at.Id == tagInstanceId);
356
            annotation.LastModifiedTagId = annotationTag.Id;
357
            annotation.ModifiedType = EModified.REMOVED;
358

    
359
            context.AnnotationTags.Remove(annotationTag);
360

    
361
            context.SaveChanges();
362
        }
363

    
364
        public void SetTagInstanceSentiment(Guid annotationId, Guid instanceId, Guid userId, ERole userRole, ETagSentiment sentiment)
365
        {
366
            Annotation annotation = null;
367
            try
368
            {
369
                annotation = context.Annotations
370
                   .Where(a => a.Id == annotationId)
371
                   .Include(a => a.User)
372
                   .Include(a => a.Document).ThenInclude(d => d.Content)
373
                   .First();
374

    
375
            }
376
            catch (Exception ex)
377
            {
378
                throw new InvalidOperationException("Could not find annotation");
379
            }
380

    
381

    
382
            if (userRole < ERole.ADMINISTRATOR)
383
            {
384
                if (annotation.User.Id != userId)
385
                {
386
                    throw new UnauthorizedAccessException($"User {userId} does not have assigned annotation {annotationId}");
387
                }
388
            }
389

    
390

    
391
            var tagInstances = context.AnnotationTags.Where(at => at.Instance == instanceId).ToList();
392
            if (tagInstances.Count() == 0)
393
            {
394
                throw new InvalidOperationException("No such instance found");
395
            }
396

    
397
            foreach (var tagInstance in tagInstances)
398
            {
399
                tagInstance.Sentiment = sentiment;
400
            }
401

    
402
            context.SaveChanges();
403
        }
404

    
405
        public void MarkAnnotationAsDone(Guid annotationId, Guid userId, ERole userRole, bool done)
406
        {
407
            Annotation annotation = null;
408
            try
409
            {
410
                annotation = context.Annotations
411
                   .Where(a => a.Id == annotationId)
412
                   .Include(a => a.User)
413
                   .Include(a => a.Document).ThenInclude(d => d.Content)
414
                   .First();
415

    
416
            }
417
            catch (Exception ex)
418
            {
419
                throw new InvalidOperationException("Could not find annotation");
420
            }
421

    
422

    
423
            if (userRole < ERole.ADMINISTRATOR)
424
            {
425
                if (annotation.User.Id != userId)
426
                {
427
                    throw new UnauthorizedAccessException($"User {userId} does not have assigned annotation {annotationId}");
428
                }
429
            }
430

    
431
            annotation.State = done ? EState.DONE : EState.IN_PROGRESS;
432
            context.SaveChanges();
433
        }
434

    
435
        public Guid CreateFinalAnnotation(Guid documentId, Guid userId)
436
        {
437
            var document = context.Documents.Single(d => d.Id == documentId);
438
            var user = context.Users.Single(u => u.Id == userId);
439

    
440
            // Remove existing 
441
            if (context.FinalAnnotations.Any(fa => fa.Document == document))
442
            {
443
                var finalAnnotationOld = context.FinalAnnotations.Single(fa => fa.Document == document);
444
                context.FinalAnnotations.Remove(finalAnnotationOld);
445
            }
446

    
447
            var annotations = context.Annotations
448
                .Include(a => a.Document)
449
                .Where(a => a.Document == document && a.State == EState.DONE)
450
                .ToList();
451

    
452
            var finalAnnotation = new FinalAnnotation()
453
            {
454
                Id = Guid.NewGuid(),
455
                DateAssigned = DateTime.Now,
456
                DateLastChanged = DateTime.Now,
457
                Document = document,
458
                User = user,
459
                UserAssigned = user,
460
                Annotations = annotations,
461
                State = EState.NEW
462
            };
463

    
464
            List<AnnotationTag> annotationTagsAll = new();
465
            foreach (var annotation in annotations)
466
            {
467
                annotationTagsAll.AddRange(context.AnnotationTags
468
                    .Where(at => at.Annotation == annotation)
469
                    .Include(at => at.Tag)
470
                    .Include(at => at.SubTag)
471
                    .Include(at => at.Annotation)
472
                    .ThenInclude(a => a.User)
473
                    .ToList());
474
            }
475
            annotationTagsAll = annotationTagsAll.OrderBy(at => at.Position).ToList();
476

    
477
            Dictionary<Guid, List<AnnotationTag>> occurenceDict = new();
478
            foreach (var annotationTag in annotationTagsAll)
479
            {
480
                if (occurenceDict.ContainsKey(annotationTag.Instance))
481
                {
482
                    occurenceDict[annotationTag.Instance].Add(annotationTag);
483
                }
484
                else
485
                {
486
                    occurenceDict[annotationTag.Instance] = new();
487
                    occurenceDict[annotationTag.Instance].Add(annotationTag);
488
                }
489
            }
490

    
491
            List<List<AnnotationTag>> occurenceLists = new();
492
            foreach (var key in occurenceDict.Keys)
493
            {
494
                occurenceLists.Add(occurenceDict[key]);
495
            }
496

    
497
            List<List<AnnotationTag>> annotationTagsProcessed = new();
498
            List<FinalAnnotationTag> finalAnnotationTags = new();
499
            for (int i = 0; i < occurenceLists.Count; i++)
500
            {
501
                var occurrenceList1 = occurenceLists[i];
502
                List<List<AnnotationTag>> sameLists = new();
503

    
504
                for (int j = 0; j < occurenceLists.Count; j++)
505
                {
506
                    var occurrenceList2 = occurenceLists[j];
507

    
508
                    if (annotationTagsProcessed.Contains(occurrenceList2))
509
                    {
510
                        continue;
511
                    }
512

    
513
                    if (SelectionsAreSame(occurrenceList1, occurrenceList2))
514
                    {
515
                        sameLists.Add(occurrenceList2);
516
                    }
517
                }
518

    
519
                // This means that this occurrence ahs already been processed as matching a previous tag
520
                if (sameLists.Count() == 0)
521
                {
522
                    continue;
523
                }
524

    
525
                List<User> relatedUsers = new();
526
                foreach (var list in sameLists)
527
                {
528
                    relatedUsers.Add(list[0].Annotation.User);
529
                }
530

    
531
                foreach (var tag in sameLists[0])
532
                {
533
                    finalAnnotationTags.Add(new()
534
                    {
535
                        Id = Guid.NewGuid(),
536
                        Tag = tag.Tag,
537
                        SubTag = tag.SubTag,
538
                        Annotation = finalAnnotation,
539
                        SelectedText = tag.SelectedText,
540
                        Sentiment = tag.Sentiment,
541
                        Instance = tag.Instance,
542
                        IsFinal = sameLists.Count == annotations.Count,
543
                        Length = tag.Length,
544
                        Position = tag.Position,
545
                        Note = "",
546
                        Users = relatedUsers
547
                    });
548
                }
549

    
550
                annotationTagsProcessed.AddRange(sameLists);
551
            }
552

    
553
            context.FinalAnnotations.Add(finalAnnotation);
554
            context.SaveChanges();
555
            context.FinalAnnotationTags.AddRange(finalAnnotationTags);
556

    
557
            context.SaveChanges();
558
            return finalAnnotation.Id;
559
        }
560

    
561
        private bool SelectionsAreSame(List<AnnotationTag> list1, List<AnnotationTag> list2)
562
        {
563
            if (list1.Count != list2.Count)
564
            {
565
                return false;
566
            }
567

    
568
            bool sentimentEnabled = list1[0].Tag.SentimentEnabled;
569

    
570
            if (sentimentEnabled)
571
            {
572
                for (int i = 0; i < list1.Count; i++)
573
                {
574
                    var tag1 = list1[i];
575
                    var tag2 = list2[i];
576
                    if (tag1.Position == tag2.Position &&
577
                        tag1.Length == tag2.Length &&
578
                        tag1.Sentiment == tag2.Sentiment &&
579
                        tag1.Tag == tag2.Tag &&
580
                        tag1.SubTag == tag2.SubTag)
581
                    {
582
                        continue;
583
                    }
584
                    else
585
                    {
586
                        return false;
587
                    }
588
                }
589
            }
590
            else
591
            {
592
                for (int i = 0; i < list1.Count; i++)
593
                {
594
                    var tag1 = list1[i];
595
                    var tag2 = list2[i];
596
                    if (tag1.Position == tag2.Position &&
597
                        tag1.Length == tag2.Length &&
598
                        tag1.Tag == tag2.Tag &&
599
                        tag1.SubTag == tag2.SubTag)
600
                    {
601
                        continue;
602
                    }
603
                    else
604
                    {
605
                        return false;
606
                    }
607
                }
608
            }
609

    
610
            return true;
611
        }
612
    }
613
}
(1-1/2)