source: trunk/autoquest-core-tasktrees/src/main/java/de/ugoe/cs/autoquest/tasktrees/temporalrelation/TaskTreeNodeMerger.java @ 1127

Last change on this file since 1127 was 1127, checked in by pharms, 11 years ago
  • complete refactoring of task detection
  • many performance improvements in task detection
  • improved merging of sequences using Myers diff algorithm
File size: 23.2 KB
Line 
1//   Copyright 2012 Georg-August-Universität Göttingen, Germany
2//
3//   Licensed under the Apache License, Version 2.0 (the "License");
4//   you may not use this file except in compliance with the License.
5//   You may obtain a copy of the License at
6//
7//       http://www.apache.org/licenses/LICENSE-2.0
8//
9//   Unless required by applicable law or agreed to in writing, software
10//   distributed under the License is distributed on an "AS IS" BASIS,
11//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//   See the License for the specific language governing permissions and
13//   limitations under the License.
14
15package de.ugoe.cs.autoquest.tasktrees.temporalrelation;
16
17import java.util.List;
18
19import de.ugoe.cs.autoquest.tasktrees.treeifc.IIteration;
20import de.ugoe.cs.autoquest.tasktrees.treeifc.IOptional;
21import de.ugoe.cs.autoquest.tasktrees.treeifc.ISelection;
22import de.ugoe.cs.autoquest.tasktrees.treeifc.ISequence;
23import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskTreeBuilder;
24import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskTreeNode;
25import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskTreeNodeFactory;
26import difflib.ChangeDelta;
27import difflib.Chunk;
28import difflib.DeleteDelta;
29import difflib.Delta;
30import difflib.InsertDelta;
31import difflib.Patch;
32import difflib.myers.DiffNode;
33import difflib.myers.MyersDiff;
34import difflib.myers.PathNode;
35import difflib.myers.Snake;
36
37/**
38 * <p>
39 * TODO comment
40 * </p>
41 *
42 * @author Patrick Harms
43 */
44class TaskTreeNodeMerger {
45   
46    /** */
47    private ITaskTreeNodeFactory taskTreeNodeFactory;
48   
49    /** */
50    private ITaskTreeBuilder taskTreeBuilder;
51   
52    /** */
53    private TaskTreeNodeComparator nodeComparator;
54   
55    /**
56     * <p>
57     * TODO: comment
58     * </p>
59     *
60     * @param taskTreeNodeFactory     the node factory to be used if nodes must be instantiated
61     * @param taskTreeBuilder         the task tree builder to be used if nodes must be merged
62     * @param nodeEqualityRuleManager the node equality rule manager that may be needed for
63     *                                comparing nodes during merge
64     * @param consideredNodeEquality  the node equality considered when comparing nodes to
65     *                                identify them as equal
66     */
67    TaskTreeNodeMerger(ITaskTreeNodeFactory   taskTreeNodeFactory,
68                       ITaskTreeBuilder       taskTreeBuilder,
69                       TaskTreeNodeComparator nodeComparator)
70    {
71        super();
72        this.taskTreeNodeFactory = taskTreeNodeFactory;
73        this.taskTreeBuilder = taskTreeBuilder;
74        this.nodeComparator = nodeComparator;
75    }
76
77    /**
78     * <p>
79     * this method merges task tree nodes in a list, if they can be merged. For this, it tries
80     * to merge every node with every other node in the provided list using the
81     * {@link #mergeTaskNodes(ITaskTreeNode, ITaskTreeNode)} method. If a merge is possible, it
82     * removes the merged nodes from the list and adds the merge result.
83     * </p>
84     *
85     * @param nodes the list of nodes to be merged
86     */
87    void mergeTaskNodes(List<ITaskTreeNode> nodes) {
88        int index1 = 0;
89        int index2 = 0;
90        ITaskTreeNode variant1;
91        ITaskTreeNode variant2;
92       
93        while (index1 < nodes.size()) {
94            variant1 = nodes.get(index1);
95            index2 = index1 + 1;
96           
97            while (index2 < nodes.size()) {
98                variant2 = nodes.get(index2);
99                ITaskTreeNode mergedChild = mergeTaskNodes(variant1, variant2);
100               
101                if (mergedChild != null) {
102                    // if we merged something start from the beginning to perform the next merge
103                    nodes.remove(index2);
104                    nodes.remove(index1);
105                    nodes.add(index1, mergedChild);
106                    index1 = -1;
107                    break;
108                }
109                else {
110                    index2++;
111                }
112            }
113           
114            index1++;
115        }
116    }
117
118    /**
119     * <p>
120     * this method merges two tasks with each other if possible. If the tasks are lexically
121     * equal, the first of them is returned as merge result. If both tasks are of the same
122     * temporal relationship type, the appropriate merge method is called to merge them. If one
123     * of the nodes is a selection, the other one is added as a variant of this selection.
124     * (However, if both nodes are selections, they are merged using the appropriate merge method.)
125     * If merging is not possible, then a selection of both provided nodes is created and
126     * returned as merge result.
127     * </p>
128     *
129     * @param node1 the first task to be merged
130     * @param node2 the second task to be merged
131     *
132     * @return the result of the merge
133     */
134    ITaskTreeNode mergeTaskNodes(ITaskTreeNode node1, ITaskTreeNode node2) {
135        ITaskTreeNode mergeResult = null;
136       
137        // both are of same parent type
138        if ((node1 instanceof ISequence) && (node2 instanceof ISequence)) {
139            mergeResult = mergeSequences((ISequence) node1, (ISequence) node2);
140        }
141        else if ((node1 instanceof ISelection) && (node2 instanceof ISelection)) {
142            mergeResult = mergeSelections((ISelection) node1, (ISelection) node2);
143        }
144        else if ((node1 instanceof IIteration) && (node2 instanceof IIteration)) {
145            mergeResult = mergeIterations((IIteration) node1, (IIteration) node2);
146        }
147        else if ((node1 instanceof IOptional) && (node2 instanceof IOptional)) {
148            mergeResult = mergeOptionals((IOptional) node1, (IOptional) node2);
149        }
150        // one is an iteration
151        else if (node1 instanceof IIteration) {
152            mergeResult = mergeTaskNodes(((IIteration) node1).getChildren().get(0), node2);
153           
154            if (mergeResult != null) {
155                taskTreeBuilder.setChild((IIteration) node1, mergeResult);
156                mergeResult = node1;
157            }
158        }
159        else if (node2 instanceof IIteration) {
160            mergeResult = mergeTaskNodes(((IIteration) node2).getChildren().get(0), node1);
161           
162            if (mergeResult != null) {
163                taskTreeBuilder.setChild((IIteration) node2, mergeResult);
164                mergeResult = node2;
165            }
166        }
167        // one is an optional
168        else if (node1 instanceof IOptional) {
169            mergeResult = mergeTaskNodes(((IOptional) node1).getChildren().get(0), node2);
170           
171            if (mergeResult != null) {
172                taskTreeBuilder.setChild((IOptional) node1, mergeResult);
173                mergeResult = node1;
174            }
175        }
176        else if (node2 instanceof IOptional) {
177            mergeResult = mergeTaskNodes(((IOptional) node2).getChildren().get(0), node1);
178           
179            if (mergeResult != null) {
180                taskTreeBuilder.setChild((IOptional) node2, mergeResult);
181                mergeResult = node2;
182            }
183        }
184        // one is a selection
185        else if (node1 instanceof ISelection) {
186            ISelection selection2 = taskTreeNodeFactory.createNewSelection();
187            taskTreeBuilder.addChild(selection2, node2);
188            mergeResult = mergeSelections((ISelection) node1, selection2);
189        }
190        else if (node2 instanceof ISelection) {
191            ISelection selection1 = taskTreeNodeFactory.createNewSelection();
192            taskTreeBuilder.addChild(selection1, node1);
193            mergeResult = mergeSelections(selection1, (ISelection) node2);
194        }
195        // one is a sequence
196        else if (node1 instanceof ISequence) {
197            ISequence sequence2 = taskTreeNodeFactory.createNewSequence();
198            taskTreeBuilder.addChild(sequence2, node2);
199            mergeResult = mergeSequences((ISequence) node1, sequence2);
200        }
201        else if (node2 instanceof ISequence) {
202            ISequence sequence1 = taskTreeNodeFactory.createNewSequence();
203            taskTreeBuilder.addChild(sequence1, node1);
204            mergeResult = mergeSequences(sequence1, (ISequence) node2);
205        }
206        // both are event tasks
207        else {
208            // only drop nodes which are definitely lexically equal
209            if (nodeComparator.areLexicallyEqual(node1, node2)) {
210                mergeResult = node1;
211            }
212        }
213
214        if (mergeResult == null) {
215            mergeResult = taskTreeNodeFactory.createNewSelection();
216            taskTreeBuilder.setDescription(mergeResult, "variants of " + node1);
217            taskTreeBuilder.addChild((ISelection) mergeResult, node1);
218            taskTreeBuilder.addChild((ISelection) mergeResult, node2);
219        }
220       
221        return mergeResult;
222    }
223
224    /**
225     * <p>
226     * merges equal sequences. This is done through performing a diff between the two sequences.
227     * For each identified difference appropriate substructures are created. For equal parts,
228     * the appropriate equal nodes are merged using
229     * {@link #mergeTaskNodes(ITaskTreeNode, ITaskTreeNode)} and the result is added to the
230     * overall merge result.
231     * </p>
232     *
233     * @param sequence1 the first sequence to be merged
234     * @param sequence2 the second sequence to be merged
235     *
236     * @return the result of the merge or null if merging was not possible
237     */
238    ITaskTreeNode mergeSequences(ISequence sequence1, ISequence sequence2) {
239        ITaskTreeNode mergeResult = taskTreeNodeFactory.createNewSequence();
240       
241        List<ITaskTreeNode> children1 = sequence1.getChildren();
242        List<ITaskTreeNode> children2 = sequence2.getChildren();
243       
244        Patch patch = new TaskTreeNodeSequenceMyersDiff().getDiff(children1, children2);
245       
246        int index1 = 0;
247        int index2 = 0;
248       
249        while ((index1 < children1.size()) || (index2 < children2.size())) {
250            boolean foundDelta = false;
251            for (Delta delta : patch.getDeltas()) {
252                if ((delta.getOriginal().getPosition() == index1) &&
253                    (delta.getRevised().getPosition() == index2))
254                {
255                    if (delta.getType() == Delta.TYPE.DELETE) {
256                        ITaskTreeNode option = getSubTree(children1, delta.getOriginal());
257                        index1 += delta.getOriginal().size();
258                       
259                        IOptional optional = taskTreeNodeFactory.createNewOptional();
260                        taskTreeBuilder.setChild(optional, option);
261                        taskTreeBuilder.addChild((ISequence) mergeResult, optional);
262                    }
263                    else if (delta.getType() == Delta.TYPE.INSERT) {
264                        ITaskTreeNode option = getSubTree(children2, delta.getRevised());
265                        index2 += delta.getRevised().size();
266                       
267                        IOptional optional = taskTreeNodeFactory.createNewOptional();
268                        taskTreeBuilder.setChild(optional, option);
269                        taskTreeBuilder.addChild((ISequence) mergeResult, optional);
270                    }
271                    else {
272                        ITaskTreeNode variant1 = getSubTree(children1, delta.getOriginal());
273                        index1 += delta.getOriginal().size();
274                       
275                        ITaskTreeNode variant2 = getSubTree(children2, delta.getRevised());
276                        index2 += delta.getRevised().size();
277                       
278                        ISelection selection = taskTreeNodeFactory.createNewSelection();
279                        taskTreeBuilder.addChild(selection, variant1);
280                        taskTreeBuilder.addChild(selection, variant2);
281                        taskTreeBuilder.addChild((ISequence) mergeResult, selection);
282                    }
283                   
284                    foundDelta = true;
285                }
286            }
287               
288            if (!foundDelta) {
289                ITaskTreeNode mergedNode =
290                    mergeTaskNodes(children1.get(index1++), children2.get(index2++));
291                taskTreeBuilder.addChild((ISequence) mergeResult, mergedNode);
292            }
293        }
294       
295        List<ITaskTreeNode> mergeResultChildren = mergeResult.getChildren();
296        if ((mergeResultChildren != null) && (mergeResultChildren.size() == 1) &&
297            (mergeResultChildren.get(0) instanceof ISelection))
298        {
299            mergeResult = mergeResultChildren.get(0);
300        }
301       
302        taskTreeBuilder.setDescription(mergeResult, sequence1.getDescription());
303       
304        return mergeResult;
305    }
306
307    /**
308     * <p>
309     * merges equal selections. This is done by adding those children of the second selection to
310     * the first selection that can not be merged with any of the children of the first selection.
311     * If a merge is possible and this merge is not a simple selection of the merged children,
312     * then the merged child replaces the child of the first selection which was merged.
313     * </p>
314     *
315     * @param selection1 the first selection to be merged
316     * @param selection2 the second selection to be merged
317     *
318     * @return the result of the merge which is never null
319     */
320    ITaskTreeNode mergeSelections(ISelection selection1, ISelection selection2) {
321        ISelection mergeResult = selection1;
322       
323        ITaskTreeNode childToMerge = null;
324        ITaskTreeNode mergedChild = null;
325       
326        List<ITaskTreeNode> children1 = selection1.getChildren();
327        List<ITaskTreeNode> children2 = selection2.getChildren();
328       
329        // check for each child of selection 2 if it is a duplicate of one of the children
330        // if selection 1. If not, add it as further child to the merge result, else skip it.
331        for (int i = 0; i < children2.size(); i++) {
332            childToMerge = children2.get(i);
333            for (int j = 0; j < children1.size(); j++) {
334                mergedChild = mergeTaskNodes(children1.get(j), childToMerge);
335               
336                // a merge must not be a selection, except it is one of the children. Otherwise
337                // no real merge was done.
338                if ((mergedChild != null) &&
339                    ((!(mergedChild instanceof ISelection)) ||
340                     (children1.get(j) == mergedChild) || (childToMerge == mergedChild)))
341                {
342                    // we found a real merge. So replace the original child in selection 1 with
343                    // the merged child
344                    taskTreeBuilder.removeChild(selection1, children1.get(j));
345                    taskTreeBuilder.addChild(selection1, mergedChild);
346                    mergedChild = null;
347                    childToMerge = null;
348                    break;
349                }
350            }
351           
352            if (childToMerge != null) {
353                taskTreeBuilder.addChild(selection1, childToMerge);
354            }
355        }
356       
357        return mergeResult;
358    }
359
360    /**
361     * <p>
362     * merges equal iterations. This is done through merging the children of both iterations. If
363     * this is possible, a resulting iteration with the merge result of the children as its own
364     * child is returned. Otherwise null is returned to indicate that merging was not possible.
365     * </p>
366     *
367     * @param iteration1 the first iteration to be merged
368     * @param iteration2 the second iteration to be merged
369     *
370     * @return the result of the merge or null if merging is not possible
371     */
372    ITaskTreeNode mergeIterations(IIteration iteration1, IIteration iteration2) {
373        ITaskTreeNode mergedChild = mergeTaskNodes
374            (iteration1.getChildren().get(0), iteration2.getChildren().get(0));
375       
376        IIteration mergeResult = null;
377       
378        if (mergedChild != null) {
379            mergeResult = iteration1;
380            taskTreeBuilder.setChild(mergeResult, mergedChild);
381        }
382       
383        return mergeResult;
384    }
385
386    /**
387     * <p>
388     * merges equal optionals. This is done through merging the children of both optionals. If
389     * this is possible, a resulting optional with the merge result of the children as its own
390     * child is returned. Otherwise null is returned to indicate that merging was not possible.
391     * </p>
392     *
393     * @param optional1 the first optional to be merged
394     * @param optional2 the second optional to be merged
395     *
396     * @return the result of the merge or null if merging is not possible
397     */
398    ITaskTreeNode mergeOptionals(IOptional optional1, IOptional optional2) {
399        ITaskTreeNode mergedChild = mergeTaskNodes
400            (optional1.getChildren().get(0), optional2.getChildren().get(0));
401       
402        IOptional mergeResult = null;
403       
404        if (mergedChild != null) {
405            mergeResult = optional1;
406            taskTreeBuilder.setChild(mergeResult, mergedChild);
407        }
408       
409        return mergeResult;
410    }
411
412    /**
413     * <p>
414     * determines a task tree node defined by the provided chunk. This is either a single node if
415     * the chunck denotes a single child, or it is a sequence of the denoted children.
416     * </p>
417     */
418    private ITaskTreeNode getSubTree(List<ITaskTreeNode> children1, Chunk chunk) {
419        ITaskTreeNode part;
420       
421        if (chunk.size() == 1) {
422            part = children1.get(chunk.getPosition());
423        }
424        else {
425            part = taskTreeNodeFactory.createNewSequence();
426       
427            for (int i = 0; i < chunk.size(); i++) {
428                taskTreeBuilder.addChild
429                    ((ISequence) part, children1.get(chunk.getPosition() + i));
430            }
431        }
432       
433        return part;
434    }
435
436    /**
437     *
438     */
439    private class TaskTreeNodeSequenceMyersDiff extends MyersDiff {
440       
441        /**
442         *
443         */
444        private Patch getDiff(List<ITaskTreeNode> variant1, List<ITaskTreeNode> variant2) {
445            PathNode path = buildPath(variant1, variant2);
446            return buildRevision(path, variant1, variant2);
447        }
448       
449        /**
450         * overwrites the default implementation just to change the tree node comparison.
451         * This is an extended version of the original implementation respecting the appropriate
452         * copyrights. Please see the copyrights of the implementers of the base class for more
453         * information
454         */
455        private PathNode buildPath(List<ITaskTreeNode> variant1, List<ITaskTreeNode> variant2) {
456            if (variant1 == null) {
457                throw new IllegalArgumentException("variant1 is null");
458            }
459           
460            if (variant2 == null) {
461                throw new IllegalArgumentException("variant2 is null");
462            }
463           
464            // these are local constants
465            final int N = variant1.size();
466            final int M = variant2.size();
467           
468            final int MAX = N + M + 1;
469            final int size = 1 + 2 * MAX;
470            final int middle = size / 2;
471            final PathNode diagonal[] = new PathNode[size];
472           
473            diagonal[middle + 1] = new Snake(0, -1, null);
474           
475            for (int d = 0; d < MAX; d++) {
476                for (int k = -d; k <= d; k += 2) {
477                    final int kmiddle = middle + k;
478                    final int kplus = kmiddle + 1;
479                    final int kminus = kmiddle - 1;
480                    PathNode prev = null;
481                   
482                    int i;
483                    if ((k == -d) || ((k != d) && (diagonal[kminus].i < diagonal[kplus].i))) {
484                        i = diagonal[kplus].i;
485                        prev = diagonal[kplus];
486                    }
487                    else {
488                        i = diagonal[kminus].i + 1;
489                        prev = diagonal[kminus];
490                    }
491                   
492                    diagonal[kminus] = null; // no longer used
493                   
494                    int j = i - k;
495                   
496                    PathNode node = new DiffNode(i, j, prev);
497                   
498                    // orig and rev are zero-based
499                    // but the algorithm is one-based
500                    // that's why there's no +1 when indexing the sequences
501                    while ((i < N) && (j < M) &&
502                           nodeComparator.equals(variant1.get(i), variant2.get(j)))
503                    {
504                        i++;
505                        j++;
506                    }
507                   
508                    if (i > node.i) {
509                        node = new Snake(i, j, node);
510                    }
511                   
512                    diagonal[kmiddle] = node;
513                   
514                    if ((i >= N) && (j >= M)) {
515                        return diagonal[kmiddle];
516                    }
517                }
518                diagonal[middle + d - 1] = null;
519               
520            }
521           
522            // According to Myers, this cannot happen
523            throw new RuntimeException("could not find a diff path");
524        }
525
526        /**
527         * overwrites the default implementation just to change the tree node comparison.
528         * This is an extended version of the original implementation respecting the appropriate
529         * copyrights. Please see the copyrights of the implementers of the base class for more
530         * information
531         */
532        private Patch buildRevision(PathNode            path,
533                                    List<ITaskTreeNode> variant1,
534                                    List<ITaskTreeNode> variant2)
535        {
536            if (path == null) {
537                throw new IllegalArgumentException("path is null");
538            }
539           
540            if (variant1 == null) {
541                throw new IllegalArgumentException("variant1 is null");
542            }
543           
544            if (variant2 == null) {
545                throw new IllegalArgumentException("variant2 is null");
546            }
547           
548            Patch patch = new Patch();
549            if (path.isSnake()) {
550                path = path.prev;
551            }
552           
553            while ((path != null) && (path.prev != null) && (path.prev.j >= 0)) {
554                if (path.isSnake()) {
555                    throw new IllegalStateException
556                        ("bad diffpath: found snake when looking for diff");
557                }
558               
559                int i = path.i;
560                int j = path.j;
561               
562                path = path.prev;
563                int ianchor = path.i;
564                int janchor = path.j;
565               
566                Chunk original = new Chunk(ianchor, variant1.subList(ianchor, i));
567                Chunk revised = new Chunk(janchor, variant2.subList(janchor, j));
568                Delta delta = null;
569               
570                if ((original.size() == 0) && (revised.size() != 0)) {
571                    delta = new InsertDelta(original, revised);
572                }
573                else if ((original.size() > 0) && (revised.size() == 0)) {
574                    delta = new DeleteDelta(original, revised);
575                }
576                else {
577                    delta = new ChangeDelta(original, revised);
578                }
579               
580                patch.addDelta(delta);
581               
582                if (path.isSnake()) {
583                    path = path.prev;
584                }
585            }
586            return patch;
587        }
588       
589    }
590}
Note: See TracBrowser for help on using the repository browser.