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