Add "specific-reference-list" 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.services.ExtendedBlueprintContainer;
24 import org.opendaylight.controller.md.sal.binding.api.ClusteredDataTreeChangeListener;
25 import org.opendaylight.controller.md.sal.binding.api.DataBroker;
26 import org.opendaylight.controller.md.sal.binding.api.DataObjectModification;
27 import org.opendaylight.controller.md.sal.binding.api.DataObjectModification.ModificationType;
28 import org.opendaylight.controller.md.sal.binding.api.DataTreeIdentifier;
29 import org.opendaylight.controller.md.sal.binding.api.DataTreeModification;
30 import org.opendaylight.controller.md.sal.binding.api.ReadOnlyTransaction;
31 import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
32 import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException;
33 import org.opendaylight.controller.sal.core.api.model.SchemaService;
34 import org.opendaylight.yangtools.binding.data.codec.api.BindingNormalizedNodeSerializer;
35 import org.opendaylight.yangtools.concepts.ListenerRegistration;
36 import org.opendaylight.yangtools.yang.binding.DataObject;
37 import org.opendaylight.yangtools.yang.binding.Identifiable;
38 import org.opendaylight.yangtools.yang.binding.Identifier;
39 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
40 import org.opendaylight.yangtools.yang.binding.util.BindingReflections;
41 import org.opendaylight.yangtools.yang.common.QName;
42 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
43 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
44 import org.opendaylight.yangtools.yang.data.impl.codec.xml.XmlUtils;
45 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
46 import org.opendaylight.yangtools.yang.data.impl.schema.transform.dom.parser.DomToNormalizedNodeParserFactory;
47 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
48 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
50 import org.opendaylight.yangtools.yang.model.api.Module;
51 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
52 import org.osgi.service.blueprint.container.ComponentDefinitionException;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55 import org.w3c.dom.Element;
56
57 /**
58  * Factory metadata corresponding to the "clustered-app-config" element that obtains an application's
59  * config data from the data store and provides the binding DataObject instance to the Blueprint container
60  * as a bean. In addition registers a DataTreeChangeListener to restart the Blueprint container when the
61  * config data is changed.
62  *
63  * @author Thomas Pantelis
64  */
65 public class DataStoreAppConfigMetadata extends AbstractDependentComponentFactoryMetadata {
66     private static final Logger LOG = LoggerFactory.getLogger(DataStoreAppConfigMetadata.class);
67
68     static final String BINDING_CLASS = "binding-class";
69     static final String DEFAULT_CONFIG = "default-config";
70     static final String LIST_KEY_VALUE = "list-key-value";
71
72     private final Element defaultAppConfigElement;
73     private final String appConfigBindingClassName;
74     private final String appConfigListKeyValue;
75     private final AtomicBoolean readingInitialAppConfig = new AtomicBoolean(true);
76
77     private volatile BindingContext bindingContext;
78     private volatile ListenerRegistration<?> appConfigChangeListenerReg;
79     private volatile DataObject currentAppConfig;
80
81     // Note: the BindingNormalizedNodeSerializer interface is annotated as deprecated because there's an
82     // equivalent interface in the mdsal project but the corresponding binding classes in the controller
83     // project are still used - conversion to the mdsal binding classes hasn't occurred yet.
84     private volatile BindingNormalizedNodeSerializer bindingSerializer;
85
86     public DataStoreAppConfigMetadata(@Nonnull String id, @Nonnull String appConfigBindingClassName,
87             @Nullable String appConfigListKeyValue, @Nullable Element defaultAppConfigElement) {
88         super(id);
89         this.defaultAppConfigElement = defaultAppConfigElement;
90         this.appConfigBindingClassName = appConfigBindingClassName;
91         this.appConfigListKeyValue = appConfigListKeyValue;
92     }
93
94     @Override
95     @SuppressWarnings("unchecked")
96     public void init(ExtendedBlueprintContainer container) {
97         super.init(container);
98
99         Class<DataObject> appConfigBindingClass;
100         try {
101             Class<?> bindingClass = container.getBundleContext().getBundle().loadClass(appConfigBindingClassName);
102             if(!DataObject.class.isAssignableFrom(bindingClass)) {
103                 throw new ComponentDefinitionException(String.format(
104                         "%s: Specified app config binding class %s does not extend %s",
105                         logName(), appConfigBindingClassName, DataObject.class.getName()));
106             }
107
108             appConfigBindingClass = (Class<DataObject>) bindingClass;
109         } catch(ClassNotFoundException e) {
110             throw new ComponentDefinitionException(String.format("%s: Error loading app config binding class %s",
111                     logName(), appConfigBindingClassName), e);
112         }
113
114         if(Identifiable.class.isAssignableFrom(appConfigBindingClass)) {
115             // The binding class corresponds to a yang list.
116             if(Strings.isNullOrEmpty(appConfigListKeyValue)) {
117                 throw new ComponentDefinitionException(String.format(
118                         "%s: App config binding class %s represents a yang list therefore \"%s\" must be specified",
119                         logName(), appConfigBindingClassName, LIST_KEY_VALUE));
120             }
121
122             try {
123                 bindingContext = ListBindingContext.newInstance(appConfigBindingClass, appConfigListKeyValue);
124             } catch(Exception e) {
125                 throw new ComponentDefinitionException(String.format(
126                         "%s: Error initializing for app config list binding class %s",
127                         logName(), appConfigBindingClassName), e);
128             }
129
130         } else {
131             bindingContext = new ContainerBindingContext(appConfigBindingClass);
132         }
133     }
134
135     @Override
136     public Object create() throws ComponentDefinitionException {
137         LOG.debug("{}: In create - currentAppConfig: {}", logName(), currentAppConfig);
138
139         super.onCreate();
140
141         return currentAppConfig;
142     }
143
144     @Override
145     protected void startTracking() {
146         // First get the BindingNormalizedNodeSerializer OSGi service. This will be used to create a default
147         // instance of the app config binding class, if necessary.
148
149         retrieveService("binding-codec", BindingNormalizedNodeSerializer.class, service -> {
150             bindingSerializer = (BindingNormalizedNodeSerializer)service;
151             retrieveDataBrokerService();
152         });
153     }
154
155     private void retrieveDataBrokerService() {
156         LOG.debug("{}: In retrieveDataBrokerService", logName());
157
158         // Get the binding DataBroker OSGi service.
159
160         retrieveService("data-broker", DataBroker.class, service -> retrieveInitialAppConfig((DataBroker)service));
161     }
162
163     private void retrieveInitialAppConfig(DataBroker dataBroker) {
164         LOG.debug("{}: Got DataBroker instance - reading app config {}", logName(), bindingContext.appConfigPath);
165
166         setDependendencyDesc("Initial app config " + bindingContext.appConfigBindingClass.getSimpleName());
167
168         // We register a DTCL to get updates and also read the app config data from the data store. If
169         // the app config data is present then both the read and initial DTCN update will return it. If the
170         // the data isn't present, we won't get an initial DTCN update so the read will indicate the data
171         // isn't present.
172
173         DataTreeIdentifier<DataObject> dataTreeId = new DataTreeIdentifier<>(LogicalDatastoreType.CONFIGURATION,
174                 bindingContext.appConfigPath);
175         appConfigChangeListenerReg = dataBroker.registerDataTreeChangeListener(dataTreeId,
176                 new ClusteredDataTreeChangeListener<DataObject>() {
177                     @Override
178                     public void onDataTreeChanged(Collection<DataTreeModification<DataObject>> changes) {
179                         onAppConfigChanged(changes);
180                     }
181                 });
182
183         readInitialAppConfig(dataBroker);
184     }
185
186     private void readInitialAppConfig(final DataBroker dataBroker) {
187
188         final ReadOnlyTransaction readOnlyTx = dataBroker.newReadOnlyTransaction();
189         CheckedFuture<Optional<DataObject>, ReadFailedException> future = readOnlyTx.read(
190                 LogicalDatastoreType.CONFIGURATION, bindingContext.appConfigPath);
191         Futures.addCallback(future, new FutureCallback<Optional<DataObject>>() {
192             @Override
193             public void onSuccess(Optional<DataObject> possibleAppConfig) {
194                 LOG.debug("{}: Read of app config {} succeeded: {}", logName(), bindingContext.appConfigBindingClass.getName(),
195                         possibleAppConfig);
196
197                 readOnlyTx.close();
198                 setInitialAppConfig(possibleAppConfig);
199             }
200
201             @Override
202             public void onFailure(Throwable t) {
203                 readOnlyTx.close();
204
205                 // We may have gotten the app config via the data tree change listener so only retry if not.
206                 if(readingInitialAppConfig.get()) {
207                     LOG.warn("{}: Read of app config {} failed - retrying", logName(),
208                             bindingContext.appConfigBindingClass.getName(), t);
209
210                     readInitialAppConfig(dataBroker);
211                 }
212             }
213         });
214     }
215
216     private void onAppConfigChanged(Collection<DataTreeModification<DataObject>> changes) {
217         for(DataTreeModification<DataObject> change: changes) {
218             DataObjectModification<DataObject> changeRoot = change.getRootNode();
219             ModificationType type = changeRoot.getModificationType();
220
221             LOG.debug("{}: onAppConfigChanged: {}, {}", logName(), type, change.getRootPath());
222
223             if(type == ModificationType.SUBTREE_MODIFIED || type == ModificationType.WRITE) {
224                 DataObject newAppConfig = changeRoot.getDataAfter();
225
226                 LOG.debug("New app config instance: {}, previous: {}", newAppConfig, currentAppConfig);
227
228                 if(!setInitialAppConfig(Optional.of(newAppConfig)) &&
229                         !Objects.equals(currentAppConfig, newAppConfig)) {
230                     LOG.debug("App config was updated - scheduling container for restart");
231
232                     restartContainer();
233                 }
234             } else if(type == ModificationType.DELETE) {
235                 LOG.debug("App config was deleted - scheduling container for restart");
236
237                 restartContainer();
238             }
239         }
240     }
241
242     private boolean setInitialAppConfig(Optional<DataObject> possibleAppConfig) {
243         boolean result = readingInitialAppConfig.compareAndSet(true, false);
244         if(result) {
245             DataObject localAppConfig;
246             if(possibleAppConfig.isPresent()) {
247                 localAppConfig = possibleAppConfig.get();
248             } else {
249                 // No app config data is present so create an empty instance via the bindingSerializer service.
250                 // This will also return default values for leafs that haven't been explicitly set.
251                 localAppConfig = createDefaultInstance();
252             }
253
254             LOG.debug("{}: Setting currentAppConfig instance: {}", logName(), localAppConfig);
255
256             // Now publish the app config instance to the volatile field and notify the callback to let the
257             // container know our dependency is now satisfied.
258             currentAppConfig = localAppConfig;
259             setSatisfied();
260         }
261
262         return result;
263     }
264
265     private DataObject createDefaultInstance() {
266         YangInstanceIdentifier yangPath = bindingSerializer.toYangInstanceIdentifier(bindingContext.appConfigPath);
267
268         LOG.debug("{}: Creating app config instance from path {}, Qname: {}", logName(), yangPath, bindingContext.bindingQName);
269
270         SchemaService schemaService = getOSGiService(SchemaService.class);
271         if(schemaService == null) {
272             setFailureMessage(String.format("%s: Could not obtain the SchemaService OSGi service", logName()));
273             return null;
274         }
275
276         SchemaContext schemaContext = schemaService.getGlobalContext();
277
278         Module module = schemaContext.findModuleByNamespaceAndRevision(bindingContext.bindingQName.getNamespace(),
279                 bindingContext.bindingQName.getRevision());
280         if(module == null) {
281             setFailureMessage(String.format("%s: Could not obtain the module schema for namespace %s, revision %s",
282                     logName(), bindingContext.bindingQName.getNamespace(), bindingContext.bindingQName.getRevision()));
283             return null;
284         }
285
286         DataSchemaNode dataSchema = module.getDataChildByName(bindingContext.bindingQName);
287         if(dataSchema == null) {
288             setFailureMessage(String.format("%s: Could not obtain the schema for %s", logName(), bindingContext.bindingQName));
289             return null;
290         }
291
292         if(!bindingContext.schemaType.isAssignableFrom(dataSchema.getClass())) {
293             setFailureMessage(String.format("%s: Expected schema type %s for %s but actual type is %s", logName(),
294                     bindingContext.schemaType, bindingContext.bindingQName, dataSchema.getClass()));
295             return null;
296         }
297
298         NormalizedNode<?, ?> dataNode = parsePossibleDefaultAppConfigElement(schemaContext, dataSchema);
299         if(dataNode == null) {
300             dataNode = bindingContext.newDefaultNode(dataSchema);
301         }
302
303         DataObject appConfig = bindingSerializer.fromNormalizedNode(yangPath, dataNode).getValue();
304
305         if(appConfig == null) {
306             // This shouldn't happen but need to handle it in case...
307             setFailureMessage(String.format("%s: Could not create instance for app config binding %s",
308                     logName(), bindingContext.appConfigBindingClass));
309         }
310
311         return appConfig;
312     }
313
314     @Nullable
315     private NormalizedNode<?, ?> parsePossibleDefaultAppConfigElement(SchemaContext schemaContext,
316             DataSchemaNode dataSchema) {
317         if(defaultAppConfigElement == null) {
318             return null;
319         }
320
321         LOG.debug("{}: parsePossibleDefaultAppConfigElement for {}", logName(), bindingContext.bindingQName);
322
323         DomToNormalizedNodeParserFactory parserFactory = DomToNormalizedNodeParserFactory.getInstance(
324                 XmlUtils.DEFAULT_XML_CODEC_PROVIDER, schemaContext);
325
326
327         LOG.debug("{}: Got app config schema: {}", logName(), dataSchema);
328
329         NormalizedNode<?, ?> dataNode = bindingContext.parseDataElement(defaultAppConfigElement, dataSchema,
330                 parserFactory);
331
332         LOG.debug("{}: Parsed data node: {}", logName(), dataNode);
333
334         return dataNode;
335     }
336
337
338     @Override
339     public void destroy(Object instance) {
340         super.destroy(instance);
341
342         if(appConfigChangeListenerReg != null) {
343             appConfigChangeListenerReg.close();
344             appConfigChangeListenerReg = null;
345         }
346     }
347
348     /**
349      * Internal base class to abstract binding type-specific behavior.
350      */
351     private static abstract class BindingContext {
352         final InstanceIdentifier<DataObject> appConfigPath;
353         final Class<DataObject> appConfigBindingClass;
354         final Class<? extends DataSchemaNode> schemaType;
355         final QName bindingQName;
356
357         protected BindingContext(Class<DataObject> appConfigBindingClass, InstanceIdentifier<DataObject> appConfigPath,
358                 Class<? extends DataSchemaNode> schemaType) {
359             this.appConfigBindingClass = appConfigBindingClass;
360             this.appConfigPath = appConfigPath;
361             this.schemaType = schemaType;
362
363             bindingQName = BindingReflections.findQName(appConfigBindingClass);
364         }
365
366         abstract NormalizedNode<?, ?> parseDataElement(Element element, DataSchemaNode dataSchema,
367                 DomToNormalizedNodeParserFactory parserFactory);
368
369         abstract NormalizedNode<?, ?> newDefaultNode(DataSchemaNode dataSchema);
370     }
371
372     /**
373      * BindingContext implementation for a container binding.
374      */
375     private static class ContainerBindingContext extends BindingContext {
376         ContainerBindingContext(Class<DataObject> appConfigBindingClass) {
377             super(appConfigBindingClass, InstanceIdentifier.create(appConfigBindingClass), ContainerSchemaNode.class);
378         }
379
380         @Override
381         NormalizedNode<?, ?> newDefaultNode(DataSchemaNode dataSchema) {
382             return ImmutableNodes.containerNode(bindingQName);
383         }
384
385         @Override
386         NormalizedNode<?, ?> parseDataElement(Element element, DataSchemaNode dataSchema,
387                 DomToNormalizedNodeParserFactory parserFactory) {
388             return parserFactory.getContainerNodeParser().parse(Collections.singletonList(element),
389                     (ContainerSchemaNode)dataSchema);
390         }
391     }
392
393     /**
394      * BindingContext implementation for a list binding.
395      */
396     private static class ListBindingContext extends BindingContext {
397         final String appConfigListKeyValue;
398
399         ListBindingContext(Class<DataObject> appConfigBindingClass, InstanceIdentifier<DataObject> appConfigPath,
400                 String appConfigListKeyValue) {
401             super(appConfigBindingClass, appConfigPath, ListSchemaNode.class);
402             this.appConfigListKeyValue = appConfigListKeyValue;
403         }
404
405         @SuppressWarnings({ "rawtypes", "unchecked" })
406         private static ListBindingContext newInstance(Class<DataObject> bindingClass, String listKeyValue)
407                 throws Exception {
408             // We assume the yang list key type is string.
409             Identifier keyInstance = (Identifier) bindingClass.getMethod("getKey").getReturnType().
410                     getConstructor(String.class).newInstance(listKeyValue);
411             InstanceIdentifier appConfigPath = InstanceIdentifier.builder((Class)bindingClass, keyInstance).build();
412             return new ListBindingContext(bindingClass, appConfigPath, listKeyValue);
413         }
414
415         @Override
416         NormalizedNode<?, ?> newDefaultNode(DataSchemaNode dataSchema) {
417             // We assume there's only one key for the list.
418             List<QName> keys = ((ListSchemaNode)dataSchema).getKeyDefinition();
419             Preconditions.checkArgument(keys.size() == 1, "Expected only 1 key for list %s", appConfigBindingClass);
420             QName listKeyQName = keys.iterator().next();
421             return ImmutableNodes.mapEntryBuilder(bindingQName, listKeyQName, appConfigListKeyValue).build();
422         }
423
424         @Override
425         NormalizedNode<?, ?> parseDataElement(Element element, DataSchemaNode dataSchema,
426                 DomToNormalizedNodeParserFactory parserFactory) {
427             return parserFactory.getMapEntryNodeParser().parse(Collections.singletonList(element),
428                     (ListSchemaNode)dataSchema);
429         }
430     }
431 }