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