Add clustered-app-config blueprint extension
[controller.git] / opendaylight / blueprint / src / main / java / org / opendaylight / controller / blueprint / ext / DataStoreAppConfigMetadata.java
1 /*
2  * Copyright (c) 2016 Brocade Communications 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.blueprint.ext;
9
10 import com.google.common.base.Optional;
11 import com.google.common.base.Preconditions;
12 import com.google.common.base.Strings;
13 import com.google.common.util.concurrent.CheckedFuture;
14 import com.google.common.util.concurrent.FutureCallback;
15 import com.google.common.util.concurrent.Futures;
16 import java.util.Collection;
17 import java.util.Collections;
18 import java.util.List;
19 import java.util.Objects;
20 import java.util.concurrent.atomic.AtomicBoolean;
21 import javax.annotation.Nonnull;
22 import javax.annotation.Nullable;
23 import org.apache.aries.blueprint.di.AbstractRecipe;
24 import org.apache.aries.blueprint.di.ExecutionContext;
25 import org.apache.aries.blueprint.di.Recipe;
26 import org.apache.aries.blueprint.ext.DependentComponentFactoryMetadata;
27 import org.apache.aries.blueprint.services.ExtendedBlueprintContainer;
28 import org.opendaylight.controller.blueprint.BlueprintContainerRestartService;
29 import org.opendaylight.controller.md.sal.binding.api.ClusteredDataTreeChangeListener;
30 import org.opendaylight.controller.md.sal.binding.api.DataBroker;
31 import org.opendaylight.controller.md.sal.binding.api.DataObjectModification;
32 import org.opendaylight.controller.md.sal.binding.api.DataObjectModification.ModificationType;
33 import org.opendaylight.controller.md.sal.binding.api.DataTreeIdentifier;
34 import org.opendaylight.controller.md.sal.binding.api.DataTreeModification;
35 import org.opendaylight.controller.md.sal.binding.api.ReadOnlyTransaction;
36 import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
37 import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException;
38 import org.opendaylight.controller.sal.core.api.model.SchemaService;
39 import org.opendaylight.yangtools.binding.data.codec.api.BindingNormalizedNodeSerializer;
40 import org.opendaylight.yangtools.concepts.ListenerRegistration;
41 import org.opendaylight.yangtools.yang.binding.DataObject;
42 import org.opendaylight.yangtools.yang.binding.Identifiable;
43 import org.opendaylight.yangtools.yang.binding.Identifier;
44 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
45 import org.opendaylight.yangtools.yang.binding.util.BindingReflections;
46 import org.opendaylight.yangtools.yang.common.QName;
47 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
48 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
49 import org.opendaylight.yangtools.yang.data.impl.codec.xml.XmlUtils;
50 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
51 import org.opendaylight.yangtools.yang.data.impl.schema.transform.dom.parser.DomToNormalizedNodeParserFactory;
52 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
53 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
54 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
55 import org.opendaylight.yangtools.yang.model.api.Module;
56 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
57 import org.osgi.framework.ServiceReference;
58 import org.osgi.service.blueprint.container.ComponentDefinitionException;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61 import org.w3c.dom.Element;
62
63 /**
64  * Factory metadata corresponding to the "clustered-app-config" element that obtains an application's
65  * config data from the data store and provides the binding DataObject instance to the Blueprint container
66  * as a bean. In addition registers a DataTreeChangeListener to restart the Blueprint container when the
67  * config data is changed.
68  *
69  * @author Thomas Pantelis
70  */
71 public class DataStoreAppConfigMetadata implements DependentComponentFactoryMetadata {
72     private static final Logger LOG = LoggerFactory.getLogger(DataStoreAppConfigMetadata.class);
73
74     static final String BINDING_CLASS = "binding-class";
75     static final String DEFAULT_CONFIG = "default-config";
76     static final String LIST_KEY_VALUE = "list-key-value";
77
78     private final String id;
79     private final Element defaultAppConfigElement;
80     private final String appConfigBindingClassName;
81     private final String appConfigListKeyValue;
82     private final AtomicBoolean readingInitialAppConfig = new AtomicBoolean(true);
83     private final AtomicBoolean started = new AtomicBoolean();
84
85     private volatile BindingContext bindingContext;
86     private volatile ExtendedBlueprintContainer container;
87     private volatile StaticServiceReferenceRecipe dataBrokerServiceRecipe;
88     private volatile StaticServiceReferenceRecipe bindingCodecServiceRecipe;
89     private volatile ListenerRegistration<?> appConfigChangeListenerReg;
90     private volatile DataObject currentAppConfig;
91     private volatile SatisfactionCallback satisfactionCallback;
92     private volatile String failureMessage;
93     private volatile String dependendencyDesc;
94
95     // Note: the BindingNormalizedNodeSerializer interface is annotated as deprecated because there's an
96     // equivalent interface in the mdsal project but the corresponding binding classes in the controller
97     // project are still used - conversion to the mdsal binding classes hasn't occurred yet.
98     private volatile BindingNormalizedNodeSerializer bindingSerializer;
99
100     public DataStoreAppConfigMetadata(@Nonnull String id, @Nonnull String appConfigBindingClassName,
101             @Nullable String appConfigListKeyValue, @Nullable Element defaultAppConfigElement) {
102         this.id = Preconditions.checkNotNull(id);
103         this.defaultAppConfigElement = defaultAppConfigElement;
104         this.appConfigBindingClassName = appConfigBindingClassName;
105         this.appConfigListKeyValue = appConfigListKeyValue;
106     }
107
108     @Override
109     public String getId() {
110         return id;
111     }
112
113     @Override
114     public int getActivation() {
115         return ACTIVATION_EAGER;
116     }
117
118     @Override
119     public List<String> getDependsOn() {
120         return Collections.emptyList();
121     }
122
123     @Override
124     public boolean isSatisfied() {
125         return currentAppConfig != null;
126     }
127
128     @Override
129     @SuppressWarnings("unchecked")
130     public void init(ExtendedBlueprintContainer container) {
131         LOG.debug("{}: In init", id);
132
133         this.container = container;
134
135         Class<DataObject> appConfigBindingClass;
136         try {
137             Class<?> bindingClass = container.getBundleContext().getBundle().loadClass(appConfigBindingClassName);
138             if(!DataObject.class.isAssignableFrom(bindingClass)) {
139                 throw new ComponentDefinitionException(String.format(
140                         "%s: Specified app config binding class %s does not extend %s",
141                         id, appConfigBindingClassName, DataObject.class.getName()));
142             }
143
144             appConfigBindingClass = (Class<DataObject>) bindingClass;
145         } catch(ClassNotFoundException e) {
146             throw new ComponentDefinitionException(String.format("%s: Error loading app config binding class %s",
147                     id, appConfigBindingClassName), e);
148         }
149
150         if(Identifiable.class.isAssignableFrom(appConfigBindingClass)) {
151             // The binding class corresponds to a yang list.
152             if(Strings.isNullOrEmpty(appConfigListKeyValue)) {
153                 throw new ComponentDefinitionException(String.format(
154                         "%s: App config binding class %s represents a yang list therefore \"%s\" must be specified",
155                         id, appConfigBindingClassName, LIST_KEY_VALUE));
156             }
157
158             try {
159                 bindingContext = ListBindingContext.newInstance(appConfigBindingClass, appConfigListKeyValue);
160             } catch(Exception e) {
161                 throw new ComponentDefinitionException(String.format(
162                         "%s: Error initializing for app config list binding class %s",
163                         id, appConfigBindingClassName), e);
164             }
165
166         } else {
167             bindingContext = new ContainerBindingContext(appConfigBindingClass);
168         }
169     }
170
171     @Override
172     public Object create() throws ComponentDefinitionException {
173         LOG.debug("{}: In create - currentAppConfig: {}", id, currentAppConfig);
174
175         if(failureMessage != null) {
176             throw new ComponentDefinitionException(failureMessage);
177         }
178
179         // The following code is a bit odd so requires some explanation. A little background... If a bean
180         // is a prototype then the corresponding Recipe create method does not register the bean as created
181         // with the BlueprintRepository and thus the destroy method isn't called on container destroy. We
182         // rely on destroy being called to close our DTCL registration. Unfortunately the default setting
183         // for the prototype flag in AbstractRecipe is true and the DependentComponentFactoryRecipe, which
184         // is created for DependentComponentFactoryMetadata types of which we are one, doesn't have a way for
185         // us to indicate the prototype state via our metadata.
186         //
187         // The ExecutionContext is actually backed by the BlueprintRepository so we access it here to call
188         // the removePartialObject method which removes any partially created instance, which does not apply
189         // in our case, and also has the side effect of registering our bean as created as if it wasn't a
190         // prototype. We also obtain our corresponding Recipe instance and clear the prototype flag. This
191         // doesn't look to be necessary but is done so for completeness. Better late than never. Note we have
192         // to do this here rather than in startTracking b/c the ExecutionContext is not available yet at that
193         // point.
194         //
195         // Now the stopTracking method is called on container destroy but startTracking/stopTracking can also
196         // be called multiple times during the container creation process for Satisfiable recipes as bean
197         // processors may modify the metadata which could affect how dependencies are satisfied. An example of
198         // this is with service references where the OSGi filter metadata can be modified by bean processors
199         // after the initial service dependency is satisfied. However we don't have any metadata that could
200         // be modified by a bean processor and we don't want to register/unregister our DTCL multiple times
201         // so we only process startTracking once and close the DTCL registration once on container destroy.
202         ExecutionContext executionContext = ExecutionContext.Holder.getContext();
203         executionContext.removePartialObject(id);
204
205         Recipe myRecipe = executionContext.getRecipe(id);
206         if(myRecipe instanceof AbstractRecipe) {
207             LOG.debug("{}: setPrototype to false", id);
208             ((AbstractRecipe)myRecipe).setPrototype(false);
209         } else {
210             LOG.warn("{}: Recipe is null or not an AbstractRecipe", id);
211         }
212
213         return currentAppConfig;
214     }
215
216     @Override
217     public void startTracking(final SatisfactionCallback satisfactionCallback) {
218         if(!started.compareAndSet(false, true)) {
219             return;
220         }
221
222         LOG.debug("{}: In startTracking", id);
223
224         this.satisfactionCallback = satisfactionCallback;
225
226         // First get the BindingNormalizedNodeSerializer OSGi service. This will be used to create a default
227         // instance of the app config binding class, if necessary.
228
229         bindingCodecServiceRecipe = new StaticServiceReferenceRecipe(id + "-binding-codec", container,
230                 BindingNormalizedNodeSerializer.class.getName());
231         dependendencyDesc = bindingCodecServiceRecipe.getOsgiFilter();
232
233         bindingCodecServiceRecipe.startTracking(service -> {
234             bindingSerializer = (BindingNormalizedNodeSerializer)service;
235             retrieveDataBrokerService();
236         });
237     }
238
239     private void retrieveDataBrokerService() {
240         LOG.debug("{}: In retrieveDataBrokerService", id);
241
242         // Get the binding DataBroker OSGi service.
243
244         dataBrokerServiceRecipe = new StaticServiceReferenceRecipe(id + "-data-broker", container,
245                 DataBroker.class.getName());
246         dependendencyDesc = dataBrokerServiceRecipe.getOsgiFilter();
247
248         dataBrokerServiceRecipe.startTracking(service -> retrieveInitialAppConfig((DataBroker)service));
249
250     }
251
252     private void retrieveInitialAppConfig(DataBroker dataBroker) {
253         LOG.debug("{}: Got DataBroker instance - reading app config {}", id, bindingContext.appConfigPath);
254
255         dependendencyDesc = "Initial app config " + bindingContext.appConfigBindingClass.getSimpleName();
256
257         // We register a DTCL to get updates and also read the app config data from the data store. If
258         // the app config data is present then both the read and initial DTCN update will return it. If the
259         // the data isn't present, we won't get an initial DTCN update so the read will indicate the data
260         // isn't present.
261
262         DataTreeIdentifier<DataObject> dataTreeId = new DataTreeIdentifier<>(LogicalDatastoreType.CONFIGURATION,
263                 bindingContext.appConfigPath);
264         appConfigChangeListenerReg = dataBroker.registerDataTreeChangeListener(dataTreeId,
265                 new ClusteredDataTreeChangeListener<DataObject>() {
266                     @Override
267                     public void onDataTreeChanged(Collection<DataTreeModification<DataObject>> changes) {
268                         onAppConfigChanged(changes);
269                     }
270                 });
271
272         readInitialAppConfig(dataBroker);
273     }
274
275     private void readInitialAppConfig(final DataBroker dataBroker) {
276
277         final ReadOnlyTransaction readOnlyTx = dataBroker.newReadOnlyTransaction();
278         CheckedFuture<Optional<DataObject>, ReadFailedException> future = readOnlyTx.read(
279                 LogicalDatastoreType.CONFIGURATION, bindingContext.appConfigPath);
280         Futures.addCallback(future, new FutureCallback<Optional<DataObject>>() {
281             @Override
282             public void onSuccess(Optional<DataObject> possibleAppConfig) {
283                 LOG.debug("{}: Read of app config {} succeeded: {}", id, bindingContext.appConfigBindingClass.getName(),
284                         possibleAppConfig);
285
286                 readOnlyTx.close();
287                 setInitialAppConfig(possibleAppConfig);
288             }
289
290             @Override
291             public void onFailure(Throwable t) {
292                 readOnlyTx.close();
293
294                 // We may have gotten the app config via the data tree change listener so only retry if not.
295                 if(readingInitialAppConfig.get()) {
296                     LOG.warn("{}: Read of app config {} failed - retrying", id,
297                             bindingContext.appConfigBindingClass.getName(), t);
298
299                     readInitialAppConfig(dataBroker);
300                 }
301             }
302         });
303     }
304
305     private void onAppConfigChanged(Collection<DataTreeModification<DataObject>> changes) {
306         for(DataTreeModification<DataObject> change: changes) {
307             DataObjectModification<DataObject> changeRoot = change.getRootNode();
308             ModificationType type = changeRoot.getModificationType();
309
310             LOG.debug("{}: onAppConfigChanged: {}, {}", id, type, change.getRootPath());
311
312             if(type == ModificationType.SUBTREE_MODIFIED || type == ModificationType.WRITE) {
313                 DataObject newAppConfig = changeRoot.getDataAfter();
314
315                 LOG.debug("New app config instance: {}, previous: {}", newAppConfig, currentAppConfig);
316
317                 if(!setInitialAppConfig(Optional.of(newAppConfig)) &&
318                         !Objects.equals(currentAppConfig, newAppConfig)) {
319                     LOG.debug("App config was updated - scheduling container for restart");
320
321                     restartContainer();
322                 }
323             } else if(type == ModificationType.DELETE) {
324                 LOG.debug("App config was deleted - scheduling container for restart");
325
326                 restartContainer();
327             }
328         }
329     }
330
331     private boolean setInitialAppConfig(Optional<DataObject> possibleAppConfig) {
332         boolean result = readingInitialAppConfig.compareAndSet(true, false);
333         if(result) {
334             DataObject localAppConfig;
335             if(possibleAppConfig.isPresent()) {
336                 localAppConfig = possibleAppConfig.get();
337             } else {
338                 // No app config data is present so create an empty instance via the bindingSerializer service.
339                 // This will also return default values for leafs that haven't been explicitly set.
340                 localAppConfig = createDefaultInstance();
341             }
342
343             LOG.debug("{}: Setting currentAppConfig instance: {}", id, localAppConfig);
344
345             // Now publish the app config instance to the volatile field and notify the callback to let the
346             // container know our dependency is now satisfied.
347             currentAppConfig = localAppConfig;
348             satisfactionCallback.notifyChanged();
349         }
350
351         return result;
352     }
353
354     private DataObject createDefaultInstance() {
355         YangInstanceIdentifier yangPath = bindingSerializer.toYangInstanceIdentifier(bindingContext.appConfigPath);
356
357         LOG.debug("{}: Creating app config instance from path {}, Qname: {}", id, yangPath, bindingContext.bindingQName);
358
359         SchemaService schemaService = getOSGiService(SchemaService.class);
360         if(schemaService == null) {
361             failureMessage = String.format("%s: Could not obtain the SchemaService OSGi service", id);
362             return null;
363         }
364
365         SchemaContext schemaContext = schemaService.getGlobalContext();
366
367         Module module = schemaContext.findModuleByNamespaceAndRevision(bindingContext.bindingQName.getNamespace(),
368                 bindingContext.bindingQName.getRevision());
369         if(module == null) {
370             failureMessage = String.format("%s: Could not obtain the module schema for namespace %s, revision %s",
371                     id, bindingContext.bindingQName.getNamespace(), bindingContext.bindingQName.getRevision());
372             return null;
373         }
374
375         DataSchemaNode dataSchema = module.getDataChildByName(bindingContext.bindingQName);
376         if(dataSchema == null) {
377             failureMessage = String.format("%s: Could not obtain the schema for %s", id, bindingContext.bindingQName);
378             return null;
379         }
380
381         if(!bindingContext.schemaType.isAssignableFrom(dataSchema.getClass())) {
382             failureMessage = String.format("%s: Expected schema type %s for %s but actual type is %s", id,
383                     bindingContext.schemaType, bindingContext.bindingQName, dataSchema.getClass());
384             return null;
385         }
386
387         NormalizedNode<?, ?> dataNode = parsePossibleDefaultAppConfigElement(schemaContext, dataSchema);
388         if(dataNode == null) {
389             dataNode = bindingContext.newDefaultNode(dataSchema);
390         }
391
392         DataObject appConfig = bindingSerializer.fromNormalizedNode(yangPath, dataNode).getValue();
393
394         if(appConfig == null) {
395             // This shouldn't happen but need to handle it in case...
396             failureMessage = String.format("%s: Could not create instance for app config binding %s",
397                     id, bindingContext.appConfigBindingClass);
398         }
399
400         return appConfig;
401     }
402
403     @Nullable
404     private NormalizedNode<?, ?> parsePossibleDefaultAppConfigElement(SchemaContext schemaContext,
405             DataSchemaNode dataSchema) {
406         if(defaultAppConfigElement == null) {
407             return null;
408         }
409
410         LOG.debug("{}: parsePossibleDefaultAppConfigElement for {}", id, bindingContext.bindingQName);
411
412         DomToNormalizedNodeParserFactory parserFactory = DomToNormalizedNodeParserFactory.getInstance(
413                 XmlUtils.DEFAULT_XML_CODEC_PROVIDER, schemaContext);
414
415
416         LOG.debug("{}: Got app config schema: {}", id, dataSchema);
417
418         NormalizedNode<?, ?> dataNode = bindingContext.parseDataElement(defaultAppConfigElement, dataSchema,
419                 parserFactory);
420
421         LOG.debug("{}: Parsed data node: {}", id, dataNode);
422
423         return dataNode;
424     }
425
426     private void restartContainer() {
427         BlueprintContainerRestartService restartService = getOSGiService(BlueprintContainerRestartService.class);
428         if(restartService != null) {
429             restartService.restartContainerAndDependents(container.getBundleContext().getBundle());
430         }
431     }
432
433     @SuppressWarnings("unchecked")
434     @Nullable
435     private <T> T getOSGiService(Class<T> serviceInterface) {
436         try {
437             ServiceReference<T> serviceReference =
438                     container.getBundleContext().getServiceReference(serviceInterface);
439             if(serviceReference == null) {
440                 LOG.warn("{}: {} reference not found", id, serviceInterface.getSimpleName());
441                 return null;
442             }
443
444             T service = (T)container.getService(serviceReference);
445             if(service == null) {
446                 // This could happen on shutdown if the service was already unregistered so we log as debug.
447                 LOG.debug("{}: {} was not found", id, serviceInterface.getSimpleName());
448             }
449
450             return service;
451         } catch(IllegalStateException e) {
452             // This is thrown if the BundleContext is no longer valid which is possible on shutdown so we
453             // log as debug.
454             LOG.debug("{}: Error obtaining {}", id, serviceInterface.getSimpleName(), e);
455         }
456
457         return null;
458     }
459
460     @Override
461     public void stopTracking() {
462         LOG.debug("{}: In stopTracking", id);
463
464         stopServiceRecipes();
465     }
466
467     @Override
468     public void destroy(Object instance) {
469         LOG.debug("{}: In destroy", id);
470
471         if(appConfigChangeListenerReg != null) {
472             appConfigChangeListenerReg.close();
473             appConfigChangeListenerReg = null;
474         }
475
476         stopServiceRecipes();
477     }
478
479     private void stopServiceRecipes() {
480         stopServiceRecipe(dataBrokerServiceRecipe);
481         stopServiceRecipe(bindingCodecServiceRecipe);
482         dataBrokerServiceRecipe = null;
483         bindingCodecServiceRecipe = null;
484     }
485
486     private void stopServiceRecipe(StaticServiceReferenceRecipe recipe) {
487         if(recipe != null) {
488             recipe.stop();
489         }
490     }
491
492     @Override
493     public String getDependencyDescriptor() {
494         return dependendencyDesc;
495     }
496
497     /**
498      * Internal base class to abstract binding type-specific behavior.
499      */
500     private static abstract class BindingContext {
501         final InstanceIdentifier<DataObject> appConfigPath;
502         final Class<DataObject> appConfigBindingClass;
503         final Class<? extends DataSchemaNode> schemaType;
504         final QName bindingQName;
505
506         protected BindingContext(Class<DataObject> appConfigBindingClass, InstanceIdentifier<DataObject> appConfigPath,
507                 Class<? extends DataSchemaNode> schemaType) {
508             this.appConfigBindingClass = appConfigBindingClass;
509             this.appConfigPath = appConfigPath;
510             this.schemaType = schemaType;
511
512             bindingQName = BindingReflections.findQName(appConfigBindingClass);
513         }
514
515         abstract NormalizedNode<?, ?> parseDataElement(Element element, DataSchemaNode dataSchema,
516                 DomToNormalizedNodeParserFactory parserFactory);
517
518         abstract NormalizedNode<?, ?> newDefaultNode(DataSchemaNode dataSchema);
519     }
520
521     /**
522      * BindingContext implementation for a container binding.
523      */
524     private static class ContainerBindingContext extends BindingContext {
525         ContainerBindingContext(Class<DataObject> appConfigBindingClass) {
526             super(appConfigBindingClass, InstanceIdentifier.create(appConfigBindingClass), ContainerSchemaNode.class);
527         }
528
529         @Override
530         NormalizedNode<?, ?> newDefaultNode(DataSchemaNode dataSchema) {
531             return ImmutableNodes.containerNode(bindingQName);
532         }
533
534         @Override
535         NormalizedNode<?, ?> parseDataElement(Element element, DataSchemaNode dataSchema,
536                 DomToNormalizedNodeParserFactory parserFactory) {
537             return parserFactory.getContainerNodeParser().parse(Collections.singletonList(element),
538                     (ContainerSchemaNode)dataSchema);
539         }
540     }
541
542     /**
543      * BindingContext implementation for a list binding.
544      */
545     private static class ListBindingContext extends BindingContext {
546         final String appConfigListKeyValue;
547
548         ListBindingContext(Class<DataObject> appConfigBindingClass, InstanceIdentifier<DataObject> appConfigPath,
549                 String appConfigListKeyValue) {
550             super(appConfigBindingClass, appConfigPath, ListSchemaNode.class);
551             this.appConfigListKeyValue = appConfigListKeyValue;
552         }
553
554         @SuppressWarnings({ "rawtypes", "unchecked" })
555         private static ListBindingContext newInstance(Class<DataObject> bindingClass, String listKeyValue)
556                 throws Exception {
557             // We assume the yang list key type is string.
558             Identifier keyInstance = (Identifier) bindingClass.getMethod("getKey").getReturnType().
559                     getConstructor(String.class).newInstance(listKeyValue);
560             InstanceIdentifier appConfigPath = InstanceIdentifier.builder((Class)bindingClass, keyInstance).build();
561             return new ListBindingContext(bindingClass, appConfigPath, listKeyValue);
562         }
563
564         @Override
565         NormalizedNode<?, ?> newDefaultNode(DataSchemaNode dataSchema) {
566             // We assume there's only one key for the list.
567             List<QName> keys = ((ListSchemaNode)dataSchema).getKeyDefinition();
568             Preconditions.checkArgument(keys.size() == 1, "Expected only 1 key for list %s", appConfigBindingClass);
569             QName listKeyQName = keys.iterator().next();
570             return ImmutableNodes.mapEntryBuilder(bindingQName, listKeyQName, appConfigListKeyValue).build();
571         }
572
573         @Override
574         NormalizedNode<?, ?> parseDataElement(Element element, DataSchemaNode dataSchema,
575                 DomToNormalizedNodeParserFactory parserFactory) {
576             return parserFactory.getMapEntryNodeParser().parse(Collections.singletonList(element),
577                     (ListSchemaNode)dataSchema);
578         }
579     }
580 }