View Javadoc
1   /*
2    * The coLAB project
3    * Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
4    *
5    * Licensed under the MIT License
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   * Set of pending changes.
29   *
30   * @author maxence
31   */
32  public class LiveUpdates implements Serializable {
33  
34      private static final long serialVersionUID = 1L;
35  
36      /** Logger */
37      private static final Logger logger = LoggerFactory.getLogger(LiveUpdates.class);
38  
39      /**
40       * JsonDiscriminator to fetch the class this change targets
41       */
42      private String targetClass;
43  
44      /**
45       * Id of the object this change targets
46       */
47      private Long targetId;
48  
49      /**
50       * initial revision of content
51       */
52      private String revision;
53  
54      /**
55       * root content
56       */
57      private String content;
58  
59      /**
60       * List of pending changes
61       */
62      private List<Change> pendingChanges = new ArrayList<>();
63  
64      /**
65       * Temp debug data
66       */
67      private transient String debugData = null;
68  
69      /**
70       * Get the JSON discriminator
71       *
72       * @return JSON discriminator
73       */
74      public String getTargetClass() {
75          return targetClass;
76      }
77  
78      /**
79       * Set the JSON discriminator
80       *
81       * @param targetClass new discriminator
82       */
83      public void setTargetClass(String targetClass) {
84          this.targetClass = targetClass;
85      }
86  
87      /**
88       * The object id.
89       *
90       * @return the id of the object
91       */
92      public Long getTargetId() {
93          return targetId;
94      }
95  
96      /**
97       * set object id
98       *
99       * @param targetId object id
100      */
101     public void setTargetId(Long targetId) {
102         this.targetId = targetId;
103     }
104 
105     /**
106      * Get the revision
107      *
108      * @return the revision
109      */
110     public String getRevision() {
111         return revision;
112     }
113 
114     /**
115      * Set the revision
116      *
117      * @param revision the revision
118      */
119     public void setRevision(String revision) {
120         this.revision = revision;
121     }
122 
123     /**
124      * Get initial "root" content
125      *
126      * @return the root content
127      */
128     public String getContent() {
129         return content;
130     }
131 
132     /**
133      * Set initial content
134      *
135      * @param content the content
136      */
137     public void setContent(String content) {
138         this.content = content;
139     }
140 
141     /**
142      * Get the list of pending changes
143      *
144      * @return changes
145      */
146     public List<Change> getPendingChanges() {
147         return pendingChanges;
148     }
149 
150     /**
151      * set the list of pending changes
152      *
153      * @param pendingChanges changes
154      */
155     public void setPendingChanges(List<Change> pendingChanges) {
156         this.pendingChanges = pendingChanges;
157     }
158 
159     /**
160      * Get change by revision
161      *
162      * @param changes  all changes
163      * @param revision the revision tag
164      *
165      * @return the change which the given revision or null if such a change does not exist
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      * Get changes which are direct children of given parent
176      *
177      * @param changes all changes
178      * @param basedOn parent id
179      *
180      * @return all changes which are based on the given parentId
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      * Get changes which are direct children of the given parent, authored by the same live-session.
191      * This method is used to detect if a live session has diverged.
192      *
193      * @param changes list of changes
194      * @param parent  parent
195      *
196      * @return children of parent if they are authored by the same person
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      * Include new offset within the map.
210      *
211      * @param offsets offsets mapped by indexes
212      * @param index   new offset index
213      * @param value   new offset value
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 //        logger.trace("  modOffset.second " + offsets);
226 //
227 //        Map<Integer, Integer> modified = new HashMap<>();
228 //
229 //        // shift offsets after current index
230 //        offsets.entrySet().forEach(entry -> {
231 //            Integer key = entry.getKey();
232 //            if (key > index && key < index + value) {
233 //                logger.trace("CONFLIT");
234 //            }
235 //            if (key > index) {
236 //                // move offset to new index
237 //                Integer v = entry.getValue();
238 //                if (v != null) {
239 //                    int newKey = key + value;
240 //                    int newValue = v;
241 //                    if (offsets.containsKey(newKey)) {
242 //                        newValue = offsets.get(newKey) + newValue;
243 //                    }
244 //                    modified.put(key, 0);
245 //                    modified.put(newKey, newValue);
246 //                }
247 //            }
248 //        });
249 //
250 //        logger.trace("  modOffset.third " + modified);
251 //
252 //        // merge shifted offsets
253 //        modified.entrySet().forEach(entry -> {
254 //            Integer key = entry.getKey();
255 //            int current = entry.getValue();
256 //            offsets.put(key, current);
257 //        });
258 //
259 //        logger.trace(" mod Offsets.done " + offsets);
260     }
261 
262     /**
263      * Apply microchange to the buffer
264      *
265      * @param buffer the buffet
266      * @param mu     the patch
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      * Compute offset the set of microchanges will generate.
288      * <li>2:20 means 20 characters are added at index 2
289      * <li>5:-10 means 10 characters are removed from index 5
290      *
291      * @param change set of microchanges
292      *
293      * @return offset mapped by index
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      * Modify microchange to reflect given offsets
313      *
314      * @param change  changes to update
315      * @param offsets offsets, mapped by indexes
316      * @param forward shif if true, unshift otherwise
317      *
318      * @return true if operation was successful, false if there ws some conflict
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         // int way = forward ? 1 : -1; // TODO
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                     // both are insetion
340                     if (mu.getO() >= offsetIndex) {
341                         // changes is after the first insertion => shift it
342                         mu.setO(mu.getO() + offsetValue);
343                     }
344                 } else if (offsetValue < 0 && mu.getT().equals(Type.D)) {
345                     // both are deletion
346                     int deleteFromIndex = offsetIndex + offsetValue;
347                     int deleteToIndex = offsetIndex;
348                     int muEnd = muStart + mu.getL();
349 
350                     // nothing to do if mu is complely before offset
351                     if (muEnd >= deleteFromIndex) {
352                         // mu is completely before offset
353                         // nothing to do
354                         if (muStart > deleteToIndex) {
355                             // mu is completely after offset
356                             // just shift mu
357                             mu.setO(mu.getO() + offsetValue);
358                         } else {
359                             // deletions overlap
360                             if (muStart <= deleteFromIndex && muEnd >= deleteToIndex) {
361                                 // mu wraps offset
362                                 // off          |---|
363                                 // mu       |---------|
364                                 // new mu   |---     -|
365                                 mu.setL(mu.getL() + offsetValue);
366                             } else if (muStart >= deleteFromIndex && muEnd <= deleteToIndex) {
367                                 // offset wraps mu => mus is useless
368                                 // off       |---------|
369                                 // mu          |---|
370                                 // new mu    canceled
371                                 microchanges.remove(i);
372                                 i--;
373                             } else if (muStart <= deleteFromIndex && muEnd <= deleteToIndex) {
374                                 // partial overlap
375                                 // off       |-----|
376                                 // mu     |-----|
377                                 // new mu |--|
378                                 mu.setL(deleteFromIndex - muStart);
379                             } else if (muStart >= deleteFromIndex && muEnd >= deleteToIndex) {
380                                 // partial overlap
381                                 // off    |-----|
382                                 // mu        |-----|
383                                 // new mu       |--| shifted at offsetStart
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                     // offset is deletion, mu is addition
394                     int deleteFromIndex = offsetIndex + offsetValue;
395                     int deleteToIndex = offsetIndex;
396 
397                     if (muStart >= deleteToIndex) {
398                         // off    |-----|
399                         // mu               |+++++|
400                         // just shift to the left
401                         mu.setO(mu.getO() + offsetValue);
402                     } else if (muStart > deleteFromIndex) {
403                         // off         |-----|
404                         // mu            |+|
405                         // mu          |+|
406                         mu.setO(deleteFromIndex);
407 //                    } else {
408                         // mu is before offset
409                         // off         |-----|
410                         // mu     |+|
411                         // nothing to do
412                     }
413                 } else if (offsetValue > 0 && mu.getT().equals(Type.D)) {
414                     // offset is addition, mu is deletion
415                     int muEnd = muStart + mu.getL();
416 
417                     // nothing to do if mu is completely before offset
418                     if (muEnd >= offsetIndex) {
419                         if (muStart > offsetIndex) {
420                             // off   |+|
421                             // mu         |---|
422                             // new mu : shift
423                             mu.setO(mu.getO() + offsetValue);
424                         } else {
425                             // off        |+|
426                             // mu      |--------|
427                             // new v1  |--|  |--|
428                             // split mu to preserve addition
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      * Compute shifted offset by reflecting changes.
451      *
452      * @param offsets original offsets
453      * @param change  change
454      *
455      * @return new map of shifted offsets
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      * Propagate offset to children
483      *
484      * @param parent  starting point
485      * @param offsets offset to propagate
486      *
487      * @return conflict free propagation or not
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                 // should propagate to children which are not based on the offsetsFromRev
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                 // merge has been done
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                 // child.getBasedOn().remove(offsetFromRev);
511             }
512         }
513         return conflictFree;
514     }
515 
516     /**
517      * Get the full set of revision the given change depends on
518      *
519      * @param changes full set of changes
520      * @param change  the change
521      *
522      * @return set of dependencies
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      * Do sets equals?
548      *
549      * @param a first set
550      * @param b second set
551      *
552      * @return true if sets equal
553      */
554     private boolean setsEqual(Set<String> a, Set<String> b) {
555         if (a == null && b == null) {
556             // both null equals
557             return true;
558         } else if (a == null || b == null) {
559             // only one is null
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      * Move a change to a new base.
571      *
572      * @param newBase
573      * @param change
574      * @param offsets
575      *
576      * @return true if rebase has been done without conflict
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                 // exact same set of dependencies: changes are sieblings
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                 // Update parents after rebase/propagation step
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             // [x] -> change -> newBase
608             // ==>[x] -> newBase -> change
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             // nothing to do as all deps are already known
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      * Filter list of change and return only those which match the given live session
640      *
641      * @param changes list of changes
642      * @param author  live-session id
643      *
644      * @return list of changes authored by the given author
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      * Apply all changes.
658      *
659      * @param strict to be implemented: fail when there is some conflicts or not
660      *
661      * @return up-to date content
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             // fetch all changes based on the current revision
684             List<Change> children = getByParent(changes, currentRevision);
685             if (!children.isEmpty()) {
686                 // Map<Integer, Integer> offsets = new HashMap<>();
687                 // logger.trace("new empty offsets " + offsets);
688                 logger.trace("All @{} children: {}", currentRevision, mapChangesRevision(children));
689 
690                 // find a child which depends only only already applied changes
691                 // NB: as I understand the algorithm, I can't figure out a case
692                 // such a child-with-unapplied-parent may event exists...
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                     // clean lists
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                     // logger.trace("Offsets" + offsets);
712                     // rebase others children
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                             // todo throw ?
719                             logger.warn("Conflict");
720                         }
721                         changes.add(0, child);
722                     }
723                     currentRevision = change.getRevision();
724                 } else {
725                     // TODO add full tree JSON formated full tree
726                     logger.error("No child found in {}", children);
727                     printDebugData();
728                     break;
729                 }
730 
731             } else {
732                 // TODO add full tree JSON formated full tree
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      * Build log message that can be easily used to reproduces the process in a test
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      * Print debug message
783      */
784     public void printDebugData() {
785         logger.warn("Debug Data {}", this.debugData);
786     }
787 }