2 * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.controller.blueprint.ext;
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;
17 import java.io.FileInputStream;
18 import java.util.Collection;
19 import java.util.Collections;
20 import java.util.List;
21 import java.util.Objects;
22 import java.util.concurrent.atomic.AtomicBoolean;
23 import javax.annotation.Nonnull;
24 import javax.annotation.Nullable;
25 import javax.xml.parsers.DocumentBuilderFactory;
26 import org.apache.aries.blueprint.services.ExtendedBlueprintContainer;
27 import org.opendaylight.controller.md.sal.binding.api.ClusteredDataTreeChangeListener;
28 import org.opendaylight.controller.md.sal.binding.api.DataBroker;
29 import org.opendaylight.controller.md.sal.binding.api.DataObjectModification;
30 import org.opendaylight.controller.md.sal.binding.api.DataObjectModification.ModificationType;
31 import org.opendaylight.controller.md.sal.binding.api.DataTreeIdentifier;
32 import org.opendaylight.controller.md.sal.binding.api.DataTreeModification;
33 import org.opendaylight.controller.md.sal.binding.api.ReadOnlyTransaction;
34 import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
35 import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException;
36 import org.opendaylight.controller.sal.core.api.model.SchemaService;
37 import org.opendaylight.yangtools.binding.data.codec.api.BindingNormalizedNodeSerializer;
38 import org.opendaylight.yangtools.concepts.ListenerRegistration;
39 import org.opendaylight.yangtools.yang.binding.DataObject;
40 import org.opendaylight.yangtools.yang.binding.Identifiable;
41 import org.opendaylight.yangtools.yang.binding.Identifier;
42 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
43 import org.opendaylight.yangtools.yang.binding.util.BindingReflections;
44 import org.opendaylight.yangtools.yang.common.QName;
45 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
46 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
47 import org.opendaylight.yangtools.yang.data.impl.codec.xml.XmlUtils;
48 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
49 import org.opendaylight.yangtools.yang.data.impl.schema.transform.dom.parser.DomToNormalizedNodeParserFactory;
50 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
51 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
52 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
53 import org.opendaylight.yangtools.yang.model.api.Module;
54 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
55 import org.osgi.service.blueprint.container.ComponentDefinitionException;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
58 import org.w3c.dom.Document;
59 import org.w3c.dom.Element;
62 * Factory metadata corresponding to the "clustered-app-config" element that obtains an application's
63 * config data from the data store and provides the binding DataObject instance to the Blueprint container
64 * as a bean. In addition registers a DataTreeChangeListener to restart the Blueprint container when the
65 * config data is changed.
67 * @author Thomas Pantelis
69 public class DataStoreAppConfigMetadata extends AbstractDependentComponentFactoryMetadata {
70 private static final Logger LOG = LoggerFactory.getLogger(DataStoreAppConfigMetadata.class);
72 static final String BINDING_CLASS = "binding-class";
73 static final String DEFAULT_CONFIG = "default-config";
74 static final String DEFAULT_CONFIG_FILE_NAME = "default-config-file-name";
75 static final String LIST_KEY_VALUE = "list-key-value";
77 private static final String DEFAULT_APP_CONFIG_FILE_PATH = "etc" + File.separator +
78 "opendaylight" + File.separator + "datastore" + File.separator + "initial" +
79 File.separator + "config";
81 private static final DocumentBuilderFactory DOC_BUILDER_FACTORY;
84 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
85 factory.setNamespaceAware(true);
86 factory.setCoalescing(true);
87 factory.setIgnoringElementContentWhitespace(true);
88 factory.setIgnoringComments(true);
89 DOC_BUILDER_FACTORY = factory;
92 private final Element defaultAppConfigElement;
93 private final String defaultAppConfigFileName;
94 private final String appConfigBindingClassName;
95 private final String appConfigListKeyValue;
96 private final AtomicBoolean readingInitialAppConfig = new AtomicBoolean(true);
98 private volatile BindingContext bindingContext;
99 private volatile ListenerRegistration<?> appConfigChangeListenerReg;
100 private volatile DataObject currentAppConfig;
102 // Note: the BindingNormalizedNodeSerializer interface is annotated as deprecated because there's an
103 // equivalent interface in the mdsal project but the corresponding binding classes in the controller
104 // project are still used - conversion to the mdsal binding classes hasn't occurred yet.
105 private volatile BindingNormalizedNodeSerializer bindingSerializer;
107 public DataStoreAppConfigMetadata(@Nonnull String id, @Nonnull String appConfigBindingClassName,
108 @Nullable String appConfigListKeyValue, @Nullable String defaultAppConfigFileName,
109 @Nullable Element defaultAppConfigElement) {
111 this.defaultAppConfigElement = defaultAppConfigElement;
112 this.defaultAppConfigFileName = defaultAppConfigFileName;
113 this.appConfigBindingClassName = appConfigBindingClassName;
114 this.appConfigListKeyValue = appConfigListKeyValue;
118 @SuppressWarnings("unchecked")
119 public void init(ExtendedBlueprintContainer container) {
120 super.init(container);
122 Class<DataObject> appConfigBindingClass;
124 Class<?> bindingClass = container.getBundleContext().getBundle().loadClass(appConfigBindingClassName);
125 if(!DataObject.class.isAssignableFrom(bindingClass)) {
126 throw new ComponentDefinitionException(String.format(
127 "%s: Specified app config binding class %s does not extend %s",
128 logName(), appConfigBindingClassName, DataObject.class.getName()));
131 appConfigBindingClass = (Class<DataObject>) bindingClass;
132 } catch(ClassNotFoundException e) {
133 throw new ComponentDefinitionException(String.format("%s: Error loading app config binding class %s",
134 logName(), appConfigBindingClassName), e);
137 if(Identifiable.class.isAssignableFrom(appConfigBindingClass)) {
138 // The binding class corresponds to a yang list.
139 if(Strings.isNullOrEmpty(appConfigListKeyValue)) {
140 throw new ComponentDefinitionException(String.format(
141 "%s: App config binding class %s represents a yang list therefore \"%s\" must be specified",
142 logName(), appConfigBindingClassName, LIST_KEY_VALUE));
146 bindingContext = ListBindingContext.newInstance(appConfigBindingClass, appConfigListKeyValue);
147 } catch(Exception e) {
148 throw new ComponentDefinitionException(String.format(
149 "%s: Error initializing for app config list binding class %s",
150 logName(), appConfigBindingClassName), e);
154 bindingContext = new ContainerBindingContext(appConfigBindingClass);
159 public Object create() throws ComponentDefinitionException {
160 LOG.debug("{}: In create - currentAppConfig: {}", logName(), currentAppConfig);
164 return currentAppConfig;
168 protected void startTracking() {
169 // First get the BindingNormalizedNodeSerializer OSGi service. This will be used to create a default
170 // instance of the app config binding class, if necessary.
172 retrieveService("binding-codec", BindingNormalizedNodeSerializer.class, service -> {
173 bindingSerializer = (BindingNormalizedNodeSerializer)service;
174 retrieveDataBrokerService();
178 private void retrieveDataBrokerService() {
179 LOG.debug("{}: In retrieveDataBrokerService", logName());
181 // Get the binding DataBroker OSGi service.
183 retrieveService("data-broker", DataBroker.class, service -> retrieveInitialAppConfig((DataBroker)service));
186 private void retrieveInitialAppConfig(DataBroker dataBroker) {
187 LOG.debug("{}: Got DataBroker instance - reading app config {}", logName(), bindingContext.appConfigPath);
189 setDependendencyDesc("Initial app config " + bindingContext.appConfigBindingClass.getSimpleName());
191 // We register a DTCL to get updates and also read the app config data from the data store. If
192 // the app config data is present then both the read and initial DTCN update will return it. If the
193 // the data isn't present, we won't get an initial DTCN update so the read will indicate the data
196 DataTreeIdentifier<DataObject> dataTreeId = new DataTreeIdentifier<>(LogicalDatastoreType.CONFIGURATION,
197 bindingContext.appConfigPath);
198 appConfigChangeListenerReg = dataBroker.registerDataTreeChangeListener(dataTreeId,
199 new ClusteredDataTreeChangeListener<DataObject>() {
201 public void onDataTreeChanged(Collection<DataTreeModification<DataObject>> changes) {
202 onAppConfigChanged(changes);
206 readInitialAppConfig(dataBroker);
209 private void readInitialAppConfig(final DataBroker dataBroker) {
211 final ReadOnlyTransaction readOnlyTx = dataBroker.newReadOnlyTransaction();
212 CheckedFuture<Optional<DataObject>, ReadFailedException> future = readOnlyTx.read(
213 LogicalDatastoreType.CONFIGURATION, bindingContext.appConfigPath);
214 Futures.addCallback(future, new FutureCallback<Optional<DataObject>>() {
216 public void onSuccess(Optional<DataObject> possibleAppConfig) {
217 LOG.debug("{}: Read of app config {} succeeded: {}", logName(), bindingContext.appConfigBindingClass.getName(),
221 setInitialAppConfig(possibleAppConfig);
225 public void onFailure(Throwable t) {
228 // We may have gotten the app config via the data tree change listener so only retry if not.
229 if(readingInitialAppConfig.get()) {
230 LOG.warn("{}: Read of app config {} failed - retrying", logName(),
231 bindingContext.appConfigBindingClass.getName(), t);
233 readInitialAppConfig(dataBroker);
239 private void onAppConfigChanged(Collection<DataTreeModification<DataObject>> changes) {
240 for(DataTreeModification<DataObject> change: changes) {
241 DataObjectModification<DataObject> changeRoot = change.getRootNode();
242 ModificationType type = changeRoot.getModificationType();
244 LOG.debug("{}: onAppConfigChanged: {}, {}", logName(), type, change.getRootPath());
246 if(type == ModificationType.SUBTREE_MODIFIED || type == ModificationType.WRITE) {
247 DataObject newAppConfig = changeRoot.getDataAfter();
249 LOG.debug("New app config instance: {}, previous: {}", newAppConfig, currentAppConfig);
251 if(!setInitialAppConfig(Optional.of(newAppConfig)) &&
252 !Objects.equals(currentAppConfig, newAppConfig)) {
253 LOG.debug("App config was updated - scheduling container for restart");
257 } else if(type == ModificationType.DELETE) {
258 LOG.debug("App config was deleted - scheduling container for restart");
265 private boolean setInitialAppConfig(Optional<DataObject> possibleAppConfig) {
266 boolean result = readingInitialAppConfig.compareAndSet(true, false);
268 DataObject localAppConfig;
269 if(possibleAppConfig.isPresent()) {
270 localAppConfig = possibleAppConfig.get();
272 // No app config data is present so create an empty instance via the bindingSerializer service.
273 // This will also return default values for leafs that haven't been explicitly set.
274 localAppConfig = createDefaultInstance();
277 LOG.debug("{}: Setting currentAppConfig instance: {}", logName(), localAppConfig);
279 // Now publish the app config instance to the volatile field and notify the callback to let the
280 // container know our dependency is now satisfied.
281 currentAppConfig = localAppConfig;
288 private DataObject createDefaultInstance() {
289 YangInstanceIdentifier yangPath = bindingSerializer.toYangInstanceIdentifier(bindingContext.appConfigPath);
291 LOG.debug("{}: Creating app config instance from path {}, Qname: {}", logName(), yangPath, bindingContext.bindingQName);
293 SchemaService schemaService = getOSGiService(SchemaService.class);
294 if(schemaService == null) {
295 setFailureMessage(String.format("%s: Could not obtain the SchemaService OSGi service", logName()));
299 SchemaContext schemaContext = schemaService.getGlobalContext();
301 Module module = schemaContext.findModuleByNamespaceAndRevision(bindingContext.bindingQName.getNamespace(),
302 bindingContext.bindingQName.getRevision());
304 setFailureMessage(String.format("%s: Could not obtain the module schema for namespace %s, revision %s",
305 logName(), bindingContext.bindingQName.getNamespace(), bindingContext.bindingQName.getRevision()));
309 DataSchemaNode dataSchema = module.getDataChildByName(bindingContext.bindingQName);
310 if(dataSchema == null) {
311 setFailureMessage(String.format("%s: Could not obtain the schema for %s", logName(), bindingContext.bindingQName));
315 if(!bindingContext.schemaType.isAssignableFrom(dataSchema.getClass())) {
316 setFailureMessage(String.format("%s: Expected schema type %s for %s but actual type is %s", logName(),
317 bindingContext.schemaType, bindingContext.bindingQName, dataSchema.getClass()));
321 NormalizedNode<?, ?> dataNode = parsePossibleDefaultAppConfigXMLFile(schemaContext, dataSchema);
322 if(dataNode == null) {
323 dataNode = parsePossibleDefaultAppConfigElement(schemaContext, dataSchema);
326 if(dataNode == null) {
327 dataNode = bindingContext.newDefaultNode(dataSchema);
330 DataObject appConfig = bindingSerializer.fromNormalizedNode(yangPath, dataNode).getValue();
332 if(appConfig == null) {
333 // This shouldn't happen but need to handle it in case...
334 setFailureMessage(String.format("%s: Could not create instance for app config binding %s",
335 logName(), bindingContext.appConfigBindingClass));
341 private NormalizedNode<?, ?> parsePossibleDefaultAppConfigXMLFile(SchemaContext schemaContext,
342 DataSchemaNode dataSchema) {
344 String appConfigFileName = defaultAppConfigFileName;
345 if(Strings.isNullOrEmpty(appConfigFileName)) {
346 String moduleName = findYangModuleName(bindingContext.bindingQName, schemaContext);
347 if(moduleName == null) {
351 appConfigFileName = moduleName + "_" + bindingContext.bindingQName.getLocalName() + ".xml";
354 File appConfigFile = new File(DEFAULT_APP_CONFIG_FILE_PATH, appConfigFileName);
356 LOG.debug("{}: parsePossibleDefaultAppConfigXMLFile looking for file {}", logName(),
357 appConfigFile.getAbsolutePath());
359 if(!appConfigFile.exists()) {
363 LOG.debug("{}: Found file {}", logName(), appConfigFile.getAbsolutePath());
365 DomToNormalizedNodeParserFactory parserFactory = DomToNormalizedNodeParserFactory.getInstance(
366 XmlUtils.DEFAULT_XML_CODEC_PROVIDER, schemaContext);
368 try(FileInputStream fis = new FileInputStream(appConfigFile)) {
369 Document root = DOC_BUILDER_FACTORY.newDocumentBuilder().parse(fis);
370 NormalizedNode<?, ?> dataNode = bindingContext.parseDataElement(root.getDocumentElement(), dataSchema,
373 LOG.debug("{}: Parsed data node: {}", logName(), dataNode);
376 } catch (Exception e) {
377 setFailureMessage(String.format("%s: Could not read/parse app config file %s", logName(), appConfigFile));
383 private String findYangModuleName(QName qname, SchemaContext schemaContext) {
384 for(Module m: schemaContext.getModules()) {
385 if(qname.getModule().equals(m.getQNameModule())) {
390 setFailureMessage(String.format("%s: Could not find yang module for QName %s", logName(), qname));
395 private NormalizedNode<?, ?> parsePossibleDefaultAppConfigElement(SchemaContext schemaContext,
396 DataSchemaNode dataSchema) {
397 if(defaultAppConfigElement == null) {
401 LOG.debug("{}: parsePossibleDefaultAppConfigElement for {}", logName(), bindingContext.bindingQName);
403 DomToNormalizedNodeParserFactory parserFactory = DomToNormalizedNodeParserFactory.getInstance(
404 XmlUtils.DEFAULT_XML_CODEC_PROVIDER, schemaContext);
407 LOG.debug("{}: Got app config schema: {}", logName(), dataSchema);
409 NormalizedNode<?, ?> dataNode = bindingContext.parseDataElement(defaultAppConfigElement, dataSchema,
412 LOG.debug("{}: Parsed data node: {}", logName(), dataNode);
419 public void destroy(Object instance) {
420 super.destroy(instance);
422 if(appConfigChangeListenerReg != null) {
423 appConfigChangeListenerReg.close();
424 appConfigChangeListenerReg = null;
429 * Internal base class to abstract binding type-specific behavior.
431 private static abstract class BindingContext {
432 final InstanceIdentifier<DataObject> appConfigPath;
433 final Class<DataObject> appConfigBindingClass;
434 final Class<? extends DataSchemaNode> schemaType;
435 final QName bindingQName;
437 protected BindingContext(Class<DataObject> appConfigBindingClass, InstanceIdentifier<DataObject> appConfigPath,
438 Class<? extends DataSchemaNode> schemaType) {
439 this.appConfigBindingClass = appConfigBindingClass;
440 this.appConfigPath = appConfigPath;
441 this.schemaType = schemaType;
443 bindingQName = BindingReflections.findQName(appConfigBindingClass);
446 abstract NormalizedNode<?, ?> parseDataElement(Element element, DataSchemaNode dataSchema,
447 DomToNormalizedNodeParserFactory parserFactory);
449 abstract NormalizedNode<?, ?> newDefaultNode(DataSchemaNode dataSchema);
453 * BindingContext implementation for a container binding.
455 private static class ContainerBindingContext extends BindingContext {
456 ContainerBindingContext(Class<DataObject> appConfigBindingClass) {
457 super(appConfigBindingClass, InstanceIdentifier.create(appConfigBindingClass), ContainerSchemaNode.class);
461 NormalizedNode<?, ?> newDefaultNode(DataSchemaNode dataSchema) {
462 return ImmutableNodes.containerNode(bindingQName);
466 NormalizedNode<?, ?> parseDataElement(Element element, DataSchemaNode dataSchema,
467 DomToNormalizedNodeParserFactory parserFactory) {
468 return parserFactory.getContainerNodeParser().parse(Collections.singletonList(element),
469 (ContainerSchemaNode)dataSchema);
474 * BindingContext implementation for a list binding.
476 private static class ListBindingContext extends BindingContext {
477 final String appConfigListKeyValue;
479 ListBindingContext(Class<DataObject> appConfigBindingClass, InstanceIdentifier<DataObject> appConfigPath,
480 String appConfigListKeyValue) {
481 super(appConfigBindingClass, appConfigPath, ListSchemaNode.class);
482 this.appConfigListKeyValue = appConfigListKeyValue;
485 @SuppressWarnings({ "rawtypes", "unchecked" })
486 private static ListBindingContext newInstance(Class<DataObject> bindingClass, String listKeyValue)
488 // We assume the yang list key type is string.
489 Identifier keyInstance = (Identifier) bindingClass.getMethod("getKey").getReturnType().
490 getConstructor(String.class).newInstance(listKeyValue);
491 InstanceIdentifier appConfigPath = InstanceIdentifier.builder((Class)bindingClass, keyInstance).build();
492 return new ListBindingContext(bindingClass, appConfigPath, listKeyValue);
496 NormalizedNode<?, ?> newDefaultNode(DataSchemaNode dataSchema) {
497 // We assume there's only one key for the list.
498 List<QName> keys = ((ListSchemaNode)dataSchema).getKeyDefinition();
499 Preconditions.checkArgument(keys.size() == 1, "Expected only 1 key for list %s", appConfigBindingClass);
500 QName listKeyQName = keys.iterator().next();
501 return ImmutableNodes.mapEntryBuilder(bindingQName, listKeyQName, appConfigListKeyValue).build();
505 NormalizedNode<?, ?> parseDataElement(Element element, DataSchemaNode dataSchema,
506 DomToNormalizedNodeParserFactory parserFactory) {
507 return parserFactory.getMapEntryNodeParser().parse(Collections.singletonList(element),
508 (ListSchemaNode)dataSchema);