From: Tom Pantelis Date: Wed, 4 May 2016 02:20:06 +0000 (-0400) Subject: Add clustered-app-config blueprint extension X-Git-Tag: release/boron~189 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?p=controller.git;a=commitdiff_plain;h=1ecaae69e61d9eca89b7521d542ca2ec4885c98d Add clustered-app-config blueprint extension Added an extension that obtains a given binding DataObject type from the data store and provides the DataObject instance to the Blueprint container as a bean that can be injected into oher beans. In addition it registers a ClusteredDataTreeChangeListener to restart the Blueprint container when the data is changed. If no DataObject instance exists, an instance is created with any defaults values populated. Default data may be specified via the "default-config" child element which must contain the XML representation of the yang data, including namespace, wrapped in a CDATA section to prevent the blueprint container from treating it as markup. It is assumed the given "binding-class" is a top-level yang container or list, which seems reasonable. If it's nested then the full path would have to be specified via XML which is doable but not worth the added work if not necessary. We'll see if there's a use case for a nested app config (I doubt it). A list agg config would be used if there's multiple instances of the app/module (eg "openflow-switch-connection-provider-impl" in the openflowplugin). The "list-key-value" must be specified. It is assumed there's only one list key and that it's a string, ie the yang list is keyed by the name of app/module. Change-Id: Ib970b003526d42c2a3db085036174967f055cbba Signed-off-by: Tom Pantelis --- diff --git a/opendaylight/blueprint/pom.xml b/opendaylight/blueprint/pom.xml index 5ccba1b8ef..f06cade19e 100644 --- a/opendaylight/blueprint/pom.xml +++ b/opendaylight/blueprint/pom.xml @@ -27,6 +27,14 @@ org.opendaylight.controller sal-binding-api + + org.opendaylight.controller + sal-core-api + + + org.opendaylight.mdsal + mdsal-binding-dom-codec + org.osgi org.osgi.core diff --git a/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/ComponentProcessor.java b/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/ComponentProcessor.java index a4b63aa427..5daa6e4f38 100644 --- a/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/ComponentProcessor.java +++ b/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/ComponentProcessor.java @@ -36,6 +36,8 @@ import org.slf4j.LoggerFactory; * @author Thomas Pantelis */ public class ComponentProcessor implements ComponentDefinitionRegistryProcessor { + static final String DEFAULT_TYPE_FILTER = "(|(type=default)(!(type=*)))"; + private static final Logger LOG = LoggerFactory.getLogger(ComponentProcessor.class); private static final String CM_PERSISTENT_ID_PROPERTY = "persistentId"; @@ -94,7 +96,7 @@ public class ComponentProcessor implements ComponentDefinitionRegistryProcessor serviceRef.getId(), filter, extFilter); if(Strings.isNullOrEmpty(filter) && Strings.isNullOrEmpty(extFilter)) { - serviceRef.setFilter("(|(type=default)(!(type=*)))"); + serviceRef.setFilter(DEFAULT_TYPE_FILTER); LOG.debug("{}: processServiceReferenceMetadata for {} set filter to {}", logName(), serviceRef.getId(), serviceRef.getFilter()); diff --git a/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/DataStoreAppConfigMetadata.java b/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/DataStoreAppConfigMetadata.java new file mode 100644 index 0000000000..b2ea693ab9 --- /dev/null +++ b/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/DataStoreAppConfigMetadata.java @@ -0,0 +1,580 @@ +/* + * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.controller.blueprint.ext; + +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.util.concurrent.CheckedFuture; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.apache.aries.blueprint.di.AbstractRecipe; +import org.apache.aries.blueprint.di.ExecutionContext; +import org.apache.aries.blueprint.di.Recipe; +import org.apache.aries.blueprint.ext.DependentComponentFactoryMetadata; +import org.apache.aries.blueprint.services.ExtendedBlueprintContainer; +import org.opendaylight.controller.blueprint.BlueprintContainerRestartService; +import org.opendaylight.controller.md.sal.binding.api.ClusteredDataTreeChangeListener; +import org.opendaylight.controller.md.sal.binding.api.DataBroker; +import org.opendaylight.controller.md.sal.binding.api.DataObjectModification; +import org.opendaylight.controller.md.sal.binding.api.DataObjectModification.ModificationType; +import org.opendaylight.controller.md.sal.binding.api.DataTreeIdentifier; +import org.opendaylight.controller.md.sal.binding.api.DataTreeModification; +import org.opendaylight.controller.md.sal.binding.api.ReadOnlyTransaction; +import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException; +import org.opendaylight.controller.sal.core.api.model.SchemaService; +import org.opendaylight.yangtools.binding.data.codec.api.BindingNormalizedNodeSerializer; +import org.opendaylight.yangtools.concepts.ListenerRegistration; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.Identifiable; +import org.opendaylight.yangtools.yang.binding.Identifier; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; +import org.opendaylight.yangtools.yang.binding.util.BindingReflections; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.impl.codec.xml.XmlUtils; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes; +import org.opendaylight.yangtools.yang.data.impl.schema.transform.dom.parser.DomToNormalizedNodeParserFactory; +import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.Module; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.blueprint.container.ComponentDefinitionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; + +/** + * Factory metadata corresponding to the "clustered-app-config" element that obtains an application's + * config data from the data store and provides the binding DataObject instance to the Blueprint container + * as a bean. In addition registers a DataTreeChangeListener to restart the Blueprint container when the + * config data is changed. + * + * @author Thomas Pantelis + */ +public class DataStoreAppConfigMetadata implements DependentComponentFactoryMetadata { + private static final Logger LOG = LoggerFactory.getLogger(DataStoreAppConfigMetadata.class); + + static final String BINDING_CLASS = "binding-class"; + static final String DEFAULT_CONFIG = "default-config"; + static final String LIST_KEY_VALUE = "list-key-value"; + + private final String id; + private final Element defaultAppConfigElement; + private final String appConfigBindingClassName; + private final String appConfigListKeyValue; + private final AtomicBoolean readingInitialAppConfig = new AtomicBoolean(true); + private final AtomicBoolean started = new AtomicBoolean(); + + private volatile BindingContext bindingContext; + private volatile ExtendedBlueprintContainer container; + private volatile StaticServiceReferenceRecipe dataBrokerServiceRecipe; + private volatile StaticServiceReferenceRecipe bindingCodecServiceRecipe; + private volatile ListenerRegistration appConfigChangeListenerReg; + private volatile DataObject currentAppConfig; + private volatile SatisfactionCallback satisfactionCallback; + private volatile String failureMessage; + private volatile String dependendencyDesc; + + // Note: the BindingNormalizedNodeSerializer interface is annotated as deprecated because there's an + // equivalent interface in the mdsal project but the corresponding binding classes in the controller + // project are still used - conversion to the mdsal binding classes hasn't occurred yet. + private volatile BindingNormalizedNodeSerializer bindingSerializer; + + public DataStoreAppConfigMetadata(@Nonnull String id, @Nonnull String appConfigBindingClassName, + @Nullable String appConfigListKeyValue, @Nullable Element defaultAppConfigElement) { + this.id = Preconditions.checkNotNull(id); + this.defaultAppConfigElement = defaultAppConfigElement; + this.appConfigBindingClassName = appConfigBindingClassName; + this.appConfigListKeyValue = appConfigListKeyValue; + } + + @Override + public String getId() { + return id; + } + + @Override + public int getActivation() { + return ACTIVATION_EAGER; + } + + @Override + public List getDependsOn() { + return Collections.emptyList(); + } + + @Override + public boolean isSatisfied() { + return currentAppConfig != null; + } + + @Override + @SuppressWarnings("unchecked") + public void init(ExtendedBlueprintContainer container) { + LOG.debug("{}: In init", id); + + this.container = container; + + Class appConfigBindingClass; + try { + Class bindingClass = container.getBundleContext().getBundle().loadClass(appConfigBindingClassName); + if(!DataObject.class.isAssignableFrom(bindingClass)) { + throw new ComponentDefinitionException(String.format( + "%s: Specified app config binding class %s does not extend %s", + id, appConfigBindingClassName, DataObject.class.getName())); + } + + appConfigBindingClass = (Class) bindingClass; + } catch(ClassNotFoundException e) { + throw new ComponentDefinitionException(String.format("%s: Error loading app config binding class %s", + id, appConfigBindingClassName), e); + } + + if(Identifiable.class.isAssignableFrom(appConfigBindingClass)) { + // The binding class corresponds to a yang list. + if(Strings.isNullOrEmpty(appConfigListKeyValue)) { + throw new ComponentDefinitionException(String.format( + "%s: App config binding class %s represents a yang list therefore \"%s\" must be specified", + id, appConfigBindingClassName, LIST_KEY_VALUE)); + } + + try { + bindingContext = ListBindingContext.newInstance(appConfigBindingClass, appConfigListKeyValue); + } catch(Exception e) { + throw new ComponentDefinitionException(String.format( + "%s: Error initializing for app config list binding class %s", + id, appConfigBindingClassName), e); + } + + } else { + bindingContext = new ContainerBindingContext(appConfigBindingClass); + } + } + + @Override + public Object create() throws ComponentDefinitionException { + LOG.debug("{}: In create - currentAppConfig: {}", id, currentAppConfig); + + if(failureMessage != null) { + throw new ComponentDefinitionException(failureMessage); + } + + // The following code is a bit odd so requires some explanation. A little background... If a bean + // is a prototype then the corresponding Recipe create method does not register the bean as created + // with the BlueprintRepository and thus the destroy method isn't called on container destroy. We + // rely on destroy being called to close our DTCL registration. Unfortunately the default setting + // for the prototype flag in AbstractRecipe is true and the DependentComponentFactoryRecipe, which + // is created for DependentComponentFactoryMetadata types of which we are one, doesn't have a way for + // us to indicate the prototype state via our metadata. + // + // The ExecutionContext is actually backed by the BlueprintRepository so we access it here to call + // the removePartialObject method which removes any partially created instance, which does not apply + // in our case, and also has the side effect of registering our bean as created as if it wasn't a + // prototype. We also obtain our corresponding Recipe instance and clear the prototype flag. This + // doesn't look to be necessary but is done so for completeness. Better late than never. Note we have + // to do this here rather than in startTracking b/c the ExecutionContext is not available yet at that + // point. + // + // Now the stopTracking method is called on container destroy but startTracking/stopTracking can also + // be called multiple times during the container creation process for Satisfiable recipes as bean + // processors may modify the metadata which could affect how dependencies are satisfied. An example of + // this is with service references where the OSGi filter metadata can be modified by bean processors + // after the initial service dependency is satisfied. However we don't have any metadata that could + // be modified by a bean processor and we don't want to register/unregister our DTCL multiple times + // so we only process startTracking once and close the DTCL registration once on container destroy. + ExecutionContext executionContext = ExecutionContext.Holder.getContext(); + executionContext.removePartialObject(id); + + Recipe myRecipe = executionContext.getRecipe(id); + if(myRecipe instanceof AbstractRecipe) { + LOG.debug("{}: setPrototype to false", id); + ((AbstractRecipe)myRecipe).setPrototype(false); + } else { + LOG.warn("{}: Recipe is null or not an AbstractRecipe", id); + } + + return currentAppConfig; + } + + @Override + public void startTracking(final SatisfactionCallback satisfactionCallback) { + if(!started.compareAndSet(false, true)) { + return; + } + + LOG.debug("{}: In startTracking", id); + + this.satisfactionCallback = satisfactionCallback; + + // First get the BindingNormalizedNodeSerializer OSGi service. This will be used to create a default + // instance of the app config binding class, if necessary. + + bindingCodecServiceRecipe = new StaticServiceReferenceRecipe(id + "-binding-codec", container, + BindingNormalizedNodeSerializer.class.getName()); + dependendencyDesc = bindingCodecServiceRecipe.getOsgiFilter(); + + bindingCodecServiceRecipe.startTracking(service -> { + bindingSerializer = (BindingNormalizedNodeSerializer)service; + retrieveDataBrokerService(); + }); + } + + private void retrieveDataBrokerService() { + LOG.debug("{}: In retrieveDataBrokerService", id); + + // Get the binding DataBroker OSGi service. + + dataBrokerServiceRecipe = new StaticServiceReferenceRecipe(id + "-data-broker", container, + DataBroker.class.getName()); + dependendencyDesc = dataBrokerServiceRecipe.getOsgiFilter(); + + dataBrokerServiceRecipe.startTracking(service -> retrieveInitialAppConfig((DataBroker)service)); + + } + + private void retrieveInitialAppConfig(DataBroker dataBroker) { + LOG.debug("{}: Got DataBroker instance - reading app config {}", id, bindingContext.appConfigPath); + + dependendencyDesc = "Initial app config " + bindingContext.appConfigBindingClass.getSimpleName(); + + // We register a DTCL to get updates and also read the app config data from the data store. If + // the app config data is present then both the read and initial DTCN update will return it. If the + // the data isn't present, we won't get an initial DTCN update so the read will indicate the data + // isn't present. + + DataTreeIdentifier dataTreeId = new DataTreeIdentifier<>(LogicalDatastoreType.CONFIGURATION, + bindingContext.appConfigPath); + appConfigChangeListenerReg = dataBroker.registerDataTreeChangeListener(dataTreeId, + new ClusteredDataTreeChangeListener() { + @Override + public void onDataTreeChanged(Collection> changes) { + onAppConfigChanged(changes); + } + }); + + readInitialAppConfig(dataBroker); + } + + private void readInitialAppConfig(final DataBroker dataBroker) { + + final ReadOnlyTransaction readOnlyTx = dataBroker.newReadOnlyTransaction(); + CheckedFuture, ReadFailedException> future = readOnlyTx.read( + LogicalDatastoreType.CONFIGURATION, bindingContext.appConfigPath); + Futures.addCallback(future, new FutureCallback>() { + @Override + public void onSuccess(Optional possibleAppConfig) { + LOG.debug("{}: Read of app config {} succeeded: {}", id, bindingContext.appConfigBindingClass.getName(), + possibleAppConfig); + + readOnlyTx.close(); + setInitialAppConfig(possibleAppConfig); + } + + @Override + public void onFailure(Throwable t) { + readOnlyTx.close(); + + // We may have gotten the app config via the data tree change listener so only retry if not. + if(readingInitialAppConfig.get()) { + LOG.warn("{}: Read of app config {} failed - retrying", id, + bindingContext.appConfigBindingClass.getName(), t); + + readInitialAppConfig(dataBroker); + } + } + }); + } + + private void onAppConfigChanged(Collection> changes) { + for(DataTreeModification change: changes) { + DataObjectModification changeRoot = change.getRootNode(); + ModificationType type = changeRoot.getModificationType(); + + LOG.debug("{}: onAppConfigChanged: {}, {}", id, type, change.getRootPath()); + + if(type == ModificationType.SUBTREE_MODIFIED || type == ModificationType.WRITE) { + DataObject newAppConfig = changeRoot.getDataAfter(); + + LOG.debug("New app config instance: {}, previous: {}", newAppConfig, currentAppConfig); + + if(!setInitialAppConfig(Optional.of(newAppConfig)) && + !Objects.equals(currentAppConfig, newAppConfig)) { + LOG.debug("App config was updated - scheduling container for restart"); + + restartContainer(); + } + } else if(type == ModificationType.DELETE) { + LOG.debug("App config was deleted - scheduling container for restart"); + + restartContainer(); + } + } + } + + private boolean setInitialAppConfig(Optional possibleAppConfig) { + boolean result = readingInitialAppConfig.compareAndSet(true, false); + if(result) { + DataObject localAppConfig; + if(possibleAppConfig.isPresent()) { + localAppConfig = possibleAppConfig.get(); + } else { + // No app config data is present so create an empty instance via the bindingSerializer service. + // This will also return default values for leafs that haven't been explicitly set. + localAppConfig = createDefaultInstance(); + } + + LOG.debug("{}: Setting currentAppConfig instance: {}", id, localAppConfig); + + // Now publish the app config instance to the volatile field and notify the callback to let the + // container know our dependency is now satisfied. + currentAppConfig = localAppConfig; + satisfactionCallback.notifyChanged(); + } + + return result; + } + + private DataObject createDefaultInstance() { + YangInstanceIdentifier yangPath = bindingSerializer.toYangInstanceIdentifier(bindingContext.appConfigPath); + + LOG.debug("{}: Creating app config instance from path {}, Qname: {}", id, yangPath, bindingContext.bindingQName); + + SchemaService schemaService = getOSGiService(SchemaService.class); + if(schemaService == null) { + failureMessage = String.format("%s: Could not obtain the SchemaService OSGi service", id); + return null; + } + + SchemaContext schemaContext = schemaService.getGlobalContext(); + + Module module = schemaContext.findModuleByNamespaceAndRevision(bindingContext.bindingQName.getNamespace(), + bindingContext.bindingQName.getRevision()); + if(module == null) { + failureMessage = String.format("%s: Could not obtain the module schema for namespace %s, revision %s", + id, bindingContext.bindingQName.getNamespace(), bindingContext.bindingQName.getRevision()); + return null; + } + + DataSchemaNode dataSchema = module.getDataChildByName(bindingContext.bindingQName); + if(dataSchema == null) { + failureMessage = String.format("%s: Could not obtain the schema for %s", id, bindingContext.bindingQName); + return null; + } + + if(!bindingContext.schemaType.isAssignableFrom(dataSchema.getClass())) { + failureMessage = String.format("%s: Expected schema type %s for %s but actual type is %s", id, + bindingContext.schemaType, bindingContext.bindingQName, dataSchema.getClass()); + return null; + } + + NormalizedNode dataNode = parsePossibleDefaultAppConfigElement(schemaContext, dataSchema); + if(dataNode == null) { + dataNode = bindingContext.newDefaultNode(dataSchema); + } + + DataObject appConfig = bindingSerializer.fromNormalizedNode(yangPath, dataNode).getValue(); + + if(appConfig == null) { + // This shouldn't happen but need to handle it in case... + failureMessage = String.format("%s: Could not create instance for app config binding %s", + id, bindingContext.appConfigBindingClass); + } + + return appConfig; + } + + @Nullable + private NormalizedNode parsePossibleDefaultAppConfigElement(SchemaContext schemaContext, + DataSchemaNode dataSchema) { + if(defaultAppConfigElement == null) { + return null; + } + + LOG.debug("{}: parsePossibleDefaultAppConfigElement for {}", id, bindingContext.bindingQName); + + DomToNormalizedNodeParserFactory parserFactory = DomToNormalizedNodeParserFactory.getInstance( + XmlUtils.DEFAULT_XML_CODEC_PROVIDER, schemaContext); + + + LOG.debug("{}: Got app config schema: {}", id, dataSchema); + + NormalizedNode dataNode = bindingContext.parseDataElement(defaultAppConfigElement, dataSchema, + parserFactory); + + LOG.debug("{}: Parsed data node: {}", id, dataNode); + + return dataNode; + } + + private void restartContainer() { + BlueprintContainerRestartService restartService = getOSGiService(BlueprintContainerRestartService.class); + if(restartService != null) { + restartService.restartContainerAndDependents(container.getBundleContext().getBundle()); + } + } + + @SuppressWarnings("unchecked") + @Nullable + private T getOSGiService(Class serviceInterface) { + try { + ServiceReference serviceReference = + container.getBundleContext().getServiceReference(serviceInterface); + if(serviceReference == null) { + LOG.warn("{}: {} reference not found", id, serviceInterface.getSimpleName()); + return null; + } + + T service = (T)container.getService(serviceReference); + if(service == null) { + // This could happen on shutdown if the service was already unregistered so we log as debug. + LOG.debug("{}: {} was not found", id, serviceInterface.getSimpleName()); + } + + return service; + } catch(IllegalStateException e) { + // This is thrown if the BundleContext is no longer valid which is possible on shutdown so we + // log as debug. + LOG.debug("{}: Error obtaining {}", id, serviceInterface.getSimpleName(), e); + } + + return null; + } + + @Override + public void stopTracking() { + LOG.debug("{}: In stopTracking", id); + + stopServiceRecipes(); + } + + @Override + public void destroy(Object instance) { + LOG.debug("{}: In destroy", id); + + if(appConfigChangeListenerReg != null) { + appConfigChangeListenerReg.close(); + appConfigChangeListenerReg = null; + } + + stopServiceRecipes(); + } + + private void stopServiceRecipes() { + stopServiceRecipe(dataBrokerServiceRecipe); + stopServiceRecipe(bindingCodecServiceRecipe); + dataBrokerServiceRecipe = null; + bindingCodecServiceRecipe = null; + } + + private void stopServiceRecipe(StaticServiceReferenceRecipe recipe) { + if(recipe != null) { + recipe.stop(); + } + } + + @Override + public String getDependencyDescriptor() { + return dependendencyDesc; + } + + /** + * Internal base class to abstract binding type-specific behavior. + */ + private static abstract class BindingContext { + final InstanceIdentifier appConfigPath; + final Class appConfigBindingClass; + final Class schemaType; + final QName bindingQName; + + protected BindingContext(Class appConfigBindingClass, InstanceIdentifier appConfigPath, + Class schemaType) { + this.appConfigBindingClass = appConfigBindingClass; + this.appConfigPath = appConfigPath; + this.schemaType = schemaType; + + bindingQName = BindingReflections.findQName(appConfigBindingClass); + } + + abstract NormalizedNode parseDataElement(Element element, DataSchemaNode dataSchema, + DomToNormalizedNodeParserFactory parserFactory); + + abstract NormalizedNode newDefaultNode(DataSchemaNode dataSchema); + } + + /** + * BindingContext implementation for a container binding. + */ + private static class ContainerBindingContext extends BindingContext { + ContainerBindingContext(Class appConfigBindingClass) { + super(appConfigBindingClass, InstanceIdentifier.create(appConfigBindingClass), ContainerSchemaNode.class); + } + + @Override + NormalizedNode newDefaultNode(DataSchemaNode dataSchema) { + return ImmutableNodes.containerNode(bindingQName); + } + + @Override + NormalizedNode parseDataElement(Element element, DataSchemaNode dataSchema, + DomToNormalizedNodeParserFactory parserFactory) { + return parserFactory.getContainerNodeParser().parse(Collections.singletonList(element), + (ContainerSchemaNode)dataSchema); + } + } + + /** + * BindingContext implementation for a list binding. + */ + private static class ListBindingContext extends BindingContext { + final String appConfigListKeyValue; + + ListBindingContext(Class appConfigBindingClass, InstanceIdentifier appConfigPath, + String appConfigListKeyValue) { + super(appConfigBindingClass, appConfigPath, ListSchemaNode.class); + this.appConfigListKeyValue = appConfigListKeyValue; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static ListBindingContext newInstance(Class bindingClass, String listKeyValue) + throws Exception { + // We assume the yang list key type is string. + Identifier keyInstance = (Identifier) bindingClass.getMethod("getKey").getReturnType(). + getConstructor(String.class).newInstance(listKeyValue); + InstanceIdentifier appConfigPath = InstanceIdentifier.builder((Class)bindingClass, keyInstance).build(); + return new ListBindingContext(bindingClass, appConfigPath, listKeyValue); + } + + @Override + NormalizedNode newDefaultNode(DataSchemaNode dataSchema) { + // We assume there's only one key for the list. + List keys = ((ListSchemaNode)dataSchema).getKeyDefinition(); + Preconditions.checkArgument(keys.size() == 1, "Expected only 1 key for list %s", appConfigBindingClass); + QName listKeyQName = keys.iterator().next(); + return ImmutableNodes.mapEntryBuilder(bindingQName, listKeyQName, appConfigListKeyValue).build(); + } + + @Override + NormalizedNode parseDataElement(Element element, DataSchemaNode dataSchema, + DomToNormalizedNodeParserFactory parserFactory) { + return parserFactory.getMapEntryNodeParser().parse(Collections.singletonList(element), + (ListSchemaNode)dataSchema); + } + } +} diff --git a/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/MandatoryServiceReferenceMetadata.java b/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/MandatoryServiceReferenceMetadata.java new file mode 100644 index 0000000000..52cfc9feb6 --- /dev/null +++ b/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/MandatoryServiceReferenceMetadata.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.controller.blueprint.ext; + +import com.google.common.base.Preconditions; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.osgi.service.blueprint.reflect.ReferenceListener; +import org.osgi.service.blueprint.reflect.ServiceReferenceMetadata; + +/** + * A ServiceReferenceMetadata implementation for a mandatory OSGi service. + * + * @author Thomas Pantelis + */ +class MandatoryServiceReferenceMetadata implements ServiceReferenceMetadata { + private final String interfaceClass; + private final String id; + + MandatoryServiceReferenceMetadata(String id, String interfaceClass) { + this.id = Preconditions.checkNotNull(id); + this.interfaceClass = interfaceClass; + } + + @Override + public String getId() { + return id; + } + + @Override + public int getActivation() { + return ACTIVATION_EAGER; + } + + @Override + public List getDependsOn() { + return Collections.emptyList(); + } + + @Override + public int getAvailability() { + return AVAILABILITY_MANDATORY; + } + + @Override + public String getInterface() { + return interfaceClass; + } + + @Override + public String getComponentName() { + return null; + } + + @Override + public String getFilter() { + return ComponentProcessor.DEFAULT_TYPE_FILTER; + } + + @Override + public Collection getReferenceListeners() { + return Collections.emptyList(); + } +} diff --git a/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/OpendaylightNamespaceHandler.java b/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/OpendaylightNamespaceHandler.java index 80417e8bd0..7d1bbf9509 100644 --- a/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/OpendaylightNamespaceHandler.java +++ b/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/OpendaylightNamespaceHandler.java @@ -8,9 +8,11 @@ package org.opendaylight.controller.blueprint.ext; import com.google.common.base.Strings; +import java.io.StringReader; import java.net.URL; import java.util.Collections; import java.util.Set; +import javax.xml.parsers.DocumentBuilderFactory; import org.apache.aries.blueprint.ComponentDefinitionRegistry; import org.apache.aries.blueprint.NamespaceHandler; import org.apache.aries.blueprint.ParserContext; @@ -38,6 +40,8 @@ import org.slf4j.LoggerFactory; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; /** * The NamespaceHandler for Opendaylight blueprint extensions. @@ -54,6 +58,7 @@ public class OpendaylightNamespaceHandler implements NamespaceHandler { private static final String COMPONENT_PROCESSOR_NAME = ComponentProcessor.class.getName(); private static final String RESTART_DEPENDENTS_ON_UPDATES = "restart-dependents-on-updates"; private static final String USE_DEFAULT_FOR_REFERENCE_TYPES = "use-default-for-reference-types"; + private static final String CLUSTERED_APP_CONFIG = "clustered-app-config"; private static final String TYPE_ATTR = "type"; private static final String INTERFACE = "interface"; private static final String REF_ATTR = "ref"; @@ -89,6 +94,8 @@ public class OpendaylightNamespaceHandler implements NamespaceHandler { return parseRpcService(element, context); } else if (nodeNameEquals(element, NotificationListenerBean.NOTIFICATION_LISTENER)) { return parseNotificationListener(element, context); + } else if (nodeNameEquals(element, CLUSTERED_APP_CONFIG)) { + return parseClusteredAppConfig(element, context); } throw new ComponentDefinitionException("Unsupported standalone element: " + element.getNodeName()); @@ -300,7 +307,7 @@ public class OpendaylightNamespaceHandler implements NamespaceHandler { return metadata; } - private void registerNotificationServiceRefBean(ParserContext context) { + private void registerNotificationServiceRefBean(ParserContext context) { ComponentDefinitionRegistry registry = context.getComponentDefinitionRegistry(); if(registry.getComponentDefinition(NOTIFICATION_SERVICE_NAME) == null) { MutableReferenceMetadata metadata = createServiceRef(context, NotificationService.class, null); @@ -309,6 +316,52 @@ public class OpendaylightNamespaceHandler implements NamespaceHandler { } } + private Metadata parseClusteredAppConfig(Element element, ParserContext context) { + LOG.debug("parseClusteredAppConfig"); + + // Find the default-config child element representing the default app config XML, if present. + Element defaultConfigElement = null; + NodeList children = element.getChildNodes(); + for(int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if(nodeNameEquals(child, DataStoreAppConfigMetadata.DEFAULT_CONFIG)) { + defaultConfigElement = (Element) child; + break; + } + } + + Element defaultAppConfigElement = null; + if(defaultConfigElement != null) { + // Find the CDATA element containing the default app config XML. + children = defaultConfigElement.getChildNodes(); + for(int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if(child.getNodeType() == Node.CDATA_SECTION_NODE) { + defaultAppConfigElement = parseXML(DataStoreAppConfigMetadata.DEFAULT_CONFIG, child.getTextContent()); + break; + } + } + } + + return new DataStoreAppConfigMetadata(getId(context, element), element.getAttribute( + DataStoreAppConfigMetadata.BINDING_CLASS), element.getAttribute( + DataStoreAppConfigMetadata.LIST_KEY_VALUE), defaultAppConfigElement); + } + + private Element parseXML(String name, String xml) { + DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); + builderFactory.setNamespaceAware(true); + builderFactory.setCoalescing(true); + builderFactory.setIgnoringElementContentWhitespace(true); + builderFactory.setIgnoringComments(true); + + try { + return builderFactory.newDocumentBuilder().parse(new InputSource(new StringReader(xml))).getDocumentElement(); + } catch(Exception e) { + throw new ComponentDefinitionException(String.format("Error %s parsing XML: %s", name, xml)); + } + } + private static ValueMetadata createValue(ParserContext context, String value) { MutableValueMetadata m = context.createMetadata(MutableValueMetadata.class); m.setStringValue(value); diff --git a/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/StaticServiceReferenceRecipe.java b/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/StaticServiceReferenceRecipe.java new file mode 100644 index 0000000000..0466aab6c2 --- /dev/null +++ b/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/StaticServiceReferenceRecipe.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.controller.blueprint.ext; + +import com.google.common.base.Preconditions; +import java.util.Collections; +import java.util.function.Consumer; +import org.apache.aries.blueprint.container.AbstractServiceReferenceRecipe; +import org.apache.aries.blueprint.container.SatisfiableRecipe; +import org.apache.aries.blueprint.services.ExtendedBlueprintContainer; +import org.osgi.framework.ServiceReference; +import org.osgi.service.blueprint.container.ComponentDefinitionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A Blueprint bean recipe for a static OSGi service reference, meaning it obtains the service instance once + * and doesn't react to service removal. In addition the returned object is the actual service instance and + * not a proxy. + * + * @author Thomas Pantelis + */ +class StaticServiceReferenceRecipe extends AbstractServiceReferenceRecipe { + private static final Logger LOG = LoggerFactory.getLogger(StaticServiceReferenceRecipe.class); + + private static final SatisfactionListener NOOP_LISTENER = new SatisfactionListener() { + @Override + public void notifySatisfaction(SatisfiableRecipe satisfiable) { + + } + }; + + private volatile ServiceReference trackedServiceReference; + private volatile Object trackedService; + private Consumer serviceSatisfiedCallback; + + StaticServiceReferenceRecipe(String name, ExtendedBlueprintContainer blueprintContainer, + String interfaceClass) { + super(name, blueprintContainer, new MandatoryServiceReferenceMetadata(name, interfaceClass), null, null, + Collections.emptyList()); + } + + void startTracking(Consumer serviceSatisfiedCallback) { + this.serviceSatisfiedCallback = serviceSatisfiedCallback; + super.start(NOOP_LISTENER); + } + + @SuppressWarnings("rawtypes") + @Override + protected void track(ServiceReference reference) { + retrack(); + } + + @SuppressWarnings("rawtypes") + @Override + protected void untrack(ServiceReference reference) { + LOG.debug("{}: In untrack {}", getName(), reference); + + if(trackedServiceReference == reference) { + LOG.debug("{}: Current reference has been untracked", getName(), trackedServiceReference); + } + } + + @Override + protected void retrack() { + LOG.debug("{}: In retrack", getName()); + + if(trackedServiceReference == null) { + trackedServiceReference = getBestServiceReference(); + + LOG.debug("{}: getBestServiceReference: {}", getName(), trackedServiceReference); + + if(trackedServiceReference != null && serviceSatisfiedCallback != null) { + serviceSatisfiedCallback.accept(internalCreate()); + } + } + } + + @Override + protected void doStop() { + LOG.debug("{}: In doStop", getName()); + + if(trackedServiceReference != null && trackedService != null) { + try { + getBundleContextForServiceLookup().ungetService(trackedServiceReference); + } catch(IllegalStateException e) { + // In case the service no longer exists, ignore. + } + + trackedServiceReference = null; + trackedService = null; + } + } + + @Override + protected Object internalCreate() throws ComponentDefinitionException { + ServiceReference localTrackedServiceReference = trackedServiceReference; + + LOG.debug("{}: In internalCreate: trackedServiceReference: {}", getName(), localTrackedServiceReference); + + // being paranoid - internalCreate should only get called once + if(trackedService != null) { + return trackedService; + } + + Preconditions.checkNotNull(localTrackedServiceReference, "trackedServiceReference is null"); + + trackedService = getServiceSecurely(localTrackedServiceReference); + + LOG.debug("{}: Returning service instance: {}", getName(), trackedService); + + Preconditions.checkNotNull(trackedService, "getService() returned null for %s", localTrackedServiceReference); + + return trackedService; + } +} diff --git a/opendaylight/blueprint/src/main/resources/opendaylight-blueprint-ext-1.0.0.xsd b/opendaylight/blueprint/src/main/resources/opendaylight-blueprint-ext-1.0.0.xsd index 0c6eaaa2f0..6ae63956a0 100644 --- a/opendaylight/blueprint/src/main/resources/opendaylight-blueprint-ext-1.0.0.xsd +++ b/opendaylight/blueprint/src/main/resources/opendaylight-blueprint-ext-1.0.0.xsd @@ -33,4 +33,13 @@ + + + + + + + + + \ No newline at end of file