1
2
3
4
5
6
7 package ch.colabproject.colab.api.microchanges.live;
8
9 import ch.colabproject.colab.api.microchanges.model.Change;
10 import ch.colabproject.colab.api.microchanges.model.MicroChange;
11 import ch.colabproject.colab.api.microchanges.model.MicroChange.Type;
12 import java.io.Serializable;
13 import java.util.ArrayList;
14 import java.util.Collection;
15 import java.util.HashMap;
16 import java.util.HashSet;
17 import java.util.LinkedList;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Optional;
21 import java.util.Set;
22 import java.util.stream.Collectors;
23 import org.apache.commons.text.StringEscapeUtils;
24 import org.slf4j.Logger;
25 import org.slf4j.LoggerFactory;
26
27
28
29
30
31
32 public class LiveUpdates implements Serializable {
33
34 private static final long serialVersionUID = 1L;
35
36
37 private static final Logger logger = LoggerFactory.getLogger(LiveUpdates.class);
38
39
40
41
42 private String targetClass;
43
44
45
46
47 private Long targetId;
48
49
50
51
52 private String revision;
53
54
55
56
57 private String content;
58
59
60
61
62 private List<Change> pendingChanges = new ArrayList<>();
63
64
65
66
67 private transient String debugData = null;
68
69
70
71
72
73
74 public String getTargetClass() {
75 return targetClass;
76 }
77
78
79
80
81
82
83 public void setTargetClass(String targetClass) {
84 this.targetClass = targetClass;
85 }
86
87
88
89
90
91
92 public Long getTargetId() {
93 return targetId;
94 }
95
96
97
98
99
100
101 public void setTargetId(Long targetId) {
102 this.targetId = targetId;
103 }
104
105
106
107
108
109
110 public String getRevision() {
111 return revision;
112 }
113
114
115
116
117
118
119 public void setRevision(String revision) {
120 this.revision = revision;
121 }
122
123
124
125
126
127
128 public String getContent() {
129 return content;
130 }
131
132
133
134
135
136
137 public void setContent(String content) {
138 this.content = content;
139 }
140
141
142
143
144
145
146 public List<Change> getPendingChanges() {
147 return pendingChanges;
148 }
149
150
151
152
153
154
155 public void setPendingChanges(List<Change> pendingChanges) {
156 this.pendingChanges = pendingChanges;
157 }
158
159
160
161
162
163
164
165
166
167 public Change getByRevision(List<Change> changes, String revision) {
168 Optional<Change> findAny = changes.stream()
169 .filter(ch -> ch.getRevision().equals(revision))
170 .findAny();
171 return findAny.isPresent() ? findAny.get() : null;
172 }
173
174
175
176
177
178
179
180
181
182 public List<Change> getByParent(List<Change> changes, String basedOn) {
183 List<Change> collect = changes.stream()
184 .filter(ch -> ch.getBasedOn().contains(basedOn))
185 .collect(Collectors.toList());
186 return new ArrayList<>(collect);
187 }
188
189
190
191
192
193
194
195
196
197
198 public List<Change> getByParentAndSession(List<Change> changes, Change parent) {
199 logger.trace("Get Children By Parent And Session");
200
201 List<Change> collect = changes.stream()
202 .filter(ch -> ch.getBasedOn().contains(parent.getRevision())
203 && ch.getLiveSession().equals(parent.getLiveSession()))
204 .collect(Collectors.toList());
205 return new ArrayList<>(collect);
206 }
207
208
209
210
211
212
213
214
215 private void modifyOffset(Map<Integer, Integer> offsets, Integer index, Integer value) {
216 Integer currentOffset = offsets.get(index);
217 if (currentOffset == null) {
218 currentOffset = 0;
219 }
220 currentOffset += value;
221 logger.trace(" modOffset.start " + offsets);
222
223 offsets.put(index, currentOffset);
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260 }
261
262
263
264
265
266
267
268 private void applyChange(StringBuilder buffer, MicroChange mu) {
269 logger.trace("Apply {} to {}", mu, buffer);
270 if (mu.getT() == MicroChange.Type.D) {
271 if (mu.getO() < buffer.length()) {
272 buffer.length();
273 buffer.delete(mu.getO(), mu.getO() + mu.getL());
274 } else {
275 logger.trace("Skip micro change");
276 }
277 } else if (mu.getT() == MicroChange.Type.I) {
278 if (mu.getO() >= buffer.length()) {
279 buffer.append(mu.getV());
280 } else {
281 buffer.insert(mu.getO(), mu.getV());
282 }
283 }
284 }
285
286
287
288
289
290
291
292
293
294
295 private Map<Integer, Integer> computeOffset(Change change) {
296 Map<Integer, Integer> offsets = new HashMap<>();
297
298 List<MicroChange> muChanges = change.getMicrochanges();
299 for (int i = muChanges.size() - 1; i >= 0; i--) {
300 MicroChange mu = muChanges.get(i);
301 if (mu.getT() == MicroChange.Type.D) {
302 modifyOffset(offsets, mu.getO() + mu.getL(), -mu.getL());
303 } else if (mu.getT() == MicroChange.Type.I) {
304 modifyOffset(offsets, mu.getO(), mu.getV().length());
305 }
306 }
307
308 return offsets;
309 }
310
311
312
313
314
315
316
317
318
319
320 private boolean shift(Change change, Map<Integer, Integer> offsets, boolean forward) {
321 boolean conflictFree = true;
322 if (forward == false) {
323 logger.warn("TODO: implement backward shift");
324 }
325
326
327 logger.trace("Shift offsets: {}", change);
328 List<MicroChange> microchanges = change.getMicrochanges();
329 for (int i = 0; i < microchanges.size(); i++) {
330 MicroChange mu = microchanges.get(i);
331
332 for (Map.Entry<Integer, Integer> entry : offsets.entrySet()) {
333 Integer offsetValue = entry.getValue();
334 Integer offsetIndex = entry.getKey();
335
336 int muStart = mu.getO();
337
338 if (offsetValue > 0 && mu.getT().equals(Type.I)) {
339
340 if (mu.getO() >= offsetIndex) {
341
342 mu.setO(mu.getO() + offsetValue);
343 }
344 } else if (offsetValue < 0 && mu.getT().equals(Type.D)) {
345
346 int deleteFromIndex = offsetIndex + offsetValue;
347 int deleteToIndex = offsetIndex;
348 int muEnd = muStart + mu.getL();
349
350
351 if (muEnd >= deleteFromIndex) {
352
353
354 if (muStart > deleteToIndex) {
355
356
357 mu.setO(mu.getO() + offsetValue);
358 } else {
359
360 if (muStart <= deleteFromIndex && muEnd >= deleteToIndex) {
361
362
363
364
365 mu.setL(mu.getL() + offsetValue);
366 } else if (muStart >= deleteFromIndex && muEnd <= deleteToIndex) {
367
368
369
370
371 microchanges.remove(i);
372 i--;
373 } else if (muStart <= deleteFromIndex && muEnd <= deleteToIndex) {
374
375
376
377
378 mu.setL(deleteFromIndex - muStart);
379 } else if (muStart >= deleteFromIndex && muEnd >= deleteToIndex) {
380
381
382
383
384 mu.setL(muEnd - deleteToIndex);
385 mu.setO(deleteFromIndex);
386 } else {
387 logger.error("Unhandled case offset{}:{}, mu:{}",
388 deleteFromIndex, offsetValue, mu);
389 }
390 }
391 }
392 } else if (offsetValue < 0 && mu.getT().equals(Type.I)) {
393
394 int deleteFromIndex = offsetIndex + offsetValue;
395 int deleteToIndex = offsetIndex;
396
397 if (muStart >= deleteToIndex) {
398
399
400
401 mu.setO(mu.getO() + offsetValue);
402 } else if (muStart > deleteFromIndex) {
403
404
405
406 mu.setO(deleteFromIndex);
407
408
409
410
411
412 }
413 } else if (offsetValue > 0 && mu.getT().equals(Type.D)) {
414
415 int muEnd = muStart + mu.getL();
416
417
418 if (muEnd >= offsetIndex) {
419 if (muStart > offsetIndex) {
420
421
422
423 mu.setO(mu.getO() + offsetValue);
424 } else {
425
426
427
428
429 Integer totalLength = mu.getL();
430 mu.setL(offsetIndex - muStart);
431 MicroChange newMu = new MicroChange();
432 newMu.setT(Type.D);
433 newMu.setO(offsetIndex + offsetValue);
434 newMu.setL(totalLength - mu.getL());
435 microchanges.add(i + 1, newMu);
436 i++;
437 }
438 }
439 }
440 }
441 }
442
443 logger.trace(
444 "Shift done: {}", change);
445
446 return conflictFree;
447 }
448
449
450
451
452
453
454
455
456
457 private Map<Integer, Integer> shiftOffsets(Map<Integer, Integer> offsets, Change change) {
458 Map<Integer, Integer> shifted = new HashMap<>();
459
460 offsets.entrySet().forEach(entry -> {
461 Integer offsetIndex = entry.getKey();
462 Integer offsetValue = entry.getValue();
463
464 List<MicroChange> muChanges = change.getMicrochanges();
465 for (int i = muChanges.size() - 1; i >= 0; i--) {
466 MicroChange mu = muChanges.get(i);
467 if (mu.getO() <= offsetIndex) {
468 if (mu.getT() == MicroChange.Type.D) {
469 offsetIndex -= mu.getL();
470 } else if (mu.getT() == MicroChange.Type.I) {
471 offsetIndex += mu.getV().length();
472 }
473 }
474 }
475 shifted.put(offsetIndex, offsetValue);
476 });
477
478 return shifted;
479 }
480
481
482
483
484
485
486
487
488
489 private boolean propagateOffsets(List<Change> changes, Change parent,
490 Map<Integer, Integer> offsets, boolean forward, String offsetFromRev) {
491 boolean conflictFree = true;
492
493 for (Change child : getByParent(changes, parent.getRevision())) {
494 Set<String> childDeps = getAllDependencies(changes, child);
495 if (!childDeps.contains(offsetFromRev)) {
496 logger.trace("PropagateOffset {}@{} to {}", offsets, offsetFromRev, child);
497
498 boolean shiftFree = this.shift(child, offsets, forward);
499 Map<Integer, Integer> shiftedOffsets = shiftOffsets(offsets, child);
500 logger.trace("Shifted Offsets: {}", shiftedOffsets);
501 boolean pFree = this.propagateOffsets(changes, child, shiftedOffsets, forward,
502 offsetFromRev);
503 conflictFree = conflictFree && shiftFree && pFree;
504 } else {
505
506 HashSet<String> newDeps = new HashSet<>(child.getBasedOn());
507 newDeps.remove(offsetFromRev);
508 logger.trace("Do not go deeper than {}, now based on {}", child, newDeps);
509 child.setBasedOn(newDeps);
510
511 }
512 }
513 return conflictFree;
514 }
515
516
517
518
519
520
521
522
523
524 private Set<String> getAllDependencies(List<Change> changes, Change change) {
525 Set<String> deps = new HashSet<>();
526
527 List<Change> queue = new LinkedList<>();
528 queue.add(change);
529
530 while (!queue.isEmpty()) {
531 Change ch = queue.remove(0);
532 ch.getBasedOn().forEach(dep -> {
533 if (!deps.contains(dep)) {
534 deps.add(dep);
535 Change parent = getByRevision(changes, dep);
536 if (parent != null && !queue.contains(parent)) {
537 queue.add(parent);
538 }
539 }
540 });
541 }
542
543 return deps;
544 }
545
546
547
548
549
550
551
552
553
554 private boolean setsEqual(Set<String> a, Set<String> b) {
555 if (a == null && b == null) {
556
557 return true;
558 } else if (a == null || b == null) {
559
560 return false;
561 } else {
562 if (a.size() != b.size()) {
563 return false;
564 }
565 return a.containsAll(b);
566 }
567 }
568
569
570
571
572
573
574
575
576
577
578 private boolean rebase(List<Change> changes, Change newBase, Change change) {
579 Set<String> baseDeps = getAllDependencies(changes, newBase);
580 Set<String> changeDeps = getAllDependencies(changes, change);
581
582 if (setsEqual(baseDeps, changeDeps)) {
583 try {
584
585 Map<Integer, Integer> offsets = computeOffset(newBase);
586 boolean conflictFree = true;
587 String newBaseRev = newBase.getRevision();
588
589 logger.trace("Rebase Sieblings: " + change + " on " + newBase
590 + " with offset " + offsets);
591
592 conflictFree = shift(change, offsets, true) && conflictFree;
593 conflictFree = propagateOffsets(changes, change,
594 offsets, true, newBaseRev) && conflictFree;
595
596
597 change.setBasedOn(Set.of(newBase.getRevision()));
598 logger.trace(" -> " + change);
599 return conflictFree;
600 } catch (StackOverflowError e) {
601 logger.warn("Major issue: fail to propagate offset");
602 printDebugData();
603 throw e;
604 }
605 } else if (setsEqual(Set.of(change.getRevision()), newBase.getBasedOn())) {
606 logger.trace("Inverse hierarchy : " + change + " on " + newBase);
607
608
609
610 boolean conflictFree = true;
611
612 Map<Integer, Integer> changeOffsets = computeOffset(change);
613
614 newBase.setBasedOn(change.getBasedOn());
615 change.setBasedOn(Set.of(newBase.getRevision()));
616
617 conflictFree = shift(newBase, changeOffsets, false) && conflictFree;
618
619 Map<Integer, Integer> newBaseOffsets = computeOffset(newBase);
620 conflictFree = shift(change, newBaseOffsets, true) && conflictFree;
621
622 logger.trace(" with offsets " + changeOffsets + " and " + newBaseOffsets);
623 logger.trace(" -> " + change);
624
625 return conflictFree;
626 } else if (changeDeps.containsAll(baseDeps)) {
627
628 logger.trace("Nothing to do: change includes all base parents");
629 return true;
630 } else {
631 logger.error(
632 "Not yet implemented: Changes: {} Change: {} NewBase: {} BaseDeps: {} ChangeDeps: {}",
633 changes, change.getRevision(), newBase.getRevision(), baseDeps, changeDeps);
634 return false;
635 }
636 }
637
638
639
640
641
642
643
644
645
646 public List<Change> filterByAuthor(List<Change> changes, String author) {
647 return changes.stream()
648 .filter(child -> child.getLiveSession().equals(author))
649 .collect(Collectors.toList());
650 }
651
652 private List<String> mapChangesRevision(Collection<Change> changes) {
653 return changes.stream().map(Change::getRevision).collect(Collectors.toList());
654 }
655
656
657
658
659
660
661
662
663 public LiveResult process(boolean strict) {
664 initDebugData();
665 logger.debug("Debug Data {}", this.debugData);
666
667 StringBuilder buffer = new StringBuilder();
668 if (this.content != null) {
669 buffer.append(this.content);
670 }
671
672 logger.trace("Process: {}", this);
673
674 String currentRevision = this.revision;
675
676 List<Change> allChanges = this.getPendingChanges();
677 List<Change> changes = new ArrayList<>(allChanges);
678
679 Set<String> appliedChanges = new HashSet<>();
680
681 while (!changes.isEmpty()) {
682 appliedChanges.add(currentRevision);
683
684 List<Change> children = getByParent(changes, currentRevision);
685 if (!children.isEmpty()) {
686
687
688 logger.trace("All @{} children: {}", currentRevision, mapChangesRevision(children));
689
690
691
692
693 Optional<Change> optChange = children.stream()
694 .filter(ch -> appliedChanges.containsAll(ch.getBasedOn()))
695 .findFirst();
696 if (optChange.isPresent()) {
697 Change change = optChange.get();
698
699 changes.remove(change);
700 children.remove(change);
701
702 logger.trace("Process: {}", change);
703
704 List<MicroChange> muChanges = change.getMicrochanges();
705 for (int i = muChanges.size() - 1; i >= 0; i--) {
706 applyChange(buffer, muChanges.get(i));
707 logger.trace(" " + i + ")" + buffer);
708 }
709
710 logger.trace(" -> {}", buffer);
711
712
713
714 changes.removeAll(children);
715 for (int i = children.size() - 1; i >= 0; i--) {
716 Change child = children.remove(i);
717 if (!rebase(allChanges, change, child) && strict) {
718
719 logger.warn("Conflict");
720 }
721 changes.add(0, child);
722 }
723 currentRevision = change.getRevision();
724 } else {
725
726 logger.error("No child found in {}", children);
727 printDebugData();
728 break;
729 }
730
731 } else {
732
733 logger.error("Some children without any parents left: {}", changes);
734 printDebugData();
735 break;
736 }
737 }
738
739 return LiveResult.build(buffer.toString(), currentRevision);
740 }
741
742 @Override
743 public String toString() {
744 return "LiveUpdates{" + "targetClass=" + targetClass + ", targetId=" + targetId
745 + ", revision=" + revision + ", content=" + content + ", pendingChanges="
746 + pendingChanges + '}';
747 }
748
749
750
751
752 public void initDebugData() {
753
754 StringBuilder sb = new StringBuilder();
755 sb.append("test('A Test', () => {")
756 .append(System.lineSeparator())
757 .append("const initialValue = \"")
758 .append(StringEscapeUtils.escapeEcmaScript(this.content))
759 .append("\";").append(System.lineSeparator())
760 .append("const initialRevision = \"")
761 .append(StringEscapeUtils.escapeEcmaScript(this.revision))
762 .append("\";").append(System.lineSeparator())
763 .append(System.lineSeparator())
764 .append(System.lineSeparator())
765 .append("const changes =[")
766 .append(System.lineSeparator());
767
768 this.pendingChanges.forEach(change -> {
769 sb.append(change.toDebugStatement()).append(", ").append(System.lineSeparator());
770 });
771
772 sb.append("];").append(System.lineSeparator())
773 .append(System.lineSeparator())
774 .append("const newValue = LiveHelper.process(initialValue, initialRevision, changes);")
775 .append(System.lineSeparator())
776 .append("});");
777
778 this.debugData = sb.toString();
779 }
780
781
782
783
784 public void printDebugData() {
785 logger.warn("Debug Data {}", this.debugData);
786 }
787 }