Bug 8303: BP odl:clustered-app-config initial/*-config.xml testability 27/56027/4
authorMichael Vorburger <vorburger@redhat.com>
Tue, 25 Apr 2017 22:32:15 +0000 (00:32 +0200)
committerRobert Varga <nite@hq.sk>
Sun, 30 Apr 2017 14:39:39 +0000 (14:39 +0000)
DataStoreAppConfigDefaultXMLReaderTest illustrates usage.

Change-Id: I342fca4583c90802238e63262871e33b4b713438
Signed-off-by: Michael Vorburger <vorburger@redhat.com>
opendaylight/blueprint/pom.xml
opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/BindingContext.java [new file with mode: 0644]
opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/ConfigXMLReaderException.java [new file with mode: 0644]
opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/DataStoreAppConfigDefaultXMLReader.java [new file with mode: 0644]
opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/DataStoreAppConfigMetadata.java
opendaylight/blueprint/src/test/java/org/opendaylight/controller/blueprint/tests/DataStoreAppConfigDefaultXMLReaderTest.java [new file with mode: 0644]
opendaylight/blueprint/src/test/resources/opendaylight-sal-test-store-config.xml [new file with mode: 0644]
opendaylight/md-sal/sal-binding-broker/src/test/java/org/opendaylight/controller/md/sal/binding/test/AbstractBaseDataBrokerTest.java

index 2a264731a233ff803309de3cbc8df3da75112754..b0e0f5eaa6245df058a7fd5e49bded78bb79ea20 100644 (file)
       <artifactId>junit</artifactId>
       <scope>test</scope>
     </dependency>
       <artifactId>junit</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>com.google.truth</groupId>
+      <artifactId>truth</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.controller</groupId>
+      <artifactId>sal-test-model</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.controller</groupId>
+      <artifactId>sal-binding-broker-impl</artifactId>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.controller</groupId>
+      <artifactId>sal-binding-broker-impl</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
   </dependencies>
 
   <build>
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 (file)
index 0000000..9189930
--- /dev/null
@@ -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<? extends DataObject> 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<DataObject> appConfigPath;
+    public final Class<DataObject> appConfigBindingClass;
+    public final Class<? extends DataSchemaNode> schemaType;
+    public final QName bindingQName;
+
+    private BindingContext(final Class<DataObject> appConfigBindingClass,
+            final InstanceIdentifier<DataObject> appConfigPath, final Class<? extends DataSchemaNode> 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<? extends DataObject> appConfigBindingClass) {
+            super((Class<DataObject>) appConfigBindingClass,
+                    InstanceIdentifier.create((Class<DataObject>) 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<? extends DataObject> appConfigBindingClass,
+                final InstanceIdentifier<? extends DataObject> appConfigPath, final String appConfigListKeyValue) {
+            super((Class<DataObject>) appConfigBindingClass, (InstanceIdentifier<DataObject>) appConfigPath,
+                    ListSchemaNode.class);
+            this.appConfigListKeyValue = appConfigListKeyValue;
+        }
+
+        @SuppressWarnings({ "rawtypes", "unchecked" })
+        private static ListBindingContext newInstance(final Class<? extends DataObject> 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<QName> 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 (file)
index 0000000..91da124
--- /dev/null
@@ -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 (file)
index 0000000..57f3abc
--- /dev/null
@@ -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<T extends DataObject> {
+
+    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<URL> 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<T> 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<URL> 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));
+    }
+
+}
index e1b4c2964d489ef8432f73dc963ec5053f749e5f..15e696225af3fb4a5c963ec40192fe375736a412 100644 (file)
@@ -8,23 +8,17 @@
 package org.opendaylight.controller.blueprint.ext;
 
 import com.google.common.base.Optional;
 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 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.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 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;
 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.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.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.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.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.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.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.w3c.dom.Element;
-import org.xml.sax.SAXException;
 
 /**
  * Factory metadata corresponding to the "clustered-app-config" element that obtains an application's
 
 /**
  * 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);
         }
 
                     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
     }
 
     @Override
@@ -171,9 +133,7 @@ public class DataStoreAppConfigMetadata extends AbstractDependentComponentFactor
 
     private void retrieveDataBrokerService() {
         LOG.debug("{}: In retrieveDataBrokerService", logName());
 
     private void retrieveDataBrokerService() {
         LOG.debug("{}: In retrieveDataBrokerService", logName());
-
         // Get the binding DataBroker OSGi service.
         // Get the binding DataBroker OSGi service.
-
         retrieveService("data-broker", DataBroker.class, service -> retrieveInitialAppConfig((DataBroker)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) {
     }
 
     private void readInitialAppConfig(final DataBroker dataBroker) {
-
         final ReadOnlyTransaction readOnlyTx = dataBroker.newReadOnlyTransaction();
         CheckedFuture<Optional<DataObject>, ReadFailedException> future = readOnlyTx.read(
                 LogicalDatastoreType.CONFIGURATION, bindingContext.appConfigPath);
         final ReadOnlyTransaction readOnlyTx = dataBroker.newReadOnlyTransaction();
         CheckedFuture<Optional<DataObject>, ReadFailedException> future = readOnlyTx.read(
                 LogicalDatastoreType.CONFIGURATION, bindingContext.appConfigPath);
@@ -279,113 +238,44 @@ public class DataStoreAppConfigMetadata extends AbstractDependentComponentFactor
     }
 
     private DataObject createDefaultInstance() {
     }
 
     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;
         }
             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
     }
 
     @Nullable
@@ -411,7 +301,6 @@ public class DataStoreAppConfigMetadata extends AbstractDependentComponentFactor
         return dataNode;
     }
 
         return dataNode;
     }
 
-
     @Override
     public void destroy(final Object instance) {
         super.destroy(instance);
     @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<DataObject> appConfigPath;
-        final Class<DataObject> appConfigBindingClass;
-        final Class<? extends DataSchemaNode> schemaType;
-        final QName bindingQName;
-
-        protected BindingContext(final Class<DataObject> appConfigBindingClass,
-                final InstanceIdentifier<DataObject> appConfigPath, final Class<? extends DataSchemaNode> 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<DataObject> 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<DataObject> appConfigBindingClass,
-                final InstanceIdentifier<DataObject> appConfigPath, final String appConfigListKeyValue) {
-            super(appConfigBindingClass, appConfigPath, ListSchemaNode.class);
-            this.appConfigListKeyValue = appConfigListKeyValue;
-        }
-
-        @SuppressWarnings({ "rawtypes", "unchecked" })
-        private static ListBindingContext newInstance(final Class<DataObject> 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<QName> 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 (file)
index 0000000..46bd699
--- /dev/null
@@ -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 (file)
index 0000000..b2744b2
--- /dev/null
@@ -0,0 +1,8 @@
+<lists xmlns="urn:opendaylight:params:xml:ns:yang:controller:md:sal:test:store">
+  <unordered-container>
+    <unordered-list>
+      <name>someName</name>
+      <value>someValue</value>
+    </unordered-list>
+  </unordered-container>
+</lists>
index 257ed9031c440d3e0047d587e5724621a04d3024..cbbfc12edb45dfb5c36f636287c0960fa59a5e55 100644 (file)
@@ -25,6 +25,13 @@ public abstract class AbstractBaseDataBrokerTest extends AbstractSchemaAwareTest
 
     protected abstract AbstractDataBrokerTestCustomizer createDataBrokerTestCustomizer();
 
 
     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();
     @Override
     protected void setupWithSchema(final SchemaContext context) {
         testCustomizer = createDataBrokerTestCustomizer();