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