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;
16 import java.util.Collection;
17 import java.util.Collections;
18 import java.util.List;
19 import java.util.Objects;
20 import java.util.concurrent.atomic.AtomicBoolean;
21 import javax.annotation.Nonnull;
22 import javax.annotation.Nullable;
23 import org.apache.aries.blueprint.di.AbstractRecipe;
24 import org.apache.aries.blueprint.di.ExecutionContext;
25 import org.apache.aries.blueprint.di.Recipe;
26 import org.apache.aries.blueprint.ext.DependentComponentFactoryMetadata;
27 import org.apache.aries.blueprint.services.ExtendedBlueprintContainer;
28 import org.opendaylight.controller.blueprint.BlueprintContainerRestartService;
29 import org.opendaylight.controller.md.sal.binding.api.ClusteredDataTreeChangeListener;
30 import org.opendaylight.controller.md.sal.binding.api.DataBroker;
31 import org.opendaylight.controller.md.sal.binding.api.DataObjectModification;
32 import org.opendaylight.controller.md.sal.binding.api.DataObjectModification.ModificationType;
33 import org.opendaylight.controller.md.sal.binding.api.DataTreeIdentifier;
34 import org.opendaylight.controller.md.sal.binding.api.DataTreeModification;
35 import org.opendaylight.controller.md.sal.binding.api.ReadOnlyTransaction;
36 import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
37 import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException;
38 import org.opendaylight.controller.sal.core.api.model.SchemaService;
39 import org.opendaylight.yangtools.binding.data.codec.api.BindingNormalizedNodeSerializer;
40 import org.opendaylight.yangtools.concepts.ListenerRegistration;
41 import org.opendaylight.yangtools.yang.binding.DataObject;
42 import org.opendaylight.yangtools.yang.binding.Identifiable;
43 import org.opendaylight.yangtools.yang.binding.Identifier;
44 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
45 import org.opendaylight.yangtools.yang.binding.util.BindingReflections;
46 import org.opendaylight.yangtools.yang.common.QName;
47 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
48 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
49 import org.opendaylight.yangtools.yang.data.impl.codec.xml.XmlUtils;
50 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
51 import org.opendaylight.yangtools.yang.data.impl.schema.transform.dom.parser.DomToNormalizedNodeParserFactory;
52 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
53 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
54 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
55 import org.opendaylight.yangtools.yang.model.api.Module;
56 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
57 import org.osgi.framework.ServiceReference;
58 import org.osgi.service.blueprint.container.ComponentDefinitionException;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61 import org.w3c.dom.Element;
64 * Factory metadata corresponding to the "clustered-app-config" element that obtains an application's
65 * config data from the data store and provides the binding DataObject instance to the Blueprint container
66 * as a bean. In addition registers a DataTreeChangeListener to restart the Blueprint container when the
67 * config data is changed.
69 * @author Thomas Pantelis
71 public class DataStoreAppConfigMetadata implements DependentComponentFactoryMetadata {
72 private static final Logger LOG = LoggerFactory.getLogger(DataStoreAppConfigMetadata.class);
74 static final String BINDING_CLASS = "binding-class";
75 static final String DEFAULT_CONFIG = "default-config";
76 static final String LIST_KEY_VALUE = "list-key-value";
78 private final String id;
79 private final Element defaultAppConfigElement;
80 private final String appConfigBindingClassName;
81 private final String appConfigListKeyValue;
82 private final AtomicBoolean readingInitialAppConfig = new AtomicBoolean(true);
83 private final AtomicBoolean started = new AtomicBoolean();
85 private volatile BindingContext bindingContext;
86 private volatile ExtendedBlueprintContainer container;
87 private volatile StaticServiceReferenceRecipe dataBrokerServiceRecipe;
88 private volatile StaticServiceReferenceRecipe bindingCodecServiceRecipe;
89 private volatile ListenerRegistration<?> appConfigChangeListenerReg;
90 private volatile DataObject currentAppConfig;
91 private volatile SatisfactionCallback satisfactionCallback;
92 private volatile String failureMessage;
93 private volatile String dependendencyDesc;
95 // Note: the BindingNormalizedNodeSerializer interface is annotated as deprecated because there's an
96 // equivalent interface in the mdsal project but the corresponding binding classes in the controller
97 // project are still used - conversion to the mdsal binding classes hasn't occurred yet.
98 private volatile BindingNormalizedNodeSerializer bindingSerializer;
100 public DataStoreAppConfigMetadata(@Nonnull String id, @Nonnull String appConfigBindingClassName,
101 @Nullable String appConfigListKeyValue, @Nullable Element defaultAppConfigElement) {
102 this.id = Preconditions.checkNotNull(id);
103 this.defaultAppConfigElement = defaultAppConfigElement;
104 this.appConfigBindingClassName = appConfigBindingClassName;
105 this.appConfigListKeyValue = appConfigListKeyValue;
109 public String getId() {
114 public int getActivation() {
115 return ACTIVATION_EAGER;
119 public List<String> getDependsOn() {
120 return Collections.emptyList();
124 public boolean isSatisfied() {
125 return currentAppConfig != null;
129 @SuppressWarnings("unchecked")
130 public void init(ExtendedBlueprintContainer container) {
131 LOG.debug("{}: In init", id);
133 this.container = container;
135 Class<DataObject> appConfigBindingClass;
137 Class<?> bindingClass = container.getBundleContext().getBundle().loadClass(appConfigBindingClassName);
138 if(!DataObject.class.isAssignableFrom(bindingClass)) {
139 throw new ComponentDefinitionException(String.format(
140 "%s: Specified app config binding class %s does not extend %s",
141 id, appConfigBindingClassName, DataObject.class.getName()));
144 appConfigBindingClass = (Class<DataObject>) bindingClass;
145 } catch(ClassNotFoundException e) {
146 throw new ComponentDefinitionException(String.format("%s: Error loading app config binding class %s",
147 id, appConfigBindingClassName), e);
150 if(Identifiable.class.isAssignableFrom(appConfigBindingClass)) {
151 // The binding class corresponds to a yang list.
152 if(Strings.isNullOrEmpty(appConfigListKeyValue)) {
153 throw new ComponentDefinitionException(String.format(
154 "%s: App config binding class %s represents a yang list therefore \"%s\" must be specified",
155 id, appConfigBindingClassName, LIST_KEY_VALUE));
159 bindingContext = ListBindingContext.newInstance(appConfigBindingClass, appConfigListKeyValue);
160 } catch(Exception e) {
161 throw new ComponentDefinitionException(String.format(
162 "%s: Error initializing for app config list binding class %s",
163 id, appConfigBindingClassName), e);
167 bindingContext = new ContainerBindingContext(appConfigBindingClass);
172 public Object create() throws ComponentDefinitionException {
173 LOG.debug("{}: In create - currentAppConfig: {}", id, currentAppConfig);
175 if(failureMessage != null) {
176 throw new ComponentDefinitionException(failureMessage);
179 // The following code is a bit odd so requires some explanation. A little background... If a bean
180 // is a prototype then the corresponding Recipe create method does not register the bean as created
181 // with the BlueprintRepository and thus the destroy method isn't called on container destroy. We
182 // rely on destroy being called to close our DTCL registration. Unfortunately the default setting
183 // for the prototype flag in AbstractRecipe is true and the DependentComponentFactoryRecipe, which
184 // is created for DependentComponentFactoryMetadata types of which we are one, doesn't have a way for
185 // us to indicate the prototype state via our metadata.
187 // The ExecutionContext is actually backed by the BlueprintRepository so we access it here to call
188 // the removePartialObject method which removes any partially created instance, which does not apply
189 // in our case, and also has the side effect of registering our bean as created as if it wasn't a
190 // prototype. We also obtain our corresponding Recipe instance and clear the prototype flag. This
191 // doesn't look to be necessary but is done so for completeness. Better late than never. Note we have
192 // to do this here rather than in startTracking b/c the ExecutionContext is not available yet at that
195 // Now the stopTracking method is called on container destroy but startTracking/stopTracking can also
196 // be called multiple times during the container creation process for Satisfiable recipes as bean
197 // processors may modify the metadata which could affect how dependencies are satisfied. An example of
198 // this is with service references where the OSGi filter metadata can be modified by bean processors
199 // after the initial service dependency is satisfied. However we don't have any metadata that could
200 // be modified by a bean processor and we don't want to register/unregister our DTCL multiple times
201 // so we only process startTracking once and close the DTCL registration once on container destroy.
202 ExecutionContext executionContext = ExecutionContext.Holder.getContext();
203 executionContext.removePartialObject(id);
205 Recipe myRecipe = executionContext.getRecipe(id);
206 if(myRecipe instanceof AbstractRecipe) {
207 LOG.debug("{}: setPrototype to false", id);
208 ((AbstractRecipe)myRecipe).setPrototype(false);
210 LOG.warn("{}: Recipe is null or not an AbstractRecipe", id);
213 return currentAppConfig;
217 public void startTracking(final SatisfactionCallback satisfactionCallback) {
218 if(!started.compareAndSet(false, true)) {
222 LOG.debug("{}: In startTracking", id);
224 this.satisfactionCallback = satisfactionCallback;
226 // First get the BindingNormalizedNodeSerializer OSGi service. This will be used to create a default
227 // instance of the app config binding class, if necessary.
229 bindingCodecServiceRecipe = new StaticServiceReferenceRecipe(id + "-binding-codec", container,
230 BindingNormalizedNodeSerializer.class.getName());
231 dependendencyDesc = bindingCodecServiceRecipe.getOsgiFilter();
233 bindingCodecServiceRecipe.startTracking(service -> {
234 bindingSerializer = (BindingNormalizedNodeSerializer)service;
235 retrieveDataBrokerService();
239 private void retrieveDataBrokerService() {
240 LOG.debug("{}: In retrieveDataBrokerService", id);
242 // Get the binding DataBroker OSGi service.
244 dataBrokerServiceRecipe = new StaticServiceReferenceRecipe(id + "-data-broker", container,
245 DataBroker.class.getName());
246 dependendencyDesc = dataBrokerServiceRecipe.getOsgiFilter();
248 dataBrokerServiceRecipe.startTracking(service -> retrieveInitialAppConfig((DataBroker)service));
252 private void retrieveInitialAppConfig(DataBroker dataBroker) {
253 LOG.debug("{}: Got DataBroker instance - reading app config {}", id, bindingContext.appConfigPath);
255 dependendencyDesc = "Initial app config " + bindingContext.appConfigBindingClass.getSimpleName();
257 // We register a DTCL to get updates and also read the app config data from the data store. If
258 // the app config data is present then both the read and initial DTCN update will return it. If the
259 // the data isn't present, we won't get an initial DTCN update so the read will indicate the data
262 DataTreeIdentifier<DataObject> dataTreeId = new DataTreeIdentifier<>(LogicalDatastoreType.CONFIGURATION,
263 bindingContext.appConfigPath);
264 appConfigChangeListenerReg = dataBroker.registerDataTreeChangeListener(dataTreeId,
265 new ClusteredDataTreeChangeListener<DataObject>() {
267 public void onDataTreeChanged(Collection<DataTreeModification<DataObject>> changes) {
268 onAppConfigChanged(changes);
272 readInitialAppConfig(dataBroker);
275 private void readInitialAppConfig(final DataBroker dataBroker) {
277 final ReadOnlyTransaction readOnlyTx = dataBroker.newReadOnlyTransaction();
278 CheckedFuture<Optional<DataObject>, ReadFailedException> future = readOnlyTx.read(
279 LogicalDatastoreType.CONFIGURATION, bindingContext.appConfigPath);
280 Futures.addCallback(future, new FutureCallback<Optional<DataObject>>() {
282 public void onSuccess(Optional<DataObject> possibleAppConfig) {
283 LOG.debug("{}: Read of app config {} succeeded: {}", id, bindingContext.appConfigBindingClass.getName(),
287 setInitialAppConfig(possibleAppConfig);
291 public void onFailure(Throwable t) {
294 // We may have gotten the app config via the data tree change listener so only retry if not.
295 if(readingInitialAppConfig.get()) {
296 LOG.warn("{}: Read of app config {} failed - retrying", id,
297 bindingContext.appConfigBindingClass.getName(), t);
299 readInitialAppConfig(dataBroker);
305 private void onAppConfigChanged(Collection<DataTreeModification<DataObject>> changes) {
306 for(DataTreeModification<DataObject> change: changes) {
307 DataObjectModification<DataObject> changeRoot = change.getRootNode();
308 ModificationType type = changeRoot.getModificationType();
310 LOG.debug("{}: onAppConfigChanged: {}, {}", id, type, change.getRootPath());
312 if(type == ModificationType.SUBTREE_MODIFIED || type == ModificationType.WRITE) {
313 DataObject newAppConfig = changeRoot.getDataAfter();
315 LOG.debug("New app config instance: {}, previous: {}", newAppConfig, currentAppConfig);
317 if(!setInitialAppConfig(Optional.of(newAppConfig)) &&
318 !Objects.equals(currentAppConfig, newAppConfig)) {
319 LOG.debug("App config was updated - scheduling container for restart");
323 } else if(type == ModificationType.DELETE) {
324 LOG.debug("App config was deleted - scheduling container for restart");
331 private boolean setInitialAppConfig(Optional<DataObject> possibleAppConfig) {
332 boolean result = readingInitialAppConfig.compareAndSet(true, false);
334 DataObject localAppConfig;
335 if(possibleAppConfig.isPresent()) {
336 localAppConfig = possibleAppConfig.get();
338 // No app config data is present so create an empty instance via the bindingSerializer service.
339 // This will also return default values for leafs that haven't been explicitly set.
340 localAppConfig = createDefaultInstance();
343 LOG.debug("{}: Setting currentAppConfig instance: {}", id, localAppConfig);
345 // Now publish the app config instance to the volatile field and notify the callback to let the
346 // container know our dependency is now satisfied.
347 currentAppConfig = localAppConfig;
348 satisfactionCallback.notifyChanged();
354 private DataObject createDefaultInstance() {
355 YangInstanceIdentifier yangPath = bindingSerializer.toYangInstanceIdentifier(bindingContext.appConfigPath);
357 LOG.debug("{}: Creating app config instance from path {}, Qname: {}", id, yangPath, bindingContext.bindingQName);
359 SchemaService schemaService = getOSGiService(SchemaService.class);
360 if(schemaService == null) {
361 failureMessage = String.format("%s: Could not obtain the SchemaService OSGi service", id);
365 SchemaContext schemaContext = schemaService.getGlobalContext();
367 Module module = schemaContext.findModuleByNamespaceAndRevision(bindingContext.bindingQName.getNamespace(),
368 bindingContext.bindingQName.getRevision());
370 failureMessage = String.format("%s: Could not obtain the module schema for namespace %s, revision %s",
371 id, bindingContext.bindingQName.getNamespace(), bindingContext.bindingQName.getRevision());
375 DataSchemaNode dataSchema = module.getDataChildByName(bindingContext.bindingQName);
376 if(dataSchema == null) {
377 failureMessage = String.format("%s: Could not obtain the schema for %s", id, bindingContext.bindingQName);
381 if(!bindingContext.schemaType.isAssignableFrom(dataSchema.getClass())) {
382 failureMessage = String.format("%s: Expected schema type %s for %s but actual type is %s", id,
383 bindingContext.schemaType, bindingContext.bindingQName, dataSchema.getClass());
387 NormalizedNode<?, ?> dataNode = parsePossibleDefaultAppConfigElement(schemaContext, dataSchema);
388 if(dataNode == null) {
389 dataNode = bindingContext.newDefaultNode(dataSchema);
392 DataObject appConfig = bindingSerializer.fromNormalizedNode(yangPath, dataNode).getValue();
394 if(appConfig == null) {
395 // This shouldn't happen but need to handle it in case...
396 failureMessage = String.format("%s: Could not create instance for app config binding %s",
397 id, bindingContext.appConfigBindingClass);
404 private NormalizedNode<?, ?> parsePossibleDefaultAppConfigElement(SchemaContext schemaContext,
405 DataSchemaNode dataSchema) {
406 if(defaultAppConfigElement == null) {
410 LOG.debug("{}: parsePossibleDefaultAppConfigElement for {}", id, bindingContext.bindingQName);
412 DomToNormalizedNodeParserFactory parserFactory = DomToNormalizedNodeParserFactory.getInstance(
413 XmlUtils.DEFAULT_XML_CODEC_PROVIDER, schemaContext);
416 LOG.debug("{}: Got app config schema: {}", id, dataSchema);
418 NormalizedNode<?, ?> dataNode = bindingContext.parseDataElement(defaultAppConfigElement, dataSchema,
421 LOG.debug("{}: Parsed data node: {}", id, dataNode);
426 private void restartContainer() {
427 BlueprintContainerRestartService restartService = getOSGiService(BlueprintContainerRestartService.class);
428 if(restartService != null) {
429 restartService.restartContainerAndDependents(container.getBundleContext().getBundle());
433 @SuppressWarnings("unchecked")
435 private <T> T getOSGiService(Class<T> serviceInterface) {
437 ServiceReference<T> serviceReference =
438 container.getBundleContext().getServiceReference(serviceInterface);
439 if(serviceReference == null) {
440 LOG.warn("{}: {} reference not found", id, serviceInterface.getSimpleName());
444 T service = (T)container.getService(serviceReference);
445 if(service == null) {
446 // This could happen on shutdown if the service was already unregistered so we log as debug.
447 LOG.debug("{}: {} was not found", id, serviceInterface.getSimpleName());
451 } catch(IllegalStateException e) {
452 // This is thrown if the BundleContext is no longer valid which is possible on shutdown so we
454 LOG.debug("{}: Error obtaining {}", id, serviceInterface.getSimpleName(), e);
461 public void stopTracking() {
462 LOG.debug("{}: In stopTracking", id);
464 stopServiceRecipes();
468 public void destroy(Object instance) {
469 LOG.debug("{}: In destroy", id);
471 if(appConfigChangeListenerReg != null) {
472 appConfigChangeListenerReg.close();
473 appConfigChangeListenerReg = null;
476 stopServiceRecipes();
479 private void stopServiceRecipes() {
480 stopServiceRecipe(dataBrokerServiceRecipe);
481 stopServiceRecipe(bindingCodecServiceRecipe);
482 dataBrokerServiceRecipe = null;
483 bindingCodecServiceRecipe = null;
486 private void stopServiceRecipe(StaticServiceReferenceRecipe recipe) {
493 public String getDependencyDescriptor() {
494 return dependendencyDesc;
498 * Internal base class to abstract binding type-specific behavior.
500 private static abstract class BindingContext {
501 final InstanceIdentifier<DataObject> appConfigPath;
502 final Class<DataObject> appConfigBindingClass;
503 final Class<? extends DataSchemaNode> schemaType;
504 final QName bindingQName;
506 protected BindingContext(Class<DataObject> appConfigBindingClass, InstanceIdentifier<DataObject> appConfigPath,
507 Class<? extends DataSchemaNode> schemaType) {
508 this.appConfigBindingClass = appConfigBindingClass;
509 this.appConfigPath = appConfigPath;
510 this.schemaType = schemaType;
512 bindingQName = BindingReflections.findQName(appConfigBindingClass);
515 abstract NormalizedNode<?, ?> parseDataElement(Element element, DataSchemaNode dataSchema,
516 DomToNormalizedNodeParserFactory parserFactory);
518 abstract NormalizedNode<?, ?> newDefaultNode(DataSchemaNode dataSchema);
522 * BindingContext implementation for a container binding.
524 private static class ContainerBindingContext extends BindingContext {
525 ContainerBindingContext(Class<DataObject> appConfigBindingClass) {
526 super(appConfigBindingClass, InstanceIdentifier.create(appConfigBindingClass), ContainerSchemaNode.class);
530 NormalizedNode<?, ?> newDefaultNode(DataSchemaNode dataSchema) {
531 return ImmutableNodes.containerNode(bindingQName);
535 NormalizedNode<?, ?> parseDataElement(Element element, DataSchemaNode dataSchema,
536 DomToNormalizedNodeParserFactory parserFactory) {
537 return parserFactory.getContainerNodeParser().parse(Collections.singletonList(element),
538 (ContainerSchemaNode)dataSchema);
543 * BindingContext implementation for a list binding.
545 private static class ListBindingContext extends BindingContext {
546 final String appConfigListKeyValue;
548 ListBindingContext(Class<DataObject> appConfigBindingClass, InstanceIdentifier<DataObject> appConfigPath,
549 String appConfigListKeyValue) {
550 super(appConfigBindingClass, appConfigPath, ListSchemaNode.class);
551 this.appConfigListKeyValue = appConfigListKeyValue;
554 @SuppressWarnings({ "rawtypes", "unchecked" })
555 private static ListBindingContext newInstance(Class<DataObject> bindingClass, String listKeyValue)
557 // We assume the yang list key type is string.
558 Identifier keyInstance = (Identifier) bindingClass.getMethod("getKey").getReturnType().
559 getConstructor(String.class).newInstance(listKeyValue);
560 InstanceIdentifier appConfigPath = InstanceIdentifier.builder((Class)bindingClass, keyInstance).build();
561 return new ListBindingContext(bindingClass, appConfigPath, listKeyValue);
565 NormalizedNode<?, ?> newDefaultNode(DataSchemaNode dataSchema) {
566 // We assume there's only one key for the list.
567 List<QName> keys = ((ListSchemaNode)dataSchema).getKeyDefinition();
568 Preconditions.checkArgument(keys.size() == 1, "Expected only 1 key for list %s", appConfigBindingClass);
569 QName listKeyQName = keys.iterator().next();
570 return ImmutableNodes.mapEntryBuilder(bindingQName, listKeyQName, appConfigListKeyValue).build();
574 NormalizedNode<?, ?> parseDataElement(Element element, DataSchemaNode dataSchema,
575 DomToNormalizedNodeParserFactory parserFactory) {
576 return parserFactory.getMapEntryNodeParser().parse(Collections.singletonList(element),
577 (ListSchemaNode)dataSchema);