Merge "Bug: 627 Added RESTConf API Explorer that dynamically generates API documentat...
[controller.git] / opendaylight / md-sal / sal-dom-broker / src / main / java / org / opendaylight / controller / md / sal / dom / store / impl / ResolveDataChangeEventsTask.java
1 /*
2  * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.controller.md.sal.dom.store.impl;
9
10 import static org.opendaylight.controller.md.sal.dom.store.impl.DOMImmutableDataChangeEvent.builder;
11 import static org.opendaylight.controller.md.sal.dom.store.impl.StoreUtils.append;
12
13 import java.util.Collection;
14 import java.util.Collections;
15 import java.util.HashSet;
16 import java.util.LinkedList;
17 import java.util.List;
18 import java.util.Map.Entry;
19 import java.util.Set;
20 import java.util.concurrent.Callable;
21
22 import org.opendaylight.controller.md.sal.common.api.data.AsyncDataBroker.DataChangeScope;
23 import org.opendaylight.controller.md.sal.dom.store.impl.DOMImmutableDataChangeEvent.Builder;
24 import org.opendaylight.controller.md.sal.dom.store.impl.DOMImmutableDataChangeEvent.SimpleEventFactory;
25 import org.opendaylight.controller.md.sal.dom.store.impl.tree.ListenerTree;
26 import org.opendaylight.controller.md.sal.dom.store.impl.tree.ListenerTree.Node;
27 import org.opendaylight.controller.md.sal.dom.store.impl.tree.ListenerTree.Walker;
28 import org.opendaylight.controller.md.sal.dom.store.impl.tree.NodeModification;
29 import org.opendaylight.controller.md.sal.dom.store.impl.tree.StoreMetadataNode;
30 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier;
31 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.NodeIdentifier;
32 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.NodeIdentifierWithPredicates;
33 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.NodeWithValue;
34 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.PathArgument;
35 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
36 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 import com.google.common.base.Optional;
41 import com.google.common.collect.HashMultimap;
42 import com.google.common.collect.ImmutableList;
43 import com.google.common.collect.Iterables;
44 import com.google.common.collect.Multimap;
45
46 /**
47  *
48  * Resolve Data Change Events based on modifications and listeners
49  *
50  * Computes data change events for all affected registered listeners in data
51  * tree.
52  *
53  * Prerequisites for computation is to set all parameters properly:
54  * <ul>
55  * <li>{@link #setRootPath(InstanceIdentifier)} - Root path of datastore
56  * <li>{@link #setListenerRoot(ListenerTree)} - Root of listener registration
57  * tree, which contains listeners to be notified
58  * <li>{@link #setModificationRoot(NodeModification)} - Modification root, for
59  * which events should be computed
60  * <li>{@link #setBeforeRoot(Optional)} - State of before modification occurred
61  * <li>{@link #setAfterRoot(Optional)} - State of after modification occurred
62  * </ul>
63  *
64  */
65 public class ResolveDataChangeEventsTask implements Callable<Iterable<ChangeListenerNotifyTask>> {
66     private static final Logger LOG = LoggerFactory.getLogger(ResolveDataChangeEventsTask.class);
67     private static final DOMImmutableDataChangeEvent NO_CHANGE = builder(DataChangeScope.BASE).build();
68
69     private InstanceIdentifier rootPath;
70     private ListenerTree listenerRoot;
71     private NodeModification modificationRoot;
72     private Optional<StoreMetadataNode> beforeRoot;
73     private Optional<StoreMetadataNode> afterRoot;
74     private final Multimap<ListenerTree.Node, DOMImmutableDataChangeEvent> events = HashMultimap.create();
75
76     protected InstanceIdentifier getRootPath() {
77         return rootPath;
78     }
79
80     protected ResolveDataChangeEventsTask setRootPath(final InstanceIdentifier rootPath) {
81         this.rootPath = rootPath;
82         return this;
83     }
84
85     protected ListenerTree getListenerRoot() {
86         return listenerRoot;
87     }
88
89     protected ResolveDataChangeEventsTask setListenerRoot(final ListenerTree listenerRoot) {
90         this.listenerRoot = listenerRoot;
91         return this;
92     }
93
94     protected NodeModification getModificationRoot() {
95         return modificationRoot;
96     }
97
98     protected ResolveDataChangeEventsTask setModificationRoot(final NodeModification modificationRoot) {
99         this.modificationRoot = modificationRoot;
100         return this;
101     }
102
103     protected Optional<StoreMetadataNode> getBeforeRoot() {
104         return beforeRoot;
105     }
106
107     protected ResolveDataChangeEventsTask setBeforeRoot(final Optional<StoreMetadataNode> beforeRoot) {
108         this.beforeRoot = beforeRoot;
109         return this;
110     }
111
112     protected Optional<StoreMetadataNode> getAfterRoot() {
113         return afterRoot;
114     }
115
116     protected ResolveDataChangeEventsTask setAfterRoot(final Optional<StoreMetadataNode> afterRoot) {
117         this.afterRoot = afterRoot;
118         return this;
119     }
120
121     /**
122      * Resolves and creates Notification Tasks
123      *
124      * Implementation of done as Map-Reduce with two steps: 1. resolving events
125      * and their mapping to listeners 2. merging events affecting same listener
126      *
127      * @return Iterable of Notification Tasks which needs to be executed in
128      *         order to delivery data change events.
129      */
130     @Override
131     public Iterable<ChangeListenerNotifyTask> call() {
132         LOG.trace("Resolving events for {}", modificationRoot);
133
134         try (final Walker w = listenerRoot.getWalker()) {
135             resolveAnyChangeEvent(rootPath, Collections.singleton(w.getRootNode()), modificationRoot, beforeRoot,
136                     afterRoot);
137             return createNotificationTasks();
138         }
139     }
140
141     /**
142      *
143      * Walks map of listeners to data change events, creates notification
144      * delivery tasks.
145      *
146      * Walks map of registered and affected listeners and creates notification
147      * tasks from set of listeners and events to be delivered.
148      *
149      * If set of listeners has more then one event (applicable to wildcarded
150      * listeners), merges all data change events into one, final which contains
151      * all separate updates.
152      *
153      * Dispatch between merge variant and reuse variant of notification task is
154      * done in
155      * {@link #addNotificationTask(com.google.common.collect.ImmutableList.Builder, Node, Collection)}
156      *
157      * @return Collection of notification tasks.
158      */
159     private Collection<ChangeListenerNotifyTask> createNotificationTasks() {
160         ImmutableList.Builder<ChangeListenerNotifyTask> taskListBuilder = ImmutableList.builder();
161         for (Entry<ListenerTree.Node, Collection<DOMImmutableDataChangeEvent>> entry : events.asMap().entrySet()) {
162             addNotificationTask(taskListBuilder, entry.getKey(), entry.getValue());
163         }
164         return taskListBuilder.build();
165     }
166
167     /**
168      * Adds notification task to task list.
169      *
170      * If entry collection contains one event, this event is reused and added to
171      * notification tasks for listeners (see
172      * {@link #addNotificationTaskByScope(com.google.common.collect.ImmutableList.Builder, Node, DOMImmutableDataChangeEvent)}
173      * . Otherwise events are merged by scope and distributed between listeners
174      * to particular scope. See
175      * {@link #addNotificationTasksAndMergeEvents(com.google.common.collect.ImmutableList.Builder, Node, Collection)}
176      * .
177      *
178      * @param taskListBuilder
179      * @param listeners
180      * @param entries
181      */
182     private static void addNotificationTask(final ImmutableList.Builder<ChangeListenerNotifyTask> taskListBuilder,
183             final ListenerTree.Node listeners, final Collection<DOMImmutableDataChangeEvent> entries) {
184
185         if (!entries.isEmpty()) {
186             if (entries.size() == 1) {
187                 addNotificationTaskByScope(taskListBuilder, listeners, Iterables.getOnlyElement(entries));
188             } else {
189                 addNotificationTasksAndMergeEvents(taskListBuilder, listeners, entries);
190             }
191         }
192     }
193
194     /**
195      *
196      * Add notification deliveries task to the listener.
197      *
198      *
199      * @param taskListBuilder
200      * @param listeners
201      * @param event
202      */
203     private static void addNotificationTaskByScope(
204             final ImmutableList.Builder<ChangeListenerNotifyTask> taskListBuilder, final ListenerTree.Node listeners,
205             final DOMImmutableDataChangeEvent event) {
206         DataChangeScope eventScope = event.getScope();
207         for (DataChangeListenerRegistration<?> listenerReg : listeners.getListeners()) {
208             DataChangeScope listenerScope = listenerReg.getScope();
209             List<DataChangeListenerRegistration<?>> listenerSet = Collections
210                     .<DataChangeListenerRegistration<?>> singletonList(listenerReg);
211             if (eventScope == DataChangeScope.BASE) {
212                 taskListBuilder.add(new ChangeListenerNotifyTask(listenerSet, event));
213             } else if (eventScope == DataChangeScope.ONE && listenerScope != DataChangeScope.BASE) {
214                 taskListBuilder.add(new ChangeListenerNotifyTask(listenerSet, event));
215             } else if (eventScope == DataChangeScope.SUBTREE && listenerScope == DataChangeScope.SUBTREE) {
216                 taskListBuilder.add(new ChangeListenerNotifyTask(listenerSet, event));
217             }
218         }
219     }
220
221     /**
222      *
223      * Add notification tasks with merged event
224      *
225      * Separate Events by scope and creates merged notification tasks for each
226      * and every scope which is present.
227      *
228      * Adds merged events to task list based on scope requested by client.
229      *
230      * @param taskListBuilder
231      * @param listeners
232      * @param entries
233      */
234     private static void addNotificationTasksAndMergeEvents(
235             final ImmutableList.Builder<ChangeListenerNotifyTask> taskListBuilder, final ListenerTree.Node listeners,
236             final Collection<DOMImmutableDataChangeEvent> entries) {
237
238         final Builder baseBuilder = builder(DataChangeScope.BASE);
239         final Builder oneBuilder = builder(DataChangeScope.ONE);
240         final Builder subtreeBuilder = builder(DataChangeScope.SUBTREE);
241
242         boolean baseModified = false;
243         boolean oneModified = false;
244         boolean subtreeModified = false;
245         for (final DOMImmutableDataChangeEvent entry : entries) {
246             switch (entry.getScope()) {
247             // Absence of breaks is intentional here. Subtree contains base and
248             // one, one also contains base
249             case BASE:
250                 baseBuilder.merge(entry);
251                 baseModified = true;
252             case ONE:
253                 oneBuilder.merge(entry);
254                 oneModified = true;
255             case SUBTREE:
256                 subtreeBuilder.merge(entry);
257                 subtreeModified = true;
258             }
259         }
260
261         if (baseModified) {
262             addNotificationTaskExclusively(taskListBuilder, listeners, baseBuilder.build());
263         }
264         if (oneModified) {
265             addNotificationTaskExclusively(taskListBuilder, listeners, oneBuilder.build());
266         }
267         if (subtreeModified) {
268             addNotificationTaskExclusively(taskListBuilder, listeners, subtreeBuilder.build());
269         }
270     }
271
272     private static void addNotificationTaskExclusively(
273             final ImmutableList.Builder<ChangeListenerNotifyTask> taskListBuilder, final Node listeners,
274             final DOMImmutableDataChangeEvent event) {
275         for (DataChangeListenerRegistration<?> listener : listeners.getListeners()) {
276             if (listener.getScope() == event.getScope()) {
277                 Set<DataChangeListenerRegistration<?>> listenerSet = Collections
278                         .<DataChangeListenerRegistration<?>> singleton(listener);
279                 taskListBuilder.add(new ChangeListenerNotifyTask(listenerSet, event));
280             }
281         }
282     }
283
284     /**
285      * Resolves data change event for supplied node
286      *
287      * @param path
288      *            Path to current node in tree
289      * @param listeners
290      *            Collection of Listener registration nodes interested in
291      *            subtree
292      * @param modification
293      *            Modification of current node
294      * @param before
295      *            - Original (before) state of current node
296      * @param after
297      *            - After state of current node
298      * @return Data Change Event of this node and all it's children
299      */
300     private DOMImmutableDataChangeEvent resolveAnyChangeEvent(final InstanceIdentifier path,
301             final Collection<ListenerTree.Node> listeners, final NodeModification modification,
302             final Optional<StoreMetadataNode> before, final Optional<StoreMetadataNode> after) {
303         // No listeners are present in listener registration subtree
304         // no before and after state is present
305         if (!before.isPresent() && !after.isPresent()) {
306             return NO_CHANGE;
307         }
308         switch (modification.getModificationType()) {
309         case SUBTREE_MODIFIED:
310             return resolveSubtreeChangeEvent(path, listeners, modification, before.get(), after.get());
311         case MERGE:
312         case WRITE:
313             if (before.isPresent()) {
314                 return resolveReplacedEvent(path, listeners, before.get().getData(), after.get().getData());
315             } else {
316                 return resolveCreateEvent(path, listeners, after.get());
317             }
318         case DELETE:
319             return resolveDeleteEvent(path, listeners, before.get());
320         default:
321             return NO_CHANGE;
322         }
323
324     }
325
326     private DOMImmutableDataChangeEvent resolveReplacedEvent(final InstanceIdentifier path,
327             final Collection<Node> listeners, final NormalizedNode<?, ?> beforeData,
328             final NormalizedNode<?, ?> afterData) {
329
330         if (beforeData instanceof NormalizedNodeContainer<?, ?, ?> && !listeners.isEmpty()) {
331             // Node is container (contains child) and we have interested
332             // listeners registered for it, that means we need to do
333             // resolution of changes on children level and can not
334             // shortcut resolution.
335
336             @SuppressWarnings("unchecked")
337             NormalizedNodeContainer<?, PathArgument, NormalizedNode<PathArgument, ?>> beforeCont = (NormalizedNodeContainer<?, PathArgument, NormalizedNode<PathArgument, ?>>) beforeData;
338             @SuppressWarnings("unchecked")
339             NormalizedNodeContainer<?, PathArgument, NormalizedNode<PathArgument, ?>> afterCont = (NormalizedNodeContainer<?, PathArgument, NormalizedNode<PathArgument, ?>>) afterData;
340             return resolveNodeContainerReplaced(path, listeners, beforeCont, afterCont);
341         } else if (!beforeData.equals(afterData)) {
342             // Node is either of Leaf type (does not contain child nodes)
343             // or we do not have listeners, so normal equals method is
344             // sufficient for determining change.
345
346             DOMImmutableDataChangeEvent event = builder(DataChangeScope.BASE).setBefore(beforeData).setAfter(afterData)
347                     .addUpdated(path, beforeData, afterData).build();
348             addPartialTask(listeners, event);
349             return event;
350         } else {
351             return NO_CHANGE;
352         }
353     }
354
355     private DOMImmutableDataChangeEvent resolveNodeContainerReplaced(final InstanceIdentifier path,
356             final Collection<Node> listeners,
357             final NormalizedNodeContainer<?, PathArgument, NormalizedNode<PathArgument, ?>> beforeCont,
358             final NormalizedNodeContainer<?, PathArgument, NormalizedNode<PathArgument, ?>> afterCont) {
359         final Set<PathArgument> alreadyProcessed = new HashSet<>();
360         final List<DOMImmutableDataChangeEvent> childChanges = new LinkedList<>();
361
362         DataChangeScope potentialScope = DataChangeScope.BASE;
363         // We look at all children from before and compare it with after state.
364         for (NormalizedNode<PathArgument, ?> beforeChild : beforeCont.getValue()) {
365             PathArgument childId = beforeChild.getIdentifier();
366             alreadyProcessed.add(childId);
367             InstanceIdentifier childPath = append(path, childId);
368             Collection<ListenerTree.Node> childListeners = getListenerChildrenWildcarded(listeners, childId);
369             Optional<NormalizedNode<PathArgument, ?>> afterChild = afterCont.getChild(childId);
370             DOMImmutableDataChangeEvent childChange = resolveNodeContainerChildUpdated(childPath, childListeners,
371                     beforeChild, afterChild);
372             // If change is empty (equals to NO_CHANGE)
373             if (childChange != NO_CHANGE) {
374                 childChanges.add(childChange);
375             }
376
377         }
378
379         for (NormalizedNode<PathArgument, ?> afterChild : afterCont.getValue()) {
380             PathArgument childId = afterChild.getIdentifier();
381             if (!alreadyProcessed.contains(childId)) {
382                 // We did not processed that child already
383                 // and it was not present in previous loop, that means it is
384                 // created.
385                 Collection<ListenerTree.Node> childListeners = getListenerChildrenWildcarded(listeners, childId);
386                 InstanceIdentifier childPath = append(path,childId);
387                 childChanges.add(resolveSameEventRecursivelly(childPath , childListeners, afterChild,
388                         DOMImmutableDataChangeEvent.getCreateEventFactory()));
389             }
390         }
391         if (childChanges.isEmpty()) {
392             return NO_CHANGE;
393         }
394
395         Builder eventBuilder = builder(potentialScope) //
396                 .setBefore(beforeCont) //
397                 .setAfter(afterCont)
398                 .addUpdated(path, beforeCont, afterCont);
399         for (DOMImmutableDataChangeEvent childChange : childChanges) {
400             eventBuilder.merge(childChange);
401         }
402
403         DOMImmutableDataChangeEvent replaceEvent = eventBuilder.build();
404         addPartialTask(listeners, replaceEvent);
405         return replaceEvent;
406     }
407
408     private DOMImmutableDataChangeEvent resolveNodeContainerChildUpdated(final InstanceIdentifier path,
409             final Collection<Node> listeners, final NormalizedNode<PathArgument, ?> before,
410             final Optional<NormalizedNode<PathArgument, ?>> after) {
411
412         if (after.isPresent()) {
413             // REPLACE or SUBTREE Modified
414             return resolveReplacedEvent(path, listeners, before, after.get());
415
416         } else {
417             // AFTER state is not present - child was deleted.
418             return resolveSameEventRecursivelly(path, listeners, before,
419                     DOMImmutableDataChangeEvent.getRemoveEventFactory());
420         }
421     }
422
423     /**
424      * Resolves create events deep down the interest listener tree.
425      *
426      *
427      * @param path
428      * @param listeners
429      * @param afterState
430      * @return
431      */
432     private DOMImmutableDataChangeEvent resolveCreateEvent(final InstanceIdentifier path,
433             final Collection<ListenerTree.Node> listeners, final StoreMetadataNode afterState) {
434         @SuppressWarnings({ "unchecked", "rawtypes" })
435         final NormalizedNode<PathArgument, ?> node = (NormalizedNode) afterState.getData();
436         return resolveSameEventRecursivelly(path, listeners, node, DOMImmutableDataChangeEvent.getCreateEventFactory());
437     }
438
439     private DOMImmutableDataChangeEvent resolveDeleteEvent(final InstanceIdentifier path,
440             final Collection<ListenerTree.Node> listeners, final StoreMetadataNode beforeState) {
441
442         @SuppressWarnings({ "unchecked", "rawtypes" })
443         final NormalizedNode<PathArgument, ?> node = (NormalizedNode) beforeState.getData();
444         return resolveSameEventRecursivelly(path, listeners, node, DOMImmutableDataChangeEvent.getRemoveEventFactory());
445     }
446
447     private DOMImmutableDataChangeEvent resolveSameEventRecursivelly(final InstanceIdentifier path,
448             final Collection<Node> listeners, final NormalizedNode<PathArgument, ?> node,
449             final SimpleEventFactory eventFactory) {
450
451         final DOMImmutableDataChangeEvent event = eventFactory.create(path, node);
452         DOMImmutableDataChangeEvent propagateEvent = event;
453             // We have listeners for this node or it's children, so we will try
454             // to do additional processing
455         if (node instanceof NormalizedNodeContainer<?, ?, ?>) {
456             Builder eventBuilder = builder(DataChangeScope.BASE);
457             eventBuilder.merge(event);
458             eventBuilder.setBefore(event.getOriginalSubtree());
459             eventBuilder.setAfter(event.getUpdatedSubtree());
460
461             // Node has children, so we will try to resolve it's children
462             // changes.
463             @SuppressWarnings("unchecked")
464             NormalizedNodeContainer<?, PathArgument, NormalizedNode<PathArgument, ?>> container = (NormalizedNodeContainer<?, PathArgument, NormalizedNode<PathArgument, ?>>) node;
465             for (NormalizedNode<PathArgument, ?> child : container.getValue()) {
466                 PathArgument childId = child.getIdentifier();
467                 Collection<Node> childListeners = getListenerChildrenWildcarded(listeners, childId);
468                 eventBuilder.merge(resolveSameEventRecursivelly(append(path, childId), childListeners, child, eventFactory));
469             }
470             propagateEvent = eventBuilder.build();
471         } else {
472             // We do not dispatch leaf events since Binding Aware components do not support them.
473             propagateEvent = builder(DataChangeScope.BASE).build();
474         }
475         if (!listeners.isEmpty()) {
476             addPartialTask(listeners, event);
477         }
478         return propagateEvent;
479     }
480
481     private DOMImmutableDataChangeEvent resolveSubtreeChangeEvent(final InstanceIdentifier path,
482             final Collection<ListenerTree.Node> listeners, final NodeModification modification,
483             final StoreMetadataNode before, final StoreMetadataNode after) {
484
485         Builder one = builder(DataChangeScope.ONE).setBefore(before.getData()).setAfter(after.getData());
486
487         Builder subtree = builder(DataChangeScope.SUBTREE).setBefore(before.getData()).setAfter(after.getData());
488
489         for (NodeModification childMod : modification.getModifications()) {
490             PathArgument childId = childMod.getIdentifier();
491             InstanceIdentifier childPath = append(path, childId);
492             Collection<ListenerTree.Node> childListeners = getListenerChildrenWildcarded(listeners, childId);
493
494             Optional<StoreMetadataNode> childBefore = before.getChild(childId);
495             Optional<StoreMetadataNode> childAfter = after.getChild(childId);
496
497             switch (childMod.getModificationType()) {
498             case WRITE:
499             case MERGE:
500             case DELETE:
501                 one.merge(resolveAnyChangeEvent(childPath, childListeners, childMod, childBefore, childAfter));
502                 break;
503             case SUBTREE_MODIFIED:
504                 subtree.merge(resolveSubtreeChangeEvent(childPath, childListeners, childMod, childBefore.get(),
505                         childAfter.get()));
506                 break;
507             case UNMODIFIED:
508                 // no-op
509                 break;
510             }
511         }
512         DOMImmutableDataChangeEvent oneChangeEvent = one.build();
513         subtree.merge(oneChangeEvent);
514         DOMImmutableDataChangeEvent subtreeEvent = subtree.build();
515         if (!listeners.isEmpty()) {
516             addPartialTask(listeners, oneChangeEvent);
517             addPartialTask(listeners, subtreeEvent);
518         }
519         return subtreeEvent;
520     }
521
522     private DOMImmutableDataChangeEvent addPartialTask(final Collection<ListenerTree.Node> listeners,
523             final DOMImmutableDataChangeEvent event) {
524
525         for (ListenerTree.Node listenerNode : listeners) {
526             if (!listenerNode.getListeners().isEmpty()) {
527                 events.put(listenerNode, event);
528             }
529         }
530         return event;
531     }
532
533     private static Collection<ListenerTree.Node> getListenerChildrenWildcarded(final Collection<ListenerTree.Node> parentNodes,
534             final PathArgument child) {
535         if (parentNodes.isEmpty()) {
536             return Collections.emptyList();
537         }
538         com.google.common.collect.ImmutableList.Builder<ListenerTree.Node> result = ImmutableList.builder();
539         if (child instanceof NodeWithValue || child instanceof NodeIdentifierWithPredicates) {
540             NodeIdentifier wildcardedIdentifier = new NodeIdentifier(child.getNodeType());
541             addChildrenNodesToBuilder(result, parentNodes, wildcardedIdentifier);
542         }
543         addChildrenNodesToBuilder(result, parentNodes, child);
544         return result.build();
545     }
546
547     private static void addChildrenNodesToBuilder(final ImmutableList.Builder<ListenerTree.Node> result,
548             final Collection<ListenerTree.Node> parentNodes, final PathArgument childIdentifier) {
549         for (ListenerTree.Node node : parentNodes) {
550             Optional<ListenerTree.Node> child = node.getChild(childIdentifier);
551             if (child.isPresent()) {
552                 result.add(child.get());
553             }
554         }
555     }
556
557     public static ResolveDataChangeEventsTask create() {
558         return new ResolveDataChangeEventsTask();
559     }
560 }