From: Michael Vorburger Date: Tue, 25 Apr 2017 22:32:15 +0000 (+0200) Subject: Bug 8303: BP odl:clustered-app-config initial/*-config.xml testability X-Git-Tag: release/nitrogen~302 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?p=controller.git;a=commitdiff_plain;h=821944277049bbb3949021626844ef7a80101f70 Bug 8303: BP odl:clustered-app-config initial/*-config.xml testability DataStoreAppConfigDefaultXMLReaderTest illustrates usage. Change-Id: I342fca4583c90802238e63262871e33b4b713438 Signed-off-by: Michael Vorburger --- diff --git a/opendaylight/blueprint/pom.xml b/opendaylight/blueprint/pom.xml index 2a264731a2..b0e0f5eaa6 100644 --- a/opendaylight/blueprint/pom.xml +++ b/opendaylight/blueprint/pom.xml @@ -94,6 +94,27 @@ junit test + + com.google.truth + truth + test + + + org.opendaylight.controller + sal-test-model + test + + + org.opendaylight.controller + sal-binding-broker-impl + test-jar + test + + + org.opendaylight.controller + sal-binding-broker-impl + test + diff --git a/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/BindingContext.java b/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/BindingContext.java new file mode 100644 index 0000000000..91899301aa --- /dev/null +++ b/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/BindingContext.java @@ -0,0 +1,144 @@ +/* + * 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 com.google.common.base.Strings; +import java.lang.reflect.InvocationTargetException; +import java.util.Collections; +import java.util.List; +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.schema.NormalizedNode; +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.osgi.service.blueprint.container.ComponentDefinitionException; +import org.w3c.dom.Element; + +/** + * Base class to abstract binding type-specific behavior. + * + * @author Thomas Pantelis (originally; re-factored by Michael Vorburger.ch) + */ +public abstract class BindingContext { + + public static BindingContext create(String logName, Class klass, + String appConfigListKeyValue) { + if (Identifiable.class.isAssignableFrom(klass)) { + // 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", + logName, klass.getName(), DataStoreAppConfigMetadata.LIST_KEY_VALUE)); + } + + try { + return ListBindingContext.newInstance(klass, appConfigListKeyValue); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException | NoSuchMethodException | SecurityException e) { + throw new ComponentDefinitionException(String.format( + "%s: Error initializing for app config list binding class %s", + logName, klass.getName()), e); + } + + } else { + return new ContainerBindingContext(klass); + } + } + + public final InstanceIdentifier appConfigPath; + public final Class appConfigBindingClass; + public final Class schemaType; + public final QName bindingQName; + + private BindingContext(final Class appConfigBindingClass, + final InstanceIdentifier appConfigPath, final Class schemaType) { + this.appConfigBindingClass = appConfigBindingClass; + this.appConfigPath = appConfigPath; + this.schemaType = schemaType; + + bindingQName = BindingReflections.findQName(appConfigBindingClass); + } + + public abstract NormalizedNode parseDataElement(Element element, DataSchemaNode dataSchema, + DomToNormalizedNodeParserFactory parserFactory); + + public abstract NormalizedNode newDefaultNode(DataSchemaNode dataSchema); + + /** + * BindingContext implementation for a container binding. + */ + private static class ContainerBindingContext extends BindingContext { + @SuppressWarnings("unchecked") + ContainerBindingContext(final Class appConfigBindingClass) { + super((Class) appConfigBindingClass, + InstanceIdentifier.create((Class) appConfigBindingClass), ContainerSchemaNode.class); + } + + @Override + public NormalizedNode newDefaultNode(final DataSchemaNode dataSchema) { + return ImmutableNodes.containerNode(bindingQName); + } + + @Override + public NormalizedNode parseDataElement(final Element element, final DataSchemaNode dataSchema, + final 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; + + @SuppressWarnings("unchecked") + ListBindingContext(final Class appConfigBindingClass, + final InstanceIdentifier appConfigPath, final String appConfigListKeyValue) { + super((Class) appConfigBindingClass, (InstanceIdentifier) appConfigPath, + ListSchemaNode.class); + this.appConfigListKeyValue = appConfigListKeyValue; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static ListBindingContext newInstance(final Class bindingClass, + final String listKeyValue) throws InstantiationException, IllegalAccessException, + IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { + // 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 + public NormalizedNode newDefaultNode(final 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.get(0); + return ImmutableNodes.mapEntryBuilder(bindingQName, listKeyQName, appConfigListKeyValue).build(); + } + + @Override + public NormalizedNode parseDataElement(final Element element, final DataSchemaNode dataSchema, + final DomToNormalizedNodeParserFactory parserFactory) { + return parserFactory.getMapEntryNodeParser().parse(Collections.singletonList(element), + (ListSchemaNode)dataSchema); + } + } +} diff --git a/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/ConfigXMLReaderException.java b/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/ConfigXMLReaderException.java new file mode 100644 index 0000000000..91da124386 --- /dev/null +++ b/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/ConfigXMLReaderException.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2017 Red Hat, 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; + +/** + * Exception thrown by {@link DataStoreAppConfigDefaultXMLReader}. + * + * @author Michael Vorburger.ch + */ +public class ConfigXMLReaderException extends Exception { + private static final long serialVersionUID = 1L; + + public ConfigXMLReaderException(String message) { + super(message); + } + + public ConfigXMLReaderException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/DataStoreAppConfigDefaultXMLReader.java b/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/DataStoreAppConfigDefaultXMLReader.java new file mode 100644 index 0000000000..57f3abce85 --- /dev/null +++ b/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/DataStoreAppConfigDefaultXMLReader.java @@ -0,0 +1,201 @@ +/* + * 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.Strings; +import com.google.common.io.Resources; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import org.opendaylight.controller.sal.core.api.model.SchemaService; +import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeSerializer; +import org.opendaylight.yangtools.util.xml.UntrustedXML; +import org.opendaylight.yangtools.yang.binding.DataObject; +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.transform.dom.parser.DomToNormalizedNodeParserFactory; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.Module; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +/** + * DataObject XML file reader used by {@link DataStoreAppConfigMetadata}. + * Available as a standalone class to make it easy to write unit tests which can + * catch malformed default "clustered-app-conf" config data XML files in + * downstream projects. + * + * @author Thomas Pantelis (originally; re-factored by Michael Vorburger.ch) + */ +public class DataStoreAppConfigDefaultXMLReader { + + private static final Logger LOG = LoggerFactory.getLogger(DataStoreAppConfigDefaultXMLReader.class); + + private final String logName; + private final String defaultAppConfigFileName; + private final SchemaService schemaService; + private final BindingNormalizedNodeSerializer bindingSerializer; + private final BindingContext bindingContext; + private final ConfigURLProvider inputStreamProvider; + + @FunctionalInterface + public interface FallbackConfigProvider { + NormalizedNode get(SchemaContext schemaContext, DataSchemaNode dataSchema); + } + + @FunctionalInterface + public interface ConfigURLProvider { + Optional getURL(String appConfigFileName) throws IOException; + } + + public DataStoreAppConfigDefaultXMLReader( + String logName, + String defaultAppConfigFileName, + SchemaService schemaService, + BindingNormalizedNodeSerializer bindingSerializer, + BindingContext bindingContext, + ConfigURLProvider inputStreamProvider) { + + this.logName = logName; + this.defaultAppConfigFileName = defaultAppConfigFileName; + this.schemaService = schemaService; + this.bindingSerializer = bindingSerializer; + this.bindingContext = bindingContext; + this.inputStreamProvider = inputStreamProvider; + } + + public DataStoreAppConfigDefaultXMLReader( + Class testClass, + String defaultAppConfigFileName, + SchemaService schemaService, + BindingNormalizedNodeSerializer bindingSerializer, + Class klass) { + this(testClass.getName(), defaultAppConfigFileName, schemaService, bindingSerializer, + BindingContext.create(testClass.getName(), klass, null), + appConfigFileName -> Optional.of(getURL(testClass, defaultAppConfigFileName))); + } + + private static URL getURL(Class testClass, String defaultAppConfigFileName) { + return Resources.getResource(testClass, defaultAppConfigFileName); + } + + public T createDefaultInstance() throws ConfigXMLReaderException { + return createDefaultInstance((schemaContext, dataSchema) -> { + throw new IllegalArgumentException("Failed to read XML " + + "(not creating model from defaults as runtime would, for better clarity in tests)"); + }); + } + + @SuppressWarnings("unchecked") + public T createDefaultInstance(FallbackConfigProvider fallback) throws ConfigXMLReaderException { + YangInstanceIdentifier yangPath = bindingSerializer.toYangInstanceIdentifier(bindingContext.appConfigPath); + + LOG.debug("{}: Creating app config instance from path {}, Qname: {}", logName, yangPath, + bindingContext.bindingQName); + + if (schemaService == null) { + throw new ConfigXMLReaderException( + String.format("%s: Could not obtain the SchemaService OSGi service", logName)); + } + + SchemaContext schemaContext = schemaService.getGlobalContext(); + + Module module = schemaContext.findModuleByNamespaceAndRevision(bindingContext.bindingQName.getNamespace(), + bindingContext.bindingQName.getRevision()); + if (module == null) { + throw new ConfigXMLReaderException( + String.format("%s: Could not obtain the module schema for namespace %s, revision %s", + logName, bindingContext.bindingQName.getNamespace(), bindingContext.bindingQName.getRevision())); + } + + DataSchemaNode dataSchema = module.getDataChildByName(bindingContext.bindingQName); + if (dataSchema == null) { + throw new ConfigXMLReaderException( + String.format("%s: Could not obtain the schema for %s", logName, + bindingContext.bindingQName)); + } + + if (!bindingContext.schemaType.isAssignableFrom(dataSchema.getClass())) { + throw new ConfigXMLReaderException( + String.format("%s: Expected schema type %s for %s but actual type is %s", logName, + bindingContext.schemaType, bindingContext.bindingQName, dataSchema.getClass())); + } + + NormalizedNode dataNode = parsePossibleDefaultAppConfigXMLFile(schemaContext, dataSchema); + if (dataNode == null) { + dataNode = fallback.get(schemaService.getGlobalContext(), dataSchema); + } + + DataObject appConfig = bindingSerializer.fromNormalizedNode(yangPath, dataNode).getValue(); + if (appConfig == null) { + // This shouldn't happen but need to handle it in case... + throw new ConfigXMLReaderException( + String.format("%s: Could not create instance for app config binding %s", + logName, bindingContext.appConfigBindingClass)); + } else { + return (T) appConfig; + } + } + + private NormalizedNode parsePossibleDefaultAppConfigXMLFile(final SchemaContext schemaContext, + final DataSchemaNode dataSchema) throws ConfigXMLReaderException { + + String appConfigFileName = defaultAppConfigFileName; + if (Strings.isNullOrEmpty(appConfigFileName)) { + String moduleName = findYangModuleName(bindingContext.bindingQName, schemaContext); + appConfigFileName = moduleName + "_" + bindingContext.bindingQName.getLocalName() + ".xml"; + } + + DomToNormalizedNodeParserFactory parserFactory = DomToNormalizedNodeParserFactory.getInstance( + XmlUtils.DEFAULT_XML_CODEC_PROVIDER, schemaContext); + + Optional optionalURL; + try { + optionalURL = inputStreamProvider.getURL(appConfigFileName); + } catch (IOException e) { + String msg = String.format("%s: Could not getURL()", logName); + LOG.error(msg, e); + throw new ConfigXMLReaderException(msg, e); + } + if (!optionalURL.isPresent()) { + return null; + } + URL url = optionalURL.get(); + try (InputStream is = url.openStream()) { + Document root = UntrustedXML.newDocumentBuilder().parse(is); + NormalizedNode dataNode = bindingContext.parseDataElement(root.getDocumentElement(), dataSchema, + parserFactory); + + LOG.debug("{}: Parsed data node: {}", logName, dataNode); + + return dataNode; + } catch (SAXException | IOException e) { + String msg = String.format("%s: Could not read/parse app config %s", logName, url); + LOG.error(msg, e); + throw new ConfigXMLReaderException(msg, e); + } + } + + private String findYangModuleName(final QName qname, final SchemaContext schemaContext) + throws ConfigXMLReaderException { + for (Module m : schemaContext.getModules()) { + if (qname.getModule().equals(m.getQNameModule())) { + return m.getName(); + } + } + throw new ConfigXMLReaderException( + String.format("%s: Could not find yang module for QName %s", logName, qname)); + } + +} 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 index e1b4c2964d..15e696225a 100644 --- 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 @@ -8,23 +8,17 @@ 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.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; 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.services.ExtendedBlueprintContainer; +import org.opendaylight.controller.blueprint.ext.DataStoreAppConfigDefaultXMLReader.ConfigURLProvider; 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; @@ -37,29 +31,16 @@ import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException; import org.opendaylight.controller.sal.core.api.model.SchemaService; import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeSerializer; import org.opendaylight.yangtools.concepts.ListenerRegistration; -import org.opendaylight.yangtools.util.xml.UntrustedXML; 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.service.blueprint.container.ComponentDefinitionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.w3c.dom.Document; import org.w3c.dom.Element; -import org.xml.sax.SAXException; /** * Factory metadata corresponding to the "clustered-app-config" element that obtains an application's @@ -127,26 +108,7 @@ public class DataStoreAppConfigMetadata extends AbstractDependentComponentFactor logName(), 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", - logName(), appConfigBindingClassName, LIST_KEY_VALUE)); - } - - try { - bindingContext = ListBindingContext.newInstance(appConfigBindingClass, appConfigListKeyValue); - } catch (InstantiationException | IllegalAccessException | IllegalArgumentException - | InvocationTargetException | NoSuchMethodException | SecurityException e) { - throw new ComponentDefinitionException(String.format( - "%s: Error initializing for app config list binding class %s", - logName(), appConfigBindingClassName), e); - } - - } else { - bindingContext = new ContainerBindingContext(appConfigBindingClass); - } + bindingContext = BindingContext.create(logName(), appConfigBindingClass, appConfigListKeyValue); } @Override @@ -171,9 +133,7 @@ public class DataStoreAppConfigMetadata extends AbstractDependentComponentFactor private void retrieveDataBrokerService() { LOG.debug("{}: In retrieveDataBrokerService", logName()); - // Get the binding DataBroker OSGi service. - retrieveService("data-broker", DataBroker.class, service -> retrieveInitialAppConfig((DataBroker)service)); } @@ -196,7 +156,6 @@ public class DataStoreAppConfigMetadata extends AbstractDependentComponentFactor } private void readInitialAppConfig(final DataBroker dataBroker) { - final ReadOnlyTransaction readOnlyTx = dataBroker.newReadOnlyTransaction(); CheckedFuture, ReadFailedException> future = readOnlyTx.read( LogicalDatastoreType.CONFIGURATION, bindingContext.appConfigPath); @@ -279,113 +238,44 @@ public class DataStoreAppConfigMetadata extends AbstractDependentComponentFactor } private DataObject createDefaultInstance() { - YangInstanceIdentifier yangPath = bindingSerializer.toYangInstanceIdentifier(bindingContext.appConfigPath); - - LOG.debug("{}: Creating app config instance from path {}, Qname: {}", logName(), yangPath, - bindingContext.bindingQName); - - SchemaService schemaService = getOSGiService(SchemaService.class); - if (schemaService == null) { - setFailureMessage(String.format("%s: Could not obtain the SchemaService OSGi service", logName())); - return null; - } - - SchemaContext schemaContext = schemaService.getGlobalContext(); - - Module module = schemaContext.findModuleByNamespaceAndRevision(bindingContext.bindingQName.getNamespace(), - bindingContext.bindingQName.getRevision()); - if (module == null) { - setFailureMessage(String.format("%s: Could not obtain the module schema for namespace %s, revision %s", - logName(), bindingContext.bindingQName.getNamespace(), bindingContext.bindingQName.getRevision())); - return null; - } - - DataSchemaNode dataSchema = module.getDataChildByName(bindingContext.bindingQName); - if (dataSchema == null) { - setFailureMessage(String.format("%s: Could not obtain the schema for %s", logName(), - bindingContext.bindingQName)); - return null; - } - - if (!bindingContext.schemaType.isAssignableFrom(dataSchema.getClass())) { - setFailureMessage(String.format("%s: Expected schema type %s for %s but actual type is %s", logName(), - bindingContext.schemaType, bindingContext.bindingQName, dataSchema.getClass())); - return null; - } - - NormalizedNode dataNode = parsePossibleDefaultAppConfigXMLFile(schemaContext, dataSchema); - if (dataNode == null) { - 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... - setFailureMessage(String.format("%s: Could not create instance for app config binding %s", - logName(), bindingContext.appConfigBindingClass)); - } - - return appConfig; - } + try { + @SuppressWarnings("resource") + ConfigURLProvider inputStreamProvider = appConfigFileName -> { + File appConfigFile = new File(DEFAULT_APP_CONFIG_FILE_PATH, appConfigFileName); + LOG.debug("{}: parsePossibleDefaultAppConfigXMLFile looking for file {}", logName(), + appConfigFile.getAbsolutePath()); + + if (!appConfigFile.exists()) { + return Optional.absent(); + } - private NormalizedNode parsePossibleDefaultAppConfigXMLFile(final SchemaContext schemaContext, - final DataSchemaNode dataSchema) { + LOG.debug("{}: Found file {}", logName(), appConfigFile.getAbsolutePath()); + + return Optional.of(appConfigFile.toURI().toURL()); + }; + + DataStoreAppConfigDefaultXMLReader reader = new DataStoreAppConfigDefaultXMLReader(logName(), + defaultAppConfigFileName, getOSGiService(SchemaService.class), bindingSerializer, bindingContext, + inputStreamProvider); + return reader.createDefaultInstance((schemaContext, dataSchema) -> { + // Fallback if file cannot be read, try XML from Config + NormalizedNode dataNode = parsePossibleDefaultAppConfigElement(schemaContext, dataSchema); + if (dataNode == null) { + // or, as last resort, defaults from the model + return bindingContext.newDefaultNode(dataSchema); + } else { + return dataNode; + } + }); - String appConfigFileName = defaultAppConfigFileName; - if (Strings.isNullOrEmpty(appConfigFileName)) { - String moduleName = findYangModuleName(bindingContext.bindingQName, schemaContext); - if (moduleName == null) { - return null; + } catch (ConfigXMLReaderException e) { + if (e.getCause() == null) { + setFailureMessage(e.getMessage()); + } else { + setFailure(e.getMessage(), e); } - - appConfigFileName = moduleName + "_" + bindingContext.bindingQName.getLocalName() + ".xml"; - } - - File appConfigFile = new File(DEFAULT_APP_CONFIG_FILE_PATH, appConfigFileName); - - LOG.debug("{}: parsePossibleDefaultAppConfigXMLFile looking for file {}", logName(), - appConfigFile.getAbsolutePath()); - - if (!appConfigFile.exists()) { return null; } - - LOG.debug("{}: Found file {}", logName(), appConfigFile.getAbsolutePath()); - - DomToNormalizedNodeParserFactory parserFactory = DomToNormalizedNodeParserFactory.getInstance( - XmlUtils.DEFAULT_XML_CODEC_PROVIDER, schemaContext); - - try (FileInputStream fis = new FileInputStream(appConfigFile)) { - Document root = UntrustedXML.newDocumentBuilder().parse(fis); - NormalizedNode dataNode = bindingContext.parseDataElement(root.getDocumentElement(), dataSchema, - parserFactory); - - LOG.debug("{}: Parsed data node: {}", logName(), dataNode); - - return dataNode; - } catch (SAXException | IOException e) { - String msg = String.format("%s: Could not read/parse app config file %s", logName(), appConfigFile); - LOG.error(msg, e); - setFailureMessage(msg); - } - - return null; - } - - private String findYangModuleName(final QName qname, final SchemaContext schemaContext) { - for (Module m : schemaContext.getModules()) { - if (qname.getModule().equals(m.getQNameModule())) { - return m.getName(); - } - } - - setFailureMessage(String.format("%s: Could not find yang module for QName %s", logName(), qname)); - return null; } @Nullable @@ -411,7 +301,6 @@ public class DataStoreAppConfigMetadata extends AbstractDependentComponentFactor return dataNode; } - @Override public void destroy(final Object instance) { super.destroy(instance); @@ -422,88 +311,4 @@ public class DataStoreAppConfigMetadata extends AbstractDependentComponentFactor } } - /** - * Internal base class to abstract binding type-specific behavior. - */ - private abstract static class BindingContext { - final InstanceIdentifier appConfigPath; - final Class appConfigBindingClass; - final Class schemaType; - final QName bindingQName; - - protected BindingContext(final Class appConfigBindingClass, - final InstanceIdentifier appConfigPath, final 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(final Class appConfigBindingClass) { - super(appConfigBindingClass, InstanceIdentifier.create(appConfigBindingClass), ContainerSchemaNode.class); - } - - @Override - NormalizedNode newDefaultNode(final DataSchemaNode dataSchema) { - return ImmutableNodes.containerNode(bindingQName); - } - - @Override - NormalizedNode parseDataElement(final Element element, final DataSchemaNode dataSchema, - final 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(final Class appConfigBindingClass, - final InstanceIdentifier appConfigPath, final String appConfigListKeyValue) { - super(appConfigBindingClass, appConfigPath, ListSchemaNode.class); - this.appConfigListKeyValue = appConfigListKeyValue; - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - private static ListBindingContext newInstance(final Class bindingClass, final String listKeyValue) - throws InstantiationException, IllegalAccessException, IllegalArgumentException, - InvocationTargetException, NoSuchMethodException, SecurityException { - // 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(final 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(final Element element, final DataSchemaNode dataSchema, - final DomToNormalizedNodeParserFactory parserFactory) { - return parserFactory.getMapEntryNodeParser().parse(Collections.singletonList(element), - (ListSchemaNode)dataSchema); - } - } } diff --git a/opendaylight/blueprint/src/test/java/org/opendaylight/controller/blueprint/tests/DataStoreAppConfigDefaultXMLReaderTest.java b/opendaylight/blueprint/src/test/java/org/opendaylight/controller/blueprint/tests/DataStoreAppConfigDefaultXMLReaderTest.java new file mode 100644 index 0000000000..46bd699668 --- /dev/null +++ b/opendaylight/blueprint/src/test/java/org/opendaylight/controller/blueprint/tests/DataStoreAppConfigDefaultXMLReaderTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2017 Red Hat, 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.tests; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.opendaylight.controller.blueprint.ext.DataStoreAppConfigDefaultXMLReader; +import org.opendaylight.controller.md.sal.binding.test.AbstractConcurrentDataBrokerTest; +import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.test.store.rev140422.Lists; +import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.test.store.rev140422.lists.unordered.container.UnorderedList; + +/** + * Example unit test using the {@link DataStoreAppConfigDefaultXMLReader}. + * + * @author Michael Vorburger.ch + */ +public class DataStoreAppConfigDefaultXMLReaderTest extends AbstractConcurrentDataBrokerTest { + + @Test + public void testConfigXML() throws Exception { + Lists lists = new DataStoreAppConfigDefaultXMLReader<>( + getClass(), "/opendaylight-sal-test-store-config.xml", + getDataBrokerTestCustomizer().getSchemaService(), + getDataBrokerTestCustomizer().getBindingToNormalized(), + Lists.class).createDefaultInstance(); + + UnorderedList element = lists.getUnorderedContainer().getUnorderedList().get(0); + assertThat(element.getName()).isEqualTo("someName"); + assertThat(element.getValue()).isEqualTo("someValue"); + } + + @Test(expected = IllegalArgumentException.class) + public void testBadXMLName() throws Exception { + new DataStoreAppConfigDefaultXMLReader<>( + getClass(), "/badname.xml", + getDataBrokerTestCustomizer().getSchemaService(), + getDataBrokerTestCustomizer().getBindingToNormalized(), + Lists.class).createDefaultInstance(); + } +} diff --git a/opendaylight/blueprint/src/test/resources/opendaylight-sal-test-store-config.xml b/opendaylight/blueprint/src/test/resources/opendaylight-sal-test-store-config.xml new file mode 100644 index 0000000000..b2744b2fea --- /dev/null +++ b/opendaylight/blueprint/src/test/resources/opendaylight-sal-test-store-config.xml @@ -0,0 +1,8 @@ + + + + someName + someValue + + + diff --git a/opendaylight/md-sal/sal-binding-broker/src/test/java/org/opendaylight/controller/md/sal/binding/test/AbstractBaseDataBrokerTest.java b/opendaylight/md-sal/sal-binding-broker/src/test/java/org/opendaylight/controller/md/sal/binding/test/AbstractBaseDataBrokerTest.java index 257ed9031c..cbbfc12edb 100644 --- a/opendaylight/md-sal/sal-binding-broker/src/test/java/org/opendaylight/controller/md/sal/binding/test/AbstractBaseDataBrokerTest.java +++ b/opendaylight/md-sal/sal-binding-broker/src/test/java/org/opendaylight/controller/md/sal/binding/test/AbstractBaseDataBrokerTest.java @@ -25,6 +25,13 @@ public abstract class AbstractBaseDataBrokerTest extends AbstractSchemaAwareTest protected abstract AbstractDataBrokerTestCustomizer createDataBrokerTestCustomizer(); + public AbstractDataBrokerTestCustomizer getDataBrokerTestCustomizer() { + if (testCustomizer == null) { + throw new IllegalStateException("testCustomizer not yet set by call to createDataBrokerTestCustomizer()"); + } + return testCustomizer; + } + @Override protected void setupWithSchema(final SchemaContext context) { testCustomizer = createDataBrokerTestCustomizer();